@llblab/pi-telegram 0.3.0 → 0.5.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/locks.ts ADDED
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Telegram singleton lock helpers
3
+ * Owns shared locks.json access and Telegram bridge ownership semantics
4
+ */
5
+
6
+ import {
7
+ existsSync,
8
+ mkdirSync,
9
+ readFileSync,
10
+ renameSync,
11
+ unlinkSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { dirname, join, resolve } from "node:path";
16
+
17
+ export const TELEGRAM_LOCK_KEY = "@llblab/pi-telegram";
18
+
19
+ function getAgentDir(): string {
20
+ return process.env.PI_CODING_AGENT_DIR
21
+ ? resolve(process.env.PI_CODING_AGENT_DIR)
22
+ : join(homedir(), ".pi", "agent");
23
+ }
24
+
25
+ function getLocksPath(): string {
26
+ return join(getAgentDir(), "locks.json");
27
+ }
28
+
29
+ export interface TelegramLockEntry {
30
+ pid: number;
31
+ cwd?: string;
32
+ }
33
+
34
+ export interface TelegramLockContext {
35
+ cwd: string;
36
+ }
37
+
38
+ export type TelegramLockState =
39
+ | { kind: "inactive" }
40
+ | { kind: "active-here"; lock: TelegramLockEntry }
41
+ | { kind: "active-elsewhere"; lock: TelegramLockEntry }
42
+ | { kind: "stale"; lock: TelegramLockEntry };
43
+
44
+ export interface TelegramLockAcquireOptions {
45
+ force?: boolean;
46
+ }
47
+
48
+ export type TelegramLockAcquireResult =
49
+ | { ok: true; lock: TelegramLockEntry; replacedStale: boolean }
50
+ | { ok: false; lock: TelegramLockEntry };
51
+
52
+ export interface TelegramLockRuntime<TContext extends TelegramLockContext> {
53
+ acquire: (
54
+ ctx: TContext,
55
+ options?: TelegramLockAcquireOptions,
56
+ ) => TelegramLockAcquireResult;
57
+ release: () => TelegramLockState;
58
+ getState: () => TelegramLockState;
59
+ getStatusLabel: () => string;
60
+ owns: (ctx?: TelegramLockContext) => boolean;
61
+ }
62
+
63
+ export interface TelegramLockRuntimeOptions {
64
+ key?: string;
65
+ locksPath?: string;
66
+ pid?: number;
67
+ isProcessAlive?: (pid: number) => boolean;
68
+ }
69
+
70
+ export interface TelegramLockedPollingStartOptions {
71
+ force?: boolean;
72
+ }
73
+
74
+ export type TelegramLockedPollingStartResult =
75
+ | { ok: true; message: string; canTakeover?: false }
76
+ | { ok: false; message: string; canTakeover?: boolean; owner?: string };
77
+
78
+ export interface TelegramLockedPollingRuntime<
79
+ TContext extends TelegramLockContext,
80
+ > {
81
+ start: (
82
+ ctx: TContext,
83
+ options?: TelegramLockedPollingStartOptions,
84
+ ) => Promise<TelegramLockedPollingStartResult>;
85
+ stop: () => Promise<string>;
86
+ suspend: () => Promise<void>;
87
+ onSessionStart: (_event: unknown, ctx: TContext) => Promise<void>;
88
+ }
89
+
90
+ export interface TelegramLockedPollingRuntimeDeps<
91
+ TContext extends TelegramLockContext,
92
+ > {
93
+ lock: TelegramLockRuntime<TContext>;
94
+ hasBotToken: () => boolean;
95
+ startPolling: (ctx: TContext) => void | Promise<void>;
96
+ stopPolling: () => Promise<void>;
97
+ updateStatus: (ctx: TContext) => void;
98
+ recordRuntimeEvent?: (
99
+ category: string,
100
+ error: unknown,
101
+ details?: Record<string, unknown>,
102
+ ) => void;
103
+ ownershipCheckMs?: number;
104
+ }
105
+
106
+ export function readLocks(path = getLocksPath()): Record<string, unknown> {
107
+ if (!existsSync(path)) return {};
108
+ try {
109
+ const value = JSON.parse(readFileSync(path, "utf8"));
110
+ return value && typeof value === "object" && !Array.isArray(value)
111
+ ? (value as Record<string, unknown>)
112
+ : {};
113
+ } catch {
114
+ return {};
115
+ }
116
+ }
117
+
118
+ export function writeLocks(path: string, locks: Record<string, unknown>): void {
119
+ mkdirSync(dirname(path), { recursive: true });
120
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
121
+ try {
122
+ writeFileSync(tempPath, `${JSON.stringify(locks, null, 2)}\n`, "utf8");
123
+ renameSync(tempPath, path);
124
+ } catch (error) {
125
+ try {
126
+ unlinkSync(tempPath);
127
+ } catch {
128
+ /* best effort */
129
+ }
130
+ throw error;
131
+ }
132
+ }
133
+
134
+ export function parseTelegramLockEntry(
135
+ value: unknown,
136
+ ): TelegramLockEntry | undefined {
137
+ if (!value || typeof value !== "object" || Array.isArray(value))
138
+ return undefined;
139
+ const record = value as Record<string, unknown>;
140
+ if (typeof record.pid !== "number") return undefined;
141
+ return {
142
+ pid: record.pid,
143
+ cwd: typeof record.cwd === "string" ? record.cwd : undefined,
144
+ };
145
+ }
146
+
147
+ export function isProcessAlive(pid: number): boolean {
148
+ if (!Number.isInteger(pid) || pid <= 0) return false;
149
+ try {
150
+ process.kill(pid, 0);
151
+ return true;
152
+ } catch (error) {
153
+ return (error as { code?: string }).code === "EPERM";
154
+ }
155
+ }
156
+
157
+ function formatLock(lock: TelegramLockEntry): string {
158
+ return lock.cwd ? `pid ${lock.pid}, cwd ${lock.cwd}` : `pid ${lock.pid}`;
159
+ }
160
+
161
+ function getLockState(
162
+ lock: TelegramLockEntry | undefined,
163
+ pid: number,
164
+ isAlive: (pid: number) => boolean,
165
+ ): TelegramLockState {
166
+ if (!lock) return { kind: "inactive" };
167
+ if (lock.pid === pid) return { kind: "active-here", lock };
168
+ if (isAlive(lock.pid)) return { kind: "active-elsewhere", lock };
169
+ return { kind: "stale", lock };
170
+ }
171
+
172
+ function ownsLockContext(
173
+ lock: TelegramLockEntry | undefined,
174
+ pid: number,
175
+ ctx?: TelegramLockContext,
176
+ ): boolean {
177
+ if (!lock || lock.pid !== pid) return false;
178
+ return !lock.cwd || !ctx || lock.cwd === ctx.cwd;
179
+ }
180
+
181
+ function snapshotLockContext(ctx: TelegramLockContext): TelegramLockContext {
182
+ return { cwd: ctx.cwd };
183
+ }
184
+
185
+ function formatLockState(state: TelegramLockState): string {
186
+ switch (state.kind) {
187
+ case "inactive":
188
+ return "inactive";
189
+ case "active-here":
190
+ return "active here";
191
+ case "active-elsewhere":
192
+ return `active elsewhere (${formatLock(state.lock)})`;
193
+ case "stale":
194
+ return `stale (${formatLock(state.lock)})`;
195
+ }
196
+ }
197
+
198
+ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
199
+ options: TelegramLockRuntimeOptions = {},
200
+ ): TelegramLockRuntime<TContext> {
201
+ const key = options.key ?? TELEGRAM_LOCK_KEY;
202
+ const locksPath = options.locksPath ?? getLocksPath();
203
+ const pid = options.pid ?? process.pid;
204
+ const isAlive = options.isProcessAlive ?? isProcessAlive;
205
+ const readLock = () => parseTelegramLockEntry(readLocks(locksPath)[key]);
206
+ const writeLock = (lock: TelegramLockEntry) => {
207
+ const locks = readLocks(locksPath);
208
+ locks[key] = lock;
209
+ writeLocks(locksPath, locks);
210
+ };
211
+ return {
212
+ acquire: (ctx, acquireOptions = {}) => {
213
+ const state = getLockState(readLock(), pid, isAlive);
214
+ if (state.kind === "active-elsewhere" && !acquireOptions.force)
215
+ return { ok: false, lock: state.lock };
216
+ const lock = { pid, cwd: ctx.cwd };
217
+ writeLock(lock);
218
+ return { ok: true, lock, replacedStale: state.kind === "stale" };
219
+ },
220
+ release: () => {
221
+ const state = getLockState(readLock(), pid, isAlive);
222
+ if (state.kind === "active-here" || state.kind === "stale") {
223
+ const locks = readLocks(locksPath);
224
+ delete locks[key];
225
+ writeLocks(locksPath, locks);
226
+ }
227
+ return state;
228
+ },
229
+ getState: () => getLockState(readLock(), pid, isAlive),
230
+ getStatusLabel: () =>
231
+ formatLockState(getLockState(readLock(), pid, isAlive)),
232
+ owns: (ctx) => ownsLockContext(readLock(), pid, ctx),
233
+ };
234
+ }
235
+
236
+ export function createTelegramLockedPollingRuntime<
237
+ TContext extends TelegramLockContext,
238
+ >(
239
+ deps: TelegramLockedPollingRuntimeDeps<TContext>,
240
+ ): TelegramLockedPollingRuntime<TContext> {
241
+ let ownershipInterval: ReturnType<typeof setInterval> | undefined;
242
+ let ownershipStop: Promise<void> | undefined;
243
+ const ownershipCheckMs = deps.ownershipCheckMs ?? 1000;
244
+ const stopOwnershipWatcher = () => {
245
+ if (!ownershipInterval) return;
246
+ clearInterval(ownershipInterval);
247
+ ownershipInterval = undefined;
248
+ };
249
+ const updateStatusSafely = (ctx: TContext, phase: string) => {
250
+ try {
251
+ deps.updateStatus(ctx);
252
+ } catch (error) {
253
+ deps.recordRuntimeEvent?.("lock", error, { phase });
254
+ }
255
+ };
256
+ const suspendPolling = async () => {
257
+ stopOwnershipWatcher();
258
+ if (ownershipStop) {
259
+ await ownershipStop;
260
+ return;
261
+ }
262
+ await deps.stopPolling();
263
+ };
264
+ const stopAfterOwnershipLoss = (ctx: TContext) => {
265
+ if (ownershipStop) return;
266
+ stopOwnershipWatcher();
267
+ ownershipStop = deps
268
+ .stopPolling()
269
+ .catch((error) =>
270
+ deps.recordRuntimeEvent?.("lock", error, { phase: "ownership-loss" }),
271
+ )
272
+ .finally(() => {
273
+ ownershipStop = undefined;
274
+ updateStatusSafely(ctx, "ownership-loss-status");
275
+ });
276
+ };
277
+ const startOwnershipWatcher = (ctx: TContext) => {
278
+ const owner = snapshotLockContext(ctx);
279
+ stopOwnershipWatcher();
280
+ ownershipInterval = setInterval(() => {
281
+ if (deps.lock.owns(owner)) return;
282
+ stopAfterOwnershipLoss(ctx);
283
+ }, ownershipCheckMs);
284
+ ownershipInterval.unref?.();
285
+ };
286
+ return {
287
+ start: async (ctx, options = {}) => {
288
+ if (!deps.hasBotToken())
289
+ return { ok: false, message: "Telegram bot is not configured." };
290
+ const acquired = deps.lock.acquire(ctx, options);
291
+ if (!acquired.ok) {
292
+ return {
293
+ ok: false,
294
+ canTakeover: true,
295
+ owner: formatLock(acquired.lock),
296
+ message: `Telegram bridge is active in another pi instance (${formatLock(acquired.lock)}).`,
297
+ };
298
+ }
299
+ await deps.startPolling(ctx);
300
+ startOwnershipWatcher(ctx);
301
+ deps.updateStatus(ctx);
302
+ const staleSuffix = acquired.replacedStale ? " Replaced stale lock." : "";
303
+ return { ok: true, message: `Telegram bridge connected.${staleSuffix}` };
304
+ },
305
+ stop: async () => {
306
+ await suspendPolling();
307
+ const state = deps.lock.release();
308
+ if (state.kind === "active-elsewhere") {
309
+ return `Telegram bridge is active in another pi instance (${formatLock(state.lock)}).`;
310
+ }
311
+ if (state.kind === "stale")
312
+ return `Removed stale Telegram bridge lock (${formatLock(state.lock)}).`;
313
+ return "Telegram bridge disconnected.";
314
+ },
315
+ suspend: suspendPolling,
316
+ onSessionStart: async (_event, ctx) => {
317
+ if (!deps.hasBotToken()) return;
318
+ const ownsCurrentLock = deps.lock.owns(ctx);
319
+ const state = ownsCurrentLock ? undefined : deps.lock.getState();
320
+ const canResumeStaleSameCwd =
321
+ state?.kind === "stale" && state.lock.cwd === ctx.cwd;
322
+ if (!ownsCurrentLock && !canResumeStaleSameCwd) return;
323
+ try {
324
+ if (canResumeStaleSameCwd) {
325
+ const acquired = deps.lock.acquire(ctx);
326
+ if (!acquired.ok) return;
327
+ }
328
+ await deps.startPolling(ctx);
329
+ startOwnershipWatcher(ctx);
330
+ deps.updateStatus(ctx);
331
+ } catch (error) {
332
+ deps.recordRuntimeEvent?.("lock", error, { phase: "auto-start" });
333
+ }
334
+ },
335
+ };
336
+ }
package/lib/media.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  * Normalizes inbound Telegram messages into reusable file, text, id, history, and media-group metadata
4
4
  */
5
5
 
6
+ import { basename, dirname } from "node:path";
7
+
6
8
  const TELEGRAM_MEDIA_GROUP_DEBOUNCE_MS = 1200;
7
9
 
8
10
  export interface TelegramPhotoSize {
@@ -89,10 +91,20 @@ export interface TelegramMediaGroupControllerOptions {
89
91
  clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
90
92
  }
91
93
 
94
+ export type TelegramAttachmentKind =
95
+ | "photo"
96
+ | "document"
97
+ | "video"
98
+ | "audio"
99
+ | "voice"
100
+ | "animation"
101
+ | "sticker";
102
+
92
103
  export interface TelegramFileInfo {
93
104
  file_id: string;
94
105
  fileName: string;
95
106
  mimeType?: string;
107
+ kind: TelegramAttachmentKind;
96
108
  isImage: boolean;
97
109
  }
98
110
 
@@ -101,6 +113,7 @@ export interface DownloadedTelegramFile {
101
113
  fileName?: string;
102
114
  isImage?: boolean;
103
115
  mimeType?: string;
116
+ kind?: TelegramAttachmentKind;
104
117
  }
105
118
 
106
119
  export interface DownloadedTelegramMessageFile {
@@ -108,6 +121,7 @@ export interface DownloadedTelegramMessageFile {
108
121
  fileName: string;
109
122
  isImage: boolean;
110
123
  mimeType?: string;
124
+ kind?: TelegramAttachmentKind;
111
125
  }
112
126
 
113
127
  export interface DownloadTelegramMessageFilesDeps {
@@ -281,17 +295,39 @@ export function createTelegramMediaGroupDispatchRuntime<
281
295
  };
282
296
  }
283
297
 
298
+ function appendTelegramListSection(
299
+ text: string,
300
+ title: string,
301
+ items: string[],
302
+ ): string {
303
+ if (items.length === 0) return text;
304
+ const prefix = text.length > 0 ? `${text}\n\n` : "";
305
+ return `${prefix}[${title}]\n${items.map((item) => `- ${item}`).join("\n")}`;
306
+ }
307
+
308
+ function appendTelegramAttachmentSection(
309
+ text: string,
310
+ files: Pick<DownloadedTelegramFile, "path">[],
311
+ ): string {
312
+ if (files.length === 0) return text;
313
+ const dirs = [...new Set(files.map((file) => dirname(file.path)))];
314
+ const sameDir = dirs.length === 1;
315
+ const header = sameDir ? `[attachments] ${dirs[0]}` : "[attachments]";
316
+ const items = sameDir
317
+ ? files.map((file) => `/${basename(file.path)}`)
318
+ : files.map((file) => file.path);
319
+ const prefix = text.length > 0 ? `${text}\n\n` : "";
320
+ return `${prefix}${header}\n${items.map((item) => `- ${item}`).join("\n")}`;
321
+ }
322
+
284
323
  export function formatTelegramHistoryText(
285
324
  rawText: string,
286
325
  files: DownloadedTelegramFile[],
326
+ handlerOutputs: string[] = [],
287
327
  ): string {
288
328
  let summary = rawText.length > 0 ? rawText : "(no text)";
289
- if (files.length > 0) {
290
- summary += `\nAttachments:`;
291
- for (const file of files) {
292
- summary += `\n- ${file.path}`;
293
- }
294
- }
329
+ summary = appendTelegramAttachmentSection(summary, files);
330
+ summary = appendTelegramListSection(summary, "outputs", handlerOutputs);
295
331
  return summary;
296
332
  }
297
333
 
@@ -306,6 +342,7 @@ export async function downloadTelegramMessageFiles(
306
342
  fileName: file.fileName,
307
343
  isImage: file.isImage,
308
344
  mimeType: file.mimeType,
345
+ kind: file.kind,
309
346
  });
310
347
  }
311
348
  return downloaded;
@@ -325,6 +362,7 @@ export function collectTelegramFileInfos(
325
362
  file_id: photo.file_id,
326
363
  fileName: `photo-${message.message_id}.jpg`,
327
364
  mimeType: "image/jpeg",
365
+ kind: "photo",
328
366
  isImage: true,
329
367
  });
330
368
  }
@@ -340,6 +378,7 @@ export function collectTelegramFileInfos(
340
378
  file_id: message.document.file_id,
341
379
  fileName,
342
380
  mimeType: message.document.mime_type,
381
+ kind: "document",
343
382
  isImage: isImageMimeType(message.document.mime_type),
344
383
  });
345
384
  }
@@ -354,6 +393,7 @@ export function collectTelegramFileInfos(
354
393
  file_id: message.video.file_id,
355
394
  fileName,
356
395
  mimeType: message.video.mime_type,
396
+ kind: "video",
357
397
  isImage: false,
358
398
  });
359
399
  }
@@ -368,6 +408,7 @@ export function collectTelegramFileInfos(
368
408
  file_id: message.audio.file_id,
369
409
  fileName,
370
410
  mimeType: message.audio.mime_type,
411
+ kind: "audio",
371
412
  isImage: false,
372
413
  });
373
414
  }
@@ -379,6 +420,7 @@ export function collectTelegramFileInfos(
379
420
  ".ogg",
380
421
  )}`,
381
422
  mimeType: message.voice.mime_type,
423
+ kind: "voice",
382
424
  isImage: false,
383
425
  });
384
426
  }
@@ -393,6 +435,7 @@ export function collectTelegramFileInfos(
393
435
  file_id: message.animation.file_id,
394
436
  fileName,
395
437
  mimeType: message.animation.mime_type,
438
+ kind: "animation",
396
439
  isImage: false,
397
440
  });
398
441
  }
@@ -401,6 +444,7 @@ export function collectTelegramFileInfos(
401
444
  file_id: message.sticker.file_id,
402
445
  fileName: `sticker-${message.message_id}.webp`,
403
446
  mimeType: "image/webp",
447
+ kind: "sticker",
404
448
  isImage: true,
405
449
  });
406
450
  }
package/lib/menu.ts CHANGED
@@ -807,10 +807,6 @@ export function buildTelegramModelCallbackPlan<
807
807
  export async function openTelegramStatusMenu<
808
808
  TModel extends MenuModel = MenuModel,
809
809
  >(deps: TelegramStatusMenuOpenDeps<TModel>): Promise<void> {
810
- if (!deps.isIdle()) {
811
- await deps.sendBusyMessage();
812
- return;
813
- }
814
810
  const state = await deps.getModelMenuState();
815
811
  const messageId = await deps.sendStatusMenu(
816
812
  state,
package/lib/pi.ts CHANGED
@@ -33,6 +33,7 @@ export interface PiSettingsManager {
33
33
 
34
34
  export interface PiExtensionApiRuntimePorts {
35
35
  sendUserMessage: ExtensionAPI["sendUserMessage"];
36
+ exec: ExtensionAPI["exec"];
36
37
  getThinkingLevel: ExtensionAPI["getThinkingLevel"];
37
38
  setThinkingLevel: ExtensionAPI["setThinkingLevel"];
38
39
  setModel: ExtensionAPI["setModel"];
@@ -41,11 +42,16 @@ export interface PiExtensionApiRuntimePorts {
41
42
  export function createExtensionApiRuntimePorts(
42
43
  api: Pick<
43
44
  ExtensionAPI,
44
- "sendUserMessage" | "getThinkingLevel" | "setThinkingLevel" | "setModel"
45
+ | "sendUserMessage"
46
+ | "exec"
47
+ | "getThinkingLevel"
48
+ | "setThinkingLevel"
49
+ | "setModel"
45
50
  >,
46
51
  ): PiExtensionApiRuntimePorts {
47
52
  return {
48
53
  sendUserMessage: (content) => api.sendUserMessage(content),
54
+ exec: (command, args, options) => api.exec(command, args, options),
49
55
  getThinkingLevel: () => api.getThinkingLevel(),
50
56
  setThinkingLevel: (level) => api.setThinkingLevel(level),
51
57
  setModel: (model) => api.setModel(model),
@@ -62,6 +68,10 @@ export function getExtensionContextModel(
62
68
  return ctx.model;
63
69
  }
64
70
 
71
+ export function getExtensionContextCwd(ctx: ExtensionContext): string {
72
+ return ctx.cwd;
73
+ }
74
+
65
75
  export function isExtensionContextIdle(ctx: ExtensionContext): boolean {
66
76
  return ctx.isIdle();
67
77
  }
package/lib/prompts.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Telegram prompt injection helpers
3
+ * Owns Telegram-specific system prompt suffixes injected into pi agent turns
4
+ */
5
+
6
+ import type { BeforeAgentStartEvent } from "./pi.ts";
7
+ import { TELEGRAM_PREFIX } from "./turns.ts";
8
+
9
+ const SYSTEM_PROMPT_SUFFIX = `
10
+
11
+ Telegram bridge extension is active.
12
+ - Messages forwarded from Telegram are prefixed with "[telegram]".
13
+ - [telegram] messages may include [attachments] sections with a base directory plus relative local file entries. Resolve and read those files as needed.
14
+ - Telegram is often read on narrow phone screens, so prefer narrow table columns when presenting tabular data; wide monospace tables can become unreadable.
15
+ - If a [telegram] user asked for a file or generated artifact, use telegram_attach with the local path instead of only mentioning the path in text.
16
+ - Do not assume mentioning a local file path in plain text will send it to Telegram. Use telegram_attach.`;
17
+
18
+ export function buildTelegramBridgeSystemPrompt(options: {
19
+ prompt: string;
20
+ systemPrompt: string;
21
+ telegramPrefix?: string;
22
+ systemPromptSuffix: string;
23
+ }): { systemPrompt: string } {
24
+ const telegramPrefix = options.telegramPrefix ?? TELEGRAM_PREFIX;
25
+ const suffix = options.prompt.trimStart().startsWith(telegramPrefix)
26
+ ? `${options.systemPromptSuffix}\n- The current user message came from Telegram.`
27
+ : options.systemPromptSuffix;
28
+ return { systemPrompt: options.systemPrompt + suffix };
29
+ }
30
+
31
+ export function createTelegramBeforeAgentStartHook(
32
+ options: {
33
+ telegramPrefix?: string;
34
+ systemPromptSuffix?: string;
35
+ } = {},
36
+ ): (event: BeforeAgentStartEvent) => { systemPrompt: string } {
37
+ return (event) =>
38
+ buildTelegramBridgeSystemPrompt({
39
+ prompt: event.prompt,
40
+ systemPrompt: event.systemPrompt,
41
+ telegramPrefix: options.telegramPrefix,
42
+ systemPromptSuffix: options.systemPromptSuffix ?? SYSTEM_PROMPT_SUFFIX,
43
+ });
44
+ }
package/lib/queue.ts CHANGED
@@ -856,8 +856,9 @@ export function createTelegramAgentEndHook<
856
856
  preserveQueuedTurnsAsHistory: deps.getPreserveQueuedTurnsAsHistory(),
857
857
  resetRuntimeState: deps.resetRuntimeState,
858
858
  updateStatus: () => deps.updateStatus(ctx),
859
- dispatchNextQueuedTelegramTurn: () =>
860
- deps.dispatchNextQueuedTelegramTurn(ctx),
859
+ dispatchNextQueuedTelegramTurn: () => {
860
+ setTimeout(() => deps.dispatchNextQueuedTelegramTurn(ctx), 0);
861
+ },
861
862
  clearPreview: deps.clearPreview,
862
863
  setPreviewPendingText: deps.setPreviewPendingText,
863
864
  finalizeMarkdownPreview: deps.finalizeMarkdownPreview,
@@ -1100,6 +1101,7 @@ export interface TelegramPromptEnqueueControllerDeps<
1100
1101
  createTurn: (
1101
1102
  messages: TMessage[],
1102
1103
  historyTurns: PendingTelegramTurn[],
1104
+ ctx: TContext,
1103
1105
  ) => Promise<PendingTelegramTurn>;
1104
1106
  updateStatus: (ctx: TContext) => void;
1105
1107
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
@@ -1365,6 +1367,8 @@ export function createTelegramPromptEnqueueController<
1365
1367
  enqueue: (messages, ctx) =>
1366
1368
  enqueueTelegramPromptTurnRuntime(messages, {
1367
1369
  ...deps,
1370
+ createTurn: (nextMessages, historyTurns) =>
1371
+ deps.createTurn(nextMessages, historyTurns, ctx),
1368
1372
  updateStatus: () => deps.updateStatus(ctx),
1369
1373
  dispatchNextQueuedTelegramTurn: () =>
1370
1374
  deps.dispatchNextQueuedTelegramTurn(ctx),
@@ -1397,8 +1401,9 @@ export interface TelegramRuntimeEventRecorderPort {
1397
1401
  ) => void;
1398
1402
  }
1399
1403
 
1400
- export interface TelegramControlRuntimeDeps<TContext>
1401
- extends TelegramRuntimeEventRecorderPort {
1404
+ export interface TelegramControlRuntimeDeps<
1405
+ TContext,
1406
+ > extends TelegramRuntimeEventRecorderPort {
1402
1407
  ctx: TContext;
1403
1408
  sendTextReply: (
1404
1409
  chatId: number,
@@ -1451,8 +1456,9 @@ export interface TelegramDispatchRuntimeDeps<TContext = unknown> {
1451
1456
  onIdle: () => void;
1452
1457
  }
1453
1458
 
1454
- export interface TelegramQueueDispatchControllerDeps<TContext = unknown>
1455
- extends TelegramRuntimeEventRecorderPort {
1459
+ export interface TelegramQueueDispatchControllerDeps<
1460
+ TContext = unknown,
1461
+ > extends TelegramRuntimeEventRecorderPort {
1456
1462
  getQueuedItems: () => TelegramQueueItem<TContext>[];
1457
1463
  setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
1458
1464
  canDispatch: (ctx: TContext) => boolean;