@llblab/pi-telegram 0.46.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/AGENTS.md +4 -2
  2. package/BACKLOG.md +0 -6
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +5 -2
  5. package/dist/api/activity.d.ts +6 -0
  6. package/dist/api/activity.js +6 -0
  7. package/dist/api/commands.d.ts +6 -0
  8. package/dist/api/commands.js +6 -0
  9. package/dist/api/delivery.d.ts +6 -0
  10. package/dist/api/delivery.js +6 -0
  11. package/dist/api/inbound.d.ts +6 -0
  12. package/dist/api/inbound.js +6 -0
  13. package/dist/api/keyboard.d.ts +6 -0
  14. package/dist/api/keyboard.js +6 -0
  15. package/dist/api/outbound.d.ts +6 -0
  16. package/dist/api/outbound.js +6 -0
  17. package/dist/api/sections.d.ts +7 -0
  18. package/dist/api/sections.js +6 -0
  19. package/dist/api/status.d.ts +6 -0
  20. package/dist/api/status.js +6 -0
  21. package/dist/api/updates.d.ts +6 -0
  22. package/dist/api/updates.js +6 -0
  23. package/dist/api/voice.d.ts +6 -0
  24. package/dist/api/voice.js +6 -0
  25. package/dist/index.d.ts +6 -0
  26. package/dist/index.js +6 -0
  27. package/dist/lib/activity-verbosity.d.ts +52 -0
  28. package/dist/lib/activity-verbosity.js +596 -0
  29. package/dist/lib/activity.d.ts +220 -0
  30. package/dist/lib/activity.js +574 -0
  31. package/dist/lib/agent-messages.d.ts +28 -0
  32. package/dist/lib/agent-messages.js +86 -0
  33. package/dist/lib/bindings.d.ts +250 -0
  34. package/dist/lib/bindings.js +936 -0
  35. package/dist/lib/bus-api.d.ts +18 -0
  36. package/dist/lib/bus-api.js +254 -0
  37. package/dist/lib/bus-follower.d.ts +464 -0
  38. package/dist/lib/bus-follower.js +1686 -0
  39. package/dist/lib/bus-leader.d.ts +292 -0
  40. package/dist/lib/bus-leader.js +2242 -0
  41. package/dist/lib/bus-transport.d.ts +64 -0
  42. package/dist/lib/bus-transport.js +140 -0
  43. package/dist/lib/bus.d.ts +518 -0
  44. package/dist/lib/bus.js +2055 -0
  45. package/dist/lib/channel-posts.d.ts +170 -0
  46. package/dist/lib/channel-posts.js +611 -0
  47. package/dist/lib/command-templates.d.ts +70 -0
  48. package/dist/lib/command-templates.js +744 -0
  49. package/dist/lib/commands.d.ts +541 -0
  50. package/dist/lib/commands.js +1155 -0
  51. package/dist/lib/config.d.ts +252 -0
  52. package/dist/lib/config.js +889 -0
  53. package/dist/lib/delivery.d.ts +163 -0
  54. package/dist/lib/delivery.js +542 -0
  55. package/dist/lib/extension.d.ts +7 -0
  56. package/dist/lib/extension.js +1604 -0
  57. package/dist/lib/generative-app-worker.mjs +104 -0
  58. package/dist/lib/generative-apps.d.ts +212 -0
  59. package/dist/lib/generative-apps.js +1006 -0
  60. package/dist/lib/inbound.d.ts +91 -0
  61. package/dist/lib/inbound.js +502 -0
  62. package/dist/lib/journal.d.ts +665 -0
  63. package/dist/lib/journal.js +3701 -0
  64. package/dist/lib/keyboard.d.ts +23 -0
  65. package/dist/lib/keyboard.js +37 -0
  66. package/dist/lib/lifecycle.d.ts +157 -0
  67. package/dist/lib/lifecycle.js +395 -0
  68. package/dist/lib/locks.d.ts +164 -0
  69. package/dist/lib/locks.js +1208 -0
  70. package/dist/lib/logging.d.ts +50 -0
  71. package/dist/lib/logging.js +274 -0
  72. package/dist/lib/media.d.ts +181 -0
  73. package/dist/lib/media.js +602 -0
  74. package/dist/lib/menu-model.d.ts +217 -0
  75. package/dist/lib/menu-model.js +663 -0
  76. package/dist/lib/menu-queue.d.ts +37 -0
  77. package/dist/lib/menu-queue.js +408 -0
  78. package/dist/lib/menu-settings.d.ts +115 -0
  79. package/dist/lib/menu-settings.js +595 -0
  80. package/dist/lib/menu-status.d.ts +32 -0
  81. package/dist/lib/menu-status.js +112 -0
  82. package/dist/lib/menu-thinking.d.ts +29 -0
  83. package/dist/lib/menu-thinking.js +82 -0
  84. package/dist/lib/menu.d.ts +171 -0
  85. package/dist/lib/menu.js +323 -0
  86. package/dist/lib/model.d.ts +127 -0
  87. package/dist/lib/model.js +409 -0
  88. package/dist/lib/outbound-attachments.d.ts +241 -0
  89. package/dist/lib/outbound-attachments.js +639 -0
  90. package/dist/lib/outbound-buttons.d.ts +69 -0
  91. package/dist/lib/outbound-buttons.js +248 -0
  92. package/dist/lib/outbound-markup.d.ts +42 -0
  93. package/dist/lib/outbound-markup.js +678 -0
  94. package/dist/lib/outbound-voice.d.ts +55 -0
  95. package/dist/lib/outbound-voice.js +152 -0
  96. package/dist/lib/outbound.d.ts +185 -0
  97. package/dist/lib/outbound.js +516 -0
  98. package/dist/lib/ownership.d.ts +77 -0
  99. package/dist/lib/ownership.js +174 -0
  100. package/dist/lib/paths.d.ts +40 -0
  101. package/dist/lib/paths.js +94 -0
  102. package/dist/lib/pi.d.ts +72 -0
  103. package/dist/lib/pi.js +121 -0
  104. package/dist/lib/polling.d.ts +351 -0
  105. package/dist/lib/polling.js +1196 -0
  106. package/dist/lib/preview.d.ts +192 -0
  107. package/dist/lib/preview.js +614 -0
  108. package/dist/lib/prompt-templates.d.ts +24 -0
  109. package/dist/lib/prompt-templates.js +118 -0
  110. package/dist/lib/prompts.d.ts +57 -0
  111. package/dist/lib/prompts.js +167 -0
  112. package/dist/lib/queue.d.ts +766 -0
  113. package/dist/lib/queue.js +1965 -0
  114. package/dist/lib/recovery.d.ts +86 -0
  115. package/dist/lib/recovery.js +285 -0
  116. package/dist/lib/rendering.d.ts +20 -0
  117. package/dist/lib/rendering.js +983 -0
  118. package/dist/lib/replies.d.ts +214 -0
  119. package/dist/lib/replies.js +737 -0
  120. package/dist/lib/routing.d.ts +207 -0
  121. package/dist/lib/routing.js +2282 -0
  122. package/dist/lib/runtime.d.ts +172 -0
  123. package/dist/lib/runtime.js +402 -0
  124. package/dist/lib/sections.d.ts +165 -0
  125. package/dist/lib/sections.js +327 -0
  126. package/dist/lib/setup.d.ts +82 -0
  127. package/dist/lib/setup.js +150 -0
  128. package/dist/lib/skills.d.ts +8 -0
  129. package/dist/lib/skills.js +12 -0
  130. package/dist/lib/status.d.ts +442 -0
  131. package/dist/lib/status.js +1008 -0
  132. package/dist/lib/sync.d.ts +179 -0
  133. package/dist/lib/sync.js +782 -0
  134. package/dist/lib/target.d.ts +20 -0
  135. package/dist/lib/target.js +27 -0
  136. package/dist/lib/telegram-api.d.ts +541 -0
  137. package/dist/lib/telegram-api.js +1159 -0
  138. package/dist/lib/text-groups.d.ts +89 -0
  139. package/dist/lib/text-groups.js +317 -0
  140. package/dist/lib/thread-cleanup-manager.d.ts +288 -0
  141. package/dist/lib/thread-cleanup-manager.js +560 -0
  142. package/dist/lib/thread-display.d.ts +46 -0
  143. package/dist/lib/thread-display.js +257 -0
  144. package/dist/lib/thread-naming.d.ts +46 -0
  145. package/dist/lib/thread-naming.js +78 -0
  146. package/dist/lib/thread-reconciler.d.ts +239 -0
  147. package/dist/lib/thread-reconciler.js +644 -0
  148. package/dist/lib/threads.d.ts +621 -0
  149. package/dist/lib/threads.js +3679 -0
  150. package/dist/lib/time-injection.d.ts +15 -0
  151. package/dist/lib/time-injection.js +56 -0
  152. package/dist/lib/turns.d.ts +109 -0
  153. package/dist/lib/turns.js +500 -0
  154. package/dist/lib/updates.d.ts +1290 -0
  155. package/dist/lib/updates.js +3634 -0
  156. package/dist/lib/voice.d.ts +117 -0
  157. package/dist/lib/voice.js +174 -0
  158. package/dist/lib/workspace-admission.d.ts +264 -0
  159. package/dist/lib/workspace-admission.js +1136 -0
  160. package/dist/lib/workspace-retirement.d.ts +219 -0
  161. package/dist/lib/workspace-retirement.js +587 -0
  162. package/dist/lib/workspace-slots.d.ts +30 -0
  163. package/dist/lib/workspace-slots.js +54 -0
  164. package/dist/package.json +126 -0
  165. package/dist/pi-telegram/index.js +1 -0
  166. package/dist/skills/generated-control-surface/SKILL.md +107 -0
  167. package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
  168. package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
  169. package/dist/skills/generative-apps/SKILL.md +115 -0
  170. package/dist/skills/show-me/SKILL.md +166 -0
  171. package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
  172. package/dist/skills/telegram-bridge/SKILL.md +129 -0
  173. package/dist/skills/telegram-bridge/references/configuration.md +15 -0
  174. package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
  175. package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
  176. package/docs/README.md +2 -0
  177. package/docs/architecture.md +1 -1
  178. package/docs/compact-matrix-literal.md +10 -3
  179. package/docs/generative-apps.md +15 -13
  180. package/docs/multi-instance-bus.md +6 -4
  181. package/docs/outbound.md +1 -1
  182. package/docs/public-api.md +3 -3
  183. package/lib/bindings.ts +89 -6
  184. package/lib/bus-follower.ts +7 -3
  185. package/lib/bus-leader.ts +44 -19
  186. package/lib/bus.ts +4 -1
  187. package/lib/commands.ts +2 -2
  188. package/lib/config.ts +14 -7
  189. package/lib/delivery.ts +38 -6
  190. package/lib/extension.ts +7 -0
  191. package/lib/generative-apps.ts +379 -13
  192. package/lib/menu-settings.ts +20 -6
  193. package/lib/outbound-markup.ts +16 -7
  194. package/lib/status.ts +15 -12
  195. package/lib/telegram-api.ts +12 -1
  196. package/lib/thread-display.ts +96 -18
  197. package/package.json +56 -13
  198. package/scripts/build-dist.mjs +44 -0
  199. package/scripts/measure-bus.mjs +8 -1
  200. package/scripts/measure-workspace.mjs +8 -1
  201. package/skills/generative-apps/SKILL.md +1 -1
  202. package/skills/show-me/SKILL.md +1 -1
  203. package/skills/show-me/references/telegram-surfaces.md +1 -1
  204. package/skills/telegram-bridge/SKILL.md +1 -1
@@ -4,7 +4,7 @@
4
4
  * Owns Generative App identity, installation, invocation, state history, and telegram_bind
5
5
  */
6
6
 
7
- import { randomUUID } from "node:crypto";
7
+ import { createHash, randomUUID } from "node:crypto";
8
8
  import {
9
9
  appendFile,
10
10
  copyFile,
@@ -33,6 +33,8 @@ const GENERATIVE_APP_LOCK_WAIT_MS = 12_000;
33
33
  const GENERATIVE_APP_RUN_MAX_TIMEOUT_MS = 30_000;
34
34
  const GENERATIVE_APP_RUN_MAX_ARGS = 64;
35
35
  const GENERATIVE_APP_RUN_MAX_STREAM_BYTES = 64 * 1024;
36
+ export const GENERATIVE_APP_MIN_REFRESH_AFTER_MS = 2_000;
37
+ export const GENERATIVE_APP_MAX_REFRESH_AFTER_MS = 24 * 60 * 60 * 1_000;
36
38
 
37
39
  export type GenerativeAppJsonValue =
38
40
  | null
@@ -66,6 +68,7 @@ export interface GenerativeAppMethodContext {
66
68
 
67
69
  export interface GenerativeAppMethodResult {
68
70
  output: string;
71
+ refreshAfterMs?: number;
69
72
  state?: GenerativeAppJsonValue;
70
73
  viewMode?: "new" | "edit";
71
74
  }
@@ -82,6 +85,7 @@ export interface GenerativeAppInvocationResult {
82
85
  output: string;
83
86
  app: string;
84
87
  revision: number;
88
+ refreshAfterMs?: number;
85
89
  stateChanged: boolean;
86
90
  viewMode: "new" | "edit";
87
91
  }
@@ -104,7 +108,89 @@ export interface GenerativeAppRuntimeOptions {
104
108
  methodTimeoutMs?: number;
105
109
  }
106
110
 
111
+ export interface GenerativeAppLiveSurfaceFrame<THandle> {
112
+ digest: string;
113
+ handle: THandle;
114
+ }
115
+
116
+ export interface GenerativeAppLiveSurface<THandle> {
117
+ app: string;
118
+ appGeneration: string;
119
+ appRevision: number;
120
+ handle: THandle;
121
+ initialDigest: string;
122
+ key: string;
123
+ refreshAfterMs: number;
124
+ }
125
+
126
+ export interface GenerativeAppLiveSurfaceRuntime<THandle> {
127
+ open: (surface: GenerativeAppLiveSurface<THandle>) => void;
128
+ cancel: (key: string) => void;
129
+ take: (key: string) => GenerativeAppLiveSurface<THandle> | undefined;
130
+ resume: (
131
+ surface: GenerativeAppLiveSurface<THandle>,
132
+ result: GenerativeAppInvocationResult,
133
+ ) => Promise<void>;
134
+ refreshNow: (key: string) => Promise<void>;
135
+ shutdown: () => void;
136
+ }
137
+
138
+ export interface GenerativeAppLiveSurfaceRuntimeDeps<THandle> {
139
+ agentDir: string;
140
+ isCurrent: (surface: GenerativeAppLiveSurface<THandle>) => boolean;
141
+ plan: (
142
+ result: GenerativeAppInvocationResult,
143
+ handle: THandle,
144
+ ) => GenerativeAppLiveSurfaceFrame<THandle>;
145
+ edit: (
146
+ frame: GenerativeAppLiveSurfaceFrame<THandle>,
147
+ ) => Promise<THandle>;
148
+ classifyEditError?: (error: unknown) =>
149
+ | { kind: "retry"; retryAfterMs?: number }
150
+ | { kind: "terminal" | "unavailable" | "unknown" };
151
+ recordRuntimeEvent?: (
152
+ category: string,
153
+ error: unknown,
154
+ details?: Record<string, unknown>,
155
+ ) => void;
156
+ setTimer?: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
157
+ clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
158
+ }
159
+
160
+ export interface TelegramBindDeliveryHandle {
161
+ readonly target: { chatId: number; threadId?: number };
162
+ readonly messageIds: readonly number[];
163
+ readonly generation: string;
164
+ }
165
+
166
+ export interface TelegramBindLiveHandle {
167
+ delivery: TelegramBindDeliveryHandle;
168
+ view?: { text: string; parseMode: "markdown"; replyMarkup?: unknown };
169
+ }
170
+
171
+ export function getTelegramBindLiveSurfaceKey(
172
+ app: string,
173
+ profile: string,
174
+ target: { chatId: number; threadId?: number },
175
+ ): string {
176
+ return `${app}/${profile}/${target.chatId}/${target.threadId ?? 0}`;
177
+ }
178
+
107
179
  export interface TelegramBindToolRegistrationDeps extends GenerativeAppRuntimeOptions {
180
+ getActiveProfileName?: () => string | undefined;
181
+ liveSurfaceSetTimer?: GenerativeAppLiveSurfaceRuntimeDeps<unknown>["setTimer"];
182
+ liveSurfaceClearTimer?: GenerativeAppLiveSurfaceRuntimeDeps<unknown>["clearTimer"];
183
+ setLiveSurfaceRuntime?: (
184
+ runtime: GenerativeAppLiveSurfaceRuntime<TelegramBindLiveHandle> | undefined,
185
+ ) => void;
186
+ isDeliveryHandleCurrent?: (handle: TelegramBindDeliveryHandle) => boolean;
187
+ editView?: (
188
+ handle: TelegramBindDeliveryHandle,
189
+ view: { text: string; parseMode: "markdown"; replyMarkup?: unknown },
190
+ ) => Promise<
191
+ | { ok: true; value: TelegramBindDeliveryHandle }
192
+ | { ok: false; reason: string; message: string; retryAfterMs?: number }
193
+ >;
108
194
  getActiveTurn?: () =>
109
195
  | {
110
196
  chatId: number;
@@ -125,6 +211,13 @@ export interface TelegramBindToolRegistrationDeps extends GenerativeAppRuntimeOp
125
211
  target?: { chatId: number; threadId?: number };
126
212
  },
127
213
  ) => Promise<number | undefined>;
214
+ sendView?: (
215
+ view: { text: string; parseMode: "markdown"; replyMarkup?: unknown },
216
+ options: { scope: { kind: "active-turn" }; replyToMessageId: number },
217
+ ) => Promise<
218
+ | { ok: true; value: TelegramBindDeliveryHandle }
219
+ | { ok: false; reason?: string; message: string }
220
+ >;
128
221
  recordRuntimeEvent?: (
129
222
  category: string,
130
223
  error: unknown,
@@ -365,8 +458,24 @@ function normalizeMethodResult(value: unknown): GenerativeAppMethodResult {
365
458
  if (viewMode !== "new" && viewMode !== "edit") {
366
459
  throw new Error("Generative App viewMode must be new or edit.");
367
460
  }
461
+ let refreshAfterMs: number | undefined;
462
+ if (Object.hasOwn(result, "refreshAfterMs")) {
463
+ if (
464
+ typeof result.refreshAfterMs !== "number" ||
465
+ !Number.isFinite(result.refreshAfterMs) ||
466
+ !Number.isInteger(result.refreshAfterMs) ||
467
+ result.refreshAfterMs <= 0
468
+ ) {
469
+ throw new Error("Generative App refreshAfterMs must be a finite positive integer.");
470
+ }
471
+ refreshAfterMs = Math.min(
472
+ GENERATIVE_APP_MAX_REFRESH_AFTER_MS,
473
+ Math.max(GENERATIVE_APP_MIN_REFRESH_AFTER_MS, result.refreshAfterMs),
474
+ );
475
+ }
368
476
  return {
369
477
  output: result.output,
478
+ ...(refreshAfterMs !== undefined ? { refreshAfterMs } : {}),
370
479
  ...(Object.hasOwn(result, "state")
371
480
  ? { state: assertJsonValue(result.state, "Generative App state") }
372
481
  : {}),
@@ -595,6 +704,9 @@ async function invokeMethod(options: {
595
704
  if (options.method === "init" && result.state === undefined) {
596
705
  throw new Error("Generative App init must return state.");
597
706
  }
707
+ if (options.method === "refresh" && result.state !== undefined) {
708
+ throw new Error("Generative App refresh must be output-only.");
709
+ }
598
710
  let nextRevision = revision;
599
711
  if (result.state !== undefined) {
600
712
  options.execution?.assertCurrent();
@@ -623,6 +735,9 @@ async function invokeMethod(options: {
623
735
  output: result.output,
624
736
  app: options.app,
625
737
  revision: nextRevision,
738
+ ...(result.refreshAfterMs !== undefined
739
+ ? { refreshAfterMs: result.refreshAfterMs }
740
+ : {}),
626
741
  stateChanged: result.state !== undefined,
627
742
  viewMode: result.viewMode ?? "new",
628
743
  };
@@ -848,6 +963,154 @@ export async function bindGenerativeApp(options: GenerativeAppRuntimeOptions & {
848
963
  });
849
964
  }
850
965
 
966
+ export function createGenerativeAppLiveSurfaceRuntime<THandle>(
967
+ deps: GenerativeAppLiveSurfaceRuntimeDeps<THandle>,
968
+ ): GenerativeAppLiveSurfaceRuntime<THandle> {
969
+ interface Record {
970
+ surface: GenerativeAppLiveSurface<THandle>;
971
+ digest?: string;
972
+ timer?: ReturnType<typeof setTimeout>;
973
+ inFlight: boolean;
974
+ retryDelayMs: number;
975
+ pending?: {
976
+ frame: GenerativeAppLiveSurfaceFrame<THandle>;
977
+ result: GenerativeAppInvocationResult;
978
+ };
979
+ }
980
+ const records = new Map<string, Record>();
981
+ const setTimer: NonNullable<GenerativeAppLiveSurfaceRuntimeDeps<THandle>["setTimer"]> =
982
+ deps.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
983
+ const clearTimer: NonNullable<GenerativeAppLiveSurfaceRuntimeDeps<THandle>["clearTimer"]> =
984
+ deps.clearTimer ?? ((timer) => clearTimeout(timer));
985
+ let active = true;
986
+
987
+ const clearRecordTimer = (record: Record): void => {
988
+ if (record.timer !== undefined) clearTimer(record.timer);
989
+ record.timer = undefined;
990
+ };
991
+ const take = (key: string): GenerativeAppLiveSurface<THandle> | undefined => {
992
+ const record = records.get(key);
993
+ if (!record) return undefined;
994
+ clearRecordTimer(record);
995
+ records.delete(key);
996
+ return { ...record.surface };
997
+ };
998
+ const cancel = (key: string): void => {
999
+ take(key);
1000
+ };
1001
+ const ownsRecord = (key: string, record: Record): boolean =>
1002
+ active && records.get(key) === record;
1003
+ const schedule = (record: Record, delayMs: number): void => {
1004
+ clearRecordTimer(record);
1005
+ if (!active || records.get(record.surface.key) !== record) return;
1006
+ const timer = setTimer(() => void refreshNow(record.surface.key), delayMs);
1007
+ record.timer = timer;
1008
+ timer.unref?.();
1009
+ };
1010
+ const refreshNow = async (key: string): Promise<void> => {
1011
+ const record = records.get(key);
1012
+ if (!active || !record || record.inFlight) return;
1013
+ clearRecordTimer(record);
1014
+ if (!deps.isCurrent(record.surface)) {
1015
+ cancel(key);
1016
+ return;
1017
+ }
1018
+ record.inFlight = true;
1019
+ try {
1020
+ let pending = record.pending;
1021
+ if (!pending) {
1022
+ const result = await invokeGenerativeApp({
1023
+ agentDir: deps.agentDir,
1024
+ expectedGeneration: record.surface.appGeneration,
1025
+ expectedRevision: record.surface.appRevision,
1026
+ method: "refresh",
1027
+ app: record.surface.app,
1028
+ });
1029
+ if (!ownsRecord(key, record) || !deps.isCurrent(record.surface)) {
1030
+ if (records.get(key) === record) cancel(key);
1031
+ return;
1032
+ }
1033
+ const frame = deps.plan(result, record.surface.handle);
1034
+ pending = { frame, result };
1035
+ if (frame.digest !== record.digest) record.pending = pending;
1036
+ }
1037
+ const { frame, result } = pending;
1038
+ if (!ownsRecord(key, record)) return;
1039
+ if (frame.digest !== record.digest) {
1040
+ record.surface.handle = await deps.edit(frame);
1041
+ if (!ownsRecord(key, record)) return;
1042
+ record.digest = frame.digest;
1043
+ record.pending = undefined;
1044
+ }
1045
+ if (result.refreshAfterMs === undefined) {
1046
+ cancel(key);
1047
+ return;
1048
+ }
1049
+ record.retryDelayMs = GENERATIVE_APP_MIN_REFRESH_AFTER_MS;
1050
+ record.surface.appRevision = result.revision;
1051
+ record.surface.refreshAfterMs = result.refreshAfterMs;
1052
+ schedule(record, result.refreshAfterMs);
1053
+ } catch (error) {
1054
+ const classification = deps.classifyEditError?.(error) ?? { kind: "unknown" as const };
1055
+ if (!ownsRecord(key, record)) return;
1056
+ deps.recordRuntimeEvent?.("generative-app", error, {
1057
+ phase: "live-surface-refresh",
1058
+ app: record.surface.app,
1059
+ outcome: classification.kind,
1060
+ });
1061
+ if (classification.kind !== "retry" || !deps.isCurrent(record.surface)) {
1062
+ cancel(key);
1063
+ return;
1064
+ }
1065
+ const delay = classification.retryAfterMs === undefined
1066
+ ? record.retryDelayMs
1067
+ : Math.max(GENERATIVE_APP_MIN_REFRESH_AFTER_MS, classification.retryAfterMs);
1068
+ record.retryDelayMs = Math.min(60_000, Math.max(4_000, delay * 2));
1069
+ schedule(record, delay);
1070
+ } finally {
1071
+ record.inFlight = false;
1072
+ }
1073
+ };
1074
+ const open = (surface: GenerativeAppLiveSurface<THandle>): void => {
1075
+ cancel(surface.key);
1076
+ if (!active) return;
1077
+ const record: Record = {
1078
+ surface: { ...surface },
1079
+ digest: surface.initialDigest,
1080
+ inFlight: false,
1081
+ retryDelayMs: GENERATIVE_APP_MIN_REFRESH_AFTER_MS,
1082
+ };
1083
+ records.set(surface.key, record);
1084
+ schedule(record, surface.refreshAfterMs);
1085
+ };
1086
+ return {
1087
+ open,
1088
+ cancel,
1089
+ take,
1090
+ async resume(surface, result) {
1091
+ if (!active || !deps.isCurrent(surface)) return;
1092
+ const frame = deps.plan(result, surface.handle);
1093
+ const handle = frame.digest === surface.initialDigest
1094
+ ? surface.handle
1095
+ : await deps.edit(frame);
1096
+ if (result.refreshAfterMs === undefined || !deps.isCurrent({ ...surface, handle })) return;
1097
+ open({
1098
+ ...surface,
1099
+ appGeneration: result.generation,
1100
+ appRevision: result.revision,
1101
+ handle,
1102
+ initialDigest: frame.digest,
1103
+ refreshAfterMs: result.refreshAfterMs,
1104
+ });
1105
+ },
1106
+ refreshNow,
1107
+ shutdown() {
1108
+ active = false;
1109
+ for (const key of [...records.keys()]) cancel(key);
1110
+ },
1111
+ };
1112
+ }
1113
+
851
1114
  export function formatGenerativeAppToolOutput(output: string): string {
852
1115
  const normalized = output.replace(/^\n+/u, "");
853
1116
  return `\n${normalized || "(Generative App returned no output)"}`;
@@ -884,6 +1147,72 @@ export function registerTelegramBindTool(
884
1147
  pi: ExtensionAPI,
885
1148
  deps: TelegramBindToolRegistrationDeps,
886
1149
  ): void {
1150
+ const planFrame = (
1151
+ result: GenerativeAppInvocationResult,
1152
+ handle: TelegramBindLiveHandle,
1153
+ ): GenerativeAppLiveSurfaceFrame<TelegramBindLiveHandle> => {
1154
+ const planned = deps.planOutput!(result.output, {
1155
+ binding: {
1156
+ generation: result.generation,
1157
+ app: result.app,
1158
+ revision: result.revision,
1159
+ },
1160
+ });
1161
+ const view = {
1162
+ text: planned.markdown,
1163
+ parseMode: "markdown" as const,
1164
+ ...(planned.replyMarkup !== undefined ? { replyMarkup: planned.replyMarkup } : {}),
1165
+ };
1166
+ return {
1167
+ digest: createHash("sha256").update(JSON.stringify(view)).digest("hex"),
1168
+ handle: { delivery: handle.delivery, view },
1169
+ };
1170
+ };
1171
+ const liveSurfaces = deps.planOutput && deps.editView && deps.isDeliveryHandleCurrent
1172
+ ? createGenerativeAppLiveSurfaceRuntime<TelegramBindLiveHandle>({
1173
+ agentDir: deps.agentDir,
1174
+ isCurrent: (surface) => deps.isDeliveryHandleCurrent!(surface.handle.delivery),
1175
+ plan: planFrame,
1176
+ async edit(frame) {
1177
+ const view = frame.handle.view;
1178
+ if (!view) throw new Error("Generative App live frame is missing its planned view.");
1179
+ const edited = await deps.editView!(frame.handle.delivery, view);
1180
+ if (!edited.ok) throw Object.assign(new Error(edited.message), {
1181
+ deliveryFailureReason: edited.reason,
1182
+ ...(edited.retryAfterMs === undefined ? {} : { retryAfterMs: edited.retryAfterMs }),
1183
+ });
1184
+ return { delivery: edited.value, view };
1185
+ },
1186
+ classifyEditError(error) {
1187
+ const failure = error as {
1188
+ deliveryFailureReason?: string;
1189
+ retryAfterMs?: number;
1190
+ };
1191
+ if (failure.deliveryFailureReason === "rate-limited") {
1192
+ return { kind: "retry", retryAfterMs: failure.retryAfterMs };
1193
+ }
1194
+ if (failure.deliveryFailureReason === "transport-retryable") {
1195
+ return { kind: "retry" };
1196
+ }
1197
+ if (failure.deliveryFailureReason === "message-unavailable") {
1198
+ return { kind: "unavailable" };
1199
+ }
1200
+ return {
1201
+ kind: failure.deliveryFailureReason === "commit-unknown" ? "unknown" : "terminal",
1202
+ };
1203
+ },
1204
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1205
+ ...(deps.liveSurfaceSetTimer ? { setTimer: deps.liveSurfaceSetTimer } : {}),
1206
+ ...(deps.liveSurfaceClearTimer ? { clearTimer: deps.liveSurfaceClearTimer } : {}),
1207
+ })
1208
+ : undefined;
1209
+ deps.setLiveSurfaceRuntime?.(liveSurfaces);
1210
+ const surfaceKey = (app: string, handle: TelegramBindDeliveryHandle): string =>
1211
+ getTelegramBindLiveSurfaceKey(
1212
+ app,
1213
+ deps.getActiveProfileName?.() ?? "default",
1214
+ handle.target,
1215
+ );
887
1216
  pi.registerTool({
888
1217
  name: "telegram_bind",
889
1218
  label: "Telegram Bind",
@@ -919,7 +1248,7 @@ export function registerTelegramBindTool(
919
1248
  const activeTurn = params.display === false
920
1249
  ? undefined
921
1250
  : deps.getActiveTurn?.();
922
- if (activeTurn && deps.planOutput && deps.sendMarkdownReply) {
1251
+ if (activeTurn && deps.planOutput && (deps.sendView || deps.sendMarkdownReply)) {
923
1252
  try {
924
1253
  const planned = deps.planOutput(result.output, {
925
1254
  binding: {
@@ -928,17 +1257,54 @@ export function registerTelegramBindTool(
928
1257
  revision: result.revision,
929
1258
  },
930
1259
  });
931
- const messageId = await deps.sendMarkdownReply(
932
- activeTurn.chatId,
933
- activeTurn.replyToMessageId,
934
- planned.markdown,
935
- {
936
- ...(planned.replyMarkup !== undefined
937
- ? { replyMarkup: planned.replyMarkup }
938
- : {}),
939
- ...(activeTurn.target ? { target: activeTurn.target } : {}),
940
- },
941
- );
1260
+ let messageId: number | undefined;
1261
+ if (deps.sendView) {
1262
+ const delivered = await deps.sendView(
1263
+ {
1264
+ text: planned.markdown,
1265
+ parseMode: "markdown",
1266
+ ...(planned.replyMarkup !== undefined
1267
+ ? { replyMarkup: planned.replyMarkup }
1268
+ : {}),
1269
+ },
1270
+ {
1271
+ scope: { kind: "active-turn" },
1272
+ replyToMessageId: activeTurn.replyToMessageId,
1273
+ },
1274
+ );
1275
+ if (!delivered.ok) throw new Error(delivered.message);
1276
+ messageId = delivered.value.messageIds[0];
1277
+ if (liveSurfaces) {
1278
+ const key = surfaceKey(result.app, delivered.value);
1279
+ if (result.refreshAfterMs === undefined) {
1280
+ liveSurfaces.cancel(key);
1281
+ } else {
1282
+ const handle = { delivery: delivered.value };
1283
+ const frame = planFrame(result, handle);
1284
+ liveSurfaces.open({
1285
+ app: result.app,
1286
+ appGeneration: result.generation,
1287
+ appRevision: result.revision,
1288
+ handle,
1289
+ initialDigest: frame.digest,
1290
+ key,
1291
+ refreshAfterMs: result.refreshAfterMs,
1292
+ });
1293
+ }
1294
+ }
1295
+ } else {
1296
+ messageId = await deps.sendMarkdownReply!(
1297
+ activeTurn.chatId,
1298
+ activeTurn.replyToMessageId,
1299
+ planned.markdown,
1300
+ {
1301
+ ...(planned.replyMarkup !== undefined
1302
+ ? { replyMarkup: planned.replyMarkup }
1303
+ : {}),
1304
+ ...(activeTurn.target ? { target: activeTurn.target } : {}),
1305
+ },
1306
+ );
1307
+ }
942
1308
  return {
943
1309
  content: [{ type: "text", text: formatDisplayedGenerativeAppToolOutput() }],
944
1310
  details: { ...result, displayed: true, messageId },
@@ -175,15 +175,18 @@ export function buildTelegramSettingsMenuText(): string {
175
175
  return SETTINGS_MENU_TITLE;
176
176
  }
177
177
 
178
- export function buildThreadDisplaySettingsText(mode: TelegramThreadDisplayMode): string {
178
+ export function buildThreadDisplaySettingsText(
179
+ mode: TelegramThreadDisplayMode,
180
+ ): string {
179
181
  return [
180
- `${THREAD_DISPLAY_SETTINGS_TITLE} <code>${mode}</code>`,
182
+ `${THREAD_DISPLAY_SETTINGS_TITLE} <code>${threadDisplayModeLabel(mode)}</code>`,
181
183
  "",
182
184
  "Choose how this bot profile labels Telegram tabs and Pi terminal status. Each slot is unique across this bot profile.",
183
185
  "",
184
186
  "<code>-</code> <code>letters</code> (default): show the unique slot, such as <b><i>A</i></b> or <b><i>B</i></b>.",
185
187
  "<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
- "<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>.",
188
+ "<code>-</code> <code>directory-title</code>: humanize the distinguishing directory path, such as <b><i>Api Tools</i></b> or <b><i>Frontend / Extensions A</i></b>.",
189
+ "<code>-</code> <code>directory-snake</code>: normalize the distinguishing directory path to lowercase, such as <b><i>api_tools</i></b> or <b><i>frontend_extensions_a</i></b>.",
187
190
  ].join("\n");
188
191
  }
189
192
 
@@ -411,11 +414,21 @@ export async function openTelegramSettingsMenu<
411
414
  deps.storeModelMenuState(state);
412
415
  }
413
416
 
417
+ function threadDisplayModeLabel(mode: TelegramThreadDisplayMode): string {
418
+ switch (mode) {
419
+ case "letters": return "letters";
420
+ case "names": return "names";
421
+ case "directory-title": return "directory-title";
422
+ case "directory-snake": return "directory-snake";
423
+ default: return "letters";
424
+ }
425
+ }
426
+
414
427
  export function buildThreadDisplaySettingsReplyMarkup(mode: TelegramThreadDisplayMode): TelegramSettingsMenuReplyMarkup {
415
428
  return { inline_keyboard: [
416
429
  [{ text: "⬆️ Back", callback_data: "settings:list" }],
417
- ...(["letters", "names", "directories"] as const).map((value) => [{
418
- text: `${mode === value ? "🟢 " : ""}${value}`,
430
+ ...(["letters", "names", "directory-title", "directory-snake"] as const).map((value) => [{
431
+ text: `${mode === value ? "🟢 " : ""}${threadDisplayModeLabel(value)}`,
419
432
  callback_data: `settings:set:thread-display:${value}`,
420
433
  }]),
421
434
  ] };
@@ -630,7 +643,8 @@ export async function handleTelegramSettingsMenuCallbackAction(
630
643
  }
631
644
  if (data.startsWith("settings:set:thread-display:")) {
632
645
  const mode = data.slice("settings:set:thread-display:".length);
633
- if (mode !== "letters" && mode !== "names" && mode !== "directories") {
646
+ if (mode !== "letters" && mode !== "names" &&
647
+ mode !== "directory-snake" && mode !== "directory-title") {
634
648
  await deps.answerCallbackQuery(callbackQueryId, "Unknown Thread display mode.");
635
649
  return true;
636
650
  }
@@ -504,14 +504,23 @@ function parseTelegramAdaptiveActionPayloadRows(
504
504
  return undefined;
505
505
  };
506
506
 
507
+ const parseVerticalSequence = (): Record<string, unknown>[][] | undefined => {
508
+ const rows: Record<string, unknown>[][] = [];
509
+ while (offset < source.length) {
510
+ skipWhitespace();
511
+ if (source[offset] !== "{") break;
512
+ const cell = parseCell();
513
+ if (!cell) return undefined;
514
+ rows.push([cell]);
515
+ if (!consumeOptionalSeparator()) return undefined;
516
+ }
517
+ return rows.length > 0 ? rows : undefined;
518
+ };
519
+
507
520
  skipWhitespace();
508
- let rows: Record<string, unknown>[][] | undefined;
509
- if (source[offset] === "{") {
510
- const cell = parseCell();
511
- rows = cell ? [[cell]] : undefined;
512
- } else {
513
- rows = parseMatrix();
514
- }
521
+ const rows = source[offset] === "{"
522
+ ? parseVerticalSequence()
523
+ : parseMatrix();
515
524
  if (!rows) return undefined;
516
525
  skipWhitespace();
517
526
  if (!options.allowTrailing && offset !== source.length) return undefined;
package/lib/status.ts CHANGED
@@ -711,6 +711,14 @@ export function createTelegramBridgeStatusRuntime<
711
711
  const hasPendingModelSwitch = deps.hasPendingModelSwitch();
712
712
  const activeToolExecutions = deps.getActiveToolExecutions();
713
713
  const compactionInProgress = deps.isCompactionInProgress();
714
+ const activeWorkCount =
715
+ hasActiveTurn ||
716
+ hasPendingModelSwitch ||
717
+ activeToolExecutions > 0 ||
718
+ compactionInProgress ||
719
+ (hasPendingDispatch && queuedItemCount === 0)
720
+ ? 1
721
+ : 0;
714
722
  const localBus = deps.getLocalBus?.();
715
723
  return {
716
724
  hasBotToken: config.botHasToken ?? Boolean(config.botToken),
@@ -735,7 +743,10 @@ export function createTelegramBridgeStatusRuntime<
735
743
  activeToolExecutions,
736
744
  queuedItems: queuedItemCount,
737
745
  }),
738
- queuedStatus: deps.formatQueuedStatus(queuedItems),
746
+ queuedStatus:
747
+ activeWorkCount + queuedItemCount > 0
748
+ ? ` +${activeWorkCount + queuedItemCount}`
749
+ : "",
739
750
  pollingStopReason: deps.getPollingState?.().stopReason,
740
751
  error,
741
752
  };
@@ -937,24 +948,16 @@ export function buildTelegramStatusBarText(
937
948
  if (!state.pollingActive && state.busRole !== "follower")
938
949
  return `${theme.fg("accent", "telegram")} ${theme.fg("dim", "disconnected")}${queued}`;
939
950
  if (state.error) {
940
- return `${label} ${theme.fg("error", "error")}`;
951
+ return `${label} ${theme.fg("error", "error")}${queued}`;
941
952
  }
942
953
  if (state.busRole === "follower" && state.followerRegistered === false) {
943
954
  return `${label} ${theme.fg("warning", "reconnecting")}${queued}`;
944
955
  }
945
- if (state.processing) {
946
- const processingStatus = state.queuedStatus
947
- ? "active"
948
- : (state.processingStatus ?? "processing");
949
- const processingToken =
950
- processingStatus === "active" ? "warning" : "accent";
951
- return `${label} ${theme.fg(processingToken, processingStatus)}${queued}`;
952
- }
953
956
  if (state.busRole === "follower")
954
957
  return `${label} ${theme.fg("success", "follower")}${queued}`;
955
958
  if (state.busRole === "leader")
956
- return `${label} ${theme.fg("success", "leader")}`;
957
- return `${label} ${theme.fg("success", "connected")}`;
959
+ return `${label} ${theme.fg("success", "leader")}${queued}`;
960
+ return `${label} ${theme.fg("success", "connected")}${queued}`;
958
961
  }
959
962
 
960
963
  function formatTelegramBridgeBotStatus(
@@ -964,7 +964,7 @@ export function isTelegramApiMethodRetrySafe(method: string): boolean {
964
964
  return TELEGRAM_RETRY_SAFE_METHODS.has(method);
965
965
  }
966
966
 
967
- function isRetryableTelegramApiError(error: unknown): boolean {
967
+ export function isRetryableTelegramApiError(error: unknown): boolean {
968
968
  return (
969
969
  error instanceof TelegramApiHttpError &&
970
970
  (error.status === 429 ||
@@ -972,6 +972,17 @@ function isRetryableTelegramApiError(error: unknown): boolean {
972
972
  );
973
973
  }
974
974
 
975
+ export function getTelegramApiRetryAfterMs(error: unknown): number | undefined {
976
+ return error instanceof TelegramApiHttpError && error.retryAfterSeconds !== undefined
977
+ ? Math.max(0, error.retryAfterSeconds * 1000)
978
+ : undefined;
979
+ }
980
+
981
+ export function isTelegramMessageUnavailableError(error: unknown): boolean {
982
+ return error instanceof TelegramApiHttpError && error.status === 400 &&
983
+ /Bad Request: (message to edit not found|message not found|message_id_invalid)/iu.test(error.message);
984
+ }
985
+
975
986
  function getTelegramRetryDelayMs(
976
987
  error: unknown,
977
988
  attempt: number,