@markusylisiurunen/tau 0.3.35 → 0.3.37

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 (90) hide show
  1. package/README.md +62 -15
  2. package/dist/core/auth/cli.js +10 -0
  3. package/dist/core/auth/cli.js.map +1 -1
  4. package/dist/core/auth/providers/openai_codex.js +36 -16
  5. package/dist/core/auth/providers/openai_codex.js.map +1 -1
  6. package/dist/core/cli.js +7 -0
  7. package/dist/core/cli.js.map +1 -1
  8. package/dist/core/config/builtin_themes.js +0 -93
  9. package/dist/core/config/builtin_themes.js.map +1 -1
  10. package/dist/core/config/client_tools.js +109 -0
  11. package/dist/core/config/client_tools.js.map +1 -0
  12. package/dist/core/config/index.js.map +1 -1
  13. package/dist/core/config/schema.js +33 -1
  14. package/dist/core/config/schema.js.map +1 -1
  15. package/dist/core/debug.js +16 -30
  16. package/dist/core/debug.js.map +1 -1
  17. package/dist/core/runtime/runtime_bootstrap.js +68 -25
  18. package/dist/core/runtime/runtime_bootstrap.js.map +1 -1
  19. package/dist/core/telegram/adapter.js +319 -67
  20. package/dist/core/telegram/adapter.js.map +1 -1
  21. package/dist/core/telegram/config.js +1 -1
  22. package/dist/core/telegram/runtime.js +20 -1
  23. package/dist/core/telegram/runtime.js.map +1 -1
  24. package/dist/core/telegram/session_manager.js +12 -0
  25. package/dist/core/telegram/session_manager.js.map +1 -1
  26. package/dist/core/tools/tool_names.js +1 -0
  27. package/dist/core/tools/tool_names.js.map +1 -1
  28. package/dist/core/tools/view_image.js +8 -5
  29. package/dist/core/tools/view_image.js.map +1 -1
  30. package/dist/core/utils/context.js +0 -1
  31. package/dist/core/utils/context.js.map +1 -1
  32. package/dist/core/utils/context_builder.js +2 -3
  33. package/dist/core/utils/context_builder.js.map +1 -1
  34. package/dist/core/utils/spawn_capture.js +58 -22
  35. package/dist/core/utils/spawn_capture.js.map +1 -1
  36. package/dist/core/utils/user_metadata.js +14 -0
  37. package/dist/core/utils/user_metadata.js.map +1 -1
  38. package/dist/core/version.js +1 -1
  39. package/dist/main.js +91 -31
  40. package/dist/main.js.map +1 -1
  41. package/dist/protocol/session_protocol.d.ts +1 -1
  42. package/dist/protocol/session_protocol.js +1 -1
  43. package/dist/sdk/client.js +7 -108
  44. package/dist/sdk/client.js.map +1 -1
  45. package/dist/sdk/index.d.ts +1 -1
  46. package/dist/sdk/index.js.map +1 -1
  47. package/dist/sdk/local_client.d.ts +3 -0
  48. package/dist/sdk/local_client.js +106 -0
  49. package/dist/sdk/local_client.js.map +1 -0
  50. package/dist/sdk/session.js +22 -3
  51. package/dist/sdk/session.js.map +1 -1
  52. package/dist/store/session_snapshot_migrations.js +5 -1
  53. package/dist/store/session_snapshot_migrations.js.map +1 -1
  54. package/dist/transport/in_process_session_transport.js +3 -0
  55. package/dist/transport/in_process_session_transport.js.map +1 -1
  56. package/dist/transport/index.d.ts +1 -1
  57. package/dist/transport/index.js.map +1 -1
  58. package/dist/transport/session_protocol_transport_helpers.js +10 -0
  59. package/dist/transport/session_protocol_transport_helpers.js.map +1 -1
  60. package/dist/transport/session_transport.d.ts +2 -0
  61. package/dist/transport/stdio_session_transport.d.ts +3 -1
  62. package/dist/transport/stdio_session_transport.js +13 -6
  63. package/dist/transport/stdio_session_transport.js.map +1 -1
  64. package/dist/transport/websocket_session_transport.d.ts +3 -1
  65. package/dist/transport/websocket_session_transport.js +11 -2
  66. package/dist/transport/websocket_session_transport.js.map +1 -1
  67. package/dist/tui/chat_controller/diff_review_service.js +12 -11
  68. package/dist/tui/chat_controller/diff_review_service.js.map +1 -1
  69. package/dist/tui/chat_controller/diff_review_user_message.js +1 -0
  70. package/dist/tui/chat_controller/diff_review_user_message.js.map +1 -1
  71. package/dist/tui/chat_controller/history_message_model.js +4 -1
  72. package/dist/tui/chat_controller/history_message_model.js.map +1 -1
  73. package/dist/tui/chat_view.js +0 -11
  74. package/dist/tui/chat_view.js.map +1 -1
  75. package/dist/tui/command_client_tools.js +80 -0
  76. package/dist/tui/command_client_tools.js.map +1 -0
  77. package/dist/tui/session_chat_app.js +72 -5
  78. package/dist/tui/session_chat_app.js.map +1 -1
  79. package/dist/tui/session_chat_controller.js +30 -56
  80. package/dist/tui/session_chat_controller.js.map +1 -1
  81. package/dist/tui/ui/theme/palette.js +0 -3
  82. package/dist/tui/ui/theme/palette.js.map +1 -1
  83. package/dist/tui/ui/theme/theme.js.map +1 -1
  84. package/dist/tui/ui/user_message.js +4 -10
  85. package/dist/tui/ui/user_message.js.map +1 -1
  86. package/package.json +1 -1
  87. package/dist/core/utils/agents_files.js +0 -282
  88. package/dist/core/utils/agents_files.js.map +0 -1
  89. package/dist/tui/memory_mode.js +0 -24
  90. package/dist/tui/memory_mode.js.map +0 -1
@@ -27,14 +27,23 @@ const TELEGRAM_MESSAGE_BYTE_BUFFER_RATIO = 0.95;
27
27
  const TELEGRAM_SAFE_MESSAGE_BYTES = Math.floor(TELEGRAM_MAX_MESSAGE_BYTES * TELEGRAM_MESSAGE_BYTE_BUFFER_RATIO);
28
28
  const TELEGRAM_RICH_MESSAGE_SAFE_BYTES = Math.floor(TELEGRAM_MAX_RICH_MESSAGE_BYTES * TELEGRAM_MESSAGE_BYTE_BUFFER_RATIO);
29
29
  const TELEGRAM_MESSAGE_SPLIT_DELAY_MS = 1000;
30
+ const TELEGRAM_DELIVERY_ATTEMPT_TIMEOUT_MS = 30_000;
31
+ const TELEGRAM_DELIVERY_RETRY_DELAYS_MS = [1000, 5000];
30
32
  const TELEGRAM_TYPING_REFRESH_MS = 4000;
31
33
  const ABORTED = Symbol("aborted");
34
+ const TIMED_OUT = Symbol("timed-out");
32
35
  const CALLBACK_ACTION_PREFIX = "tau:action:";
33
36
  const MAX_TELEGRAM_ATTACHMENTS_PER_TURN = 10;
34
37
  const MAX_TELEGRAM_ATTACHMENT_FILE_BYTES = 20 * 1024 * 1024;
35
38
  const MAX_TELEGRAM_ATTACHMENT_TOTAL_BYTES = 50 * 1024 * 1024;
36
39
  const MAX_TELEGRAM_GROUP_PENDING_MESSAGES = 50;
37
40
  const TELEGRAM_ATTACHMENT_TEMP_DIR_PREFIX = "tau-telegram-attachments-";
41
+ const TELEGRAM_REASONING_EFFORTS = [
42
+ "low",
43
+ "medium",
44
+ "high",
45
+ "xhigh",
46
+ ];
38
47
  const NO_ACTIVE_SESSION_MESSAGE = "no active session. use /new.";
39
48
  const SUPPORTED_TEXT_ATTACHMENT_EXTENSIONS = new Set([
40
49
  ".txt",
@@ -350,29 +359,41 @@ function formatTelegramSender(user) {
350
359
  }
351
360
  return `id ${user.id}`;
352
361
  }
353
- function formatTelegramSessionName(session) {
354
- return `${session.projectId} session ${session.id}`;
362
+ function formatTelegramSessionName(sessionId, projectLabel) {
363
+ return `${projectLabel} session ${sessionId}`;
355
364
  }
356
365
  function formatProjectLabel(projectId, project) {
357
366
  return "projectIds" in project ? `${projectId} (${project.projectIds.join(", ")})` : projectId;
358
367
  }
359
368
  function formatSessionStatus(session, project, snapshot, preferredProjectId) {
360
- const projectStatus = `project: ${formatProjectLabel(session.projectId, project)}.`;
369
+ const sessionName = formatTelegramSessionName(session.id, formatProjectLabel(session.projectId, project));
361
370
  const preferenceStatus = preferredProjectId && preferredProjectId !== session.projectId
362
- ? ` new chats will use: ${preferredProjectId}.`
371
+ ? ` new chats will use ${preferredProjectId}.`
363
372
  : "";
364
- if (session.state === "failed" && session.error) {
365
- return `${projectStatus} your session ${session.id} failed. ${ensureTerminalPunctuation(session.error)}${preferenceStatus}`;
366
- }
367
- const status = `${projectStatus} your session ${session.id} is ${session.state}.`;
373
+ if (session.state === "failed") {
374
+ const errorStatus = session.error ? ` ${ensureTerminalPunctuation(session.error)}` : "";
375
+ return `your ${sessionName} failed.${errorStatus}${preferenceStatus}`;
376
+ }
377
+ const state = {
378
+ queued: "queued",
379
+ "preparing-workspace": "preparing its workspace",
380
+ running: "running",
381
+ "waiting-input": "waiting for input",
382
+ }[session.state];
383
+ const status = `your ${sessionName} is ${state}.`;
368
384
  if (!snapshot) {
369
385
  return `${status}${preferenceStatus}`;
370
386
  }
387
+ const goalStatus = snapshot.goal
388
+ ? snapshot.goal.status === "active"
389
+ ? " it is pursuing a goal."
390
+ : " its goal is blocked."
391
+ : "";
371
392
  const model = snapshot.bootstrap.model.name || snapshot.bootstrap.model.id;
372
393
  const reasoning = snapshot.settings.reasoning ?? "none";
373
394
  const context = formatTelegramContextUsage(snapshot);
374
395
  const cost = formatTelegramSessionCost(getTelegramSessionCostTotal(snapshot));
375
- return `${status} it is using ${model} with ${reasoning} reasoning. context usage is ${context}. cumulative cost is ${cost}.${preferenceStatus}`;
396
+ return `${status}${goalStatus} it is using ${model} with ${reasoning} reasoning. context usage is ${context}. cumulative cost is ${cost}.${preferenceStatus}`;
376
397
  }
377
398
  function ensureTerminalPunctuation(text) {
378
399
  const trimmed = text.trim();
@@ -391,6 +412,17 @@ function formatTelegramContextUsage(snapshot) {
391
412
  function getTelegramSessionCostTotal(snapshot) {
392
413
  return snapshot.costTotal;
393
414
  }
415
+ function supportsReasoningEffort(snapshot, reasoning) {
416
+ if (!snapshot.bootstrap.model.reasoning) {
417
+ return false;
418
+ }
419
+ const persona = snapshot.catalog.personas.find((candidate) => candidate.id === snapshot.settings.personaId);
420
+ if (!persona) {
421
+ return false;
422
+ }
423
+ const allowed = persona.allowedReasoningLevels;
424
+ return !allowed || allowed.length === 0 || allowed.includes(reasoning);
425
+ }
394
426
  function formatTelegramSessionCost(total) {
395
427
  return `$${formatAdaptiveNumber(total, 2, 5)}`;
396
428
  }
@@ -443,17 +475,23 @@ function parseOrThrow(schema, raw, message) {
443
475
  }
444
476
  return parsed.data;
445
477
  }
478
+ const TelegramFailureEnvelopeSchema = z.object({
479
+ ok: z.literal(false),
480
+ error_code: z.number().int(),
481
+ description: z.string(),
482
+ parameters: z
483
+ .object({
484
+ retry_after: z.number().int().nonnegative().optional(),
485
+ })
486
+ .optional(),
487
+ });
446
488
  const TelegramEnvelopeSchema = z.discriminatedUnion("ok", [
447
489
  z.object({
448
490
  ok: z.literal(true),
449
491
  description: z.string().optional(),
450
492
  result: z.unknown(),
451
493
  }),
452
- z.object({
453
- ok: z.literal(false),
454
- description: z.string().optional(),
455
- result: z.unknown().optional(),
456
- }),
494
+ TelegramFailureEnvelopeSchema,
457
495
  ]);
458
496
  const TelegramChatSchema = telegramObject({ id: z.number(), type: z.string() });
459
497
  const TelegramUserSchema = telegramPartialObject({
@@ -508,6 +546,26 @@ const TelegramGetFileResultSchema = z.object({ file_path: z.string() });
508
546
  const TelegramGetMeResultSchema = telegramObject({ username: z.string() });
509
547
  const TelegramAckResultSchema = z.literal(true);
510
548
  const MAX_TELEGRAM_ERROR_DETAIL_LENGTH = 500;
549
+ export class TelegramRequestError extends Error {
550
+ retryable;
551
+ retryAfterMs;
552
+ constructor(message, options) {
553
+ super(message, { cause: options.cause });
554
+ this.name = "TelegramRequestError";
555
+ this.retryable = options.retryable;
556
+ this.retryAfterMs = options.retryAfterMs;
557
+ }
558
+ }
559
+ class TelegramDeliveryError extends Error {
560
+ attempts;
561
+ retryable;
562
+ constructor(cause, attempts, retryable) {
563
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
564
+ this.name = "TelegramDeliveryError";
565
+ this.attempts = attempts;
566
+ this.retryable = retryable;
567
+ }
568
+ }
511
569
  function getErrorCode(error) {
512
570
  if (typeof error !== "object" || error === null || !("code" in error)) {
513
571
  return undefined;
@@ -528,23 +586,38 @@ function truncateTelegramErrorDetail(detail) {
528
586
  }
529
587
  return `${detail.slice(0, MAX_TELEGRAM_ERROR_DETAIL_LENGTH)}…`;
530
588
  }
531
- async function readTelegramHttpErrorDetail(response) {
532
- const responseText = (await response.text()).trim();
533
- if (!responseText) {
534
- return "";
589
+ function isRetryableTelegramErrorCode(errorCode) {
590
+ return errorCode === 408 || errorCode === 429 || (errorCode >= 500 && errorCode <= 599);
591
+ }
592
+ function getTelegramRetryAfterMs(envelope) {
593
+ const retryAfterSeconds = envelope.parameters?.retry_after;
594
+ return retryAfterSeconds === undefined ? undefined : retryAfterSeconds * 1000;
595
+ }
596
+ function parseTelegramHttpError(responseText) {
597
+ const trimmedResponseText = responseText.trim();
598
+ if (!trimmedResponseText) {
599
+ return { detail: "" };
535
600
  }
536
601
  try {
537
- const parsed = TelegramEnvelopeSchema.safeParse(JSON.parse(responseText));
538
- if (parsed.success && parsed.data.description?.trim()) {
539
- return truncateTelegramErrorDetail(parsed.data.description.trim());
602
+ const parsed = TelegramFailureEnvelopeSchema.safeParse(JSON.parse(trimmedResponseText));
603
+ if (parsed.success) {
604
+ return {
605
+ detail: truncateTelegramErrorDetail(parsed.data.description.trim()),
606
+ errorCode: parsed.data.error_code,
607
+ retryAfterMs: getTelegramRetryAfterMs(parsed.data),
608
+ };
540
609
  }
541
610
  }
542
611
  catch { }
543
- return truncateTelegramErrorDetail(responseText);
612
+ return { detail: truncateTelegramErrorDetail(trimmedResponseText) };
613
+ }
614
+ function asPermanentTelegramRequestError(error) {
615
+ const message = error instanceof Error ? error.message : String(error);
616
+ return new TelegramRequestError(message, { retryable: false, cause: error });
544
617
  }
545
618
  function createTelegramApi(botToken) {
546
619
  const apiUrl = `https://api.telegram.org/bot${botToken}`;
547
- async function callTelegramMethod(method, payload, resultSchema) {
620
+ async function callTelegramMethod(method, payload, resultSchema, signal) {
548
621
  let response;
549
622
  try {
550
623
  response = await fetch(`${apiUrl}/${method}`, {
@@ -553,28 +626,68 @@ function createTelegramApi(botToken) {
553
626
  "content-type": "application/json",
554
627
  },
555
628
  body: JSON.stringify(payload),
629
+ signal,
556
630
  });
557
631
  }
558
632
  catch (error) {
559
- throw new Error(`telegram ${method} network request failed: ${formatTelegramNetworkError(error)}`);
633
+ throw new TelegramRequestError(`telegram ${method} network request failed: ${formatTelegramNetworkError(error)}`, { retryable: true, cause: error });
560
634
  }
561
635
  if (!response.ok) {
562
- const detail = await readTelegramHttpErrorDetail(response);
563
- throw new Error(`telegram ${method} failed: HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
636
+ let responseText;
637
+ try {
638
+ responseText = await response.text();
639
+ }
640
+ catch (error) {
641
+ throw new TelegramRequestError(`telegram ${method} failed: HTTP ${response.status}: response body read failed: ${formatTelegramNetworkError(error)}`, {
642
+ retryable: isRetryableTelegramErrorCode(response.status),
643
+ cause: error,
644
+ });
645
+ }
646
+ const { detail, errorCode, retryAfterMs } = parseTelegramHttpError(responseText);
647
+ throw new TelegramRequestError(`telegram ${method} failed: HTTP ${response.status}${detail ? `: ${detail}` : ""}`, {
648
+ retryable: isRetryableTelegramErrorCode(response.status) ||
649
+ (errorCode !== undefined && isRetryableTelegramErrorCode(errorCode)),
650
+ ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
651
+ });
652
+ }
653
+ let responseText;
654
+ try {
655
+ responseText = await response.text();
656
+ }
657
+ catch (error) {
658
+ throw new TelegramRequestError(`telegram ${method} response body read failed: ${formatTelegramNetworkError(error)}`, { retryable: true, cause: error });
564
659
  }
565
660
  let raw;
566
661
  try {
567
- raw = await response.json();
662
+ raw = JSON.parse(responseText);
568
663
  }
569
- catch {
570
- throw new Error(`telegram ${method} returned invalid JSON`);
664
+ catch (error) {
665
+ throw new TelegramRequestError(`telegram ${method} returned invalid JSON`, {
666
+ retryable: false,
667
+ cause: error,
668
+ });
669
+ }
670
+ let envelope;
671
+ try {
672
+ envelope = parseOrThrow(TelegramEnvelopeSchema, raw, `telegram ${method} returned an invalid response payload`);
673
+ }
674
+ catch (error) {
675
+ throw asPermanentTelegramRequestError(error);
571
676
  }
572
- const envelope = parseOrThrow(TelegramEnvelopeSchema, raw, `telegram ${method} returned an invalid response payload`);
573
677
  if (!envelope.ok) {
574
- const detail = envelope.description?.trim() ?? "";
575
- throw new Error(`telegram ${method} request failed${detail ? `: ${detail}` : ""}`);
678
+ const detail = envelope.description.trim();
679
+ const retryAfterMs = getTelegramRetryAfterMs(envelope);
680
+ throw new TelegramRequestError(`telegram ${method} request failed${detail ? `: ${detail}` : ""}`, {
681
+ retryable: isRetryableTelegramErrorCode(envelope.error_code),
682
+ ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
683
+ });
684
+ }
685
+ try {
686
+ return parseOrThrow(resultSchema, envelope.result, `telegram ${method} returned an invalid result`);
687
+ }
688
+ catch (error) {
689
+ throw asPermanentTelegramRequestError(error);
576
690
  }
577
- return parseOrThrow(resultSchema, envelope.result, `telegram ${method} returned an invalid result`);
578
691
  }
579
692
  return {
580
693
  async getMe() {
@@ -592,7 +705,7 @@ function createTelegramApi(botToken) {
592
705
  chat_id: chatId,
593
706
  text,
594
707
  ...(options?.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
595
- }, z.unknown());
708
+ }, z.unknown(), options.signal);
596
709
  },
597
710
  async sendRichMessage(chatId, markdown, options) {
598
711
  await callTelegramMethod("sendRichMessage", {
@@ -601,7 +714,7 @@ function createTelegramApi(botToken) {
601
714
  markdown,
602
715
  },
603
716
  ...(options?.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
604
- }, z.unknown());
717
+ }, z.unknown(), options.signal);
605
718
  },
606
719
  async sendChatAction(chatId, action) {
607
720
  await callTelegramMethod("sendChatAction", {
@@ -681,6 +794,8 @@ class TelegramAdapterImpl {
681
794
  attachmentTempDirsBySession = new Map();
682
795
  updateQueueTailByKey = new Map();
683
796
  inFlightUpdateTasks = new Set();
797
+ notificationQueueTailByChat = new Map();
798
+ inFlightNotificationTasks = new Set();
684
799
  unsubscribeSessionEvents;
685
800
  loopPromise;
686
801
  nextUpdateOffset = 0;
@@ -750,6 +865,7 @@ class TelegramAdapterImpl {
750
865
  try {
751
866
  await this.loopPromise;
752
867
  await this.waitForInFlightUpdateTasks();
868
+ await Promise.allSettled(Array.from(this.inFlightNotificationTasks));
753
869
  for (const sessionId of Array.from(this.chatsBySession.keys())) {
754
870
  this.stopTypingIndicators(sessionId);
755
871
  }
@@ -771,36 +887,38 @@ class TelegramAdapterImpl {
771
887
  {
772
888
  command: "/new",
773
889
  description: "start a new session",
774
- usage: "/new",
775
890
  callbackAction: "new",
776
891
  handler: async (chatId, args, sourceMessageId) => this.handleNew(chatId, args, sourceMessageId),
777
892
  },
778
893
  {
779
894
  command: "/status",
780
895
  description: "show active session status",
781
- usage: "/status",
782
896
  callbackAction: "status",
783
897
  handler: async (chatId) => this.handleStatus(chatId),
784
898
  },
785
899
  {
786
900
  command: "/compact",
787
901
  description: "compact session context",
788
- usage: "/compact",
789
902
  handler: async (chatId, args) => this.handleCompact(chatId, args),
790
903
  },
791
904
  {
792
905
  command: "/interrupt",
793
906
  description: "interrupt active run",
794
- usage: "/interrupt",
795
907
  callbackAction: "interrupt",
796
908
  handler: async (chatId) => this.handleInterrupt(chatId),
797
909
  },
798
910
  ];
911
+ for (const reasoning of TELEGRAM_REASONING_EFFORTS) {
912
+ definitions.push({
913
+ command: `/effort_${reasoning}`,
914
+ description: `set reasoning effort to ${reasoning}`,
915
+ handler: async (chatId, args) => this.handleSetReasoning(chatId, reasoning, args),
916
+ });
917
+ }
799
918
  for (const projectId of this.allowedProjectIds) {
800
919
  definitions.push({
801
920
  command: `/use_${projectId}`,
802
921
  description: `use ${projectId} for new sessions`,
803
- usage: `/use_${projectId}`,
804
922
  handler: async (chatId, args) => this.handleUseProject(chatId, projectId, args),
805
923
  });
806
924
  }
@@ -901,14 +1019,14 @@ class TelegramAdapterImpl {
901
1019
  timeoutSeconds: this.requestTimeoutSeconds,
902
1020
  allowedUpdates: ["message", "callback_query"],
903
1021
  }));
904
- if (updates === undefined) {
1022
+ if (updates === ABORTED) {
905
1023
  return [];
906
1024
  }
907
1025
  return updates;
908
1026
  }
909
1027
  async raceWithAbort(promise) {
910
1028
  if (this.abortController.signal.aborted) {
911
- return undefined;
1029
+ return ABORTED;
912
1030
  }
913
1031
  let abortListener;
914
1032
  const abortPromise = new Promise((resolve) => {
@@ -921,7 +1039,7 @@ class TelegramAdapterImpl {
921
1039
  this.abortController.signal.removeEventListener("abort", abortListener);
922
1040
  }
923
1041
  if (raceResult === ABORTED) {
924
- return undefined;
1042
+ return ABORTED;
925
1043
  }
926
1044
  if (!raceResult.ok) {
927
1045
  throw raceResult.error;
@@ -1467,6 +1585,32 @@ class TelegramAdapterImpl {
1467
1585
  await this.reply(chatId, `failed to save project preference: ${error instanceof Error ? error.message : String(error)}`);
1468
1586
  }
1469
1587
  }
1588
+ async handleSetReasoning(chatId, reasoning, args) {
1589
+ if (args.length > 0) {
1590
+ await this.reply(chatId, `usage: /effort_${reasoning}`);
1591
+ return;
1592
+ }
1593
+ const session = await this.requireActiveSession(chatId);
1594
+ if (!session) {
1595
+ return;
1596
+ }
1597
+ try {
1598
+ const sessionManager = this.getSessionManagerForChat(chatId);
1599
+ const snapshot = await sessionManager.getSessionSnapshot(session.id);
1600
+ if (!snapshot) {
1601
+ throw new TelegramSessionManagerError("not_ready", "session is still preparing");
1602
+ }
1603
+ if (!supportsReasoningEffort(snapshot, reasoning)) {
1604
+ await this.reply(chatId, `reasoning effort ${reasoning} is not supported by this session.`);
1605
+ return;
1606
+ }
1607
+ await sessionManager.setReasoning(session.id, reasoning);
1608
+ await this.reply(chatId, `reasoning effort set to ${reasoning}.`);
1609
+ }
1610
+ catch (error) {
1611
+ await this.reply(chatId, this.formatManagerError(error));
1612
+ }
1613
+ }
1470
1614
  async handleNew(chatId, args, sourceMessageId) {
1471
1615
  const parsed = this.resolveNewCommand(chatId, args);
1472
1616
  if ("error" in parsed) {
@@ -1495,7 +1639,7 @@ class TelegramAdapterImpl {
1495
1639
  const session = this.getActiveSession(chatId);
1496
1640
  if (!session) {
1497
1641
  await this.reply(chatId, preferredProjectId
1498
- ? `new chats will use: ${preferredProjectId}.`
1642
+ ? `new chats will use ${preferredProjectId}.`
1499
1643
  : "no active session. select a project with /use_<project>.");
1500
1644
  return;
1501
1645
  }
@@ -1544,10 +1688,10 @@ class TelegramAdapterImpl {
1544
1688
  const sessionManager = this.getSessionManagerForChat(chatId);
1545
1689
  const result = await sessionManager.interruptSession(session.id);
1546
1690
  if (!result.interrupted) {
1547
- await this.reply(chatId, `nothing is running in ${formatTelegramSessionName(result.session)}.`);
1691
+ await this.reply(chatId, `nothing is running in ${formatTelegramSessionName(result.session.id, result.session.projectId)}.`);
1548
1692
  return;
1549
1693
  }
1550
- await this.reply(chatId, `stopping ${formatTelegramSessionName(result.session)}.`);
1694
+ await this.reply(chatId, `stopping ${formatTelegramSessionName(result.session.id, result.session.projectId)}.`);
1551
1695
  }
1552
1696
  catch (error) {
1553
1697
  await this.reply(chatId, this.formatManagerError(error));
@@ -1891,8 +2035,7 @@ class TelegramAdapterImpl {
1891
2035
  return;
1892
2036
  }
1893
2037
  this.notifySession(event.sessionId, [
1894
- formatSessionHeadline(event.sessionId, "provision failed"),
1895
- `project: ${event.targetProjectId}`,
2038
+ `provisioning ${event.targetProjectId} failed in your ${formatTelegramSessionName(event.sessionId, event.projectId)}.`,
1896
2039
  truncateText(event.diagnostic, MAX_PROVISION_DIAGNOSTIC_CHARS),
1897
2040
  "the session remains available.",
1898
2041
  ].join("\n"));
@@ -1975,19 +2118,19 @@ class TelegramAdapterImpl {
1975
2118
  }
1976
2119
  this.notifySession(event.sessionId, isVerbose
1977
2120
  ? [formatSessionHeadline(event.sessionId, "assistant message"), event.progress.text].join("\n")
1978
- : event.progress.text, { rich: true });
2121
+ : event.progress.text, { rich: true, messageId: event.progress.messageId });
1979
2122
  }
1980
2123
  notifyLifecycle(sessionId, projectId, state) {
1981
- const stateLabel = {
1982
- started: "run started",
1983
- finished: "run finished",
1984
- failed: "run failed",
1985
- }[state];
2124
+ const sessionName = formatTelegramSessionName(sessionId, projectId);
1986
2125
  if (state === "failed") {
1987
- this.notifySession(sessionId, `something went wrong in ${projectId} session ${sessionId}.`);
2126
+ this.notifySession(sessionId, `something went wrong in your ${sessionName}.`);
1988
2127
  return;
1989
2128
  }
1990
- this.notifySession(sessionId, [formatSessionHeadline(sessionId, stateLabel), `project: ${projectId}`].join("\n"));
2129
+ const stateLabel = {
2130
+ started: "started a run",
2131
+ finished: "finished its run",
2132
+ }[state];
2133
+ this.notifySession(sessionId, `your ${sessionName} ${stateLabel}.`);
1991
2134
  }
1992
2135
  notifySession(sessionId, text, options = {}) {
1993
2136
  const chatIds = this.chatsBySession.get(sessionId);
@@ -1995,14 +2138,44 @@ class TelegramAdapterImpl {
1995
2138
  return;
1996
2139
  }
1997
2140
  for (const chatId of chatIds) {
1998
- void this.reply(chatId, text, { rich: options.rich }).catch((error) => {
1999
- this.log("warn", "failed to send telegram notification", {
2000
- chatId,
2001
- cause: error instanceof Error ? error.message : String(error),
2002
- });
2003
- });
2141
+ this.enqueueNotification(sessionId, chatId, text, options);
2004
2142
  }
2005
2143
  }
2144
+ enqueueNotification(sessionId, chatId, text, options) {
2145
+ const previousTask = this.notificationQueueTailByChat.get(chatId) ?? Promise.resolve();
2146
+ const queuedTask = previousTask
2147
+ .then(async () => {
2148
+ if (this.abortController.signal.aborted) {
2149
+ return;
2150
+ }
2151
+ await this.reply(chatId, text, { rich: options.rich });
2152
+ })
2153
+ .catch((error) => {
2154
+ if (this.abortController.signal.aborted) {
2155
+ return;
2156
+ }
2157
+ const deliveryError = error instanceof TelegramDeliveryError
2158
+ ? error
2159
+ : new TelegramDeliveryError(error, 1, false);
2160
+ this.log("error", deliveryError.retryable
2161
+ ? "telegram notification delivery retries exhausted"
2162
+ : "failed to send telegram notification", {
2163
+ sessionId,
2164
+ chatId,
2165
+ ...(options.rich === true ? { messageId: options.messageId } : {}),
2166
+ attempts: deliveryError.attempts,
2167
+ cause: deliveryError.message,
2168
+ });
2169
+ });
2170
+ const trackedTask = queuedTask.finally(() => {
2171
+ this.inFlightNotificationTasks.delete(trackedTask);
2172
+ if (this.notificationQueueTailByChat.get(chatId) === trackedTask) {
2173
+ this.notificationQueueTailByChat.delete(chatId);
2174
+ }
2175
+ });
2176
+ this.notificationQueueTailByChat.set(chatId, trackedTask);
2177
+ this.inFlightNotificationTasks.add(trackedTask);
2178
+ }
2006
2179
  startTypingIndicators(sessionId) {
2007
2180
  const chatIds = this.chatsBySession.get(sessionId);
2008
2181
  if (!chatIds) {
@@ -2079,7 +2252,7 @@ class TelegramAdapterImpl {
2079
2252
  }
2080
2253
  }
2081
2254
  formatSessionReady(sessionId, projectId) {
2082
- return `all set, your ${projectId} session ${sessionId} is ready.`;
2255
+ return `all set, your ${formatTelegramSessionName(sessionId, projectId)} is ready.`;
2083
2256
  }
2084
2257
  formatMessageQueued(sessionId) {
2085
2258
  return formatSessionHeadline(sessionId, "message queued");
@@ -2115,9 +2288,13 @@ class TelegramAdapterImpl {
2115
2288
  async replyWithRichMessage(chatId, text, replyMarkup) {
2116
2289
  const chunks = splitTelegramRichMessage(text);
2117
2290
  for (const [index, chunk] of chunks.entries()) {
2118
- await this.api.sendRichMessage(chatId, chunk, {
2291
+ const result = await this.sendWithRetry((signal) => this.api.sendRichMessage(chatId, chunk, {
2119
2292
  replyMarkup: index === chunks.length - 1 ? replyMarkup : undefined,
2120
- });
2293
+ signal,
2294
+ }));
2295
+ if (result === ABORTED) {
2296
+ return;
2297
+ }
2121
2298
  if (index < chunks.length - 1) {
2122
2299
  await this.wait(TELEGRAM_MESSAGE_SPLIT_DELAY_MS);
2123
2300
  }
@@ -2126,14 +2303,89 @@ class TelegramAdapterImpl {
2126
2303
  async replyWithPlainMessage(chatId, text, replyMarkup) {
2127
2304
  const chunks = splitTelegramMessage(text);
2128
2305
  for (const [index, chunk] of chunks.entries()) {
2129
- await this.api.sendMessage(chatId, chunk, {
2306
+ const result = await this.sendWithRetry((signal) => this.api.sendMessage(chatId, chunk, {
2130
2307
  replyMarkup: index === chunks.length - 1 ? replyMarkup : undefined,
2131
- });
2308
+ signal,
2309
+ }));
2310
+ if (result === ABORTED) {
2311
+ return;
2312
+ }
2132
2313
  if (index < chunks.length - 1) {
2133
2314
  await this.wait(TELEGRAM_MESSAGE_SPLIT_DELAY_MS);
2134
2315
  }
2135
2316
  }
2136
2317
  }
2318
+ async sendWithRetry(send) {
2319
+ for (let attempt = 1;; attempt += 1) {
2320
+ try {
2321
+ const result = await this.runDeliveryAttempt(send);
2322
+ if (result === ABORTED) {
2323
+ return ABORTED;
2324
+ }
2325
+ return;
2326
+ }
2327
+ catch (error) {
2328
+ if (this.abortController.signal.aborted) {
2329
+ return ABORTED;
2330
+ }
2331
+ const retryDelayMs = TELEGRAM_DELIVERY_RETRY_DELAYS_MS[attempt - 1];
2332
+ const retryable = error instanceof TelegramRequestError && error.retryable;
2333
+ if (!retryable || retryDelayMs === undefined) {
2334
+ throw new TelegramDeliveryError(error, attempt, retryable);
2335
+ }
2336
+ await this.wait(Math.max(retryDelayMs, error.retryAfterMs ?? 0));
2337
+ if (this.abortController.signal.aborted) {
2338
+ return ABORTED;
2339
+ }
2340
+ }
2341
+ }
2342
+ }
2343
+ async runDeliveryAttempt(send) {
2344
+ if (this.abortController.signal.aborted) {
2345
+ return ABORTED;
2346
+ }
2347
+ const attemptController = new AbortController();
2348
+ let abortListener;
2349
+ const abortPromise = new Promise((resolve) => {
2350
+ abortListener = () => {
2351
+ resolve(ABORTED);
2352
+ attemptController.abort();
2353
+ };
2354
+ this.abortController.signal.addEventListener("abort", abortListener, { once: true });
2355
+ });
2356
+ let timeout;
2357
+ const timeoutPromise = new Promise((resolve) => {
2358
+ timeout = setTimeout(() => {
2359
+ resolve(TIMED_OUT);
2360
+ attemptController.abort();
2361
+ }, TELEGRAM_DELIVERY_ATTEMPT_TIMEOUT_MS);
2362
+ timeout.unref?.();
2363
+ });
2364
+ let sendPromise;
2365
+ try {
2366
+ sendPromise = send(attemptController.signal);
2367
+ }
2368
+ catch (error) {
2369
+ sendPromise = Promise.reject(error);
2370
+ }
2371
+ const settled = sendPromise.then(() => ({ ok: true }), (error) => ({ ok: false, error }));
2372
+ const raceResult = await Promise.race([settled, abortPromise, timeoutPromise]);
2373
+ if (timeout) {
2374
+ clearTimeout(timeout);
2375
+ }
2376
+ if (abortListener) {
2377
+ this.abortController.signal.removeEventListener("abort", abortListener);
2378
+ }
2379
+ if (raceResult === ABORTED) {
2380
+ return ABORTED;
2381
+ }
2382
+ if (raceResult === TIMED_OUT) {
2383
+ throw new TelegramRequestError(`telegram delivery attempt timed out after ${TELEGRAM_DELIVERY_ATTEMPT_TIMEOUT_MS / 1000} seconds`, { retryable: true });
2384
+ }
2385
+ if (!raceResult.ok) {
2386
+ throw raceResult.error;
2387
+ }
2388
+ }
2137
2389
  async wait(durationMs) {
2138
2390
  if (durationMs <= 0 || this.abortController.signal.aborted) {
2139
2391
  return;