@llblab/pi-telegram 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram bridge extension entrypoint and orchestration layer
3
+ * Zones: telegram, pi agent, orchestration
3
4
  * Keeps the runtime wiring in one place while delegating reusable domain logic to /lib modules
4
5
  */
5
6
 
@@ -9,14 +10,17 @@ import * as Attachments from "./lib/attachments.ts";
9
10
  import * as Commands from "./lib/commands.ts";
10
11
  import * as CommandTemplates from "./lib/command-templates.ts";
11
12
  import * as Config from "./lib/config.ts";
13
+ import * as Keyboard from "./lib/keyboard.ts";
12
14
  import * as Lifecycle from "./lib/lifecycle.ts";
13
15
  import * as Locks from "./lib/locks.ts";
14
16
  import * as Media from "./lib/media.ts";
15
17
  import * as Menu from "./lib/menu.ts";
18
+ import * as MenuQueue from "./lib/menu-queue.ts";
16
19
  import * as Model from "./lib/model.ts";
17
20
  import * as Pi from "./lib/pi.ts";
18
21
  import * as Polling from "./lib/polling.ts";
19
22
  import * as Preview from "./lib/preview.ts";
23
+ import * as PromptTemplates from "./lib/prompt-templates.ts";
20
24
  import * as Prompts from "./lib/prompts.ts";
21
25
  import * as Queue from "./lib/queue.ts";
22
26
  import * as Replies from "./lib/replies.ts";
@@ -33,7 +37,15 @@ type RuntimeTelegramQueueItem = Queue.TelegramQueueItem<Pi.ExtensionContext>;
33
37
 
34
38
  export default function (pi: Pi.ExtensionAPI) {
35
39
  const piRuntime = Pi.createExtensionApiRuntimePorts(pi);
40
+ const {
41
+ getCommands,
42
+ getThinkingLevel,
43
+ sendUserMessage,
44
+ setModel,
45
+ setThinkingLevel,
46
+ } = piRuntime;
36
47
  const bridgeRuntime = Runtime.createTelegramBridgeRuntime();
48
+ const { abort, lifecycle, queue, setup, typing } = bridgeRuntime;
37
49
  const configStore = Config.createTelegramConfigStore();
38
50
  const lockRuntime = Locks.createTelegramLockRuntime<Pi.ExtensionContext>();
39
51
  const activeTurnRuntime = Queue.createTelegramActiveTurnStore();
@@ -46,6 +58,11 @@ export default function (pi: Pi.ExtensionAPI) {
46
58
  const runtimeEvents = Status.createTelegramRuntimeEventRecorder({
47
59
  getBotToken: configStore.getBotToken,
48
60
  });
61
+ const recordRuntimeEvent = runtimeEvents.record;
62
+ const getContextModel = Pi.getExtensionContextModel;
63
+ const isIdle = Pi.isExtensionContextIdle;
64
+ const hasPendingMessages = Pi.hasExtensionContextPendingMessages;
65
+ const compact = Pi.compactExtensionContext;
49
66
  const mediaGroupRuntime = Media.createTelegramMediaGroupController<
50
67
  Api.TelegramMessage,
51
68
  Pi.ExtensionContext
@@ -54,7 +71,7 @@ export default function (pi: Pi.ExtensionAPI) {
54
71
  Queue.createTelegramQueueStore<Pi.ExtensionContext>();
55
72
  const deferredQueueDispatchRuntime =
56
73
  Queue.createTelegramDeferredQueueDispatchRuntime<Pi.ExtensionContext>({
57
- recordRuntimeEvent: runtimeEvents.record,
74
+ recordRuntimeEvent,
58
75
  });
59
76
  const pollingControllerState = Polling.createTelegramPollingControllerState();
60
77
  const { getStatusLines, updateStatus } =
@@ -68,9 +85,9 @@ export default function (pi: Pi.ExtensionAPI) {
68
85
  ),
69
86
  getActiveSourceMessageIds: activeTurnRuntime.getSourceMessageIds,
70
87
  hasActiveTurn: activeTurnRuntime.has,
71
- hasDispatchPending: bridgeRuntime.lifecycle.hasDispatchPending,
72
- isCompactionInProgress: bridgeRuntime.lifecycle.isCompactionInProgress,
73
- getActiveToolExecutions: bridgeRuntime.lifecycle.getActiveToolExecutions,
88
+ hasDispatchPending: lifecycle.hasDispatchPending,
89
+ isCompactionInProgress: lifecycle.isCompactionInProgress,
90
+ getActiveToolExecutions: lifecycle.getActiveToolExecutions,
74
91
  hasPendingModelSwitch: pendingModelSwitchStore.has,
75
92
  getQueuedItems: telegramQueueStore.getQueuedItems,
76
93
  formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
@@ -81,16 +98,15 @@ export default function (pi: Pi.ExtensionAPI) {
81
98
  Pi.ExtensionContext,
82
99
  ActivePiModel
83
100
  >({
84
- getContextModel: Pi.getExtensionContextModel,
101
+ getContextModel,
85
102
  updateStatus,
86
103
  });
87
104
  const queueMutationRuntime =
88
105
  Queue.createTelegramQueueMutationController<Pi.ExtensionContext>({
89
106
  ...telegramQueueStore,
90
- getNextPriorityReactionOrder:
91
- bridgeRuntime.queue.getNextPriorityReactionOrder,
107
+ getNextPriorityReactionOrder: queue.getNextPriorityReactionOrder,
92
108
  incrementNextPriorityReactionOrder:
93
- bridgeRuntime.queue.incrementNextPriorityReactionOrder,
109
+ queue.incrementNextPriorityReactionOrder,
94
110
  updateStatus,
95
111
  });
96
112
  const attachmentHandlerRuntime =
@@ -99,7 +115,7 @@ export default function (pi: Pi.ExtensionAPI) {
99
115
  getHandlers: configStore.getAttachmentHandlers,
100
116
  execCommand: CommandTemplates.execCommandTemplate,
101
117
  getCwd: Pi.getExtensionContextCwd,
102
- recordRuntimeEvent: runtimeEvents.record,
118
+ recordRuntimeEvent,
103
119
  },
104
120
  );
105
121
 
@@ -119,19 +135,19 @@ export default function (pi: Pi.ExtensionAPI) {
119
135
  prepareTempDir,
120
136
  } = Api.createDefaultTelegramBridgeApiRuntime({
121
137
  getBotToken: configStore.getBotToken,
122
- recordRuntimeEvent: runtimeEvents.record,
138
+ recordRuntimeEvent,
123
139
  });
124
140
 
125
141
  // --- Message Delivery & Preview ---
126
142
 
127
143
  const promptDispatchRuntime =
128
144
  Runtime.createTelegramPromptDispatchRuntime<Pi.ExtensionContext>({
129
- lifecycle: bridgeRuntime.lifecycle,
130
- typing: bridgeRuntime.typing,
145
+ lifecycle,
146
+ typing,
131
147
  getDefaultChatId: activeTurnRuntime.getChatId,
132
148
  sendTypingAction,
133
149
  updateStatus,
134
- recordRuntimeEvent: runtimeEvents.record,
150
+ recordRuntimeEvent,
135
151
  });
136
152
 
137
153
  // --- Reply Runtime Wiring ---
@@ -143,7 +159,7 @@ export default function (pi: Pi.ExtensionAPI) {
143
159
  editInteractiveMessage,
144
160
  sendInteractiveMessage,
145
161
  } =
146
- Replies.createTelegramRenderedMessageDeliveryRuntime<Menu.TelegramReplyMarkup>(
162
+ Replies.createTelegramRenderedMessageDeliveryRuntime<Keyboard.TelegramInlineKeyboardMarkup>(
147
163
  {
148
164
  sendMessage,
149
165
  editMessage: editTelegramMessageText,
@@ -152,19 +168,22 @@ export default function (pi: Pi.ExtensionAPI) {
152
168
  const dispatchNextQueuedTelegramTurn =
153
169
  Queue.createTelegramQueueDispatchRuntime<Pi.ExtensionContext>({
154
170
  ...telegramQueueStore,
155
- isCompactionInProgress: bridgeRuntime.lifecycle.isCompactionInProgress,
171
+ isCompactionInProgress: lifecycle.isCompactionInProgress,
156
172
  hasActiveTurn: activeTurnRuntime.has,
157
- hasDispatchPending: bridgeRuntime.lifecycle.hasDispatchPending,
158
- isIdle: Pi.isExtensionContextIdle,
159
- hasPendingMessages: Pi.hasExtensionContextPendingMessages,
173
+ hasDispatchPending: lifecycle.hasDispatchPending,
174
+ isIdle,
175
+ hasPendingMessages,
160
176
  hasDispatchContext: deferredQueueDispatchRuntime.isBound,
161
177
  updateStatus,
162
178
  sendTextReply,
163
- recordRuntimeEvent: runtimeEvents.record,
179
+ recordRuntimeEvent,
164
180
  ...promptDispatchRuntime,
165
- sendUserMessage: piRuntime.sendUserMessage,
181
+ sendUserMessage,
166
182
  }).dispatchNext;
167
- const previewRuntime = Preview.createTelegramAssistantPreviewRuntime({
183
+ const previewRuntime = Preview.createTelegramAssistantPreviewRuntime<
184
+ unknown,
185
+ Keyboard.TelegramInlineKeyboardMarkup
186
+ >({
168
187
  getActiveTurn: activeTurnRuntime.get,
169
188
  isAssistantMessage: Replies.isAssistantAgentMessage,
170
189
  getMessageText: Replies.getAgentMessageText,
@@ -182,18 +201,26 @@ export default function (pi: Pi.ExtensionAPI) {
182
201
  Pi.ExtensionContext,
183
202
  Model.ScopedTelegramModel<ActivePiModel>
184
203
  >({
185
- isIdle: Pi.isExtensionContextIdle,
204
+ isIdle,
186
205
  getPendingModelSwitch: pendingModelSwitchStore.get,
187
206
  setPendingModelSwitch: pendingModelSwitchStore.set,
188
207
  getActiveTurn: activeTurnRuntime.get,
189
- getAbortHandler: bridgeRuntime.abort.getHandler,
190
- hasAbortHandler: bridgeRuntime.abort.hasHandler,
191
- getActiveToolExecutions: bridgeRuntime.lifecycle.getActiveToolExecutions,
192
- allocateItemOrder: bridgeRuntime.queue.allocateItemOrder,
193
- allocateControlOrder: bridgeRuntime.queue.allocateControlOrder,
208
+ getAbortHandler: abort.getHandler,
209
+ hasAbortHandler: abort.hasHandler,
210
+ getActiveToolExecutions: lifecycle.getActiveToolExecutions,
211
+ allocateItemOrder: queue.allocateItemOrder,
212
+ allocateControlOrder: queue.allocateControlOrder,
194
213
  appendQueuedItem: queueMutationRuntime.append,
195
214
  updateStatus,
196
215
  });
216
+ const getQueueItemCount = Queue.createTelegramQueueItemCountGetter(
217
+ telegramQueueStore,
218
+ );
219
+ const getPromptTemplateCommands =
220
+ PromptTemplates.createTelegramPromptTemplateCommandGetter({
221
+ getCommands,
222
+ reservedCommandNames: Commands.TELEGRAM_RESERVED_COMMAND_NAMES,
223
+ });
197
224
  const menuActions = Menu.createTelegramMenuActionRuntimeWithStateBuilder<
198
225
  ActivePiModel,
199
226
  Pi.ExtensionContext
@@ -201,20 +228,80 @@ export default function (pi: Pi.ExtensionAPI) {
201
228
  runtime: modelMenuRuntime,
202
229
  createSettingsManager: Pi.createSettingsManager,
203
230
  getActiveModel: currentModelRuntime.get,
204
- getThinkingLevel: piRuntime.getThinkingLevel,
205
- buildStatusHtml: Status.createTelegramStatusHtmlBuilder({
206
- getActiveModel: currentModelRuntime.get,
231
+ getThinkingLevel,
232
+ getQueueItemCount,
233
+ buildStatusHtml: Commands.createTelegramAppMenuHtmlBuilder({
234
+ buildStatusHtml: Status.createTelegramStatusHtmlBuilder({
235
+ getActiveModel: currentModelRuntime.get,
236
+ }),
237
+ getPromptTemplateCommands,
207
238
  }),
208
239
  storeModelMenuState: modelMenuRuntime.storeState,
209
- isIdle: Pi.isExtensionContextIdle,
240
+ isIdle,
210
241
  canOfferInFlightModelSwitch: modelSwitchController.canOfferInFlightSwitch,
211
242
  sendTextReply,
212
243
  editInteractiveMessage,
213
244
  sendInteractiveMessage,
214
245
  });
215
246
 
247
+ // --- Queue Menu ---
248
+
249
+ const getQueueMenuState = Menu.createTelegramModelMenuStateBuilder({
250
+ runtime: modelMenuRuntime,
251
+ createSettingsManager: Pi.createSettingsManager,
252
+ getActiveModel: currentModelRuntime.get,
253
+ });
254
+ const queueMenuRuntime = MenuQueue.createTelegramQueueMenuRuntime({
255
+ telegramQueueStore,
256
+ queueMutationRuntime,
257
+ sendInteractiveMessage,
258
+ editInteractiveMessage,
259
+ answerCallbackQuery,
260
+ getModelMenuState: getQueueMenuState,
261
+ getStoredModelMenuState: modelMenuRuntime.getState,
262
+ storeModelMenuState: modelMenuRuntime.storeState,
263
+ updateStatusMessage: menuActions.updateStatusMessage,
264
+ updateStatus,
265
+ });
266
+
216
267
  // --- Polling ---
217
268
 
269
+ const inboundRouteRuntime = Routing.createTelegramInboundRouteRuntime<
270
+ Api.TelegramUpdate,
271
+ Api.TelegramMessage,
272
+ Api.TelegramCallbackQuery,
273
+ Pi.ExtensionContext,
274
+ ActivePiModel
275
+ >({
276
+ configStore,
277
+ bridgeRuntime,
278
+ activeTurnRuntime,
279
+ mediaGroupRuntime,
280
+ telegramQueueStore,
281
+ queueMutationRuntime,
282
+ modelMenuRuntime,
283
+ currentModelRuntime,
284
+ modelSwitchController,
285
+ menuActions,
286
+ openQueueMenu: queueMenuRuntime.openQueueMenu,
287
+ queueMenuCallbackHandler: queueMenuRuntime.handleCallbackQuery,
288
+ buttonActionStore,
289
+ attachmentHandlerRuntime,
290
+ updateStatus,
291
+ dispatchNextQueuedTelegramTurn,
292
+ answerCallbackQuery,
293
+ sendTextReply,
294
+ setMyCommands,
295
+ getCommands,
296
+ downloadFile: downloadTelegramBridgeFile,
297
+ getThinkingLevel,
298
+ setThinkingLevel,
299
+ setModel,
300
+ isIdle,
301
+ hasPendingMessages,
302
+ compact,
303
+ recordRuntimeEvent,
304
+ });
218
305
  const pollingRuntime = Polling.createTelegramPollingControllerRuntime<
219
306
  Api.TelegramUpdate,
220
307
  Pi.ExtensionContext
@@ -225,42 +312,10 @@ export default function (pi: Pi.ExtensionAPI) {
225
312
  deleteWebhook,
226
313
  getUpdates,
227
314
  persistConfig: configStore.persist,
228
- handleUpdate: Routing.createTelegramInboundRouteRuntime<
229
- Api.TelegramUpdate,
230
- Api.TelegramMessage,
231
- Api.TelegramCallbackQuery,
232
- Pi.ExtensionContext,
233
- ActivePiModel
234
- >({
235
- configStore,
236
- bridgeRuntime,
237
- activeTurnRuntime,
238
- mediaGroupRuntime,
239
- telegramQueueStore,
240
- queueMutationRuntime,
241
- modelMenuRuntime,
242
- currentModelRuntime,
243
- modelSwitchController,
244
- menuActions,
245
- buttonActionStore,
246
- attachmentHandlerRuntime,
247
- updateStatus,
248
- dispatchNextQueuedTelegramTurn,
249
- answerCallbackQuery,
250
- sendTextReply,
251
- setMyCommands,
252
- downloadFile: downloadTelegramBridgeFile,
253
- getThinkingLevel: piRuntime.getThinkingLevel,
254
- setThinkingLevel: piRuntime.setThinkingLevel,
255
- setModel: piRuntime.setModel,
256
- isIdle: Pi.isExtensionContextIdle,
257
- hasPendingMessages: Pi.hasExtensionContextPendingMessages,
258
- compact: Pi.compactExtensionContext,
259
- recordRuntimeEvent: runtimeEvents.record,
260
- }).handleUpdate,
261
- stopTypingLoop: bridgeRuntime.typing.stop,
315
+ handleUpdate: inboundRouteRuntime.handleUpdate,
316
+ stopTypingLoop: typing.stop,
262
317
  updateStatus,
263
- recordRuntimeEvent: runtimeEvents.record,
318
+ recordRuntimeEvent,
264
319
  });
265
320
  const lockedPollingRuntime = Locks.createTelegramLockedPollingRuntime({
266
321
  lock: lockRuntime,
@@ -268,34 +323,35 @@ export default function (pi: Pi.ExtensionAPI) {
268
323
  startPolling: pollingRuntime.start,
269
324
  stopPolling: pollingRuntime.stop,
270
325
  updateStatus,
271
- recordRuntimeEvent: runtimeEvents.record,
326
+ recordRuntimeEvent,
327
+ });
328
+ const queueSessionLifecycle = Queue.createTelegramSessionLifecycleRuntime<
329
+ Pi.ExtensionContext,
330
+ RuntimeTelegramQueueItem,
331
+ ActivePiModel
332
+ >({
333
+ getCurrentModel: getContextModel,
334
+ loadConfig: configStore.load,
335
+ setQueuedItems: telegramQueueStore.setQueuedItems,
336
+ setCurrentModel: currentModelRuntime.set,
337
+ setPendingModelSwitch: pendingModelSwitchStore.set,
338
+ syncCounters: queue.syncCounters,
339
+ syncFlags: lifecycle.syncFlags,
340
+ bindDeferredDispatchContext: deferredQueueDispatchRuntime.bind,
341
+ prepareTempDir,
342
+ updateStatus,
343
+ unbindDeferredDispatchContext: deferredQueueDispatchRuntime.unbind,
344
+ clearPendingMediaGroups: mediaGroupRuntime.clear,
345
+ clearModelMenuState: modelMenuRuntime.clear,
346
+ getActiveTurnChatId: activeTurnRuntime.getChatId,
347
+ clearPreview: previewRuntime.clear,
348
+ clearActiveTurn: activeTurnRuntime.clear,
349
+ clearAbort: abort.clearHandler,
350
+ stopPolling: lockedPollingRuntime.suspend,
351
+ recordRuntimeEvent,
272
352
  });
273
353
  const sessionLifecycleRuntime = Lifecycle.appendTelegramLifecycleHooks(
274
- Queue.createTelegramSessionLifecycleRuntime<
275
- Pi.ExtensionContext,
276
- RuntimeTelegramQueueItem,
277
- ActivePiModel
278
- >({
279
- getCurrentModel: Pi.getExtensionContextModel,
280
- loadConfig: configStore.load,
281
- setQueuedItems: telegramQueueStore.setQueuedItems,
282
- setCurrentModel: currentModelRuntime.set,
283
- setPendingModelSwitch: pendingModelSwitchStore.set,
284
- syncCounters: bridgeRuntime.queue.syncCounters,
285
- syncFlags: bridgeRuntime.lifecycle.syncFlags,
286
- bindDeferredDispatchContext: deferredQueueDispatchRuntime.bind,
287
- prepareTempDir,
288
- updateStatus,
289
- unbindDeferredDispatchContext: deferredQueueDispatchRuntime.unbind,
290
- clearPendingMediaGroups: mediaGroupRuntime.clear,
291
- clearModelMenuState: modelMenuRuntime.clear,
292
- getActiveTurnChatId: activeTurnRuntime.getChatId,
293
- clearPreview: previewRuntime.clear,
294
- clearActiveTurn: activeTurnRuntime.clear,
295
- clearAbort: bridgeRuntime.abort.clearHandler,
296
- stopPolling: lockedPollingRuntime.suspend,
297
- recordRuntimeEvent: runtimeEvents.record,
298
- }),
354
+ queueSessionLifecycle,
299
355
  { onSessionStart: lockedPollingRuntime.onSessionStart },
300
356
  );
301
357
 
@@ -303,19 +359,19 @@ export default function (pi: Pi.ExtensionAPI) {
303
359
 
304
360
  Attachments.registerTelegramAttachmentTool(pi, {
305
361
  getActiveTurn: activeTurnRuntime.get,
306
- recordRuntimeEvent: runtimeEvents.record,
362
+ recordRuntimeEvent,
307
363
  });
308
364
 
309
365
  Commands.registerTelegramBridgeCommands(pi, {
310
366
  promptForConfig: Setup.createTelegramSetupPromptRuntime({
311
367
  getConfig: configStore.get,
312
368
  setConfig: configStore.set,
313
- setupGuard: bridgeRuntime.setup,
369
+ setupGuard: setup,
314
370
  getMe: Api.fetchTelegramBotIdentity,
315
371
  persistConfig: configStore.persist,
316
372
  startPolling: lockedPollingRuntime.start,
317
373
  updateStatus,
318
- recordRuntimeEvent: runtimeEvents.record,
374
+ recordRuntimeEvent,
319
375
  }),
320
376
  getStatusLines,
321
377
  reloadConfig: configStore.load,
@@ -327,68 +383,76 @@ export default function (pi: Pi.ExtensionAPI) {
327
383
 
328
384
  // --- Lifecycle Hooks ---
329
385
 
386
+ const agentEndResetter = Runtime.createTelegramAgentEndResetter({
387
+ abort,
388
+ typing,
389
+ clearActiveTurn: activeTurnRuntime.clear,
390
+ resetToolExecutions: lifecycle.resetActiveToolExecutions,
391
+ clearPendingModelSwitch: modelSwitchController.clearPendingSwitch,
392
+ clearDispatchPending: lifecycle.clearDispatchPending,
393
+ });
394
+ const queuedAttachmentSender =
395
+ Attachments.createTelegramQueuedAttachmentSender({
396
+ sendMultipart: callMultipart,
397
+ sendTextReply,
398
+ recordRuntimeEvent,
399
+ });
400
+ const outboundReplyPlanner =
401
+ OutboundHandlers.createTelegramOutboundReplyPlanner(buttonActionStore);
402
+ const outboundReplyArtifactSender =
403
+ OutboundHandlers.createTelegramOutboundReplyArtifactSender({
404
+ execCommand: CommandTemplates.execCommandTemplate,
405
+ sendMultipart: callMultipart,
406
+ sendTextReply,
407
+ getHandlers: configStore.getOutboundHandlers,
408
+ recordRuntimeEvent,
409
+ });
410
+ const agentLifecycleHooks = Queue.createTelegramAgentLifecycleHooks<
411
+ Queue.PendingTelegramTurn,
412
+ Pi.ExtensionContext,
413
+ unknown,
414
+ Keyboard.TelegramInlineKeyboardMarkup
415
+ >({
416
+ setAbortHandler: Runtime.createTelegramContextAbortHandlerSetter(abort),
417
+ getQueuedItems: telegramQueueStore.getQueuedItems,
418
+ hasPendingDispatch: lifecycle.hasDispatchPending,
419
+ hasActiveTurn: activeTurnRuntime.has,
420
+ resetToolExecutions: lifecycle.resetActiveToolExecutions,
421
+ resetPendingModelSwitch: modelSwitchController.clearPendingSwitch,
422
+ setQueuedItems: telegramQueueStore.setQueuedItems,
423
+ clearDispatchPending: lifecycle.clearDispatchPending,
424
+ setActiveTurn: activeTurnRuntime.set,
425
+ createPreviewState: previewRuntime.resetState,
426
+ startTypingLoop: promptDispatchRuntime.startTypingLoop,
427
+ updateStatus,
428
+ getActiveTurn: activeTurnRuntime.get,
429
+ extractAssistant: Replies.extractLatestAssistantMessageText,
430
+ getPreserveQueuedTurnsAsHistory: lifecycle.shouldPreserveQueuedTurnsAsHistory,
431
+ resetRuntimeState: agentEndResetter,
432
+ dispatchNextQueuedTelegramTurn,
433
+ requestDeferredDispatchNextQueuedTelegramTurn:
434
+ deferredQueueDispatchRuntime.request,
435
+ clearPreview: previewRuntime.clear,
436
+ setPreviewPendingText: previewRuntime.setPendingText,
437
+ finalizeMarkdownPreview: previewRuntime.finalizeMarkdown,
438
+ sendMarkdownReply,
439
+ sendTextReply,
440
+ sendQueuedAttachments: queuedAttachmentSender,
441
+ planOutboundReply: outboundReplyPlanner,
442
+ sendOutboundReplyArtifacts: outboundReplyArtifactSender,
443
+ getActiveToolExecutions: lifecycle.getActiveToolExecutions,
444
+ setActiveToolExecutions: lifecycle.setActiveToolExecutions,
445
+ triggerPendingModelSwitchAbort: modelSwitchController.triggerPendingAbort,
446
+ });
447
+ // Wire transport-level reply dedup reset via lifecycle
448
+ Lifecycle.setResetTransportReplyDedup(Replies.resetTransportReplyDedup);
449
+ const agentStartWithDedupReset = Lifecycle.createAgentStartDedupHook(agentLifecycleHooks.onAgentStart);
330
450
  Lifecycle.registerTelegramLifecycleHooks(pi, {
331
451
  ...sessionLifecycleRuntime,
452
+ ...agentLifecycleHooks,
453
+ onAgentStart: agentStartWithDedupReset,
332
454
  onBeforeAgentStart: Prompts.createTelegramBeforeAgentStartHook(),
333
455
  onModelSelect: currentModelRuntime.onModelSelect,
334
- ...Queue.createTelegramAgentLifecycleHooks<
335
- Queue.PendingTelegramTurn,
336
- Pi.ExtensionContext,
337
- unknown
338
- >({
339
- setAbortHandler: Runtime.createTelegramContextAbortHandlerSetter(
340
- bridgeRuntime.abort,
341
- ),
342
- getQueuedItems: telegramQueueStore.getQueuedItems,
343
- hasPendingDispatch: bridgeRuntime.lifecycle.hasDispatchPending,
344
- hasActiveTurn: activeTurnRuntime.has,
345
- resetToolExecutions: bridgeRuntime.lifecycle.resetActiveToolExecutions,
346
- resetPendingModelSwitch: modelSwitchController.clearPendingSwitch,
347
- setQueuedItems: telegramQueueStore.setQueuedItems,
348
- clearDispatchPending: bridgeRuntime.lifecycle.clearDispatchPending,
349
- setActiveTurn: activeTurnRuntime.set,
350
- createPreviewState: previewRuntime.resetState,
351
- startTypingLoop: promptDispatchRuntime.startTypingLoop,
352
- updateStatus,
353
- getActiveTurn: activeTurnRuntime.get,
354
- extractAssistant: Replies.extractLatestAssistantMessageText,
355
- getPreserveQueuedTurnsAsHistory:
356
- bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory,
357
- resetRuntimeState: Runtime.createTelegramAgentEndResetter({
358
- abort: bridgeRuntime.abort,
359
- typing: bridgeRuntime.typing,
360
- clearActiveTurn: activeTurnRuntime.clear,
361
- resetToolExecutions: bridgeRuntime.lifecycle.resetActiveToolExecutions,
362
- clearPendingModelSwitch: modelSwitchController.clearPendingSwitch,
363
- clearDispatchPending: bridgeRuntime.lifecycle.clearDispatchPending,
364
- }),
365
- dispatchNextQueuedTelegramTurn,
366
- requestDeferredDispatchNextQueuedTelegramTurn:
367
- deferredQueueDispatchRuntime.request,
368
- clearPreview: previewRuntime.clear,
369
- setPreviewPendingText: previewRuntime.setPendingText,
370
- finalizeMarkdownPreview: previewRuntime.finalizeMarkdown,
371
- sendMarkdownReply,
372
- sendTextReply,
373
- sendQueuedAttachments: Attachments.createTelegramQueuedAttachmentSender({
374
- sendMultipart: callMultipart,
375
- sendTextReply,
376
- recordRuntimeEvent: runtimeEvents.record,
377
- }),
378
- planOutboundReply:
379
- OutboundHandlers.createTelegramOutboundReplyPlanner(buttonActionStore),
380
- sendOutboundReplyArtifacts:
381
- OutboundHandlers.createTelegramOutboundReplyArtifactSender({
382
- execCommand: CommandTemplates.execCommandTemplate,
383
- sendMultipart: callMultipart,
384
- sendTextReply,
385
- getHandlers: configStore.getOutboundHandlers,
386
- recordRuntimeEvent: runtimeEvents.record,
387
- }),
388
- getActiveToolExecutions: bridgeRuntime.lifecycle.getActiveToolExecutions,
389
- setActiveToolExecutions: bridgeRuntime.lifecycle.setActiveToolExecutions,
390
- triggerPendingModelSwitchAbort: modelSwitchController.triggerPendingAbort,
391
- }),
392
456
  onMessageStart: previewRuntime.onMessageStart,
393
457
  onMessageUpdate: previewRuntime.onMessageUpdate,
394
458
  });
package/lib/api.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram API transport helpers
3
+ * Zones: telegram transport, filesystem, runtime diagnostics
3
4
  * Wraps bot API calls, file downloads, runtime transport binding, and Telegram temp-file cleanup
4
5
  */
5
6
 
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram inbound attachment handler pipeline
3
+ * Zones: telegram inbound, command templates, prompt preparation
3
4
  * Owns MIME/type matching, command-template execution, fallback handling, and prompt injection before prompt enqueueing
4
5
  */
5
6
 
@@ -299,6 +300,7 @@ async function executeTelegramAttachmentHandlerInvocation(
299
300
  const result = await deps.execCommand(invocation.command, invocation.args, {
300
301
  cwd,
301
302
  timeout,
303
+ ...(typeof handler === "object" && handler.retry !== undefined ? { retry: handler.retry } : {}),
302
304
  ...(stdin !== undefined ? { stdin } : {}),
303
305
  });
304
306
  if (result.code !== 0)
@@ -342,15 +344,20 @@ async function executeTelegramAttachmentHandler(
342
344
  const startedAt = Date.now();
343
345
  let output = "";
344
346
  for (const [index, step] of steps.entries()) {
345
- output = await executeTelegramAttachmentHandlerInvocation(
346
- step,
347
- file,
348
- cwd,
349
- deps,
350
- false,
351
- getTelegramAttachmentCompositionStepTimeout(handler, step, startedAt),
352
- index === 0 ? undefined : output,
353
- );
347
+ try {
348
+ output = await executeTelegramAttachmentHandlerInvocation(
349
+ step,
350
+ file,
351
+ cwd,
352
+ deps,
353
+ false,
354
+ getTelegramAttachmentCompositionStepTimeout(handler, step, startedAt),
355
+ index === 0 ? undefined : output,
356
+ );
357
+ } catch (error) {
358
+ if (typeof step === "object" && step.critical) throw error;
359
+ output = "";
360
+ }
354
361
  }
355
362
  return output.trim();
356
363
  }
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram attachment domain helpers
3
+ * Zones: telegram outbound, pi agent tool, filesystem
3
4
  * Owns telegram_attach registration, attachment queueing, and attachment delivery so Telegram file output stays in one domain module
4
5
  */
5
6
 
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Command-template standard helpers
3
+ * Zones: shared utils, local process execution, automation standard
3
4
  * Owns shell-free command-template splitting, placeholder defaults, composition expansion, executable path expansion, and direct execution
4
5
  */
5
6
 
@@ -7,12 +8,16 @@ import { spawn } from "node:child_process";
7
8
  import { homedir } from "node:os";
8
9
  import { isAbsolute, resolve } from "node:path";
9
10
 
11
+ export const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
12
+
10
13
  export interface CommandTemplateObjectConfig {
11
14
  template?: CommandTemplateValue;
12
15
  args?: string[];
13
16
  defaults?: Record<string, unknown>;
14
17
  timeout?: number;
15
18
  output?: string;
19
+ retry?: number;
20
+ critical?: boolean;
16
21
  }
17
22
 
18
23
  export type CommandTemplateValue = string | CommandTemplateConfig[];
@@ -34,6 +39,7 @@ export interface CommandTemplateExecOptions {
34
39
  signal?: AbortSignal;
35
40
  stdin?: string;
36
41
  killGrace?: number;
42
+ retry?: number;
37
43
  }
38
44
 
39
45
  export interface CommandTemplateExecResult {
@@ -100,7 +106,13 @@ export function expandCommandTemplateConfigs(
100
106
  }
101
107
  if (typeof normalizedConfig.template !== "string") return [];
102
108
  return [
103
- { ...normalizedConfig, ...context, template: normalizedConfig.template },
109
+ {
110
+ ...normalizedConfig,
111
+ ...context,
112
+ template: normalizedConfig.template,
113
+ retry: normalizedConfig.retry,
114
+ critical: normalizedConfig.critical,
115
+ },
104
116
  ];
105
117
  }
106
118
 
@@ -192,7 +204,22 @@ export function substituteCommandTemplateToken(
192
204
  );
193
205
  }
194
206
 
195
- export function execCommandTemplate(
207
+ export async function execCommandTemplate(
208
+ command: string,
209
+ args: string[],
210
+ options: CommandTemplateExecOptions = {},
211
+ ): Promise<CommandTemplateExecResult> {
212
+ const maxAttempts = options.retry ?? 1;
213
+ let lastResult: CommandTemplateExecResult = { stdout: "", stderr: "", code: 1, killed: false };
214
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
215
+ const result = await execCommandTemplateOnce(command, args, options);
216
+ if (result.code === 0) return result;
217
+ lastResult = result;
218
+ }
219
+ return lastResult;
220
+ }
221
+
222
+ function execCommandTemplateOnce(
196
223
  command: string,
197
224
  args: string[],
198
225
  options: CommandTemplateExecOptions = {},
@@ -231,8 +258,10 @@ export function execCommandTemplate(
231
258
  else
232
259
  options.signal.addEventListener("abort", killProcess, { once: true });
233
260
  }
234
- if (options.timeout && options.timeout > 0)
261
+ if (options.timeout !== undefined && options.timeout > 0)
235
262
  timeoutId = setTimeout(killProcess, options.timeout);
263
+ else if (options.timeout === undefined)
264
+ timeoutId = setTimeout(killProcess, DEFAULT_COMMAND_TIMEOUT_MS);
236
265
  proc.stdout?.on("data", (data) => {
237
266
  stdout += data.toString();
238
267
  });