@llblab/pi-telegram 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +12 -7
- package/BACKLOG.md +1 -23
- package/CHANGELOG.md +53 -1
- package/README.md +3 -1
- package/docs/README.md +1 -1
- package/docs/activity.md +8 -0
- package/docs/architecture.md +17 -15
- package/docs/locks.md +22 -12
- package/docs/multi-instance-bus.md +17 -16
- package/docs/outbound.md +16 -0
- package/docs/public-api.md +9 -4
- package/index.ts +74 -59
- package/lib/activity.ts +100 -3
- package/lib/bindings.ts +81 -6
- package/lib/bus-follower.ts +205 -17
- package/lib/bus-leader.ts +339 -133
- package/lib/bus.ts +82 -19
- package/lib/commands.ts +28 -4
- package/lib/config.ts +40 -28
- package/lib/locks.ts +318 -34
- package/lib/logs.ts +7 -3
- package/lib/media.ts +102 -1
- package/lib/menu-settings.ts +4 -1
- package/lib/outbound-attachments.ts +150 -1
- package/lib/outbound.ts +106 -1
- package/lib/polling.ts +5 -0
- package/lib/queue.ts +41 -48
- package/lib/routing.ts +88 -1
- package/lib/sync.ts +40 -3
- package/lib/telegram-api.ts +77 -13
- package/lib/text-groups.ts +183 -16
- package/lib/thread-reconciler.ts +66 -22
- package/lib/threads.ts +131 -23
- package/lib/turns.ts +102 -13
- package/lib/updates.ts +2 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -61,8 +61,9 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
61
61
|
## 5.1 Flat Domain DAG Shape
|
|
62
62
|
|
|
63
63
|
- The project follows a `Flat Domain DAG`: cohesive bridge domains live as flat `/lib/*.ts` modules, and local imports must form a directed acyclic graph
|
|
64
|
-
- `index.ts` stays the single extension entrypoint and composition root for live Pi/Telegram ports, SDK adapters, and session
|
|
64
|
+
- `index.ts` stays the single extension entrypoint and composition root for live Pi/Telegram ports, SDK adapters, and session-state ports
|
|
65
65
|
- `index.ts` should compose high-level domain runtimes and ports, not own per-domain policies, loops, capability decisions, lifecycle recovery rules, or constants. When composition needs non-trivial branching over Threaded Mode, bus leader/follower state, sync slices, reconciliation, or session replacement, extract a named runtime/factory into the owning `/lib` domain and leave only port wiring in the entrypoint.
|
|
66
|
+
- `bindings` owns Pi-facing registration and cohesive cross-domain assembly such as assistant-output observation → authority → sender coordination. It may connect established domain ports and project failures, but policy, mutable sequencing, routing authority, rendering, and transport remain with their owning domains.
|
|
66
67
|
- Reusable runtime logic should be split into flat domain files under `/lib`
|
|
67
68
|
- Mutable counters, generations, target identity, sync state, provisioning activity, retry defaults, and lifecycle ordering are domain policy even when only the entrypoint currently consumes them. Put them behind a cohesive owner runtime with narrow getters/actions; do not leave them as composition-root closure state.
|
|
68
69
|
- Compression stops when `index.ts` contains high-level runtime construction and direct one-to-one live-port wiring. Do not create a one-use wrapper merely to reduce line count; extract only when the owner gains policy, state, sequencing, normalization, projection, or reusable adaptation.
|
|
@@ -76,17 +77,19 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
76
77
|
- The bridge is session-local, paired to one allowed Telegram user, and owns a local queue aligned with Pi lifecycle hooks
|
|
77
78
|
- The Telegram lock owns external Telegram transport, not local Pi execution. Local extension state and queue runtime are per Pi instance: losing exact `/telegram-connect` ownership must not clear accepted queue state or stop local Pi dispatch, but preview, final delivery, attachments, menus, and other Bot API mutations fail closed once another runtime owns transport. A replacement owner must never receive stale direct mutations merely to preserve old-runtime Telegram output. Bus leader lock entries carry a stable `leaderEpoch` minted on acquisition and preserved across heartbeat refreshes; leader-owned Thread Reconciler cleanup/provisioning actions stamp that epoch and skip destructive side effects if current leadership differs.
|
|
78
79
|
- The Telegram lock is always the transport ownership lock. Every shared `locks.json` mutation must serialize the complete cross-process read/check/write transaction, preserve unrelated registry keys, and fail closed on transaction contention; atomic payload replacement alone does not make ownership linearizable. Refresh, release, takeover, and irreversible leader work must additionally fence against the exact acquired owner and leader epoch. In classic/private-chat mode, transport ownership is the whole bridge ownership and `/telegram-connect` from another live Pi should offer the classic takeover confirmation. In Threaded Mode, transport ownership is the bus leader role and `/telegram-connect` from another live Pi should register as follower instead of offering takeover while the leader is live. If follower registration is not applicable because Threaded Mode is disabled or unavailable, fall back to the classic takeover path.
|
|
80
|
+
- Shared filesystem transactions publish a fully initialized private guard directory containing one exact generation-specific `owner.<generation>.json` through same-parent atomic rename. The owner filename and payload generation must match so stale observation cannot rename replacement metadata. Release verifies the owner and atomically renames the stable guard away before cleanup. Directory recovery first claims owner metadata inside the still-occupied guard so a crashed recoverer can itself be replaced without exposing the stable path; transient guard moves and rollback both retry, while inactive process-global claim generations remain recoverable after failed restoration. If secondary recovery cleanup fails after publishing a replacement main guard, release that exact replacement before propagating the failure. Legacy regular-file recovery remains separately serialized through recoverable directory guards. New guards must not depend on hard links, direct file rename-overwrite, incomplete create-then-write state, or age-only recovery. Legacy regular-file guards may be read only for bounded upgrade recovery; malformed or unsupported guards fail closed.
|
|
79
81
|
- Telegram profiles are parallel bot/runtime realities, not merely alternate credentials. Shared bridge settings remain global, but any lock, IPC endpoint, state snapshot, diagnostics log, thread/bus binding, runtime cache, ownership map, or operator-visible status surface that could mix bot identities must be deliberately classified as global, session-local, or profile-scoped. Preserve default-profile legacy paths/keys for compatibility, and give named profiles isolated observable surfaces unless there is an explicit top-level shared-config reason not to.
|
|
80
82
|
- Telegram private-chat Threaded Mode is the public switch for the local multi-instance bus. There is no separate public `telegram.json` bus switch: classic/private-chat singleton DM behavior is the base product mode, and when Telegram reports Threaded Mode available for the bot the bridge enables leader/follower bus behavior. If Telegram reports Threaded Mode unavailable or thread operations prove it unavailable, the bridge returns to classic singleton behavior as a first-class mode. During a live Threaded Mode downgrade, the current transport leader keeps the singleton classic polling role and followers disconnect instead of taking over or offering takeover while active thread bindings prove this is a downgrade from a live bus organism. Do not inject slot/thread rename guidance in classic mode. Blocked instances should not silently register as followers unless the effective runtime mode is Threaded Mode.
|
|
81
|
-
- In Threaded Mode, exactly one live leader owns `getUpdates` and direct Telegram Bot API calls for one bot token. `/telegram-connect` means join the local Telegram organism: if no live leader exists the instance becomes leader; if a live leader exists the terminal-visible instance registers as follower; ordinary connect must not offer manual takeover while the leader heartbeat is live. Leadership is an ephemeral transport role, not permanent ownership by the first terminal-visible Pi instance. Followers must be started by the operator as real Pi processes, register over the local bus, heartbeat, receive only their routed inbound updates, and send outbound replies/previews/files/chat-actions through the leader transport; Telegram must not spawn hidden Pi processes, launch follower subprocesses, or expose a slash command that creates a new local Pi instance. If a leader reloads while followers stay alive, followers must treat rejected/unknown/unauthorized heartbeats as a registration-loss signal and optimistically re-register with the currently active leader before considering promotion; after a short grace window without successful re-registration, a live follower may promote only through an atomic expected-owner transition after the exact observed leader lease becomes stale or inactive. An unreachable IPC endpoint does not authorize replacing a still-live lock owner; remain disconnected/retrying rather than creating split-brain polling. Terminal status must not remain `follower` while the leader registry can no longer route that follower's thread. Heartbeat pruning is liveness bookkeeping, not permission to delete, mark offline, or send a Telegram-visible disconnected notice for a follower's thread; preserve active follower tabs/bindings across transient reload/reconnect incidents and reserve destructive teardown/offline transitions for explicit disconnect or confirmed reconciliation cases. If the leader dies, a live follower should eventually be able to take over through stale-leader election/recovery so the local multi-instance organism keeps running. Explicit `/telegram-disconnect` is a manual thread teardown: a leader deletes its own bound thread before releasing transport ownership, while a follower
|
|
82
|
-
- Runtime routing binds to a live `instanceId
|
|
83
|
-
- Follower registration readiness should stay on the smallest critical path that makes the follower routable: create or reuse the Telegram target, persist the active binding, mark target-bindings fresh, and return the target to the follower. Manual follower identity must survive a Pi process reload in the same terminal so reload is not treated as a new follower/thread; do not key durable follower ownership only by the short-lived Pi process id. Telegram-visible connected notices, duplicate/replaced thread cleanup, and other reconciliation work are important but non-critical; run them after registration and record slow/failing background steps as runtime events rather than delaying follower usability.
|
|
84
|
-
- Instance slots are extension-owned ring-order metadata. Allocate fresh slots from the `bot.lastSlot` cursor as `A` → `B` → ... → `Z` → `A`, allowing the cursor to move to lower letters after wraparound. Only current live/recovering bindings plus unexpired pending provisions and explicit reservations occupy slots; historical records must not block allocation indefinitely. Preserve a slot on ordinary current binding/thread reuse without advancing or rewinding the fresh-allocation cursor. Follower bindings
|
|
83
|
+
- In Threaded Mode, exactly one live leader owns `getUpdates` and direct Telegram Bot API calls for one bot token. `/telegram-connect` means join the local Telegram organism: if no live leader exists the instance becomes leader; if a live leader exists the terminal-visible instance registers as follower; ordinary connect must not offer manual takeover while the leader heartbeat is live. Leadership is an ephemeral transport role, not permanent ownership by the first terminal-visible Pi instance. Heartbeat acknowledgements carry the authenticated live follower-slot roster: on leader loss, the lowest observed live slot attempts the atomic lock first, while higher slots defer for one bounded grace and then re-check so a missing lower follower cannot block recovery indefinitely. Followers must be started by the operator as real Pi processes, register over the local bus, heartbeat, receive only their routed inbound updates, and send outbound replies/previews/files/chat-actions through the leader transport; Telegram must not spawn hidden Pi processes, launch follower subprocesses, or expose a slash command that creates a new local Pi instance. If a leader reloads while followers stay alive, followers must treat rejected/unknown/unauthorized heartbeats as a registration-loss signal and optimistically re-register with the currently active leader before considering promotion; after a short grace window without successful re-registration, a live follower may promote only through an atomic expected-owner transition after the exact observed leader lease becomes stale or inactive. An unreachable IPC endpoint does not authorize replacing a still-live lock owner; remain disconnected/retrying rather than creating split-brain polling. Terminal status must not remain `follower` while the leader registry can no longer route that follower's thread. Heartbeat pruning is liveness bookkeeping, not permission to delete, mark offline, or send a Telegram-visible disconnected notice for a follower's thread; preserve active follower tabs/bindings across transient reload/reconnect incidents and reserve destructive teardown/offline transitions for explicit disconnect or confirmed reconciliation cases. If the leader dies, a live follower should eventually be able to take over through stale-leader election/recovery so the local multi-instance organism keeps running. Explicit `/telegram-disconnect` is a manual thread teardown: a leader deletes its own bound thread before releasing transport ownership, while a follower sends an authenticated exact-registration-generation disconnect envelope and waits for the leader to delete its assigned thread, mark the durable binding offline, and remove live routing authority before unregistering. Cleanup must be confirmed by successful deletion or explicit already-gone evidence; incomplete cleanup preserves binding/routing state, keeps the session open, and returns actionable retry guidance. A promoted leader authorizes inherited follower-thread cleanup by its current leader epoch rather than the historical owner label. Status is reactive to the effective runtime role and bot capability: classic/non-thread mode shows ordinary `telegram connected`, active bus leadership shows `telegram leader`, and registered followers show `telegram follower`; stable multi-instance truth belongs in the bus role (`leader`/`follower`), while lifecycle is reserved for transient recovery state such as `electing` and must not duplicate stable roles with labels like `leader-active`. When leadership or Threaded Mode availability changes at runtime, the status line and `/telegram-status` must converge promptly instead of preserving a stale role label.
|
|
84
|
+
- Runtime routing binds to a live `instanceId` plus its exact follower registration generation; current thread identity should use explicit owner records (`leader`, `manual-follower`, `pending-topic`) rather than hiding ownership direction in legacy string keys. Persisted manual-follower records may act as stable restart hints, but only an authenticated live registry entry authorizes inbound forwarding or outbound transport; stale session records must not be treated as live owners. A live instance owns at most one active bound thread; provisioning a new target for the same `instanceId` must remove older current-state bindings and close duplicate Telegram threads when possible. A reload creates a new runtime `instanceId`; if the previous leader slot is still known/alive, keep it occupied in reservations and assign the new leader the next free slot instead of creating a duplicate same-letter tab or blocking startup on Telegram UI convergence.
|
|
85
|
+
- Follower registration readiness should stay on the smallest critical path that makes the follower routable: create or reuse the Telegram target, persist the active binding, mark target-bindings fresh, and return the target to the follower. Registration requires a present exact generation. Cross-session target reuse and any carried target absent from current bindings require one synchronous visibility probe: explicit stale evidence may provision a replacement, while ambiguous/non-stale failure must reject registration without replay, binding invalidation, or speculative replacement. Persist an ambiguously observed absent target only as non-routable `probe-required` restoration evidence so targetless retries and leader reloads must probe the exact target again before activation or replacement. Registration and explicit disconnect mutations for one durable follower profile must serialize across awaited cleanup so an old runtime's disconnect cannot delete or unregister a replacement generation. Manual follower identity must survive a Pi process reload in the same terminal so reload is not treated as a new follower/thread; do not key durable follower ownership only by the short-lived Pi process id. Telegram-visible connected notices, duplicate/replaced thread cleanup, and other reconciliation work are important but non-critical; run them after registration and record slow/failing background steps as runtime events rather than delaying follower usability.
|
|
86
|
+
- Instance slots are extension-owned ring-order metadata. Allocate fresh slots from the `bot.lastSlot` cursor as `A` → `B` → ... → `Z` → `A`, allowing the cursor to move to lower letters after wraparound. Only current live/recovering bindings plus unexpired pending provisions and explicit reservations occupy slots; historical records must not block allocation indefinitely. Preserve a slot on ordinary current binding/thread reuse without advancing or rewinding the fresh-allocation cursor. Follower bindings survive process absence as stable restoration hints until explicit stale/deleted/offline evidence invalidates them. Successful target reuse must refresh the binding timestamp and replacement runtime identity; generic leader startup or heartbeat pruning must not erase the target merely because no follower registration is currently live. An authenticated live follower whose carried exact target is missing from current persisted bindings should recover that target instead of creating another Telegram thread; preserve a carried slot only when it does not collide with another current record. Preserve a cursor that belongs to a live or recoverable binding and continue skipping retained restart hints plus unexpired pending/reserved slots. Explicit stale/deleted/offline reconciliation may release that slot; generic process absence may not. The alphabet cap is a feature — compact single-letter tabs are scannable.
|
|
85
87
|
- Instance thread names are provisioner-authored identity metadata, not model-authored output. Fresh Threaded Mode threads select one of five curated compact 4-6 letter Latin names for the assigned slot letter using provisioning timestamp entropy, then create the Telegram thread with that thread name immediately. The title is the thread name itself, not `{slot} {threadName}` or `{slot} — {threadName}`. Do not register or reintroduce an agent-facing thread rename tool; removing the extra rename prompt/tool turn is an intentional simplicity constraint. Thread names must mirror into terminal status, live diagnostics, and `[telegram|thread:name]` through one target-aware current-instance identity resolver. Registered follower/leader metadata takes precedence over a stale shared record for the same target, with the record used only as fallback; do not hide a valid baked/custom thread name behind old slot-prefix validation. A follower that later becomes leader keeps its existing name; leadership changes are transport role changes, not identity resets.
|
|
86
88
|
- Telegram private-chat Threaded Mode treats `All` as an aggregate/control surface, not a durable `General` thread and not a process launcher. Leader activation creates/reuses a leader thread in the private bot DM (chat = allowedUserId) proactively. If all threads are closed and the owner writes from `All`, a first new unbound thread should be reclaimed by the leader when the leader lacks an active bound thread. Later unknown threads require explicit routing or restore; creating another Pi instance is a manual terminal action followed by `/telegram-connect`.
|
|
87
89
|
- Unbound thread detection: when a message arrives from the owner in an unknown `message_thread_id` while Threaded Mode is active, the default runtime first reclaims it for the leader if the leader has no active bound thread and routes the prompt locally. Later unknown threads are preserved by default and receive an in-thread reroute chooser that can send the captured prompt to a live thread or restore a stale leader/follower binding through explicit restore actions; destructive cleanup happens only after explicit user confirmation and through `thread-reconciler`. If the operator wants another Pi instance, they start Pi in a terminal and run `/telegram-connect`; Telegram-originated hidden auto-spawn and `/thread`-style process launch flows are intentionally absent. If Threaded Mode is unavailable, the message is processed normally through classic routing.
|
|
88
|
-
- Telegram extension work should not hold Pi's core agent lifecycle hostage once Pi has produced the semantic result. For Telegram-originated turns, final Telegram delivery, attachment upload, and transport cleanup are extension-owned side effects; schedule them off the critical `agent_end` path when ordering can still be preserved, record failures as runtime events, and keep dispatch of the next Telegram turn behind the delivery task when needed to avoid interleaving previews/finals. Public activity handlers run through isolated asynchronous per-handler queues; lifecycle hooks enqueue normalized events and never await consumer work.
|
|
90
|
+
- Telegram extension work should not hold Pi's core agent lifecycle hostage once Pi has produced the semantic result. For Telegram-originated turns, final Telegram delivery, attachment upload, and transport cleanup are extension-owned side effects; schedule them off the critical `agent_end` path when ordering can still be preserved, record failures as runtime events, and keep dispatch of the next Telegram turn behind the delivery task when needed to avoid interleaving previews/finals. Public activity handlers run through isolated asynchronous per-handler queues; lifecycle hooks enqueue normalized events and never await consumer work. Proactive push defaults enabled and requires explicit `assistant.proactivePush: false` to opt out. While enabled, core delivery projects each completed Pi-visible assistant text block from local/autonomous work once and in order; bind admitted blocks to the exact target, profile/token transport stamp, direct leader epoch or follower registration generation, and session generation. Token deltas, hidden reasoning, tool traffic, stale authority, and Telegram-owned turns never enter that projection.
|
|
89
91
|
- Telegram runtime state should be treated as event-driven reconciliation of extension assumptions against observable Telegram signals, not as a full Telegram bot read-model and not as a reason to query Telegram on every action. Keep a local assumption model for bot identity/capabilities, pairing assumptions, thread support, known thread lifecycle, bound instance targets, reservations, and transport health. Invalidate and reconcile the relevant slice on meaningful events: startup/reload, lifecycle service messages, stale-send/API errors, setup/pairing changes, follower register/prune, explicit status/debug requests, and bounded low-frequency leader health ticks. `lib/sync.ts` owns sync slices, observation intake, invalidation triggers, status/debug freshness, and reconciliation scheduling; it must not promise complete bot-state mirroring because Bot API lacks a complete topic listing surface. `lib/thread-reconciler.ts` owns pure Threaded Mode lifecycle planning and should become the only policy authority for destructive thread cleanup decisions. `threads` owns current Telegram UI thread/tab binding primitives and thread-name helpers mapped to Bot API topic transport, `status` renders projections, and `index.ts` only wires ports. `tmp/telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging and resets on extension start / runtime scope changes; it is diagnostics only, not routing/provisioning authority. `state.json` should be an observable/debug snapshot aligned with `/telegram-status` (extension runtime, bot capabilities including `bot.lastSlot`, runtime role, live roster projection, reservations, diagnostics), not routing/provisioning authority. Because every process on one Telegram profile shares this file, only the active transport lock owner may persist it; followers read current state and gain write authority only after promotion. Status-only writes must refresh disk-backed bindings before serialization so a stale loaded snapshot cannot erase newer leader state. Live bus/runtime state is authoritative; file reservations and fresh capability observations may be startup hints/collision guards only; stale snapshots must re-probe before suppressing bus/topic behavior. Do not persist stale/offline/failed thread target history as source of truth.
|
|
92
|
+
- Cross-session follower binding reuse must surface the target with one compact connected probe before registration succeeds. Explicit stale-topic rejection may replace the target with the next monotonic slot; ambiguous/non-stale send failure must not replay, replace, or claim proof that the tab is stale.
|
|
90
93
|
- Thread bindings are bidirectional. From the Pi-instance side, an instance knows and preserves its target, slot, thread name, lifecycle state, and direct-delivery defaults. From the Telegram-client/bot side, the bridge observes thread creation/close/reopen/stale-send signals and reconciles them into instance binding state. Lifecycle transitions that affect operator understanding should be Telegram-visible when a live transport remains: every successful follower registration/re-registration gets a compact connected notice in the assigned thread, while heartbeat pruning stays silent because it is only liveness suspicion. These notices should use the instance thread name when known and fall back to the slot letter only while the thread is still unnamed. New unknown threads (owner writing in `All`) are preserved until the user explicitly chooses a reroute/restore/new-instance action that makes cleanup eligible; any destructive cleanup must go through `thread-reconciler`. Unknown `forum_topic_created` service events are observations, not destructive cleanup proof.
|
|
91
94
|
- The multi-instance bus uses private bot DMs with Telegram private-chat Threaded Mode enabled for the bot. No supergroup, group, or forum configuration is needed; the thread chat is always the private chat with the paired owner. Threaded capability checks must use bot/private-chat evidence such as `getMe.has_topics_enabled`, incoming `message_thread_id`, and topic operation success/failure; do not use group chat metadata as the control-plane truth for Telegram private-chat Threaded Mode.
|
|
92
95
|
- Target-scoped behavior must preserve `{ chatId, threadId? }` through inbound messages/edits/callbacks/reactions, thread lifecycle service messages, media and split-text grouping, queue mutations, active-turn cleanup, previews, reply deduplication, menus, sections, generated buttons, voice, attachments, and direct delivery. Threaded Mode replies must keep Telegram's reply affordance to the triggering message just like classic DM mode. In Telegram private-chat Threaded Mode, mobile Telegram has been live-verified to render `sendMessage` replies that include top-level `message_thread_id` plus same-chat `reply_parameters.message_id`; Telegram Desktop may fail to show the visual reply header for the same correct payload, so do not treat Desktop-only absence as bridge failure without mobile/payload evidence.
|
|
@@ -101,7 +104,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
101
104
|
- UI emoji semantics live in `docs/ui-style.md` as the project-level emoji registry. Before adding a new UI emoji, reuse an existing registered meaning or update the registry in the same change. Use `🧵` consistently as the canonical Telegram/Pi thread marker in thread chooser headings, thread lifecycle copy, and target-routing status text; do not decorate every concrete thread-target button with it when the button label is already the thread name or slot. Operator-facing Threaded Mode routing copy should use the `Pi` glyph for Pi instance/thread/session concepts; reserve `Pi` spelling for product prose, package/API names, and surrounding docs where the wordmark is clearer.
|
|
102
105
|
- Telegram prompt context metadata uses `[tag|key:value|key:value]` without spaces around `:`. `[telegram...]` identifies the current inbound surface only: owner/current turns use `[telegram]` or `[telegram|thread:<name>]`, while guest-mode turns use `[telegram|guest:<group-title-or-peer-username-or-id>]`. In private guest replies authored by the owner, `guest:<...>` must name the replied guest peer, not the owner. Alternate/source authors belong on source-specific blocks such as `[reply|from:<username-or-id>]`, `[forward|from:<username-or-id>]`, and `[attachments|from:<username-or-id>]`; plain `[attachments]` remains current-turn/current-owner attachments and should render immediately after the `[telegram...]` current text before reply/forward/source attachment context.
|
|
103
106
|
- Telegram `reply_to_message` context is prompt-only and must not affect slash-command parsing; when Telegram includes `rich_message` blocks on a quoted rich reply, extract plain text from those blocks before falling back to raw `text`/`caption` so prompt context does not leak raw Rich Markdown source
|
|
104
|
-
- Long-lived timers, pollers, watchers, and deferred queue dispatch must be session-bound and avoid stale live Pi contexts after session replacement. Pi session replacement/reload changes agent context, so Threaded Mode followers must stop the old receiver/heartbeat and never retain a stale context or socket.
|
|
107
|
+
- Long-lived timers, pollers, watchers, and deferred queue dispatch must be session-bound and avoid stale live Pi contexts after session replacement. Pi session replacement/reload changes agent context, so Threaded Mode followers must stop the old receiver/heartbeat and never retain a stale context or socket. Retain a short-lived same-process handoff containing the assigned target identity immediately after follower promotion succeeds and refresh it before suspension; followers re-register through the live leader with that target, while leaders—including promoted followers—restore and persist the exact target, slot, and thread name under acquired leader authority before topic provisioning. During election and leader handoff, status identity may use the current instance's inactive manual-follower record as a display/restoration hint, but never as live routing authority. Bind leader handoffs to the exact Telegram profile owner key so session replacement cannot transfer a target across profiles. Preserve the existing Telegram thread during either handoff, show `Telegram disconnected` only while restoration is genuinely pending or failed, and require explicit `/telegram-connect` only when no fresh same-process handoff can be restored.
|
|
105
108
|
- Do not add Telegram commands that imitate Pi interactive session replacement, navigation, or TUI rendering through private internals, ANSI terminal clearing, raw TTY injection, or a shadow `pi` subprocess. Features such as a real Telegram `/new` require a public Pi API that runs the same session-replacement path as the terminal command.
|
|
106
109
|
- In-flight `/model` switching is limited to Telegram-owned active turns; if a tool call is active, abort is delayed until the tool finishes
|
|
107
110
|
|
|
@@ -113,6 +116,8 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
113
116
|
- Real code blocks must stay literal and escaped
|
|
114
117
|
- `telegram_attach` is the canonical outbound file-delivery path for Telegram-originated requests; outside active Telegram turns it may send immediately to the paired/default chat, a registered follower's assigned target, or explicit `chat_id` plus optional `thread_id` for explicit local/TUI delivery requests only when this Pi instance owns `/telegram-connect` or is registered with the Threaded Mode multi-instance bus. `telegram_message` is the first-class direct Telegram Markdown text tool for local/TUI prompts and follows the same ownership-or-bus-registration gate; neither direct tool replaces normal active-turn replies. It reuses top-level `telegram_button` comments for inline buttons; buttons must be attached to a text message, never sent as standalone actions
|
|
115
118
|
- Telegram prompt guidance is layered and context-frugal: unconfigured sessions receive no bridge suffix, configured sessions receive compact direct-delivery/help guidance, and Telegram-originated turns receive only the minimal per-turn contract. Detailed voice/button/direct-delivery/Threaded Mode/formatting/debug guidance belongs in the agent-facing `telegram_help` tool instead of being repeated in every prompt.
|
|
119
|
+
- Telegram inbound prompt topology keeps current user material contiguous: `[outputs]` produced from current attachments follows `[attachments]` immediately, then independent `[reply]` or forwarded-source context follows the complete current-user block. Forwarded Rich Message media belongs under source-attributed `[attachments|from:<identity>]` immediately after its `[forward|from:<identity>]` block and remains available as model image content. Do not place source context between an attachment and its transcription or handler output.
|
|
120
|
+
- Forward-comment grouping uses a bounded one-second candidate window for ordinary short human text so an adjacent same-sender/chat/thread forward can join across polling responses; a matching forward flushes immediately. Commands, bots, captions, media groups, target/sender mismatches, reversed ids, and distant message ids bypass grouping. Preserve the grouped candidate across session replacement and follower forwarding without weakening exact routing-generation authority.
|
|
116
121
|
- `telegram_voice` and `telegram_button` are not Pi tools; keep prompts/docs explicit that agents should author markup while voice synthesis provider extensions own TTS/OGG conversion, and pi-telegram owns button routing plus Telegram delivery
|
|
117
122
|
- Voice reply policy and prompt context are owned by pi-telegram's `telegram.json` `voice.replyMode`: missing/invalid config behaves as `manual` but does not add a `[voice]` prompt-context block; only an explicit valid `voice.replyMode` renders context. Render a single voice field as `[voice] reply mode: manual|mirror|always`, and render multiple fields as a `[voice]` list; place voice context after `[outputs]` when handler output exists, otherwise after `[attachments]`; provider prompt contributions are optional provider-specific additions, not the default policy channel
|
|
118
123
|
- Optional `telegram.json` `time` may add `[time] YYYY-MM-DD HH:mm:ss <timezone>` to Telegram-originated prompts for wall-clock context. It is hidden by default, uses `time.injectionMode` values `hidden|always|interval`, stores `time.interval` in milliseconds, uses the system timezone, and should render last after `[attachments]`, `[outputs]`, and `[voice]` sections. The Settings row `🕒 Time injection: hidden|always|interval` controls `time.injectionMode` only.
|
package/BACKLOG.md
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
# Project Backlog
|
|
2
2
|
|
|
3
|
-
_This backlog tracks only open release-relevant work: live runtime verification, evidence-gated Telegram client follow-ups, and upstream Pi API blockers. Completed outcomes and validation evidence belong in `CHANGELOG.md`, not in this queue._
|
|
4
|
-
|
|
5
|
-
## P1 — Promoted Follower Reload Evidence
|
|
6
|
-
|
|
7
|
-
Context: deterministic coverage protects promoted follower thread preservation, and the latest live Linux smoke closed reload routing, follower Active, and reroute/restore regressions. The exact promoted-leader reload path is deliberately outside the 0.20.1 profile IPC hotfix because it is unrelated to profile transport isolation; keep it as an evidence-gated follow-up rather than blocking that release.
|
|
8
|
-
|
|
9
|
-
Open work:
|
|
10
|
-
|
|
11
|
-
- [ ] Capture live evidence that leader → follower promotes → `/reload` preserves the promoted leader's Telegram thread identity.
|
|
12
|
-
|
|
13
|
-
Done when: promoted-follower reload identity has direct live Telegram evidence.
|
|
3
|
+
_This backlog tracks only open release-relevant work: hotfixes, live runtime verification, evidence-gated Telegram client follow-ups, and upstream Pi API blockers. Completed outcomes and validation evidence belong in `CHANGELOG.md`, not in this queue._
|
|
14
4
|
|
|
15
5
|
## P1 — Native Windows Threaded Mode Follow-Ups
|
|
16
6
|
|
|
@@ -36,18 +26,6 @@ Open work:
|
|
|
36
26
|
|
|
37
27
|
Done when: new client quirks are either fixed with targeted coverage or documented as evidence-backed exceptions, without keeping broad manual smoke matrices in the backlog.
|
|
38
28
|
|
|
39
|
-
## P1 — Evidence-Backed Rich Markdown Normalization
|
|
40
|
-
|
|
41
|
-
Context: Native Rich Markdown is the default assistant delivery path. Existing regressions cover known parser/client edges such as space-after-marker blockquotes, dollar-prefixed ticker atoms, list indentation, code fences, links, display math normalization, and long-message splitting. Further rewrites should be evidence-driven, not speculative.
|
|
42
|
-
|
|
43
|
-
Open work:
|
|
44
|
-
|
|
45
|
-
- [ ] Capture any new Telegram parser-breaking sequence from live/client evidence or a minimized fixture.
|
|
46
|
-
- [ ] Add a conservative normalization or safe-degradation rule only for confirmed sequences.
|
|
47
|
-
- [ ] Keep unconfirmed speculative rewrites out of the delivery path.
|
|
48
|
-
|
|
49
|
-
Done when: newly observed Rich Markdown failures have minimized fixtures and targeted regressions, while stable rendering behavior remains unchanged for unsupported guesses.
|
|
50
|
-
|
|
51
29
|
## Blocked — Same-Thread Telegram `/new`
|
|
52
30
|
|
|
53
31
|
Blocked: upstream Pi core API. Issue: https://github.com/earendil-works/pi/issues/5952
|
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.23.0: Telegram Bot API 10.2 Rich Output And Proactive Projection
|
|
4
|
+
|
|
5
|
+
- `Follower Tab Visibility`: Added one synchronous connected probe when a follower reuses a target across session identity or carries an exact target absent from current bindings. A successful probe surfaces or recovers the existing Telegram tab; explicit stale-topic rejection marks the old target deleted and provisions a monotonic replacement before registration succeeds, while other failures—including ambiguous non-idempotent acknowledgement—preserve non-routable probe-required restoration evidence and reject registration without replay or speculative replacement; targetless retries and leader reloads must probe that exact target again. Impact: a follower cannot silently report an invisible or post-disconnect deleted target as restored, and stale replacements advance rather than resetting the slot sequence.
|
|
6
|
+
- `Explicit Thread Disconnect`: Added an authenticated follower disconnect envelope, required present exact generations for registration and disconnect, serialized both mutations per durable follower profile across awaited cleanup, and added a thread-named destructive confirmation in Threaded Mode. Cleanup now reports incomplete actions: only successful deletion or explicit already-gone evidence permits offline persistence, routing-authority removal, and acknowledgement; otherwise leader, promoted leader, or follower state remains available with actionable retry guidance. Impact: `/telegram-disconnect` removes the current Telegram tab consistently without letting an old runtime delete or unregister a replacing follower or falsely report success while the tab remains visible.
|
|
7
|
+
- `Three-Instance Topology Smoke`: Repeated the live three-instance connect, visibility, failover, and promoted-leader reload scenario. Every follower surfaced a usable tab and the resulting topology remained healthy; observed promotion order still depended occasionally on runtime timing, which the operator accepted because exact atomic ownership and bounded failover remained intact. Impact: follower visibility and multi-instance recovery have direct operator evidence without misrepresenting the lowest-slot grace as a strict consensus election.
|
|
8
|
+
- `Lowest-Slot Succession`: Added the authenticated live follower-slot roster to heartbeat acknowledgements. During failover, a follower whose slot has a lower live predecessor defers for one bounded election grace and then rechecks the atomic lock; the lowest observed slot proceeds immediately, while a dead lower candidate can delay but never indefinitely block a higher survivor. Impact: clean multi-follower leader loss usually favors `C` before `D` without replacing exact lock fencing with timing-only ownership or introducing a consensus phase.
|
|
9
|
+
- `Promoted Leader Reload Live Smoke`: Confirmed that a follower promoted to leader records `follower-promoted-session-handoff`, reload restores the exact prior target/slot/name, and leader startup reports `reused: true` without creating a replacement tab. Impact: the original promoted-leader identity-loss blocker is closed with direct runtime evidence; the same three-instance smoke separately exposed follower-tab visibility and deterministic successor-order follow-ups.
|
|
10
|
+
- `Promoted Leader Reload Handoff`: Retained the promoted target, slot, and thread name immediately after successful election and refreshed that short-lived process-local handoff before leader session suspension, binding both paths to the exact Telegram profile owner key. Restoration persists the binding under acquired leader authority before topic provisioning. The fallback identity source includes the current instance's inherited manual-follower record even after registration stops, because repeated live diagnostics proved leader-only/default identity projection remained empty and no shutdown handoff event was emitted. The same restoration projection now keeps the inherited thread name visible as `Cinder electing`-style status instead of regressing to generic `telegram electing`. Impact: promotion and election status retain the existing instance identity, while promoted leader reload reclaims the same Telegram thread instead of resetting to slot `A` and allocating a new tab, as confirmed by the promoted-leader reload smoke.
|
|
11
|
+
- `Leader/Follower Election Cycle Smoke`: Confirmed live with two instances that leader disconnect promotes the registered follower, reconnecting the previous leader registers it as follower, disconnecting the promoted leader promotes the remaining follower back, and reconnecting the second instance restores it as follower. Impact: repeated bidirectional election, promotion, disconnect, and re-registration converge without manual takeover or a stranded topology.
|
|
12
|
+
- `Composition Root Compression`: Reduced `index.ts` from 1,119 to 1,083 lines by moving assistant-output admission/authority/sender coordination into the existing Pi-binding orchestration domain, moving config-persist sync sequencing and stale-topic API recovery adapters into `sync`, and replacing local forwarding callbacks with direct live-port wiring where signatures already matched. Impact: the entrypoint retains visible runtime composition while low-level policy, failure projection, mutable sequencing, and reusable adapters stay with their owning domains.
|
|
13
|
+
- `Forward Comment Live Smoke`: After reload, confirmed a short comment and a Rich-image forward arriving through the active Telegram thread produce one Pi turn ordered as comment → `[forward|from:…]` → `[attachments|from:…]`, with the downloaded image present as model input. Impact: cross-poll grouping and forwarded Rich media attribution now have direct live evidence in addition to deterministic coverage.
|
|
14
|
+
- `Cross-Batch Forward Comments`: Added a bounded one-second candidate window for ordinary short human text and immediate flush when the adjacent same-sender/chat/thread forwarded message arrives, including Rich Message text and follower-routed updates. Impact: a comment and its forward form one Pi turn across separate long-poll responses instead of relying on incidental Telegram batching.
|
|
15
|
+
- `Forwarded Rich Attachments`: Extracted photo, video, animation, audio, and voice-note files from nested inbound `rich_message` blocks and rendered them under source-attributed attachment context immediately after the corresponding forward block. Impact: forwarded Rich images remain visible to the model as image content without being mislabeled as current-user attachments.
|
|
16
|
+
- `Follower Rich Attachment Smoke`: Confirmed from an assigned follower Telegram topic that one queued PNG plus final text arrives as one reply-anchored Rich Message, with no separate attachment notice, second upload, or duplicate final. Impact: Bot API 10.2 one-result media delivery now has live client evidence across both direct-leader and follower transports.
|
|
17
|
+
- `Follower Bidirectional Routing`: Propagated the exact live follower registration generation from target ownership into every leader-forwarded message envelope and recorded rejected forwarding acknowledgements. Impact: follower threads can receive Telegram prompts again without weakening replacement-generation fencing, and a future route rejection leaves actionable diagnostics instead of disappearing silently.
|
|
18
|
+
- `Follower Thread Restoration`: Preserved stable manual-follower owner/target bindings across process absence and changed replacement registration to reclaim the existing current thread rather than creating another same-named tab. Persisted bindings remain restart hints only; authenticated live registration remains mandatory routing authority, while explicit stale/deleted/offline evidence still invalidates reuse. Impact: restarting the same follower restores its recognizable thread and slot without allowing absent processes to receive work.
|
|
19
|
+
- `Inbound Prompt Topology Smoke`: Confirmed in a live voice reply to an image message that the current voice attachment and its transcription appear before the independent `[reply]` block and replied-message attachment context. Impact: live prompt evidence matches the deterministic current-user-first topology without separating an attachment from its derived output.
|
|
20
|
+
- `Inbound Prompt Topology`: Kept attachment-derived `[outputs]`, including voice transcription, immediately after the current user `[attachments]` block and moved independent reply/source context after the complete current-user material. Impact: attachment meaning remains topologically connected to the user input that produced it instead of being split by quoted-message context.
|
|
21
|
+
- `Portable Lock Standard`: Synchronized `docs/locks.md` bit-for-bit with the reusable cross-project standard and removed Telegram-specific runtime, bus, profile, and release-history policy from that portable artifact. Impact: the standard can be copied into another singleton-extension project as a complete implementation contract, while project-specific behavior remains owned by architecture documentation.
|
|
22
|
+
- `Proactive Default`: Enabled `assistant.proactivePush` by default when the nested setting is absent while preserving nested explicit `false` as the canonical opt-out. Updated settings copy and operator documentation for completed public block projection. Impact: connected local/autonomous work reaches Telegram without an extra setup toggle, while operators retain a direct disable switch.
|
|
23
|
+
- `Assistant Config`: Moved the proactive local-result delivery switch under `assistant.proactivePush` beside assistant rendering and draft-preview policy, without retaining the removed top-level key as a compatibility alias. Impact: configuration and Settings UI use one coherent assistant namespace; operators who want projection disabled set `assistant.proactivePush` to `false`.
|
|
24
|
+
- `Config Transactions`: Valid Telegram configuration snapshots now load directly from the atomically published file, while malformed-file recovery and every merge/write remain serialized by the cross-process transaction guard. Impact: ordinary `telegram.json` reads no longer create transaction-directory churn, without weakening concurrent persistence or destructive recovery safety.
|
|
25
|
+
- `Proactive Projection Documentation`: Updated the README, Activity, Outbound, Public API, architecture ownership map, and durable project contract to define `assistant.proactivePush` as ordered projection of every completed public local/autonomous text block rather than a final-only notification. Impact: commentary/final visibility, hidden reasoning/tool exclusions, Rich/HTML behavior, exact authority fences, follower routing, and no-replay semantics now have one consistent operator and maintainer contract.
|
|
26
|
+
- `Release Preflight`: Ran the complete 0.23.0 validation suite after follower visibility, bounded succession preference, explicit cross-role thread disconnect, cross-batch grouping, and forwarded Rich media fixes: 1,292 tests passed with one platform-only skip, typecheck passed, npm audit reported zero vulnerabilities, package dry-run produced `@llblab/pi-telegram@0.23.0`, and `git diff --check` stayed clean. Impact: deterministic, packaging, dependency, and repository hygiene gates are green, and the final independent release review reported SHIP with no blockers.
|
|
27
|
+
- `Follower Rich Smoke Classification`: Confirmed the first follower live attempt originated from the follower TUI rather than its Telegram topic. With no active Telegram turn, `telegram_attach` correctly used explicit direct attachment delivery and proactive projection sent the final block separately; this is expected compatibility behavior and is not counted as integrated Rich one-result evidence. Impact: release evidence now distinguishes the direct local tool path from the Telegram-turn queue orchestration that still requires a corrected follower smoke.
|
|
28
|
+
- `Rich Replacement Fence`: Added exact agent-end barriers for an eligible single-artifact Rich final across session replacement: an old generation cannot begin the scheduled multipart mutation after replacement, and ownership loss while an already-started send settles prevents stale preview cleanup, fallback text/upload, queue dispatch, or other continuation effects. Impact: replacement sessions cannot inherit delayed Rich attachment work or trigger duplicate compatibility delivery after an in-flight result.
|
|
29
|
+
- `Proactive Settings Copy`: Expanded the boolean detail card to describe both values consistently with other settings, placing `on` (default) first as the projection behavior and `off` second as the explicit opt-out that keeps local/autonomous public blocks in Pi without affecting normal Telegram-originated replies. Impact: operators can understand the behavioral difference before changing the toggle.
|
|
30
|
+
- `Proactive Multi-Block Live Smoke`: After a full reload, an autonomous iteration emitted one public commentary checkpoint, ran an internal tool check, and emitted one final block. The operator confirmed both visible blocks arrived separately and in order in the assigned Telegram thread, while tool traffic produced no Telegram text and no duplicate appeared. Impact: normalized commentary-plus-final projection now has live client evidence in addition to deterministic direct/follower, privacy, ordering, and generation-fence coverage.
|
|
31
|
+
- `Assistant Output Ownership`: Extended the existing Activity assistant-output path with proactive local/autonomous projection instead of introducing a separate proactive domain. Activity coordinates ordered admission, routing owns exact authority, outbound composes transformation and reply delivery, and `index.ts` retains direct live-port composition. Impact: intermediate proactive push remains a small extension of normalized assistant output rather than an artificial parallel state machine.
|
|
32
|
+
- `Proactive Final-Mutation Fence`: Added an exact admission-authority guard at each proactive `sendMessage`, `sendRichMessage`, and edit transport mutation after asynchronous outbound text/button transformation and immediately before bus-aware direct/follower route selection. Added direct-epoch and follower-registration replacement barriers proving no mutation enters replacement transport. Impact: an outbound handler cannot create a stale-authority timing window between proactive queue admission checks and non-idempotent Bot API execution.
|
|
33
|
+
- `Proactive Admission Authority`: Captured the authorized target, profile/token transport stamp, and exact direct leader epoch or follower registration generation with each admitted public block, then revalidated that immutable authority immediately before sending through the configured Rich/HTML reply runtime. Impact: delayed proactive work cannot drift across target changes, profile replacement, ownership loss/reacquisition, follower re-registration, or session replacement; known transport ambiguity retains the existing no-replay behavior.
|
|
34
|
+
- `Proactive Follower Ordering`: Composed the proactive queue with the bus-aware API runtime under follower ownership and held the first routed Rich send behind a barrier. The second block did not enter follower IPC until the first completed, and both retained source order and target fields. Impact: proactive checkpoints cannot reorder when a follower routes them through the leader.
|
|
35
|
+
- `Proactive Replacement Fence`: Added an extension-level barrier where an old session starts one proactive send, queues its final behind that send, shuts down, and starts a replacement session before the first acknowledgement returns. Only the already-started mutation executes; the queued old-generation final is dropped. Impact: session replacement cannot deliver delayed proactive blocks through the replacement runtime.
|
|
36
|
+
- `Proactive Multi-Block Integration`: Added an extension-level local-run fixture with a completed checkpoint, tool boundary, hidden reasoning event, final block, and `agent_end`. It proves configured Rich delivery emits checkpoint then final exactly once and in order, excludes reasoning text, and does not revive the removed final-only path. Impact: the Grow-Loop-shaped output sequence now has deterministic end-to-end coverage without any runtime dependency on Grow Loop.
|
|
37
|
+
- `Proactive Projection Wiring`: Connected normalized Activity `assistant-segment` events to Activity's assistant-output projection, rendered each admitted local/autonomous block through the configured Rich/HTML assistant sender and authorized instance target, started/stopped projection with the session generation, and removed Queue's old final-only `agent_end` sender. Impact: intermediate and final public blocks now share one ordered non-blocking path with no final duplication, while Telegram-owned turns and authority failures remain excluded.
|
|
38
|
+
- `Assistant Output Projection`: Extended Activity's normalized output runtime to admit completed local/autonomous public assistant segments, deduplicate event identity, preserve source order, revalidate policy and delivery authority before each send, isolate failures, and drop queued work after generation stop. Impact: lifecycle wiring projects intermediate and final public blocks without a new domain, blocking Pi, or importing Grow Loop behavior; Telegram/unknown sources, empty text, disabled policy, and stale queued work remain silent.
|
|
39
|
+
- `Proactive Output Boundary`: Confirmed Pi's `message_update.assistantMessageEvent` provides `text_end` as the completed public text boundary, distinct `thinking_*` and `toolcall_*` event families, and optional commentary/final phase signatures. Added an autonomous multi-block regression proving the existing Activity normalizer flushes completed text as intermediate/final assistant segments while reasoning and tool boundaries never enter segment text. Impact: proactive projection can consume normalized public blocks instead of token deltas or provider-private content without depending on Grow Loop.
|
|
40
|
+
- `Integrated Rich Attachment Smoke`: After a full Pi reload, delivered one ordinary `telegram_attach` PNG plus final Markdown through the configured Rich assistant path in an assigned private topic. The operator confirmed one reply-anchored composite message with no duplicate attachment notice or second upload. Impact: the implemented direct-leader orchestration has live evidence beyond raw Bot API probing.
|
|
41
|
+
- `Rich Thinking Safety`: Restricted the internal outgoing `InputRichMessage` contract to Markdown or HTML plus typed media, excluding explicit block arrays, and added an invariant that core reply, preview, queue, and attachment paths never construct Telegram Thinking blocks. Impact: draft-only Thinking placeholders cannot become an accidental transport for hidden reasoning; any future Activity use must start from explicitly public text.
|
|
42
|
+
- `Rich Output Documentation`: Documented the single-artifact Rich result, exact ownership, reply/thread targeting, HTML/voice/Guest/multi-file compatibility boundaries, known-failure fallback, ambiguity no-replay rule, and remaining live follower evidence across outbound, architecture, multi-instance bus, public API, and docs navigation references. Impact: documented behavior now matches only probe-confirmed and deterministically tested implementation rather than broader Bot API possibility.
|
|
43
|
+
- `Rich Attachment Delivery`: Integrated exactly one probe-confirmed PNG/JPEG photo, MP4 video, or MP3 audio artifact with final assistant Markdown into one configured-Rich multipart result. Successful sends record exact message ownership; known-safe rejection returns to the existing text-plus-attachment path, while `commit-unknown` and malformed successful results stop without fallback or replay. HTML mode, multiple/unsupported artifacts, Guest Mode, voice-only output, explicit voice markup, and OGG/Opus remain unchanged. Impact: requested supported media can arrive as one reply-anchored final result without weakening rendering choice, voice policy, generation checks, or ambiguity safety.
|
|
44
|
+
- `Rich Attachment Planning`: Added an outbound-domain planner for exactly one probe-confirmed PNG/JPEG photo, MP4 video, or MP3 audio artifact in configured Rich mode. It constructs one target-scoped, reply-anchored multipart Rich Message with normalized Markdown and optional inline keyboard, while HTML mode, multiple artifacts, empty text, and OGG/Opus voice remain on existing paths. Impact: assistant integration can consume one deterministic request plan without changing voice policy or compatibility behavior.
|
|
45
|
+
- `Follower Rich Upload Authorization`: Allowed target-scoped multipart `sendRichMessage` calls through the follower API boundary and added exact own-thread acceptance plus cross-thread rejection coverage. Impact: a future probe-confirmed Rich media upload can use the same direct/follower runtime without widening follower authority beyond its registered target.
|
|
46
|
+
- `Rich Media Live Probe`: Confirmed Bot API and client behavior in an assigned private topic for one composite Rich Message containing HTTPS photo/video/audio, one multipart `attach://` voice note, and one cached-`file_id` voice note. The operator confirmed the composite media renders as one result and the independently requested voice notes remain separate messages; all three probe messages were then deleted successfully. Impact: URL, multipart, cached-file, media-classification, topic-targeting, and composite one-result behavior now have live evidence for bounded assistant-path integration.
|
|
47
|
+
- `Structured Rich Markdown`: Added one Bot API 10.2 fixture that preserves a native table, inline and display mathematics, a preformatted code block, details, unordered and ordered lists, and quotation content through normalization and final `sendRichMessage` request construction. Impact: the established Markdown renderer already covers the targeted structured blocks without a parallel explicit-block renderer.
|
|
48
|
+
- `Rich Media Request Probe`: Added deterministic direct and follower transport probes for Rich Message HTTPS photo references, cached voice-note `file_id`, and single-file `attach://` multipart request construction. Impact: JSON and multipart payload shapes now cross existing transport boundaries unchanged; Bot API acceptance and Telegram client rendering remain explicitly live-gated before automatic assistant integration.
|
|
49
|
+
- `Rich Media Transport Types`: Added narrow Bot API 10.2 types for `InputRichMessage.media`, `InputRichMessageMedia`, photo/video/animation/audio media, and `InputMediaVoiceNote`, including native Rich Message body pass-through across direct and follower bus transports. Impact: URL or cached-file Rich media requests can retain their complete typed shape without exposing raw Bot API credentials or changing current rendering behavior; multipart upload remains evidence-gated open work.
|
|
50
|
+
- `Telegram Bot API Reference`: Synchronized the vendored Telegram bot skill reference with Bot API 10.2 while preserving the established changelog, section, object-table, and method structure. Added complete Rich Message input blocks/media, voice-note media, Ephemeral Messages, Communities, subscription updates, and refreshed task-oriented line/anchor indexes. Impact: implementation and review can resolve current 10.2 fields and methods locally instead of relying on stale model knowledge.
|
|
51
|
+
|
|
52
|
+
## 0.22.1: Termux-Compatible Filesystem Transactions
|
|
53
|
+
|
|
54
|
+
- `Filesystem Transactions`: Replaced hard-link guard publication with private staged directories containing complete exact-owner metadata, atomically published each non-empty guard by same-parent rename, and atomically renamed exact-owned guards away before recursive cleanup. Bound each owner generation into a unique filename so delayed stale observations cannot claim replacement metadata, retained exact stale recovery through recoverable internal claimant markers, retried reclaim and rollback moves without peer-visible stalls, released newly recovered ownership when secondary cleanup fails, kept fail-closed malformed-state handling, and bounded legacy regular-file guard recovery. Impact: shared Telegram state no longer requires the hard-link operation rejected by Android/Termux, while exactly-one cross-process ownership remains locally enforced; subsequent reporter confirmation established that the released path works on Termux.
|
|
55
|
+
- `Diagnostics`: Contained synchronous and queued JSONL persistence failures inside the diagnostics boundary while preserving later queued records after a failed append or rotation. Impact: an unavailable diagnostics path cannot terminate Pi through an unhandled rejection or permanently poison subsequent runtime evidence.
|
|
4
56
|
|
|
5
57
|
## 0.22.0: Concurrency And Runtime Ownership Hardening
|
|
6
58
|
|
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
It is a **runtime adapter**, not a remote terminal. Start or supervise work in the Pi TUI, then continue from Telegram while away from the keyboard. Each Telegram destination follows a running Pi instance and sends prompts into that instance's currently active session; it is not permanently bound to one session file or session identity. The bridge preserves Pi session semantics instead of pretending Telegram is a PTY, shell, process launcher, or session browser. That boundary is the product: Telegram gets safe runtime handles, not raw terminal power.
|
|
10
10
|
|
|
11
|
+
Proactive push is enabled by default. `assistant.proactivePush` projects every completed public assistant text block from local or autonomous work—including visible checkpoints and the final answer—to the authorized Telegram target once and in order; set it explicitly to `false` to disable projection. It never mirrors local prompts, hidden reasoning, tool traffic, token deltas, Telegram-owned turns, or stale-generation work. See [Outbound](docs/outbound.md#proactive-public-output) and the [configuration reference](docs/public-api.md#configuration-api).
|
|
12
|
+
|
|
11
13
|
This repository is an actively maintained fork of [`badlogic/pi-telegram`](https://github.com/badlogic/pi-telegram). It started from upstream commit [`cb34008`](https://github.com/badlogic/pi-telegram/commit/cb34008460b6c1ca036d92322f69d87f626be0fc) and has since diverged substantially.
|
|
12
14
|
|
|
13
15
|
## Install
|
|
@@ -150,7 +152,7 @@ Run these inside Pi.
|
|
|
150
152
|
| `/telegram-setup <profile>` | Save or update a named-profile bot token |
|
|
151
153
|
| `/telegram-connect` | Activate the default profile and acquire its transport ownership |
|
|
152
154
|
| `/telegram-connect <profile>` | Activate a named profile and acquire its transport ownership |
|
|
153
|
-
| `/telegram-disconnect` | Stop polling and release ownership |
|
|
155
|
+
| `/telegram-disconnect` | Stop polling and release ownership; in Threaded Mode, confirm deletion of this instance's current Telegram thread |
|
|
154
156
|
| `/telegram-status` | Inspect connection, mode, queue, transport, and recent diagnostics |
|
|
155
157
|
|
|
156
158
|
Named profile identifiers contain only lowercase ASCII letters and digits (maximum 32 characters); `default`, `main`, and `active` remain reserved.
|
package/docs/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Living index of project documentation in `/docs`.
|
|
|
14
14
|
- [../.agents/skills/domain-dag/SKILL.md](../.agents/skills/domain-dag/SKILL.md) — Project-local Domain DAG architecture skill and validator guidance
|
|
15
15
|
- [command-templates.md](./command-templates.md) — Portable command-template standard core
|
|
16
16
|
- [inbound.md](./inbound.md) — Local `pi-telegram` inbound text/media handler bus, programmatic inbound handlers, registered STT provider fallbacks, legacy `attachmentHandlers` compatibility, placeholders, and fallbacks
|
|
17
|
-
- [outbound.md](./outbound.md) — Local `pi-telegram` outbound-handler config, text/voice/button behavior, voice synthesis provider fallback priority, artifact outputs, and callback routing
|
|
17
|
+
- [outbound.md](./outbound.md) — Local `pi-telegram` outbound-handler config, text/voice/button behavior, single-artifact Rich results, voice synthesis provider fallback priority, artifact outputs, and callback routing
|
|
18
18
|
- [callback-namespaces.md](./callback-namespaces.md) — Shared Telegram `callback_data` namespace standard for layered extensions
|
|
19
19
|
- [updates.md](./updates.md) — Update classification and runtime handler registry that lets layered extensions observe and consume Telegram updates without owning their own polling connection
|
|
20
20
|
- [multi-instance-bus.md](./multi-instance-bus.md) — Optional multi-instance Telegram bus architecture: profile-scoped transport, leader/follower routing, thread targets, instance slots, manual follower registration, and recovery semantics
|
package/docs/activity.md
CHANGED
|
@@ -234,6 +234,14 @@ Pi lifecycle must not wait for extension rendering or Telegram transport.
|
|
|
234
234
|
|
|
235
235
|
The Delivery API independently serializes concrete Telegram operations per target. Activity serialization preserves semantic event order; delivery serialization preserves transport order.
|
|
236
236
|
|
|
237
|
+
### Core proactive projection
|
|
238
|
+
|
|
239
|
+
When `assistant.proactivePush` is enabled, Activity's built-in assistant-output projection uses the same normalized `assistant-segment` boundary exposed to public handlers. It projects every completed public block from `local` or `autonomous` activity, including intermediate commentary/checkpoints and the final block. It never projects text token deltas, reasoning events, tool events or payloads, Telegram-owned activity, unknown-source activity, or empty text.
|
|
240
|
+
|
|
241
|
+
The projection does not delay Activity dispatch or Pi lifecycle. Its ordered admission tail deduplicates normalized event identity, while existing routing and outbound owners revalidate the immutable admission-time target, profile/token transport generation, direct leader epoch or follower registration generation, and session generation immediately before each send. A replacement or stale owner drops queued work rather than rerouting it. An already-started non-idempotent Bot API mutation follows the normal `commit-unknown` no-replay contract.
|
|
242
|
+
|
|
243
|
+
This projection consumes ordinary normalized Pi output. It has no runtime dependency on any workflow, loop, or companion extension that produced the public blocks.
|
|
244
|
+
|
|
237
245
|
## Lifecycle Mapping
|
|
238
246
|
|
|
239
247
|
The bridge maps Pi hooks as follows:
|
package/docs/architecture.md
CHANGED
|
@@ -60,14 +60,14 @@ The repository uses a **Flat Domain DAG**:
|
|
|
60
60
|
|
|
61
61
|
### Domain Ownership Map
|
|
62
62
|
|
|
63
|
-
- `index.ts`: composition root for live ports, session
|
|
63
|
+
- `index.ts`: composition root for live ports, session-state ports, transport adapters, and lifecycle registration. It exposes cross-domain wiring but does not own mutable domain state or reusable adapters.
|
|
64
64
|
- `api`: Bot API helpers, retries, uploads/downloads, temp cleanup, byte limits, chat actions, lazy token clients, and API error recording.
|
|
65
65
|
- `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.
|
|
66
66
|
- `locks` / `polling`: serialized singleton lock storage, exact-owner epoch exposure, process-global reload generations, lock-aware polling lifecycle/takeover/follower registration, and the cohesive classic-vs-Threaded capability state/monitor/observation/polling orchestration. Polling also owns long-poll controller state, offset admission/persistence, and poll-loop wiring.
|
|
67
67
|
- `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 shared protocol, process identity, profile-aware local endpoints, and IPC primitives; `bus-leader` owns leader runtime, leader envelope handling, activation scheduling, and leader polling/server/prune orchestration; `bus-follower` owns process-stable manual-follower keys plus this Pi instance's follower-side registration, heartbeat, one-sequence authenticated client assembly, forwarded-update adaptation/receiving, recovery retry defaults, and routed API caller without any process spawning.
|
|
68
|
-
- `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, observation intake, status/debug freshness, and reconciliation scheduling across bot identity, pairing assumptions, live target bindings, reservations, and transport health after meaningful observable signals. It should call narrower domain primitives rather than letting `index.ts`, `threads`, or `status` accumulate cross-cutting reconciliation policy.
|
|
68
|
+
- `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, stale-topic API recovery adaptation, observation intake, status/debug freshness, and reconciliation scheduling across bot identity, pairing assumptions, live target bindings, reservations, and transport health after meaningful observable signals. It should call narrower domain primitives rather than letting `index.ts`, `threads`, or `status` accumulate cross-cutting reconciliation policy.
|
|
69
69
|
- `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.
|
|
70
|
-
- `threads`: Telegram UI thread/tab binding state mapped to Bot API `message_thread_id` / `ForumTopic` transport. Owns leader and current-instance identity state, status projections, slot allocation from the current extension state, baked compact thread-name selection, current binding persistence, and primitive thread provision helpers. It should not persist stale/offline/failed target history, own destructive cleanup policy, grow into the general Telegram synchronization domain, or expose a rename tool.
|
|
70
|
+
- `threads`: Telegram UI thread/tab binding state mapped to Bot API `message_thread_id` / `ForumTopic` transport. Owns leader and current-instance identity state, profile-bound same-process leader session handoff, status projections, slot allocation from the current extension state, baked compact thread-name selection, current binding persistence, and primitive thread provision helpers. It should not persist stale/offline/failed target history, own destructive cleanup policy, grow into the general Telegram synchronization domain, or expose a rename tool.
|
|
71
71
|
- `updates` / `routing`: update classification, authorization planning, callbacks, edited messages, reactions, target-owner forwarding, inbound bus ownership/live-target/local-label projection, and inbound route composition.
|
|
72
72
|
- `media` / `text-groups` / `time-injection` / `turns` / `inbound`: inbound text/media/file extraction, rich-message reply-context plaintext recovery, media-group debounce, long-text coalescing, optional `[time]` context, handler execution, and prompt-turn assembly/editing.
|
|
73
73
|
- `queue`: queue item contracts, profile/token transport-generation stamping, lane admission/order, readiness gates, mutations, dispatch runtime, prompt/control enqueueing, and session/agent/tool lifecycle sequencing.
|
|
@@ -77,12 +77,12 @@ The repository uses a **Flat Domain DAG**:
|
|
|
77
77
|
- `keyboard`: shared inline-keyboard reply-markup shape only; feature domains own labels, callback data, and behavior.
|
|
78
78
|
- `preview` / `replies` / `rendering`: throttled native Rich Markdown draft delivery, native final reply delivery, reply parameters, transport-limit chunking, and remaining Telegram HTML rendering for bridge-owned UI/compatibility surfaces.
|
|
79
79
|
- `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.
|
|
80
|
-
- `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.
|
|
80
|
+
- `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 proactive push: 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.
|
|
81
81
|
- `outbound-markup`: top-level assistant action comment parsing, attribute parsing, voice reply planning, and preview/delivery stripping.
|
|
82
82
|
- `outbound`: outbound text transformations, voice/button artifact delivery, and generated callback actions.
|
|
83
|
-
- `outbound-attachments`: `telegram_attach`, queued outbound files, stat/limit checks,
|
|
84
|
-
- `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, status snapshot scheduling, and grouped diagnostics. `status` remains a structural leaf; `logs` composes filesystem evidence with status projections.
|
|
85
|
-
- `lifecycle` / `prompts` / `prompt-templates` / `pi`: session-generation fencing and start/shutdown
|
|
83
|
+
- `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.
|
|
84
|
+
- `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.
|
|
85
|
+
- `bindings` / `lifecycle` / `prompts` / `prompt-templates` / `pi`: Pi-facing command/tool/hook registration and cohesive cross-domain binding assembly; session-generation fencing and start/shutdown sequencing across Queue, grouped input, Delivery, polling, capability monitor, watchdog, follower refresh, and assistant-output projection; Telegram prompt guidance; prompt-template discovery/expansion; and centralized direct Pi SDK imports.
|
|
86
86
|
- `command-templates`: shell-free command-template helpers, composition expansion, placeholder substitution, executable resolution, warnings, and retry/timeout semantics.
|
|
87
87
|
|
|
88
88
|
### Guarded Invariants
|
|
@@ -118,15 +118,15 @@ Telegram configuration lives in `~/.pi/agent/telegram.json`. Polling ownership l
|
|
|
118
118
|
### Runtime Ownership
|
|
119
119
|
|
|
120
120
|
- `/telegram-connect` acquires or moves singleton polling ownership before polling starts.
|
|
121
|
-
- `/telegram-disconnect` stops polling and releases ownership. In Threaded Mode it first tears down the disconnecting instance's bound Telegram thread: leaders delete their own thread directly, and followers
|
|
121
|
+
- `/telegram-disconnect` stops polling and releases ownership. In Threaded Mode it first names the current thread in a destructive confirmation, then 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. Unconfirmed cleanup keeps binding/routing state available for an explicit retry.
|
|
122
122
|
- Session start schedules Telegram polling resume asynchronously only when the existing lock already points at the current `pid`/`cwd`, or when a stale same-`cwd` lock can be safely replaced after process restart. Startup and `/resume` should not wait on Telegram leader election, Bot API probes, poller handoff, or thread reconciliation before restoring the Pi session.
|
|
123
123
|
- Pi `print`/`json` run modes stay passive: they do not start or resume Telegram polling even if a lock is present. Older Pi runtimes without `ctx.mode` keep the previous compatibility behavior.
|
|
124
124
|
- Inherited child sessions that see the same `telegram.json` but do not own the `pid`/`cwd` lock must not auto-start polling or call `getUpdates` unless the operator force-takes ownership.
|
|
125
125
|
- Session replacement suspends polling/watchers without releasing ownership so the next session-start hook 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 automatically re-registers the new session context through the live leader without marking or replacing its Telegram thread.
|
|
126
126
|
- Live polling owners require explicit takeover confirmation.
|
|
127
127
|
- Long-lived polling timers use snapshotted ownership context and stop local polling when the lock no longer points at their own process.
|
|
128
|
-
- `locks.json` owns only external Telegram control/polling. Local extension and queue state
|
|
129
|
-
- Proactive local/
|
|
128
|
+
- `locks.json` owns only external Telegram control/polling. Local extension and accepted queue state remain per Pi instance when ownership moves, but previews, final delivery, dispatch transport mutations, and other delayed work stop until exact direct or follower authority becomes valid again; ownership loss never permits delivery through replacement transport.
|
|
129
|
+
- Proactive local/autonomous public-output projection is not accepted-turn delivery. It is allowed only when `assistant.proactivePush` is enabled and this instance has exact direct ownership or an authenticated follower registration. Every completed public block, including visible commentary and the final block, retains its admission-time target and transport authority; hidden reasoning, tools, token deltas, stale work, and Telegram-owned turns are excluded.
|
|
130
130
|
|
|
131
131
|
Deleting `locks.json` resets runtime ownership without deleting Telegram configuration.
|
|
132
132
|
|
|
@@ -138,7 +138,7 @@ Named Telegram profiles are orthogonal to Threaded Mode. The selected profile ch
|
|
|
138
138
|
|
|
139
139
|
Profile reality follows three explicit storage classes. `telegram.json` shared settings and extension registries are process-global platform configuration; profile bot/session fields and 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 named-profile offsets and unrelated global/profile updates do not stale-replace one another. 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 is limited to stale scratch files and cannot redirect live traffic.
|
|
140
140
|
|
|
141
|
-
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. Follower liveness is intentionally fast because heartbeat traffic is local IPC: followers heartbeat every `1s`, the leader treats them as stale after `2s`, and the prune loop runs every `1s` so stopped followers are detected promptly while active forwarded updates/API calls still refresh liveness. Heartbeat pruning is silent liveness bookkeeping: it preserves the follower thread binding 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. Successful follower target reuse refreshes the binding's recovery timestamp
|
|
141
|
+
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. Follower liveness is intentionally fast because heartbeat traffic is local IPC: followers heartbeat every `1s`, the leader treats them as stale after `2s`, and the prune loop runs every `1s` so stopped followers are detected promptly while active forwarded updates/API calls still refresh liveness. Heartbeat pruning is silent liveness bookkeeping: it preserves the follower thread binding 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. 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, 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. 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.
|
|
142
142
|
|
|
143
143
|
Follower binding is manual and process-first: the operator starts another Pi process, then runs `/telegram-connect`; only then does that process register as a follower with an instance-scoped internal binding identity and cause the leader to create/reuse a thread for it. 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.
|
|
144
144
|
|
|
@@ -168,7 +168,7 @@ All inbound updates are gated by the configured authorized user id.
|
|
|
168
168
|
2. Persist update offsets only after successful handling; repeated handler failures are bounded.
|
|
169
169
|
3. Filter to the paired private user; guest-mode updates require an existing paired user and cannot establish first pairing.
|
|
170
170
|
4. Dispatch owned callbacks and controls before fallback prompt forwarding.
|
|
171
|
-
5. Coalesce media groups
|
|
171
|
+
5. Coalesce media groups, likely split long text, and a short human comment followed by an adjacent forwarded message when needed.
|
|
172
172
|
6. Download files into `~/.pi/agent/tmp/telegram` with size limits and partial-download cleanup.
|
|
173
173
|
7. Run configured/programmatic inbound handlers in order, appending successful stdout under `[outputs]`.
|
|
174
174
|
8. Add local attachments under `[attachments]`, optional voice context, and optional final `[time]` context.
|
|
@@ -176,7 +176,7 @@ All inbound updates are gated by the configured authorized user id.
|
|
|
176
176
|
10. Handle `edited_message` updates separately while the original turn is still queued.
|
|
177
177
|
11. Dispatch only when all safety gates are clear.
|
|
178
178
|
|
|
179
|
-
Long-text split recovery
|
|
179
|
+
Long-text split recovery remains conservative: only human text at or above the near-limit threshold opens its debounce window. A separate bounded one-second comment window applies to ordinary short human text so a forwarded message arriving in the next polling response can join the same Pi turn; an adjacent matching forward flushes immediately. Commands, bots, captions, media groups, different senders/targets, reversed ids, and distant message ids do not enter this pairing path.
|
|
180
180
|
|
|
181
181
|
### Queue And Dispatch Safety
|
|
182
182
|
|
|
@@ -207,6 +207,8 @@ Post-agent-end queue dispatch uses a session-bound deferred dispatcher. It is ac
|
|
|
207
207
|
|
|
208
208
|
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.
|
|
209
209
|
|
|
210
|
+
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.
|
|
211
|
+
|
|
210
212
|
### Controls And Menus
|
|
211
213
|
|
|
212
214
|
Telegram controls execute through command/callback domains, not by entering the normal prompt queue unless they intentionally create a prompt turn.
|
|
@@ -249,7 +251,7 @@ Assistant delivery guarantees:
|
|
|
249
251
|
|
|
250
252
|
- 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.
|
|
251
253
|
- 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.
|
|
252
|
-
- 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.
|
|
254
|
+
- 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.
|
|
253
255
|
- 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.
|
|
254
256
|
- 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.
|
|
255
257
|
- When Draft previews are enabled, streaming previews pass structurally closed assistant Markdown prefixes through to `sendRichMessageDraft` with ownership checks, voice suppression, and serialized flushes. 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.
|
|
@@ -300,7 +302,7 @@ Queue reactions are shortcut controls for waiting turns. Promotion reactions (`
|
|
|
300
302
|
|
|
301
303
|
`/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. Expected preview noise such as unchanged edit responses is filtered out.
|
|
302
304
|
|
|
303
|
-
When
|
|
305
|
+
When `assistant.proactivePush` is enabled and this instance has exact direct or follower transport authority, completed public assistant text blocks from local/autonomous work are sent once and in source order to the instance's authorized target. Visible commentary/checkpoints and the final block use the configured Rich or HTML renderer. Hidden reasoning, tool traffic, token deltas, local prompt text, Telegram-owned turns, and stale generations are not mirrored. 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.
|
|
304
306
|
|
|
305
307
|
Telegram prompt guidance is context-aware. Unconfigured sessions receive no bridge suffix. Local/TUI prompts receive only explicit direct-delivery guidance so ordinary terminal replies do not learn raw Telegram action-comment syntax. Telegram-originated turns receive the full inbound context, phone-width output, and native action contract, including the 37-display-cell mobile readability hint.
|
|
306
308
|
|
package/docs/locks.md
CHANGED
|
@@ -58,13 +58,13 @@ During a user-initiated start/connect event, an extension should:
|
|
|
58
58
|
1. Read its lock entry
|
|
59
59
|
2. If `pid` is stale, replace the entry
|
|
60
60
|
3. If `pid` and `cwd` match the current pi instance, refresh or keep the entry
|
|
61
|
-
4. If a live
|
|
61
|
+
4. If a live external owner exists, ask interactively whether to move singleton ownership here
|
|
62
62
|
|
|
63
63
|
## Acquisition timing
|
|
64
64
|
|
|
65
65
|
Lock writes must be caused by an explicit user-initiated runtime event, such as a start/connect command or a confirmed takeover prompt.
|
|
66
66
|
|
|
67
|
-
Extension initialization and session-start hooks may read `locks.json`, update local status, install ownership watchers, and resume local work when the existing lock already points at the current `pid`/`cwd`. After a full process restart, a session-start hook may replace a stale lock from the same `cwd` to restore explicitly requested ownership. They must not create ownership from an inactive lock, take over a live
|
|
67
|
+
Extension initialization and session-start hooks may read `locks.json`, update local status, install ownership watchers, and resume local work when the existing lock already points at the current `pid`/`cwd`. After a full process restart, a session-start hook may replace a stale lock from the same `cwd` to restore explicitly requested ownership. They must not create ownership from an inactive lock, take over a live external owner, or replace a stale lock from another directory by themselves. Such locks should stay visible as state until the user runs the start/connect command. Session replacement should suspend local runtime work and ownership watchers without releasing the lock, so the next session in the same `pid`/`cwd` can resume from explicit ownership.
|
|
68
68
|
|
|
69
69
|
## Optional fields
|
|
70
70
|
|
|
@@ -98,16 +98,16 @@ Singleton extensions with footer/status presence should expose quiet but explici
|
|
|
98
98
|
- `on` when this pi instance owns the runtime but has no pending runtime detail to show
|
|
99
99
|
- `[16:32:39]` when the runtime owns scheduled work and can show the next countdown
|
|
100
100
|
|
|
101
|
-
Extensions may prefix
|
|
101
|
+
Extensions may prefix active states with their own compact name, such as `telegram on` or `wakeup [00:10:00]`. Quiet idle states may be hidden when status-line width is more valuable than an explicit off marker.
|
|
102
102
|
|
|
103
103
|
## Interactive takeover
|
|
104
104
|
|
|
105
105
|
Start/connect commands should make singleton moves easy:
|
|
106
106
|
|
|
107
107
|
1. If no live owner exists, take ownership without an extra prompt
|
|
108
|
-
2. If a live
|
|
108
|
+
2. If a live external owner exists, ask whether to move singleton ownership to this pi instance
|
|
109
109
|
3. On confirmation, write the current `{ "pid": ..., "cwd": ... }` to this extension's key in `locks.json`
|
|
110
|
-
4. The previous owner must notice that `locks.json` no longer points at its own `pid`/`cwd` and stop
|
|
110
|
+
4. The previous owner must notice that `locks.json` no longer points at its own `pid`/`cwd` and stop local runtime work without deleting the new lock
|
|
111
111
|
|
|
112
112
|
Takeover prompts should use the extension name as the dialog title, then the question, a blank line, and source/target lines:
|
|
113
113
|
|
|
@@ -121,7 +121,7 @@ to: /new
|
|
|
121
121
|
|
|
122
122
|
Avoid repeating the extension name in the body. Color is encouraged: extension title/name accent, question warning, `from:`/`to:` muted.
|
|
123
123
|
|
|
124
|
-
The previous owner may use `fs.watch`, mtime polling, or an existing status/timer tick. Long-lived watchers should compare against a snapshotted `pid`/`cwd` identity rather than a live pi context object, because session replacement such as `/new` makes captured contexts stale. The important contract is graceful
|
|
124
|
+
The previous owner may use `fs.watch`, mtime polling, or an existing status/timer tick. Long-lived watchers should compare against a snapshotted `pid`/`cwd` identity rather than a live pi context object, because session replacement such as `/new` makes captured contexts stale. The important contract is graceful local shutdown after ownership mismatch.
|
|
125
125
|
|
|
126
126
|
## Reset
|
|
127
127
|
|
|
@@ -129,17 +129,27 @@ Delete `~/.pi/agent/locks.json` to reset singleton runtime ownership for all par
|
|
|
129
129
|
|
|
130
130
|
## Atomicity
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
`locks.json` is one shared registry, so preserving unrelated keys in memory is not sufficient. Every writer must serialize the complete cross-process read/check/write transaction through the same guard. Otherwise two extensions can read the same snapshot, update different keys, and publish snapshots that erase one another.
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
The canonical guard path is:
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
```text
|
|
137
|
+
~/.pi/agent/locks.json.transaction
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
All participating extensions must follow one compatible protocol:
|
|
137
141
|
|
|
138
|
-
|
|
142
|
+
- Acquire the guard before every ownership acquisition, refresh, release, takeover, or other registry mutation.
|
|
143
|
+
- Publish fully initialized private owner metadata atomically. A portable implementation may stage a non-empty directory containing `owner.<generation>.json`, require filename/payload generation agreement, and rename that directory into the stable guard path.
|
|
144
|
+
- Do not depend on hard links or platform-specific advisory locks; the protocol must work on Linux, macOS, native Windows, and Android/Termux filesystems supported by Pi.
|
|
145
|
+
- Read and validate the latest complete registry only after guard acquisition, change only the owned extension key, and preserve every unrelated key from that guarded snapshot.
|
|
146
|
+
- Publish the JSON payload through a same-directory temporary file and atomic rename. Atomic payload replacement prevents torn JSON but does not replace transaction serialization.
|
|
147
|
+
- Release only the exact acquired owner by atomically renaming the stable guard away before cleanup. Stale recovery must prove the observed owner process is dead and must fence delayed recovery against replacement-owner ABA races.
|
|
148
|
+
- Fail closed on malformed owner metadata, malformed registry state, unverifiable generations, contention timeout, or unsupported atomic filesystem behavior.
|
|
139
149
|
|
|
140
|
-
|
|
150
|
+
Lock-free reads remain appropriate for status display when readers tolerate an old-or-new complete snapshot. Any decision that mutates shared ownership must re-read and validate under the transaction.
|
|
141
151
|
|
|
142
|
-
|
|
152
|
+
Cross-writer safety is compositional: every writer targeting the same registry must participate in the protocol. One compliant writer cannot guarantee lost-update safety against another writer that bypasses the shared transaction.
|
|
143
153
|
|
|
144
154
|
## Migration
|
|
145
155
|
|