@llblab/pi-telegram 0.35.2 → 0.36.1

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/lib/queue.ts CHANGED
@@ -34,7 +34,17 @@ export type TelegramQueueLane = "control" | "priority" | "default";
34
34
  export type TelegramQueueReactionDisposition =
35
35
  | { kind: "default" }
36
36
  | { kind: "priority"; emoji: string }
37
- | { kind: "suppressed"; emoji: string };
37
+ | { kind: "suppressed"; emoji: string }
38
+ | {
39
+ kind: "priority-suppressed";
40
+ priorityEmoji: string;
41
+ suppressionEmoji: string;
42
+ }
43
+ | {
44
+ kind: "reaction-transition";
45
+ priorityEmoji?: string | null;
46
+ suppressionEmoji?: string | null;
47
+ };
38
48
 
39
49
  export interface TelegramQueueAdmissionReceipt {
40
50
  queueKind: TelegramQueueItemKind;
@@ -899,7 +909,7 @@ export function applyTelegramQueuePromptReactionDisposition<
899
909
  items: TelegramQueueItem<TContext>[],
900
910
  messageId: number,
901
911
  disposition: TelegramQueueReactionDisposition,
902
- priorityLaneOrder?: number,
912
+ destinationLaneOrder?: number,
903
913
  scope?: TelegramQueueMessageScope,
904
914
  ): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
905
915
  let nextItems = items;
@@ -911,17 +921,38 @@ export function applyTelegramQueuePromptReactionDisposition<
911
921
  ) {
912
922
  continue;
913
923
  }
914
- const queueLane: TelegramQueueLane =
915
- disposition.kind === "priority" ? "priority" : "default";
916
- const laneOrder =
917
- disposition.kind === "priority" ? priorityLaneOrder : item.queueOrder;
924
+ const isPriority = disposition.kind === "reaction-transition"
925
+ ? disposition.priorityEmoji === undefined
926
+ ? item.queueLane === "priority"
927
+ : disposition.priorityEmoji !== null
928
+ : disposition.kind === "priority" ||
929
+ disposition.kind === "priority-suppressed";
930
+ const queueLane: TelegramQueueLane = isPriority ? "priority" : "default";
931
+ const laneOrder = item.queueLane === queueLane
932
+ ? item.laneOrder
933
+ : destinationLaneOrder;
918
934
  if (laneOrder === undefined) {
919
- throw new Error("Telegram priority reaction order is unavailable.");
935
+ throw new Error("Telegram destination lane order is unavailable.");
920
936
  }
921
- const priorityEmoji =
922
- disposition.kind === "priority" ? disposition.emoji : undefined;
937
+ const priorityEmoji = disposition.kind === "reaction-transition"
938
+ ? disposition.priorityEmoji === undefined
939
+ ? item.priorityEmoji
940
+ : disposition.priorityEmoji ?? undefined
941
+ : disposition.kind === "priority"
942
+ ? disposition.emoji
943
+ : disposition.kind === "priority-suppressed"
944
+ ? disposition.priorityEmoji
945
+ : undefined;
923
946
  const reactionSuppressionEmoji =
924
- disposition.kind === "suppressed" ? disposition.emoji : undefined;
947
+ disposition.kind === "reaction-transition"
948
+ ? disposition.suppressionEmoji === undefined
949
+ ? item.reactionSuppressionEmoji
950
+ : disposition.suppressionEmoji ?? undefined
951
+ : disposition.kind === "suppressed"
952
+ ? disposition.emoji
953
+ : disposition.kind === "priority-suppressed"
954
+ ? disposition.suppressionEmoji
955
+ : undefined;
925
956
  if (
926
957
  item.queueLane === queueLane &&
927
958
  item.laneOrder === laneOrder &&
@@ -1984,7 +2015,6 @@ export interface TelegramSessionShutdownState<TQueueItem> {
1984
2015
  queuedTelegramItems: TQueueItem[];
1985
2016
  nextQueuedTelegramItemOrder: number;
1986
2017
  nextQueuedTelegramControlOrder: number;
1987
- nextPriorityReactionOrder: number;
1988
2018
  currentTelegramModel: undefined;
1989
2019
  activeTelegramToolExecutions: number;
1990
2020
  pendingTelegramModelSwitch: undefined;
@@ -1996,7 +2026,6 @@ export interface TelegramSessionShutdownState<TQueueItem> {
1996
2026
  export interface TelegramSessionRuntimeCounterState {
1997
2027
  nextQueuedTelegramItemOrder?: number;
1998
2028
  nextQueuedTelegramControlOrder?: number;
1999
- nextPriorityReactionOrder?: number;
2000
2029
  }
2001
2030
 
2002
2031
  export interface TelegramSessionRuntimeFlagState {
@@ -2104,8 +2133,7 @@ export interface TelegramQueueMutationRuntimeDeps<
2104
2133
  TContext,
2105
2134
  > extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2106
2135
  ctx: TContext;
2107
- getNextPriorityReactionOrder?: () => number;
2108
- incrementNextPriorityReactionOrder?: () => void;
2136
+ allocateLaneOrder?: () => number;
2109
2137
  onItemsDiscarded?: (
2110
2138
  items: readonly TelegramQueueItem<TContext>[],
2111
2139
  ctx: TContext,
@@ -2116,8 +2144,7 @@ export interface TelegramQueueMutationRuntimeDeps<
2116
2144
  export interface TelegramQueueMutationControllerDeps<
2117
2145
  TContext,
2118
2146
  > extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2119
- getNextPriorityReactionOrder?: () => number;
2120
- incrementNextPriorityReactionOrder?: () => void;
2147
+ allocateLaneOrder?: () => number;
2121
2148
  onItemsDiscarded?: (
2122
2149
  items: readonly TelegramQueueItem<TContext>[],
2123
2150
  ctx: TContext,
@@ -2227,7 +2254,6 @@ export function buildTelegramSessionShutdownState<
2227
2254
  queuedTelegramItems: [],
2228
2255
  nextQueuedTelegramItemOrder: 0,
2229
2256
  nextQueuedTelegramControlOrder: 0,
2230
- nextPriorityReactionOrder: 0,
2231
2257
  currentTelegramModel: undefined,
2232
2258
  activeTelegramToolExecutions: 0,
2233
2259
  pendingTelegramModelSwitch: undefined,
@@ -2492,24 +2518,40 @@ export function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(
2492
2518
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
2493
2519
  scope?: TelegramQueueMessageScope,
2494
2520
  ): boolean {
2495
- const priorityLaneOrder =
2496
- disposition.kind === "priority"
2497
- ? deps.getNextPriorityReactionOrder?.()
2498
- : undefined;
2499
- if (disposition.kind === "priority" && priorityLaneOrder === undefined) {
2500
- return false;
2501
- }
2521
+ const queuedItems = deps.getQueuedItems();
2522
+ const changesLane = queuedItems.some((item) => {
2523
+ if (
2524
+ !isPendingTelegramTurn(item) ||
2525
+ !isTelegramQueueItemInMessageScope(item, scope) ||
2526
+ !item.sourceMessageIds.includes(messageId)
2527
+ ) {
2528
+ return false;
2529
+ }
2530
+ const queueLane: TelegramQueueLane =
2531
+ disposition.kind === "reaction-transition"
2532
+ ? disposition.priorityEmoji === undefined
2533
+ ? item.queueLane
2534
+ : disposition.priorityEmoji === null
2535
+ ? "default"
2536
+ : "priority"
2537
+ : disposition.kind === "priority" ||
2538
+ disposition.kind === "priority-suppressed"
2539
+ ? "priority"
2540
+ : "default";
2541
+ return item.queueLane !== queueLane;
2542
+ });
2543
+ const destinationLaneOrder = changesLane
2544
+ ? deps.allocateLaneOrder?.()
2545
+ : undefined;
2546
+ if (changesLane && destinationLaneOrder === undefined) return false;
2502
2547
  const { changed, items } = applyTelegramQueuePromptReactionDisposition(
2503
- deps.getQueuedItems(),
2548
+ queuedItems,
2504
2549
  messageId,
2505
2550
  disposition,
2506
- priorityLaneOrder,
2551
+ destinationLaneOrder,
2507
2552
  scope,
2508
2553
  );
2509
2554
  if (!changed) return false;
2510
- if (disposition.kind === "priority") {
2511
- deps.incrementNextPriorityReactionOrder?.();
2512
- }
2513
2555
  commitReorderedTelegramQueueItemsRuntime(items, deps);
2514
2556
  return true;
2515
2557
  }
@@ -2931,18 +2973,33 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
2931
2973
  { phase: "transport-generation" },
2932
2974
  );
2933
2975
  }
2934
- const dispatchableItems: TelegramQueueItem<TContext>[] = [];
2935
- const suppressedActiveItems: TelegramQueueItem<TContext>[] = [];
2936
- for (const item of activeItems) {
2937
- if (
2938
- item.kind === "prompt" &&
2939
- item.reactionSuppressionEmoji !== undefined
2940
- ) {
2941
- suppressedActiveItems.push(item);
2942
- } else {
2943
- dispatchableItems.push(item);
2976
+ const canDispatch = deps.canDispatch(ctx);
2977
+ let nextActiveIndex = 0;
2978
+ if (canDispatch) {
2979
+ while (nextActiveIndex < activeItems.length) {
2980
+ const candidate = activeItems[nextActiveIndex];
2981
+ if (
2982
+ !candidate ||
2983
+ candidate.kind !== "prompt" ||
2984
+ candidate.reactionSuppressionEmoji === undefined
2985
+ ) {
2986
+ break;
2987
+ }
2988
+ if (deps.hasPendingInboundQueueMutationForItem?.(candidate)) {
2989
+ deps.updateStatus(ctx);
2990
+ return;
2991
+ }
2992
+ if (
2993
+ deps.isQueueItemAdmissionReady &&
2994
+ !deps.isQueueItemAdmissionReady(candidate)
2995
+ ) {
2996
+ deps.updateStatus(ctx);
2997
+ return;
2998
+ }
2999
+ nextActiveIndex += 1;
2944
3000
  }
2945
3001
  }
3002
+ const dispatchableItems = activeItems.slice(nextActiveIndex);
2946
3003
  const nextItem = dispatchableItems[0];
2947
3004
  if (
2948
3005
  nextItem &&
@@ -2961,12 +3018,11 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
2961
3018
  }
2962
3019
  const dispatchPlan = planNextTelegramQueueAction(
2963
3020
  dispatchableItems,
2964
- deps.canDispatch(ctx),
3021
+ canDispatch,
2965
3022
  );
2966
- if (dispatchPlan.kind !== "none") {
3023
+ if (nextActiveIndex > 0 || dispatchPlan.kind !== "none") {
2967
3024
  deps.setQueuedItems([
2968
3025
  ...dispatchPlan.remainingItems,
2969
- ...suppressedActiveItems,
2970
3026
  ...protectedInactiveItems,
2971
3027
  ]);
2972
3028
  }
package/lib/routing.ts CHANGED
@@ -590,6 +590,11 @@ export interface TelegramInboundRouteRuntimeDeps<
590
590
  ctx: TContext,
591
591
  ) => Promise<boolean>;
592
592
  buttonActionStore?: OutboundHandlers.TelegramButtonActionStore;
593
+ invokeBoundButtonAction?: (
594
+ action: OutboundHandlers.TelegramOutboundButtonAction,
595
+ query: TCallbackQuery,
596
+ ctx: TContext,
597
+ ) => Promise<false | "new" | "edit">;
593
598
  inboundHandlerRuntime: TelegramInboundHandlerRuntime<TContext>;
594
599
  threadStore?: Threads.TelegramTopicTargetStore;
595
600
  updateStatus: (ctx: TContext, error?: string) => void;
@@ -1566,6 +1571,16 @@ export function createTelegramInboundRouteRuntime<
1566
1571
  {
1567
1572
  resolveAction: deps.buttonActionStore.resolve,
1568
1573
  answerCallbackQuery: deps.answerCallbackQuery,
1574
+ ...(deps.invokeBoundButtonAction
1575
+ ? {
1576
+ invokeBoundAction: (buttonQuery, action, context) =>
1577
+ deps.invokeBoundButtonAction!(
1578
+ action,
1579
+ buttonQuery as TCallbackQuery,
1580
+ context,
1581
+ ),
1582
+ }
1583
+ : {}),
1569
1584
  editMessageReplyMarkup: deps.editMessageReplyMarkup
1570
1585
  ? async (chatId, messageId, replyMarkup) => {
1571
1586
  try {
package/lib/runtime.ts CHANGED
@@ -10,7 +10,6 @@ const TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS = 250;
10
10
  export interface TelegramRuntimeQueueCounters {
11
11
  nextQueuedTelegramItemOrder: number;
12
12
  nextQueuedTelegramControlOrder: number;
13
- nextPriorityReactionOrder: number;
14
13
  }
15
14
 
16
15
  export interface TelegramRuntimeLifecycleFlags {
@@ -34,8 +33,6 @@ export interface TelegramRuntimeQueuePort {
34
33
  syncCounters: (counters: Partial<TelegramRuntimeQueueCounters>) => void;
35
34
  allocateItemOrder: () => number;
36
35
  allocateControlOrder: () => number;
37
- getNextPriorityReactionOrder: () => number;
38
- incrementNextPriorityReactionOrder: () => void;
39
36
  }
40
37
 
41
38
  export interface TelegramRuntimeLifecyclePort {
@@ -85,7 +82,6 @@ export function createTelegramBridgeRuntimeState(): TelegramBridgeRuntimeState {
85
82
  return {
86
83
  nextQueuedTelegramItemOrder: 0,
87
84
  nextQueuedTelegramControlOrder: 0,
88
- nextPriorityReactionOrder: 0,
89
85
  activeTelegramToolExecutions: 0,
90
86
  telegramTurnDispatchPending: false,
91
87
  compactionInProgress: false,
@@ -104,10 +100,6 @@ export function createTelegramBridgeRuntime(
104
100
  syncTelegramQueueRuntimeCounters(state, counters),
105
101
  allocateItemOrder: () => allocateTelegramQueueItemOrder(state),
106
102
  allocateControlOrder: () => allocateTelegramQueueControlOrder(state),
107
- getNextPriorityReactionOrder: () =>
108
- getNextTelegramPriorityReactionOrder(state),
109
- incrementNextPriorityReactionOrder: () =>
110
- incrementNextTelegramPriorityReactionOrder(state),
111
103
  },
112
104
  lifecycle: {
113
105
  syncFlags: (flags) => syncTelegramLifecycleRuntimeFlags(state, flags),
@@ -159,9 +151,6 @@ export function syncTelegramQueueRuntimeCounters(
159
151
  state.nextQueuedTelegramControlOrder =
160
152
  counters.nextQueuedTelegramControlOrder;
161
153
  }
162
- if (counters.nextPriorityReactionOrder !== undefined) {
163
- state.nextPriorityReactionOrder = counters.nextPriorityReactionOrder;
164
- }
165
154
  }
166
155
 
167
156
  export function allocateTelegramQueueItemOrder(
@@ -176,18 +165,6 @@ export function allocateTelegramQueueControlOrder(
176
165
  return state.nextQueuedTelegramControlOrder++;
177
166
  }
178
167
 
179
- export function getNextTelegramPriorityReactionOrder(
180
- state: TelegramBridgeRuntimeState,
181
- ): number {
182
- return state.nextPriorityReactionOrder;
183
- }
184
-
185
- export function incrementNextTelegramPriorityReactionOrder(
186
- state: TelegramBridgeRuntimeState,
187
- ): void {
188
- state.nextPriorityReactionOrder += 1;
189
- }
190
-
191
168
  export function syncTelegramLifecycleRuntimeFlags(
192
169
  state: TelegramBridgeRuntimeState,
193
170
  flags: Partial<TelegramRuntimeLifecycleFlags>,
package/lib/updates.ts CHANGED
@@ -133,24 +133,61 @@ export function getTelegramQueueReactionDisposition(
133
133
  emojis,
134
134
  TELEGRAM_REMOVAL_REACTION_EMOJIS,
135
135
  );
136
- if (suppressionEmoji) return { kind: "suppressed", emoji: suppressionEmoji };
137
136
  const priorityEmoji = getTelegramReactionEmoji(
138
137
  emojis,
139
138
  TELEGRAM_PRIORITY_REACTION_EMOJIS,
140
139
  );
140
+ if (suppressionEmoji && priorityEmoji) {
141
+ return {
142
+ kind: "priority-suppressed",
143
+ priorityEmoji,
144
+ suppressionEmoji,
145
+ };
146
+ }
147
+ if (suppressionEmoji) return { kind: "suppressed", emoji: suppressionEmoji };
141
148
  if (priorityEmoji) return { kind: "priority", emoji: priorityEmoji };
142
149
  return { kind: "default" };
143
150
  }
144
151
 
145
- function areTelegramQueueReactionDispositionsEqual(
146
- left: TelegramQueueReactionDisposition,
147
- right: TelegramQueueReactionDisposition,
148
- ): boolean {
149
- return (
150
- left.kind === right.kind &&
151
- (left.kind === "default" ||
152
- (right.kind !== "default" && left.emoji === right.emoji))
152
+ function getTelegramQueueReactionTransition(
153
+ oldReactions: TelegramReactionType[],
154
+ newReactions: TelegramReactionType[],
155
+ ): TelegramQueueReactionDisposition | undefined {
156
+ const oldEmojis = collectTelegramReactionEmojis(oldReactions);
157
+ const newEmojis = collectTelegramReactionEmojis(newReactions);
158
+ const oldPriorityEmoji = getTelegramReactionEmoji(
159
+ oldEmojis,
160
+ TELEGRAM_PRIORITY_REACTION_EMOJIS,
153
161
  );
162
+ const newPriorityEmoji = getTelegramReactionEmoji(
163
+ newEmojis,
164
+ TELEGRAM_PRIORITY_REACTION_EMOJIS,
165
+ );
166
+ const oldSuppressionEmoji = getTelegramReactionEmoji(
167
+ oldEmojis,
168
+ TELEGRAM_REMOVAL_REACTION_EMOJIS,
169
+ );
170
+ const newSuppressionEmoji = getTelegramReactionEmoji(
171
+ newEmojis,
172
+ TELEGRAM_REMOVAL_REACTION_EMOJIS,
173
+ );
174
+ if (
175
+ oldPriorityEmoji === newPriorityEmoji &&
176
+ oldSuppressionEmoji === newSuppressionEmoji
177
+ ) {
178
+ return undefined;
179
+ }
180
+ const transition: Extract<
181
+ TelegramQueueReactionDisposition,
182
+ { kind: "reaction-transition" }
183
+ > = { kind: "reaction-transition" };
184
+ if (oldPriorityEmoji !== newPriorityEmoji) {
185
+ transition.priorityEmoji = newPriorityEmoji ?? null;
186
+ }
187
+ if (oldSuppressionEmoji !== newSuppressionEmoji) {
188
+ transition.suppressionEmoji = newSuppressionEmoji ?? null;
189
+ }
190
+ return transition;
154
191
  }
155
192
 
156
193
  export function extractDeletedTelegramMessageIds(
@@ -849,6 +886,19 @@ export type TelegramMessageOwnershipRecorder = (
849
886
  input: TelegramMessageOwnershipRecorderInput,
850
887
  ) => void;
851
888
 
889
+ interface TelegramUnauthorizedReplyOptions {
890
+ parseMode?: "HTML";
891
+ target?: { chatId: number; threadId?: number };
892
+ }
893
+
894
+ const TELEGRAM_UNAUTHORIZED_DENIAL_COPY = "Access denied.";
895
+
896
+ function formatTelegramUnauthorizedDenial(format: "plain" | "html"): string {
897
+ return format === "html"
898
+ ? `🚫 <b>${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}</b>`
899
+ : `🚫 ${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}`;
900
+ }
901
+
852
902
  export interface TelegramUpdateRuntimeDeps<
853
903
  TContext = unknown,
854
904
  TReactionUpdate extends TelegramMessageReactionUpdated =
@@ -890,7 +940,11 @@ export interface TelegramUpdateRuntimeDeps<
890
940
  callbackQueryId: string,
891
941
  text?: string,
892
942
  ) => Promise<void>;
893
- answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
943
+ answerGuestQuery: (
944
+ guestQueryId: string,
945
+ text?: string,
946
+ options?: Pick<TelegramUnauthorizedReplyOptions, "parseMode">,
947
+ ) => Promise<void>;
894
948
  handleAuthorizedTelegramCallbackQuery: (
895
949
  query: TCallbackQuery,
896
950
  ctx: TContext,
@@ -899,7 +953,7 @@ export interface TelegramUpdateRuntimeDeps<
899
953
  chatId: number,
900
954
  replyToMessageId: number,
901
955
  text: string,
902
- options?: { target?: { chatId: number; threadId?: number } },
956
+ options?: TelegramUnauthorizedReplyOptions,
903
957
  ) => Promise<number | undefined>;
904
958
  handleAuthorizedTelegramMessage: (
905
959
  message: TMessage,
@@ -959,7 +1013,11 @@ export interface TelegramUpdateRuntimeControllerDeps<
959
1013
  callbackQueryId: string,
960
1014
  text?: string,
961
1015
  ) => Promise<void>;
962
- answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
1016
+ answerGuestQuery: (
1017
+ guestQueryId: string,
1018
+ text?: string,
1019
+ options?: Pick<TelegramUnauthorizedReplyOptions, "parseMode">,
1020
+ ) => Promise<void>;
963
1021
  handleAuthorizedTelegramCallbackQuery: (
964
1022
  query: TCallbackQuery,
965
1023
  ctx: TContext,
@@ -968,7 +1026,7 @@ export interface TelegramUpdateRuntimeControllerDeps<
968
1026
  chatId: number,
969
1027
  replyToMessageId: number,
970
1028
  text: string,
971
- options?: { target?: { chatId: number; threadId?: number } },
1029
+ options?: TelegramUnauthorizedReplyOptions,
972
1030
  ) => Promise<number | undefined>;
973
1031
  handleAuthorizedTelegramMessage: (
974
1032
  message: TMessage,
@@ -1299,17 +1357,11 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
1299
1357
  typeof reactionUpdate.chat.id === "number"
1300
1358
  ? { chatId: reactionUpdate.chat.id }
1301
1359
  : undefined;
1302
- const oldDisposition = getTelegramQueueReactionDisposition(
1360
+ const reactionTransition = getTelegramQueueReactionTransition(
1303
1361
  reactionUpdate.old_reaction,
1304
- );
1305
- const newDisposition = getTelegramQueueReactionDisposition(
1306
1362
  reactionUpdate.new_reaction,
1307
1363
  );
1308
- if (
1309
- areTelegramQueueReactionDispositionsEqual(oldDisposition, newDisposition)
1310
- ) {
1311
- return;
1312
- }
1364
+ if (!reactionTransition) return;
1313
1365
  deps.assertExecutionCurrent?.();
1314
1366
  await deps.flushPendingMediaGroupMessage?.(reactionUpdate.message_id);
1315
1367
  deps.assertExecutionCurrent?.();
@@ -1317,7 +1369,7 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
1317
1369
  deps.assertExecutionCurrent?.();
1318
1370
  deps.applyQueuedTelegramTurnReactionByMessageId(
1319
1371
  reactionUpdate.message_id,
1320
- newDisposition,
1372
+ reactionTransition,
1321
1373
  deps.ctx,
1322
1374
  reactionScope,
1323
1375
  );
@@ -1418,7 +1470,7 @@ export async function executeTelegramUpdatePlan<
1418
1470
  assertExecutionCurrent();
1419
1471
  await deps.answerCallbackQuery(
1420
1472
  callbackQueryId,
1421
- "This bot is not authorized for your account.",
1473
+ formatTelegramUnauthorizedDenial("plain"),
1422
1474
  );
1423
1475
  }
1424
1476
  return;
@@ -1433,7 +1485,8 @@ export async function executeTelegramUpdatePlan<
1433
1485
  assertExecutionCurrent();
1434
1486
  await deps.answerGuestQuery(
1435
1487
  plan.guestMessage.guest_query_id,
1436
- "🚫 Access denied.",
1488
+ formatTelegramUnauthorizedDenial("html"),
1489
+ { parseMode: "HTML" },
1437
1490
  );
1438
1491
  return;
1439
1492
  }
@@ -1553,8 +1606,8 @@ export async function executeTelegramUpdatePlan<
1553
1606
  await deps.sendTextReply(
1554
1607
  replyTarget.chatId,
1555
1608
  replyTarget.messageId,
1556
- "This bot is not authorized for your account.",
1557
- { target: replyTarget },
1609
+ formatTelegramUnauthorizedDenial("html"),
1610
+ { parseMode: "HTML", target: replyTarget },
1558
1611
  );
1559
1612
  }
1560
1613
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.35.2",
3
+ "version": "0.36.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: generated-control-surface
3
- description: Proactively compiles current state, available capabilities, and user intent into contextual, evidence-backed, ephemeral prompt-button interfaces. Use on Telegram turns and other prompt-button transports whenever controls materially shorten likely feedback, without requiring an explicit user request, while omitting decorative UI and preserving domain ownership; fixed transport menus and callbacks remain with their runtime owners.
3
+ description: Proactively compiles current state, available capabilities, and user intent into contextual, evidence-backed, ephemeral prompt-button interfaces. Use on Telegram turns and other prompt-button transports whenever controls materially shorten likely feedback, without requiring an explicit user request; route reusable deterministic loops toward Generative Apps, omit decorative UI, and preserve domain ownership while fixed transport menus and callbacks remain with their runtime owners.
4
4
  ---
5
5
 
6
6
  # Generated Control Surface
@@ -23,7 +23,11 @@ The primitive belongs to the Surface plane: it projects State, exposes Agency ca
23
23
 
24
24
  ## Scope
25
25
 
26
- Use this Skill only to synthesize a state-derived prompt-button surface. Operating or modifying an existing Telegram bridge menu, callback interface, fixed frontend, or runtime-owned control stays with that subsystem and does not route here merely because it contains buttons.
26
+ Use this Skill only to synthesize a state-derived prompt-button surface. Operating or modifying an existing Telegram bridge menu, callback interface, fixed frontend, runtime-owned control, or installed Generative App stays with that subsystem and does not route here merely because it contains buttons.
27
+
28
+ This Skill and `generative-apps` share one logical button-matrix and `label + prompt` interaction model while retaining different execution owners. On Telegram, the bridge runtime owns the full JSON/CML action notation: this Skill emits an ephemeral surface from current context, while a Generative App returns reusable views through its script ABI. Shared rendering does not justify a third button Skill or move transport grammar into either interaction Skill.
29
+
30
+ When a generated surface reveals repeated stable interaction with bounded state and deterministic transitions, load the complementary `generative-apps` Skill and consider compiling that loop. A Generative App may retain ordinary model-mediated prompt buttons beside deterministic bound methods, so only the stable actions need bypass inference. Conversely, keep one-off, interpretive, changing, and context-heavy interaction here when a reusable script would add no concrete latency, token, cost, reliability, or UX value.
27
31
 
28
32
  On Telegram turns, evaluate this Skill proactively rather than waiting for the user to ask for buttons. Load and apply it when a likely next decision, approval, navigation step, inspection, or bounded action can be made materially easier through controls; its correct output may still contain zero buttons when the admission test fails.
29
33
 
@@ -80,7 +84,9 @@ A surface normally contains:
80
84
 
81
85
  Prefer 2–6 controls for feedback and decisions; navigation collections may use up to 12 when the additional entries remain scannable. Split larger sets by category or page instead of building a button wall. Do not add navigation controls when the surface is a one-step decision.
82
86
 
83
- Present compact metadata as stacked key-value rows that reuse status-surface grammar: a short bold label, a colon, and an inline-code value when the value is path-like, numeric, an identifier, or machine state. Prefer ``**Path:** `/home/llb` `` and ``**Entries:** `1–10 of 52` `` on separate lines over prose fragments joined by a middle dot or other decorative section separator.
87
+ When a logical grid has no semantic column headings but Markdown table syntax requires a header row, use the grid's first data row as the syntactic header and render each remaining row once beneath it. Do not insert blank, dash-only, duplicate, or invented placeholder headings: they add a false row to the projected topology. Use ordinary semantic headings when the data actually has named columns.
88
+
89
+ Present compact metadata as stacked key-value rows that reuse status-surface grammar: a short bold label, a colon, and an inline-code value when the value is path-like, numeric, an identifier, or machine state. In Telegram Rich Markdown, use an actual Markdown list or blank paragraph boundaries so soft line breaks cannot collapse several fields into one visual line. Prefer ``- **Path:** `/home/llb` `` and ``- **Entries:** `1–10 of 52` `` over prose fragments joined by a middle dot or other decorative section separator.
84
90
 
85
91
  ## Truth Modes
86
92
 
@@ -118,7 +124,7 @@ Re-check mutable targets immediately before execution. Access denial never autho
118
124
 
119
125
  ## Prompt Buttons
120
126
 
121
- Use the transport's canonical prompt-button syntax. For pi-telegram, one top-level `telegram_button` comment accepts one JSON object, double-quoted attributes, a JSON matrix, or Compact Matrix Literal (CML). CML uses `{value}` or `{label|prompt}`, trims atom boundaries, preserves other printable text literally, and decodes only `\|`, `\}`, and `\\`; use JSON for multiline prompts, styles, or metadata. A top-level cell becomes one full-width row, while a nested row groups one or more controls horizontally without a parser-level width cap. Prefer one layout comment for multiple controls instead of repeating the marker; `telegram_buttons` is a plural alias, not a different format.
127
+ Use the transport's canonical prompt-button syntax. For pi-telegram, one top-level `telegram_button` comment accepts one JSON object, double-quoted attributes, an adaptive JSON/CML matrix, or positional Compact Matrix Literal (CML). One matrix or row may mix named JSON objects with positional cells, and commas are optional only between completed elements while JSON object internals remain strict. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires an explicit prompt and accepts only `primary`, `success`, or `danger`. It trims atom boundaries, preserves other printable text literally, and decodes only `\|`, `\}`, and `\\`. Prefer CML whenever the model authors the control and it can express the required surface; fall back to expanded JSON only for multiline prompts, non-positional metadata, or a concrete CML parse/render failure, never merely from implementation habit. Deterministic Generative App scripts may return ordinary JSON because their source payload does not consume model-output tokens; author and operate those adapters through the `generative-apps` Skill rather than growing a parallel app workflow here. A top-level cell becomes one full-width row, while a nested row groups one or more controls horizontally without a parser-level width cap. Prefer one layout comment for multiple controls instead of repeating the marker; `telegram_buttons` is a plural alias, not a different format.
122
128
 
123
129
  ### Semantic Row Composition
124
130
 
@@ -158,6 +164,8 @@ Preserve the ordinary admission test: proactively offer an interactive surface e
158
164
  - Keep trivial interaction state in the visible conversation. When state becomes too large, long-lived, or error-prone for reliable conversational reconstruction, persist a small human-auditable Markdown state artifact at a deterministic task-owned path and render from it. The artifact belongs to the underlying task or domain, not to this Skill as shadow application state.
159
165
  - When transition rules are non-trivial or correctness-sensitive, use a small deterministic state-transition owner—script, module, tool, or existing domain API—that validates `current state + admitted action → next state`; let the model compile the surface from its result instead of informally simulating every transition. Do not create code or files for a trivial one-step interaction.
160
166
  - Treat repeated clicks against current state, not stale button appearance. If an action is already consumed or unavailable, keep state unchanged and say so briefly. Preserve an occupied or selected button when spatial layout matters, using its label or selected style as the visual state; omit unavailable controls when layout does not matter. Transport-level disabled buttons are optional, not assumed.
167
+ - Preserve tap-ahead on transports where existing controls remain actionable and rapid clicks queue separate turns. In a source-then-destination interaction, persist the source selection but do not regenerate the board, enumerate destinations, or duplicate controls between the two prompts; emit at most a minimal acknowledgement and let the already visible surface carry the destination click. Regenerate after the completed transition, invalid input, or evidence that the transport cannot preserve the intermediate surface.
168
+ - Resolve coordinate selection by current state rather than rigid click parity. Clicking any currently selectable source selects or replaces the source and then waits; acknowledge a replacement tersely without regenerating the surface. Only a click that is not a selectable source becomes a destination attempt when a source is already selected, at which point the domain owner validates the transition. Without a selected source, a non-source coordinate is a no-op.
161
169
 
162
170
  ```html
163
171
  <!-- telegram_button {"label":"🔍 Inspect run","prompt":"Inspect Run run:example read-only, summarize its current status and latest material evidence, then regenerate relevant supervision controls."} -->