@markusylisiurunen/tau 0.3.1 → 0.3.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.
Files changed (63) hide show
  1. package/README.md +15 -57
  2. package/dist/core/cli.js +2 -2
  3. package/dist/core/cli.js.map +1 -1
  4. package/dist/core/config/content_loader.js +3 -3
  5. package/dist/core/config/content_loader.js.map +1 -1
  6. package/dist/core/config/index.js.map +1 -1
  7. package/dist/core/config/schema.js +0 -159
  8. package/dist/core/config/schema.js.map +1 -1
  9. package/dist/core/index.js +2 -2
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/core/personas.js +2 -2
  12. package/dist/core/personas.js.map +1 -1
  13. package/dist/core/subagents/control_plane.js +12 -4
  14. package/dist/core/subagents/control_plane.js.map +1 -1
  15. package/dist/core/{async/telegram.js → telegram/adapter.js} +229 -329
  16. package/dist/core/telegram/adapter.js.map +1 -0
  17. package/dist/core/telegram/cli.js +148 -0
  18. package/dist/core/telegram/cli.js.map +1 -0
  19. package/dist/core/telegram/config.js +241 -0
  20. package/dist/core/telegram/config.js.map +1 -0
  21. package/dist/core/telegram/index.js +4 -0
  22. package/dist/core/telegram/index.js.map +1 -0
  23. package/dist/core/telegram/runtime.js +96 -0
  24. package/dist/core/telegram/runtime.js.map +1 -0
  25. package/dist/core/{async → telegram}/session_manager.js +22 -22
  26. package/dist/core/telegram/session_manager.js.map +1 -0
  27. package/dist/core/telegram/workspace.js.map +1 -0
  28. package/dist/core/tools/catalog.js +2 -2
  29. package/dist/core/tools/catalog.js.map +1 -1
  30. package/dist/core/tools/registry.js.map +1 -1
  31. package/dist/core/tools/spawn_agent.js +1 -1
  32. package/dist/core/tools/spawn_agent.js.map +1 -1
  33. package/dist/core/tools/tool_names.js +2 -2
  34. package/dist/core/tools/tool_names.js.map +1 -1
  35. package/dist/core/tools/{wait_for_agent.js → wait_for_agents.js} +30 -22
  36. package/dist/core/tools/wait_for_agents.js.map +1 -0
  37. package/dist/core/version.js +1 -1
  38. package/dist/main.js +7 -7
  39. package/dist/main.js.map +1 -1
  40. package/dist/tui/tool_ui_router.js +7 -7
  41. package/dist/tui/tool_ui_router.js.map +1 -1
  42. package/dist/tui/ui/tool_ui_registry.js +3 -3
  43. package/dist/tui/ui/tool_ui_registry.js.map +1 -1
  44. package/package.json +1 -1
  45. package/dist/core/async/cli.js +0 -557
  46. package/dist/core/async/cli.js.map +0 -1
  47. package/dist/core/async/cron.js +0 -370
  48. package/dist/core/async/cron.js.map +0 -1
  49. package/dist/core/async/daemon_runtime.js +0 -120
  50. package/dist/core/async/daemon_runtime.js.map +0 -1
  51. package/dist/core/async/http_protocol.js +0 -177
  52. package/dist/core/async/http_protocol.js.map +0 -1
  53. package/dist/core/async/http_server.js +0 -278
  54. package/dist/core/async/http_server.js.map +0 -1
  55. package/dist/core/async/index.js +0 -10
  56. package/dist/core/async/index.js.map +0 -1
  57. package/dist/core/async/server_config.js +0 -420
  58. package/dist/core/async/server_config.js.map +0 -1
  59. package/dist/core/async/session_manager.js.map +0 -1
  60. package/dist/core/async/telegram.js.map +0 -1
  61. package/dist/core/async/workspace.js.map +0 -1
  62. package/dist/core/tools/wait_for_agent.js.map +0 -1
  63. /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,22 @@ 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_PREAMBLE_VISIBLE_MS = 5000;
27
+ const TELEGRAM_TYPING_REFRESH_MS = 4000;
23
28
  const ABORTED = Symbol("aborted");
24
29
  const CALLBACK_ACTION_PREFIX = "tau:action:";
25
- const CALLBACK_USE_PREFIX = "tau:use:";
26
- const MAX_SESSION_PREVIEW_CHARS = 64;
27
30
  const MAX_TELEGRAM_ATTACHMENTS_PER_TURN = 10;
28
31
  const MAX_TELEGRAM_ATTACHMENT_FILE_BYTES = 20 * 1024 * 1024;
29
32
  const MAX_TELEGRAM_ATTACHMENT_TOTAL_BYTES = 50 * 1024 * 1024;
30
33
  const MAX_TELEGRAM_GROUP_PENDING_MESSAGES = 50;
31
34
  const TELEGRAM_ATTACHMENT_TEMP_DIR_PREFIX = "tau-telegram-attachments-";
32
- const NO_ACTIVE_SESSION_MESSAGE = "no active session. use /new or /sessions";
35
+ const NO_ACTIVE_SESSION_MESSAGE = "no active session. use /new";
33
36
  const SUPPORTED_TEXT_ATTACHMENT_EXTENSIONS = new Set([
34
37
  ".txt",
35
38
  ".md",
@@ -98,11 +101,6 @@ async function sweepStaleTelegramAttachmentTempDirs() {
98
101
  return;
99
102
  }
100
103
  }
101
- const QUICK_ACTION_ROWS = [
102
- ["new", "sessions", "status"],
103
- ["interrupt", "close"],
104
- ["quiet", "verbose"],
105
- ];
106
104
  function splitCommandText(text) {
107
105
  return text
108
106
  .trim()
@@ -196,15 +194,15 @@ function truncateText(text, maxChars) {
196
194
  }
197
195
  return `${trimmed.slice(0, maxChars - 1)}…`;
198
196
  }
199
- function splitTelegramMessage(text) {
200
- if (Buffer.byteLength(text, "utf8") <= TELEGRAM_SAFE_MESSAGE_BYTES) {
197
+ function splitTelegramTextByBytes(text, maxBytes) {
198
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) {
201
199
  return [text];
202
200
  }
203
201
  const chunks = [];
204
202
  let currentChunk = "";
205
203
  for (const character of text) {
206
204
  const nextChunk = `${currentChunk}${character}`;
207
- if (Buffer.byteLength(nextChunk, "utf8") <= TELEGRAM_SAFE_MESSAGE_BYTES) {
205
+ if (Buffer.byteLength(nextChunk, "utf8") <= maxBytes) {
208
206
  currentChunk = nextChunk;
209
207
  continue;
210
208
  }
@@ -220,6 +218,12 @@ function splitTelegramMessage(text) {
220
218
  }
221
219
  return chunks;
222
220
  }
221
+ function splitTelegramMessage(text) {
222
+ return splitTelegramTextByBytes(text, TELEGRAM_SAFE_MESSAGE_BYTES);
223
+ }
224
+ function splitTelegramRichMessage(text) {
225
+ return splitTelegramTextByBytes(text, TELEGRAM_RICH_MESSAGE_SAFE_BYTES);
226
+ }
223
227
  function normalizeSizeBytes(value) {
224
228
  if (typeof value !== "number" || !Number.isFinite(value)) {
225
229
  return undefined;
@@ -478,6 +482,28 @@ function createTelegramApi(botToken) {
478
482
  ...(options?.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
479
483
  }, z.unknown());
480
484
  },
485
+ async sendRichMessage(chatId, markdown, options) {
486
+ await callTelegramMethod("sendRichMessage", {
487
+ chat_id: chatId,
488
+ rich_message: {
489
+ markdown,
490
+ },
491
+ ...(options?.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
492
+ }, z.unknown());
493
+ },
494
+ async sendMessageDraft(chatId, draftId, text) {
495
+ await callTelegramMethod("sendMessageDraft", {
496
+ chat_id: chatId,
497
+ draft_id: draftId,
498
+ text,
499
+ }, TelegramAckResultSchema);
500
+ },
501
+ async sendChatAction(chatId, action) {
502
+ await callTelegramMethod("sendChatAction", {
503
+ chat_id: chatId,
504
+ action,
505
+ }, TelegramAckResultSchema);
506
+ },
481
507
  async downloadFile(fileId) {
482
508
  const parsed = await callTelegramMethod("getFile", {
483
509
  file_id: fileId,
@@ -514,7 +540,7 @@ function createTelegramApi(botToken) {
514
540
  },
515
541
  };
516
542
  }
517
- class AsyncTelegramAdapterImpl {
543
+ class TelegramAdapterImpl {
518
544
  projects;
519
545
  defaultProjectId;
520
546
  systemMessage;
@@ -540,7 +566,9 @@ class AsyncTelegramAdapterImpl {
540
566
  activeSessionsByChat = new Map();
541
567
  sessionsByChat = new Map();
542
568
  chatsBySession = new Map();
543
- sessionVerbosityBySession = new Map();
569
+ chatTypesByChat = new Map();
570
+ typingIntervalsBySessionChat = new Map();
571
+ draftStatesBySessionChat = new Map();
544
572
  lastCommandBySession = new Map();
545
573
  lastAssistantMessageBySession = new Map();
546
574
  latestAssistantMessageByRun = new Map();
@@ -612,6 +640,10 @@ class AsyncTelegramAdapterImpl {
612
640
  try {
613
641
  await this.loopPromise;
614
642
  await this.waitForInFlightUpdateTasks();
643
+ for (const sessionId of Array.from(this.chatsBySession.keys())) {
644
+ this.stopTypingIndicators(sessionId);
645
+ this.clearSessionDrafts(sessionId);
646
+ }
615
647
  for (const sessionId of Array.from(this.attachmentTempDirsBySession.keys())) {
616
648
  await this.cleanupSessionAttachmentTempDirs(sessionId);
617
649
  }
@@ -627,38 +659,13 @@ class AsyncTelegramAdapterImpl {
627
659
  }
628
660
  createCommandDefinitions() {
629
661
  return [
630
- {
631
- command: "/help",
632
- description: "show supported commands",
633
- usage: "/help",
634
- handler: async (chatId) => this.handleHelp(chatId),
635
- },
636
662
  {
637
663
  command: "/new",
638
664
  description: "start a new session",
639
- usage: "/new [projectId]",
665
+ usage: "/new",
640
666
  callbackAction: "new",
641
667
  handler: async (chatId, args, sourceMessageId) => this.handleNew(chatId, args, sourceMessageId),
642
668
  },
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
669
  {
663
670
  command: "/status",
664
671
  description: "show active session status",
@@ -673,33 +680,9 @@ class AsyncTelegramAdapterImpl {
673
680
  callbackAction: "interrupt",
674
681
  handler: async (chatId) => this.handleInterrupt(chatId),
675
682
  },
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
683
  ];
698
684
  }
699
685
  async syncCommands() {
700
- if (!this.api.setCommands) {
701
- return;
702
- }
703
686
  const commands = this.commandDefinitions.map((definition) => ({
704
687
  command: definition.command.slice(1),
705
688
  description: definition.description,
@@ -837,6 +820,7 @@ class AsyncTelegramAdapterImpl {
837
820
  if (!chat) {
838
821
  return;
839
822
  }
823
+ this.chatTypesByChat.set(chat.id, chat.type);
840
824
  if (chat.type === "private") {
841
825
  await this.handlePrivateMessage(chat.id, message);
842
826
  return;
@@ -1277,7 +1261,7 @@ class AsyncTelegramAdapterImpl {
1277
1261
  if (!this.enforceChatOwnership) {
1278
1262
  return this.sessionManager;
1279
1263
  }
1280
- return createScopedAsyncSessionManager({
1264
+ return createScopedTelegramSessionManager({
1281
1265
  sessionManager: this.sessionManager,
1282
1266
  ownerId: this.ownerIdForChat(chatId),
1283
1267
  allowedProjectIds: this.allowedProjectIds,
@@ -1289,20 +1273,12 @@ class AsyncTelegramAdapterImpl {
1289
1273
  const args = parts.slice(1);
1290
1274
  const handler = this.commandHandlers.get(command);
1291
1275
  if (!handler) {
1292
- await this.reply(chatId, "unsupported command. use /help");
1276
+ await this.reply(chatId, "unsupported command. supported commands: /new, /status, /interrupt");
1293
1277
  return;
1294
1278
  }
1295
1279
  await handler(chatId, args, sourceMessageId);
1296
1280
  }
1297
1281
  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
1282
  if (!callbackData.startsWith(CALLBACK_ACTION_PREFIX)) {
1307
1283
  return false;
1308
1284
  }
@@ -1314,77 +1290,29 @@ class AsyncTelegramAdapterImpl {
1314
1290
  await handler(chatId, []);
1315
1291
  return true;
1316
1292
  }
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
1293
  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]" };
1294
+ if (args.length > 0) {
1295
+ return { error: "usage: /new" };
1351
1296
  }
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]) {
1297
+ const projectIds = Object.keys(this.projects);
1298
+ if (this.defaultProjectId) {
1299
+ if (!this.projects[this.defaultProjectId]) {
1358
1300
  return {
1359
- error: `unknown project '${projectId}'. usage: /new [projectId]. use /projects to list options`,
1301
+ error: `telegram.<botId>.defaultProjectId '${this.defaultProjectId}' is not configured`,
1360
1302
  };
1361
1303
  }
1362
- return { projectId };
1304
+ return { projectId: this.defaultProjectId };
1363
1305
  }
1364
- const fallback = resolveFallbackProjectId();
1365
- if (!fallback.projectId) {
1366
- return {
1367
- error: fallback.error ?? "unable to resolve project id",
1368
- };
1306
+ if (projectIds.length === 1) {
1307
+ return { projectId: projectIds[0] };
1308
+ }
1309
+ if (projectIds.length === 0) {
1310
+ return { error: "no telegram projects configured" };
1369
1311
  }
1370
1312
  return {
1371
- projectId: fallback.projectId,
1313
+ error: "multiple projects configured. set defaultProjectId for this bot before using /new",
1372
1314
  };
1373
1315
  }
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
1316
  async handleNew(chatId, args, sourceMessageId) {
1389
1317
  const parsed = this.resolveNewCommand(args);
1390
1318
  if ("error" in parsed) {
@@ -1393,134 +1321,27 @@ class AsyncTelegramAdapterImpl {
1393
1321
  }
1394
1322
  try {
1395
1323
  const sessionManager = this.getSessionManagerForChat(chatId);
1324
+ const previousSession = this.getActiveSession(chatId);
1325
+ if (previousSession) {
1326
+ await sessionManager.closeSession(previousSession.id);
1327
+ this.clearClosedSession(previousSession.id);
1328
+ }
1396
1329
  const session = await sessionManager.createSession({
1397
1330
  projectId: parsed.projectId,
1398
1331
  });
1399
1332
  this.setActiveSession(chatId, session.id);
1400
- this.sessionVerbosityBySession.set(session.id, "quiet");
1401
1333
  void this.reactToQueuedMessage(chatId, sourceMessageId);
1402
1334
  }
1403
1335
  catch (error) {
1404
1336
  await this.reply(chatId, this.formatManagerError(error));
1405
1337
  }
1406
1338
  }
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
1339
  async handleStatus(chatId) {
1517
1340
  const session = await this.requireActiveSession(chatId);
1518
1341
  if (!session) {
1519
1342
  return;
1520
1343
  }
1521
- await this.reply(chatId, formatSessionStatus(session), {
1522
- replyMarkup: this.buildQuickActionsKeyboard(),
1523
- });
1344
+ await this.reply(chatId, formatSessionStatus(session));
1524
1345
  }
1525
1346
  async handleInterrupt(chatId) {
1526
1347
  const session = await this.requireActiveSession(chatId);
@@ -1540,57 +1361,6 @@ class AsyncTelegramAdapterImpl {
1540
1361
  await this.reply(chatId, this.formatManagerError(error));
1541
1362
  }
1542
1363
  }
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
1364
  async handleMessage(chatId, text, sourceMessageId) {
1595
1365
  const session = await this.requireActiveOrSingleSession(chatId);
1596
1366
  if (!session) {
@@ -1893,6 +1663,8 @@ class AsyncTelegramAdapterImpl {
1893
1663
  }
1894
1664
  }
1895
1665
  clearClosedSession(sessionId) {
1666
+ this.stopTypingIndicators(sessionId);
1667
+ this.clearSessionDrafts(sessionId);
1896
1668
  for (const [chatId, activeSessionId] of this.activeSessionsByChat) {
1897
1669
  if (activeSessionId === sessionId) {
1898
1670
  this.activeSessionsByChat.delete(chatId);
@@ -1902,17 +1674,13 @@ class AsyncTelegramAdapterImpl {
1902
1674
  for (const chatId of chatIds) {
1903
1675
  this.unlinkChatFromSession(chatId, sessionId);
1904
1676
  }
1905
- this.sessionVerbosityBySession.delete(sessionId);
1906
1677
  this.lastCommandBySession.delete(sessionId);
1907
1678
  this.lastAssistantMessageBySession.delete(sessionId);
1908
1679
  this.latestAssistantMessageByRun.delete(sessionId);
1909
1680
  this.clearSessionAttachments(sessionId);
1910
1681
  }
1911
- getSessionVerbosity(sessionId) {
1912
- return this.sessionVerbosityBySession.get(sessionId) ?? "quiet";
1913
- }
1914
- isVerboseSession(sessionId) {
1915
- return this.getSessionVerbosity(sessionId) === "verbose";
1682
+ isVerboseSession(_sessionId) {
1683
+ return false;
1916
1684
  }
1917
1685
  onSessionEvent(event) {
1918
1686
  if (event.type === "session-progress") {
@@ -1930,6 +1698,8 @@ class AsyncTelegramAdapterImpl {
1930
1698
  }
1931
1699
  if (event.state === "running") {
1932
1700
  this.latestAssistantMessageByRun.delete(event.sessionId);
1701
+ this.startTypingIndicators(event.sessionId);
1702
+ this.showDraftThinking(event.sessionId);
1933
1703
  if (this.isVerboseSession(event.sessionId)) {
1934
1704
  this.notifyLifecycle(event.sessionId, event.projectId, "started");
1935
1705
  }
@@ -1937,6 +1707,8 @@ class AsyncTelegramAdapterImpl {
1937
1707
  }
1938
1708
  if (event.state === "failed") {
1939
1709
  this.latestAssistantMessageByRun.delete(event.sessionId);
1710
+ this.stopTypingIndicators(event.sessionId);
1711
+ this.clearSessionDrafts(event.sessionId);
1940
1712
  this.notifyLifecycle(event.sessionId, event.projectId, "failed");
1941
1713
  return;
1942
1714
  }
@@ -1945,6 +1717,8 @@ class AsyncTelegramAdapterImpl {
1945
1717
  return;
1946
1718
  }
1947
1719
  if (event.state === "waiting-input" && event.previousState === "running") {
1720
+ this.stopTypingIndicators(event.sessionId);
1721
+ this.clearSessionDrafts(event.sessionId);
1948
1722
  if (this.isVerboseSession(event.sessionId)) {
1949
1723
  this.notifyLifecycle(event.sessionId, event.projectId, "finished");
1950
1724
  return;
@@ -1952,7 +1726,7 @@ class AsyncTelegramAdapterImpl {
1952
1726
  const message = this.latestAssistantMessageByRun.get(event.sessionId);
1953
1727
  this.latestAssistantMessageByRun.delete(event.sessionId);
1954
1728
  if (message) {
1955
- this.notifySession(event.sessionId, message);
1729
+ this.notifySession(event.sessionId, message, { rich: true });
1956
1730
  }
1957
1731
  }
1958
1732
  }
@@ -1985,6 +1759,7 @@ class AsyncTelegramAdapterImpl {
1985
1759
  }
1986
1760
  this.lastAssistantMessageBySession.set(event.sessionId, event.progress.text);
1987
1761
  this.latestAssistantMessageByRun.set(event.sessionId, event.progress.text);
1762
+ this.showDraftPreamble(event.sessionId, event.progress.text);
1988
1763
  if (!isVerbose) {
1989
1764
  return;
1990
1765
  }
@@ -2002,19 +1777,138 @@ class AsyncTelegramAdapterImpl {
2002
1777
  }
2003
1778
  this.notifySession(sessionId, [formatSessionHeadline(sessionId, stateLabel), `project: ${projectId}`].join("\n"));
2004
1779
  }
2005
- notifySession(sessionId, text) {
1780
+ notifySession(sessionId, text, options = {}) {
2006
1781
  const chatIds = this.chatsBySession.get(sessionId);
2007
1782
  if (!chatIds || chatIds.size === 0) {
2008
1783
  return;
2009
1784
  }
2010
1785
  for (const chatId of chatIds) {
2011
- void this.reply(chatId, text);
1786
+ void this.reply(chatId, text, { rich: options.rich }).catch((error) => {
1787
+ this.log("warn", "failed to send telegram notification", {
1788
+ chatId,
1789
+ cause: error instanceof Error ? error.message : String(error),
1790
+ });
1791
+ });
2012
1792
  }
2013
1793
  }
2014
- async reactToQueuedMessage(chatId, messageId) {
2015
- if (!this.api.setMessageReaction) {
1794
+ startTypingIndicators(sessionId) {
1795
+ const chatIds = this.chatsBySession.get(sessionId);
1796
+ if (!chatIds) {
1797
+ return;
1798
+ }
1799
+ for (const chatId of chatIds) {
1800
+ const key = this.getSessionChatKey(sessionId, chatId);
1801
+ if (this.typingIntervalsBySessionChat.has(key)) {
1802
+ continue;
1803
+ }
1804
+ const sendTyping = () => {
1805
+ void this.api.sendChatAction(chatId, "typing").catch((error) => {
1806
+ this.log("warn", "failed to send telegram typing action", {
1807
+ chatId,
1808
+ cause: error instanceof Error ? error.message : String(error),
1809
+ });
1810
+ });
1811
+ };
1812
+ sendTyping();
1813
+ const interval = setInterval(sendTyping, TELEGRAM_TYPING_REFRESH_MS);
1814
+ interval.unref?.();
1815
+ this.typingIntervalsBySessionChat.set(key, interval);
1816
+ }
1817
+ }
1818
+ stopTypingIndicators(sessionId) {
1819
+ const chatIds = this.chatsBySession.get(sessionId);
1820
+ if (!chatIds) {
1821
+ return;
1822
+ }
1823
+ for (const chatId of chatIds) {
1824
+ const key = this.getSessionChatKey(sessionId, chatId);
1825
+ const interval = this.typingIntervalsBySessionChat.get(key);
1826
+ if (interval) {
1827
+ clearInterval(interval);
1828
+ this.typingIntervalsBySessionChat.delete(key);
1829
+ }
1830
+ }
1831
+ }
1832
+ showDraftThinking(sessionId) {
1833
+ const chatIds = this.chatsBySession.get(sessionId);
1834
+ if (!chatIds) {
1835
+ return;
1836
+ }
1837
+ for (const chatId of chatIds) {
1838
+ if (this.chatTypesByChat.get(chatId) !== "private") {
1839
+ continue;
1840
+ }
1841
+ void this.updateMessageDraft(sessionId, chatId, "");
1842
+ }
1843
+ }
1844
+ showDraftPreamble(sessionId, text) {
1845
+ const chatIds = this.chatsBySession.get(sessionId);
1846
+ if (!chatIds) {
2016
1847
  return;
2017
1848
  }
1849
+ for (const chatId of chatIds) {
1850
+ if (this.chatTypesByChat.get(chatId) !== "private") {
1851
+ continue;
1852
+ }
1853
+ const key = this.getSessionChatKey(sessionId, chatId);
1854
+ const state = this.getDraftState(key);
1855
+ if (state.preambleTimeout) {
1856
+ clearTimeout(state.preambleTimeout);
1857
+ }
1858
+ void this.updateMessageDraft(sessionId, chatId, truncateText(text, TELEGRAM_DRAFT_MAX_CHARS));
1859
+ const timeout = setTimeout(() => {
1860
+ const currentState = this.draftStatesBySessionChat.get(key);
1861
+ if (currentState === state) {
1862
+ void this.updateMessageDraft(sessionId, chatId, "");
1863
+ }
1864
+ }, TELEGRAM_DRAFT_PREAMBLE_VISIBLE_MS);
1865
+ timeout.unref?.();
1866
+ state.preambleTimeout = timeout;
1867
+ }
1868
+ }
1869
+ async updateMessageDraft(sessionId, chatId, text) {
1870
+ const key = this.getSessionChatKey(sessionId, chatId);
1871
+ const state = this.getDraftState(key);
1872
+ try {
1873
+ await this.api.sendMessageDraft(chatId, state.draftId, text);
1874
+ }
1875
+ catch (error) {
1876
+ this.log("warn", "failed to update telegram message draft", {
1877
+ chatId,
1878
+ cause: error instanceof Error ? error.message : String(error),
1879
+ });
1880
+ }
1881
+ }
1882
+ clearSessionDrafts(sessionId) {
1883
+ const chatIds = this.chatsBySession.get(sessionId);
1884
+ if (!chatIds) {
1885
+ return;
1886
+ }
1887
+ for (const chatId of chatIds) {
1888
+ const key = this.getSessionChatKey(sessionId, chatId);
1889
+ const state = this.draftStatesBySessionChat.get(key);
1890
+ if (state?.preambleTimeout) {
1891
+ clearTimeout(state.preambleTimeout);
1892
+ }
1893
+ this.draftStatesBySessionChat.delete(key);
1894
+ }
1895
+ }
1896
+ getDraftState(key) {
1897
+ const existing = this.draftStatesBySessionChat.get(key);
1898
+ if (existing) {
1899
+ return existing;
1900
+ }
1901
+ const state = { draftId: this.createDraftId() };
1902
+ this.draftStatesBySessionChat.set(key, state);
1903
+ return state;
1904
+ }
1905
+ createDraftId() {
1906
+ return Math.floor(Math.random() * 2_000_000_000) + 1;
1907
+ }
1908
+ getSessionChatKey(sessionId, chatId) {
1909
+ return `${sessionId}:${chatId}`;
1910
+ }
1911
+ async reactToQueuedMessage(chatId, messageId) {
2018
1912
  if (typeof messageId !== "number" || !Number.isInteger(messageId) || messageId <= 0) {
2019
1913
  return;
2020
1914
  }
@@ -2034,9 +1928,6 @@ class AsyncTelegramAdapterImpl {
2034
1928
  }
2035
1929
  }
2036
1930
  async answerCallbackQuery(callbackQueryId, text) {
2037
- if (!this.api.answerCallbackQuery) {
2038
- return;
2039
- }
2040
1931
  const trimmedCallbackQueryId = callbackQueryId?.trim();
2041
1932
  if (!trimmedCallbackQueryId) {
2042
1933
  return;
@@ -2058,26 +1949,35 @@ class AsyncTelegramAdapterImpl {
2058
1949
  return formatSessionHeadline(sessionId, "message queued");
2059
1950
  }
2060
1951
  formatManagerError(error) {
2061
- if (error instanceof AsyncSessionManagerError) {
1952
+ if (error instanceof TelegramSessionManagerError) {
2062
1953
  return error.message;
2063
1954
  }
2064
1955
  return error instanceof Error ? error.message : String(error);
2065
1956
  }
2066
1957
  async reply(chatId, text, options) {
2067
- const chunks = splitTelegramMessage(text);
1958
+ if (options?.rich === true) {
1959
+ await this.replyWithRichMessage(chatId, text, options.replyMarkup);
1960
+ return;
1961
+ }
1962
+ await this.replyWithPlainMessage(chatId, text, options?.replyMarkup);
1963
+ }
1964
+ async replyWithRichMessage(chatId, text, replyMarkup) {
1965
+ const chunks = splitTelegramRichMessage(text);
2068
1966
  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;
1967
+ await this.api.sendRichMessage(chatId, chunk, {
1968
+ replyMarkup: index === chunks.length - 1 ? replyMarkup : undefined,
1969
+ });
1970
+ if (index < chunks.length - 1) {
1971
+ await this.wait(TELEGRAM_MESSAGE_SPLIT_DELAY_MS);
2080
1972
  }
1973
+ }
1974
+ }
1975
+ async replyWithPlainMessage(chatId, text, replyMarkup) {
1976
+ const chunks = splitTelegramMessage(text);
1977
+ for (const [index, chunk] of chunks.entries()) {
1978
+ await this.api.sendMessage(chatId, chunk, {
1979
+ replyMarkup: index === chunks.length - 1 ? replyMarkup : undefined,
1980
+ });
2081
1981
  if (index < chunks.length - 1) {
2082
1982
  await this.wait(TELEGRAM_MESSAGE_SPLIT_DELAY_MS);
2083
1983
  }
@@ -2101,14 +2001,14 @@ class AsyncTelegramAdapterImpl {
2101
2001
  });
2102
2002
  }
2103
2003
  }
2104
- export async function startAsyncTelegramAdapter(options) {
2004
+ export async function startTelegramAdapter(options) {
2105
2005
  await sweepStaleTelegramAttachmentTempDirs();
2106
2006
  const api = options.api ?? createTelegramApi(options.botToken);
2107
2007
  const botUsername = normalizeTelegramUsername((await api.getMe()).username);
2108
2008
  if (!botUsername) {
2109
2009
  throw new Error("telegram bot username is missing");
2110
2010
  }
2111
- const adapter = new AsyncTelegramAdapterImpl({
2011
+ const adapter = new TelegramAdapterImpl({
2112
2012
  ...options,
2113
2013
  api,
2114
2014
  botUsername,
@@ -2117,4 +2017,4 @@ export async function startAsyncTelegramAdapter(options) {
2117
2017
  close: () => adapter.close(),
2118
2018
  };
2119
2019
  }
2120
- //# sourceMappingURL=telegram.js.map
2020
+ //# sourceMappingURL=adapter.js.map