arisa 4.3.4 → 5.0.2

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 (41) hide show
  1. package/AGENTS.md +18 -17
  2. package/README.md +30 -9
  3. package/package.json +6 -2
  4. package/pnpm-workspace.yaml +1 -0
  5. package/src/core/agent/agent-manager.js +288 -29
  6. package/src/core/agent/auth-flow.js +12 -8
  7. package/src/core/agent/model-selection.js +54 -14
  8. package/src/core/agent/model-speed.js +59 -0
  9. package/src/core/config/config-defaults.js +56 -4
  10. package/src/core/config/config-store.js +5 -1
  11. package/src/core/conversation/conversation-history-store.js +142 -0
  12. package/src/core/tasks/task-store.js +16 -0
  13. package/src/core/tools/daemon-health.js +11 -2
  14. package/src/core/tools/daemon-processes.js +92 -2
  15. package/src/core/tools/daemon-runtime.js +4 -2
  16. package/src/core/tools/ipc-client.js +15 -3
  17. package/src/core/tools/tool-registry.js +27 -0
  18. package/src/index.js +61 -6
  19. package/src/runtime/arisa-capabilities.js +45 -1
  20. package/src/runtime/bootstrap.js +3 -2
  21. package/src/runtime/create-app.js +47 -11
  22. package/src/runtime/doctor.js +307 -0
  23. package/src/runtime/log-viewer.js +165 -0
  24. package/src/runtime/paths.js +4 -1
  25. package/src/runtime/service-manager.js +106 -8
  26. package/src/runtime/tool-process-supervisor.js +107 -10
  27. package/src/transport/telegram/bot.js +533 -99
  28. package/src/transport/telegram/model-picker.js +28 -2
  29. package/test/agent-tool-policy.test.js +26 -1
  30. package/test/auth-flow.test.js +28 -2
  31. package/test/capabilities-security.test.js +37 -0
  32. package/test/context-and-task-bounds.test.js +279 -0
  33. package/test/daemon-runtime.test.js +130 -2
  34. package/test/dependency-warnings.test.js +17 -0
  35. package/test/doctor.test.js +90 -0
  36. package/test/log-viewer.test.js +90 -0
  37. package/test/model-selection.test.js +125 -2
  38. package/test/paths.test.js +8 -0
  39. package/test/pi-compaction.test.js +43 -0
  40. package/test/service-manager.test.js +234 -0
  41. package/test/task-store.test.js +31 -0
@@ -3,16 +3,58 @@ import path from "node:path";
3
3
  import { authorizeChat } from "./auth.js";
4
4
  import { captureIncomingArtifact, formatLocationText } from "./media.js";
5
5
  import { buildDeviceCodeTelegramMessage } from "./device-code-message.js";
6
- import { buildEffortPicker, buildModelPicker, parseEffortPickerAction, parseModelPickerAction } from "./model-picker.js";
6
+ import { buildEffortPicker, buildModelPicker, buildSpeedPicker, parseEffortPickerAction, parseModelPickerAction, parseSpeedPickerAction, reverseModelOrder } from "./model-picker.js";
7
7
  import { renderTelegramHtml } from "./text-format.js";
8
8
  import { buildPiAuthRecoveryBlockedMessage, buildPiAuthTelegramMessage, getErrorMessage, getPiAuthIssue, getPiAuthStatus } from "../../core/agent/auth-flow.js";
9
9
  import { createPiOAuthLogin } from "../../core/agent/pi-auth-login.js";
10
- import { resolveChatModel, resolveChatThinkingLevel, selectChatModel, selectChatThinkingLevel } from "../../core/agent/model-selection.js";
10
+ import { getAgentConfig, resolveChatModel, resolveChatSpeed, resolveChatThinkingLevel, selectChatModel, selectChatSpeed, selectChatThinkingLevel } from "../../core/agent/model-selection.js";
11
11
  import { clampModelThinkingLevel, createPiRuntime, listModelThinkingLevels, listProviderModels, modelSupportsThinking } from "../../core/agent/pi-runtime.js";
12
+ import { clampModelSpeed, MODEL_SPEEDS, modelSupportsSpeed } from "../../core/agent/model-speed.js";
12
13
  import { normalizeArtifactForReasoning, shouldNormalizeArtifactToText } from "../../core/artifacts/normalize-for-reasoning.js";
14
+ import { formatPortableSessionHistory } from "../../core/agent/agent-manager.js";
15
+ import { ConversationHistoryStore } from "../../core/conversation/conversation-history-store.js";
16
+ import { formatDoctorReport } from "../../runtime/doctor.js";
13
17
 
14
18
  const slowPromptNoticeMs = 300_000;
15
19
 
20
+ export const telegramCommands = Object.freeze([
21
+ { command: "new", description: "Start a new chat context" },
22
+ { command: "restart", description: "Restart the Arisa service" },
23
+ { command: "doctor", description: "Check and repair Arisa runtime health" },
24
+ { command: "model", description: "Choose the model for this chat" },
25
+ { command: "effort", description: "Choose reasoning effort for this chat" },
26
+ { command: "speed", description: "Choose model speed for this chat" },
27
+ { command: "auth", description: "Show authentication status" }
28
+ ]);
29
+
30
+ export function createTelegramRestartHandler({ authorize, requestRestart, logger }) {
31
+ if (typeof authorize !== "function" || typeof requestRestart !== "function") {
32
+ throw new Error("Telegram restart requires authorization and restart handoff functions");
33
+ }
34
+
35
+ let restartRequested = false;
36
+ return async (ctx) => {
37
+ const auth = await authorize(ctx);
38
+ if (!auth.ok) return;
39
+
40
+ if (restartRequested) {
41
+ await ctx.reply("An Arisa restart is already in progress.");
42
+ return;
43
+ }
44
+
45
+ restartRequested = true;
46
+ try {
47
+ await ctx.reply("Arisa is restarting. I'll be back shortly.");
48
+ const handoff = await requestRestart();
49
+ logger?.log("telegram", `restart handed off to process ${handoff.pid}`);
50
+ } catch (error) {
51
+ restartRequested = false;
52
+ logger?.error("telegram", `restart handoff failed: ${getErrorMessage(error)}`);
53
+ await ctx.reply(`Arisa could not be restarted: ${getErrorMessage(error)}`);
54
+ }
55
+ };
56
+ }
57
+
16
58
  function quotedMessageSummary(message) {
17
59
  if (!message) return [];
18
60
 
@@ -219,12 +261,13 @@ function buildStartupMessage(chatMeta = {}) {
219
261
  return "Arisa is back online.";
220
262
  }
221
263
 
222
- async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {}) {
264
+ export async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {}) {
223
265
  let text = "";
224
266
  let assistantErrorMessage = "";
225
267
  let shouldSeparateAssistantMessage = false;
226
268
  let slowPromptTimer = null;
227
269
  const unsubscribe = session.subscribe((event) => {
270
+ if (event.arisaPromptScoped === false) return;
228
271
  if (event.type === "message_start" && event.message.role === "assistant") {
229
272
  shouldSeparateAssistantMessage = text.trim().length > 0;
230
273
  }
@@ -235,8 +278,13 @@ async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {
235
278
  }
236
279
  text += event.assistantMessageEvent.delta;
237
280
  }
238
- if (event.type === "message_end" && event.message?.stopReason === "error") {
239
- assistantErrorMessage = event.message.errorMessage || "assistant message ended with error";
281
+ if (event.type === "message_end" && event.message?.role === "assistant") {
282
+ if (event.message.stopReason === "error") {
283
+ assistantErrorMessage = event.message.errorMessage || "assistant message ended with error";
284
+ } else if (event.message.stopReason !== "aborted") {
285
+ // Auto-compaction and retry can emit a transient error before a successful continuation.
286
+ assistantErrorMessage = "";
287
+ }
240
288
  }
241
289
  const logMessage = sessionEventLogMessage(event);
242
290
  if (logMessage) logger?.log("agent", `chat ${chatId} ${logMessage}`);
@@ -265,6 +313,32 @@ async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {
265
313
  return text.trim();
266
314
  }
267
315
 
316
+ export function isSilentReply(text) {
317
+ return /^(?:NO_REPLY|No reply needed\.|No action needed\.)(?:\s+(?:NO_REPLY|No reply needed\.|No action needed\.))*$/.test(String(text || "").trim());
318
+ }
319
+
320
+ function buildSessionHandoffPrompt() {
321
+ return [
322
+ "Prepare a concise handoff for the next Arisa session.",
323
+ "Review the entire active session, including any previous compaction summaries and the latest messages.",
324
+ "Keep only durable context: current goals or projects, decisions, user preferences, unresolved tasks, and important facts needed to continue.",
325
+ "Use at most 8 short bullets and at most 1600 characters.",
326
+ "Exclude secrets, tokens, passwords, cookies, API keys, private file paths, full transcripts, and stale chatter.",
327
+ "Do not take actions, call tools, send messages, or explain the process.",
328
+ "Return only the handoff."
329
+ ].join("\n");
330
+ }
331
+
332
+ function sanitizeSessionHandoff(text) {
333
+ const sanitized = String(text || "")
334
+ .replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi, "[redacted private key]")
335
+ .replace(/\b(?:sk-[A-Za-z0-9_-]{12,}|gh[opsu]_[A-Za-z0-9_-]{12,}|Bearer\s+[A-Za-z0-9._-]+)\b/gi, "[redacted credential]")
336
+ .replace(/(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|client[_ -]?secret|password|cookie|secret)\s*[:=]\s*[^\s,;]+/gi, "[redacted credential]")
337
+ .trim();
338
+ if (sanitized.length <= 4000) return sanitized;
339
+ return `${sanitized.slice(0, 3997).trim()}...`;
340
+ }
341
+
268
342
  async function withTyping(ctx, work) {
269
343
  await ctx.api.sendChatAction(ctx.chat.id, "typing");
270
344
  const timer = setInterval(() => {
@@ -278,14 +352,151 @@ async function withTyping(ctx, work) {
278
352
  }
279
353
  }
280
354
 
281
- export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger }) {
355
+ export function createChatStateStore() {
356
+ const states = new Map();
357
+
358
+ function reset(chatId) {
359
+ const state = {
360
+ processing: false,
361
+ pendingPrompts: [],
362
+ continueAfterClose: false,
363
+ historyRevision: 0,
364
+ beforeNextPrompt: null,
365
+ activeSession: null,
366
+ activeSteers: []
367
+ };
368
+ states.set(String(chatId), state);
369
+ return state;
370
+ }
371
+
372
+ return {
373
+ get(chatId) {
374
+ const key = String(chatId);
375
+ return states.get(key) || reset(key);
376
+ },
377
+ reset,
378
+ anyProcessing() {
379
+ return [...states.values()].some((state) => state.processing);
380
+ }
381
+ };
382
+ }
383
+
384
+ export function queueChatPrompt(chatState, prompt, { replace = false } = {}) {
385
+ if (replace) chatState.pendingPrompts = [];
386
+ chatState.pendingPrompts.push(prompt);
387
+ }
388
+
389
+ function takeQueuedPrompt(chatState) {
390
+ return chatState.pendingPrompts.shift() || "";
391
+ }
392
+
393
+ export function resolveTelegramBusyMessageMode(config, chatId) {
394
+ const chatMode = config.telegram?.chatMeta?.[String(chatId)]?.busyMessageMode;
395
+ const mode = chatMode || config.telegram?.busyMessageMode;
396
+ return mode === "steer" ? "steer" : "queue";
397
+ }
398
+
399
+ export async function routeBusyPrompt({ chatState, prompt, mode = "queue", replaceQueued = false }) {
400
+ const session = chatState.activeSession;
401
+ if (
402
+ mode === "steer"
403
+ && !replaceQueued
404
+ && !chatState.continueAfterClose
405
+ && !chatState.beforeNextPrompt
406
+ && session?.isStreaming
407
+ && typeof session.steer === "function"
408
+ ) {
409
+ try {
410
+ await session.steer(prompt);
411
+ chatState.activeSteers.push(prompt);
412
+ return { disposition: "steered" };
413
+ } catch (error) {
414
+ queueChatPrompt(chatState, prompt);
415
+ return { disposition: "queued", steerError: error };
416
+ }
417
+ }
418
+
419
+ queueChatPrompt(chatState, prompt, { replace: replaceQueued });
420
+ return { disposition: "queued" };
421
+ }
422
+
423
+ export async function drainChatPromptQueue({
424
+ chatState,
425
+ initialPrompt,
426
+ initialCtx = null,
427
+ processPrompt,
428
+ onPromptFailure,
429
+ onPromptInterrupted,
430
+ beforeInitialPrompt
431
+ }) {
432
+ let currentPrompt = initialPrompt;
433
+ let currentCtx = initialCtx;
434
+
435
+ try {
436
+ await beforeInitialPrompt?.();
437
+ while (currentPrompt) {
438
+ while (chatState.beforeNextPrompt) {
439
+ const gate = chatState.beforeNextPrompt;
440
+ await gate;
441
+ if (chatState.beforeNextPrompt === gate) chatState.beforeNextPrompt = null;
442
+ }
443
+ if (chatState.continueAfterClose && chatState.pendingPrompts.length) {
444
+ currentPrompt = takeQueuedPrompt(chatState);
445
+ chatState.continueAfterClose = false;
446
+ currentCtx = null;
447
+ }
448
+ try {
449
+ await processPrompt({ prompt: currentPrompt, ctx: currentCtx });
450
+ } catch (error) {
451
+ if (chatState.continueAfterClose && chatState.pendingPrompts.length) {
452
+ await onPromptInterrupted?.(error);
453
+ } else {
454
+ await onPromptFailure?.(error);
455
+ throw error;
456
+ }
457
+ } finally {
458
+ currentCtx = null;
459
+ }
460
+
461
+ currentPrompt = takeQueuedPrompt(chatState);
462
+ chatState.continueAfterClose = false;
463
+ }
464
+ } finally {
465
+ chatState.processing = false;
466
+ chatState.activeSession = null;
467
+ chatState.activeSteers = [];
468
+ }
469
+ }
470
+
471
+ export async function closeModelPicker(ctx, { messageText, callbackText }) {
472
+ await ctx.api.editMessageText(
473
+ ctx.chat.id,
474
+ ctx.callbackQuery.message.message_id,
475
+ messageText
476
+ );
477
+ await ctx.answerCallbackQuery({ text: callbackText });
478
+ }
479
+
480
+ export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, doctor, requestRestart, logger }) {
282
481
  const bot = new Bot(config.telegram.token);
283
- const perChatState = new Map();
482
+ const perChatState = createChatStateStore();
483
+ const conversationHistory = new ConversationHistoryStore();
284
484
  const notifiedPromptErrors = new WeakSet();
285
485
  const authRenewals = new Map();
286
486
  let piAuthIssue = null;
287
487
  let taskTimer = null;
288
488
 
489
+ const handleRestartCommand = createTelegramRestartHandler({
490
+ authorize: (ctx) => authorizeChat({
491
+ config,
492
+ chatId: ctx.chat.id,
493
+ saveConfig,
494
+ chatMeta: getIncomingChatMeta(ctx)
495
+ }),
496
+ requestRestart,
497
+ logger
498
+ });
499
+
289
500
  function chatKey(chatId) {
290
501
  return String(chatId);
291
502
  }
@@ -309,7 +520,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
309
520
  if (!issue) return false;
310
521
 
311
522
  try {
312
- await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, issue }));
523
+ await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, issue }));
313
524
  markPromptErrorNotified(error);
314
525
  return true;
315
526
  } catch (notifyError) {
@@ -328,16 +539,16 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
328
539
  async function finishAuthRenewal(chatId, renewal) {
329
540
  try {
330
541
  await renewal.promise;
331
- await agentManager.validatePiAgent();
542
+ await agentManager.validateAgent();
332
543
  agentManager.clearSessionCache(chatId);
333
544
  piAuthIssue = null;
334
545
  logger?.log("telegram", `Pi auth renewal completed for chat ${chatId}`);
335
- await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, verified: true }));
546
+ await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, verified: true }));
336
547
  } catch (error) {
337
548
  const issue = rememberPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
338
549
  piAuthIssue = issue;
339
550
  logger?.error("telegram", `Pi auth renewal failed for chat ${chatId}: ${getErrorMessage(error)}`);
340
- await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, issue })).catch((notifyError) => {
551
+ await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, issue })).catch((notifyError) => {
341
552
  logger?.error("telegram", `auth renewal failure notice failed for chat ${chatId}: ${getErrorMessage(notifyError)}`);
342
553
  });
343
554
  } finally {
@@ -407,26 +618,25 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
407
618
  }
408
619
 
409
620
  function getChatState(chatId) {
410
- if (!perChatState.has(chatId)) {
411
- perChatState.set(chatId, { processing: false, nextPrompt: "" });
412
- }
413
621
  return perChatState.get(chatId);
414
622
  }
415
623
 
416
- function getProviderModels() {
624
+ async function getProviderModels(chatId) {
417
625
  const runtime = createPiRuntime({
418
626
  provider: config.pi.provider,
419
627
  apiKey: config.pi.apiKey
420
628
  });
421
- return listProviderModels(config.pi.provider, runtime);
629
+ return reverseModelOrder(listProviderModels(config.pi.provider, runtime));
422
630
  }
423
631
 
424
632
  async function showModelPicker(ctx, page = 0) {
633
+ const agentConfig = getAgentConfig(config);
425
634
  const picker = buildModelPicker({
426
- provider: config.pi.provider,
427
- models: getProviderModels(),
635
+ provider: agentConfig.provider,
636
+ models: await getProviderModels(ctx.chat.id),
428
637
  selectedModelId: resolveChatModel(config, ctx.chat.id),
429
638
  selectedThinkingLevel: resolveChatThinkingLevel(config, ctx.chat.id),
639
+ selectedSpeed: resolveChatSpeed(config, ctx.chat.id),
430
640
  page,
431
641
  pageSize: config.telegram.modelPickerPageSize
432
642
  });
@@ -439,10 +649,11 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
439
649
  }
440
650
 
441
651
  async function showEffortPicker(ctx, { model, modelIndex, selectedThinkingLevel } = {}) {
442
- const models = getProviderModels();
652
+ const agentConfig = getAgentConfig(config);
653
+ const models = await getProviderModels(ctx.chat.id);
443
654
  const resolvedModel = model || models.find((item) => item.id === resolveChatModel(config, ctx.chat.id));
444
655
  if (!resolvedModel) {
445
- throw new Error(`Model not found for provider ${config.pi.provider}`);
656
+ throw new Error(`Model not found for provider ${agentConfig.provider}`);
446
657
  }
447
658
  if (!modelSupportsThinking(resolvedModel)) {
448
659
  const text = `${resolvedModel.provider}/${resolvedModel.id} does not support effort levels.`;
@@ -468,20 +679,46 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
468
679
  return ctx.reply(picker.text, extra);
469
680
  }
470
681
 
682
+ async function showSpeedPicker(ctx) {
683
+ const agentConfig = getAgentConfig(config);
684
+ const models = await getProviderModels(ctx.chat.id);
685
+ const model = models.find((item) => item.id === resolveChatModel(config, ctx.chat.id));
686
+ if (!model) throw new Error(`Model not found for provider ${agentConfig.provider}`);
687
+ if (!modelSupportsSpeed(model)) {
688
+ const text = `${model.provider}/${model.id} does not support speed 1.5x.`;
689
+ if (ctx.callbackQuery?.message?.message_id) {
690
+ return ctx.api.editMessageText(ctx.chat.id, ctx.callbackQuery.message.message_id, text);
691
+ }
692
+ return ctx.reply(text);
693
+ }
694
+ const picker = buildSpeedPicker({
695
+ provider: model.provider,
696
+ modelId: model.id,
697
+ speeds: MODEL_SPEEDS,
698
+ selectedSpeed: resolveChatSpeed(config, ctx.chat.id)
699
+ });
700
+ const extra = { reply_markup: picker.replyMarkup };
701
+ const messageId = ctx.callbackQuery?.message?.message_id;
702
+ if (messageId) return ctx.api.editMessageText(ctx.chat.id, messageId, picker.text, extra);
703
+ return ctx.reply(picker.text, extra);
704
+ }
705
+
471
706
  async function persistChatModel(chatId, model, thinkingLevel) {
707
+ const agentConfig = getAgentConfig(config);
472
708
  const key = chatKey(chatId);
473
- const hadSelections = Boolean(config.pi.chatModels);
474
- const previousSelection = config.pi.chatModels?.[key];
709
+ const hadSelections = Boolean(agentConfig.chatModels);
710
+ const previousSelection = agentConfig.chatModels?.[key];
475
711
  const level = clampModelThinkingLevel(model, thinkingLevel ?? resolveChatThinkingLevel(config, chatId));
476
- selectChatModel(config, chatId, model, { thinkingLevel: level });
712
+ const speed = clampModelSpeed(model, resolveChatSpeed(config, chatId));
713
+ selectChatModel(config, chatId, model, { thinkingLevel: level, speed });
477
714
  try {
478
715
  await saveConfig(config);
479
716
  } catch (error) {
480
717
  if (previousSelection) {
481
- config.pi.chatModels[key] = previousSelection;
718
+ agentConfig.chatModels[key] = previousSelection;
482
719
  } else {
483
- delete config.pi.chatModels[key];
484
- if (!hadSelections) delete config.pi.chatModels;
720
+ delete agentConfig.chatModels[key];
721
+ if (!hadSelections) delete agentConfig.chatModels;
485
722
  }
486
723
  throw error;
487
724
  }
@@ -490,20 +727,44 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
490
727
  }
491
728
 
492
729
  async function persistChatEffort(chatId, model, thinkingLevel) {
730
+ const agentConfig = getAgentConfig(config);
493
731
  const key = chatKey(chatId);
494
- const hadSelections = Boolean(config.pi.chatModels);
495
- const previousSelection = config.pi.chatModels?.[key];
732
+ const hadSelections = Boolean(agentConfig.chatModels);
733
+ const previousSelection = agentConfig.chatModels?.[key];
496
734
  const level = clampModelThinkingLevel(model, thinkingLevel);
497
735
  selectChatThinkingLevel(config, chatId, level);
498
736
  try {
499
737
  await saveConfig(config);
500
738
  } catch (error) {
501
739
  if (previousSelection) {
502
- config.pi.chatModels[key] = previousSelection;
740
+ agentConfig.chatModels[key] = previousSelection;
741
+ } else {
742
+ delete agentConfig.chatModels[key];
743
+ if (!hadSelections) delete agentConfig.chatModels;
744
+ }
745
+ throw error;
746
+ }
747
+ return level;
748
+ }
749
+
750
+ async function persistChatSpeed(chatId, model, speed) {
751
+ const agentConfig = getAgentConfig(config);
752
+ const key = chatKey(chatId);
753
+ const hadSelections = Boolean(agentConfig.chatModels);
754
+ const previousSelection = agentConfig.chatModels?.[key];
755
+ const level = clampModelSpeed(model, speed);
756
+ await agentManager.setModelSpeed(chatId, level);
757
+ selectChatSpeed(config, chatId, level);
758
+ try {
759
+ await saveConfig(config);
760
+ } catch (error) {
761
+ if (previousSelection) {
762
+ agentConfig.chatModels[key] = previousSelection;
503
763
  } else {
504
- delete config.pi.chatModels[key];
505
- if (!hadSelections) delete config.pi.chatModels;
764
+ delete agentConfig.chatModels[key];
765
+ if (!hadSelections) delete agentConfig.chatModels;
506
766
  }
767
+ agentManager.clearSessionCache(chatId);
507
768
  throw error;
508
769
  }
509
770
  return level;
@@ -526,6 +787,11 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
526
787
  async function sendTextReply({ sendText, sendDocument, chatId, text }) {
527
788
  const maxInlineReplyLength = 3500;
528
789
 
790
+ if (isSilentReply(text)) {
791
+ logger?.log("telegram", `suppressing silent reply for chat ${chatId}`);
792
+ return;
793
+ }
794
+
529
795
  if (text.length > maxInlineReplyLength) {
530
796
  logger?.log("telegram", `sending long reply as markdown attachment for chat ${chatId}`);
531
797
  const chatArtifactStore = artifactStore.forChat(chatId);
@@ -561,10 +827,35 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
561
827
  };
562
828
  }
563
829
 
830
+ agentManager.setArtifactDeliveryHandler?.(async ({ chatId, artifact, caption, method }) => {
831
+ const resolvedMethod = method
832
+ || artifact.metadata?.delivery?.method
833
+ || (artifact.kind === "audio" || artifact.mimeType?.startsWith("audio/") ? "audio"
834
+ : artifact.kind === "image" || artifact.mimeType?.startsWith("image/") ? "photo"
835
+ : artifact.kind === "video" || artifact.mimeType?.startsWith("video/") ? "video"
836
+ : "document");
837
+ const safeCaption = caption && !/(^|\s)(\/[^\s]|[A-Za-z]:[\\/])/.test(caption) ? caption : undefined;
838
+ await createTelegramSessionBridge(chatId).sendMedia(artifact.path, {
839
+ method: resolvedMethod,
840
+ caption: safeCaption,
841
+ filename: path.basename(artifact.path)
842
+ });
843
+ return { ok: true, artifactId: artifact.id, method: resolvedMethod };
844
+ });
845
+
564
846
  async function processPromptForChat({ chatId, prompt, ctx = null }) {
565
847
  const work = async () => {
566
848
  const { session } = await agentManager.getSessionContext(chatId, createTelegramSessionBridge(chatId));
849
+ const historyRevision = getChatState(chatId).historyRevision;
850
+ await conversationHistory.ensureSeed(chatId, {
851
+ runtime: "pi",
852
+ history: formatPortableSessionHistory(session.messages)
853
+ });
567
854
  let text = "";
855
+ let steeredPrompts = [];
856
+ const chatState = getChatState(chatId);
857
+ chatState.activeSession = session;
858
+ chatState.activeSteers = [];
568
859
  try {
569
860
  text = await collectText(session, prompt, {
570
861
  logger,
@@ -577,6 +868,20 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
577
868
  } catch (error) {
578
869
  agentManager.resetSession(chatId);
579
870
  throw error;
871
+ } finally {
872
+ steeredPrompts = [...chatState.activeSteers];
873
+ if (chatState.activeSession === session) chatState.activeSession = null;
874
+ chatState.activeSteers = [];
875
+ }
876
+ if (getChatState(chatId).historyRevision === historyRevision) {
877
+ const historyPrompt = steeredPrompts.length
878
+ ? [prompt, ...steeredPrompts.map((message) => `[Steering message]\n${message}`)].join("\n\n")
879
+ : prompt;
880
+ await conversationHistory.appendTurn(chatId, {
881
+ runtime: "pi",
882
+ prompt: historyPrompt,
883
+ response: text
884
+ });
580
885
  }
581
886
  if (text) {
582
887
  await sendTextReply({
@@ -592,46 +897,53 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
592
897
  return work();
593
898
  }
594
899
 
595
- async function enqueuePrompt({ chatId, prompt, label, ctx = null }) {
900
+ async function enqueuePrompt({ chatId, prompt, label, ctx = null, replaceQueued = false, busyMessageMode = "queue" }) {
596
901
  const chatState = getChatState(chatId);
597
902
 
598
903
  if (chatState.processing) {
599
- logger?.log("telegram", `chat ${chatId} busy, queueing ${label}`);
600
- chatState.nextPrompt = chatState.nextPrompt
601
- ? `${chatState.nextPrompt}\n\n${prompt}`
602
- : prompt;
904
+ const routed = await routeBusyPrompt({
905
+ chatState,
906
+ prompt,
907
+ mode: busyMessageMode,
908
+ replaceQueued
909
+ });
910
+ if (routed.disposition === "steered") {
911
+ logger?.log("telegram", `chat ${chatId} busy, steering ${label}`);
912
+ } else {
913
+ logger?.log("telegram", `chat ${chatId} busy, queueing ${label}`);
914
+ if (routed.steerError) {
915
+ logger?.log("telegram", `steer failed for chat ${chatId}, queued instead: ${getErrorMessage(routed.steerError)}`);
916
+ }
917
+ }
918
+ if (replaceQueued) chatState.continueAfterClose = true;
603
919
  return;
604
920
  }
605
921
 
606
922
  chatState.processing = true;
607
923
  logger?.log("telegram", `processing ${label} in chat ${chatId}`);
608
- let currentPrompt = prompt;
609
- let currentCtx = ctx;
610
-
611
- try {
612
- while (currentPrompt) {
613
- try {
614
- logger?.log("telegram", `prompt dispatch for chat ${chatId}`);
615
- await processPromptForChat({ chatId, prompt: currentPrompt, ctx: currentCtx });
616
- } catch (error) {
617
- const message = getErrorMessage(error);
618
- logger?.error("telegram", `${label} failed for chat ${chatId}: ${message}`);
619
- await notifyPiAuthIssueIfNeeded(chatId, error);
620
- throw error;
621
- } finally {
622
- currentCtx = null;
623
- }
924
+ return processChatPromptQueue({ chatId, prompt, label, ctx });
925
+ }
624
926
 
625
- if (chatState.nextPrompt) {
626
- currentPrompt = chatState.nextPrompt;
627
- chatState.nextPrompt = "";
628
- } else {
629
- currentPrompt = "";
630
- }
927
+ function processChatPromptQueue({ chatId, prompt, label, ctx = null, beforeInitialPrompt }) {
928
+ const chatState = getChatState(chatId);
929
+ return drainChatPromptQueue({
930
+ chatState,
931
+ initialPrompt: prompt,
932
+ initialCtx: ctx,
933
+ beforeInitialPrompt,
934
+ processPrompt: ({ prompt: currentPrompt, ctx: currentCtx }) => {
935
+ logger?.log("telegram", `prompt dispatch for chat ${chatId}`);
936
+ return processPromptForChat({ chatId, prompt: currentPrompt, ctx: currentCtx });
937
+ },
938
+ onPromptInterrupted: (error) => {
939
+ logger?.log("telegram", `${label} interrupted by queued /new for chat ${chatId}: ${getErrorMessage(error)}`);
940
+ },
941
+ onPromptFailure: async (error) => {
942
+ const message = getErrorMessage(error);
943
+ logger?.error("telegram", `${label} failed for chat ${chatId}: ${message}`);
944
+ await notifyPiAuthIssueIfNeeded(chatId, error);
631
945
  }
632
- } finally {
633
- chatState.processing = false;
634
- }
946
+ });
635
947
  }
636
948
 
637
949
  async function enqueueOrProcess(ctx) {
@@ -639,10 +951,14 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
639
951
 
640
952
  if (chatState.processing) {
641
953
  const incomingPrompt = await buildIncomingPrompt(ctx);
954
+ const busyMessageMode = typeof ctx.message?.text === "string"
955
+ ? resolveTelegramBusyMessageMode(config, ctx.chat.id)
956
+ : "queue";
642
957
  return enqueuePrompt({
643
958
  chatId: ctx.chat.id,
644
959
  prompt: incomingPrompt,
645
- label: `message ${ctx.msg.message_id}`
960
+ label: `message ${ctx.msg.message_id}`,
961
+ busyMessageMode
646
962
  });
647
963
  }
648
964
 
@@ -747,14 +1063,61 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
747
1063
  }
748
1064
  }
749
1065
 
1066
+ async function summarizeSessionBeforeReset(chatId) {
1067
+ try {
1068
+ const context = await agentManager.getSessionContext(chatId, createTelegramSessionBridge(chatId));
1069
+ const parentSession = context.session.sessionFile || "";
1070
+ if (!context.session.messages.length) return { handoff: "", parentSession: "" };
1071
+
1072
+ const summary = await collectText(context.session, buildSessionHandoffPrompt(), { logger, chatId });
1073
+ return { handoff: sanitizeSessionHandoff(summary), parentSession };
1074
+ } catch (error) {
1075
+ logger?.log("agent", `session handoff summary failed for chat ${chatId}: ${getErrorMessage(error)}`);
1076
+ return { handoff: "", parentSession: "" };
1077
+ }
1078
+ }
1079
+
750
1080
  async function handleNewCommand(ctx) {
751
- agentManager.resetSession(ctx.chat.id);
752
- perChatState.set(ctx.chat.id, { processing: false, nextPrompt: "" });
753
- await enqueuePrompt({
1081
+ const chatState = getChatState(ctx.chat.id);
1082
+ const wasProcessing = chatState.processing;
1083
+ chatState.historyRevision += 1;
1084
+ const commandRevision = chatState.historyRevision;
1085
+ const prompt = buildNewSessionPrompt(ctx);
1086
+
1087
+ if (wasProcessing) {
1088
+ logger?.log("telegram", `chat ${ctx.chat.id} busy, queueing new-session command`);
1089
+ queueChatPrompt(chatState, prompt, { replace: true });
1090
+ chatState.continueAfterClose = true;
1091
+ const reset = (async () => {
1092
+ await conversationHistory.reset(ctx.chat.id, { runtime: "pi" });
1093
+ agentManager.resetSession(ctx.chat.id);
1094
+ })();
1095
+ chatState.beforeNextPrompt = reset;
1096
+ try {
1097
+ await reset;
1098
+ } finally {
1099
+ if (chatState.beforeNextPrompt === reset) chatState.beforeNextPrompt = null;
1100
+ }
1101
+ return;
1102
+ }
1103
+
1104
+ chatState.processing = true;
1105
+ logger?.log("telegram", `processing new-session command in chat ${ctx.chat.id}`);
1106
+ await processChatPromptQueue({
754
1107
  chatId: ctx.chat.id,
755
- prompt: buildNewSessionPrompt(ctx),
1108
+ prompt,
756
1109
  label: "new-session command",
757
- ctx
1110
+ ctx,
1111
+ beforeInitialPrompt: async () => {
1112
+ const handoff = await withTyping(ctx, () => summarizeSessionBeforeReset(ctx.chat.id));
1113
+ if (chatState.historyRevision !== commandRevision) return;
1114
+ await conversationHistory.reset(ctx.chat.id, {
1115
+ runtime: "pi",
1116
+ history: handoff.handoff
1117
+ });
1118
+ if (chatState.historyRevision !== commandRevision) return;
1119
+ agentManager.resetSession(ctx.chat.id, handoff);
1120
+ }
758
1121
  });
759
1122
  }
760
1123
 
@@ -775,6 +1138,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
775
1138
  if (piAuthIssue) {
776
1139
  await ctx.reply(buildPiAuthRecoveryBlockedMessage({
777
1140
  config,
1141
+ chatId: ctx.chat.id,
778
1142
  issue: piAuthIssue,
779
1143
  renewalActive: authRenewals.has(chatKey(ctx.chat.id))
780
1144
  }));
@@ -783,6 +1147,21 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
783
1147
  await handleNewCommand(ctx);
784
1148
  });
785
1149
 
1150
+ bot.command("restart", handleRestartCommand);
1151
+
1152
+ bot.command("doctor", async (ctx) => {
1153
+ const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
1154
+ if (!auth.ok) return;
1155
+ await withTyping(ctx, async () => {
1156
+ try {
1157
+ await ctx.reply(formatDoctorReport(await doctor()));
1158
+ } catch (error) {
1159
+ logger?.error("doctor", `doctor command failed: ${getErrorMessage(error)}`);
1160
+ await ctx.reply(`Arisa Doctor failed: ${getErrorMessage(error)}`);
1161
+ }
1162
+ });
1163
+ });
1164
+
786
1165
  bot.command("model", async (ctx) => {
787
1166
  const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
788
1167
  if (!auth.ok) return;
@@ -795,22 +1174,28 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
795
1174
  await showEffortPicker(ctx);
796
1175
  });
797
1176
 
1177
+ bot.command("speed", async (ctx) => {
1178
+ const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
1179
+ if (!auth.ok) return;
1180
+ await showSpeedPicker(ctx);
1181
+ });
1182
+
798
1183
  bot.command("auth", async (ctx) => {
799
1184
  const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
800
1185
  if (!auth.ok) return;
801
1186
 
802
- const status = getPiAuthStatus(config);
1187
+ const status = getPiAuthStatus(config, ctx.chat.id);
803
1188
  if (status.hasApiKey || !status.supportsOAuth) {
804
1189
  await withTyping(ctx, async () => {
805
1190
  try {
806
- await agentManager.validatePiAgent();
1191
+ await agentManager.validateAgent();
807
1192
  agentManager.clearSessionCache(ctx.chat.id);
808
1193
  piAuthIssue = null;
809
- await ctx.reply(buildPiAuthTelegramMessage({ config, verified: true }));
1194
+ await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, verified: true }));
810
1195
  } catch (error) {
811
1196
  const issue = rememberPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
812
1197
  piAuthIssue = issue;
813
- await ctx.reply(buildPiAuthTelegramMessage({ config, issue }));
1198
+ await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue }));
814
1199
  }
815
1200
  });
816
1201
  return;
@@ -824,14 +1209,15 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
824
1209
  } catch (error) {
825
1210
  const issue = rememberPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
826
1211
  piAuthIssue = issue;
827
- await ctx.reply(buildPiAuthTelegramMessage({ config, issue }));
1212
+ await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue }));
828
1213
  }
829
1214
  });
830
1215
 
831
1216
  bot.on("callback_query:data", async (ctx, next) => {
832
1217
  const modelAction = parseModelPickerAction(ctx.callbackQuery.data);
833
1218
  const effortAction = modelAction ? null : parseEffortPickerAction(ctx.callbackQuery.data);
834
- const action = modelAction || effortAction;
1219
+ const speedAction = modelAction || effortAction ? null : parseSpeedPickerAction(ctx.callbackQuery.data);
1220
+ const action = modelAction || effortAction || speedAction;
835
1221
  if (!action) return next();
836
1222
  if (action.type === "noop") {
837
1223
  await ctx.answerCallbackQuery();
@@ -851,17 +1237,8 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
851
1237
  return;
852
1238
  }
853
1239
 
854
- if (getChatState(ctx.chat.id).processing) {
855
- await ctx.answerCallbackQuery({
856
- text: action.type === "effort" || action.type === "model-effort"
857
- ? "Wait for the current response before changing effort."
858
- : "Wait for the current response before changing models.",
859
- show_alert: true
860
- });
861
- return;
862
- }
863
-
864
- const models = getProviderModels();
1240
+ const models = await getProviderModels(ctx.chat.id);
1241
+ const chatBusy = getChatState(ctx.chat.id).processing;
865
1242
 
866
1243
  if (action.type === "select") {
867
1244
  const model = models[action.value];
@@ -873,6 +1250,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
873
1250
  return;
874
1251
  }
875
1252
 
1253
+ // Reasoning models open the effort picker only — no session reset yet.
876
1254
  if (modelSupportsThinking(model)) {
877
1255
  await showEffortPicker(ctx, {
878
1256
  model,
@@ -883,10 +1261,21 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
883
1261
  return;
884
1262
  }
885
1263
 
1264
+ if (chatBusy) {
1265
+ await ctx.answerCallbackQuery({
1266
+ text: "Wait for the current response before changing models.",
1267
+ show_alert: true
1268
+ });
1269
+ return;
1270
+ }
1271
+
886
1272
  const currentModelId = resolveChatModel(config, ctx.chat.id);
887
1273
  const currentEffort = resolveChatThinkingLevel(config, ctx.chat.id);
888
1274
  if (model.id === currentModelId && currentEffort === "off") {
889
- await ctx.answerCallbackQuery({ text: `Already using ${model.id}.` });
1275
+ await closeModelPicker(ctx, {
1276
+ messageText: `Already using ${model.provider}/${model.id}.`,
1277
+ callbackText: `Already using ${model.id}.`
1278
+ });
890
1279
  return;
891
1280
  }
892
1281
 
@@ -921,10 +1310,14 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
921
1310
  const currentModelId = resolveChatModel(config, ctx.chat.id);
922
1311
  const currentEffort = resolveChatThinkingLevel(config, ctx.chat.id);
923
1312
  if (model.id === currentModelId && action.level === currentEffort) {
924
- await ctx.answerCallbackQuery({ text: `Already using ${model.id} at ${action.level}.` });
1313
+ await closeModelPicker(ctx, {
1314
+ messageText: `Already using ${model.provider}/${model.id} (effort: ${action.level}).`,
1315
+ callbackText: `Already using ${model.id} at ${action.level}.`
1316
+ });
925
1317
  return;
926
1318
  }
927
1319
 
1320
+ // Effort-only updates do not reset the session, so they are safe while busy.
928
1321
  if (model.id === currentModelId) {
929
1322
  await persistChatEffort(ctx.chat.id, model, action.level);
930
1323
  await ctx.api.editMessageText(
@@ -936,6 +1329,14 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
936
1329
  return;
937
1330
  }
938
1331
 
1332
+ if (chatBusy) {
1333
+ await ctx.answerCallbackQuery({
1334
+ text: "Wait for the current response before changing models.",
1335
+ show_alert: true
1336
+ });
1337
+ return;
1338
+ }
1339
+
939
1340
  await persistChatModel(ctx.chat.id, model, action.level);
940
1341
  await ctx.api.editMessageText(
941
1342
  ctx.chat.id,
@@ -972,7 +1373,10 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
972
1373
  }
973
1374
  const currentEffort = resolveChatThinkingLevel(config, ctx.chat.id);
974
1375
  if (action.level === currentEffort) {
975
- await ctx.answerCallbackQuery({ text: `Already using effort ${action.level}.` });
1376
+ await closeModelPicker(ctx, {
1377
+ messageText: `Already using effort ${action.level} for ${model.provider}/${model.id}.`,
1378
+ callbackText: `Already using effort ${action.level}.`
1379
+ });
976
1380
  return;
977
1381
  }
978
1382
  await persistChatEffort(ctx.chat.id, model, action.level);
@@ -982,11 +1386,45 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
982
1386
  `Effort set to ${action.level} for ${model.provider}/${model.id}.`
983
1387
  );
984
1388
  await ctx.answerCallbackQuery({ text: `Effort: ${action.level}.` });
1389
+ return;
1390
+ }
1391
+
1392
+ if (action.type === "speed") {
1393
+ const model = models.find((item) => item.id === resolveChatModel(config, ctx.chat.id));
1394
+ if (!model) {
1395
+ await ctx.answerCallbackQuery({
1396
+ text: "Current model is unavailable. Run /model again.",
1397
+ show_alert: true
1398
+ });
1399
+ return;
1400
+ }
1401
+ if (!modelSupportsSpeed(model)) {
1402
+ await ctx.answerCallbackQuery({
1403
+ text: "This model does not support speed 1.5x.",
1404
+ show_alert: true
1405
+ });
1406
+ return;
1407
+ }
1408
+ const currentSpeed = resolveChatSpeed(config, ctx.chat.id);
1409
+ if (action.speed === currentSpeed) {
1410
+ await closeModelPicker(ctx, {
1411
+ messageText: `Already using speed ${action.speed.toFixed(1)}x for ${model.provider}/${model.id}.`,
1412
+ callbackText: `Already using speed ${action.speed.toFixed(1)}x.`
1413
+ });
1414
+ return;
1415
+ }
1416
+ await persistChatSpeed(ctx.chat.id, model, action.speed);
1417
+ await ctx.api.editMessageText(
1418
+ ctx.chat.id,
1419
+ ctx.callbackQuery.message.message_id,
1420
+ `Speed set to ${action.speed.toFixed(1)}x for ${model.provider}/${model.id}.`
1421
+ );
1422
+ await ctx.answerCallbackQuery({ text: `Speed: ${action.speed.toFixed(1)}x.` });
985
1423
  }
986
1424
  } catch (error) {
987
1425
  logger?.error("telegram", `model selection failed for chat ${ctx.chat.id}: ${getErrorMessage(error)}`);
988
1426
  await ctx.answerCallbackQuery({
989
- text: "Could not change the model or effort.",
1427
+ text: "Could not change the model, effort, or speed.",
990
1428
  show_alert: true
991
1429
  }).catch(() => {});
992
1430
  }
@@ -1004,34 +1442,30 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
1004
1442
  if (piAuthIssue) {
1005
1443
  await ctx.reply(buildPiAuthRecoveryBlockedMessage({
1006
1444
  config,
1445
+ chatId: ctx.chat.id,
1007
1446
  issue: piAuthIssue,
1008
1447
  renewalActive: authRenewals.has(chatKey(ctx.chat.id))
1009
1448
  }));
1010
1449
  return;
1011
1450
  }
1012
1451
 
1013
- try {
1014
- await enqueueOrProcess(ctx);
1015
- } catch (error) {
1452
+ // grammY long polling awaits each middleware. Keep prompt execution in the background so
1453
+ // the next Telegram update can reach the active session as a steer or queued message.
1454
+ enqueueOrProcess(ctx).catch(async (error) => {
1016
1455
  const chatState = getChatState(ctx.chat.id);
1017
1456
  chatState.processing = false;
1018
1457
  if (wasPromptErrorNotified(error)) return;
1019
1458
  const issue = getPiAuthIssue(error);
1020
1459
  await ctx.reply(issue
1021
- ? buildPiAuthTelegramMessage({ config, issue })
1460
+ ? buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue })
1022
1461
  : getErrorMessage(error));
1023
- }
1462
+ });
1024
1463
  });
1025
1464
 
1026
1465
  return {
1027
1466
  async start({ skipAgentStartupPrompts = false } = {}) {
1028
1467
  config.telegram.chatMeta ||= {};
1029
- await bot.api.setMyCommands([
1030
- { command: "new", description: "Start a new chat context" },
1031
- { command: "model", description: "Choose the model for this chat" },
1032
- { command: "effort", description: "Choose reasoning effort for this chat" },
1033
- { command: "auth", description: "Show Pi authentication status" }
1034
- ]);
1468
+ await bot.api.setMyCommands(telegramCommands);
1035
1469
  if (!taskTimer) {
1036
1470
  taskTimer = setInterval(() => {
1037
1471
  dispatchDueTasks().catch((error) => {