@llblab/pi-kit 0.10.4 → 0.10.6

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/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.10.6 - 2026-09-12
6
+
7
+ - `Guest Placeholder And Delivery`: Advances the exact Telegram pin to `0.45.6`, so the guest ACK rotates the globe placeholder in whole cycles and holds `🌏 Working on it...`, guest answers are still delivered when a companion extension suppresses the final assistant message, `429` retry waits are recorded as `api` runtime events, the Thread display settings card drops the manual `/name` hint, and empty-queue headings carry no trailing period. The package set, resource inventory, and explicit load order remain unchanged.
8
+
9
+ ## 0.10.5 - 2026-09-11
10
+
11
+ - `State Flow Telegram Surface`: Advances the exact State Flow pin to `0.9.6`, so the submenu help line is exactly what State Flow records and when a new session resumes from the last committed point, and the state value (`off` or `#N`) renders in monospace. The package set, resource inventory, and explicit load order remain unchanged.
12
+
5
13
  ## 0.10.4 - 2026-09-11
6
14
 
7
15
  - `State Flow Telegram Surface`: Advances the exact State Flow pin to `0.9.5`, so the submenu help line opens directly with what State Flow records and when to start or stop it, without the former introductory prefix. The package set, resource inventory, and explicit load order remain unchanged.
package/README.md CHANGED
@@ -14,8 +14,8 @@ Package links lead to the owning repositories for usage, documentation, issues,
14
14
  | [`@llblab/pi-clean-room`](https://github.com/llblab/pi-clean-room) | `0.1.1` | Isolated nested Pi TUI with explicitly selected extensions |
15
15
  | [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.9.4` | Compact Codex/Spark subscription-limit status |
16
16
  | [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.8.1` | Visible continuation scheduling and bounded worker Skills |
17
- | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.9.5` | Atomic scoped state cohorts with final-eligibility resolution |
18
- | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.5` | Telegram companion, queues, files, voice, controls, and Generative Apps guidance |
17
+ | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.9.6` | Atomic scoped state cohorts with final-eligibility resolution |
18
+ | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.6` | Telegram companion, queues, files, voice, controls, and Generative Apps guidance |
19
19
  | [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
20
20
 
21
21
  Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
@@ -2,6 +2,11 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
+ ## 0.9.6: Monospaced state value in the submenu
6
+
7
+ - `Interop`: The submenu help line now consists solely of the operator-specified sentence "Records the latest accepted state after every turn, so a new session resumes from the last committed point."; the trailing "Start it to keep memory live on this branch, stop it to pause." sentence was removed.
8
+ - `Interop`: The submenu state line renders the live value in monospace — `off` or `#N` — while the section button keeps the same plain-text label.
9
+
5
10
  ## 0.9.5: Shorter submenu help line
6
11
 
7
12
  - `Interop`: The submenu help line now opens directly with "Records the latest accepted state after every turn…"; the "Durable memory for this conversation: State Flow" prefix was removed at the operator's request.
@@ -66,7 +66,7 @@ Stopping changes the current session/branch configuration, preserves semantic ch
66
66
 
67
67
  The compact status is an accent `state-flow` and dim `#<step>`. Detailed status distinguishes runtime config/meta from semantic materialization, reports scope keys, the selected temporal head and runtime revision, available hot offsets, per-scope retained patch tails, artifact counts, stale Markdown reasons, and pending publication. Retained tails may include inherited pre-origin records, so their counts are not active history depth. It labels global/CWD/session/effective state without dumping source bodies. Failed inspection means unavailable evidence, never a falsely clean count.
68
68
 
69
- When the optional `pi-telegram` extension is installed, State Flow appears exactly once in Telegram: the inline main menu places a `🌀 State Flow: #N` or `🌀 State Flow: off` section button directly before Settings, and that button's label carries the live status. The button opens a compact submenu whose header repeats the same state line, followed by a short explanation of what State Flow is; the only action is `Start` (or `Stop` while enabled), with the transport-owned `⬆️ Main menu` row above it. Starting while a run is active waits for that run to settle; stopping applies immediately. Registration fails open when the transport is absent, retries when the section registry becomes ready, and disposes on session shutdown; the `/state-flow-*` commands remain the authority and nothing changes for hosts without `pi-telegram`.
69
+ When the optional `pi-telegram` extension is installed, State Flow appears exactly once in Telegram: the inline main menu places a `🌀 State Flow: #N` or `🌀 State Flow: off` section button directly before Settings, and that button's label carries the live status. The button opens a compact submenu whose header repeats the same state line with the live value in monospace, followed by a short explanation of what State Flow is; the only action is `Start` (or `Stop` while enabled), with the transport-owned `⬆️ Main menu` row above it. Starting while a run is active waits for that run to settle; stopping applies immediately. Registration fails open when the transport is absent, retries when the section registry becomes ready, and disposes on session shutdown; the `/state-flow-*` commands remain the authority and nothing changes for hosts without `pi-telegram`.
70
70
 
71
71
  ## State, scope, and time
72
72
 
@@ -75,12 +75,22 @@ export interface StateFlowTelegramAdapter {
75
75
 
76
76
  /** Main-menu section label doubles as the live status value: the spiral identity is constant, the value is not. */
77
77
  export function formatStateFlowSectionLabel(snapshot: StateFlowTelegramSnapshot): string {
78
- return snapshot.enabled ? `🌀 State Flow: #${snapshot.step}` : "🌀 State Flow: off";
78
+ return `🌀 State Flow: ${stateFlowLabelValue(snapshot)}`;
79
+ }
80
+
81
+ /** Shared live value: plain in the button label, monospaced in the submenu state line. */
82
+ function stateFlowLabelValue(snapshot: StateFlowTelegramSnapshot): string {
83
+ return snapshot.enabled ? `#${snapshot.step}` : "off";
84
+ }
85
+
86
+ /** Submenu state line: the same identity as the button label, with the live value in monospace. */
87
+ function formatStateFlowSectionHeader(snapshot: StateFlowTelegramSnapshot): string {
88
+ return `<b>🌀 State Flow: <code>${stateFlowLabelValue(snapshot)}</code></b>`;
79
89
  }
80
90
 
81
91
  /** Short help under the state line: what State Flow is and why its action button exists. */
82
92
  const STATE_FLOW_SECTION_HELP =
83
- "Records the latest accepted state after every turn, so a new session resumes from the last committed point. Start it to keep memory live on this branch, stop it to pause.";
93
+ "Records the latest accepted state after every turn, so a new session resumes from the last committed point.";
84
94
 
85
95
  /** The submenu header repeats the button's state line; the single action matches the current state. */
86
96
  export function buildStateFlowSectionView(
@@ -91,7 +101,7 @@ export function buildStateFlowSectionView(
91
101
  ? { text: "⏹ Stop", callback_data: callbackData("stop") }
92
102
  : { text: "▶️ Start", callback_data: callbackData("start") };
93
103
  return {
94
- text: [`<b>${formatStateFlowSectionLabel(snapshot)}</b>`, "", STATE_FLOW_SECTION_HELP].join("\n"),
104
+ text: [formatStateFlowSectionHeader(snapshot), "", STATE_FLOW_SECTION_HELP].join("\n"),
95
105
  parseMode: "html",
96
106
  replyMarkup: { inline_keyboard: [[action]] },
97
107
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-state-flow",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "private": false,
5
5
  "description": "Incremental scoped state/context compiler for Pi, inspired by SKILL.state",
6
6
  "keywords": [
@@ -4,6 +4,14 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.45.6: Guest Placeholder And Delivery Hotfix
8
+
9
+ - `Guest Placeholder`: The guest ACK answers with the first bold globe frame (`🌎 Working on it.`) and rotates `🌍`/`🌏` once per second with the dots growing every two seconds. Rotation now completes whole 6-frame cycles and stops only after the 20 s minimum, so a pending answer holds the cycle's final frame (`🌏 Working on it...`) instead of cutting mid-step; a 26 s safety bound stays clear of the ~28 s Telegram flood-control wall, and error backoff and pre-replacement cancellation are unchanged.
10
+ - `Guest Answer Delivery`: The guest replacement now falls back to the run's latest completed assistant text when Pi's final assistant message is empty, so an answer preserved by a companion extension's fallback turn (for example State Flow's suppressed final:true patch turn) is still edited into the guest message instead of being skipped as "no editable text".
11
+ - `Rate-Limit Visibility`: A `429` retry wait now records an `api` runtime event with the method, wait duration, attempt, and server `retry_after`, so a silent flood-control pause before the final guest replacement is visible in the runtime log instead of appearing only as a frozen frame.
12
+ - `Thread Display Hint`: The Thread display settings card no longer carries the manual `/name Name` override hint; the current-value line, the three mode descriptions, and the live chooser are unchanged.
13
+ - `Queue Empty Headings`: The default empty-queue line and every rotating refresh title drop the trailing period, so the fully bold queue headings read as headings; wording, emoji, rotation order, callbacks, and refresh behavior are unchanged.
14
+
7
15
  ## 0.45.5: Queue Refresh Icon Hotfix
8
16
 
9
17
  - `Queue Refresh Icon`: The queue menu's Refresh row now uses `🔄`, the canonical refresh glyph, reserving `🌀` for the State Flow Telegram identity. The button label is the only change; queue refresh behavior, callbacks, and the rotating empty-queue notices are unchanged.
@@ -104,7 +104,7 @@ The menu may clear internal Skip but cannot remove a reaction created by the use
104
104
 
105
105
  ### Decorative Or Local-Example Emoji
106
106
 
107
- Some emoji are intentionally local examples or decorative variants, not global semantics. Empty-queue rotating messages (`🫙`, `🍃`, `🕳`, `🦗`, `🌙`, `🧘`, `🪐`, `🧺`, `🔭`, `🫧`, `🛸`) are copy flavor only and must not become controls. Example extension icons such as `🧪`, `🔧`, and `🗂` are documentation fixtures for companion extensions, not built-in pi-telegram meanings.
107
+ Some emoji are intentionally local examples or decorative variants, not global semantics. Empty-queue rotating messages (`🫙`, `🍃`, `🕳`, `🦗`, `🌙`, `🧘`, `🪐`, `🧺`, `🔭`, `🫧`, `🛸`) are copy flavor only and must not become controls. The Guest Mode placeholder frames (`🌎`, `🌍`, `🌏` stepping every second with dots growing once every two seconds) are the same kind of decorative copy: they complete whole 6-frame cycles over at least a ~20 s rotation while a guest answer is pending and then hold the cycle's final frame (a 26 s safety bound caps slow streams), never become controls, and must not carry another meaning. Example extension icons such as `🧪`, `🔧`, and `🗂` are documentation fixtures for companion extensions, not built-in pi-telegram meanings.
108
108
 
109
109
  Thread UI rule: when a message heading, chooser, or status line is specifically about Telegram/Pi threads or target thread selection, start the heading with `🧵`. Button labels for concrete thread targets should stay clean (`threadName` or slot fallback) and should not add `🧵` to every target button unless the row would otherwise be ambiguous.
110
110
 
@@ -224,7 +224,7 @@ Message cards and standalone informational notices sent by the bot should start
224
224
  Rules:
225
225
 
226
226
  - Start with a bold heading or, for dialogs, a bold question.
227
- - Format standalone notices as one fully bold line: relevant emoji, one space, concise sentence, and terminal period. Menu or chooser headings use the same fully bold form but end in a colon when controls or detail follow.
227
+ - Format standalone notices as one fully bold line: relevant emoji, one space, concise sentence, and terminal period. Menu or chooser headings use the same fully bold form but end in a colon when controls or detail follow. Empty-queue headings are the deliberate exception: fully bold, with no trailing period or colon.
228
228
  - Keep the emoji and complete sentence or heading inside the single bold span; do not bold only a fragment. A material name or phrase may receive nested italic emphasis without breaking the outer bold hierarchy—for example `<b>📡 Instance <i>Cedar</i> connected.</b>`.
229
229
  - Apply the same hierarchy to success, progress, empty, busy, unavailable, cancellation, and failure notices.
230
230
  - Callback alerts remain plain text because Telegram does not support rich text there, but still keep the relevant emoji and concise sentence.
@@ -823,6 +823,12 @@ interface TelegramLifecycleBindingDeps {
823
823
  Pi.AgentEndEvent["messages"][number],
824
824
  Keyboard.TelegramInlineKeyboardMarkup
825
825
  >["editGuestReply"];
826
+ stopGuestPlaceholder?: Queue.TelegramAgentEndHookRuntimeDeps<
827
+ Queue.PendingTelegramTurn,
828
+ Pi.ExtensionContext,
829
+ Pi.AgentEndEvent["messages"][number],
830
+ Keyboard.TelegramInlineKeyboardMarkup
831
+ >["stopGuestPlaceholder"];
826
832
  preparePreviewDelivery?: Queue.TelegramAgentEndRuntimeDeps<Queue.PendingTelegramTurn>["preparePreviewDelivery"];
827
833
  finalizeMarkdownPreview: Queue.TelegramAgentEndHookRuntimeDeps<
828
834
  Queue.PendingTelegramTurn,
@@ -876,6 +882,7 @@ export function registerTelegramLifecycleRuntimeHooks({
876
882
  deleteMessage,
877
883
  sendGuestReply,
878
884
  editGuestReply,
885
+ stopGuestPlaceholder,
879
886
  preparePreviewDelivery,
880
887
  finalizeMarkdownPreview,
881
888
  proactivePushTargetGetter,
@@ -1056,7 +1063,7 @@ export function registerTelegramLifecycleRuntimeHooks({
1056
1063
  updateStatus,
1057
1064
  getActiveTurn: activeTurnRuntime.get,
1058
1065
  loadConfig: configStore.load,
1059
- extractAssistant: Replies.extractLatestAssistantMessageText,
1066
+ extractAssistant: Replies.extractRunAssistantMessage,
1060
1067
  getFoldQueuedPromptsIntoHistory:
1061
1068
  lifecycle.shouldFoldQueuedPromptsIntoHistory,
1062
1069
  resetRuntimeState: agentEndResetter,
@@ -1090,6 +1097,7 @@ export function registerTelegramLifecycleRuntimeHooks({
1090
1097
  answerGuestQuery,
1091
1098
  sendGuestReply,
1092
1099
  editGuestReply,
1100
+ stopGuestPlaceholder,
1093
1101
  sendGuestAttachment,
1094
1102
  sendGuestVoiceReply,
1095
1103
  planOutboundReply: outboundReplyPlanner,
@@ -1429,7 +1429,7 @@ export async function handleTelegramNextCommand(deps: {
1429
1429
  deps.clearPendingModelSwitch();
1430
1430
  if (!deps.hasQueuedItems()) {
1431
1431
  await deps.sendTextReply(
1432
- formatTelegramInformationHeading("⌛", "Queue is empty."),
1432
+ formatTelegramInformationHeading("⌛", "Queue is empty"),
1433
1433
  { parseMode: "HTML" },
1434
1434
  );
1435
1435
  return;
@@ -588,6 +588,12 @@ export default function (pi: Pi.ExtensionAPI) {
588
588
  const editGuestReply = Replies.createGuestMarkdownReplyEditor({
589
589
  editGuestInlineMessage: telegramApiRuntime.editGuestInlineMessage,
590
590
  });
591
+ // Rotate the guest placeholder frames until the final replacement stops them.
592
+ const guestPlaceholderRuntime =
593
+ Replies.createTelegramGuestPlaceholderRuntime({
594
+ editGuestInlineMessage: telegramApiRuntime.editGuestInlineMessage,
595
+ recordRuntimeEvent,
596
+ });
591
597
 
592
598
  const promptDispatchRuntime = Runtime.createTelegramPromptDispatchRuntime({
593
599
  lifecycle,
@@ -978,6 +984,7 @@ export default function (pi: Pi.ExtensionAPI) {
978
984
  deleteMessage: deleteTelegramMessage,
979
985
  answerGuestQuery,
980
986
  answerGuestQueryForInlineMessage,
987
+ startGuestPlaceholder: guestPlaceholderRuntime.start,
981
988
  sendTextReply,
982
989
  setMyCommands,
983
990
  validateThreadName(threadName) {
@@ -1508,6 +1515,7 @@ export default function (pi: Pi.ExtensionAPI) {
1508
1515
  },
1509
1516
  capabilityMonitor: telegramThreadCapabilityMonitor,
1510
1517
  queueWatchdog: queueDispatchWatchdogRuntime,
1518
+ guestPlaceholder: { stopAll: guestPlaceholderRuntime.stopAll },
1511
1519
  },
1512
1520
  });
1513
1521
  const sessionLifecycleRuntime =
@@ -1828,6 +1836,7 @@ export default function (pi: Pi.ExtensionAPI) {
1828
1836
  deleteMessage: deleteTelegramMessage,
1829
1837
  sendGuestReply,
1830
1838
  editGuestReply,
1839
+ stopGuestPlaceholder: guestPlaceholderRuntime.stop,
1831
1840
  finalizeMarkdownPreview,
1832
1841
  preparePreviewDelivery,
1833
1842
  proactivePushTargetGetter,
@@ -265,6 +265,7 @@ export interface TelegramBridgeSessionServiceRuntime {
265
265
  inboundWorker: { onSessionShutdown(): Promise<void> };
266
266
  capabilityMonitor: { start(ctx: ExtensionContext): void; stop(): void };
267
267
  queueWatchdog: { start(ctx: ExtensionContext): void; stop(): void };
268
+ guestPlaceholder?: { stopAll(): void };
268
269
  }
269
270
 
270
271
  export interface TelegramBridgeSessionLifecycleAssemblyDeps<
@@ -320,6 +321,7 @@ export interface TelegramBridgeSessionLifecyclePorts<
320
321
  inboundWorker: TelegramBridgeSessionServiceRuntime["inboundWorker"];
321
322
  capabilityMonitor: TelegramBridgeSessionServiceRuntime["capabilityMonitor"];
322
323
  queueWatchdog: TelegramBridgeSessionServiceRuntime["queueWatchdog"];
324
+ guestPlaceholder?: TelegramBridgeSessionServiceRuntime["guestPlaceholder"];
323
325
  };
324
326
  }
325
327
 
@@ -347,6 +349,7 @@ export function createTelegramBridgeSessionLifecycleDeps<
347
349
  inboundWorker: ports.services.inboundWorker,
348
350
  capabilityMonitor: ports.services.capabilityMonitor,
349
351
  queueWatchdog: ports.services.queueWatchdog,
352
+ guestPlaceholder: ports.services.guestPlaceholder,
350
353
  },
351
354
  };
352
355
  }
@@ -383,6 +386,7 @@ export function createTelegramBridgeSessionLifecycleAssembly<
383
386
  },
384
387
  async onSessionShutdown(event, ctx) {
385
388
  if (!isSessionActive(ctx)) return;
389
+ deps.services.guestPlaceholder?.stopAll();
386
390
  await deps.services.delivery.onSessionShutdown();
387
391
  if (!isSessionActive(ctx)) return;
388
392
  deps.services.queueWatchdog.stop();
@@ -14,18 +14,18 @@ import * as Queue from "./queue.ts";
14
14
  const QUEUE_ITEM_PROMPT_HTML_LIMIT = 3600;
15
15
  const QUEUE_ITEM_PROMPT_TRUNCATION_SUFFIX = "\n… [truncated]";
16
16
  const EMPTY_QUEUE_REFRESH_TITLES = [
17
- "<b>⌛ Queue is still empty.</b>",
18
- "<b>🫙 Still nothing in queue.</b>",
19
- "<b>🍃 Queue remains empty.</b>",
20
- "<b>🕳 Nothing queued yet.</b>",
21
- "<b>🦗 Queue crickets continue.</b>",
22
- "<b>🌙 Queue is peacefully idle.</b>",
23
- "<b>🧘 Nothing waiting. Very zen.</b>",
24
- "<b>🪐 Queue orbit is clear.</b>",
25
- "<b>🧺 Basket is empty.</b>",
26
- "<b>🔭 No prompts on the horizon.</b>",
27
- "<b>🫧 Queue bubbles: none.</b>",
28
- "<b>🛸 No queued signals detected.</b>",
17
+ "<b>⌛ Queue is still empty</b>",
18
+ "<b>🫙 Still nothing in queue</b>",
19
+ "<b>🍃 Queue remains empty</b>",
20
+ "<b>🕳 Nothing queued yet</b>",
21
+ "<b>🦗 Queue crickets continue</b>",
22
+ "<b>🌙 Queue is peacefully idle</b>",
23
+ "<b>🧘 Nothing waiting. Very zen</b>",
24
+ "<b>🪐 Queue orbit is clear</b>",
25
+ "<b>🧺 Basket is empty</b>",
26
+ "<b>🔭 No prompts on the horizon</b>",
27
+ "<b>🫧 Queue bubbles: none</b>",
28
+ "<b>🛸 No queued signals detected</b>",
29
29
  ] as const;
30
30
  type TelegramQueueMenuReplyMarkup = TelegramInlineKeyboardMarkup;
31
31
  interface TelegramQueueMenuItem {
@@ -340,7 +340,7 @@ function getTelegramQueueMenuListText(
340
340
  emptyRefreshIndex?: number,
341
341
  ): string {
342
342
  if (items.length > 0) return "<b>⏳ Queue:</b>";
343
- if (emptyRefreshIndex === undefined) return "<b>⌛ Queue is empty.</b>";
343
+ if (emptyRefreshIndex === undefined) return "<b>⌛ Queue is empty</b>";
344
344
  return EMPTY_QUEUE_REFRESH_TITLES[
345
345
  emptyRefreshIndex % EMPTY_QUEUE_REFRESH_TITLES.length
346
346
  ];
@@ -184,7 +184,6 @@ export function buildThreadDisplaySettingsText(mode: TelegramThreadDisplayMode):
184
184
  "<code>-</code> <code>letters</code> (default): show the unique slot, such as <b><i>A</i></b> or <b><i>B</i></b>.",
185
185
  "<code>-</code> <code>names</code>: show the generated dictionary name for the slot, such as <b><i>Anchor</i></b> or <b><i>Briar</i></b>.",
186
186
  "<code>-</code> <code>directories</code>: show the directory, such as <b><i>extensions</i></b>; shared Workspaces keep slot suffixes, such as <b><i>extensions_a</i></b> and <b><i>extensions_c</i></b>.",
187
- "A manual <code>/name Name</code> overrides this Thread display name until reset.",
188
187
  ].join("\n");
189
188
  }
190
189
 
@@ -1554,6 +1554,8 @@ export interface TelegramAgentEndRuntimeDeps<
1554
1554
  sendGuestReply?: (guestQueryId: string, markdown: string) => Promise<void>;
1555
1555
  /** Replaces the early guest ACK with the final text. */
1556
1556
  editGuestReply?: (inlineMessageId: string, markdown: string) => Promise<void>;
1557
+ /** Cancels the animated guest placeholder before the final edit. */
1558
+ stopGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
1557
1559
  sendGuestAttachment?: (
1558
1560
  turn: TTurn,
1559
1561
  attachment: QueuedAttachment,
@@ -1633,6 +1635,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
1633
1635
  answerGuestQuery?: TelegramAgentEndRuntimeDeps<TTurn>["answerGuestQuery"];
1634
1636
  sendGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestReply"];
1635
1637
  editGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["editGuestReply"];
1638
+ stopGuestPlaceholder?: TelegramAgentEndRuntimeDeps<TTurn>["stopGuestPlaceholder"];
1636
1639
  sendGuestAttachment?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestAttachment"];
1637
1640
  sendGuestVoiceReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestVoiceReply"];
1638
1641
  planOutboundReply?: TelegramAgentEndRuntimeDeps<
@@ -1774,6 +1777,7 @@ export function createTelegramAgentEndHook<
1774
1777
  answerGuestQuery: deps.answerGuestQuery,
1775
1778
  sendGuestReply: deps.sendGuestReply,
1776
1779
  editGuestReply: deps.editGuestReply,
1780
+ stopGuestPlaceholder: deps.stopGuestPlaceholder,
1777
1781
  sendGuestAttachment: deps.sendGuestAttachment,
1778
1782
  sendGuestVoiceReply: deps.sendGuestVoiceReply,
1779
1783
  planOutboundReply: deps.planOutboundReply,
@@ -1866,6 +1870,16 @@ export async function handleTelegramAgentEndRuntime<
1866
1870
  return;
1867
1871
  }
1868
1872
  if (turn.guestQueryId) {
1873
+ if (turn.guestInlineMessageId && deps.stopGuestPlaceholder) {
1874
+ try {
1875
+ await deps.stopGuestPlaceholder(turn.guestInlineMessageId);
1876
+ } catch (error) {
1877
+ deps.recordRuntimeEvent?.("delivery", error, {
1878
+ phase: "guest-placeholder-stop",
1879
+ guestQueryId: turn.guestQueryId,
1880
+ });
1881
+ }
1882
+ }
1869
1883
  if (turn.guestInlineMessageId && deps.editGuestReply) {
1870
1884
  const experimentText = assistant.errorMessage
1871
1885
  ? "Telegram bridge: Pi failed while processing the request."
@@ -1875,7 +1889,7 @@ export async function handleTelegramAgentEndRuntime<
1875
1889
  await deps.editGuestReply(turn.guestInlineMessageId, experimentText);
1876
1890
  deps.recordRuntimeEvent?.(
1877
1891
  "guest",
1878
- new Error("Guest ACK experiment edited the guest answer"),
1892
+ new Error("Guest ACK edited the guest answer"),
1879
1893
  { phase: "guest-ack-edited", guestQueryId: turn.guestQueryId },
1880
1894
  );
1881
1895
  } catch (error) {
@@ -1887,7 +1901,7 @@ export async function handleTelegramAgentEndRuntime<
1887
1901
  } else {
1888
1902
  deps.recordRuntimeEvent?.(
1889
1903
  "delivery",
1890
- new Error("Guest ACK experiment turn produced no editable text"),
1904
+ new Error("Guest ACK turn produced no editable text"),
1891
1905
  { phase: "guest-ack-edit-empty", guestQueryId: turn.guestQueryId },
1892
1906
  );
1893
1907
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Telegram reply delivery helpers
3
3
  * Zones: telegram outbound, native rich markdown, UI/compat rendering transport
4
- * Owns native assistant replies, rendered UI delivery, reply transport wiring, and plain text replies
4
+ * Owns native assistant replies, rendered UI delivery, guest placeholder rotation, reply transport wiring, and plain text replies
5
5
  */
6
6
 
7
7
  import { assertTelegramInlineKeyboardCallbackData } from "./keyboard.ts";
@@ -177,6 +177,47 @@ export function extractLatestAssistantMessageText(
177
177
  return {};
178
178
  }
179
179
 
180
+ /**
181
+ * Extract the run's answer without trusting an empty final assistant message.
182
+ * A low-level run may end with a completed assistant message whose content was
183
+ * suppressed (a companion extension preserving an earlier draft, for example
184
+ * State Flow's fallback final:true patch turn). In that case the run's answer
185
+ * is the latest earlier completed assistant message that carries text;
186
+ * tool-use prefaces, errors, and aborts stay excluded.
187
+ */
188
+ export function extractRunAssistantMessage(
189
+ messages: readonly unknown[],
190
+ ): {
191
+ text?: string;
192
+ stopReason?: string;
193
+ errorMessage?: string;
194
+ } {
195
+ const latest = extractLatestAssistantMessageText(messages);
196
+ if (latest.text || latest.stopReason !== "stop") return latest;
197
+ for (let i = messages.length - 1; i >= 0; i--) {
198
+ const message = messages[i];
199
+ if (!message || !isAssistantAgentMessage(message)) continue;
200
+ const rawStopReason = getAgentMessageField(message, "stopReason");
201
+ if (
202
+ rawStopReason === "toolUse" ||
203
+ rawStopReason === "error" ||
204
+ rawStopReason === "aborted"
205
+ ) {
206
+ continue;
207
+ }
208
+ const text = getAgentMessageText(message);
209
+ if (!text) continue;
210
+ const rawErrorMessage = getAgentMessageField(message, "errorMessage");
211
+ return {
212
+ text,
213
+ stopReason: typeof rawStopReason === "string" ? rawStopReason : undefined,
214
+ errorMessage:
215
+ typeof rawErrorMessage === "string" ? rawErrorMessage : undefined,
216
+ };
217
+ }
218
+ return latest;
219
+ }
220
+
180
221
  export interface TelegramReplyOwnershipRecorder {
181
222
  record: (input: {
182
223
  chatId: number;
@@ -900,9 +941,9 @@ export function createGuestMarkdownReplySender(deps: {
900
941
  }
901
942
 
902
943
  /**
903
- * Guest reply editor: replaces an early Guest Mode answer (the temporary ACK
904
- * experiment) with native Rich Markdown content addressed by
905
- * `inline_message_id` instead of a chat/message pair.
944
+ * Guest reply editor: replaces the early Guest Mode placeholder ACK with
945
+ * native Rich Markdown content addressed by `inline_message_id` instead of a
946
+ * chat/message pair.
906
947
  */
907
948
  export function createGuestMarkdownReplyEditor(deps: {
908
949
  editGuestInlineMessage: (
@@ -917,3 +958,219 @@ export function createGuestMarkdownReplyEditor(deps: {
917
958
  });
918
959
  };
919
960
  }
961
+
962
+ /**
963
+ * Guest Mode placeholder rotation: the early ACK is the first placeholder
964
+ * frame and the runtime steps the remaining frames once per interval, moving
965
+ * the globe every second while the trailing dots grow once every two seconds.
966
+ *
967
+ * Rotation completes whole six-frame cycles and only stops once at least
968
+ * `TELEGRAM_GUEST_PLACEHOLDER_MIN_MS` has elapsed, so a pending answer holds
969
+ * the finished cycle's last frame (`🌏 Working on it...`) instead of whatever
970
+ * step a hard time cap happens to cut. The `TELEGRAM_GUEST_PLACEHOLDER_MAX_MS`
971
+ * safety bound keeps the edit stream clear of the first flood-control
972
+ * rejections measured in live guest runs (+26.5 s at ~53 edits, +27.8 s at
973
+ * ~28 edits): rotation caps at 23 edits and never starts a frame after 26 s.
974
+ */
975
+
976
+ export const TELEGRAM_GUEST_PLACEHOLDER_FRAME_MS = 1_000;
977
+
978
+ export const TELEGRAM_GUEST_PLACEHOLDER_MIN_MS = 20_000;
979
+
980
+ export const TELEGRAM_GUEST_PLACEHOLDER_MAX_MS = 26_000;
981
+
982
+ export const TELEGRAM_GUEST_PLACEHOLDER_FRAMES = [
983
+ "<b>🌎 Working on it.</b>",
984
+ "<b>🌍 Working on it.</b>",
985
+ "<b>🌏 Working on it..</b>",
986
+ "<b>🌎 Working on it..</b>",
987
+ "<b>🌍 Working on it...</b>",
988
+ "<b>🌏 Working on it...</b>",
989
+ ] as const;
990
+
991
+ export function buildTelegramGuestPlaceholderFrame(step: number): string {
992
+ const frames: readonly string[] = TELEGRAM_GUEST_PLACEHOLDER_FRAMES;
993
+ const index = ((step % frames.length) + frames.length) % frames.length;
994
+ return frames[index]!;
995
+ }
996
+
997
+ export interface TelegramGuestPlaceholderRuntimeDeps {
998
+ editGuestInlineMessage: (
999
+ inlineMessageId: string,
1000
+ content: { text: string; parseMode: "HTML" },
1001
+ ) => Promise<void>;
1002
+ recordRuntimeEvent?: (
1003
+ category: string,
1004
+ error: unknown,
1005
+ details?: Record<string, unknown>,
1006
+ ) => void;
1007
+ intervalMs?: number;
1008
+ minMs?: number;
1009
+ maxMs?: number;
1010
+ now?: () => number;
1011
+ setTimer?: (
1012
+ callback: () => void,
1013
+ ms: number,
1014
+ ) => ReturnType<typeof setTimeout>;
1015
+ clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
1016
+ }
1017
+
1018
+ export interface TelegramGuestPlaceholderRuntime {
1019
+ /** Starts the frame loop on an answered guest inline message. */
1020
+ start: (inlineMessageId: string) => void;
1021
+ /** Cancels the loop and waits for any in-flight frame edit before returning. */
1022
+ stop: (inlineMessageId: string) => Promise<void>;
1023
+ /** Cancels every loop without waiting for in-flight edits (session shutdown). */
1024
+ stopAll: () => void;
1025
+ }
1026
+
1027
+ interface TelegramGuestPlaceholderSession {
1028
+ step: number;
1029
+ stopped: boolean;
1030
+ startedAtMs: number;
1031
+ finished: boolean;
1032
+ timer?: ReturnType<typeof setTimeout>;
1033
+ inflight?: Promise<void>;
1034
+ }
1035
+
1036
+ function getTelegramGuestPlaceholderRetryDelayMs(
1037
+ error: unknown,
1038
+ fallbackMs: number,
1039
+ ): number {
1040
+ const retryAfterSeconds = (
1041
+ error as { retryAfterSeconds?: unknown } | undefined
1042
+ )?.retryAfterSeconds;
1043
+ return typeof retryAfterSeconds === "number" && retryAfterSeconds > 0
1044
+ ? Math.max(fallbackMs, retryAfterSeconds * 1_000)
1045
+ : fallbackMs;
1046
+ }
1047
+
1048
+ export function createTelegramGuestPlaceholderRuntime(
1049
+ deps: TelegramGuestPlaceholderRuntimeDeps,
1050
+ ): TelegramGuestPlaceholderRuntime {
1051
+ const intervalMs = deps.intervalMs ?? TELEGRAM_GUEST_PLACEHOLDER_FRAME_MS;
1052
+ const minMs = deps.minMs ?? TELEGRAM_GUEST_PLACEHOLDER_MIN_MS;
1053
+ const maxMs = deps.maxMs ?? TELEGRAM_GUEST_PLACEHOLDER_MAX_MS;
1054
+ const now = deps.now ?? Date.now;
1055
+ const setTimer =
1056
+ deps.setTimer ??
1057
+ ((callback: () => void, ms: number): ReturnType<typeof setTimeout> =>
1058
+ setTimeout(callback, ms));
1059
+ const clearTimer =
1060
+ deps.clearTimer ??
1061
+ ((timer: ReturnType<typeof setTimeout>): void => clearTimeout(timer));
1062
+ const sessions = new Map<string, TelegramGuestPlaceholderSession>();
1063
+
1064
+ const finishRotation = (
1065
+ session: TelegramGuestPlaceholderSession,
1066
+ elapsedMs: number,
1067
+ ): void => {
1068
+ if (session.finished) return;
1069
+ session.finished = true;
1070
+ deps.recordRuntimeEvent?.(
1071
+ "guest",
1072
+ "Guest placeholder rotation reached its bound",
1073
+ {
1074
+ phase: "guest-placeholder-capped",
1075
+ minMs,
1076
+ maxMs,
1077
+ elapsedMs,
1078
+ step: session.step,
1079
+ },
1080
+ );
1081
+ };
1082
+
1083
+ const scheduleFrame = (
1084
+ inlineMessageId: string,
1085
+ session: TelegramGuestPlaceholderSession,
1086
+ delayMs: number,
1087
+ ): void => {
1088
+ if (session.stopped) return;
1089
+ const elapsedMs = now() - session.startedAtMs;
1090
+ const frameCount = TELEGRAM_GUEST_PLACEHOLDER_FRAMES.length;
1091
+ const cycleComplete =
1092
+ session.step > 0 && session.step % frameCount === frameCount - 1;
1093
+ if (cycleComplete && elapsedMs >= minMs) {
1094
+ finishRotation(session, elapsedMs);
1095
+ return;
1096
+ }
1097
+ if (elapsedMs + delayMs > maxMs) {
1098
+ finishRotation(session, elapsedMs);
1099
+ return;
1100
+ }
1101
+ const timer = setTimer(() => {
1102
+ session.timer = undefined;
1103
+ runFrame(inlineMessageId, session);
1104
+ }, delayMs);
1105
+ timer.unref?.();
1106
+ session.timer = timer;
1107
+ };
1108
+
1109
+ const runFrame = (
1110
+ inlineMessageId: string,
1111
+ session: TelegramGuestPlaceholderSession,
1112
+ ): void => {
1113
+ if (session.stopped) return;
1114
+ session.step += 1;
1115
+ let nextDelayMs = intervalMs;
1116
+ session.inflight = (async () => {
1117
+ try {
1118
+ await deps.editGuestInlineMessage(inlineMessageId, {
1119
+ text: buildTelegramGuestPlaceholderFrame(session.step),
1120
+ parseMode: "HTML",
1121
+ });
1122
+ } catch (error) {
1123
+ nextDelayMs = getTelegramGuestPlaceholderRetryDelayMs(error, intervalMs);
1124
+ deps.recordRuntimeEvent?.("guest", error, {
1125
+ phase: "guest-placeholder-edit",
1126
+ retryAfterMs: nextDelayMs,
1127
+ });
1128
+ } finally {
1129
+ session.inflight = undefined;
1130
+ scheduleFrame(inlineMessageId, session, nextDelayMs);
1131
+ }
1132
+ })();
1133
+ };
1134
+
1135
+ const stop = async (inlineMessageId: string): Promise<void> => {
1136
+ const session = sessions.get(inlineMessageId);
1137
+ if (!session) return;
1138
+ sessions.delete(inlineMessageId);
1139
+ session.stopped = true;
1140
+ if (session.timer !== undefined) {
1141
+ clearTimer(session.timer);
1142
+ session.timer = undefined;
1143
+ }
1144
+ try {
1145
+ await session.inflight;
1146
+ } catch {
1147
+ // Frame failures are reported as runtime events; stopping stays fail-open.
1148
+ }
1149
+ };
1150
+
1151
+ return {
1152
+ start(inlineMessageId) {
1153
+ const existing = sessions.get(inlineMessageId);
1154
+ if (existing) {
1155
+ existing.stopped = true;
1156
+ if (existing.timer !== undefined) clearTimer(existing.timer);
1157
+ }
1158
+ const session: TelegramGuestPlaceholderSession = {
1159
+ step: 0,
1160
+ stopped: false,
1161
+ startedAtMs: now(),
1162
+ finished: false,
1163
+ };
1164
+ sessions.set(inlineMessageId, session);
1165
+ scheduleFrame(inlineMessageId, session, intervalMs);
1166
+ },
1167
+ stop,
1168
+ stopAll() {
1169
+ for (const session of sessions.values()) {
1170
+ session.stopped = true;
1171
+ if (session.timer !== undefined) clearTimer(session.timer);
1172
+ }
1173
+ sessions.clear();
1174
+ },
1175
+ };
1176
+ }
@@ -16,6 +16,7 @@ import * as Model from "./model.ts";
16
16
  import * as OutboundHandlers from "./outbound.ts";
17
17
  import * as PromptTemplates from "./prompt-templates.ts";
18
18
  import * as Queue from "./queue.ts";
19
+ import * as Replies from "./replies.ts";
19
20
  import type { TelegramBridgeRuntime } from "./runtime.ts";
20
21
  import type { TelegramSectionRegistry } from "./sections.ts";
21
22
  import * as TextGroups from "./text-groups.ts";
@@ -665,6 +666,8 @@ export interface TelegramInboundRouteRuntimeDeps<
665
666
  text: string,
666
667
  options?: { parseMode?: "HTML" },
667
668
  ) => Promise<string | undefined>;
669
+ /** Starts the animated placeholder on an answered guest inline message. */
670
+ startGuestPlaceholder?: (inlineMessageId: string) => void;
668
671
  sendTextReply: (
669
672
  chatId: number,
670
673
  replyToMessageId: number,
@@ -2662,8 +2665,10 @@ export function createTelegramInboundRouteRuntime<
2662
2665
  assertExecutionCurrent();
2663
2666
  };
2664
2667
  // Answer the guest query immediately so the agent-end edit can replace the
2665
- // early ACK once the turn settles. See BACKLOG.md for live acceptance.
2666
- const TELEGRAM_GUEST_ACK_HTML = "<b>⚙️ Received. Working on it…</b>";
2668
+ // early ACK once the turn settles. The ACK is the first placeholder frame and
2669
+ // the loop rotates through the remaining frames until the replacement.
2670
+ const TELEGRAM_GUEST_ACK_HTML =
2671
+ Replies.buildTelegramGuestPlaceholderFrame(0);
2667
2672
  const handleAuthorizedTelegramGuestMessage = async (
2668
2673
  guestMessage: Updates.TelegramGuestMessage & { from: TelegramUser },
2669
2674
  ctx: TContext,
@@ -2679,9 +2684,12 @@ export function createTelegramInboundRouteRuntime<
2679
2684
  TELEGRAM_GUEST_ACK_HTML,
2680
2685
  { parseMode: "HTML" },
2681
2686
  );
2687
+ if (guestInlineMessageId) {
2688
+ deps.startGuestPlaceholder?.(guestInlineMessageId);
2689
+ }
2682
2690
  deps.recordRuntimeEvent?.(
2683
2691
  "guest",
2684
- new Error("Guest ACK experiment answered the guest query"),
2692
+ new Error("Guest ACK answered the guest query"),
2685
2693
  {
2686
2694
  phase: "guest-ack-sent",
2687
2695
  guestQueryId: guestMessage.guest_query_id,
@@ -374,6 +374,13 @@ interface TelegramApiResponse<T> {
374
374
  parameters?: { retry_after?: number };
375
375
  }
376
376
 
377
+ export interface TelegramApiRetryWait {
378
+ method: string;
379
+ delayMs: number;
380
+ attempt: number;
381
+ retryAfterSeconds?: number;
382
+ }
383
+
377
384
  export interface TelegramApiCallOptions {
378
385
  signal?: AbortSignal;
379
386
  maxAttempts?: number;
@@ -381,6 +388,8 @@ export interface TelegramApiCallOptions {
381
388
  retrySafety?: "safe" | "non-idempotent";
382
389
  retryBaseDelayMs?: number;
383
390
  sleep?: (ms: number) => Promise<void>;
391
+ /** Observability hook fired before a 429 retry wait, never for 5xx waits. */
392
+ onRetryWait?: (wait: TelegramApiRetryWait) => void;
384
393
  }
385
394
 
386
395
  interface TelegramGetFileResult {
@@ -434,6 +443,7 @@ export interface TelegramAnswerGuestQueryOptions {
434
443
  export interface TelegramEditGuestInlineMessageContent {
435
444
  text?: string;
436
445
  richMessage?: TelegramInputRichMessage;
446
+ parseMode?: "HTML";
437
447
  }
438
448
 
439
449
  export interface TelegramAnswerCallbackQueryOptions {
@@ -1380,7 +1390,25 @@ async function callTelegramWithRetry<TResponse>(
1380
1390
  isTelegramApiMethodRetrySafe(method));
1381
1391
  const maxAttempts = Math.max(1, options?.maxAttempts ?? 3);
1382
1392
  const retryBaseDelayMs = options?.retryBaseDelayMs ?? 500;
1383
- const waitBeforeRetry = async (ms: number): Promise<void> => {
1393
+ const waitBeforeRetry = async (
1394
+ error: unknown,
1395
+ attempt: number,
1396
+ ): Promise<void> => {
1397
+ const ms = getTelegramRetryDelayMs(error, attempt, retryBaseDelayMs);
1398
+ if (
1399
+ !options?.signal?.aborted &&
1400
+ error instanceof TelegramApiHttpError &&
1401
+ error.status === 429
1402
+ ) {
1403
+ options?.onRetryWait?.({
1404
+ method,
1405
+ delayMs: ms,
1406
+ attempt,
1407
+ ...(error.retryAfterSeconds !== undefined
1408
+ ? { retryAfterSeconds: error.retryAfterSeconds }
1409
+ : {}),
1410
+ });
1411
+ }
1384
1412
  if (options?.sleep) await options.sleep(ms);
1385
1413
  else await sleepTelegramRetry(ms, options?.signal);
1386
1414
  throwIfTelegramApiCallAborted(options?.signal);
@@ -1406,9 +1434,7 @@ async function callTelegramWithRetry<TResponse>(
1406
1434
  if (!retrySafe) {
1407
1435
  if (error instanceof TelegramApiHttpError && error.status === 429) {
1408
1436
  if (attempt >= maxAttempts - 1) throw error;
1409
- await waitBeforeRetry(
1410
- getTelegramRetryDelayMs(error, attempt, retryBaseDelayMs),
1411
- );
1437
+ await waitBeforeRetry(error, attempt);
1412
1438
  continue;
1413
1439
  }
1414
1440
  if (
@@ -1423,9 +1449,7 @@ async function callTelegramWithRetry<TResponse>(
1423
1449
  throw error;
1424
1450
  }
1425
1451
  if (attempt >= maxAttempts - 1 || !retryable) throw error;
1426
- await waitBeforeRetry(
1427
- getTelegramRetryDelayMs(error, attempt, retryBaseDelayMs),
1428
- );
1452
+ await waitBeforeRetry(error, attempt);
1429
1453
  }
1430
1454
  }
1431
1455
  }
@@ -2095,6 +2119,7 @@ export function createTelegramBridgeApiRuntime(
2095
2119
  ...(content.richMessage
2096
2120
  ? { rich_message: content.richMessage }
2097
2121
  : { text: content.text }),
2122
+ ...(content.parseMode ? { parse_mode: content.parseMode } : {}),
2098
2123
  });
2099
2124
  },
2100
2125
  prepareTempDir: () =>
@@ -2117,6 +2142,7 @@ export function createTelegramApiClient(
2117
2142
  options: TelegramAnswerCallbackQueryOptions & { now?: () => number } = {},
2118
2143
  ): TelegramApiClient {
2119
2144
  const now = options.now ?? Date.now;
2145
+ const recordRuntimeEvent = options.recordRuntimeEvent;
2120
2146
  const draftRetryNotBeforeByTarget = new Map<string, number>();
2121
2147
  return {
2122
2148
  call: async <TResponse>(
@@ -2140,9 +2166,31 @@ export function createTelegramApiClient(
2140
2166
  }
2141
2167
  try {
2142
2168
  // A draft is a replaceable snapshot, not a body to replay after backoff.
2143
- return await callTelegram<TResponse>(
2144
- token, method, body, isDraft ? { ...options, maxAttempts: 1 } : options,
2145
- );
2169
+ const retryWaitOptions: TelegramApiCallOptions = recordRuntimeEvent
2170
+ ? {
2171
+ onRetryWait: (wait) => {
2172
+ recordRuntimeEvent(
2173
+ "api",
2174
+ new Error(
2175
+ `Telegram API rate limit: waiting ${wait.delayMs} ms before retrying ${wait.method}`,
2176
+ ),
2177
+ {
2178
+ phase: "retry-wait",
2179
+ method: wait.method,
2180
+ waitMs: wait.delayMs,
2181
+ attempt: wait.attempt,
2182
+ ...(wait.retryAfterSeconds !== undefined
2183
+ ? { retryAfterSeconds: wait.retryAfterSeconds }
2184
+ : {}),
2185
+ },
2186
+ );
2187
+ },
2188
+ }
2189
+ : {};
2190
+ return await callTelegram<TResponse>(token, method, body, {
2191
+ ...(isDraft ? { ...options, maxAttempts: 1 } : options),
2192
+ ...retryWaitOptions,
2193
+ });
2146
2194
  } catch (error) {
2147
2195
  if (draftKey && isRetryableTelegramApiError(error)) {
2148
2196
  draftRetryNotBeforeByTarget.set(draftKey, Math.max(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.45.5",
3
+ "version": "0.45.6",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.10.4",
3
+ "version": "0.10.6",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -44,8 +44,8 @@
44
44
  "@llblab/pi-clean-room": "0.1.1",
45
45
  "@llblab/pi-codex-usage": "0.9.4",
46
46
  "@llblab/pi-grow-loop": "0.8.1",
47
- "@llblab/pi-state-flow": "0.9.5",
48
- "@llblab/pi-telegram": "0.45.5",
47
+ "@llblab/pi-state-flow": "0.9.6",
48
+ "@llblab/pi-telegram": "0.45.6",
49
49
  "@llblab/skills": "1.15.0"
50
50
  },
51
51
  "bundledDependencies": [