@llblab/pi-telegram 0.24.10 → 0.25.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
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import * as Activity from "./lib/activity.ts";
8
+ import * as ActivityVerbosity from "./lib/activity-verbosity.ts";
8
9
  import * as Bindings from "./lib/bindings.ts";
9
10
  import * as BusApi from "./lib/bus-api.ts";
10
11
  import * as BusFollower from "./lib/bus-follower.ts";
@@ -67,11 +68,8 @@ export default function (pi: Pi.ExtensionAPI) {
67
68
  const getActiveTelegramThreadProfile = function (): string | undefined {
68
69
  return configStore.getActiveProfileName();
69
70
  };
70
- const busProcessRuntime = Bus.createTelegramBusProcessRuntime({
71
+ const busProcessRuntime = Bus.createCurrentTelegramBusProcessRuntime({
71
72
  getActiveProfileName: getActiveTelegramThreadProfile,
72
- pid: process.pid,
73
- parentPid: process.ppid,
74
- createdAtMs: Date.now(),
75
73
  });
76
74
  const {
77
75
  instanceId: telegramInstanceId,
@@ -119,14 +117,8 @@ export default function (pi: Pi.ExtensionAPI) {
119
117
  const runtimeEvents = runtimeDiagnostics.events;
120
118
  const recordRuntimeEvent = runtimeDiagnostics.recordRuntimeEvent;
121
119
  const configStore = Config.createTelegramConfigStore({ recordRuntimeEvent });
122
- const isTelegramBusConfigured = function (): boolean {
123
- return true;
124
- };
125
120
  const isTelegramBusRuntimeEnabled = function (): boolean {
126
- return (
127
- isTelegramBusConfigured() &&
128
- !telegramThreadCapabilityState.isTopicModeUnavailable()
129
- );
121
+ return !telegramThreadCapabilityState.isTopicModeUnavailable();
130
122
  };
131
123
  Config.bindGlobalTelegramConfigRuntime(configStore);
132
124
  const configControls = Config.createTelegramConfigControls(configStore);
@@ -397,6 +389,7 @@ export default function (pi: Pi.ExtensionAPI) {
397
389
  sendRichMessageDraft,
398
390
  downloadFile: downloadTelegramBridgeFile,
399
391
  editMessageText: editTelegramMessageText,
392
+ editMessageReplyMarkup: editTelegramMessageReplyMarkup,
400
393
  answerCallbackQuery,
401
394
  answerGuestQuery,
402
395
  deleteMessage: deleteTelegramMessage,
@@ -478,6 +471,9 @@ export default function (pi: Pi.ExtensionAPI) {
478
471
  getHandlers: configStore.getOutboundHandlers,
479
472
  recordRuntimeEvent,
480
473
  });
474
+ let activityVerbosityRuntime:
475
+ | ActivityVerbosity.TelegramActivityVerbosityRuntime
476
+ | undefined;
481
477
  const assistantOutputBindingRuntime =
482
478
  Bindings.createTelegramAssistantOutputBindingRuntime({
483
479
  isEnabled: configControls.isProactivePushEnabled,
@@ -502,12 +498,40 @@ export default function (pi: Pi.ExtensionAPI) {
502
498
  getHandlers: configStore.getOutboundHandlers,
503
499
  recordRuntimeEvent,
504
500
  },
501
+ waitForActivityIdle() {
502
+ return activityVerbosityRuntime?.waitForIdle() ?? Promise.resolve();
503
+ },
505
504
  recordRuntimeEvent,
506
505
  });
507
506
  const assistantOutputRuntime = assistantOutputBindingRuntime.runtime;
507
+ activityVerbosityRuntime =
508
+ ActivityVerbosity.createTelegramActivityVerbosityRuntime({
509
+ isVerbose() {
510
+ return configControls.getActivityVerbosity() === "verbose";
511
+ },
512
+ resolveTarget(event) {
513
+ return event.target ?? proactivePushTargetGetter();
514
+ },
515
+ captureAuthority: assistantOutputBindingRuntime.authority.captureAuthority,
516
+ isAuthorityActive:
517
+ assistantOutputBindingRuntime.authority.isAuthorityActive,
518
+ sendMessage,
519
+ sendRichMessageDraft,
520
+ editMessageText: editTelegramMessageText,
521
+ recordFailure(operation, event, error) {
522
+ recordRuntimeEvent("activity", error, {
523
+ operation,
524
+ eventType: event.type,
525
+ activityId: event.activityId,
526
+ });
527
+ },
528
+ });
508
529
  const activityRuntime = Activity.createTelegramActivityBridgeRuntime({
509
530
  generation: deliveryGenerationSeed,
510
- observeEvent: assistantOutputBindingRuntime.observeEvent,
531
+ observeEvent(event) {
532
+ assistantOutputBindingRuntime.observeEvent(event);
533
+ activityVerbosityRuntime.accept(event);
534
+ },
511
535
  recordFailure(handlerId, event, error) {
512
536
  recordRuntimeEvent("activity", error, {
513
537
  handlerId,
@@ -741,13 +765,7 @@ export default function (pi: Pi.ExtensionAPI) {
741
765
  stopTypingLoop: typing.stop,
742
766
  answerCallbackQuery,
743
767
  editInteractiveMessage,
744
- async editMessageReplyMarkup(chatId, messageId, replyMarkup) {
745
- await callTelegramApi("editMessageReplyMarkup", {
746
- chat_id: chatId,
747
- message_id: messageId,
748
- reply_markup: replyMarkup,
749
- });
750
- },
768
+ editMessageReplyMarkup: editTelegramMessageReplyMarkup,
751
769
  sendInteractiveMessage,
752
770
  deleteMessage: deleteTelegramMessage,
753
771
  answerGuestQuery,
@@ -932,7 +950,6 @@ export default function (pi: Pi.ExtensionAPI) {
932
950
  getAllowedUserId: configStore.getAllowedUserId,
933
951
  callApi: callTelegramApi,
934
952
  topicTargetStore: threadStore,
935
- isBusConfigured: isTelegramBusConfigured,
936
953
  isBusRuntimeEnabled: isTelegramBusRuntimeEnabled,
937
954
  ownsLock: lockRuntime.owns,
938
955
  startClassicPolling: pollingRuntime.start,
@@ -1069,6 +1086,7 @@ export default function (pi: Pi.ExtensionAPI) {
1069
1086
  },
1070
1087
  onTransportChanged() {
1071
1088
  deliveryLifecycleRuntime.onSessionStart();
1089
+ activityVerbosityRuntime.reset();
1072
1090
  modelContextAvailabilityRuntime.reconcile();
1073
1091
  },
1074
1092
  getStatusLines,
@@ -1096,6 +1114,7 @@ export default function (pi: Pi.ExtensionAPI) {
1096
1114
  onModelSelect: currentModelRuntime.onModelSelect,
1097
1115
  },
1098
1116
  activityRuntime,
1117
+ activityVerbosityRuntime,
1099
1118
  assistantOutputRuntime,
1100
1119
  configStore,
1101
1120
  abort,
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Bridge-owned Telegram activity verbosity projection
3
+ * Zones: telegram activity, rich rendering, operational delivery
4
+ * Owns ephemeral reasoning drafts and bounded durable tool disclosures; excludes activity normalization, assistant answer rendering, and transport authority policy
5
+ */
6
+
7
+ import type { TelegramActivityEvent } from "./activity.ts";
8
+ import { escapeHtml } from "./rendering.ts";
9
+ import type {
10
+ TelegramEditMessageTextBody,
11
+ TelegramRichText,
12
+ TelegramSendMessageBody,
13
+ TelegramSendRichMessageDraftBody,
14
+ TelegramSentMessage,
15
+ } from "./telegram-api.ts";
16
+ import type { TelegramTarget } from "./target.ts";
17
+
18
+ export const TELEGRAM_TOOL_ACTIVITY_ICON = "🛠";
19
+ export const TELEGRAM_ACTIVITY_DETAIL_MAX_CHARS = 1_200;
20
+ export const TELEGRAM_ACTIVITY_MESSAGE_MAX_CHARS = 3_900;
21
+ export const TELEGRAM_ACTIVITY_MESSAGE_MAX_TOOLS = 6;
22
+ export const TELEGRAM_REASONING_DRAFT_MAX_FRAMES = 24;
23
+ export const TELEGRAM_REASONING_BUFFER_MAX_CHARS = 1_200;
24
+ export const TELEGRAM_TOOL_UPDATE_MAX_ENTRIES = 4;
25
+
26
+ interface ToolActivity {
27
+ id: string;
28
+ name: string;
29
+ args: string;
30
+ updates: string[];
31
+ droppedUpdates: number;
32
+ result?: string;
33
+ isError?: boolean;
34
+ complete: boolean;
35
+ }
36
+
37
+ interface ToolMessage {
38
+ messageId: number;
39
+ tools: ToolActivity[];
40
+ target: TelegramTarget;
41
+ }
42
+
43
+ function targetEquals(left: TelegramTarget, right: TelegramTarget): boolean {
44
+ return left.chatId === right.chatId && left.threadId === right.threadId;
45
+ }
46
+
47
+ function redactActivityText(text: string): string {
48
+ return text
49
+ .replace(/\b\d{8,12}:[A-Za-z0-9_-]{30,}\b/g, "[REDACTED_BOT_TOKEN]")
50
+ .replace(
51
+ /\b(Bearer\s+)[A-Za-z0-9._~+/=-]{16,}\b/gi,
52
+ "$1[REDACTED]",
53
+ )
54
+ .replace(
55
+ /(["']?(?:api[_-]?key|token|password|secret)["']?\s*[:=]\s*["']?)[^"',\s}]+/gi,
56
+ "$1[REDACTED]",
57
+ );
58
+ }
59
+
60
+ function renderReasoningRichText(text: string): TelegramRichText {
61
+ const parts: TelegramRichText[] = [];
62
+ let cursor = 0;
63
+ while (cursor < text.length) {
64
+ const codeStart = text.indexOf("`", cursor);
65
+ const boldStart = text.indexOf("**", cursor);
66
+ const starts = [codeStart, boldStart].filter((index) => index >= 0);
67
+ if (starts.length === 0) {
68
+ parts.push(text.slice(cursor));
69
+ break;
70
+ }
71
+ const start = Math.min(...starts);
72
+ if (start > cursor) parts.push(text.slice(cursor, start));
73
+ const marker = start === codeStart ? "`" : "**";
74
+ const end = text.indexOf(marker, start + marker.length);
75
+ if (end < 0) {
76
+ parts.push(text.slice(start));
77
+ break;
78
+ }
79
+ parts.push({
80
+ type: marker === "`" ? "code" : "bold",
81
+ text: text.slice(start + marker.length, end),
82
+ });
83
+ cursor = end + marker.length;
84
+ }
85
+ if (parts.length === 0) return "";
86
+ return parts.length === 1 ? parts[0]! : parts;
87
+ }
88
+
89
+ function serializeActivityValue(value: unknown): string {
90
+ const seen = new WeakSet<object>();
91
+ let text: string;
92
+ try {
93
+ text =
94
+ JSON.stringify(
95
+ value,
96
+ (_key, nested) => {
97
+ if (typeof nested === "bigint") return nested.toString();
98
+ if (nested && typeof nested === "object") {
99
+ if (seen.has(nested)) return "[Circular]";
100
+ seen.add(nested);
101
+ }
102
+ return nested;
103
+ },
104
+ 2,
105
+ ) ?? JSON.stringify(String(value));
106
+ } catch {
107
+ text = JSON.stringify(String(value));
108
+ }
109
+ const redacted = redactActivityText(text);
110
+ if (redacted.length <= TELEGRAM_ACTIVITY_DETAIL_MAX_CHARS) return redacted;
111
+ const omitted = redacted.length - TELEGRAM_ACTIVITY_DETAIL_MAX_CHARS;
112
+ return `${redacted.slice(0, TELEGRAM_ACTIVITY_DETAIL_MAX_CHARS)}\n… [${omitted} chars truncated]`;
113
+ }
114
+
115
+ function renderToolActivityHtml(tool: ToolActivity): string {
116
+ const evidence = [`arguments: ${tool.args}`];
117
+ if (tool.droppedUpdates > 0) {
118
+ evidence.push(`… [${tool.droppedUpdates} earlier updates omitted]`);
119
+ }
120
+ tool.updates.forEach((update, index) => {
121
+ evidence.push(
122
+ `update ${tool.droppedUpdates + index + 1}: ${update}`,
123
+ );
124
+ });
125
+ if (tool.complete && tool.result !== undefined) {
126
+ evidence.push(`${tool.isError ? "error" : "result"}: ${tool.result}`);
127
+ }
128
+ const status = tool.complete
129
+ ? tool.isError
130
+ ? "failed"
131
+ : "done"
132
+ : "running";
133
+ return [
134
+ `<b>${TELEGRAM_TOOL_ACTIVITY_ICON}&#160; ${escapeHtml(tool.name)}:</b> <code>${status}</code>`,
135
+ `<blockquote expandable>${escapeHtml(evidence.join("\n\n"))}</blockquote>`,
136
+ ].join("\n");
137
+ }
138
+
139
+ export function renderTelegramToolActivityHtml(
140
+ tools: readonly ToolActivity[],
141
+ ): string {
142
+ return tools.map(renderToolActivityHtml).join("\n\n");
143
+ }
144
+
145
+ function toolMessageSize(tools: readonly ToolActivity[]): number {
146
+ return renderTelegramToolActivityHtml(tools).length;
147
+ }
148
+
149
+ function draftIdForActivity(activityId: string): number {
150
+ let hash = 2166136261;
151
+ for (const character of activityId) {
152
+ hash ^= character.charCodeAt(0);
153
+ hash = Math.imul(hash, 16777619);
154
+ }
155
+ return (hash >>> 0) || 1;
156
+ }
157
+
158
+ export interface TelegramActivityVerbosityRuntime {
159
+ accept: (event: TelegramActivityEvent) => void;
160
+ reset: () => void;
161
+ stop: () => void;
162
+ waitForIdle: () => Promise<void>;
163
+ }
164
+
165
+ export function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
166
+ isVerbose: () => boolean;
167
+ resolveTarget: (event: TelegramActivityEvent) => TelegramTarget | undefined;
168
+ captureAuthority: () => TAuthority;
169
+ isAuthorityActive: (authority: TAuthority) => boolean;
170
+ sendMessage: (body: TelegramSendMessageBody) => Promise<TelegramSentMessage>;
171
+ sendRichMessageDraft: (
172
+ body: TelegramSendRichMessageDraftBody,
173
+ ) => Promise<boolean>;
174
+ editMessageText: (
175
+ body: TelegramEditMessageTextBody,
176
+ ) => Promise<"edited" | "unchanged">;
177
+ recordFailure?: (
178
+ operation: "reasoning-draft" | "tool-send" | "tool-edit",
179
+ event: TelegramActivityEvent,
180
+ error: unknown,
181
+ ) => void;
182
+ }): TelegramActivityVerbosityRuntime {
183
+ let active = true;
184
+ let generation = 0;
185
+ let tail = Promise.resolve();
186
+ let activityId: string | undefined;
187
+ let authority: TAuthority | undefined;
188
+ let target: TelegramTarget | undefined;
189
+ let reasoningBuffer = "";
190
+ let reasoningChars = 0;
191
+ let reasoningDraftFrames = 0;
192
+ let lastReasoningDraftChars = 0;
193
+ let toolMessage: ToolMessage | undefined;
194
+ const tools = new Map<string, ToolActivity>();
195
+ const toolOrder: string[] = [];
196
+
197
+ const clearActivity = () => {
198
+ activityId = undefined;
199
+ authority = undefined;
200
+ target = undefined;
201
+ reasoningBuffer = "";
202
+ reasoningChars = 0;
203
+ reasoningDraftFrames = 0;
204
+ lastReasoningDraftChars = 0;
205
+ toolMessage = undefined;
206
+ tools.clear();
207
+ toolOrder.length = 0;
208
+ };
209
+ const hasAuthority = (): boolean =>
210
+ authority !== undefined && deps.isAuthorityActive(authority);
211
+ const ensureActivity = (event: TelegramActivityEvent): boolean => {
212
+ if (!deps.isVerbose()) return false;
213
+ if (activityId === event.activityId) return hasAuthority();
214
+ clearActivity();
215
+ const resolvedTarget = deps.resolveTarget(event);
216
+ if (!resolvedTarget) return false;
217
+ activityId = event.activityId;
218
+ target = { ...resolvedTarget };
219
+ authority = deps.captureAuthority();
220
+ return hasAuthority();
221
+ };
222
+ const closeToolBatch = () => {
223
+ toolMessage = undefined;
224
+ };
225
+ const sendReasoningDraft = async (
226
+ event: TelegramActivityEvent,
227
+ acceptedGeneration: number,
228
+ ) => {
229
+ if (
230
+ generation !== acceptedGeneration ||
231
+ !target ||
232
+ !hasAuthority()
233
+ ) {
234
+ return;
235
+ }
236
+ const omitted = reasoningChars - reasoningBuffer.length;
237
+ const text =
238
+ omitted > 0
239
+ ? `… [${omitted} earlier chars omitted]\n${reasoningBuffer}`
240
+ : reasoningBuffer;
241
+ try {
242
+ await deps.sendRichMessageDraft({
243
+ chat_id: target.chatId,
244
+ ...(target.threadId === undefined
245
+ ? {}
246
+ : { message_thread_id: target.threadId }),
247
+ draft_id: draftIdForActivity(event.activityId),
248
+ rich_message: {
249
+ blocks: [
250
+ {
251
+ type: "thinking",
252
+ text: renderReasoningRichText(redactActivityText(text)),
253
+ },
254
+ ],
255
+ skip_entity_detection: true,
256
+ },
257
+ });
258
+ if (generation !== acceptedGeneration) return;
259
+ reasoningDraftFrames += 1;
260
+ lastReasoningDraftChars = reasoningChars;
261
+ } catch (error) {
262
+ deps.recordFailure?.("reasoning-draft", event, error);
263
+ }
264
+ };
265
+ const publishTool = async (
266
+ event: TelegramActivityEvent,
267
+ tool: ToolActivity,
268
+ acceptedGeneration: number,
269
+ ) => {
270
+ if (
271
+ generation !== acceptedGeneration ||
272
+ !target ||
273
+ !hasAuthority()
274
+ ) {
275
+ return;
276
+ }
277
+ const canAppend =
278
+ toolMessage &&
279
+ targetEquals(toolMessage.target, target) &&
280
+ toolMessage.tools.length < TELEGRAM_ACTIVITY_MESSAGE_MAX_TOOLS &&
281
+ toolMessageSize([...toolMessage.tools, tool]) <=
282
+ TELEGRAM_ACTIVITY_MESSAGE_MAX_CHARS;
283
+ try {
284
+ if (canAppend && toolMessage) {
285
+ const nextTools = [...toolMessage.tools, tool];
286
+ await deps.editMessageText({
287
+ chat_id: target.chatId,
288
+ message_id: toolMessage.messageId,
289
+ text: renderTelegramToolActivityHtml(nextTools),
290
+ parse_mode: "HTML",
291
+ });
292
+ if (generation !== acceptedGeneration) return;
293
+ toolMessage.tools = nextTools;
294
+ return;
295
+ }
296
+ const sent = await deps.sendMessage({
297
+ chat_id: target.chatId,
298
+ ...(target.threadId === undefined
299
+ ? {}
300
+ : { message_thread_id: target.threadId }),
301
+ text: renderTelegramToolActivityHtml([tool]),
302
+ parse_mode: "HTML",
303
+ });
304
+ if (generation !== acceptedGeneration) return;
305
+ toolMessage = {
306
+ messageId: sent.message_id,
307
+ tools: [tool],
308
+ target: { ...target },
309
+ };
310
+ } catch (error) {
311
+ deps.recordFailure?.(canAppend ? "tool-edit" : "tool-send", event, error);
312
+ closeToolBatch();
313
+ }
314
+ };
315
+ const process = async (
316
+ event: TelegramActivityEvent,
317
+ acceptedGeneration: number,
318
+ ) => {
319
+ if (!ensureActivity(event)) {
320
+ if (activityId === event.activityId && !deps.isVerbose()) clearActivity();
321
+ return;
322
+ }
323
+ if (
324
+ event.type === "assistant-text-delta" ||
325
+ event.type === "assistant-segment" ||
326
+ event.type === "reasoning-delta" ||
327
+ event.type === "reasoning-end"
328
+ ) {
329
+ closeToolBatch();
330
+ }
331
+ if (event.type === "reasoning-delta") {
332
+ reasoningChars += event.delta.length;
333
+ reasoningBuffer = `${reasoningBuffer}${event.delta}`.slice(
334
+ -TELEGRAM_REASONING_BUFFER_MAX_CHARS,
335
+ );
336
+ if (
337
+ reasoningDraftFrames < TELEGRAM_REASONING_DRAFT_MAX_FRAMES &&
338
+ (reasoningDraftFrames === 0 ||
339
+ reasoningChars - lastReasoningDraftChars >= 160)
340
+ ) {
341
+ await sendReasoningDraft(event, acceptedGeneration);
342
+ }
343
+ return;
344
+ }
345
+ if (event.type === "reasoning-end") {
346
+ if (
347
+ reasoningChars > lastReasoningDraftChars &&
348
+ reasoningDraftFrames < TELEGRAM_REASONING_DRAFT_MAX_FRAMES
349
+ ) {
350
+ await sendReasoningDraft(event, acceptedGeneration);
351
+ }
352
+ reasoningBuffer = "";
353
+ reasoningChars = 0;
354
+ return;
355
+ }
356
+ if (event.type === "tool-start") {
357
+ tools.set(event.toolCallId, {
358
+ id: event.toolCallId,
359
+ name: event.toolName,
360
+ args: serializeActivityValue(event.args),
361
+ updates: [],
362
+ droppedUpdates: 0,
363
+ complete: false,
364
+ });
365
+ toolOrder.push(event.toolCallId);
366
+ return;
367
+ }
368
+ if (event.type === "tool-update") {
369
+ const tool = tools.get(event.toolCallId);
370
+ if (!tool) return;
371
+ tool.updates.push(serializeActivityValue(event.update));
372
+ if (tool.updates.length > TELEGRAM_TOOL_UPDATE_MAX_ENTRIES) {
373
+ tool.updates.shift();
374
+ tool.droppedUpdates += 1;
375
+ }
376
+ return;
377
+ }
378
+ if (event.type === "tool-end") {
379
+ const tool = tools.get(event.toolCallId) ?? {
380
+ id: event.toolCallId,
381
+ name: event.toolName,
382
+ args: serializeActivityValue(undefined),
383
+ updates: [],
384
+ droppedUpdates: 0,
385
+ complete: false,
386
+ };
387
+ if (!tools.has(event.toolCallId)) toolOrder.push(event.toolCallId);
388
+ tool.result = serializeActivityValue(event.result);
389
+ tool.isError = event.isError;
390
+ tool.complete = true;
391
+ tools.set(event.toolCallId, tool);
392
+ while (toolOrder.length > 0) {
393
+ const next = tools.get(toolOrder[0]!);
394
+ if (!next?.complete) break;
395
+ toolOrder.shift();
396
+ tools.delete(next.id);
397
+ await publishTool(event, next, acceptedGeneration);
398
+ if (generation !== acceptedGeneration) return;
399
+ }
400
+ return;
401
+ }
402
+ if (event.type === "agent-end" || event.type === "agent-settled") {
403
+ clearActivity();
404
+ }
405
+ };
406
+ return {
407
+ accept(event) {
408
+ if (!active) return;
409
+ const acceptedGeneration = generation;
410
+ tail = tail
411
+ .then(() => {
412
+ if (!active || generation !== acceptedGeneration) return;
413
+ return process(event, acceptedGeneration);
414
+ })
415
+ .catch((error) => {
416
+ deps.recordFailure?.("tool-send", event, error);
417
+ });
418
+ },
419
+ reset() {
420
+ generation += 1;
421
+ clearActivity();
422
+ tail = Promise.resolve();
423
+ },
424
+ stop() {
425
+ active = false;
426
+ generation += 1;
427
+ clearActivity();
428
+ tail = Promise.resolve();
429
+ },
430
+ waitForIdle() {
431
+ return tail;
432
+ },
433
+ };
434
+ }
package/lib/bindings.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import * as Activity from "./activity.ts";
8
+ import type { TelegramActivityVerbosityRuntime } from "./activity-verbosity.ts";
8
9
  import * as CommandTemplates from "./command-templates.ts";
9
10
  import * as Commands from "./commands.ts";
10
11
  import * as Config from "./config.ts";
@@ -61,19 +62,24 @@ export function createTelegramAssistantOutputBindingRuntime<
61
62
  sender: Parameters<
62
63
  typeof OutboundHandlers.createTelegramAssistantOutputSender<TTransportStamp>
63
64
  >[0];
65
+ waitForActivityIdle?: () => Promise<void>;
64
66
  recordRuntimeEvent: TelegramRuntimeEventRecorder;
65
67
  }): TelegramAssistantOutputBindingRuntime<TTransportStamp> {
66
68
  const authority = Routing.createTelegramAssistantOutputAuthorityRuntime(
67
69
  deps.authority,
68
70
  );
69
- const send =
71
+ const sendOutput =
70
72
  OutboundHandlers.createTelegramAssistantOutputSender<TTransportStamp>(
71
73
  deps.sender,
72
74
  );
73
75
  const runtime = Activity.createTelegramAssistantOutputRuntime({
74
76
  isEnabled: deps.isEnabled,
75
77
  ...authority,
76
- send,
78
+ async send(event, authority, isAuthorityActive) {
79
+ await deps.waitForActivityIdle?.();
80
+ if (!isAuthorityActive()) return;
81
+ await sendOutput(event, authority, isAuthorityActive);
82
+ },
77
83
  recordFailure(event, error) {
78
84
  deps.recordRuntimeEvent("proactive-push", error, {
79
85
  activityId: event.activityId,
@@ -278,6 +284,7 @@ export function registerTelegramCommandsAndTools({
278
284
  interface TelegramLifecycleBindingDeps {
279
285
  pi: Pi.ExtensionAPI;
280
286
  activityRuntime: Activity.TelegramActivityRuntime;
287
+ activityVerbosityRuntime?: TelegramActivityVerbosityRuntime;
281
288
  assistantOutputRuntime: Pick<
282
289
  Activity.TelegramAssistantOutputRuntime,
283
290
  "start" | "stop"
@@ -363,6 +370,7 @@ interface TelegramLifecycleBindingDeps {
363
370
  export function registerTelegramLifecycleRuntimeHooks({
364
371
  pi,
365
372
  activityRuntime,
373
+ activityVerbosityRuntime,
366
374
  assistantOutputRuntime,
367
375
  sessionLifecycleRuntime,
368
376
  configStore,
@@ -556,6 +564,7 @@ export function registerTelegramLifecycleRuntimeHooks({
556
564
  isSessionActive: isSessionContextActive,
557
565
  isTurnTransportActive,
558
566
  waitForTypingIdle: typing.waitForIdle,
567
+ waitForActivityIdle: activityVerbosityRuntime?.waitForIdle,
559
568
  dispatchNextQueuedTelegramTurn,
560
569
  requestDeferredDispatchNextQueuedTelegramTurn:
561
570
  deferredQueueDispatchRuntime.request,
@@ -638,6 +647,7 @@ export function registerTelegramLifecycleRuntimeHooks({
638
647
  previewRuntime.invalidate();
639
648
  assistantOutputRuntime.start();
640
649
  activityRuntime.onSessionStart?.();
650
+ activityVerbosityRuntime?.reset();
641
651
  modelContextAvailabilityRuntime.reconcile();
642
652
  await sessionLifecycleRuntime.onSessionStart(event, ctx);
643
653
  },
@@ -645,6 +655,7 @@ export function registerTelegramLifecycleRuntimeHooks({
645
655
  if (!isSessionContextActive(ctx)) return;
646
656
  agentLifecycleHooks.clearRetainedAgentEnd();
647
657
  activityRuntime.onSessionShutdown();
658
+ activityVerbosityRuntime?.reset();
648
659
  assistantOutputRuntime.stop();
649
660
  compactionObserver.onSessionShutdown();
650
661
  if (event.reason === "quit" && disconnectOnQuit) {
package/lib/bus-api.ts CHANGED
@@ -265,6 +265,24 @@ export function createTelegramBusAwareApiRuntime(
265
265
  await deps.callFollowerApi("call", ["editMessageText", body]);
266
266
  return "edited";
267
267
  },
268
+ async editMessageReplyMarkup(
269
+ chatId: number,
270
+ messageId: number,
271
+ replyMarkup: unknown,
272
+ ): Promise<void> {
273
+ if (deps.ownsDirect()) {
274
+ await deps.directRuntime.editMessageReplyMarkup(
275
+ chatId,
276
+ messageId,
277
+ replyMarkup,
278
+ );
279
+ return;
280
+ }
281
+ await deps.callFollowerApi("call", [
282
+ "editMessageReplyMarkup",
283
+ { chat_id: chatId, message_id: messageId, reply_markup: replyMarkup },
284
+ ]);
285
+ },
268
286
  async answerCallbackQuery(
269
287
  callbackQueryId: string,
270
288
  text?: string,
package/lib/bus.ts CHANGED
@@ -72,6 +72,20 @@ export function getTelegramProcessBirthIdentity(
72
72
  return `${pid}:generation:${fallbackGeneration}`;
73
73
  }
74
74
 
75
+ export function createCurrentTelegramBusProcessRuntime(input: {
76
+ getActiveProfileName: () => string | undefined;
77
+ pid?: number;
78
+ parentPid?: number;
79
+ createdAtMs?: number;
80
+ }): TelegramBusProcessRuntime {
81
+ return createTelegramBusProcessRuntime({
82
+ getActiveProfileName: input.getActiveProfileName,
83
+ pid: input.pid ?? process.pid,
84
+ parentPid: input.parentPid ?? process.ppid,
85
+ createdAtMs: input.createdAtMs ?? Date.now(),
86
+ });
87
+ }
88
+
75
89
  export function createTelegramBusProcessRuntime(input: {
76
90
  getActiveProfileName: () => string | undefined;
77
91
  pid: number;