@llblab/pi-telegram 0.27.11 โ 0.28.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 +150 -258
- package/BACKLOG.md +1 -169
- package/CHANGELOG.md +396 -441
- package/README.md +9 -6
- package/api/updates.ts +5 -0
- package/docs/architecture.md +71 -26
- package/docs/multi-instance-bus.md +23 -5
- package/docs/public-api.md +7 -6
- package/docs/ui-style.md +6 -6
- package/docs/updates.md +29 -11
- package/index.ts +356 -246
- package/lib/activity-verbosity.ts +26 -0
- package/lib/bindings.ts +240 -2
- package/lib/bus-follower.ts +436 -238
- package/lib/bus-leader.ts +395 -42
- package/lib/bus.ts +994 -153
- package/lib/commands.ts +184 -30
- package/lib/config.ts +23 -2
- package/lib/journal.ts +3140 -0
- package/lib/lifecycle.ts +4 -0
- package/lib/locks.ts +21 -21
- package/lib/media.ts +71 -32
- package/lib/menu-queue.ts +31 -17
- package/lib/menu.ts +5 -3
- package/lib/model.ts +51 -24
- package/lib/ownership.ts +42 -7
- package/lib/paths.ts +35 -0
- package/lib/polling.ts +591 -106
- package/lib/prompts.ts +17 -0
- package/lib/queue.ts +732 -143
- package/lib/routing.ts +291 -64
- package/lib/runtime.ts +26 -10
- package/lib/status.ts +257 -18
- package/lib/sync.ts +131 -5
- package/lib/telegram-api.ts +41 -11
- package/lib/text-groups.ts +75 -35
- package/lib/threads.ts +112 -4
- package/lib/turns.ts +79 -14
- package/lib/updates.ts +3771 -223
- package/package.json +3 -3
- package/scripts/check-downgrade.mjs +435 -0
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ Open the bot DM and send:
|
|
|
65
65
|
/start
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
The first Telegram user to message the bot becomes the allowed owner. Other users are ignored.
|
|
68
|
+
The first Telegram user to message the bot becomes the allowed owner. Other users are ignored. After required pairing state is persisted, `/start` is admitted independently from best-effort menu rendering and BotFather command-list synchronization, so either Telegram side effect can fail or remain in flight without stopping later inbound updates.
|
|
69
69
|
|
|
70
70
|
### 5. Enable optional bot capabilities in BotFather
|
|
71
71
|
|
|
@@ -73,7 +73,7 @@ Enable the optional capabilities the bridge needs in [@BotFather](https://t.me/B
|
|
|
73
73
|
|
|
74
74
|
1. Enable guest mode so the bot can answer mentions and replies in chats where it is not a member.
|
|
75
75
|
2. Enable private-chat Threaded Mode; when it is available, one live instance becomes the profile's leader and later visible Pi instances register as followers. Without it, the bridge stays in classic single-owner DM mode.
|
|
76
|
-
3. Make the bot an administrator in any chat where the queue reaction shortcuts (๐
|
|
76
|
+
3. Make the bot an administrator in any chat where the queue reaction shortcuts (๐ prioritize, ๐ suppress) should work. Reaction updates require admin rights, so the shortcuts silently do nothing in non-admin chats; private chats deliver reactions without admin rights.
|
|
77
77
|
|
|
78
78
|
## What It Feels Like
|
|
79
79
|
|
|
@@ -121,7 +121,7 @@ Enable the optional capabilities the bridge needs in [@BotFather](https://t.me/B
|
|
|
121
121
|
| Threaded Mode | Run one leader plus visible follower Pi instances through named private-chat threads. | One bot can host a local multi-instance Pi organism without hidden process spawning. |
|
|
122
122
|
| Reroute and restore | Give unknown and command-created temporary threads explicit forward and replace/restore choices. | Forward removes the temporary tab; restore rebinds it and removes only the replaced old tab, so Telegram client state repairs without orphan controls. |
|
|
123
123
|
| Extension sections | Add menu sections, commands, status rows, settings, callbacks, and delivery helpers from companion extensions. | `pi-telegram` becomes a platform surface for other Pi extensions. |
|
|
124
|
-
| Runtime diagnostics | Use `/telegram-status` and recent runtime events for connection, role, queue, transport, and
|
|
124
|
+
| Runtime diagnostics | Use `/telegram-status` and recent runtime events for connection, role, negotiated bus protocol/build/capabilities, separate polling and inbound-worker progress, journal depth, local/foreign queue ownership, automatic retry waits, transport, and failures. | Compatible build skew, foreign semantic authority, a healthy poller, durable backoff and an infrastructure-blocked worker remain distinguishable without hidden logs. |
|
|
125
125
|
| Safety and ownership | Pair one owner, lock transport, scope targets, and reject fake terminal behavior. | Remote access remains explicit, bounded, and understandable. |
|
|
126
126
|
|
|
127
127
|
## Core Loop
|
|
@@ -163,8 +163,8 @@ Run these inside Pi.
|
|
|
163
163
|
| `/telegram-setup <profile>` | Save or update a named-profile bot token |
|
|
164
164
|
| `/telegram-connect` / `/telegram-connect default` | Activate `profiles.default` and acquire its transport ownership |
|
|
165
165
|
| `/telegram-connect <profile>` | Activate a named profile and acquire its transport ownership |
|
|
166
|
-
| `/telegram-disconnect` | Confirm, then stop polling, release ownership, and delete this instance's Threaded Mode tab; graceful Pi quit
|
|
167
|
-
| `/telegram-status` | Inspect connection, mode, queue, transport, and recent diagnostics |
|
|
166
|
+
| `/telegram-disconnect` | Confirm, then stop polling, release ownership, and delete this instance's Threaded Mode tab; graceful Pi quit always preserves restart ownership and independently deletes the tab only when automatic cleanup is enabled |
|
|
167
|
+
| `/telegram-status` | Inspect connection, mode, separate polling/worker progress, journal depth, queue, transport, automatic retry state, and recent diagnostics |
|
|
168
168
|
|
|
169
169
|
Named profile identifiers contain only lowercase ASCII letters and digits (maximum 32 characters); `default`, `main`, and `active` remain reserved. If graceful thread deletion was interrupted, a same-profile replacement reuses its still-active thread and cancels the superseded cleanup instead of deleting and recreating the tab during startup.
|
|
170
170
|
|
|
@@ -176,7 +176,7 @@ Named profile identifiers contain only lowercase ASCII letters and digits (maxim
|
|
|
176
176
|
|
|
177
177
|
### Queue Runtime
|
|
178
178
|
|
|
179
|
-
Messages sent while Pi is busy become queued turns. Priority lanes support control actions and model-switch continuations. Queue controls let you inspect, delete, promote, and dispatch work from Telegram without touching the terminal. If Pi automatically retries a transient provider failure, the active Telegram turn stays bound until the successful reply arrives or Pi confirms that the run has settled.
|
|
179
|
+
Messages sent while Pi is busy become queued turns. Priority lanes support control actions and model-switch continuations. Queue controls let you inspect, delete, promote, and dispatch work from Telegram without touching the terminal. Reaction shortcuts are reversible while a turn is waiting: priority reactions move it ahead, removal reactions suppress it, and changing or removing those reactions restores the corresponding priority or default state. If Pi automatically retries a transient provider failure, the active Telegram turn stays bound until the successful reply arrives or Pi confirms that the run has settled.
|
|
180
180
|
|
|
181
181
|
### Native Rich Markdown
|
|
182
182
|
|
|
@@ -201,6 +201,7 @@ Classic private DM mode is the base product mode. When Telegram private-chat Thr
|
|
|
201
201
|
- One live leader owns `getUpdates`.
|
|
202
202
|
- Followers are visible Pi processes started by the operator.
|
|
203
203
|
- Each connected instance gets a Telegram thread target.
|
|
204
|
+
- Queued work for a live follower transfers through authenticated exact-journal handoff rather than replaying under the transport owner.
|
|
204
205
|
- Follower session replacement automatically reconnects the new session context to the same thread instead of requiring another manual connect.
|
|
205
206
|
- Unknown threads are preserved and offered explicit reroute/restore choices.
|
|
206
207
|
- Telegram never launches hidden Pi processes.
|
|
@@ -243,6 +244,8 @@ Stable public entrypoints are documented in [Public API](./docs/public-api.md),
|
|
|
243
244
|
|
|
244
245
|
## Safety Boundaries
|
|
245
246
|
|
|
247
|
+
Durable inbound admission is a **process-crash recovery** guarantee. Atomic private-file replacement preserves acknowledged journal authority across ordinary process exit, crash, kill, and replacement, but the extension does not flush files or parent directories for host/kernel/filesystem/device/power-loss durability. Keep `~/.pi/agent` on appropriately managed storage and backups if that stronger operational guarantee is required. Before downgrading below `0.28.0`, run `node scripts/check-downgrade.mjs`; a blocked result means `0.28.x` must drain the retained journal first. See [Durable Admission And Recovery](./docs/architecture.md#durable-admission-and-recovery).
|
|
248
|
+
|
|
246
249
|
`pi-telegram` intentionally does not:
|
|
247
250
|
|
|
248
251
|
- Spawn hidden Pi follower processes.
|
package/api/updates.ts
CHANGED
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export {
|
|
8
|
+
assertTelegramUpdateExecutionCurrent,
|
|
9
|
+
carryTelegramUpdateExecutionFence,
|
|
10
|
+
createTelegramUpdateExecutionFenceGuard,
|
|
11
|
+
getTelegramUpdateExecutionFence,
|
|
8
12
|
registerTelegramUpdateHandler,
|
|
13
|
+
type TelegramUpdateExecutionFence,
|
|
9
14
|
type TelegramUpdateHandler,
|
|
10
15
|
type TelegramUpdateHandlerVerdict,
|
|
11
16
|
} from "../lib/updates.ts";
|
package/docs/architecture.md
CHANGED
|
@@ -60,17 +60,18 @@ The repository uses a **Flat Domain DAG**:
|
|
|
60
60
|
|
|
61
61
|
### Domain Ownership Map
|
|
62
62
|
|
|
63
|
-
- `index.ts`: composition root for live ports,
|
|
63
|
+
- `index.ts`: composition root for live ports, domain runtime construction, cross-domain port wiring, and lifecycle registration. It exposes wiring but owns no process identity, journal-binding selection, mutable late-binding state, admission lifecycle selection, reusable policy, or low-level adapter mechanics; those belong to `bus`, `journal`, `prompts`, `activity-verbosity`, `updates`, and other named domains.
|
|
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
|
-
- `locks` / `polling`: extension-local transport owner storage, exact-owner epoch exposure, process-global reload generations, owner-aware polling lifecycle/takeover/follower registration, and
|
|
67
|
-
- `
|
|
68
|
-
- `
|
|
66
|
+
- `locks` / `polling`: extension-local transport owner storage, exact-owner epoch exposure, process-global reload generations, owner-aware polling lifecycle/takeover/follower registration, and classic-vs-Threaded capability orchestration. Polling owns long-poll state, worker-before-poller startup, strict batch validation, journal-before-offset admission, one offset commit per response, and non-awaited worker signaling.
|
|
67
|
+
- `journal`: private profile/bot-scoped raw-update authority with strict v1 identity/schema validation, exact deduplication, bounded transaction-serialized `0600` publication, process/session/acquisition-bound prompt/control receipts, owner-fenced completion, durable retry state, and generic removal that rejects queued or legacy failed authority. Runtime/recovery identity separates token rotation from future proof-gated queue-owner recovery.
|
|
68
|
+
- `bus` / `bus-api` / `bus-leader` / `bus-follower` / `ownership` / `target`: Threaded Mode multi-instance bus contracts, profile-scoped process/endpoint identity, local leader/follower IPC, leader-only orchestration, follower-side manual registration/session runtime, follower-routed Bot API calls, live message ownership, and `{ chatId, threadId? }` target identity. `bus` owns protocol v1 independently from package build, canonical capabilities, compatibility, process identity, profile-aware endpoints, and IPC primitives. Registration rejects missing/mismatched protocol before provisioning while preserving compatible package skew; negotiated identity reaches status/state. Leader runtime, leader envelope handling, follower assembly, and follower registration construction require explicit protocol identity, preventing identity-less composition at both production and low-level runtime boundaries. `bus-leader` owns leader envelope handling and polling/server/prune orchestration; `bus-follower` owns registration/ack negotiation, active leader-auth/election state, heartbeat, authenticated clients, forwarded receiving, and recovery.
|
|
69
|
+
- `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, paired manual-disconnect/session-restart cleanup assembly, and reconciliation scheduling across bot identity, pairing assumptions, live target bindings, reservations, and transport health after meaningful observable signals. It should call narrower domain primitives rather than letting `index.ts`, `threads`, or `status` accumulate cross-cutting reconciliation policy.
|
|
69
70
|
- `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, profile-bound same-process leader session handoff,
|
|
71
|
-
- `updates` / `routing`: update classification, authorization
|
|
72
|
-
- `media` / `text-groups` / `time-injection` / `turns` / `inbound`: inbound
|
|
73
|
-
- `queue`: queue
|
|
71
|
+
- `threads`: Telegram UI thread/tab binding state mapped to Bot API `message_thread_id` / `ForumTopic` transport. Owns leader and current-instance identity state, active-turn โ follower โ leader target preference, matching status projection assembly, profile-bound same-process leader session handoff, slot allocation from 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.
|
|
72
|
+
- `updates` / `routing`: update classification, authorization, callbacks, edits, reactions, forwarding, and inbound composition. `updates` owns production journal workers, leader/follower admission lifecycle construction, binding and settlement selection, queue-handoff projection across recipient journals/admission/IPC/live queue state, process/session queue-owner projection, post-public source binding, exact-signal late settlement, durable receipt readiness, same-process claim reconstruction, and structural worker state. `routing` converts message, callback, guest, section, reroute, and control admissions into exact receipts.
|
|
73
|
+
- `media` / `text-groups` / `time-injection` / `turns` / `inbound`: inbound extraction, rich reply plaintext, grouped debounce, split-text coalescing, optional time context, handlers, and prompt assembly/editing. Group replay replaces stale generation-local message/report bindings without duplicating content.
|
|
74
|
+
- `queue`: queue contracts, transport stamps, lanes, readiness, mutations, dispatch, enqueueing, and lifecycle sequencing. Durable admission uses deterministic receipts, canonical source sets, replay dedupe, multiple folded-history receipts, append-before-dispatch reporting, exact handoff/control/discard settlement, and a readiness gate. Receipt-bearing inactive-profile work is preserved after current-profile work rather than dropped.
|
|
74
75
|
- `runtime`: session-local coordination primitives: counters, flags, setup guard, abort handler, typing timers, dispatch flags, and reset binding.
|
|
75
76
|
- `model` / `menu-model` / `menu-thinking` / `menu-status` / `menu-queue` / `menu-settings` / `menu` / `commands`: model identity, thinking levels, scoped model handling, menu render/callback behavior, slash commands, bot commands, and interactive controls.
|
|
76
77
|
- `sections`: Telegram menu-section registry, opaque section callback tokens, render/callback dispatch, safe section ports, and diagnostics.
|
|
@@ -82,7 +83,7 @@ The repository uses a **Flat Domain DAG**:
|
|
|
82
83
|
- `outbound`: outbound text transformations, voice/button artifact delivery, and generated callback actions.
|
|
83
84
|
- `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
85
|
- `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,
|
|
86
|
+
- `bindings` / `lifecycle` / `prompts` / `prompt-templates` / `pi`: Pi-facing command/tool/hook registration and cohesive cross-domain binding assembly, including queue mutation/dispatch/watchdog composition over admission and transport ports; session-generation fencing and start/shutdown sequencing across Queue, grouped input, Delivery, polling, capability monitor, follower refresh, and assistant-output projection; Telegram prompt guidance; prompt-template discovery/expansion; and centralized direct Pi SDK imports.
|
|
86
87
|
- `command-templates`: shell-free command-template helpers, composition expansion, placeholder substitution, executable resolution, warnings, and retry/timeout semantics.
|
|
87
88
|
|
|
88
89
|
### Guarded Invariants
|
|
@@ -119,8 +120,11 @@ Telegram configuration lives in `~/.pi/agent/telegram.json`. Bot/session identit
|
|
|
119
120
|
|
|
120
121
|
### Runtime Ownership
|
|
121
122
|
|
|
122
|
-
- `/telegram-connect` acquires or moves the active profile's owner slot before polling starts. `/telegram-disconnect` keeps its destructive confirmation, then stops polling and releases only that exact slot. In Threaded Mode it tears down the disconnecting instance's bound Telegram thread: leaders delete their own thread directly, and followers send an authenticated exact-generation disconnect envelope and wait for confirmed leader cleanup before unregistering. Graceful Pi `quit`
|
|
123
|
+
- `/telegram-connect` acquires or moves the active profile's owner slot before polling starts. `/telegram-disconnect` keeps its destructive confirmation, then stops polling and releases only that exact slot. In Threaded Mode it tears down the disconnecting instance's bound Telegram thread: leaders delete their own thread directly, and followers send an authenticated exact-generation disconnect envelope and wait for confirmed leader cleanup before unregistering. Graceful Pi `quit` always preserves the owner slot as restart intent, allowing a reopened same-`cwd` session to reclaim the stale lease. When `threads.automaticCleanup` is enabled (the default), quit also deletes the bound Telegram tab without releasing that restart intent; disabling it preserves the tab through replacement-style suspension. Failed automatic cleanup records diagnostics and falls back to safe suspension so remaining lifecycle cleanup still runs.
|
|
123
124
|
- Session start schedules polling resume asynchronously only when the owner slot already points at the current `pid`/`cwd`, or when a stale same-`cwd` owner can be safely replaced after process restart. Startup and `/resume` do not wait on leader election, Bot API probes, poller handoff, or thread reconciliation before restoring the Pi session.
|
|
125
|
+
- The polling owner alone bounds `getUpdates`: each request derives its cancellation budget from Telegram's declared long-poll timeout plus 10 seconds of transport grace (10 seconds for the zero-timeout initial sync and 40 seconds for the normal 30-second poll). The request-local controller inherits poller cancellation, rejects its owner at the budget, and fences any late transport result. Ordinary Bot API and media operations do not receive speculative blanket deadlines. Existing caller signals remain authoritative through API retry waits, only retry-safe methods replay explicit retryable responses, and non-idempotent sends preserve commit-unknown evidence instead of risking duplicate mutation.
|
|
126
|
+
- `pollingActive` reports only whether this runtime still owns an unresolved polling lifecycle; it is not health evidence. A separate observable state records `starting`, `long-poll`, `persisting-journal`, `persisting-offset`, `retrying`, or `stopped`, together with phase start, current update id, last successful response time/count, and terminal stop reason. This distinguishes a stuck HTTP poll from downstream update work without a wall-clock stale heuristic.
|
|
127
|
+
- Built-in read-only menu commands return after required local mutation and schedule context-fenced rendering and command synchronization independently, so those effects cannot withhold the next inbound offset.
|
|
124
128
|
- Pi `print`/`json` run modes stay passive. Inherited child sessions that share `telegram.json` but do not own the exact `pid`/`cwd` slot must not poll or call `getUpdates` unless the operator force-takes ownership.
|
|
125
129
|
- Session replacement through `reload`, `new`, `resume`, or `fork` suspends polling/watchers without releasing ownership so the next session in the same process can resume. A registered follower snapshots its assigned target into a short-lived same-process handoff, stops the old receiver/heartbeat, and re-registers through the live leader without marking or replacing its Telegram thread. Hard process termination cannot run graceful teardown, so stale recovery retains its restart-hint path.
|
|
126
130
|
- Live external owners require explicit takeover confirmation. Long-lived timers compare against snapshotted owner identity and stop local transport work when the slot no longer matches.
|
|
@@ -134,7 +138,7 @@ Telegram configuration lives in `~/.pi/agent/telegram.json`. Bot/session identit
|
|
|
134
138
|
The three runtime files have different authority and write pressure. Preserve that distinction when optimizing them:
|
|
135
139
|
|
|
136
140
|
- `owners.json` is safety-critical transport authority. Acquire, release, takeover, stale recovery, and two-second leader lease refresh mutate it. The steady-state baseline is one cached atomic rewrite every two seconds per active profile, or 43,200 refreshes/day; one-second ownership checks are read-only. Every mutation serializes the full cross-process read/check/write through `owners.json.transaction`.
|
|
137
|
-
- `state.json` combines recovery-critical thread/capability state with observational runtime projections. Every explicit thread-store `persist()` builds a semantic snapshot, but an unchanged payload skips temporary-file creation and rename after ignoring `writtenAtMs`; changed snapshots retain the full atomic replacement path. Diagnostics scheduling coalesces requests across a bounded 100 ms window. Only the exact transport owner commits; non-owners reload current disk state instead of publishing.
|
|
141
|
+
- `state.json` combines recovery-critical thread/capability state with observational runtime projections. Every explicit thread-store `persist()` builds a semantic snapshot, but an unchanged payload skips temporary-file creation and rename after ignoring `writtenAtMs`; changed snapshots retain the full atomic replacement path. Diagnostics scheduling coalesces requests across a bounded 100 ms window. Polling phase transitions and successful `getUpdates` responses schedule this observational projection, so an idle leader normally produces one changed polling snapshot per completed long-poll cycle. Only the exact transport owner commits; non-owners reload current disk state instead of publishing.
|
|
138
142
|
- `logs.jsonl` is fail-soft observational evidence, never routing authority. Runtime events admitted in one JavaScript turn batch by captured profile path into one size check, one profile-wide file transaction, and one append while preserving event order. Batching adds no timer or shutdown-loss window; separate profiles remain isolated, and one failed group does not drop another. Scope reset and rotation retain their serialized copy/replace path. The 5 MiB value is a rotation threshold: an authorized writer rotates between batched records before the next record crosses it, so overshoot is bounded to one admitted record plus reset metadata; a writer without reset authority defers rotation to the owner.
|
|
139
143
|
|
|
140
144
|
This baseline counts write-producing code paths rather than filesystem implementation details that vary between ext4, APFS, NTFS, and network-backed home directories. Optimization evidence should compare these deterministic triggers first, then use platform smoke evidence for rename, named-pipe, crash, and cleanup behavior. Recovery-critical `state.json` fields are `bot`, `identities`, `reservations`, `pendingProvisions`, `syncObservations`, and `threads`; `runtime`, `liveRoster`, `diagnostics`, and `writtenAtMs` are observational and may use bounded coalescing when authority checks remain unchanged.
|
|
@@ -143,13 +147,13 @@ Version `0.24.0` intentionally does not read or migrate the former agent-level `
|
|
|
143
147
|
|
|
144
148
|
### Threaded Mode Multi-Instance Bus
|
|
145
149
|
|
|
146
|
-
Telegram private-chat Threaded Mode is the public switch for multi-instance Telegram operation. Classic single-DM polling is the base mode. When Telegram private-chat threads are available for the bot, the bridge enables the local leader/follower bus automatically; when threads are unavailable or later disabled, the bridge returns to classic single-DM polling as a first-class mode.
|
|
150
|
+
Telegram private-chat Threaded Mode is the public switch for multi-instance Telegram operation. Classic single-DM polling is the base mode. When Telegram private-chat threads are available for the bot, the bridge enables the local leader/follower bus automatically; when threads are unavailable or later disabled, the bridge returns to classic single-DM polling as a first-class mode. Before a non-owner `/telegram-connect` chooses follower registration or singleton takeover, it discards process-local status/capability projections and reads the current owner-published mode: `enabled` registers a follower without a takeover prompt, while `disabled` uses the classic confirmation flow.
|
|
147
151
|
|
|
148
152
|
Named Telegram profiles are orthogonal to Threaded Mode. The selected profile chooses the bot/session slice (`botToken`, `botId`, `botUsername`, `allowedUserId`, `lastUpdateId`) and scopes the `owners.json` slot, diagnostics logs, state files, thread/bus ownership, and leader/follower IPC endpoints; it must not change the Threaded Mode rules. Within one selected profile, leader/follower election, bus transport, thread provisioning, routing, ownership forwarding, cleanup, and runtime diagnostics behave exactly as they do for the `default` slot. A different selected profile is a parallel bot runtime: its owner slot, `tmp/telegram/state.<profile>.json`, `tmp/telegram/logs.<profile>.jsonl`, `tmp/telegram/logs.<profile>._prev.jsonl`, thread bindings, Unix sockets, and Windows named pipes are isolated from the default profile and other named profiles while shared bridge settings remain top-level/global.
|
|
149
153
|
|
|
150
154
|
Profile reality follows three explicit storage classes. `telegram.json` shared settings and extension registries are process-global platform configuration; `profiles.default` and `profiles.<name>` bot/session fields plus observable transport/routing authority are profile-scoped; queues, active turns, ownership caches, menu state, and runtime controllers are session-local memory. Config persistence serializes cross-process writers and applies each recursive mutation delta to the latest disk snapshot, so named-profile offsets and unrelated global/profile updates do not stale-replace one another. Polling publishes only its monotonic `lastUpdateId` into the current config-store snapshot; it never persists the detached full config object captured at poll start, which would interpret newer Settings fields as deletions. 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.
|
|
151
155
|
|
|
152
|
-
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 and does not send a Telegram-visible disconnected notice, because the common cause may be leader reload or IPC handoff rather than a dead follower. Pruning alone preserves the binding; when Thread cleanup is enabled, only a subsequent OS check that confirms the exact registered PID absent may create fenced cleanup intent, and that cleanup serializes ahead of replacement registration. Successful follower target reuse refreshes the binding's recovery timestamp. Absent follower bindings remain durable restoration hints until explicit stale, deleted, offline, or reconciliation evidence invalidates them; process absence and heartbeat pruning alone do not remove them. If an authenticated live follower carries an exact target that is absent from current bindings, the leader recovers it only behind a synchronous visibility probe: success activates it, explicit stale evidence provisions a replacement, and ambiguous failure rejects registration. A carried slot is restored only when it is not already occupied. `tmp/telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging; it resets on extension start and runtime scope changes, and must not become routing/provisioning authority. `tmp/telegram/state.json` is an extension+bot observable/debug snapshot aligned with status diagnostics: `source: "snapshot"` and `writtenAtMs` mark it as observational, not authoritative. All instances on one Telegram profile read the same snapshot, but only the active transport lock owner persists it; followers become writers only after promotion. Status-only persistence reloads current disk bindings before serialization, preventing a stale follower/status view from erasing newer leader-owned targets. Fresh capability observations may skip redundant startup probes, but stale snapshots re-probe before suppressing bus/thread behavior. Top-level `bot` mirrors bot-wide capabilities such as thread mode, `runtime` describes process role/status, `liveRoster` mirrors followers/current targets/reservations, `diagnostics` mirrors recent status/debug signals including the latest thread-reconciler phase/counts, `threads` stores current routeable bindings, 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.
|
|
156
|
+
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 and does not send a Telegram-visible disconnected notice, because the common cause may be leader reload or IPC handoff rather than a dead follower. Pruning alone preserves the binding; when Thread cleanup is enabled, only a subsequent OS check that confirms the exact registered PID absent may create fenced cleanup intent, and that cleanup serializes ahead of replacement registration. Successful follower target reuse refreshes the binding's recovery timestamp. Absent follower bindings remain durable restoration hints until explicit stale, deleted, offline, or reconciliation evidence invalidates them; process absence and heartbeat pruning alone do not remove them. If an authenticated live follower carries an exact target that is absent from current bindings, the leader recovers it only behind a synchronous visibility probe: success activates it, explicit stale evidence provisions a replacement, and ambiguous failure rejects registration. A carried slot is restored only when it is not already occupied. `tmp/telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging; it resets on extension start and runtime scope changes, and must not become routing/provisioning authority. `tmp/telegram/state.json` is an extension+bot observable/debug snapshot aligned with status diagnostics: `source: "snapshot"` and `writtenAtMs` mark it as observational, not authoritative. All instances on one Telegram profile read the same snapshot, but only the active transport lock owner persists it; followers become writers only after promotion. Status-only persistence reloads current disk bindings before serialization, preventing a stale follower/status view from erasing newer leader-owned targets. Fresh capability observations may skip redundant startup probes, but stale snapshots re-probe before suppressing bus/thread behavior. Top-level `bot` mirrors bot-wide capabilities such as thread mode, `runtime` describes process role/status, `liveRoster` mirrors followers/current targets/reservations, `diagnostics` mirrors recent status/debug signals including the latest thread-reconciler phase/counts, `threads` stores current routeable bindings, TTL-bounded reservations explain short-lived slot collision guards, and TTL-pruned `pendingProvisions` protects in-flight topic creation slots from cleanup/allocation races. Fresh provisioning writes pending state before the Bot API create call, adds the returned target to the pending record, persists a `starting` binding, then promotes it to `active` and clears pending state. If final binding persistence fails after Telegram returns a thread id, the targeted pending provision remains as cleanup/retry evidence. Once targeted pending provisions expire, they are retained for `thread-reconciler` close/delete cleanup and pending scratchpad removal after a successful cleanup apply; untargeted expired pending records can prune without cleanup because no Telegram thread id exists. Runtime events coalesce status-snapshot writes so transient bus/API/update failures remain inspectable even when the operator has not opened `/telegram-status`. The bridge must not keep a durable `telegram-targets.json` target history; stale/offline/failed thread observations are pruned instead of reused. Previous-process leader bindings that still probe alive become reservations/collision guards, not routeable active threads, so a reloaded leader can take the next free slot without duplicating the same visible tab name. The thread chat is always the private bot DM with the paired owner (`allowedUserId`). In Telegram private-chat Threaded Mode, the leader creates/reuses its own thread before polling โ it is a real bound instance, not a dispatcher. Followers authenticate bus envelopes with the leader-minted capability secret stored in the active lock entry. Bot capability monitoring does not probe through the bus until the process either owns that direct lock or has completed authenticated follower registration. Leader lock entries also carry a stable `leaderEpoch` minted on acquisition and preserved across heartbeat refreshes; leader-owned cleanup/provisioning plans stamp that epoch, and Thread Reconciler apply skips destructive work if leadership has moved on before side effects run. Followers own their own Pi session state, queue, active turns, previews, menus, and lifecycle hooks, but route allowlisted, target-scoped Telegram API calls through the leader. When a follower promotes after heartbeat loss, status/state diagnostics expose only the transient `electing` lifecycle phase; stable `leader`/`follower` identity stays in the bus role so diagnostics do not duplicate role state. The TUI status bar and `/telegram-status` report `leader` or `follower` role so a registered follower is not shown as generically disconnected. Terminal status identity and the `[telegram|thread:name]` prompt label use the same target-aware current-instance resolver: registered local metadata wins over a stale shared binding for the matching target, while the binding remains a fallback for partial metadata.
|
|
153
157
|
|
|
154
158
|
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.
|
|
155
159
|
|
|
@@ -175,17 +179,58 @@ All inbound updates are gated by the configured authorized user id.
|
|
|
175
179
|
|
|
176
180
|
### Inbound Turn Flow
|
|
177
181
|
|
|
178
|
-
1. Poll updates through `getUpdates
|
|
179
|
-
2.
|
|
180
|
-
3.
|
|
181
|
-
4.
|
|
182
|
+
1. Poll updates through `getUpdates` under the polling owner's request budget.
|
|
183
|
+
2. Validate and atomically journal each complete response batch before advancing its offset once.
|
|
184
|
+
3. Signal the independent source-bound worker and begin the next poll without awaiting semantic execution.
|
|
185
|
+
4. Run stable public raw-update handlers in registration order, then authorize and route retained built-in traffic.
|
|
182
186
|
5. Coalesce media groups, likely split long text, and one adjacent forward-plus-comment pair in either order when needed.
|
|
183
|
-
6. Download files
|
|
184
|
-
7.
|
|
185
|
-
8.
|
|
186
|
-
9.
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
6. Download files with size limits and partial-download cleanup, then run configured/programmatic inbound handlers.
|
|
188
|
+
7. Build a prompt or control queue item carrying an exact durable receipt for every contributing update id.
|
|
189
|
+
8. Remove journal authority only after prompt handoff, control settlement, or durable follower acknowledgement; execution failures remain durable until automatic replay succeeds.
|
|
190
|
+
9. Handle `edited_message` updates separately while the original turn is still queued and dispatch only when all safety gates are clear.
|
|
191
|
+
|
|
192
|
+
#### Durable Admission And Recovery
|
|
193
|
+
|
|
194
|
+
Here, **durable** means recovery across ordinary process exit, crash, kill, and replacement after a successful atomic rename is visible to the filesystem. It does not promise survival across host, kernel, filesystem, storage-device, or power failure: journal and offset publication do not call `fsync`/`fdatasync`, and parent directories are not flushed. A host-level failure may therefore lose a recently acknowledged rename despite correct process-level ordering. Operators requiring that stronger boundary must place the agent directory on storage with an independently managed durability/backup policy; `0.28.0` must not be described as power-loss durable.
|
|
195
|
+
|
|
196
|
+
The profile-scoped journal separates transport progress from semantic progress. Leader/classic snapshots live at `tmp/telegram/inbox[.<profile>].json`; follower paths add a stable target-binding hash. The selected post-v1 storage design is one revisioned compacted snapshot plus immutable atomic transaction segments beside it. Existing v1 files load as implicit revision `0`, while positive snapshot revisions are explicit. Immutable revision segments publish privately and atomically under the existing journal transaction lock; exact repeats are idempotent, while gaps and conflicting duplicate revisions fail closed. Each segment carries one complete mutation, and readers reconstruct ordered upserts, removals, and operator-disposition state only from revisions newer than the snapshot. Malformed or gapped segments, filename/revision disagreement, and foreign journal identity fail closed. After the initial snapshot, append, batch completion, queue receipt/owner/handoff, retry/terminal, recovery, and operator dispositions publish only changed upserts/removals and disposition replacement in one segment. This avoids rewriting retained raw updates during completion-heavy drains without splitting exact queue, failure, recovery, or disposition transactions.
|
|
197
|
+
|
|
198
|
+
Compaction runs under the journal transaction lock when either 256 unapplied segments or 4 MiB of segment bytes is reached. It publishes the complete private (`0600`) snapshot at revision `R` before best-effort deletion of segments `<= R`; failed cleanup leaves redundant segments that readers ignore. Interrupted cleanup therefore leaves either an older snapshot plus newer authoritative segments or a newer snapshot plus harmless redundant older segments. Revision gaps, conflicting duplicates, malformed segments, and identity mismatches fail closed. The logical reconstructed journal and aggregate unapplied segment bytes are independently bounded at 10,000 entries and 32 MiB as applicable; rejected growth publishes neither snapshot nor segment bytes. Compaction may temporarily require exactly one private complete snapshot of at most 32 MiB. Capacity pauses polling and authority files are never automatically deleted, reset, or quarantined.
|
|
199
|
+
|
|
200
|
+
`pending` entries remain immediately executable while raw interception, routing, or grouping is incomplete. Every execution failure becomes `retry-wait` with durable attempt count, next eligible time, failure class, bounded summary, and latest failure time. The `failed` state remains schema-compatible only for legacy candidate journals and is converted to automatic retry during lifecycle startup. `queued` entries carry exact prompt/control receipts plus the acquiring Pi runtime instance, OS pid/birth identity, session generation, acquisition id, and acquisition time. Queueing alone is never completion.
|
|
201
|
+
|
|
202
|
+
Queue receipt ownership is independent from the Telegram transport lock. A same-instance, same-process generation may reconstruct its local receipt across a fenced session replacement and may settle it after transport ownership moves. A different process reports the receipt as foreign, never republishes it into its local queue, and cannot complete it even if it reads the acquisition id. Startup no longer treats process replacement as proof that an owner died: foreign and legacy unowned receipts remain durable.
|
|
203
|
+
|
|
204
|
+
Recovery and live handoff are compare-and-set under the journal transaction. Queue discard during exact queue-lifecycle cancellation requires the exact local owner/acquisition and removes all receipt sources atomically. Before admission worker start, the lifecycle groups each foreign receipt and asks the journal to recheck OS pid liveness plus process-birth identity under the same transaction; a live owner returns `owner-alive` and a live owner without stable birth proof returns `owner-unverifiable`, both without mutation, while exact negative proof converts the complete receipt back to `pending`. Replacement registration carries its exact pid/process-birth before this check; when registration and recovery race, that live identity wins the liveness proof and the queued receipt remains untouched. Only then does the replacement worker start, replay, and acquire a fresh receipt id/acquisition, fencing every stale owner.
|
|
205
|
+
|
|
206
|
+
Authenticated live handoff uses journal CAS plus bounded local IPC. The donor creates a one-time high-entropy token and durably offers the complete receipt to one exact recipient runtime/process/session identity; the journal stores only a digest bound to queue kind, receipt sources, donor acquisition, and recipient identity. While offered, donor completion/discard and dead-owner recovery fail closed, so authority cannot disappear during payload transfer. Prompt payloads carry all queue fields; control payloads carry only their stable `status`/`model` identity and rebuild executable closures locally. The separately negotiated `queue-handoff-v1` capability gates this envelope for leader and both peer generations. Each receipt carries its exact source journal binding; the donor derives the recipient follower-journal binding from the authenticated stable follower profile before routing. The bus validates payload shape/size and exact donor/recipient registration generations, and the recipient selects only that matching active lifecycle, stages one complete receipt idempotently, accepts the journal CAS, and returns the exact receipt plus newly minted owner in its ACK. Malformed, legacy-unbound, inactive-generation, or unavailable bindings fail closed.
|
|
207
|
+
|
|
208
|
+
During recipient staging, presenting the token atomically replaces the journal owner with a fresh acquisition carrying the handoff digest, removes the offer, and permanently fences donor settlement. The donor treats only an ACK carrying that exact accepted owner as success and never repeats acceptance against a donor-bound journal runtime. The recipient can repeat the same acceptance idempotently; a different token cannot claim an already accepted receipt. The coordinator contract orders offer โ stage/accept exact receipt-and-owner ACK โ donor removal โ recipient readiness for direct leaderโfollower and followerโfollower routing. Before acceptance, negative or mismatched acknowledgement exactly cancels the offer and keeps donor work. After acceptance, a lost acknowledgement cannot roll authority back: cancellation fails closed and donor memory remains frozen until exact accepted-owner reconciliation removes it. Recipient registration carries exact process-birth/session identity, and staged payloads remain outside the live dispatch store until accepted journal authority has been reconstructed. Production advertises `queue-handoff-v1` only with this exact role/journal selection and uses the same coordinator ordering for direct leaderโfollower and followerโfollower routes.
|
|
209
|
+
|
|
210
|
+
Queued semantic authority has no elapsed-time lease. A timeout cannot prove either owner death or effect quiescence, so it cannot safely recover a receipt. Resolution is limited to authenticated live handoff, exact owner discard/settlement, or transaction-rechecked negative PID plus process-birth evidence. Live or unverifiable owners remain queued indefinitely rather than risking duplicate execution.
|
|
211
|
+
|
|
212
|
+
The initial `offset: -1` cursor bootstrap is allowed only when both cursor and journal are absent or empty. Thereafter process-level ordering is journal atomic rename โ one monotonic offset atomic rename โ worker signal. Failure before journal publication leaves the offset unchanged; failure after journal publication but before offset publication permits Telegram redelivery and journal dedupe; failure after offset publication but before worker signal replays from the journal on restart. Queue-owner, retry, terminal, handoff, and completion transitions use the same journal publication primitive and therefore share this process-crash boundary. The final completion window is at-least-once, so replay-sensitive external effects must use `update_id` or the stable delivery id as an idempotency key.
|
|
213
|
+
|
|
214
|
+
Threaded Mode forwarding is a two-journal handoff. Only peers that mutually advertise protocol v1 and `durable-follower-admission-v1` may route or become election-eligible. The follower validates its exact binding and registration generation, durably appends the source-bound delivery, and only then returns the exact receipt. The leader classifies each attempt as `accepted`, `retryable`, or `terminal-rejected` with its delivery identity and failure class; only `accepted` with the expected `deliveryId` and `sourceUpdateId` may complete leader journal authority. Missing, negative, stale-generation, or mismatched-receipt acknowledgements remain durable, and a callback error answer is only an operator-facing side effect.
|
|
215
|
+
|
|
216
|
+
Delivery ids derive only from envelope kind, source `update_id`, and stable recipient binding. Live registration generation remains a separate attempt fence, while callback/reaction message ownership carries that stable binding and rebinds to its current authenticated follower registration after replacement. Lost acknowledgements therefore replay idempotently into the same follower journal identity without changing the delivery id. Package build skew is allowed only while protocol version and capabilities remain compatible.
|
|
217
|
+
|
|
218
|
+
Worker execution ownership is per `update_id` across same-runtime generations. Aborting a generation ends its authority but does not prove its handler settled; replacement replay remains blocked on that exact settlement. Late success and failure are both diagnostic events. Public and built-in handlers receive the same optional execution fence (`signal`, generation/update identity, and pre-effect assertion); the runtime binds it non-enumerably to every internal update carrier and checks it before routing-plan effects. Prompt construction rechecks after downloads and inbound handlers before queue mutation, pairing rechecks around persistence, command/menu and extension-command delegation retain the source fence across detached effects, lifecycle sync rechecks after store load before reconciliation, and reroute clones carry the source fence through forwarding, thread replacement, cleanup, persistence, and Bot API rename boundaries. Legacy handlers remain source-compatible but must not commit unfenced late effects.
|
|
219
|
+
|
|
220
|
+
The canonical update transition contract is:
|
|
221
|
+
|
|
222
|
+
- `pending โ executing`: the generation-local worker selects an unclaimed source; `executing` is a runtime phase, not a separately persisted entry state.
|
|
223
|
+
- `executing โ completed | queued | pending | retry-wait`: exact local completion removes the entry, queue admission persists its receipt, deferred grouping retains replay authority, and every execution failure persists retry evidence.
|
|
224
|
+
- `retry-wait โ executing`: only after `nextRetryAtMs`; repeated signals before eligibility do not execute the entry. Automatic retries continue indefinitely with exponential `1s โ 2s โ 4s โ 8s โ 16s โ 32s โ 60s` delay capped at `60s`, while later independent updates continue draining.
|
|
225
|
+
- Legacy `failed โ retry-wait`: startup atomically resumes terminal entries written by earlier `0.28.0` candidates. Runtime policy never silently discards durable inbound authority and exposes no Pi command for manual retry/discard.
|
|
226
|
+
- `queued โ offered โ staged โ queued`: only the exact persisted donor may offer or cancel a live handoff; an offer preserves donor ownership but freezes ordinary settlement and recovery. Authenticated bounded IPC stages one exact payload/receipt outside the live queue. Exact recipient acceptance mints a fresh acquisition, reconstructs local ownership, removes donor work, then publishes recipient dispatch readiness.
|
|
227
|
+
- `queued โ completed | pending`: only the exact persisted owner receipt may complete or discard queued sources; generic completion rejects queued state. Process-birth-proven owner death may atomically recover the complete unoffered receipt to `pending`; live, unverifiable, or offered owners remain queued.
|
|
228
|
+
|
|
229
|
+
The worker executes at most 64 eligible entries from one validated journal snapshot, commits ordinary completions through one journal transaction, then yields through a generation-checked event-loop boundary. Retry, queue, or prior-generation boundaries first flush completed ids and force a fresh snapshot, preserving exact state-transition atomicity without per-entry parse/rewrite churn. A deterministic 2,048-entry stress gate requires exactly 32 completion publications, 33 reads including the final empty snapshot, continued 1ms timer progress, and less than 250ms maximum observed heartbeat delay. Byte-capacity tests cover failed and retry-wait diagnostics, queue receipt/owner and handoff metadata, and operator dispositions; every rejected growth leaves the prior authority bytes unchanged. It still scans later independent entries after retry or terminal persistence. An unresolved reaction remains a queue-mutation dependency even in `retry-wait` or `failed`, but dispatch checks that dependency against the candidate queue item's exact chat and source message ids instead of globally blocking unrelated targets. Successful replay or an exact discard disposition releases the dependency. Worker state, debug status, state snapshots, and redacted runtime events expose journal depth, retry/terminal counts, the next retry, latest terminal identity, copyable operator commands, and the exact first foreign queued owner identity (instance, PID/birth, session, and acquisition) when semantic authority belongs to another process.
|
|
230
|
+
|
|
231
|
+
Upgrades create journals lazily before the first post-upgrade offset advance. A bot/profile identity change with unresolved authority fails closed. Once reconstructed authority is empty, the next read atomically rebinds profile and bot identity under the journal transaction and removes redundant old-identity segments best-effort; stable-`botId` token rotation remains valid even with entries. Downgrading below `0.28.0` with a non-empty journal is unsafe because the older runtime cannot drain updates whose Telegram offsets already advanced. Run `node scripts/check-downgrade.mjs [agent-dir]`; a blocked result requires draining with a compatible `0.28.x` runtime, while a safe result confirms all reconstructed journal authority is empty before downgrade. Runtime state from an older release must recover without deleting `telegram.json`, ownership state, or journal authority.
|
|
232
|
+
|
|
233
|
+
Polling and inbound-worker diagnostics remain separate so an executing, deferred, locally queued, foreign-queued, or blocked journal head cannot masquerade as a stalled `getUpdates` request.
|
|
189
234
|
|
|
190
235
|
Long-text split recovery remains conservative: only human text at or above the near-limit threshold opens its debounce window. Forward annotation has two semantic layers: the forward owns its source text/caption/media, while an optional separate owner-authored annotation normally precedes it. A bounded one-second pairing window joins that annotation and adjacent forward in either transport order, including a media-only forward without source caption text; the matching opposite-kind message flushes immediately. Same-kind rapid messages, commands, bots, ordinary non-forward captions, media groups, different senders/targets, reversed ids, and distant message ids do not enter this pairing path. Prompt construction always places the owner annotation first, followed by `[forward|from:...]` with the forward's own source text/caption, then source-attributed forwarded attachments, regardless of arrival order.
|
|
191
236
|
|
|
@@ -222,7 +267,7 @@ For a configured Rich response with final text and exactly one supported queued
|
|
|
222
267
|
|
|
223
268
|
### Controls And Menus
|
|
224
269
|
|
|
225
|
-
Telegram controls execute through command/callback domains, not by entering the normal prompt queue unless they intentionally create a prompt turn.
|
|
270
|
+
Telegram controls execute through command/callback domains, not by entering the normal prompt queue unless they intentionally create a prompt turn. Built-in read-only menu commands are admitted once required local state mutation finishes: first-user pairing still persists before `/start` is accepted, while menu rendering and BotFather command synchronization run as context-fenced best-effort effects with diagnostic failure sinks. Their unresolved Telegram calls therefore cannot retain the durable polling offset or prevent the next `getUpdates` request. Detached effects, deferred dispatch/watchdog, typing, and diagnostics callbacks contain primary and diagnostic failure; stale typing context is ignored, while snapshot publication serializes one write plus one retained coalesced rerun. Raw companion handlers still run before durable built-in routing and should return quickly even though their execution no longer retains polling.
|
|
226
271
|
|
|
227
272
|
Immediate controls:
|
|
228
273
|
|
|
@@ -309,9 +354,9 @@ The bridge does not mirror arbitrary `ctx.ui.confirm/input/select/custom` prompt
|
|
|
309
354
|
|
|
310
355
|
Status rendering distinguishes connected, active, dispatching, queued, tool-running, model-switching, and compacting states. If a queue mutation removes the last waiting item while Telegram-owned work still has running tools, status remains active instead of degrading to connected.
|
|
311
356
|
|
|
312
|
-
Queue reactions are shortcut controls for waiting turns.
|
|
357
|
+
Queue reactions are reversible shortcut controls for waiting turns. The runtime reconciles each complete `MessageReactionUpdated.new_reaction` set: any removal reaction (`๐`, `๐ป`, `๐`, `๐ฉ`, `๐`) suppresses the governed prompt without discarding its queue authority; otherwise any promotion reaction (`๐`, `โก๏ธ`, `โค๏ธ`, `๐`, `๐ฅ`) moves it to priority; otherwise it returns to the default lane. Suppressed prompts remain visible in the queue menu, survive authenticated queue handoff, and do not block unrelated dispatch. Reaction changes first flush a matching delayed text or media group so the governed turn exists before mutation. Once Pi has consumed a prompt, reactions cannot retract it.
|
|
313
358
|
|
|
314
|
-
`/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. The compact TUI status renders only `error`; detailed failure text remains in diagnostics and profile-scoped logs instead of expanding the status line.
|
|
359
|
+
`/telegram-status` records grouped diagnostics for transport/API, polling/update, prompt dispatch, controls, typing, compaction, setup, session lifecycle, attachment queue/delivery, and recent redacted runtime events. Polling diagnostics expose the exact phase, phase start, current update, last successful `getUpdates` response, and stop reason; outbound success never substitutes for inbound progress. Expected preview noise such as unchanged edit responses is filtered out. The compact TUI status renders only `error`; detailed failure text remains in diagnostics and profile-scoped logs instead of expanding the status line.
|
|
315
360
|
|
|
316
361
|
Complete intermediate assistant text blocks from Telegram-originated activity are sent once to the immutable originating target before active-turn final delivery; final and terminal-partial segments stay with settlement so replies are not duplicated. When `assistant.proactivePush` is enabled and this instance has exact direct or follower transport authority, completed public blocks from local/autonomous work are also sent once and in source order to the instance's authorized target. Both paths use the configured Rich or HTML renderer and exclude reasoning, tool traffic, token deltas, local prompt text, unknown sources, and stale generations. Each admitted block remains fenced to its exact target, profile/token stamp, leader epoch or follower registration generation, and session generation; non-idempotent acknowledgement ambiguity never authorizes replay.
|
|
317
362
|
|
|
@@ -85,6 +85,8 @@ tmp/telegram/owners.json / <profile-slot> -> bus leader identity + heartbeat
|
|
|
85
85
|
|
|
86
86
|
Followers do not poll. They register with the leader and receive routed inbound updates from it. Followers still own their local queue, active-turn state, previews, final delivery planning, model switches, and Pi lifecycle. The leader owns only Telegram transport and update fanout. Pi session replacement (`new`) changes follower agent context, not bus membership: a registered follower preserves its registration and refreshes the live context instead of disconnecting. The Telegram bus belongs to the local set of cooperating visible Pi instances rather than to the first terminal session forever: if the visible terminal leader exits, a live registered follower can take over leadership.
|
|
87
87
|
|
|
88
|
+
A follower may route Bot API work or capability checks only after authenticated registration; an unregistered process that does not own the direct transport lock remains transport-passive. Follower request settlement remains owned by the existing local IPC operation budget rather than being inferred from Bot API method names; followers never invoke `getUpdates`.
|
|
89
|
+
|
|
88
90
|
## Target Abstraction
|
|
89
91
|
|
|
90
92
|
The bridge uses a first-class target abstraction:
|
|
@@ -159,6 +161,22 @@ Followers first try to re-register after leader reload or unknown-heartbeat resp
|
|
|
159
161
|
|
|
160
162
|
## Leader/Follower Communication
|
|
161
163
|
|
|
164
|
+
### Protocol identity and compatibility
|
|
165
|
+
|
|
166
|
+
The local wire contract has protocol version `1`, independent from the npm package version. Follower registration and the leader acknowledgement carry `{ protocolVersion, runtimeBuild, capabilities }`. Capability names are canonical, unique, and sorted. A leader rejects missing or mismatched protocol identity before provisioning a target or publishing the follower into live routing; a strict follower likewise rejects an acknowledgement without compatible leader identity. Different package builds remain compatible when their protocol versions agree. `durable-follower-admission-v1` gates source forwarding, while `queue-handoff-v1` independently gates live semantic queue transfer; every participant in a routed handoff must advertise it.
|
|
167
|
+
|
|
168
|
+
Negotiated identities remain on the live follower registry and appear in `/telegram-status --debug` plus the observational state snapshot. The Threaded Mode capability monitor owns one in-flight probe across lifecycle generations: stop/restart invalidates a late read, and a replacement monitor waits for the previous request to settle instead of creating overlapping transport transitions. Durable follower admission is authorized only when both peers advertise `durable-follower-admission-v1`, never inferred from package version: a capable runtime rejects missing support before provisioning, inbound routing, or election-roster eligibility. Authentication and exact registration generation remain mandatory independently of protocol compatibility. `follower.register` is the sole bootstrap request and must carry a fresh generation before provisioning; every other request is exact-generation-fenced against a live registry entry. `bus.ack` is response-only and is rejected if submitted as a server request. Leader forwarding never synthesizes authority for an unknown recipient and preserves the follower's exact durable receipt end to end.
|
|
169
|
+
|
|
170
|
+
Foreign update forwarding returns an explicit `accepted`, `retryable`, or `terminal-rejected` settlement. Acceptance requires an acknowledgement for the exact request whose receipt contains the expected stable `deliveryId` and source `update_id`; a callback error popup never substitutes for that receipt. Missing or negative acknowledgements, stale registrations, absent follower context, binding rejection, journal admission failure, and missing or mismatched receipts all retain the leader source. Message, edited-message, reaction, and callback paths share this contract.
|
|
171
|
+
|
|
172
|
+
The delivery id excludes the replaceable runtime instance and registration generation: it derives from envelope kind, source `update_id`, and the stable manual-follower binding. Stored message ownership carries that binding and may rebind to the current authenticated registration after follower replacement, preserving one retry identity while still fencing each attempt by the current generation. A lost acknowledgement can therefore replay idempotently into the follower journal and becomes accepted only when the exact durable receipt returns.
|
|
173
|
+
|
|
174
|
+
Transport leadership does not own already-queued semantics. Each queued journal receipt binds the acquiring runtime instance, OS process birth, session generation, and acquisition. A replacement leader or follower process sees another live process's receipt as foreign and cannot replay or settle it; the original process may complete its local Pi queue after transport moves. Startup preserves foreign and legacy unowned receipts. Before a replacement admission worker starts, tri-state pid/process-birth proof: an absent PID or mismatched stable Linux/macOS birth identity permits recovery, while a live matching owner stays `alive` and Windows or inaccessible birth metadata stays `unverifiable`; both non-dead outcomes preserve the receipt may transactionally recover a dead owner's complete receipt to pending, after which replacement replay creates fresh queue authority. Registration publishes the replacement's exact pid/process-birth identity first; a concurrent recovery that observes it returns `owner-alive`, while live or unverifiable owners remain untouched.
|
|
175
|
+
|
|
176
|
+
Live-process handoff combines journal CAS with authenticated bounded bus payloads. A donor-generated one-time token is hashed together with the exact receipt, donor acquisition, and recipient runtime/process/session identity. Offering retains donor ownership but freezes donor completion/discard and dead-owner recovery. Prompt payloads serialize queue data; control payloads serialize only stable `status`/`model` identity and rebuild closures at the recipient. Leader and follower receivers require exact live donor/recipient registration generations, reject malformed/oversized payloads, stage one complete receipt idempotently, accept its journal authority, and acknowledge only that exact receipt plus newly minted owner. Receipts name their source journal binding, while the donor derives the recipient follower-journal binding from the authenticated stable profile. The recipient accepts only that matching active lifecycle. Production advertises `queue-handoff-v1` with this exact role/path composition; legacy-unbound or unavailable bindings fail closed.
|
|
177
|
+
|
|
178
|
+
As part of staging, the exact recipient presents the token and atomically receives a fresh acquisition carrying the handoff digest; donor settlement is then stale, and the donor trusts only an ACK carrying that accepted owner. Registration carries recipient process-birth and session generation so journal authority matches the live runtime exactly. The coordinator contract shares one ordering across leaderโfollower and followerโfollower paths: offer, route bounded payload, require the exact staged-and-accepted receipt-and-owner ACK, remove donor work, then publish recipient dispatch readiness. Negative or mismatched acknowledgement cancels the still-unaccepted offer and retains donor work. A lost acknowledgement after recipient acceptance cannot revoke the accepted owner; cancellation fails closed and donor memory remains frozen for exact accepted-owner reconciliation. Repeated exact acceptance is idempotent; a different token cannot claim the accepted receipt. Queue authority has no elapsed-time lease: only exact handoff, owner action, or transaction-rechecked PID/process-birth death proof may move it.
|
|
179
|
+
|
|
162
180
|
Implemented transport:
|
|
163
181
|
|
|
164
182
|
### Local IPC endpoint with bounded native paths
|
|
@@ -207,7 +225,7 @@ Current portability audit:
|
|
|
207
225
|
- Ownership/config/state/temp files: path construction uses `path.join`/`path.resolve` under the Pi agent directory. File permission calls remain best-effort private-mode hardening; native Windows may emulate POSIX modes, so broad Windows ACL auditing is outside this extension's current local-bus baseline.
|
|
208
226
|
- Process liveness: lock ownership uses `process.kill(pid, 0)`, which Node supports on Windows for existence checks. Cross-user permission failures are treated as alive, matching Unix semantics.
|
|
209
227
|
- Shell/provider commands: outbound handler command templates remain operator-configured and platform-dependent; Threaded Mode bus portability does not guarantee every configured STT/TTS/shell provider is Windows-native.
|
|
210
|
-
- Manual follower identity: process ids are local liveness hints, paired with OS process-birth metadata where available rather than treated as cross-machine identifiers. Linux uses `/proc` start ticks and macOS uses the parent process start time
|
|
228
|
+
- Manual follower identity: process ids are local liveness hints, paired with OS process-birth metadata where available rather than treated as cross-machine identifiers. Linux uses `/proc` start ticks and macOS uses the parent process start time. Fallback generation strings identify a runtime but are not independent death proofs: Windows or inaccessible process-birth metadata is `unverifiable` while the PID remains live. A fresh authenticated session handoff carries the previous runtime identity and exact target through initial registration, allowing fallback identities to migrate without provisioning another thread.
|
|
211
229
|
|
|
212
230
|
Remaining risk is live native Windows behavior: named-pipe creation/connect timing, antivirus/firewall/ACL interference, and provider command availability need operator smoke evidence.
|
|
213
231
|
|
|
@@ -358,7 +376,7 @@ Rules:
|
|
|
358
376
|
Current state under the agent dir:
|
|
359
377
|
|
|
360
378
|
- `tmp/telegram/owners.json`: authoritative extension-local transport owners keyed by `default` or named profile. Each owner contains the bus leader identity, capability secret, heartbeat, generation, and cleanup fencing epoch. Mutations serialize through `owners.json.transaction`; followers never write owner slots. The local bus endpoint is derived from the agent directory by default; legacy `busSocketPath` entry fields are tolerated inside current owner records but are not required.
|
|
361
|
-
- `tmp/telegram/state.json`: volatile extension+bot observable/debug snapshot, not routing authority. It writes `source: "snapshot"` and `writtenAtMs` so consumers do not confuse it with an authoritative database. Every process on one Telegram profile reads this shared path, but only the active transport owner may persist it; followers become writers only after promotion. Status-only persistence refreshes disk-backed bindings before serialization so an already-loaded stale view cannot erase newer leader records. It mirrors `/telegram-status`-style projections: top-level `bot` stores bot-wide capability state such as `threadMode: "unknown" | "enabled" | "disabled"
|
|
379
|
+
- `tmp/telegram/state.json`: volatile extension+bot observable/debug snapshot, not routing authority. It writes `source: "snapshot"` and `writtenAtMs` so consumers do not confuse it with an authoritative database. Every process on one Telegram profile reads this shared path, but only the active transport owner may persist it; followers become writers only after promotion. Status-only persistence refreshes disk-backed bindings before serialization so an already-loaded stale view cannot erase newer leader records. It mirrors `/telegram-status`-style projections: top-level `bot` stores bot-wide capability state such as `threadMode: "unknown" | "enabled" | "disabled"`; `runtime` identifies leader/follower role, lifecycle activity, and the exact polling phase/progress snapshot; `liveRoster` mirrors followers/current targets/reservations; `diagnostics` mirrors status/debug signals; `threads` stores current routeable bindings; `bot.lastSlot` stores the compact slot cursor used when all current threads are gone; and `reservations` records short-lived slot collision guards.
|
|
362
380
|
- Local bus endpoints: Unix-like platforms expose stable `tmp/telegram/bus.sock` and `tmp/telegram/followers/*` symlinks backed by private generation sockets; native Windows uses deterministic named pipes under `\\.\pipe\pi-telegram-...`. These are transient IPC endpoints, not durable routing state.
|
|
363
381
|
|
|
364
382
|
If an unclean host shutdown truncates `owners.json`, a profile `state*.json`, or the ownership transaction guard, `/telegram-connect` classifies the damage before recovery. With no verifiable live owner, one cross-process recovery winner quarantines only those damaged disposable artifacts and startup retries once; followers or leaders appearing during the final guarded reread stop the reset. `telegram.json`, `logs*.jsonl`, other profiles' valid state, and unrelated extension data remain untouched. A blocked or failed reset reports which Pi must restart instead of emitting repeated raw parse/transaction errors.
|
|
@@ -384,14 +402,14 @@ All files containing routing, chat ids, thread ids, or process details use priva
|
|
|
384
402
|
|
|
385
403
|
### Follower heartbeat is missed
|
|
386
404
|
|
|
387
|
-
- Leader prunes the follower from the live registry after missed heartbeats, but heartbeat pruning is only immediate liveness bookkeeping.
|
|
405
|
+
- Leader prunes the follower from the live registry after missed heartbeats, but heartbeat pruning is only immediate liveness bookkeeping. One leader generation owns at most one prune operation; stop makes late endpoint, policy, and cleanup settlement inert, while durable-profile mutation serialization prevents replacement registration from crossing confirmed-dead cleanup.
|
|
388
406
|
- A missed heartbeat does not delete, close, mark offline, or send a disconnected notice for the follower's Telegram thread binding because the common cause may be leader reload, IPC handoff, or transient reconnect rather than a dead follower.
|
|
389
407
|
- Followers treat rejected/missing heartbeat acknowledgements as registration loss: retain the last known target locally, clear registered truth, try to re-register with the current leader, wait a short leader-reload grace window, and retry. They promote only after the exact leader lease becomes stale or inactive; a live owner with an unreachable endpoint leaves the follower disconnected/retrying rather than creating a competing poller.
|
|
390
408
|
- Persisted current manual-follower bindings survive abrupt process absence as restoration hints when Thread cleanup is disabled. When enabled, graceful Pi quit requests exact-generation teardown before lifecycle suspension; if that envelope is missed, stale pruning may delete only after the leader's OS confirms the exact registered PID has exited.
|
|
391
409
|
- Fresh registration sends one compact connected notice in the assigned thread. An exact immediate session handoff uses a target-scoped `sendChatAction` as its synchronous visibility probe, avoiding a duplicate notice while retaining stale/ambiguous recovery; other cross-session restoration keeps the connected notice as its probe.
|
|
392
410
|
- Registration requires a present generation, and explicit disconnect requires that same exact live generation. Leader-side registration and disconnect mutations serialize per durable follower profile across old and replacement runtime instance IDs, so a replacement registration cannot overtake awaited destructive cleanup and an old disconnect cannot remove its successor's routing authority.
|
|
393
|
-
- Successful forwarded updates and follower-originated API calls refresh liveness, so active followers are not pruned only because the interval heartbeat tick lagged.
|
|
394
|
-
- Destructive follower thread teardown belongs to confirmed `/telegram-disconnect`, graceful Pi quit, or confirmed reconciliation actions, not generic heartbeat pruning. Manual disconnect retains its destructive confirmation; quit
|
|
411
|
+
- Successful forwarded updates and follower-originated API calls refresh liveness, so active followers are not pruned only because the interval heartbeat tick lagged. Each follower lifecycle owns at most one in-flight heartbeat for its exact registration generation; stop/replacement makes late settlement inert, and recovery or diagnostic failure cannot escape as an unhandled interval Promise.
|
|
412
|
+
- Destructive follower thread teardown belongs to confirmed `/telegram-disconnect`, graceful Pi quit, or confirmed reconciliation actions, not generic heartbeat pruning. Manual disconnect retains its destructive confirmation and clears restart ownership; quit deletes the tab without prompting when Thread cleanup is enabled (default) but preserves the owner slot independently so a same-directory restart can reclaim leadership. Confirmed leader/follower teardown first persists an exact target/runtime-generation cleanup intent. The active leader attempts deletion under its current epoch; interruption preserves the intent so that leader or a successor can replay it under current authority, and confirmed deletion removes the binding plus intent in the same persisted state transition. If the graceful request is missed, stale heartbeat plus OS-confirmed absence of the exact registered PID may authorize the same cleanup while enabled; this action serializes ahead of replacement registration. Disabled cleanup, silence, heartbeat expiry alone, IPC/auth failure, and live or unknown process liveness remain non-destructive. Incomplete cleanup preserves durable intent for retry. A promoted leader uses its current owned leader epoch even when the inherited record still carries a historical `manual-follower` owner label.
|
|
395
413
|
- Explicit stale/deleted/offline observations invalidate reuse. Process absence affects reuse only through the enabled, exact-PID confirmed-dead cleanup path.
|
|
396
414
|
|
|
397
415
|
### Thread is deleted
|
package/docs/public-api.md
CHANGED
|
@@ -42,7 +42,7 @@ Stable commands inside Pi:
|
|
|
42
42
|
- `/telegram-setup` โ configure/update the bot token.
|
|
43
43
|
- `/telegram-connect` โ start polling here and acquire external Telegram control ownership. Accepted queue/reply state stays local if ownership later moves elsewhere.
|
|
44
44
|
- `/telegram-disconnect` โ after destructive confirmation, stop polling and release ownership without deleting or silencing accepted local queue state. In Threaded Mode it deletes this instance's current Telegram thread; a follower waits for its active leader to confirm generation-fenced cleanup before stopping. Graceful Pi `quit` performs the same teardown without prompting, while `reload`, `new`, `resume`, and `fork` preserve same-process handoff.
|
|
45
|
-
- `/telegram-status` โ show connection, polling, execution, queue, and recent event diagnostics.
|
|
45
|
+
- `/telegram-status` โ show connection, polling, execution, queue, and recent event diagnostics; debug output separates poller and worker progress, durable automatic-retry state, exact foreign queued-owner identity, and negotiated protocol/build/capabilities.
|
|
46
46
|
|
|
47
47
|
### Telegram commands
|
|
48
48
|
|
|
@@ -117,7 +117,7 @@ The file is global across Pi instances. Cooperating instances serialize recursiv
|
|
|
117
117
|
|
|
118
118
|
Hidden/default semantics are represented by absence:
|
|
119
119
|
|
|
120
|
-
- `threads.automaticCleanup` defaults to `true`; graceful Pi quit deletes the instance's bound Threaded Mode tab without prompting. Set it to `false`, or use `๐งน Thread cleanup` in Telegram Settings, to preserve the tab
|
|
120
|
+
- `threads.automaticCleanup` defaults to `true`; graceful Pi quit deletes the instance's bound Threaded Mode tab without prompting but preserves the owner slot as independent restart intent. Set it to `false`, or use `๐งน Thread cleanup` in Telegram Settings, to preserve the tab too. A confirmed `/telegram-disconnect`, unlike quit, clears restart ownership. Settings views and cleanup reload shared config before evaluating this switch, so another live Pi instance's update takes effect without restarting. Confirmed leader/follower teardown persists an exact target/runtime-generation cleanup intent before Telegram deletion; an interrupted attempt remains retryable by the current or successor leader under current authority and clears only after confirmed deletion. A same-profile replacement leader first adopts any still-active binding and cancels its superseded cleanup, so startup never deletes and recreates a reusable thread. If a follower's graceful envelope is missed, the leader may create the same fenced cleanup only after its heartbeat is stale, the OS confirms the exact registered PID no longer exists, cleanup remains enabled, and no replacement registration can overtake deletion. Heartbeat loss alone, live/unknown process liveness, IPC failure, and auth failure remain non-destructive. Invalid-config recovery makes the setting unresolved and therefore skips destructive cleanup. Manual `/telegram-disconnect` keeps its confirmation and teardown behavior regardless of this setting.
|
|
121
121
|
- Every complete intermediate assistant text block from a Telegram-originated turn is delivered once to its immutable target before the existing final reply. This active-turn commentary path remains enabled when `assistant.proactivePush` is `false`; final and terminal-partial segments stay with settlement to prevent duplicate replies. `assistant.proactivePush` defaults to `true` only for local/autonomous work: omit it to project every completed public block, including commentary and the final block, or set it explicitly to `false` to disable that projection. Both paths exclude token deltas, hidden reasoning, tool calls/arguments/results, empty blocks, unknown sources, and stale authority. Projection uses the configured Rich or HTML assistant renderer and binds admitted work to the exact target, profile/token transport generation, direct leader epoch or follower registration generation, and session generation. The old top-level `proactivePush` key is ignored; move the setting manually under `assistant`.
|
|
122
122
|
- `assistant.activity` accepts exactly `"quiet"`, `"thinking"`, `"tools"`, or `"verbose"`; omitted values default to `"verbose"`, explicit values remain unchanged, and invalid values fail closed to `"quiet"`. Each Pi process reloads the shared file-backed value at `agent-start`, so multi-instance activity isolation never relies on a stale process-local config snapshot. `thinking` shows only provider-exposed thinking, `tools` shows only completed tool activity, and `verbose` shows both. Thinking uses persistent ordinary HTML `sendMessage`/`editMessageText` disclosure with a standard expandable blockquote, a `๐ง ` header carrying the current Pi thinking level, and bounded redacted text whose inline Markdown renders as Telegram HTML. Tools use native Rich Messages with one header followed by separate closed details and JSON pre blocks for bounded redacted arguments, retained updates, and results/errors. Thinking disables link previews on every HTML send/edit and neutralizes HTTP(S) auto-link detection; Rich tool output disables automatic entity detection, with the same protections retained by its HTML fallback. Consecutive tools coalesce only inside the same ordered activity segment and bounded message. Legacy `assistant.activityVerbosity` is read only when `assistant.activity` is absent and is removed by the next Activity Settings write.
|
|
123
123
|
- Voice Reply `hidden`: no `voice.replyMode` key is persisted; legacy `manual` resolves to this silent default. `mirror` adds `[voice] delivery: automatic voice` only to voice/audio-input turns, while `always` adds the same effective line to every Telegram turn.
|
|
@@ -158,7 +158,7 @@ Low-level stable buses:
|
|
|
158
158
|
|
|
159
159
|
- `registerTelegramUpdateHandler()`
|
|
160
160
|
- Identity: no id.
|
|
161
|
-
- Purpose: observe or consume raw Telegram updates before default routing.
|
|
161
|
+
- Purpose: observe or consume raw Telegram updates before default routing. Its optional execution fence supplies cancellation and a required pre-effect authority check for long-running handlers.
|
|
162
162
|
- `registerTelegramInboundHandler()`
|
|
163
163
|
- Identity: no id.
|
|
164
164
|
- Purpose: generic Telegram-to-Pi transforms.
|
|
@@ -342,14 +342,15 @@ Contract:
|
|
|
342
342
|
|
|
343
343
|
## Updates
|
|
344
344
|
|
|
345
|
-
Import from `@llblab/pi-telegram/updates`.
|
|
345
|
+
Import `registerTelegramUpdateHandler`, `TelegramUpdateExecutionFence`, and the advanced `getTelegramUpdateExecutionFence`, `createTelegramUpdateExecutionFenceGuard`, `carryTelegramUpdateExecutionFence`, and `assertTelegramUpdateExecutionCurrent` helpers from `@llblab/pi-telegram/updates`.
|
|
346
346
|
|
|
347
347
|
```ts
|
|
348
|
-
const off = registerTelegramUpdateHandler(async (update) => {
|
|
348
|
+
const off = registerTelegramUpdateHandler(async (update, execution) => {
|
|
349
349
|
const data = (update as { callback_query?: { data?: string } }).callback_query
|
|
350
350
|
?.data;
|
|
351
351
|
if (!data?.startsWith("myext:")) return "pass";
|
|
352
|
-
await handleMyCallback(data);
|
|
352
|
+
await handleMyCallback(data, execution?.signal);
|
|
353
|
+
execution?.assertCurrent();
|
|
353
354
|
return "consume";
|
|
354
355
|
});
|
|
355
356
|
```
|
package/docs/ui-style.md
CHANGED
|
@@ -54,7 +54,7 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
|
|
|
54
54
|
| `โก๏ธ` | Choose replacement target | Thread replace/restore target buttons that select which Pi instance should move to the current thread | Use inside the second replace/restore chooser, not for ordinary reroutes. |
|
|
55
55
|
| `โ๏ธ` | Activate / choose this item | Model detail activation action, generated button-only choice heading | Positive selection cue; use `๐ข Active` for already-current state. |
|
|
56
56
|
| `โ` | No / cancel | Confirmation cancel buttons | Use for safe cancellation, not destructive removal. |
|
|
57
|
-
| `๐` | Delete /
|
|
57
|
+
| `๐` | Delete / suppress | Queue delete actions, destructive confirmations, suppression reaction | The explicit queue button deletes; the reaction reversibly suppresses a waiting turn. |
|
|
58
58
|
|
|
59
59
|
### State Indicators And Button Grammars
|
|
60
60
|
|
|
@@ -77,11 +77,11 @@ Queue reactions are shortcut controls for waiting turns. Preserve their semantic
|
|
|
77
77
|
| `โค` / `โค๏ธ` | Promote to priority | Queue reaction shortcut | Normalize display consistently where code normalizes reactions. |
|
|
78
78
|
| `๐` / `๐๏ธ` | Promote to priority | Queue reaction shortcut | Soft/peaceful promotion gesture. |
|
|
79
79
|
| `๐ฅ` | Promote to priority | Queue reaction shortcut | Urgent/hot promotion gesture. |
|
|
80
|
-
| `๐` |
|
|
81
|
-
| `๐ป` |
|
|
82
|
-
| `๐` |
|
|
83
|
-
| `๐ฉ` |
|
|
84
|
-
| `๐` |
|
|
80
|
+
| `๐` | Suppress waiting turn | Queue reaction shortcut and suppressed queue badge | Suppression is reversible and is not negative feedback to the agent. |
|
|
81
|
+
| `๐ป` | Suppress waiting turn | Queue reaction shortcut and suppressed queue badge | Disappear/suppress metaphor. |
|
|
82
|
+
| `๐` | Suppress waiting turn | Queue reaction shortcut and suppressed queue badge | Reversible cancel metaphor. |
|
|
83
|
+
| `๐ฉ` | Suppress waiting turn | Queue reaction shortcut and suppressed queue badge | Reversible reject metaphor. |
|
|
84
|
+
| `๐` | Suppress or explicitly delete | Queue reaction shortcut, suppressed queue badge, and queue delete UI | The reaction is reversible suppression; only the explicit queue button is destructive. |
|
|
85
85
|
|
|
86
86
|
### Decorative Or Local-Example Emoji
|
|
87
87
|
|