@markusylisiurunen/tau 0.3.2 → 0.3.4

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.
Files changed (43) hide show
  1. package/README.md +13 -55
  2. package/dist/core/cli.js +2 -2
  3. package/dist/core/cli.js.map +1 -1
  4. package/dist/core/config/index.js.map +1 -1
  5. package/dist/core/config/schema.js +0 -159
  6. package/dist/core/config/schema.js.map +1 -1
  7. package/dist/core/index.js +1 -1
  8. package/dist/core/index.js.map +1 -1
  9. package/dist/core/{async/telegram.js → telegram/adapter.js} +256 -329
  10. package/dist/core/telegram/adapter.js.map +1 -0
  11. package/dist/core/telegram/cli.js +148 -0
  12. package/dist/core/telegram/cli.js.map +1 -0
  13. package/dist/core/telegram/config.js +241 -0
  14. package/dist/core/telegram/config.js.map +1 -0
  15. package/dist/core/telegram/index.js +4 -0
  16. package/dist/core/telegram/index.js.map +1 -0
  17. package/dist/core/telegram/runtime.js +96 -0
  18. package/dist/core/telegram/runtime.js.map +1 -0
  19. package/dist/core/{async → telegram}/session_manager.js +22 -22
  20. package/dist/core/telegram/session_manager.js.map +1 -0
  21. package/dist/core/telegram/workspace.js.map +1 -0
  22. package/dist/core/version.js +1 -1
  23. package/dist/main.js +7 -7
  24. package/dist/main.js.map +1 -1
  25. package/package.json +1 -1
  26. package/dist/core/async/cli.js +0 -557
  27. package/dist/core/async/cli.js.map +0 -1
  28. package/dist/core/async/cron.js +0 -370
  29. package/dist/core/async/cron.js.map +0 -1
  30. package/dist/core/async/daemon_runtime.js +0 -120
  31. package/dist/core/async/daemon_runtime.js.map +0 -1
  32. package/dist/core/async/http_protocol.js +0 -177
  33. package/dist/core/async/http_protocol.js.map +0 -1
  34. package/dist/core/async/http_server.js +0 -278
  35. package/dist/core/async/http_server.js.map +0 -1
  36. package/dist/core/async/index.js +0 -10
  37. package/dist/core/async/index.js.map +0 -1
  38. package/dist/core/async/server_config.js +0 -420
  39. package/dist/core/async/server_config.js.map +0 -1
  40. package/dist/core/async/session_manager.js.map +0 -1
  41. package/dist/core/async/telegram.js.map +0 -1
  42. package/dist/core/async/workspace.js.map +0 -1
  43. /package/dist/core/{async → telegram}/workspace.js +0 -0
@@ -4,7 +4,7 @@ import { basename, extname, join } from "node:path";
4
4
  import { z } from "zod";
5
5
  import { transcribeAudio } from "../utils/speech_to_text.js";
6
6
  import { formatZodError } from "../utils/zod.js";
7
- import { AsyncSessionManagerError, createScopedAsyncSessionManager, } from "./session_manager.js";
7
+ import { createScopedTelegramSessionManager, TelegramSessionManagerError, } from "./session_manager.js";
8
8
  const DEFAULT_POLL_INTERVAL_MS = 1000;
9
9
  const DEFAULT_REQUEST_TIMEOUT_SECONDS = 30;
10
10
  const MAX_COMMAND_PREVIEW_CHARS = 128;
@@ -17,19 +17,24 @@ const DEFAULT_TELEGRAM_DOCUMENT_MIME_TYPE = "application/octet-stream";
17
17
  const MESSAGE_QUEUED_REACTION_EMOJI = "👀";
18
18
  const MESSAGE_QUEUED_REACTION_DELAY_MS = 1000;
19
19
  const TELEGRAM_MAX_MESSAGE_BYTES = 4096;
20
+ const TELEGRAM_MAX_RICH_MESSAGE_BYTES = 32 * 1024;
20
21
  const TELEGRAM_MESSAGE_BYTE_BUFFER_RATIO = 0.95;
21
22
  const TELEGRAM_SAFE_MESSAGE_BYTES = Math.floor(TELEGRAM_MAX_MESSAGE_BYTES * TELEGRAM_MESSAGE_BYTE_BUFFER_RATIO);
23
+ const TELEGRAM_RICH_MESSAGE_SAFE_BYTES = Math.floor(TELEGRAM_MAX_RICH_MESSAGE_BYTES * TELEGRAM_MESSAGE_BYTE_BUFFER_RATIO);
22
24
  const TELEGRAM_MESSAGE_SPLIT_DELAY_MS = 1000;
25
+ const TELEGRAM_DRAFT_MAX_CHARS = 4096;
26
+ const TELEGRAM_DRAFT_THINKING_MARKDOWN = "<tg-thinking>Thinking...</tg-thinking>";
27
+ const TELEGRAM_DRAFT_PREAMBLE_VISIBLE_MS = 10_000;
28
+ const TELEGRAM_DRAFT_REFRESH_MS = 20_000;
29
+ const TELEGRAM_TYPING_REFRESH_MS = 4000;
23
30
  const ABORTED = Symbol("aborted");
24
31
  const CALLBACK_ACTION_PREFIX = "tau:action:";
25
- const CALLBACK_USE_PREFIX = "tau:use:";
26
- const MAX_SESSION_PREVIEW_CHARS = 64;
27
32
  const MAX_TELEGRAM_ATTACHMENTS_PER_TURN = 10;
28
33
  const MAX_TELEGRAM_ATTACHMENT_FILE_BYTES = 20 * 1024 * 1024;
29
34
  const MAX_TELEGRAM_ATTACHMENT_TOTAL_BYTES = 50 * 1024 * 1024;
30
35
  const MAX_TELEGRAM_GROUP_PENDING_MESSAGES = 50;
31
36
  const TELEGRAM_ATTACHMENT_TEMP_DIR_PREFIX = "tau-telegram-attachments-";
32
- const NO_ACTIVE_SESSION_MESSAGE = "no active session. use /new or /sessions";
37
+ const NO_ACTIVE_SESSION_MESSAGE = "no active session. use /new";
33
38
  const SUPPORTED_TEXT_ATTACHMENT_EXTENSIONS = new Set([
34
39
  ".txt",
35
40
  ".md",
@@ -98,11 +103,6 @@ async function sweepStaleTelegramAttachmentTempDirs() {
98
103
  return;
99
104
  }
100
105
  }
101
- const QUICK_ACTION_ROWS = [
102
- ["new", "sessions", "status"],
103
- ["interrupt", "close"],
104
- ["quiet", "verbose"],
105
- ];
106
106
  function splitCommandText(text) {
107
107
  return text
108
108
  .trim()
@@ -196,15 +196,15 @@ function truncateText(text, maxChars) {
196
196
  }
197
197
  return `${trimmed.slice(0, maxChars - 1)}…`;
198
198
  }
199
- function splitTelegramMessage(text) {
200
- if (Buffer.byteLength(text, "utf8") <= TELEGRAM_SAFE_MESSAGE_BYTES) {
199
+ function splitTelegramTextByBytes(text, maxBytes) {
200
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) {
201
201
  return [text];
202
202
  }
203
203
  const chunks = [];
204
204
  let currentChunk = "";
205
205
  for (const character of text) {
206
206
  const nextChunk = `${currentChunk}${character}`;
207
- if (Buffer.byteLength(nextChunk, "utf8") <= TELEGRAM_SAFE_MESSAGE_BYTES) {
207
+ if (Buffer.byteLength(nextChunk, "utf8") <= maxBytes) {
208
208
  currentChunk = nextChunk;
209
209
  continue;
210
210
  }
@@ -220,6 +220,12 @@ function splitTelegramMessage(text) {
220
220
  }
221
221
  return chunks;
222
222
  }
223
+ function splitTelegramMessage(text) {
224
+ return splitTelegramTextByBytes(text, TELEGRAM_SAFE_MESSAGE_BYTES);
225
+ }
226
+ function splitTelegramRichMessage(text) {
227
+ return splitTelegramTextByBytes(text, TELEGRAM_RICH_MESSAGE_SAFE_BYTES);
228
+ }
223
229
  function normalizeSizeBytes(value) {
224
230
  if (typeof value !== "number" || !Number.isFinite(value)) {
225
231
  return undefined;
@@ -478,6 +484,30 @@ function createTelegramApi(botToken) {
478
484
  ...(options?.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
479
485
  }, z.unknown());
480
486
  },
487
+ async sendRichMessage(chatId, markdown, options) {
488
+ await callTelegramMethod("sendRichMessage", {
489
+ chat_id: chatId,
490
+ rich_message: {
491
+ markdown,
492
+ },
493
+ ...(options?.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
494
+ }, z.unknown());
495
+ },
496
+ async sendRichMessageDraft(chatId, draftId, markdown) {
497
+ await callTelegramMethod("sendRichMessageDraft", {
498
+ chat_id: chatId,
499
+ draft_id: draftId,
500
+ rich_message: {
501
+ markdown,
502
+ },
503
+ }, TelegramAckResultSchema);
504
+ },
505
+ async sendChatAction(chatId, action) {
506
+ await callTelegramMethod("sendChatAction", {
507
+ chat_id: chatId,
508
+ action,
509
+ }, TelegramAckResultSchema);
510
+ },
481
511
  async downloadFile(fileId) {
482
512
  const parsed = await callTelegramMethod("getFile", {
483
513
  file_id: fileId,
@@ -514,7 +544,7 @@ function createTelegramApi(botToken) {
514
544
  },
515
545
  };
516
546
  }
517
- class AsyncTelegramAdapterImpl {
547
+ class TelegramAdapterImpl {
518
548
  projects;
519
549
  defaultProjectId;
520
550
  systemMessage;
@@ -540,7 +570,9 @@ class AsyncTelegramAdapterImpl {
540
570
  activeSessionsByChat = new Map();
541
571
  sessionsByChat = new Map();
542
572
  chatsBySession = new Map();
543
- sessionVerbosityBySession = new Map();
573
+ chatTypesByChat = new Map();
574
+ typingIntervalsBySessionChat = new Map();
575
+ draftStatesBySessionChat = new Map();
544
576
  lastCommandBySession = new Map();
545
577
  lastAssistantMessageBySession = new Map();
546
578
  latestAssistantMessageByRun = new Map();
@@ -612,6 +644,10 @@ class AsyncTelegramAdapterImpl {
612
644
  try {
613
645
  await this.loopPromise;
614
646
  await this.waitForInFlightUpdateTasks();
647
+ for (const sessionId of Array.from(this.chatsBySession.keys())) {
648
+ this.stopTypingIndicators(sessionId);
649
+ this.clearSessionDrafts(sessionId);
650
+ }
615
651
  for (const sessionId of Array.from(this.attachmentTempDirsBySession.keys())) {
616
652
  await this.cleanupSessionAttachmentTempDirs(sessionId);
617
653
  }
@@ -627,38 +663,13 @@ class AsyncTelegramAdapterImpl {
627
663
  }
628
664
  createCommandDefinitions() {
629
665
  return [
630
- {
631
- command: "/help",
632
- description: "show supported commands",
633
- usage: "/help",
634
- handler: async (chatId) => this.handleHelp(chatId),
635
- },
636
666
  {
637
667
  command: "/new",
638
668
  description: "start a new session",
639
- usage: "/new [projectId]",
669
+ usage: "/new",
640
670
  callbackAction: "new",
641
671
  handler: async (chatId, args, sourceMessageId) => this.handleNew(chatId, args, sourceMessageId),
642
672
  },
643
- {
644
- command: "/projects",
645
- description: "list configured projects",
646
- usage: "/projects",
647
- handler: async (chatId) => this.handleProjects(chatId),
648
- },
649
- {
650
- command: "/use",
651
- description: "switch active session",
652
- usage: "/use <sessionId|prefix|index>",
653
- handler: async (chatId, args) => this.handleUse(chatId, args),
654
- },
655
- {
656
- command: "/sessions",
657
- description: "list sessions",
658
- usage: "/sessions",
659
- callbackAction: "sessions",
660
- handler: async (chatId) => this.handleSessions(chatId),
661
- },
662
673
  {
663
674
  command: "/status",
664
675
  description: "show active session status",
@@ -673,33 +684,9 @@ class AsyncTelegramAdapterImpl {
673
684
  callbackAction: "interrupt",
674
685
  handler: async (chatId) => this.handleInterrupt(chatId),
675
686
  },
676
- {
677
- command: "/close",
678
- description: "close session(s)",
679
- usage: "/close [<sessionId>|all]",
680
- callbackAction: "close",
681
- handler: async (chatId, args) => this.handleClose(chatId, args),
682
- },
683
- {
684
- command: "/verbose",
685
- description: "stream progress updates",
686
- usage: "/verbose",
687
- callbackAction: "verbose",
688
- handler: async (chatId) => this.handleVerbosityCommand(chatId, "verbose"),
689
- },
690
- {
691
- command: "/quiet",
692
- description: "only send final assistant message",
693
- usage: "/quiet",
694
- callbackAction: "quiet",
695
- handler: async (chatId) => this.handleVerbosityCommand(chatId, "quiet"),
696
- },
697
687
  ];
698
688
  }
699
689
  async syncCommands() {
700
- if (!this.api.setCommands) {
701
- return;
702
- }
703
690
  const commands = this.commandDefinitions.map((definition) => ({
704
691
  command: definition.command.slice(1),
705
692
  description: definition.description,
@@ -837,6 +824,7 @@ class AsyncTelegramAdapterImpl {
837
824
  if (!chat) {
838
825
  return;
839
826
  }
827
+ this.chatTypesByChat.set(chat.id, chat.type);
840
828
  if (chat.type === "private") {
841
829
  await this.handlePrivateMessage(chat.id, message);
842
830
  return;
@@ -1277,7 +1265,7 @@ class AsyncTelegramAdapterImpl {
1277
1265
  if (!this.enforceChatOwnership) {
1278
1266
  return this.sessionManager;
1279
1267
  }
1280
- return createScopedAsyncSessionManager({
1268
+ return createScopedTelegramSessionManager({
1281
1269
  sessionManager: this.sessionManager,
1282
1270
  ownerId: this.ownerIdForChat(chatId),
1283
1271
  allowedProjectIds: this.allowedProjectIds,
@@ -1289,20 +1277,12 @@ class AsyncTelegramAdapterImpl {
1289
1277
  const args = parts.slice(1);
1290
1278
  const handler = this.commandHandlers.get(command);
1291
1279
  if (!handler) {
1292
- await this.reply(chatId, "unsupported command. use /help");
1280
+ await this.reply(chatId, "unsupported command. supported commands: /new, /status, /interrupt");
1293
1281
  return;
1294
1282
  }
1295
1283
  await handler(chatId, args, sourceMessageId);
1296
1284
  }
1297
1285
  async handleCallback(chatId, callbackData) {
1298
- if (callbackData.startsWith(CALLBACK_USE_PREFIX)) {
1299
- const sessionId = callbackData.slice(CALLBACK_USE_PREFIX.length).trim();
1300
- if (!sessionId) {
1301
- return false;
1302
- }
1303
- await this.handleUse(chatId, [sessionId]);
1304
- return true;
1305
- }
1306
1286
  if (!callbackData.startsWith(CALLBACK_ACTION_PREFIX)) {
1307
1287
  return false;
1308
1288
  }
@@ -1314,77 +1294,29 @@ class AsyncTelegramAdapterImpl {
1314
1294
  await handler(chatId, []);
1315
1295
  return true;
1316
1296
  }
1317
- async handleHelp(chatId) {
1318
- const lines = [
1319
- "commands:",
1320
- ...this.commandDefinitions.map((definition) => definition.usage),
1321
- "",
1322
- "tip: use /sessions and tap a session button to switch quickly",
1323
- ];
1324
- await this.reply(chatId, lines.join("\n"), {
1325
- replyMarkup: this.buildQuickActionsKeyboard(),
1326
- });
1327
- }
1328
1297
  resolveNewCommand(args) {
1329
- const projectIds = Object.keys(this.projects);
1330
- const resolveFallbackProjectId = () => {
1331
- if (this.defaultProjectId) {
1332
- if (!this.projects[this.defaultProjectId]) {
1333
- return {
1334
- error: `telegram.<botId>.defaultProjectId '${this.defaultProjectId}' is not configured`,
1335
- };
1336
- }
1337
- return { projectId: this.defaultProjectId };
1338
- }
1339
- if (projectIds.length === 1) {
1340
- return { projectId: projectIds[0] };
1341
- }
1342
- if (projectIds.length === 0) {
1343
- return { error: "no async projects configured" };
1344
- }
1345
- return {
1346
- error: "missing project id. usage: /new [projectId]. use /projects to list options",
1347
- };
1348
- };
1349
- if (args.length > 1) {
1350
- return { error: "usage: /new [projectId]" };
1298
+ if (args.length > 0) {
1299
+ return { error: "usage: /new" };
1351
1300
  }
1352
- if (args.length === 1) {
1353
- const projectId = args[0];
1354
- if (!projectId) {
1355
- return { error: "usage: /new [projectId]" };
1356
- }
1357
- if (!this.projects[projectId]) {
1301
+ const projectIds = Object.keys(this.projects);
1302
+ if (this.defaultProjectId) {
1303
+ if (!this.projects[this.defaultProjectId]) {
1358
1304
  return {
1359
- error: `unknown project '${projectId}'. usage: /new [projectId]. use /projects to list options`,
1305
+ error: `telegram.<botId>.defaultProjectId '${this.defaultProjectId}' is not configured`,
1360
1306
  };
1361
1307
  }
1362
- return { projectId };
1308
+ return { projectId: this.defaultProjectId };
1363
1309
  }
1364
- const fallback = resolveFallbackProjectId();
1365
- if (!fallback.projectId) {
1366
- return {
1367
- error: fallback.error ?? "unable to resolve project id",
1368
- };
1310
+ if (projectIds.length === 1) {
1311
+ return { projectId: projectIds[0] };
1312
+ }
1313
+ if (projectIds.length === 0) {
1314
+ return { error: "no telegram projects configured" };
1369
1315
  }
1370
1316
  return {
1371
- projectId: fallback.projectId,
1317
+ error: "multiple projects configured. set defaultProjectId for this bot before using /new",
1372
1318
  };
1373
1319
  }
1374
- async handleProjects(chatId) {
1375
- const entries = Object.entries(this.projects).sort(([left], [right]) => left.localeCompare(right));
1376
- if (entries.length === 0) {
1377
- await this.reply(chatId, "no async projects configured");
1378
- return;
1379
- }
1380
- const lines = entries.map(([projectId, project]) => {
1381
- if (!project.description) {
1382
- return projectId;
1383
- }
1384
- return `${projectId}: ${project.description}`;
1385
- });
1386
- await this.reply(chatId, [`projects:`, ...lines].join("\n"));
1387
- }
1388
1320
  async handleNew(chatId, args, sourceMessageId) {
1389
1321
  const parsed = this.resolveNewCommand(args);
1390
1322
  if ("error" in parsed) {
@@ -1393,134 +1325,27 @@ class AsyncTelegramAdapterImpl {
1393
1325
  }
1394
1326
  try {
1395
1327
  const sessionManager = this.getSessionManagerForChat(chatId);
1328
+ const previousSession = this.getActiveSession(chatId);
1329
+ if (previousSession) {
1330
+ await sessionManager.closeSession(previousSession.id);
1331
+ this.clearClosedSession(previousSession.id);
1332
+ }
1396
1333
  const session = await sessionManager.createSession({
1397
1334
  projectId: parsed.projectId,
1398
1335
  });
1399
1336
  this.setActiveSession(chatId, session.id);
1400
- this.sessionVerbosityBySession.set(session.id, "quiet");
1401
1337
  void this.reactToQueuedMessage(chatId, sourceMessageId);
1402
1338
  }
1403
1339
  catch (error) {
1404
1340
  await this.reply(chatId, this.formatManagerError(error));
1405
1341
  }
1406
1342
  }
1407
- async handleUse(chatId, args) {
1408
- const selector = args[0]?.trim();
1409
- if (!selector) {
1410
- await this.reply(chatId, "usage: /use <sessionId|prefix|index>");
1411
- return;
1412
- }
1413
- const sessionManager = this.getSessionManagerForChat(chatId);
1414
- const sessions = sessionManager.listSessions();
1415
- const selectedSession = this.resolveSessionSelector(selector, sessions, sessionManager);
1416
- if ("error" in selectedSession) {
1417
- await this.reply(chatId, selectedSession.error);
1418
- return;
1419
- }
1420
- this.setActiveSession(chatId, selectedSession.session.id);
1421
- await this.reply(chatId, `switched to ${formatTelegramSessionName(selectedSession.session)}.`);
1422
- }
1423
- resolveSessionSelector(selector, sessions, sessionManager) {
1424
- const exactSession = sessionManager.getSession(selector);
1425
- if (exactSession) {
1426
- return { session: exactSession };
1427
- }
1428
- if (/^\d+$/.test(selector)) {
1429
- const index = Number.parseInt(selector, 10);
1430
- if (index <= 0 || index > sessions.length) {
1431
- return {
1432
- error: sessions.length === 0
1433
- ? "no sessions"
1434
- : `session index '${selector}' is out of range (1-${sessions.length})`,
1435
- };
1436
- }
1437
- const indexedSession = sessions[index - 1];
1438
- if (!indexedSession) {
1439
- return { error: "session index lookup failed" };
1440
- }
1441
- return { session: indexedSession };
1442
- }
1443
- const prefixMatches = sessions.filter((session) => session.id.startsWith(selector));
1444
- if (prefixMatches.length === 1) {
1445
- const [prefixMatch] = prefixMatches;
1446
- if (!prefixMatch) {
1447
- return { error: "session prefix lookup failed" };
1448
- }
1449
- return { session: prefixMatch };
1450
- }
1451
- if (prefixMatches.length > 1) {
1452
- return {
1453
- error: `session prefix '${selector}' is ambiguous: ${prefixMatches.map((session) => session.id).join(", ")}`,
1454
- };
1455
- }
1456
- return { error: `session '${selector}' not found` };
1457
- }
1458
- async handleSessions(chatId) {
1459
- const sessionManager = this.getSessionManagerForChat(chatId);
1460
- const sessions = sessionManager.listSessions();
1461
- const lines = this.formatSessions(chatId, sessions);
1462
- await this.reply(chatId, lines.join("\n"), {
1463
- replyMarkup: this.buildSessionsKeyboard(chatId, sessions),
1464
- });
1465
- }
1466
- formatSessions(chatId, sessions) {
1467
- if (sessions.length === 0) {
1468
- return ["no sessions"];
1469
- }
1470
- const activeSessionId = this.activeSessionsByChat.get(chatId);
1471
- return [
1472
- "sessions:",
1473
- ...sessions.map((session, index) => {
1474
- const marker = session.id === activeSessionId ? "*" : "-";
1475
- const preview = this.formatSessionPreview(session.id);
1476
- return `${index + 1}. ${marker} ${session.id} ${session.projectId} ${session.state}${preview ? ` · ${preview}` : ""}`;
1477
- }),
1478
- ];
1479
- }
1480
- formatSessionPreview(sessionId) {
1481
- const previews = [];
1482
- const lastCommand = this.lastCommandBySession.get(sessionId);
1483
- if (lastCommand) {
1484
- previews.push(`$ ${truncateText(lastCommand, MAX_SESSION_PREVIEW_CHARS)}`);
1485
- }
1486
- const lastAssistantMessage = this.lastAssistantMessageBySession.get(sessionId);
1487
- if (lastAssistantMessage) {
1488
- previews.push(truncateText(lastAssistantMessage, MAX_SESSION_PREVIEW_CHARS));
1489
- }
1490
- if (previews.length === 0) {
1491
- return undefined;
1492
- }
1493
- return previews.join(" | ");
1494
- }
1495
- buildSessionsKeyboard(chatId, sessions) {
1496
- const activeSessionId = this.activeSessionsByChat.get(chatId);
1497
- const inlineKeyboard = sessions.map((session, index) => [
1498
- {
1499
- text: `${index + 1}. ${session.id}${session.id === activeSessionId ? " *" : ""}`,
1500
- callback_data: `${CALLBACK_USE_PREFIX}${session.id}`,
1501
- },
1502
- ]);
1503
- inlineKeyboard.push(...this.buildQuickActionsKeyboard().inline_keyboard);
1504
- return {
1505
- inline_keyboard: inlineKeyboard,
1506
- };
1507
- }
1508
- buildQuickActionsKeyboard() {
1509
- return {
1510
- inline_keyboard: QUICK_ACTION_ROWS.map((row) => row.map((action) => ({
1511
- text: `/${action}`,
1512
- callback_data: `${CALLBACK_ACTION_PREFIX}${action}`,
1513
- }))),
1514
- };
1515
- }
1516
1343
  async handleStatus(chatId) {
1517
1344
  const session = await this.requireActiveSession(chatId);
1518
1345
  if (!session) {
1519
1346
  return;
1520
1347
  }
1521
- await this.reply(chatId, formatSessionStatus(session), {
1522
- replyMarkup: this.buildQuickActionsKeyboard(),
1523
- });
1348
+ await this.reply(chatId, formatSessionStatus(session));
1524
1349
  }
1525
1350
  async handleInterrupt(chatId) {
1526
1351
  const session = await this.requireActiveSession(chatId);
@@ -1540,57 +1365,6 @@ class AsyncTelegramAdapterImpl {
1540
1365
  await this.reply(chatId, this.formatManagerError(error));
1541
1366
  }
1542
1367
  }
1543
- async handleClose(chatId, args) {
1544
- if (args.length > 1) {
1545
- await this.reply(chatId, "usage: /close [<sessionId>|all]");
1546
- return;
1547
- }
1548
- const target = args[0]?.trim();
1549
- if (target === "all") {
1550
- try {
1551
- const sessionManager = this.getSessionManagerForChat(chatId);
1552
- const closed = await sessionManager.closeInactiveSessions();
1553
- for (const session of closed) {
1554
- this.clearClosedSession(session.id);
1555
- }
1556
- if (closed.length === 0) {
1557
- await this.reply(chatId, "there are no idle sessions to close.");
1558
- return;
1559
- }
1560
- const label = closed.length === 1 ? "session" : "sessions";
1561
- await this.reply(chatId, `closed ${closed.length} idle ${label}.`);
1562
- }
1563
- catch (error) {
1564
- await this.reply(chatId, this.formatManagerError(error));
1565
- }
1566
- return;
1567
- }
1568
- const sessionId = target ?? this.getActiveSession(chatId)?.id;
1569
- if (!sessionId) {
1570
- await this.reply(chatId, NO_ACTIVE_SESSION_MESSAGE);
1571
- return;
1572
- }
1573
- try {
1574
- const sessionManager = this.getSessionManagerForChat(chatId);
1575
- const closed = await sessionManager.closeSession(sessionId);
1576
- this.clearClosedSession(closed.id);
1577
- await this.reply(chatId, `closed ${formatTelegramSessionName(closed)}.`);
1578
- }
1579
- catch (error) {
1580
- await this.reply(chatId, this.formatManagerError(error));
1581
- }
1582
- }
1583
- async handleVerbosityCommand(chatId, verbosity) {
1584
- const session = await this.requireActiveSession(chatId);
1585
- if (!session) {
1586
- return;
1587
- }
1588
- this.sessionVerbosityBySession.set(session.id, verbosity);
1589
- const message = verbosity === "quiet"
1590
- ? `${formatTelegramSessionName(session)} is now quiet.`
1591
- : `${formatTelegramSessionName(session)} will now show progress updates.`;
1592
- await this.reply(chatId, message);
1593
- }
1594
1368
  async handleMessage(chatId, text, sourceMessageId) {
1595
1369
  const session = await this.requireActiveOrSingleSession(chatId);
1596
1370
  if (!session) {
@@ -1893,6 +1667,8 @@ class AsyncTelegramAdapterImpl {
1893
1667
  }
1894
1668
  }
1895
1669
  clearClosedSession(sessionId) {
1670
+ this.stopTypingIndicators(sessionId);
1671
+ this.clearSessionDrafts(sessionId);
1896
1672
  for (const [chatId, activeSessionId] of this.activeSessionsByChat) {
1897
1673
  if (activeSessionId === sessionId) {
1898
1674
  this.activeSessionsByChat.delete(chatId);
@@ -1902,17 +1678,13 @@ class AsyncTelegramAdapterImpl {
1902
1678
  for (const chatId of chatIds) {
1903
1679
  this.unlinkChatFromSession(chatId, sessionId);
1904
1680
  }
1905
- this.sessionVerbosityBySession.delete(sessionId);
1906
1681
  this.lastCommandBySession.delete(sessionId);
1907
1682
  this.lastAssistantMessageBySession.delete(sessionId);
1908
1683
  this.latestAssistantMessageByRun.delete(sessionId);
1909
1684
  this.clearSessionAttachments(sessionId);
1910
1685
  }
1911
- getSessionVerbosity(sessionId) {
1912
- return this.sessionVerbosityBySession.get(sessionId) ?? "quiet";
1913
- }
1914
- isVerboseSession(sessionId) {
1915
- return this.getSessionVerbosity(sessionId) === "verbose";
1686
+ isVerboseSession(_sessionId) {
1687
+ return false;
1916
1688
  }
1917
1689
  onSessionEvent(event) {
1918
1690
  if (event.type === "session-progress") {
@@ -1930,6 +1702,8 @@ class AsyncTelegramAdapterImpl {
1930
1702
  }
1931
1703
  if (event.state === "running") {
1932
1704
  this.latestAssistantMessageByRun.delete(event.sessionId);
1705
+ this.startTypingIndicators(event.sessionId);
1706
+ this.showDraftThinking(event.sessionId);
1933
1707
  if (this.isVerboseSession(event.sessionId)) {
1934
1708
  this.notifyLifecycle(event.sessionId, event.projectId, "started");
1935
1709
  }
@@ -1937,6 +1711,8 @@ class AsyncTelegramAdapterImpl {
1937
1711
  }
1938
1712
  if (event.state === "failed") {
1939
1713
  this.latestAssistantMessageByRun.delete(event.sessionId);
1714
+ this.stopTypingIndicators(event.sessionId);
1715
+ this.clearSessionDrafts(event.sessionId);
1940
1716
  this.notifyLifecycle(event.sessionId, event.projectId, "failed");
1941
1717
  return;
1942
1718
  }
@@ -1945,6 +1721,8 @@ class AsyncTelegramAdapterImpl {
1945
1721
  return;
1946
1722
  }
1947
1723
  if (event.state === "waiting-input" && event.previousState === "running") {
1724
+ this.stopTypingIndicators(event.sessionId);
1725
+ this.clearSessionDrafts(event.sessionId);
1948
1726
  if (this.isVerboseSession(event.sessionId)) {
1949
1727
  this.notifyLifecycle(event.sessionId, event.projectId, "finished");
1950
1728
  return;
@@ -1952,7 +1730,7 @@ class AsyncTelegramAdapterImpl {
1952
1730
  const message = this.latestAssistantMessageByRun.get(event.sessionId);
1953
1731
  this.latestAssistantMessageByRun.delete(event.sessionId);
1954
1732
  if (message) {
1955
- this.notifySession(event.sessionId, message);
1733
+ this.notifySession(event.sessionId, message, { rich: true });
1956
1734
  }
1957
1735
  }
1958
1736
  }
@@ -1985,6 +1763,7 @@ class AsyncTelegramAdapterImpl {
1985
1763
  }
1986
1764
  this.lastAssistantMessageBySession.set(event.sessionId, event.progress.text);
1987
1765
  this.latestAssistantMessageByRun.set(event.sessionId, event.progress.text);
1766
+ this.showDraftPreamble(event.sessionId, event.progress.text);
1988
1767
  if (!isVerbose) {
1989
1768
  return;
1990
1769
  }
@@ -2002,19 +1781,161 @@ class AsyncTelegramAdapterImpl {
2002
1781
  }
2003
1782
  this.notifySession(sessionId, [formatSessionHeadline(sessionId, stateLabel), `project: ${projectId}`].join("\n"));
2004
1783
  }
2005
- notifySession(sessionId, text) {
1784
+ notifySession(sessionId, text, options = {}) {
2006
1785
  const chatIds = this.chatsBySession.get(sessionId);
2007
1786
  if (!chatIds || chatIds.size === 0) {
2008
1787
  return;
2009
1788
  }
2010
1789
  for (const chatId of chatIds) {
2011
- void this.reply(chatId, text);
1790
+ void this.reply(chatId, text, { rich: options.rich }).catch((error) => {
1791
+ this.log("warn", "failed to send telegram notification", {
1792
+ chatId,
1793
+ cause: error instanceof Error ? error.message : String(error),
1794
+ });
1795
+ });
2012
1796
  }
2013
1797
  }
2014
- async reactToQueuedMessage(chatId, messageId) {
2015
- if (!this.api.setMessageReaction) {
1798
+ startTypingIndicators(sessionId) {
1799
+ const chatIds = this.chatsBySession.get(sessionId);
1800
+ if (!chatIds) {
2016
1801
  return;
2017
1802
  }
1803
+ for (const chatId of chatIds) {
1804
+ const key = this.getSessionChatKey(sessionId, chatId);
1805
+ if (this.typingIntervalsBySessionChat.has(key)) {
1806
+ continue;
1807
+ }
1808
+ const sendTyping = () => {
1809
+ void this.api.sendChatAction(chatId, "typing").catch((error) => {
1810
+ this.log("warn", "failed to send telegram typing action", {
1811
+ chatId,
1812
+ cause: error instanceof Error ? error.message : String(error),
1813
+ });
1814
+ });
1815
+ };
1816
+ sendTyping();
1817
+ const interval = setInterval(sendTyping, TELEGRAM_TYPING_REFRESH_MS);
1818
+ interval.unref?.();
1819
+ this.typingIntervalsBySessionChat.set(key, interval);
1820
+ }
1821
+ }
1822
+ stopTypingIndicators(sessionId) {
1823
+ const chatIds = this.chatsBySession.get(sessionId);
1824
+ if (!chatIds) {
1825
+ return;
1826
+ }
1827
+ for (const chatId of chatIds) {
1828
+ const key = this.getSessionChatKey(sessionId, chatId);
1829
+ const interval = this.typingIntervalsBySessionChat.get(key);
1830
+ if (interval) {
1831
+ clearInterval(interval);
1832
+ this.typingIntervalsBySessionChat.delete(key);
1833
+ }
1834
+ }
1835
+ }
1836
+ showDraftThinking(sessionId) {
1837
+ const chatIds = this.chatsBySession.get(sessionId);
1838
+ if (!chatIds) {
1839
+ return;
1840
+ }
1841
+ for (const chatId of chatIds) {
1842
+ if (this.chatTypesByChat.get(chatId) !== "private") {
1843
+ continue;
1844
+ }
1845
+ void this.updateMessageDraft(sessionId, chatId, TELEGRAM_DRAFT_THINKING_MARKDOWN);
1846
+ }
1847
+ }
1848
+ showDraftPreamble(sessionId, text) {
1849
+ const chatIds = this.chatsBySession.get(sessionId);
1850
+ if (!chatIds) {
1851
+ return;
1852
+ }
1853
+ for (const chatId of chatIds) {
1854
+ if (this.chatTypesByChat.get(chatId) !== "private") {
1855
+ continue;
1856
+ }
1857
+ const key = this.getSessionChatKey(sessionId, chatId);
1858
+ const state = this.getDraftState(key);
1859
+ if (state.preambleTimeout) {
1860
+ clearTimeout(state.preambleTimeout);
1861
+ }
1862
+ void this.updateMessageDraft(sessionId, chatId, truncateText(text, TELEGRAM_DRAFT_MAX_CHARS));
1863
+ const timeout = setTimeout(() => {
1864
+ const currentState = this.draftStatesBySessionChat.get(key);
1865
+ if (currentState === state) {
1866
+ void this.updateMessageDraft(sessionId, chatId, TELEGRAM_DRAFT_THINKING_MARKDOWN);
1867
+ }
1868
+ }, TELEGRAM_DRAFT_PREAMBLE_VISIBLE_MS);
1869
+ timeout.unref?.();
1870
+ state.preambleTimeout = timeout;
1871
+ }
1872
+ }
1873
+ async updateMessageDraft(sessionId, chatId, markdown) {
1874
+ const key = this.getSessionChatKey(sessionId, chatId);
1875
+ const state = this.getDraftState(key);
1876
+ state.markdown = markdown;
1877
+ this.startMessageDraftRefresh(sessionId, chatId, state);
1878
+ await this.sendMessageDraft(chatId, state);
1879
+ }
1880
+ startMessageDraftRefresh(sessionId, chatId, state) {
1881
+ if (state.refreshInterval) {
1882
+ return;
1883
+ }
1884
+ const interval = setInterval(() => {
1885
+ const currentState = this.draftStatesBySessionChat.get(this.getSessionChatKey(sessionId, chatId));
1886
+ if (currentState !== state) {
1887
+ clearInterval(interval);
1888
+ return;
1889
+ }
1890
+ void this.sendMessageDraft(chatId, currentState);
1891
+ }, TELEGRAM_DRAFT_REFRESH_MS);
1892
+ interval.unref?.();
1893
+ state.refreshInterval = interval;
1894
+ }
1895
+ async sendMessageDraft(chatId, state) {
1896
+ try {
1897
+ await this.api.sendRichMessageDraft(chatId, state.draftId, state.markdown);
1898
+ }
1899
+ catch (error) {
1900
+ this.log("warn", "failed to update telegram message draft", {
1901
+ chatId,
1902
+ cause: error instanceof Error ? error.message : String(error),
1903
+ });
1904
+ }
1905
+ }
1906
+ clearSessionDrafts(sessionId) {
1907
+ const chatIds = this.chatsBySession.get(sessionId);
1908
+ if (!chatIds) {
1909
+ return;
1910
+ }
1911
+ for (const chatId of chatIds) {
1912
+ const key = this.getSessionChatKey(sessionId, chatId);
1913
+ const state = this.draftStatesBySessionChat.get(key);
1914
+ if (state?.preambleTimeout) {
1915
+ clearTimeout(state.preambleTimeout);
1916
+ }
1917
+ if (state?.refreshInterval) {
1918
+ clearInterval(state.refreshInterval);
1919
+ }
1920
+ this.draftStatesBySessionChat.delete(key);
1921
+ }
1922
+ }
1923
+ getDraftState(key) {
1924
+ const existing = this.draftStatesBySessionChat.get(key);
1925
+ if (existing) {
1926
+ return existing;
1927
+ }
1928
+ const state = { draftId: this.createDraftId(), markdown: TELEGRAM_DRAFT_THINKING_MARKDOWN };
1929
+ this.draftStatesBySessionChat.set(key, state);
1930
+ return state;
1931
+ }
1932
+ createDraftId() {
1933
+ return Math.floor(Math.random() * 2_000_000_000) + 1;
1934
+ }
1935
+ getSessionChatKey(sessionId, chatId) {
1936
+ return `${sessionId}:${chatId}`;
1937
+ }
1938
+ async reactToQueuedMessage(chatId, messageId) {
2018
1939
  if (typeof messageId !== "number" || !Number.isInteger(messageId) || messageId <= 0) {
2019
1940
  return;
2020
1941
  }
@@ -2034,9 +1955,6 @@ class AsyncTelegramAdapterImpl {
2034
1955
  }
2035
1956
  }
2036
1957
  async answerCallbackQuery(callbackQueryId, text) {
2037
- if (!this.api.answerCallbackQuery) {
2038
- return;
2039
- }
2040
1958
  const trimmedCallbackQueryId = callbackQueryId?.trim();
2041
1959
  if (!trimmedCallbackQueryId) {
2042
1960
  return;
@@ -2058,26 +1976,35 @@ class AsyncTelegramAdapterImpl {
2058
1976
  return formatSessionHeadline(sessionId, "message queued");
2059
1977
  }
2060
1978
  formatManagerError(error) {
2061
- if (error instanceof AsyncSessionManagerError) {
1979
+ if (error instanceof TelegramSessionManagerError) {
2062
1980
  return error.message;
2063
1981
  }
2064
1982
  return error instanceof Error ? error.message : String(error);
2065
1983
  }
2066
1984
  async reply(chatId, text, options) {
2067
- const chunks = splitTelegramMessage(text);
1985
+ if (options?.rich === true) {
1986
+ await this.replyWithRichMessage(chatId, text, options.replyMarkup);
1987
+ return;
1988
+ }
1989
+ await this.replyWithPlainMessage(chatId, text, options?.replyMarkup);
1990
+ }
1991
+ async replyWithRichMessage(chatId, text, replyMarkup) {
1992
+ const chunks = splitTelegramRichMessage(text);
2068
1993
  for (const [index, chunk] of chunks.entries()) {
2069
- try {
2070
- await this.api.sendMessage(chatId, chunk, {
2071
- replyMarkup: index === chunks.length - 1 ? options?.replyMarkup : undefined,
2072
- });
2073
- }
2074
- catch (error) {
2075
- this.log("warn", "failed to send telegram message", {
2076
- chatId,
2077
- cause: error instanceof Error ? error.message : String(error),
2078
- });
2079
- return;
1994
+ await this.api.sendRichMessage(chatId, chunk, {
1995
+ replyMarkup: index === chunks.length - 1 ? replyMarkup : undefined,
1996
+ });
1997
+ if (index < chunks.length - 1) {
1998
+ await this.wait(TELEGRAM_MESSAGE_SPLIT_DELAY_MS);
2080
1999
  }
2000
+ }
2001
+ }
2002
+ async replyWithPlainMessage(chatId, text, replyMarkup) {
2003
+ const chunks = splitTelegramMessage(text);
2004
+ for (const [index, chunk] of chunks.entries()) {
2005
+ await this.api.sendMessage(chatId, chunk, {
2006
+ replyMarkup: index === chunks.length - 1 ? replyMarkup : undefined,
2007
+ });
2081
2008
  if (index < chunks.length - 1) {
2082
2009
  await this.wait(TELEGRAM_MESSAGE_SPLIT_DELAY_MS);
2083
2010
  }
@@ -2101,14 +2028,14 @@ class AsyncTelegramAdapterImpl {
2101
2028
  });
2102
2029
  }
2103
2030
  }
2104
- export async function startAsyncTelegramAdapter(options) {
2031
+ export async function startTelegramAdapter(options) {
2105
2032
  await sweepStaleTelegramAttachmentTempDirs();
2106
2033
  const api = options.api ?? createTelegramApi(options.botToken);
2107
2034
  const botUsername = normalizeTelegramUsername((await api.getMe()).username);
2108
2035
  if (!botUsername) {
2109
2036
  throw new Error("telegram bot username is missing");
2110
2037
  }
2111
- const adapter = new AsyncTelegramAdapterImpl({
2038
+ const adapter = new TelegramAdapterImpl({
2112
2039
  ...options,
2113
2040
  api,
2114
2041
  botUsername,
@@ -2117,4 +2044,4 @@ export async function startAsyncTelegramAdapter(options) {
2117
2044
  close: () => adapter.close(),
2118
2045
  };
2119
2046
  }
2120
- //# sourceMappingURL=telegram.js.map
2047
+ //# sourceMappingURL=adapter.js.map