@llblab/pi-telegram 0.2.9 → 0.3.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/lib/queue.ts CHANGED
@@ -3,9 +3,6 @@
3
3
  * Owns queue items, queue mutations, dispatch and lifecycle planning, session resets, and queue-adjacent runtime helpers
4
4
  */
5
5
 
6
- import type { ImageContent, Model, TextContent } from "@mariozechner/pi-ai";
7
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
8
-
9
6
  // --- Queue Items ---
10
7
 
11
8
  export interface QueuedAttachment {
@@ -13,8 +10,56 @@ export interface QueuedAttachment {
13
10
  fileName: string;
14
11
  }
15
12
 
13
+ export interface TelegramPromptTextContent {
14
+ type: "text";
15
+ text: string;
16
+ }
17
+
18
+ export interface TelegramPromptImageContent {
19
+ type: "image";
20
+ data: string;
21
+ mimeType: string;
22
+ }
23
+
24
+ export type TelegramPromptContent =
25
+ | TelegramPromptTextContent
26
+ | TelegramPromptImageContent;
27
+
16
28
  export type TelegramQueueItemKind = "prompt" | "control";
17
29
  export type TelegramQueueLane = "control" | "priority" | "default";
30
+ export type TelegramQueueAdmissionMode =
31
+ | "control-queue"
32
+ | "priority-queue"
33
+ | "default-queue";
34
+
35
+ export interface TelegramQueueLaneContract {
36
+ lane: TelegramQueueLane;
37
+ admissionMode: TelegramQueueAdmissionMode;
38
+ dispatchRank: number;
39
+ allowedKinds: readonly TelegramQueueItemKind[];
40
+ }
41
+
42
+ export const TELEGRAM_QUEUE_LANE_CONTRACTS: readonly TelegramQueueLaneContract[] =
43
+ [
44
+ {
45
+ lane: "control",
46
+ admissionMode: "control-queue",
47
+ dispatchRank: 0,
48
+ allowedKinds: ["control", "prompt"],
49
+ },
50
+ {
51
+ lane: "priority",
52
+ admissionMode: "priority-queue",
53
+ dispatchRank: 1,
54
+ allowedKinds: ["prompt"],
55
+ },
56
+ {
57
+ lane: "default",
58
+ admissionMode: "default-queue",
59
+ dispatchRank: 2,
60
+ allowedKinds: ["prompt"],
61
+ },
62
+ ] as const;
18
63
 
19
64
  export interface TelegramQueueItemBase {
20
65
  kind: TelegramQueueItemKind;
@@ -30,19 +75,44 @@ export interface PendingTelegramTurn extends TelegramQueueItemBase {
30
75
  kind: "prompt";
31
76
  sourceMessageIds: number[];
32
77
  queuedAttachments: QueuedAttachment[];
33
- content: Array<TextContent | ImageContent>;
78
+ content: TelegramPromptContent[];
34
79
  historyText: string;
35
80
  }
36
81
 
37
- export interface PendingTelegramControlItem extends TelegramQueueItemBase {
82
+ export interface PendingTelegramControlItem<
83
+ TContext = unknown,
84
+ > extends TelegramQueueItemBase {
38
85
  kind: "control";
39
86
  controlType: "status" | "model";
40
- execute: (ctx: ExtensionContext) => Promise<void>;
87
+ execute: (ctx: TContext) => Promise<void>;
41
88
  }
42
89
 
43
- export type TelegramQueueItem =
90
+ export type TelegramQueueItem<TContext = unknown> =
44
91
  | PendingTelegramTurn
45
- | PendingTelegramControlItem;
92
+ | PendingTelegramControlItem<TContext>;
93
+
94
+ export interface TelegramQueueStore<TContext = unknown> {
95
+ getQueuedItems: () => TelegramQueueItem<TContext>[];
96
+ setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
97
+ }
98
+
99
+ export interface TelegramQueueStateStore<
100
+ TContext = unknown,
101
+ > extends TelegramQueueStore<TContext> {
102
+ hasQueuedItems: () => boolean;
103
+ }
104
+
105
+ export interface TelegramActiveTurnStore<
106
+ TTurn extends PendingTelegramTurn = PendingTelegramTurn,
107
+ > {
108
+ get: () => TTurn | undefined;
109
+ has: () => boolean;
110
+ set: (turn: TTurn) => void;
111
+ clear: () => void;
112
+ getChatId: () => number | undefined;
113
+ getReplyToMessageId: () => number | undefined;
114
+ getSourceMessageIds: () => number[] | undefined;
115
+ }
46
116
 
47
117
  export interface TelegramDispatchGuardState {
48
118
  compactionInProgress: boolean;
@@ -52,39 +122,91 @@ export interface TelegramDispatchGuardState {
52
122
  hasPendingMessages: boolean;
53
123
  }
54
124
 
55
- export interface TelegramInFlightModelSwitchState {
56
- isIdle: boolean;
57
- hasActiveTelegramTurn: boolean;
58
- hasAbortHandler: boolean;
125
+ export function getTelegramQueueLaneContract(
126
+ lane: TelegramQueueLane,
127
+ ): TelegramQueueLaneContract {
128
+ const contract = TELEGRAM_QUEUE_LANE_CONTRACTS.find(
129
+ (entry) => entry.lane === lane,
130
+ );
131
+ if (!contract) throw new Error(`Unknown Telegram queue lane: ${lane}`);
132
+ return contract;
133
+ }
134
+
135
+ export function getTelegramQueueItemAdmissionMode(
136
+ item: Pick<TelegramQueueItem, "queueLane">,
137
+ ): TelegramQueueAdmissionMode {
138
+ return getTelegramQueueLaneContract(item.queueLane).admissionMode;
139
+ }
140
+
141
+ export function isTelegramQueueItemAdmissionValid(
142
+ item: Pick<TelegramQueueItem, "kind" | "queueLane">,
143
+ ): boolean {
144
+ return getTelegramQueueLaneContract(item.queueLane).allowedKinds.includes(
145
+ item.kind,
146
+ );
147
+ }
148
+
149
+ export function assertTelegramQueueItemAdmissionValid(
150
+ item: Pick<TelegramQueueItem, "kind" | "queueLane">,
151
+ ): void {
152
+ if (isTelegramQueueItemAdmissionValid(item)) return;
153
+ throw new Error(
154
+ `Invalid Telegram queue admission: ${item.kind} item cannot use ${item.queueLane} lane`,
155
+ );
59
156
  }
60
157
 
61
158
  function getTelegramQueueLaneRank(lane: TelegramQueueLane): number {
62
- switch (lane) {
63
- case "control":
64
- return 0;
65
- case "priority":
66
- return 1;
67
- default:
68
- return 2;
69
- }
159
+ return getTelegramQueueLaneContract(lane).dispatchRank;
70
160
  }
71
161
 
72
- export function isPendingTelegramTurn(
73
- item: TelegramQueueItem,
162
+ export function isPendingTelegramTurn<TContext = unknown>(
163
+ item: TelegramQueueItem<TContext>,
74
164
  ): item is PendingTelegramTurn {
75
165
  return item.kind === "prompt";
76
166
  }
77
167
 
168
+ export function createTelegramQueueStore<TContext = unknown>(
169
+ initialItems: TelegramQueueItem<TContext>[] = [],
170
+ ): TelegramQueueStateStore<TContext> {
171
+ let queuedItems = initialItems;
172
+ return {
173
+ getQueuedItems: () => queuedItems,
174
+ setQueuedItems: (items) => {
175
+ queuedItems = items;
176
+ },
177
+ hasQueuedItems: () => queuedItems.length > 0,
178
+ };
179
+ }
180
+
181
+ export function createTelegramActiveTurnStore<
182
+ TTurn extends PendingTelegramTurn = PendingTelegramTurn,
183
+ >(): TelegramActiveTurnStore<TTurn> {
184
+ let activeTurn: TTurn | undefined;
185
+ return {
186
+ get: () => activeTurn,
187
+ has: () => !!activeTurn,
188
+ set: (turn) => {
189
+ activeTurn = { ...turn };
190
+ },
191
+ clear: () => {
192
+ activeTurn = undefined;
193
+ },
194
+ getChatId: () => activeTurn?.chatId,
195
+ getReplyToMessageId: () => activeTurn?.replyToMessageId,
196
+ getSourceMessageIds: () => activeTurn?.sourceMessageIds,
197
+ };
198
+ }
199
+
78
200
  // --- Queue Mutations ---
79
201
 
80
- export function partitionTelegramQueueItemsForHistory(
81
- items: TelegramQueueItem[],
202
+ export function partitionTelegramQueueItemsForHistory<TContext = unknown>(
203
+ items: TelegramQueueItem<TContext>[],
82
204
  ): {
83
205
  historyTurns: PendingTelegramTurn[];
84
- remainingItems: TelegramQueueItem[];
206
+ remainingItems: TelegramQueueItem<TContext>[];
85
207
  } {
86
208
  const historyTurns: PendingTelegramTurn[] = [];
87
- const remainingItems: TelegramQueueItem[] = [];
209
+ const remainingItems: TelegramQueueItem<TContext>[] = [];
88
210
  for (const item of items) {
89
211
  if (isPendingTelegramTurn(item)) {
90
212
  historyTurns.push(item);
@@ -95,10 +217,36 @@ export function partitionTelegramQueueItemsForHistory(
95
217
  return { historyTurns, remainingItems };
96
218
  }
97
219
 
98
- export function compareTelegramQueueItems(
99
- left: TelegramQueueItem,
100
- right: TelegramQueueItem,
220
+ export function planTelegramPromptEnqueue<TContext = unknown>(
221
+ items: TelegramQueueItem<TContext>[],
222
+ preserveQueuedTurnsAsHistory: boolean,
223
+ ): {
224
+ historyTurns: PendingTelegramTurn[];
225
+ remainingItems: TelegramQueueItem<TContext>[];
226
+ } {
227
+ if (!preserveQueuedTurnsAsHistory) {
228
+ return { historyTurns: [], remainingItems: items };
229
+ }
230
+ return partitionTelegramQueueItemsForHistory(items);
231
+ }
232
+
233
+ export function appendTelegramQueueItem<
234
+ TContext = unknown,
235
+ TItem extends TelegramQueueItem<TContext> = TelegramQueueItem<TContext>,
236
+ >(
237
+ items: TelegramQueueItem<TContext>[],
238
+ item: TItem,
239
+ ): TelegramQueueItem<TContext>[] {
240
+ assertTelegramQueueItemAdmissionValid(item);
241
+ return [...items, item];
242
+ }
243
+
244
+ export function compareTelegramQueueItems<TContext = unknown>(
245
+ left: TelegramQueueItem<TContext>,
246
+ right: TelegramQueueItem<TContext>,
101
247
  ): number {
248
+ assertTelegramQueueItemAdmissionValid(left);
249
+ assertTelegramQueueItemAdmissionValid(right);
102
250
  const laneRankDelta =
103
251
  getTelegramQueueLaneRank(left.queueLane) -
104
252
  getTelegramQueueLaneRank(right.queueLane);
@@ -109,10 +257,10 @@ export function compareTelegramQueueItems(
109
257
  return left.queueOrder - right.queueOrder;
110
258
  }
111
259
 
112
- export function removeTelegramQueueItemsByMessageIds(
113
- items: TelegramQueueItem[],
260
+ export function removeTelegramQueueItemsByMessageIds<TContext = unknown>(
261
+ items: TelegramQueueItem<TContext>[],
114
262
  messageIds: number[],
115
- ): { items: TelegramQueueItem[]; removedCount: number } {
263
+ ): { items: TelegramQueueItem<TContext>[]; removedCount: number } {
116
264
  if (messageIds.length === 0 || items.length === 0) {
117
265
  return { items, removedCount: 0 };
118
266
  }
@@ -129,10 +277,10 @@ export function removeTelegramQueueItemsByMessageIds(
129
277
  };
130
278
  }
131
279
 
132
- export function clearTelegramQueuePromptPriority(
133
- items: TelegramQueueItem[],
280
+ export function clearTelegramQueuePromptPriority<TContext = unknown>(
281
+ items: TelegramQueueItem<TContext>[],
134
282
  messageId: number,
135
- ): { items: TelegramQueueItem[]; changed: boolean } {
283
+ ): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
136
284
  let changed = false;
137
285
  const nextItems = items.map((item) => {
138
286
  if (
@@ -152,11 +300,11 @@ export function clearTelegramQueuePromptPriority(
152
300
  return { items: nextItems, changed };
153
301
  }
154
302
 
155
- export function prioritizeTelegramQueuePrompt(
156
- items: TelegramQueueItem[],
303
+ export function prioritizeTelegramQueuePrompt<TContext = unknown>(
304
+ items: TelegramQueueItem<TContext>[],
157
305
  messageId: number,
158
306
  laneOrder: number,
159
- ): { items: TelegramQueueItem[]; changed: boolean } {
307
+ ): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
160
308
  let changed = false;
161
309
  const nextItems = items.map((item) => {
162
310
  if (
@@ -175,10 +323,13 @@ export function prioritizeTelegramQueuePrompt(
175
323
  return { items: nextItems, changed };
176
324
  }
177
325
 
178
- export function consumeDispatchedTelegramPrompt(
179
- items: TelegramQueueItem[],
326
+ export function consumeDispatchedTelegramPrompt<TContext = unknown>(
327
+ items: TelegramQueueItem<TContext>[],
180
328
  hasPendingDispatch: boolean,
181
- ): { activeTurn?: PendingTelegramTurn; remainingItems: TelegramQueueItem[] } {
329
+ ): {
330
+ activeTurn?: PendingTelegramTurn;
331
+ remainingItems: TelegramQueueItem<TContext>[];
332
+ } {
182
333
  if (!hasPendingDispatch) {
183
334
  return { activeTurn: undefined, remainingItems: items };
184
335
  }
@@ -189,15 +340,17 @@ export function consumeDispatchedTelegramPrompt(
189
340
  return { activeTurn: nextItem, remainingItems: items.slice(1) };
190
341
  }
191
342
 
192
- function formatTelegramQueueItemStatusSummary(item: TelegramQueueItem): string {
343
+ function formatTelegramQueueItemStatusSummary<TContext = unknown>(
344
+ item: TelegramQueueItem<TContext>,
345
+ ): string {
193
346
  if (item.queueLane === "priority") {
194
347
  return `⬆ ${item.statusSummary}`;
195
348
  }
196
349
  return item.statusSummary;
197
350
  }
198
351
 
199
- export function formatQueuedTelegramItemsStatus(
200
- items: TelegramQueueItem[],
352
+ export function formatQueuedTelegramItemsStatus<TContext = unknown>(
353
+ items: TelegramQueueItem<TContext>[],
201
354
  ): string {
202
355
  if (items.length === 0) return "";
203
356
  const previewCount = 4;
@@ -222,45 +375,90 @@ export function canDispatchTelegramTurnState(
222
375
  );
223
376
  }
224
377
 
225
- export function canRestartTelegramTurnForModelSwitch(
226
- state: TelegramInFlightModelSwitchState,
227
- ): boolean {
228
- return !state.isIdle && state.hasActiveTelegramTurn && state.hasAbortHandler;
378
+ export interface TelegramDispatchReadinessDeps<TContext> {
379
+ isCompactionInProgress: () => boolean;
380
+ hasActiveTurn: () => boolean;
381
+ hasDispatchPending: () => boolean;
382
+ isIdle: (ctx: TContext) => boolean;
383
+ hasPendingMessages: (ctx: TContext) => boolean;
229
384
  }
230
385
 
231
- export function shouldTriggerPendingTelegramModelSwitchAbort(state: {
232
- hasPendingModelSwitch: boolean;
233
- hasActiveTelegramTurn: boolean;
234
- hasAbortHandler: boolean;
235
- activeToolExecutions: number;
236
- }): boolean {
237
- return (
238
- state.hasPendingModelSwitch &&
239
- state.hasActiveTelegramTurn &&
240
- state.hasAbortHandler &&
241
- state.activeToolExecutions === 0
242
- );
386
+ export function createTelegramDispatchReadinessChecker<TContext>(
387
+ deps: TelegramDispatchReadinessDeps<TContext>,
388
+ ): (ctx: TContext) => boolean {
389
+ return (ctx) =>
390
+ canDispatchTelegramTurnState({
391
+ compactionInProgress: deps.isCompactionInProgress(),
392
+ hasActiveTelegramTurn: deps.hasActiveTurn(),
393
+ hasPendingTelegramDispatch: deps.hasDispatchPending(),
394
+ isIdle: deps.isIdle(ctx),
395
+ hasPendingMessages: deps.hasPendingMessages(ctx),
396
+ });
397
+ }
398
+
399
+ export function buildPendingTelegramControlItem<TContext = unknown>(options: {
400
+ chatId: number;
401
+ replyToMessageId: number;
402
+ controlType: PendingTelegramControlItem<TContext>["controlType"];
403
+ queueOrder: number;
404
+ laneOrder: number;
405
+ statusSummary: string;
406
+ execute: PendingTelegramControlItem<TContext>["execute"];
407
+ }): PendingTelegramControlItem<TContext> {
408
+ return {
409
+ kind: "control",
410
+ controlType: options.controlType,
411
+ chatId: options.chatId,
412
+ replyToMessageId: options.replyToMessageId,
413
+ queueOrder: options.queueOrder,
414
+ queueLane: "control",
415
+ laneOrder: options.laneOrder,
416
+ statusSummary: options.statusSummary,
417
+ execute: options.execute,
418
+ };
419
+ }
420
+
421
+ export interface TelegramControlItemBuilderDeps {
422
+ allocateItemOrder: () => number;
423
+ allocateControlOrder: () => number;
424
+ }
425
+
426
+ export function createTelegramControlItemBuilder<TContext = unknown>(
427
+ deps: TelegramControlItemBuilderDeps,
428
+ ): (options: {
429
+ chatId: number;
430
+ replyToMessageId: number;
431
+ controlType: PendingTelegramControlItem<TContext>["controlType"];
432
+ statusSummary: string;
433
+ execute: PendingTelegramControlItem<TContext>["execute"];
434
+ }) => PendingTelegramControlItem<TContext> {
435
+ return (options) =>
436
+ buildPendingTelegramControlItem<TContext>({
437
+ ...options,
438
+ queueOrder: deps.allocateItemOrder(),
439
+ laneOrder: deps.allocateControlOrder(),
440
+ });
243
441
  }
244
442
 
245
443
  // --- Dispatch Planning ---
246
444
 
247
- export type TelegramQueueDispatchAction =
248
- | { kind: "none"; remainingItems: TelegramQueueItem[] }
445
+ export type TelegramQueueDispatchAction<TContext = unknown> =
446
+ | { kind: "none"; remainingItems: TelegramQueueItem<TContext>[] }
249
447
  | {
250
448
  kind: "control";
251
- item: PendingTelegramControlItem;
252
- remainingItems: TelegramQueueItem[];
449
+ item: PendingTelegramControlItem<TContext>;
450
+ remainingItems: TelegramQueueItem<TContext>[];
253
451
  }
254
452
  | {
255
453
  kind: "prompt";
256
454
  item: PendingTelegramTurn;
257
- remainingItems: TelegramQueueItem[];
455
+ remainingItems: TelegramQueueItem<TContext>[];
258
456
  };
259
457
 
260
- export function planNextTelegramQueueAction(
261
- items: TelegramQueueItem[],
458
+ export function planNextTelegramQueueAction<TContext = unknown>(
459
+ items: TelegramQueueItem<TContext>[],
262
460
  canDispatch: boolean,
263
- ): TelegramQueueDispatchAction {
461
+ ): TelegramQueueDispatchAction<TContext> {
264
462
  if (!canDispatch || items.length === 0) {
265
463
  return { kind: "none", remainingItems: items };
266
464
  }
@@ -268,6 +466,7 @@ export function planNextTelegramQueueAction(
268
466
  if (!firstItem) {
269
467
  return { kind: "none", remainingItems: items };
270
468
  }
469
+ assertTelegramQueueItemAdmissionValid(firstItem);
271
470
  if (isPendingTelegramTurn(firstItem)) {
272
471
  return { kind: "prompt", item: firstItem, remainingItems: items };
273
472
  }
@@ -288,19 +487,74 @@ export function shouldDispatchAfterTelegramAgentEnd(options: {
288
487
 
289
488
  // --- Agent Runtime ---
290
489
 
291
- export interface TelegramAgentStartPlan {
490
+ export interface TelegramAgentStartPlan<TContext = unknown> {
292
491
  activeTurn?: PendingTelegramTurn;
293
- remainingItems: TelegramQueueItem[];
492
+ remainingItems: TelegramQueueItem<TContext>[];
294
493
  shouldResetPendingModelSwitch: boolean;
295
494
  shouldResetToolExecutions: boolean;
296
495
  shouldClearDispatchPending: boolean;
297
496
  }
298
497
 
299
- export function buildTelegramAgentStartPlan(options: {
300
- queuedItems: TelegramQueueItem[];
498
+ export interface TelegramAgentStartRuntimeDeps<
499
+ TTurn extends PendingTelegramTurn,
500
+ TContext = unknown,
501
+ > {
502
+ queuedItems: TelegramQueueItem<TContext>[];
301
503
  hasPendingDispatch: boolean;
302
504
  hasActiveTurn: boolean;
303
- }): TelegramAgentStartPlan {
505
+ resetToolExecutions: () => void;
506
+ resetPendingModelSwitch: () => void;
507
+ setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
508
+ clearDispatchPending: () => void;
509
+ setActiveTurn: (turn: TTurn) => void;
510
+ createPreviewState: () => void;
511
+ startTypingLoop: () => void;
512
+ updateStatus: () => void;
513
+ }
514
+
515
+ export interface TelegramAgentStartHookRuntimeDeps<
516
+ TTurn extends PendingTelegramTurn,
517
+ TContext = unknown,
518
+ > {
519
+ setAbortHandler: (ctx: TContext) => void;
520
+ getQueuedItems: () => TelegramQueueItem<TContext>[];
521
+ hasPendingDispatch: () => boolean;
522
+ hasActiveTurn: () => boolean;
523
+ resetToolExecutions: () => void;
524
+ resetPendingModelSwitch: () => void;
525
+ setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
526
+ clearDispatchPending: () => void;
527
+ setActiveTurn: (turn: TTurn) => void;
528
+ createPreviewState: () => void;
529
+ startTypingLoop: (ctx: TContext) => void;
530
+ updateStatus: (ctx: TContext) => void;
531
+ }
532
+
533
+ export type TelegramAgentStartHookEvent = unknown;
534
+
535
+ export interface TelegramToolExecutionRuntimeDeps {
536
+ hasActiveTurn: () => boolean;
537
+ getActiveToolExecutions: () => number;
538
+ setActiveToolExecutions: (count: number) => void;
539
+ }
540
+
541
+ export interface TelegramToolExecutionEndRuntimeDeps extends TelegramToolExecutionRuntimeDeps {
542
+ triggerPendingModelSwitchAbort: () => void;
543
+ }
544
+
545
+ export interface TelegramToolExecutionHookRuntimeDeps<
546
+ TContext,
547
+ > extends TelegramToolExecutionRuntimeDeps {
548
+ triggerPendingModelSwitchAbort: (ctx: TContext) => unknown;
549
+ }
550
+
551
+ export type TelegramToolExecutionHookEvent = unknown;
552
+
553
+ export function buildTelegramAgentStartPlan<TContext = unknown>(options: {
554
+ queuedItems: TelegramQueueItem<TContext>[];
555
+ hasPendingDispatch: boolean;
556
+ hasActiveTurn: boolean;
557
+ }): TelegramAgentStartPlan<TContext> {
304
558
  if (options.hasActiveTurn || !options.hasPendingDispatch) {
305
559
  return {
306
560
  activeTurn: undefined,
@@ -323,6 +577,52 @@ export function buildTelegramAgentStartPlan(options: {
323
577
  };
324
578
  }
325
579
 
580
+ export function handleTelegramAgentStartRuntime<
581
+ TTurn extends PendingTelegramTurn,
582
+ TContext = unknown,
583
+ >(deps: TelegramAgentStartRuntimeDeps<TTurn, TContext>): void {
584
+ const startPlan = buildTelegramAgentStartPlan({
585
+ queuedItems: deps.queuedItems,
586
+ hasPendingDispatch: deps.hasPendingDispatch,
587
+ hasActiveTurn: deps.hasActiveTurn,
588
+ });
589
+ if (startPlan.shouldResetToolExecutions) deps.resetToolExecutions();
590
+ if (startPlan.shouldResetPendingModelSwitch) deps.resetPendingModelSwitch();
591
+ deps.setQueuedItems(startPlan.remainingItems);
592
+ if (startPlan.shouldClearDispatchPending) deps.clearDispatchPending();
593
+ if (startPlan.activeTurn) {
594
+ deps.setActiveTurn(startPlan.activeTurn as TTurn);
595
+ deps.createPreviewState();
596
+ deps.startTypingLoop();
597
+ }
598
+ deps.updateStatus();
599
+ }
600
+
601
+ export function createTelegramAgentStartHook<
602
+ TTurn extends PendingTelegramTurn,
603
+ TContext = unknown,
604
+ >(deps: TelegramAgentStartHookRuntimeDeps<TTurn, TContext>) {
605
+ return async function onAgentStart(
606
+ _event: TelegramAgentStartHookEvent,
607
+ ctx: TContext,
608
+ ): Promise<void> {
609
+ deps.setAbortHandler(ctx);
610
+ handleTelegramAgentStartRuntime<TTurn, TContext>({
611
+ queuedItems: deps.getQueuedItems(),
612
+ hasPendingDispatch: deps.hasPendingDispatch(),
613
+ hasActiveTurn: deps.hasActiveTurn(),
614
+ resetToolExecutions: deps.resetToolExecutions,
615
+ resetPendingModelSwitch: deps.resetPendingModelSwitch,
616
+ setQueuedItems: deps.setQueuedItems,
617
+ clearDispatchPending: deps.clearDispatchPending,
618
+ setActiveTurn: deps.setActiveTurn,
619
+ createPreviewState: deps.createPreviewState,
620
+ startTypingLoop: () => deps.startTypingLoop(ctx),
621
+ updateStatus: () => deps.updateStatus(ctx),
622
+ });
623
+ };
624
+ }
625
+
326
626
  export function getNextTelegramToolExecutionCount(options: {
327
627
  hasActiveTurn: boolean;
328
628
  currentCount: number;
@@ -335,6 +635,75 @@ export function getNextTelegramToolExecutionCount(options: {
335
635
  return Math.max(0, options.currentCount - 1);
336
636
  }
337
637
 
638
+ export function handleTelegramToolExecutionStartRuntime(
639
+ deps: TelegramToolExecutionRuntimeDeps,
640
+ ): void {
641
+ deps.setActiveToolExecutions(
642
+ getNextTelegramToolExecutionCount({
643
+ hasActiveTurn: deps.hasActiveTurn(),
644
+ currentCount: deps.getActiveToolExecutions(),
645
+ event: "start",
646
+ }),
647
+ );
648
+ }
649
+
650
+ export function handleTelegramToolExecutionEndRuntime(
651
+ deps: TelegramToolExecutionEndRuntimeDeps,
652
+ ): void {
653
+ const hasActiveTurn = deps.hasActiveTurn();
654
+ deps.setActiveToolExecutions(
655
+ getNextTelegramToolExecutionCount({
656
+ hasActiveTurn,
657
+ currentCount: deps.getActiveToolExecutions(),
658
+ event: "end",
659
+ }),
660
+ );
661
+ if (hasActiveTurn) deps.triggerPendingModelSwitchAbort();
662
+ }
663
+
664
+ export type TelegramAgentLifecycleHooksRuntimeDeps<
665
+ TTurn extends PendingTelegramTurn,
666
+ TContext,
667
+ TMessage,
668
+ > = TelegramAgentStartHookRuntimeDeps<TTurn, TContext> &
669
+ TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage> &
670
+ TelegramToolExecutionHookRuntimeDeps<TContext>;
671
+
672
+ export function createTelegramAgentLifecycleHooks<
673
+ TTurn extends PendingTelegramTurn,
674
+ TContext,
675
+ TMessage,
676
+ >(deps: TelegramAgentLifecycleHooksRuntimeDeps<TTurn, TContext, TMessage>) {
677
+ return {
678
+ onAgentStart: createTelegramAgentStartHook<TTurn, TContext>(deps),
679
+ onAgentEnd: createTelegramAgentEndHook<TTurn, TContext, TMessage>(deps),
680
+ ...createTelegramToolExecutionHooks<TContext>(deps),
681
+ };
682
+ }
683
+
684
+ export function createTelegramToolExecutionHooks<TContext>(
685
+ deps: TelegramToolExecutionHookRuntimeDeps<TContext>,
686
+ ) {
687
+ return {
688
+ onToolExecutionStart: (): void => {
689
+ handleTelegramToolExecutionStartRuntime(deps);
690
+ },
691
+ onToolExecutionEnd: (
692
+ _event: TelegramToolExecutionHookEvent,
693
+ ctx: TContext,
694
+ ): void => {
695
+ handleTelegramToolExecutionEndRuntime({
696
+ hasActiveTurn: deps.hasActiveTurn,
697
+ getActiveToolExecutions: deps.getActiveToolExecutions,
698
+ setActiveToolExecutions: deps.setActiveToolExecutions,
699
+ triggerPendingModelSwitchAbort: () => {
700
+ deps.triggerPendingModelSwitchAbort(ctx);
701
+ },
702
+ });
703
+ },
704
+ };
705
+ }
706
+
338
707
  // --- Agent End Lifecycle ---
339
708
 
340
709
  export interface TelegramAgentEndPlan {
@@ -345,6 +714,66 @@ export interface TelegramAgentEndPlan {
345
714
  shouldSendAttachmentNotice: boolean;
346
715
  }
347
716
 
717
+ export interface TelegramAgentEndAssistantResult {
718
+ text?: string;
719
+ stopReason?: string;
720
+ errorMessage?: string;
721
+ }
722
+
723
+ export interface TelegramAgentEndRuntimeDeps<
724
+ TTurn extends PendingTelegramTurn,
725
+ > {
726
+ turn: TTurn | undefined;
727
+ assistant: TelegramAgentEndAssistantResult;
728
+ preserveQueuedTurnsAsHistory: boolean;
729
+ resetRuntimeState: () => void;
730
+ updateStatus: () => void;
731
+ dispatchNextQueuedTelegramTurn: () => void;
732
+ clearPreview: (chatId: number) => Promise<void>;
733
+ setPreviewPendingText: (text: string) => void;
734
+ finalizeMarkdownPreview: (
735
+ chatId: number,
736
+ markdown: string,
737
+ replyToMessageId: number,
738
+ ) => Promise<boolean>;
739
+ sendMarkdownReply: (
740
+ chatId: number,
741
+ replyToMessageId: number,
742
+ markdown: string,
743
+ ) => Promise<unknown>;
744
+ sendTextReply: (
745
+ chatId: number,
746
+ replyToMessageId: number,
747
+ text: string,
748
+ ) => Promise<unknown>;
749
+ sendQueuedAttachments: (turn: TTurn) => Promise<void>;
750
+ }
751
+
752
+ export interface TelegramAgentEndHookRuntimeDeps<
753
+ TTurn extends PendingTelegramTurn,
754
+ TContext,
755
+ TMessage,
756
+ > {
757
+ getActiveTurn: () => TTurn | undefined;
758
+ extractAssistant: (
759
+ messages: readonly TMessage[],
760
+ ) => TelegramAgentEndAssistantResult;
761
+ getPreserveQueuedTurnsAsHistory: () => boolean;
762
+ resetRuntimeState: () => void;
763
+ updateStatus: (ctx: TContext) => void;
764
+ dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
765
+ clearPreview: (chatId: number) => Promise<void>;
766
+ setPreviewPendingText: (text: string) => void;
767
+ finalizeMarkdownPreview: TelegramAgentEndRuntimeDeps<TTurn>["finalizeMarkdownPreview"];
768
+ sendMarkdownReply: TelegramAgentEndRuntimeDeps<TTurn>["sendMarkdownReply"];
769
+ sendTextReply: TelegramAgentEndRuntimeDeps<TTurn>["sendTextReply"];
770
+ sendQueuedAttachments: (turn: TTurn) => Promise<void>;
771
+ }
772
+
773
+ export interface TelegramAgentEndHookEvent<TMessage> {
774
+ messages: readonly TMessage[];
775
+ }
776
+
348
777
  export function buildTelegramAgentEndPlan(options: {
349
778
  hasTurn: boolean;
350
779
  stopReason?: string;
@@ -411,42 +840,105 @@ export function buildTelegramAgentEndPlan(options: {
411
840
  };
412
841
  }
413
842
 
414
- // --- Session Runtime ---
415
-
416
- export interface TelegramPollingStartState {
417
- hasBotToken: boolean;
418
- hasPollingPromise: boolean;
843
+ export function createTelegramAgentEndHook<
844
+ TTurn extends PendingTelegramTurn,
845
+ TContext,
846
+ TMessage,
847
+ >(deps: TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage>) {
848
+ return async function onAgentEnd(
849
+ event: TelegramAgentEndHookEvent<TMessage>,
850
+ ctx: TContext,
851
+ ): Promise<void> {
852
+ const turn = deps.getActiveTurn();
853
+ await handleTelegramAgentEndRuntime({
854
+ turn,
855
+ assistant: turn ? deps.extractAssistant(event.messages) : {},
856
+ preserveQueuedTurnsAsHistory: deps.getPreserveQueuedTurnsAsHistory(),
857
+ resetRuntimeState: deps.resetRuntimeState,
858
+ updateStatus: () => deps.updateStatus(ctx),
859
+ dispatchNextQueuedTelegramTurn: () =>
860
+ deps.dispatchNextQueuedTelegramTurn(ctx),
861
+ clearPreview: deps.clearPreview,
862
+ setPreviewPendingText: deps.setPreviewPendingText,
863
+ finalizeMarkdownPreview: deps.finalizeMarkdownPreview,
864
+ sendMarkdownReply: deps.sendMarkdownReply,
865
+ sendTextReply: deps.sendTextReply,
866
+ sendQueuedAttachments: deps.sendQueuedAttachments,
867
+ });
868
+ };
419
869
  }
420
870
 
421
- export function shouldStartTelegramPolling(
422
- state: TelegramPollingStartState,
423
- ): boolean {
424
- return state.hasBotToken && !state.hasPollingPromise;
871
+ export async function handleTelegramAgentEndRuntime<
872
+ TTurn extends PendingTelegramTurn,
873
+ >(deps: TelegramAgentEndRuntimeDeps<TTurn>): Promise<void> {
874
+ const { turn, assistant } = deps;
875
+ const finalText = assistant.text;
876
+ deps.resetRuntimeState();
877
+ deps.updateStatus();
878
+ const endPlan = buildTelegramAgentEndPlan({
879
+ hasTurn: !!turn,
880
+ stopReason: assistant.stopReason,
881
+ hasFinalText: !!finalText,
882
+ hasQueuedAttachments: (turn?.queuedAttachments.length ?? 0) > 0,
883
+ preserveQueuedTurnsAsHistory: deps.preserveQueuedTurnsAsHistory,
884
+ });
885
+ if (!turn) {
886
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
887
+ return;
888
+ }
889
+ if (endPlan.shouldClearPreview) {
890
+ await deps.clearPreview(turn.chatId);
891
+ }
892
+ if (endPlan.shouldSendErrorMessage) {
893
+ await deps.sendTextReply(
894
+ turn.chatId,
895
+ turn.replyToMessageId,
896
+ assistant.errorMessage ||
897
+ "Telegram bridge: pi failed while processing the request.",
898
+ );
899
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
900
+ return;
901
+ }
902
+ if (finalText) deps.setPreviewPendingText(finalText);
903
+ if (endPlan.kind === "text" && finalText) {
904
+ const finalized = await deps.finalizeMarkdownPreview(
905
+ turn.chatId,
906
+ finalText,
907
+ turn.replyToMessageId,
908
+ );
909
+ if (!finalized) {
910
+ await deps.clearPreview(turn.chatId);
911
+ await deps.sendMarkdownReply(
912
+ turn.chatId,
913
+ turn.replyToMessageId,
914
+ finalText,
915
+ );
916
+ }
917
+ }
918
+ if (endPlan.shouldSendAttachmentNotice) {
919
+ await deps.sendTextReply(
920
+ turn.chatId,
921
+ turn.replyToMessageId,
922
+ "Attached requested file(s).",
923
+ );
924
+ }
925
+ await deps.sendQueuedAttachments(turn);
926
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
425
927
  }
426
928
 
427
- export function buildTelegramSessionStartState(
428
- currentModel: Model<any> | undefined,
429
- ): {
430
- currentTelegramModel: Model<any> | undefined;
929
+ // --- Session Runtime ---
930
+
931
+ export interface TelegramSessionStartState<TModel = unknown> {
932
+ currentTelegramModel: TModel | undefined;
431
933
  activeTelegramToolExecutions: number;
432
934
  pendingTelegramModelSwitch: undefined;
433
935
  nextQueuedTelegramItemOrder: number;
434
936
  nextQueuedTelegramControlOrder: number;
435
937
  telegramTurnDispatchPending: boolean;
436
938
  compactionInProgress: boolean;
437
- } {
438
- return {
439
- currentTelegramModel: currentModel,
440
- activeTelegramToolExecutions: 0,
441
- pendingTelegramModelSwitch: undefined,
442
- nextQueuedTelegramItemOrder: 0,
443
- nextQueuedTelegramControlOrder: 0,
444
- telegramTurnDispatchPending: false,
445
- compactionInProgress: false,
446
- };
447
939
  }
448
940
 
449
- export function buildTelegramSessionShutdownState<TQueueItem>(): {
941
+ export interface TelegramSessionShutdownState<TQueueItem> {
450
942
  queuedTelegramItems: TQueueItem[];
451
943
  nextQueuedTelegramItemOrder: number;
452
944
  nextQueuedTelegramControlOrder: number;
@@ -457,7 +949,183 @@ export function buildTelegramSessionShutdownState<TQueueItem>(): {
457
949
  telegramTurnDispatchPending: boolean;
458
950
  compactionInProgress: boolean;
459
951
  preserveQueuedTurnsAsHistory: boolean;
460
- } {
952
+ }
953
+
954
+ export interface TelegramSessionRuntimeCounterState {
955
+ nextQueuedTelegramItemOrder?: number;
956
+ nextQueuedTelegramControlOrder?: number;
957
+ nextPriorityReactionOrder?: number;
958
+ }
959
+
960
+ export interface TelegramSessionRuntimeFlagState {
961
+ activeTelegramToolExecutions?: number;
962
+ telegramTurnDispatchPending?: boolean;
963
+ compactionInProgress?: boolean;
964
+ preserveQueuedTurnsAsHistory?: boolean;
965
+ }
966
+
967
+ export interface TelegramSessionStateApplierDeps<TQueueItem, TModel> {
968
+ setQueuedItems: (items: TQueueItem[]) => void;
969
+ setCurrentModel: (model: TModel | undefined) => void;
970
+ setPendingModelSwitch: (selection: undefined) => void;
971
+ syncCounters: (state: TelegramSessionRuntimeCounterState) => void;
972
+ syncFlags: (state: TelegramSessionRuntimeFlagState) => void;
973
+ }
974
+
975
+ export interface TelegramSessionStateApplier<TQueueItem, TModel> {
976
+ applyStartState: (state: TelegramSessionStartState<TModel>) => void;
977
+ applyShutdownState: (state: TelegramSessionShutdownState<TQueueItem>) => void;
978
+ }
979
+
980
+ export interface TelegramSessionStartRuntimeDeps<TModel = unknown> {
981
+ currentModel: TModel | undefined;
982
+ loadConfig: () => Promise<void>;
983
+ applyState: (state: TelegramSessionStartState<TModel>) => void;
984
+ prepareTempDir: () => Promise<unknown>;
985
+ updateStatus: () => void;
986
+ }
987
+
988
+ export interface TelegramSessionShutdownRuntimeDeps<TQueueItem> {
989
+ applyState: (state: TelegramSessionShutdownState<TQueueItem>) => void;
990
+ clearPendingMediaGroups: () => void;
991
+ clearModelMenuState: () => void;
992
+ getActiveTurnChatId: () => number | undefined;
993
+ clearPreview: (chatId: number) => Promise<void>;
994
+ clearActiveTurn: () => void;
995
+ clearAbort: () => void;
996
+ stopPolling: () => Promise<void>;
997
+ }
998
+
999
+ export interface TelegramSessionLifecycleHookRuntimeDeps<
1000
+ TContext,
1001
+ TQueueItem,
1002
+ TModel = unknown,
1003
+ > extends TelegramRuntimeEventRecorderPort {
1004
+ getCurrentModel: (ctx: TContext) => TModel | undefined;
1005
+ loadConfig: () => Promise<void>;
1006
+ applySessionStartState: (state: TelegramSessionStartState<TModel>) => void;
1007
+ prepareTempDir: () => Promise<unknown>;
1008
+ updateStatus: (ctx: TContext) => void;
1009
+ applySessionShutdownState: (
1010
+ state: TelegramSessionShutdownState<TQueueItem>,
1011
+ ) => void;
1012
+ clearPendingMediaGroups: () => void;
1013
+ clearModelMenuState: () => void;
1014
+ getActiveTurnChatId: () => number | undefined;
1015
+ clearPreview: (chatId: number) => Promise<void>;
1016
+ clearActiveTurn: () => void;
1017
+ clearAbort: () => void;
1018
+ stopPolling: () => Promise<void>;
1019
+ }
1020
+
1021
+ export type TelegramSessionLifecycleHookEvent = unknown;
1022
+
1023
+ export function createTelegramSessionStateApplier<TQueueItem, TModel>(
1024
+ deps: TelegramSessionStateApplierDeps<TQueueItem, TModel>,
1025
+ ): TelegramSessionStateApplier<TQueueItem, TModel> {
1026
+ return {
1027
+ applyStartState: (state) => {
1028
+ deps.setCurrentModel(state.currentTelegramModel);
1029
+ deps.setPendingModelSwitch(state.pendingTelegramModelSwitch);
1030
+ deps.syncCounters(state);
1031
+ deps.syncFlags(state);
1032
+ },
1033
+ applyShutdownState: (state) => {
1034
+ deps.setQueuedItems(state.queuedTelegramItems);
1035
+ deps.syncCounters(state);
1036
+ deps.syncFlags(state);
1037
+ deps.setCurrentModel(state.currentTelegramModel);
1038
+ deps.setPendingModelSwitch(state.pendingTelegramModelSwitch);
1039
+ },
1040
+ };
1041
+ }
1042
+
1043
+ export interface TelegramQueueMutationRuntimeDeps<
1044
+ TContext,
1045
+ > extends TelegramQueueStore<TContext> {
1046
+ ctx: TContext;
1047
+ getNextPriorityReactionOrder?: () => number;
1048
+ incrementNextPriorityReactionOrder?: () => void;
1049
+ updateStatus: (ctx: TContext) => void;
1050
+ }
1051
+
1052
+ export interface TelegramQueueMutationControllerDeps<
1053
+ TContext,
1054
+ > extends TelegramQueueStore<TContext> {
1055
+ getNextPriorityReactionOrder?: () => number;
1056
+ incrementNextPriorityReactionOrder?: () => void;
1057
+ updateStatus: (ctx: TContext) => void;
1058
+ }
1059
+
1060
+ export interface TelegramQueueMutationController<TContext> {
1061
+ append: (item: TelegramQueueItem<TContext>, ctx: TContext) => void;
1062
+ reorder: (ctx: TContext) => void;
1063
+ removeByMessageIds: (messageIds: number[], ctx: TContext) => number;
1064
+ clearPriorityByMessageId: (messageId: number, ctx: TContext) => boolean;
1065
+ prioritizeByMessageId: (messageId: number, ctx: TContext) => boolean;
1066
+ }
1067
+
1068
+ export interface TelegramControlQueueControllerDeps<TContext> {
1069
+ appendControlItem: (
1070
+ item: PendingTelegramControlItem<TContext>,
1071
+ ctx: TContext,
1072
+ ) => void;
1073
+ dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
1074
+ }
1075
+
1076
+ export interface TelegramControlQueueController<TContext> {
1077
+ enqueue: (item: PendingTelegramControlItem<TContext>, ctx: TContext) => void;
1078
+ }
1079
+
1080
+ export interface TelegramPromptEnqueueRuntimeDeps<
1081
+ TMessage,
1082
+ TContext = unknown,
1083
+ > extends TelegramQueueStore<TContext> {
1084
+ getPreserveQueuedTurnsAsHistory: () => boolean;
1085
+ setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
1086
+ createTurn: (
1087
+ messages: TMessage[],
1088
+ historyTurns: PendingTelegramTurn[],
1089
+ ) => Promise<PendingTelegramTurn>;
1090
+ updateStatus: () => void;
1091
+ dispatchNextQueuedTelegramTurn: () => void;
1092
+ }
1093
+
1094
+ export interface TelegramPromptEnqueueControllerDeps<
1095
+ TMessage,
1096
+ TContext = unknown,
1097
+ > extends TelegramQueueStore<TContext> {
1098
+ getPreserveQueuedTurnsAsHistory: () => boolean;
1099
+ setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
1100
+ createTurn: (
1101
+ messages: TMessage[],
1102
+ historyTurns: PendingTelegramTurn[],
1103
+ ) => Promise<PendingTelegramTurn>;
1104
+ updateStatus: (ctx: TContext) => void;
1105
+ dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
1106
+ }
1107
+
1108
+ export interface TelegramPromptEnqueueController<TMessage, TContext = unknown> {
1109
+ enqueue: (messages: TMessage[], ctx: TContext) => Promise<void>;
1110
+ }
1111
+
1112
+ export function buildTelegramSessionStartState<TModel = unknown>(
1113
+ currentModel: TModel | undefined,
1114
+ ): TelegramSessionStartState<TModel> {
1115
+ return {
1116
+ currentTelegramModel: currentModel,
1117
+ activeTelegramToolExecutions: 0,
1118
+ pendingTelegramModelSwitch: undefined,
1119
+ nextQueuedTelegramItemOrder: 0,
1120
+ nextQueuedTelegramControlOrder: 0,
1121
+ telegramTurnDispatchPending: false,
1122
+ compactionInProgress: false,
1123
+ };
1124
+ }
1125
+
1126
+ export function buildTelegramSessionShutdownState<
1127
+ TQueueItem,
1128
+ >(): TelegramSessionShutdownState<TQueueItem> {
461
1129
  return {
462
1130
  queuedTelegramItems: [],
463
1131
  nextQueuedTelegramItemOrder: 0,
@@ -472,10 +1140,266 @@ export function buildTelegramSessionShutdownState<TQueueItem>(): {
472
1140
  };
473
1141
  }
474
1142
 
1143
+ export async function startTelegramSessionRuntime<TModel = unknown>(
1144
+ deps: TelegramSessionStartRuntimeDeps<TModel>,
1145
+ ): Promise<void> {
1146
+ await deps.loadConfig();
1147
+ deps.applyState(buildTelegramSessionStartState(deps.currentModel));
1148
+ await deps.prepareTempDir();
1149
+ deps.updateStatus();
1150
+ }
1151
+
1152
+ export async function shutdownTelegramSessionRuntime<TQueueItem>(
1153
+ deps: TelegramSessionShutdownRuntimeDeps<TQueueItem>,
1154
+ ): Promise<void> {
1155
+ deps.applyState(buildTelegramSessionShutdownState<TQueueItem>());
1156
+ deps.clearPendingMediaGroups();
1157
+ deps.clearModelMenuState();
1158
+ const activeTurnChatId = deps.getActiveTurnChatId();
1159
+ if (activeTurnChatId !== undefined) {
1160
+ await deps.clearPreview(activeTurnChatId);
1161
+ }
1162
+ deps.clearActiveTurn();
1163
+ deps.clearAbort();
1164
+ await deps.stopPolling();
1165
+ }
1166
+
1167
+ export type TelegramSessionLifecycleRuntimeDeps<
1168
+ TContext,
1169
+ TQueueItem,
1170
+ TModel = unknown,
1171
+ > = Omit<
1172
+ TelegramSessionLifecycleHookRuntimeDeps<TContext, TQueueItem, TModel>,
1173
+ "applySessionStartState" | "applySessionShutdownState"
1174
+ > &
1175
+ TelegramSessionStateApplierDeps<TQueueItem, TModel>;
1176
+
1177
+ export function createTelegramSessionLifecycleRuntime<
1178
+ TContext,
1179
+ TQueueItem,
1180
+ TModel = unknown,
1181
+ >(deps: TelegramSessionLifecycleRuntimeDeps<TContext, TQueueItem, TModel>) {
1182
+ const stateApplier = createTelegramSessionStateApplier({
1183
+ setQueuedItems: deps.setQueuedItems,
1184
+ setCurrentModel: deps.setCurrentModel,
1185
+ setPendingModelSwitch: deps.setPendingModelSwitch,
1186
+ syncCounters: deps.syncCounters,
1187
+ syncFlags: deps.syncFlags,
1188
+ });
1189
+ return createTelegramSessionLifecycleHooks({
1190
+ getCurrentModel: deps.getCurrentModel,
1191
+ loadConfig: deps.loadConfig,
1192
+ applySessionStartState: stateApplier.applyStartState,
1193
+ prepareTempDir: deps.prepareTempDir,
1194
+ updateStatus: deps.updateStatus,
1195
+ applySessionShutdownState: stateApplier.applyShutdownState,
1196
+ clearPendingMediaGroups: deps.clearPendingMediaGroups,
1197
+ clearModelMenuState: deps.clearModelMenuState,
1198
+ getActiveTurnChatId: deps.getActiveTurnChatId,
1199
+ clearPreview: deps.clearPreview,
1200
+ clearActiveTurn: deps.clearActiveTurn,
1201
+ clearAbort: deps.clearAbort,
1202
+ stopPolling: deps.stopPolling,
1203
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1204
+ });
1205
+ }
1206
+
1207
+ export function createTelegramSessionLifecycleHooks<
1208
+ TContext,
1209
+ TQueueItem,
1210
+ TModel = unknown,
1211
+ >(deps: TelegramSessionLifecycleHookRuntimeDeps<TContext, TQueueItem, TModel>) {
1212
+ return {
1213
+ onSessionStart: async (
1214
+ _event: TelegramSessionLifecycleHookEvent,
1215
+ ctx: TContext,
1216
+ ): Promise<void> => {
1217
+ try {
1218
+ await startTelegramSessionRuntime({
1219
+ currentModel: deps.getCurrentModel(ctx),
1220
+ loadConfig: deps.loadConfig,
1221
+ applyState: deps.applySessionStartState,
1222
+ prepareTempDir: deps.prepareTempDir,
1223
+ updateStatus: () => deps.updateStatus(ctx),
1224
+ });
1225
+ } catch (error) {
1226
+ deps.recordRuntimeEvent?.("session", error, { phase: "start" });
1227
+ throw error;
1228
+ }
1229
+ },
1230
+ onSessionShutdown: async (): Promise<void> => {
1231
+ try {
1232
+ await shutdownTelegramSessionRuntime<TQueueItem>({
1233
+ applyState: deps.applySessionShutdownState,
1234
+ clearPendingMediaGroups: deps.clearPendingMediaGroups,
1235
+ clearModelMenuState: deps.clearModelMenuState,
1236
+ getActiveTurnChatId: deps.getActiveTurnChatId,
1237
+ clearPreview: deps.clearPreview,
1238
+ clearActiveTurn: deps.clearActiveTurn,
1239
+ clearAbort: deps.clearAbort,
1240
+ stopPolling: deps.stopPolling,
1241
+ });
1242
+ } catch (error) {
1243
+ deps.recordRuntimeEvent?.("session", error, { phase: "shutdown" });
1244
+ throw error;
1245
+ }
1246
+ },
1247
+ };
1248
+ }
1249
+
1250
+ export function createTelegramQueueMutationController<TContext>(
1251
+ deps: TelegramQueueMutationControllerDeps<TContext>,
1252
+ ): TelegramQueueMutationController<TContext> {
1253
+ const buildRuntimeDeps = (
1254
+ ctx: TContext,
1255
+ ): TelegramQueueMutationRuntimeDeps<TContext> => ({
1256
+ ...deps,
1257
+ ctx,
1258
+ });
1259
+ return {
1260
+ append: (item, ctx) =>
1261
+ appendTelegramQueueItemRuntime(item, buildRuntimeDeps(ctx)),
1262
+ reorder: (ctx) => reorderTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)),
1263
+ removeByMessageIds: (messageIds, ctx) =>
1264
+ removeTelegramQueueItemsByMessageIdsRuntime(
1265
+ messageIds,
1266
+ buildRuntimeDeps(ctx),
1267
+ ),
1268
+ clearPriorityByMessageId: (messageId, ctx) =>
1269
+ clearTelegramQueuePromptPriorityRuntime(messageId, buildRuntimeDeps(ctx)),
1270
+ prioritizeByMessageId: (messageId, ctx) =>
1271
+ prioritizeTelegramQueuePromptRuntime(messageId, buildRuntimeDeps(ctx)),
1272
+ };
1273
+ }
1274
+
1275
+ function appendTelegramQueueItemRuntime<TContext>(
1276
+ item: TelegramQueueItem<TContext>,
1277
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
1278
+ ): void {
1279
+ deps.setQueuedItems(appendTelegramQueueItem(deps.getQueuedItems(), item));
1280
+ reorderTelegramQueueItemsRuntime(deps);
1281
+ }
1282
+
1283
+ export function reorderTelegramQueueItemsRuntime<TContext>(
1284
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
1285
+ ): void {
1286
+ deps.setQueuedItems(
1287
+ [...deps.getQueuedItems()].sort(compareTelegramQueueItems),
1288
+ );
1289
+ deps.updateStatus(deps.ctx);
1290
+ }
1291
+
1292
+ export function removeTelegramQueueItemsByMessageIdsRuntime<TContext>(
1293
+ messageIds: number[],
1294
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
1295
+ ): number {
1296
+ const { items, removedCount } = removeTelegramQueueItemsByMessageIds(
1297
+ deps.getQueuedItems(),
1298
+ messageIds,
1299
+ );
1300
+ if (removedCount === 0) return 0;
1301
+ deps.setQueuedItems(items);
1302
+ deps.updateStatus(deps.ctx);
1303
+ return removedCount;
1304
+ }
1305
+
1306
+ export function clearTelegramQueuePromptPriorityRuntime<TContext>(
1307
+ messageId: number,
1308
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
1309
+ ): boolean {
1310
+ const { changed, items } = clearTelegramQueuePromptPriority(
1311
+ deps.getQueuedItems(),
1312
+ messageId,
1313
+ );
1314
+ if (!changed) return false;
1315
+ deps.setQueuedItems(items);
1316
+ reorderTelegramQueueItemsRuntime(deps);
1317
+ return true;
1318
+ }
1319
+
1320
+ export function prioritizeTelegramQueuePromptRuntime<TContext>(
1321
+ messageId: number,
1322
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
1323
+ ): boolean {
1324
+ const nextPriorityReactionOrder = deps.getNextPriorityReactionOrder?.();
1325
+ if (nextPriorityReactionOrder === undefined) return false;
1326
+ const { changed, items } = prioritizeTelegramQueuePrompt(
1327
+ deps.getQueuedItems(),
1328
+ messageId,
1329
+ nextPriorityReactionOrder,
1330
+ );
1331
+ if (!changed) return false;
1332
+ deps.setQueuedItems(items);
1333
+ deps.incrementNextPriorityReactionOrder?.();
1334
+ reorderTelegramQueueItemsRuntime(deps);
1335
+ return true;
1336
+ }
1337
+
1338
+ export async function enqueueTelegramPromptTurnRuntime<
1339
+ TMessage,
1340
+ TContext = unknown,
1341
+ >(
1342
+ messages: TMessage[],
1343
+ deps: TelegramPromptEnqueueRuntimeDeps<TMessage, TContext>,
1344
+ ): Promise<void> {
1345
+ const enqueuePlan = planTelegramPromptEnqueue(
1346
+ deps.getQueuedItems(),
1347
+ deps.getPreserveQueuedTurnsAsHistory(),
1348
+ );
1349
+ deps.setPreserveQueuedTurnsAsHistory(false);
1350
+ const turn = await deps.createTurn(messages, enqueuePlan.historyTurns);
1351
+ deps.setQueuedItems(
1352
+ appendTelegramQueueItem(enqueuePlan.remainingItems, turn),
1353
+ );
1354
+ deps.updateStatus();
1355
+ deps.dispatchNextQueuedTelegramTurn();
1356
+ }
1357
+
1358
+ export function createTelegramPromptEnqueueController<
1359
+ TMessage,
1360
+ TContext = unknown,
1361
+ >(
1362
+ deps: TelegramPromptEnqueueControllerDeps<TMessage, TContext>,
1363
+ ): TelegramPromptEnqueueController<TMessage, TContext> {
1364
+ return {
1365
+ enqueue: (messages, ctx) =>
1366
+ enqueueTelegramPromptTurnRuntime(messages, {
1367
+ ...deps,
1368
+ updateStatus: () => deps.updateStatus(ctx),
1369
+ dispatchNextQueuedTelegramTurn: () =>
1370
+ deps.dispatchNextQueuedTelegramTurn(ctx),
1371
+ }),
1372
+ };
1373
+ }
1374
+
1375
+ export function createTelegramControlQueueController<TContext>(
1376
+ deps: TelegramControlQueueControllerDeps<TContext>,
1377
+ ): TelegramControlQueueController<TContext> {
1378
+ return {
1379
+ enqueue: (item, ctx) => {
1380
+ deps.appendControlItem(item, ctx);
1381
+ deps.dispatchNextQueuedTelegramTurn(ctx);
1382
+ },
1383
+ };
1384
+ }
1385
+
475
1386
  // --- Control Runtime ---
476
1387
 
477
- export interface TelegramControlRuntimeDeps {
478
- ctx: ExtensionContext;
1388
+ function getTelegramQueueErrorMessage(error: unknown): string {
1389
+ return error instanceof Error ? error.message : String(error);
1390
+ }
1391
+
1392
+ export interface TelegramRuntimeEventRecorderPort {
1393
+ recordRuntimeEvent?: (
1394
+ category: string,
1395
+ error: unknown,
1396
+ details?: Record<string, unknown>,
1397
+ ) => void;
1398
+ }
1399
+
1400
+ export interface TelegramControlRuntimeDeps<TContext>
1401
+ extends TelegramRuntimeEventRecorderPort {
1402
+ ctx: TContext;
479
1403
  sendTextReply: (
480
1404
  chatId: number,
481
1405
  replyToMessageId: number,
@@ -484,14 +1408,19 @@ export interface TelegramControlRuntimeDeps {
484
1408
  onSettled: () => void;
485
1409
  }
486
1410
 
487
- export async function executeTelegramControlItemRuntime(
488
- item: PendingTelegramControlItem,
489
- deps: TelegramControlRuntimeDeps,
1411
+ export async function executeTelegramControlItemRuntime<TContext>(
1412
+ item: PendingTelegramControlItem<TContext>,
1413
+ deps: TelegramControlRuntimeDeps<TContext>,
490
1414
  ): Promise<void> {
491
1415
  try {
492
1416
  await item.execute(deps.ctx);
493
1417
  } catch (error) {
494
- const message = error instanceof Error ? error.message : String(error);
1418
+ const message = getTelegramQueueErrorMessage(error);
1419
+ deps.recordRuntimeEvent?.("control", error, {
1420
+ controlType: item.controlType,
1421
+ chatId: item.chatId,
1422
+ replyToMessageId: item.replyToMessageId,
1423
+ });
495
1424
  await deps.sendTextReply(
496
1425
  item.chatId,
497
1426
  item.replyToMessageId,
@@ -504,9 +1433,12 @@ export async function executeTelegramControlItemRuntime(
504
1433
 
505
1434
  // --- Dispatch Runtime ---
506
1435
 
507
- export interface TelegramDispatchRuntimeDeps {
1436
+ export interface TelegramDispatchRuntimeDeps<TContext = unknown> {
508
1437
  executeControlItem: (
509
- item: Extract<TelegramQueueDispatchAction, { kind: "control" }>["item"],
1438
+ item: Extract<
1439
+ TelegramQueueDispatchAction<TContext>,
1440
+ { kind: "control" }
1441
+ >["item"],
510
1442
  ) => void;
511
1443
  onPromptDispatchStart: (chatId: number) => void;
512
1444
  sendUserMessage: (
@@ -519,9 +1451,25 @@ export interface TelegramDispatchRuntimeDeps {
519
1451
  onIdle: () => void;
520
1452
  }
521
1453
 
522
- export function executeTelegramQueueDispatchPlan(
523
- plan: TelegramQueueDispatchAction,
524
- deps: TelegramDispatchRuntimeDeps,
1454
+ export interface TelegramQueueDispatchControllerDeps<TContext = unknown>
1455
+ extends TelegramRuntimeEventRecorderPort {
1456
+ getQueuedItems: () => TelegramQueueItem<TContext>[];
1457
+ setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
1458
+ canDispatch: (ctx: TContext) => boolean;
1459
+ updateStatus: (ctx: TContext, error?: string) => void;
1460
+ sendTextReply: TelegramControlRuntimeDeps<TContext>["sendTextReply"];
1461
+ onPromptDispatchStart: (ctx: TContext, chatId: number) => void;
1462
+ sendUserMessage: TelegramDispatchRuntimeDeps<TContext>["sendUserMessage"];
1463
+ onPromptDispatchFailure: (ctx: TContext, message: string) => void;
1464
+ }
1465
+
1466
+ export interface TelegramQueueDispatchController<TContext = unknown> {
1467
+ dispatchNext: (ctx: TContext) => void;
1468
+ }
1469
+
1470
+ export function executeTelegramQueueDispatchPlan<TContext = unknown>(
1471
+ plan: TelegramQueueDispatchAction<TContext>,
1472
+ deps: TelegramDispatchRuntimeDeps<TContext>,
525
1473
  ): void {
526
1474
  if (plan.kind === "none") {
527
1475
  deps.onIdle();
@@ -535,7 +1483,83 @@ export function executeTelegramQueueDispatchPlan(
535
1483
  try {
536
1484
  deps.sendUserMessage(plan.item.content);
537
1485
  } catch (error) {
538
- const message = error instanceof Error ? error.message : String(error);
1486
+ const message = getTelegramQueueErrorMessage(error);
539
1487
  deps.onPromptDispatchFailure(message);
540
1488
  }
541
1489
  }
1490
+
1491
+ export type TelegramQueueDispatchRuntimeDeps<TContext = unknown> = Omit<
1492
+ TelegramQueueDispatchControllerDeps<TContext>,
1493
+ "canDispatch"
1494
+ > &
1495
+ TelegramDispatchReadinessDeps<TContext>;
1496
+
1497
+ export function createTelegramQueueDispatchRuntime<TContext = unknown>(
1498
+ deps: TelegramQueueDispatchRuntimeDeps<TContext>,
1499
+ ): TelegramQueueDispatchController<TContext> {
1500
+ return createTelegramQueueDispatchController({
1501
+ getQueuedItems: deps.getQueuedItems,
1502
+ setQueuedItems: deps.setQueuedItems,
1503
+ canDispatch: createTelegramDispatchReadinessChecker({
1504
+ isCompactionInProgress: deps.isCompactionInProgress,
1505
+ hasActiveTurn: deps.hasActiveTurn,
1506
+ hasDispatchPending: deps.hasDispatchPending,
1507
+ isIdle: deps.isIdle,
1508
+ hasPendingMessages: deps.hasPendingMessages,
1509
+ }),
1510
+ updateStatus: deps.updateStatus,
1511
+ sendTextReply: deps.sendTextReply,
1512
+ onPromptDispatchStart: deps.onPromptDispatchStart,
1513
+ sendUserMessage: deps.sendUserMessage,
1514
+ onPromptDispatchFailure: deps.onPromptDispatchFailure,
1515
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1516
+ });
1517
+ }
1518
+
1519
+ export function createTelegramQueueDispatchController<TContext = unknown>(
1520
+ deps: TelegramQueueDispatchControllerDeps<TContext>,
1521
+ ): TelegramQueueDispatchController<TContext> {
1522
+ let controlDispatchPending = false;
1523
+ const controller: TelegramQueueDispatchController<TContext> = {
1524
+ dispatchNext: (ctx) => {
1525
+ if (controlDispatchPending) {
1526
+ deps.updateStatus(ctx);
1527
+ return;
1528
+ }
1529
+ const dispatchPlan = planNextTelegramQueueAction(
1530
+ deps.getQueuedItems(),
1531
+ deps.canDispatch(ctx),
1532
+ );
1533
+ if (dispatchPlan.kind !== "none") {
1534
+ deps.setQueuedItems(dispatchPlan.remainingItems);
1535
+ }
1536
+ executeTelegramQueueDispatchPlan(dispatchPlan, {
1537
+ executeControlItem: (item) => {
1538
+ controlDispatchPending = true;
1539
+ deps.updateStatus(ctx);
1540
+ void executeTelegramControlItemRuntime(item, {
1541
+ ctx,
1542
+ sendTextReply: deps.sendTextReply,
1543
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1544
+ onSettled: () => {
1545
+ controlDispatchPending = false;
1546
+ deps.updateStatus(ctx);
1547
+ controller.dispatchNext(ctx);
1548
+ },
1549
+ });
1550
+ },
1551
+ onPromptDispatchStart: (chatId) => {
1552
+ deps.onPromptDispatchStart(ctx, chatId);
1553
+ },
1554
+ sendUserMessage: deps.sendUserMessage,
1555
+ onPromptDispatchFailure: (message) => {
1556
+ deps.onPromptDispatchFailure(ctx, message);
1557
+ },
1558
+ onIdle: () => {
1559
+ deps.updateStatus(ctx);
1560
+ },
1561
+ });
1562
+ },
1563
+ };
1564
+ return controller;
1565
+ }