pi-telegram-plus 0.0.2 → 0.0.3
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/README.md +100 -232
- package/index.ts +17 -5
- package/lib/attachments.ts +7 -3
- package/lib/commands/session.ts +4 -1
- package/lib/commands/telegram-commands.ts +7 -4
- package/lib/config.ts +6 -3
- package/lib/controller.ts +53 -14
- package/lib/custom-dialogs.ts +668 -0
- package/lib/heartbeat.ts +5 -1
- package/lib/logger.ts +304 -0
- package/lib/markdown.ts +267 -61
- package/lib/menu-commands.ts +4 -1
- package/lib/polling.ts +13 -8
- package/lib/renderer.ts +141 -14
- package/lib/telegram-api.ts +83 -28
- package/lib/telegram-ui.ts +89 -10
- package/lib/text-split.ts +216 -1
- package/package.json +1 -1
package/lib/controller.ts
CHANGED
|
@@ -12,6 +12,9 @@ import type {
|
|
|
12
12
|
TelegramTurn,
|
|
13
13
|
} from "./types.ts";
|
|
14
14
|
import type { TelegramUiRuntime } from "./telegram-ui.ts";
|
|
15
|
+
import { log } from "./logger.ts";
|
|
16
|
+
|
|
17
|
+
const ctrlLog = log.child("controller");
|
|
15
18
|
|
|
16
19
|
export type TelegramController = {
|
|
17
20
|
handleMessage(message: TelegramMessage): Promise<void>;
|
|
@@ -221,7 +224,7 @@ export function createTelegramController(deps: {
|
|
|
221
224
|
const mode = deps.getMessageMode();
|
|
222
225
|
if (mode === "steer") {
|
|
223
226
|
const task = runPrompt(text, chatId, replaceMessageId);
|
|
224
|
-
void task.catch((
|
|
227
|
+
void task.catch(ctrlLog.swallow("error", "steer-mode prompt task failed", { chatId }));
|
|
225
228
|
return;
|
|
226
229
|
}
|
|
227
230
|
|
|
@@ -233,7 +236,7 @@ export function createTelegramController(deps: {
|
|
|
233
236
|
if (generation !== getInterruptGeneration(chatId)) return;
|
|
234
237
|
return runPrompt(text, chatId, replaceMessageId);
|
|
235
238
|
})
|
|
236
|
-
.catch((
|
|
239
|
+
.catch(ctrlLog.swallow("error", "queue-mode prompt task failed", { chatId }));
|
|
237
240
|
setTail(chatId, task);
|
|
238
241
|
};
|
|
239
242
|
|
|
@@ -247,11 +250,45 @@ export function createTelegramController(deps: {
|
|
|
247
250
|
// prompts (e.g. /new confirmation), and awaiting them would stall the
|
|
248
251
|
// polling loop and block callback processing for that update.
|
|
249
252
|
const telegramUi = deps.ui.create(chatId);
|
|
253
|
+
const ctx = session.extensionRunner.createCommandContext();
|
|
254
|
+
// Capture idleness BEFORE the handler runs. Commands like /sisyphus and
|
|
255
|
+
// /goals enqueue the agent turn fire-and-forget via pi.sendUserMessage and
|
|
256
|
+
// return immediately; the actual turn (and any goal_question /
|
|
257
|
+
// propose_goal_draft / goal_questionnaire dialogs it raises) runs AFTER the
|
|
258
|
+
// handler resolves. If we let runWithTelegramUi restore the TUI UI at that
|
|
259
|
+
// point, those dialogs render to the local TUI and Telegram gets nothing.
|
|
260
|
+
// Only hold the swap when the agent was idle when the command arrived: if a
|
|
261
|
+
// local turn was already streaming, holding the swap would hijack the local
|
|
262
|
+
// user's TUI (modals would route to Telegram, editor would no-op).
|
|
263
|
+
const idleFn = typeof (ctx as any).isIdle === "function" ? (ctx as any).isIdle : undefined;
|
|
264
|
+
const waitFn = typeof (ctx as any).waitForIdle === "function" ? (ctx as any).waitForIdle : undefined;
|
|
265
|
+
const wasIdle = idleFn ? idleFn.call(ctx) : false;
|
|
250
266
|
void runWithTelegramUi({
|
|
251
267
|
session,
|
|
252
268
|
ui: telegramUi,
|
|
253
|
-
run: async () =>
|
|
254
|
-
|
|
269
|
+
run: async () => {
|
|
270
|
+
await handler(args, ctx);
|
|
271
|
+
if (!wasIdle || !idleFn || !waitFn) return;
|
|
272
|
+
// Hold the Telegram UI swap across the command's enqueued turn and any
|
|
273
|
+
// auto-continue chain it spawns. pi-goal schedules continuation turns via
|
|
274
|
+
// setTimeout(0 / 50ms) after a turn ends, so a single waitForIdle()
|
|
275
|
+
// resolves before the next turn starts. After each idle, yield one
|
|
276
|
+
// macrotask window (120ms > pi-goal's 50ms CONTINUATION_IDLE_RETRY_MS) to
|
|
277
|
+
// let a pending continuation begin; if the agent starts streaming again,
|
|
278
|
+
// keep waiting. Stop when the agent stays idle through the window — the
|
|
279
|
+
// chain has drained and the local TUI is fully usable again.
|
|
280
|
+
try {
|
|
281
|
+
for (;;) {
|
|
282
|
+
await waitFn.call(ctx);
|
|
283
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
284
|
+
if (idleFn.call(ctx)) break;
|
|
285
|
+
}
|
|
286
|
+
} catch (err) {
|
|
287
|
+
// Session disposed / torn down during the wait — nothing to do but log.
|
|
288
|
+
ctrlLog.debug("waitForIdle during enqueued turn interrupted", { err });
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
}).catch(ctrlLog.swallow("warn", "beginTelegramTurn enqueued-turn wrapper failed"));
|
|
255
292
|
};
|
|
256
293
|
|
|
257
294
|
const tryHandleSlashCommand = async (text: string, chatId: number): Promise<boolean> => {
|
|
@@ -287,7 +324,7 @@ export function createTelegramController(deps: {
|
|
|
287
324
|
async handleCallbackQuery(query) {
|
|
288
325
|
const chatId = query.message?.chat?.id;
|
|
289
326
|
if (typeof chatId !== "number") return;
|
|
290
|
-
await deps.transport.answerCallbackQuery(query.id).catch((
|
|
327
|
+
await deps.transport.answerCallbackQuery(query.id).catch(ctrlLog.swallow("debug", "answerCallbackQuery failed", { queryId: query.id }));
|
|
291
328
|
if (!(await deps.authorizeUser(query.from?.id))) return;
|
|
292
329
|
await deps.setActiveChatId(chatId);
|
|
293
330
|
|
|
@@ -297,15 +334,17 @@ export function createTelegramController(deps: {
|
|
|
297
334
|
const result = deps.ui.resolveInput(chatId, uiValue, query.message?.message_id, true);
|
|
298
335
|
if (!result.handled) {
|
|
299
336
|
await deps.transport.sendText(chatId, "This prompt is no longer active.");
|
|
300
|
-
} else {
|
|
301
|
-
// Remove inline keyboards only for terminal UI callbacks. Select
|
|
302
|
-
// pagination callbacks continue the same flow and immediately edit the
|
|
303
|
-
// same message with the next page of buttons; removing the keyboard here
|
|
304
|
-
// can race after that edit and strip the new page buttons.
|
|
305
|
-
const isPaginationCallback = /^f:[^:]+:p:\d+$/.test(uiValue);
|
|
306
|
-
const promptMessageId = result.promptMessageId ?? query.message?.message_id;
|
|
307
|
-
if (!isPaginationCallback && promptMessageId) void deps.transport.removeInlineKeyboard(chatId, promptMessageId);
|
|
308
337
|
}
|
|
338
|
+
// Keyboard cleanup is OWNED by each UI flow (confirm/input/select and the
|
|
339
|
+
// custom-dialog bridge): each calls removeInlineKeyboard on its own prompt
|
|
340
|
+
// message right before resolving a terminal value (yes/no/cancel/submit/...).
|
|
341
|
+
// The controller must NOT speculatively strip keyboards here. Doing so races
|
|
342
|
+
// with continuation flows (multi-question tab navigation, single-question
|
|
343
|
+
// option toggles, select pagination) that edit the SAME message in place:
|
|
344
|
+
// removeInlineKeyboard and editButtons hit Telegram concurrently on one
|
|
345
|
+
// message, and when the strip lands last the message is left with no
|
|
346
|
+
// buttons — the "answered Q1 but Q2 never appeared" bug. Terminal flows
|
|
347
|
+
// clean up themselves, so no controller-side strip is needed.
|
|
309
348
|
return;
|
|
310
349
|
}
|
|
311
350
|
|
|
@@ -368,7 +407,7 @@ export function createTelegramController(deps: {
|
|
|
368
407
|
formatIncomingAttachmentReceipt("saved", downloadedMediaLines),
|
|
369
408
|
formatIncomingAttachmentReceipt("failed", failedMediaLines),
|
|
370
409
|
].filter(Boolean);
|
|
371
|
-
await deps.transport.sendText(chatId, lines.join("\n\n")).catch((
|
|
410
|
+
await deps.transport.sendText(chatId, lines.join("\n\n")).catch(ctrlLog.swallow("warn", "sendText media receipt failed", { chatId }));
|
|
372
411
|
}
|
|
373
412
|
}
|
|
374
413
|
const hasPromptInput = hasTextInput || hasMediaInput;
|