@llblab/pi-telegram 0.27.9 → 0.27.11

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 CHANGED
@@ -91,7 +91,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
91
91
  - Telegram extension work should not hold Pi's core agent lifecycle hostage once Pi has produced the semantic result. For Telegram-originated turns, final Telegram delivery, attachment upload, and transport cleanup are extension-owned side effects; schedule them off the critical `agent_end` path when ordering can still be preserved, record failures as runtime events, and keep dispatch of the next Telegram turn behind the delivery task when needed to avoid interleaving previews/finals. Treat a low-level error `agent_end` as retryable while its Telegram turn remains active; only a later successful `agent_end` or Pi's `agent_settled` boundary may finalize and release that turn. Public activity handlers run through isolated asynchronous per-handler queues; lifecycle hooks enqueue normalized events and never await consumer work. Core assistant-output projection always delivers each completed `intermediate` Telegram-originated text segment once to its immutable turn target before final delivery; final and terminal-partial segments remain with settlement to prevent duplication. Proactive push defaults enabled for local/autonomous work and requires explicit `assistant.proactivePush: false` to opt out of only that source class. Bind every admitted block to the exact target, profile/token transport stamp, direct leader epoch or follower registration generation, and session generation. Token deltas, hidden reasoning, tool traffic, unknown sources, and stale authority never enter projection.
92
92
  - Core thinking/tool visibility is controlled by file-backed `assistant.activity: "quiet" | "thinking" | "tools" | "verbose"`; every Pi instance reloads the shared config at `agent-start` before admitting activity so process-local cache cannot broaden the selected class. It defaults to `verbose` when absent and remains separate from proactive public prose and assistant Markdown: provider-exposed thinking uses only a durable ordinary HTML expandable blockquote, while completed executed tools use native Rich Messages whose closed `<Tool>: <status>` root details replace snake-case separators with spaces, title-case each word, and preserve an uppercase initial two- or three-letter repeated prefix per word, then contain an open-by-default `arguments` child plus closed `update`, `result`, or `error` children with lowercase monospaced labels and JSON pre blocks; treat each label as one quote-free outer JSON key and add no icon, list marker, or heading emphasis. Thinking renders inline Markdown as Telegram HTML without spending a separate header row; `verbose` enables both classes, and every activity send/edit disables Telegram link previews while technical evidence also neutralizes HTTP(S) auto-link detection as a client-safe fallback. Keep it compact by default, exact-target/transport-generation fenced, start-order preserving, boundary-aware, memory/transport bounded, and non-blocking; final delivery waits for admitted technical activity only inside the extension-owned background task, and session replacement abandons queued old-generation work without waiting on old transport. Read legacy `assistant.activityVerbosity` only when `assistant.activity` is absent and remove it on the next Settings write. Never invent unavailable provider-private thinking or replay a possibly committed technical message.
93
93
  - Telegram runtime state should be treated as event-driven reconciliation of extension assumptions against observable Telegram signals, not as a full Telegram bot read-model and not as a reason to query Telegram on every action. Keep a local assumption model for bot identity/capabilities, pairing assumptions, thread support, known thread lifecycle, bound instance targets, reservations, and transport health. Invalidate and reconcile the relevant slice on meaningful events: startup/reload, lifecycle service messages, stale-send/API errors, setup/pairing changes, follower register/prune, explicit status/debug requests, and bounded low-frequency leader health ticks. `lib/sync.ts` owns sync slices, observation intake, invalidation triggers, status/debug freshness, and reconciliation scheduling; it must not promise complete bot-state mirroring because Bot API lacks a complete topic listing surface. `lib/thread-reconciler.ts` owns pure Threaded Mode lifecycle planning and should become the only policy authority for destructive thread cleanup decisions. `threads` owns current Telegram UI thread/tab binding primitives and thread-name helpers mapped to Bot API topic transport, `status` renders projections, and `index.ts` only wires ports. `tmp/telegram/logs.jsonl` is a session-local redacted runtime evidence stream for race debugging and resets on extension start / runtime scope changes; it is diagnostics only, not routing/provisioning authority. `state.json` should be an observable/debug snapshot aligned with `/telegram-status` (extension runtime, bot capabilities including `bot.lastSlot`, runtime role, live roster projection, reservations, diagnostics), not routing/provisioning authority. Because every process on one Telegram profile shares this file, only the active transport lock owner may persist it; followers read current state and gain write authority only after promotion. Status-only writes must refresh disk-backed bindings before serialization so a stale loaded snapshot cannot erase newer leader state. Live bus/runtime state is authoritative; file reservations and fresh capability observations may be startup hints/collision guards only; stale snapshots must re-probe before suppressing bus/topic behavior. Do not persist stale/offline/failed thread target history as source of truth.
94
- - Thread bindings are bidirectional. From the Pi-instance side, an instance knows and preserves its target, slot, thread name, lifecycle state, and direct-delivery defaults. From the Telegram-client/bot side, the bridge observes thread creation/close/reopen/stale-send signals and reconciles them into instance binding state. Lifecycle transitions that affect operator understanding should be Telegram-visible when a live transport remains: every successful follower registration/re-registration gets a compact connected notice in the assigned thread, while heartbeat pruning stays silent because it is only liveness suspicion. These notices should use the instance thread name when known and fall back to the slot letter only while the thread is still unnamed. New unknown or command-created temporary threads receive the complete forward plus replace/restore control set and remain preserved until the user chooses one. Successful forward deletes the chooser and temporary thread; successful replace/restore always deletes the chooser, rebinds the temporary thread, and deletes only the replaced old thread. Any destructive cleanup must go through `thread-reconciler`. Unknown `forum_topic_created` service events are observations, not destructive cleanup proof.
94
+ - Thread bindings are bidirectional. From the Pi-instance side, an instance knows and preserves its target, slot, thread name, lifecycle state, and direct-delivery defaults. From the Telegram-client/bot side, the bridge observes thread creation/close/reopen/stale-send signals and reconciles them into instance binding state. Lifecycle transitions that affect operator understanding should be Telegram-visible when a live transport remains: every fresh follower registration gets a compact connected notice in the assigned thread, while an exact immediate session handoff verifies the carried thread with a non-message chat action so it cannot duplicate that notice; heartbeat pruning stays silent because it is only liveness suspicion. These notices should use the instance thread name when known and fall back to the slot letter only while the thread is still unnamed. New unknown or command-created temporary threads receive the complete forward plus replace/restore control set and remain preserved until the user chooses one. Successful forward deletes the chooser and temporary thread; successful replace/restore always deletes the chooser, rebinds the temporary thread, and deletes only the replaced old thread. Any destructive cleanup must go through `thread-reconciler`. Unknown `forum_topic_created` service events are observations, not destructive cleanup proof.
95
95
  - The multi-instance bus uses private bot DMs with Telegram private-chat Threaded Mode enabled for the bot. No supergroup, group, or forum configuration is needed; the thread chat is always the private chat with the paired owner. Threaded capability checks must use bot/private-chat evidence such as `getMe.has_topics_enabled`, incoming `message_thread_id`, and topic operation success/failure; do not use group chat metadata as the control-plane truth for Telegram private-chat Threaded Mode.
96
96
  - Target-scoped behavior must preserve `{ chatId, threadId? }` through inbound messages/edits/callbacks/reactions, thread lifecycle service messages, media and split-text grouping, queue mutations, active-turn cleanup, previews, reply deduplication, menus, sections, generated buttons, voice, attachments, and direct delivery. Threaded Mode replies must keep Telegram's reply affordance to the triggering message just like classic DM mode. In Telegram private-chat Threaded Mode, mobile Telegram has been live-verified to render `sendMessage` replies that include top-level `message_thread_id` plus same-chat `reply_parameters.message_id`; Telegram Desktop may fail to show the visual reply header for the same correct payload, so do not treat Desktop-only absence as bridge failure without mobile/payload evidence.
97
97
  - Local bus IPC is a local-only trust boundary: private socket directories/files are necessary but not the whole authorization model; never let arbitrary local processes inject prompts, callbacks, API sends, or artifact routes. Ordinary follower Bot API sends remain assigned-target scoped; runtime-marked `telegram_message` cross-target sends may address only a different thread in the same paired chat, and the leader must strip that internal authorization marker before Telegram transport.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.27.11: Follower Connection Notice Hotfix
4
+
5
+ - `Follower Connection Notice`: An exact immediate follower session handoff now verifies its carried thread with a target-scoped chat action instead of sending the connected notice again. Impact: fresh follower startup still announces the assigned instance once, while the automatic session replacement no longer produces duplicate `Instance <name> connected` messages.
6
+ - `Handoff Coverage`: A regression now exercises fresh thread provisioning followed by exact session handoff and requires one visible connected notice plus a non-message visibility probe. Impact: target validation and stale-thread recovery remain intact without reintroducing duplicate chat bubbles.
7
+
8
+ ## 0.27.10: Leader Thread Rebind Hotfix
9
+
10
+ - `Live Leader Target`: Reclaiming or replacing the current leader thread now updates the process-local leader identity immediately after the durable binding commits. Impact: live thread-name resolution no longer returns the deleted previous leader target, so another instance can address the rebound leader without receiving `message thread not found`.
11
+ - `Rebind Coverage`: The explicit new-slot reroute regression now requires the durable record, live leader target, slot, and thread name to converge before old-thread cleanup. Impact: future Thread reconciler changes cannot update persisted state while leaving agent-message routing on a stale target.
12
+
3
13
  ## 0.27.9: Symmetric Agent-Turn Metadata Hotfix
4
14
 
5
15
  - `Agent-Turn Prompt Shape`: Cross-instance turns now fold trusted source-thread attribution into the canonical Telegram prefix as `[telegram|thread:<destination>|from-thread:<source>] <message>` instead of emitting a separate `[agent|from-thread:<source>]` block. Impact: inter-thread prompts match the existing compact metadata grammar without allowing ordinary Telegram text to impersonate transport attribution.
@@ -388,7 +388,7 @@ All files containing routing, chat ids, thread ids, or process details use priva
388
388
  - 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
389
  - 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
390
  - 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
- - Fresh registration sends a compact connected notice in the assigned thread; cross-session restoration uses that same notice as the visibility probe and follows the stale/ambiguous recovery contract defined above.
391
+ - 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
392
  - 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
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
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 performs the same action without prompting when Thread cleanup is enabled (default). 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.
package/index.ts CHANGED
@@ -729,6 +729,7 @@ export default function (pi: Pi.ExtensionAPI) {
729
729
  recordMessageOwnership: messageOwnershipRuntime.recordRouted,
730
730
  ...inboundBusProjectionRuntime,
731
731
  getCurrentLeaderEpoch,
732
+ setCurrentLeaderIdentity: telegramBusLeaderState.set,
732
733
  getThreadReconciliationMachineState: threadReconciliationRuntime.getState,
733
734
  recordThreadReconciliationPlan,
734
735
  handleTelegramTopicLifecycleUpdate: topicLifecycleSync,
package/lib/bus-leader.ts CHANGED
@@ -523,6 +523,12 @@ export function createTelegramBusFollowerTargetProvisioner(
523
523
  }
524
524
  const probeRequiredRecord =
525
525
  reconnectRecord?.status === "probe-required";
526
+ const exactSessionHandoff =
527
+ crossSessionReuse &&
528
+ !!requestedTarget &&
529
+ registration.previousInstanceId === reconnectRecord?.instanceId &&
530
+ requestedTarget.chatId === reconnectRecord.target.chatId &&
531
+ requestedTarget.threadId === reconnectRecord.target.threadId;
526
532
  const requiresVisibilityProbe =
527
533
  crossSessionReuse ||
528
534
  probeRequiredRecord ||
@@ -538,12 +544,21 @@ export function createTelegramBusFollowerTargetProvisioner(
538
544
  : undefined;
539
545
  if (requiresVisibilityProbe && connectedAnnouncement) {
540
546
  try {
541
- await deps.callApi("sendMessage", {
542
- chat_id: connectedAnnouncement.target.chatId,
543
- message_thread_id: connectedAnnouncement.target.threadId,
544
- text: connectedAnnouncement.text,
545
- parse_mode: connectedAnnouncement.parseMode,
546
- });
547
+ await deps.callApi(
548
+ exactSessionHandoff ? "sendChatAction" : "sendMessage",
549
+ exactSessionHandoff
550
+ ? {
551
+ chat_id: connectedAnnouncement.target.chatId,
552
+ message_thread_id: connectedAnnouncement.target.threadId,
553
+ action: "typing",
554
+ }
555
+ : {
556
+ chat_id: connectedAnnouncement.target.chatId,
557
+ message_thread_id: connectedAnnouncement.target.threadId,
558
+ text: connectedAnnouncement.text,
559
+ parse_mode: connectedAnnouncement.parseMode,
560
+ },
561
+ );
547
562
  if (recoverableTarget || probeRequiredRecord) {
548
563
  const activatedRecord = deps.topicTargetStore.upsert({
549
564
  ...result.record,
package/lib/routing.ts CHANGED
@@ -523,6 +523,11 @@ export interface TelegramInboundRouteRuntimeDeps<
523
523
  target: Queue.TelegramQueueTarget,
524
524
  ) => string | undefined;
525
525
  getCurrentLeaderEpoch?: () => number | string | undefined;
526
+ setCurrentLeaderIdentity?: (identity: {
527
+ target: Queue.TelegramQueueTarget;
528
+ slot?: string;
529
+ threadName?: string;
530
+ }) => void;
526
531
  getThreadReconciliationMachineState?: () =>
527
532
  ThreadReconciler.ThreadReconciliationMachineState | undefined;
528
533
  recordThreadReconciliationPlan?: (
@@ -1359,6 +1364,11 @@ export function createTelegramInboundRouteRuntime<
1359
1364
  rerouteConfirmedAtMs: nowMs,
1360
1365
  });
1361
1366
  await deps.threadStore.persist();
1367
+ deps.setCurrentLeaderIdentity?.({
1368
+ target: sourceTarget,
1369
+ slot,
1370
+ threadName,
1371
+ });
1362
1372
  if (deps.callApi) {
1363
1373
  try {
1364
1374
  await deps.callApi("editForumTopic", {
@@ -2340,6 +2350,11 @@ export function createTelegramInboundRouteRuntime<
2340
2350
  slot,
2341
2351
  });
2342
2352
  await deps.threadStore.persist();
2353
+ deps.setCurrentLeaderIdentity?.({
2354
+ target: { chatId: target.chatId, threadId: target.threadId },
2355
+ slot,
2356
+ threadName,
2357
+ });
2343
2358
  deps.recordRuntimeEvent?.(
2344
2359
  "bus",
2345
2360
  "Bus leader reclaimed unbound thread",
@@ -2449,6 +2464,11 @@ export function createTelegramInboundRouteRuntime<
2449
2464
  slot,
2450
2465
  });
2451
2466
  await deps.threadStore.persist();
2467
+ deps.setCurrentLeaderIdentity?.({
2468
+ target: { chatId: target.chatId, threadId: target.threadId },
2469
+ slot,
2470
+ threadName,
2471
+ });
2452
2472
  deps.recordRuntimeEvent?.(
2453
2473
  "bus",
2454
2474
  "Bus leader reclaimed stale-current unbound thread",
@@ -2515,6 +2535,11 @@ export function createTelegramInboundRouteRuntime<
2515
2535
  slot,
2516
2536
  });
2517
2537
  await deps.threadStore.persist();
2538
+ deps.setCurrentLeaderIdentity?.({
2539
+ target: { chatId: target.chatId, threadId: target.threadId },
2540
+ slot,
2541
+ threadName,
2542
+ });
2518
2543
  deps.recordRuntimeEvent?.(
2519
2544
  "bus",
2520
2545
  "Bus leader reclaimed unbound thread",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.27.9",
3
+ "version": "0.27.11",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"