@grinev/opencode-telegram-bot 0.11.1 → 0.11.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.
package/README.md CHANGED
@@ -102,7 +102,7 @@ opencode-telegram config
102
102
  | ----------------- | ------------------------------------------------------- |
103
103
  | `/status` | Server health, current project, session, and model info |
104
104
  | `/new` | Create a new session |
105
- | `/stop` | Abort the current task |
105
+ | `/abort` | Abort the current task |
106
106
  | `/sessions` | Browse and switch between recent sessions |
107
107
  | `/projects` | Switch between OpenCode projects |
108
108
  | `/rename` | Rename the current session |
@@ -4,6 +4,7 @@ import { config } from "../config.js";
4
4
  import { loadSettings } from "../settings/manager.js";
5
5
  import { processManager } from "../process/manager.js";
6
6
  import { warmupSessionDirectoryCache } from "../session/cache-manager.js";
7
+ import { reconcileStoredModelSelection } from "../model/manager.js";
7
8
  import { getRuntimeMode } from "../runtime/mode.js";
8
9
  import { getRuntimePaths } from "../runtime/paths.js";
9
10
  import { logger } from "../utils/logger.js";
@@ -29,6 +30,7 @@ export async function startBotApp() {
29
30
  logger.debug(`[Runtime] Application start mode: ${mode}`);
30
31
  await loadSettings();
31
32
  await processManager.initialize();
33
+ await reconcileStoredModelSelection();
32
34
  await warmupSessionDirectoryCache();
33
35
  const bot = createBot();
34
36
  const webhookInfo = await bot.api.getWebhookInfo();
@@ -6,10 +6,10 @@ import { summaryAggregator } from "../../summary/aggregator.js";
6
6
  import { logger } from "../../utils/logger.js";
7
7
  import { t } from "../../i18n/index.js";
8
8
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9
- function stopLocalStreaming() {
9
+ function abortLocalStreaming() {
10
10
  stopEventListening();
11
11
  summaryAggregator.clear();
12
- clearAllInteractionState("stop_command");
12
+ clearAllInteractionState("abort_command");
13
13
  }
14
14
  async function pollSessionStatus(sessionId, directory, maxWaitMs = 5000) {
15
15
  const startedAt = Date.now();
@@ -33,21 +33,34 @@ async function pollSessionStatus(sessionId, directory, maxWaitMs = 5000) {
33
33
  await sleep(pollIntervalMs);
34
34
  }
35
35
  catch (error) {
36
- logger.warn("[Stop] Failed to poll session status:", error);
36
+ logger.warn("[Abort] Failed to poll session status:", error);
37
37
  break;
38
38
  }
39
39
  }
40
40
  return "busy";
41
41
  }
42
- export async function stopCommand(ctx) {
42
+ export async function abortCurrentOperation(ctx, options = {}) {
43
+ const notifyUser = options.notifyUser ?? true;
43
44
  try {
44
- stopLocalStreaming();
45
+ abortLocalStreaming();
45
46
  const currentSession = getCurrentSession();
46
47
  if (!currentSession) {
47
- await ctx.reply(t("stop.no_active_session"));
48
+ if (notifyUser) {
49
+ await ctx.reply(t("stop.no_active_session"));
50
+ }
48
51
  return;
49
52
  }
50
- const waitingMessage = await ctx.reply(t("stop.in_progress"));
53
+ let waitingMessageId = null;
54
+ let chatId = null;
55
+ if (notifyUser) {
56
+ const waitingMessage = await ctx.reply(t("stop.in_progress"));
57
+ waitingMessageId = waitingMessage.message_id;
58
+ chatId = ctx.chat?.id ?? null;
59
+ if (!chatId) {
60
+ logger.warn("[Abort] Chat context is missing while aborting active session");
61
+ return;
62
+ }
63
+ }
51
64
  const controller = new AbortController();
52
65
  const timeoutId = setTimeout(() => controller.abort(), 5000);
53
66
  try {
@@ -57,35 +70,50 @@ export async function stopCommand(ctx) {
57
70
  }, { signal: controller.signal });
58
71
  clearTimeout(timeoutId);
59
72
  if (abortError) {
60
- logger.warn("[Stop] Abort request failed:", abortError);
61
- await ctx.api.editMessageText(ctx.chat.id, waitingMessage.message_id, t("stop.warn_unconfirmed"));
73
+ logger.warn("[Abort] Abort request failed:", abortError);
74
+ if (notifyUser && chatId !== null && waitingMessageId !== null) {
75
+ await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_unconfirmed"));
76
+ }
62
77
  return;
63
78
  }
64
79
  if (abortResult !== true) {
65
- await ctx.api.editMessageText(ctx.chat.id, waitingMessage.message_id, t("stop.warn_maybe_finished"));
80
+ if (notifyUser && chatId !== null && waitingMessageId !== null) {
81
+ await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_maybe_finished"));
82
+ }
66
83
  return;
67
84
  }
68
85
  const finalStatus = await pollSessionStatus(currentSession.id, currentSession.directory, 5000);
69
86
  if (finalStatus === "idle" || finalStatus === "not-found") {
70
- await ctx.api.editMessageText(ctx.chat.id, waitingMessage.message_id, t("stop.success"));
87
+ if (notifyUser && chatId !== null && waitingMessageId !== null) {
88
+ await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.success"));
89
+ }
71
90
  }
72
91
  else {
73
- await ctx.api.editMessageText(ctx.chat.id, waitingMessage.message_id, t("stop.warn_still_busy"));
92
+ if (notifyUser && chatId !== null && waitingMessageId !== null) {
93
+ await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_still_busy"));
94
+ }
74
95
  }
75
96
  }
76
97
  catch (error) {
77
98
  clearTimeout(timeoutId);
78
99
  if (error instanceof Error && error.name === "AbortError") {
79
- await ctx.api.editMessageText(ctx.chat.id, waitingMessage.message_id, t("stop.warn_timeout"));
100
+ if (notifyUser && chatId !== null && waitingMessageId !== null) {
101
+ await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_timeout"));
102
+ }
80
103
  }
81
104
  else {
82
- logger.error("[Stop] Error while aborting session:", error);
83
- await ctx.api.editMessageText(ctx.chat.id, waitingMessage.message_id, t("stop.warn_local_only"));
105
+ logger.error("[Abort] Error while aborting session:", error);
106
+ if (notifyUser && chatId !== null && waitingMessageId !== null) {
107
+ await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_local_only"));
108
+ }
84
109
  }
85
110
  }
86
111
  }
87
112
  catch (error) {
88
- logger.error("[Stop] Unexpected error:", error);
113
+ logger.error("[Abort] Unexpected error:", error);
89
114
  await ctx.reply(t("stop.error"));
90
115
  }
91
116
  }
117
+ export async function abortCommand(ctx) {
118
+ await abortCurrentOperation(ctx);
119
+ }
@@ -6,7 +6,7 @@ import { t } from "../../i18n/index.js";
6
6
  const COMMAND_DEFINITIONS = [
7
7
  { command: "status", descriptionKey: "cmd.description.status" },
8
8
  { command: "new", descriptionKey: "cmd.description.new" },
9
- { command: "stop", descriptionKey: "cmd.description.stop" },
9
+ { command: "abort", descriptionKey: "cmd.description.stop" },
10
10
  { command: "sessions", descriptionKey: "cmd.description.sessions" },
11
11
  { command: "projects", descriptionKey: "cmd.description.projects" },
12
12
  { command: "rename", descriptionKey: "cmd.description.rename" },
@@ -4,6 +4,9 @@ import { getStoredModel } from "../../model/manager.js";
4
4
  import { formatVariantForButton } from "../../variant/manager.js";
5
5
  import { pinnedMessageManager } from "../../pinned/manager.js";
6
6
  import { keyboardManager } from "../../keyboard/manager.js";
7
+ import { clearSession } from "../../session/manager.js";
8
+ import { clearProject } from "../../settings/manager.js";
9
+ import { abortCurrentOperation } from "./abort.js";
7
10
  import { t } from "../../i18n/index.js";
8
11
  export async function startCommand(ctx) {
9
12
  if (ctx.chat) {
@@ -12,6 +15,11 @@ export async function startCommand(ctx) {
12
15
  }
13
16
  keyboardManager.initialize(ctx.api, ctx.chat.id);
14
17
  }
18
+ await abortCurrentOperation(ctx, { notifyUser: false });
19
+ clearSession();
20
+ clearProject();
21
+ keyboardManager.clearContext();
22
+ await pinnedMessageManager.clear();
15
23
  if (pinnedMessageManager.getContextLimit() === 0) {
16
24
  await pinnedMessageManager.refreshContextLimit();
17
25
  }
package/dist/bot/index.js CHANGED
@@ -16,7 +16,7 @@ import { AGENT_MODE_BUTTON_TEXT_PATTERN, MODEL_BUTTON_TEXT_PATTERN, VARIANT_BUTT
16
16
  import { sessionsCommand, handleSessionSelect } from "./commands/sessions.js";
17
17
  import { newCommand } from "./commands/new.js";
18
18
  import { projectsCommand, handleProjectSelect } from "./commands/projects.js";
19
- import { stopCommand } from "./commands/stop.js";
19
+ import { abortCommand } from "./commands/abort.js";
20
20
  import { opencodeStartCommand } from "./commands/opencode-start.js";
21
21
  import { opencodeStopCommand } from "./commands/opencode-stop.js";
22
22
  import { renameCommand, handleRenameCancel, handleRenameTextAnswer } from "./commands/rename.js";
@@ -456,7 +456,7 @@ export function createBot() {
456
456
  bot.command("projects", projectsCommand);
457
457
  bot.command("sessions", sessionsCommand);
458
458
  bot.command("new", newCommand);
459
- bot.command("stop", stopCommand);
459
+ bot.command("abort", abortCommand);
460
460
  bot.command("rename", renameCommand);
461
461
  bot.command("commands", commandsCommand);
462
462
  bot.on("message:text", unknownCommandMiddleware);
package/dist/i18n/de.js CHANGED
@@ -38,11 +38,11 @@ export const de = {
38
38
  "bot.creating_session": "🔄 Erstelle eine neue Sitzung...",
39
39
  "bot.create_session_error": "🔴 Sitzung konnte nicht erstellt werden. Versuche /new oder prüfe den Serverstatus mit /status.",
40
40
  "bot.session_created": "✅ Sitzung erstellt: {title}",
41
- "bot.session_busy": "⏳ Agent führt bereits eine Aufgabe aus. Warte auf Abschluss oder nutze /stop, um den aktuellen Lauf zu unterbrechen.",
41
+ "bot.session_busy": "⏳ Agent führt bereits eine Aufgabe aus. Warte auf Abschluss oder nutze /abort, um den aktuellen Lauf zu unterbrechen.",
42
42
  "bot.session_reset_project_mismatch": "⚠️ Die aktive Sitzung passt nicht zum ausgewählten Projekt und wurde daher zurückgesetzt. Nutze /sessions zur Auswahl oder /new, um eine neue Sitzung zu erstellen.",
43
43
  "bot.prompt_send_error": "Anfrage konnte nicht an OpenCode gesendet werden.",
44
44
  "bot.session_error": "🔴 OpenCode meldete einen Fehler: {message}",
45
- "bot.session_retry": "🔁 {message}\n\nDer Provider liefert bei wiederholten Versuchen immer wieder denselben Fehler. Mit /stop abbrechen.",
45
+ "bot.session_retry": "🔁 {message}\n\nDer Provider liefert bei wiederholten Versuchen immer wieder denselben Fehler. Mit /abort abbrechen.",
46
46
  "bot.unknown_command": "⚠️ Unbekannter Befehl: {command}. Nutze /help, um verfügbare Befehle zu sehen.",
47
47
  "bot.photo_downloading": "⏳ Lade Foto herunter...",
48
48
  "bot.photo_too_large": "⚠️ Foto ist zu groß (max. {maxSizeMb}MB)",
@@ -105,13 +105,13 @@ export const de = {
105
105
  "new.create_error": "🔴 OpenCode-Server ist nicht verfügbar oder beim Erstellen der Sitzung ist ein Fehler aufgetreten.",
106
106
  "stop.no_active_session": "🛑 Agent wurde nicht gestartet\n\nErstelle eine Sitzung mit /new oder wähle eine über /sessions aus.",
107
107
  "stop.in_progress": "🛑 Event-Stream gestoppt, sende Abbruchsignal...\n\nWarte darauf, dass der Agent stoppt.",
108
- "stop.warn_unconfirmed": "⚠️ Event-Stream gestoppt, aber der Server hat den Abbruch nicht bestätigt.\n\nPrüfe /status und versuche /stop in ein paar Sekunden erneut.",
108
+ "stop.warn_unconfirmed": "⚠️ Event-Stream gestoppt, aber der Server hat den Abbruch nicht bestätigt.\n\nPrüfe /status und versuche /abort in ein paar Sekunden erneut.",
109
109
  "stop.warn_maybe_finished": "⚠️ Event-Stream gestoppt, aber der Agent konnte bereits fertig sein.",
110
110
  "stop.success": "✅ Agent-Aktion unterbrochen. Von diesem Lauf werden keine weiteren Nachrichten gesendet.",
111
111
  "stop.warn_still_busy": "⚠️ Signal gesendet, aber der Agent ist noch beschäftigt.\n\nDer Event-Stream ist bereits deaktiviert, daher werden keine Zwischenmeldungen gesendet.",
112
- "stop.warn_timeout": "⚠️ Timeout beim Abbruch.\n\nDer Event-Stream ist bereits deaktiviert, versuche /stop in ein paar Sekunden erneut.",
112
+ "stop.warn_timeout": "⚠️ Timeout beim Abbruch.\n\nDer Event-Stream ist bereits deaktiviert, versuche /abort in ein paar Sekunden erneut.",
113
113
  "stop.warn_local_only": "⚠️ Event-Stream lokal gestoppt, aber serverseitiger Abbruch ist fehlgeschlagen.",
114
- "stop.error": "🔴 Aktion konnte nicht gestoppt werden.\n\nEvent-Stream ist gestoppt, versuche /stop erneut.",
114
+ "stop.error": "🔴 Aktion konnte nicht gestoppt werden.\n\nEvent-Stream ist gestoppt, versuche /abort erneut.",
115
115
  "opencode_start.already_running_managed": "⚠️ OpenCode-Server läuft bereits\n\nPID: {pid}\nBetriebszeit: {seconds} Sekunden",
116
116
  "opencode_start.already_running_external": "✅ OpenCode-Server läuft bereits als externer Prozess\n\nVersion: {version}\n\nDieser Server wurde nicht vom Bot gestartet, daher kann /opencode-stop ihn nicht stoppen.",
117
117
  "opencode_start.starting": "🔄 Starte OpenCode-Server...",
package/dist/i18n/en.js CHANGED
@@ -38,11 +38,11 @@ export const en = {
38
38
  "bot.creating_session": "🔄 Creating a new session...",
39
39
  "bot.create_session_error": "🔴 Failed to create session. Try /new or check server status with /status.",
40
40
  "bot.session_created": "✅ Session created: {title}",
41
- "bot.session_busy": "⏳ Agent is already running a task. Wait for completion or use /stop to interrupt current run.",
41
+ "bot.session_busy": "⏳ Agent is already running a task. Wait for completion or use /abort to interrupt current run.",
42
42
  "bot.session_reset_project_mismatch": "⚠️ Active session does not match the selected project, so it was reset. Use /sessions to pick one or /new to create a new session.",
43
43
  "bot.prompt_send_error": "Failed to send request to OpenCode.",
44
44
  "bot.session_error": "🔴 OpenCode returned an error: {message}",
45
- "bot.session_retry": "🔁 {message}\n\nProvider keeps returning the same error on repeated retries. Use /stop to abort.",
45
+ "bot.session_retry": "🔁 {message}\n\nProvider keeps returning the same error on repeated retries. Use /abort to abort.",
46
46
  "bot.unknown_command": "⚠️ Unknown command: {command}. Use /help to see available commands.",
47
47
  "bot.photo_downloading": "⏳ Downloading photo...",
48
48
  "bot.photo_too_large": "⚠️ Photo is too large (max {maxSizeMb}MB)",
@@ -105,13 +105,13 @@ export const en = {
105
105
  "new.create_error": "🔴 OpenCode Server is unavailable or an error occurred while creating session.",
106
106
  "stop.no_active_session": "🛑 Agent was not started\n\nCreate a session with /new or select one via /sessions.",
107
107
  "stop.in_progress": "🛑 Event stream stopped, sending abort signal...\n\nWaiting for agent to stop.",
108
- "stop.warn_unconfirmed": "⚠️ Event stream stopped, but server did not confirm abort.\n\nCheck /status and retry /stop in a few seconds.",
108
+ "stop.warn_unconfirmed": "⚠️ Event stream stopped, but server did not confirm abort.\n\nCheck /status and retry /abort in a few seconds.",
109
109
  "stop.warn_maybe_finished": "⚠️ Event stream stopped, but the agent may have already finished.",
110
110
  "stop.success": "✅ Agent action interrupted. No more messages from this run will be sent.",
111
111
  "stop.warn_still_busy": "⚠️ Signal sent, but agent is still busy.\n\nEvent stream is already disabled, so no intermediate messages will be sent.",
112
- "stop.warn_timeout": "⚠️ Abort request timeout.\n\nEvent stream is already disabled, retry /stop in a few seconds.",
112
+ "stop.warn_timeout": "⚠️ Abort request timeout.\n\nEvent stream is already disabled, retry /abort in a few seconds.",
113
113
  "stop.warn_local_only": "⚠️ Event stream stopped locally, but server-side abort failed.",
114
- "stop.error": "🔴 Failed to stop action.\n\nEvent stream is stopped, try /stop again.",
114
+ "stop.error": "🔴 Failed to stop action.\n\nEvent stream is stopped, try /abort again.",
115
115
  "opencode_start.already_running_managed": "⚠️ OpenCode Server is already running\n\nPID: {pid}\nUptime: {seconds} seconds",
116
116
  "opencode_start.already_running_external": "✅ OpenCode Server is already running as an external process\n\nVersion: {version}\n\nThis server was not started by bot, so /opencode-stop cannot stop it.",
117
117
  "opencode_start.starting": "🔄 Starting OpenCode Server...",
package/dist/i18n/es.js CHANGED
@@ -38,11 +38,11 @@ export const es = {
38
38
  "bot.creating_session": "🔄 Creando una sesión nueva...",
39
39
  "bot.create_session_error": "🔴 No se pudo crear la sesión. Prueba /new o revisa el estado del servidor con /status.",
40
40
  "bot.session_created": "✅ Sesión creada: {title}",
41
- "bot.session_busy": "⏳ El agente ya está ejecutando una tarea. Espera a que termine o usa /stop para interrumpir la ejecución actual.",
41
+ "bot.session_busy": "⏳ El agente ya está ejecutando una tarea. Espera a que termine o usa /abort para interrumpir la ejecución actual.",
42
42
  "bot.session_reset_project_mismatch": "⚠️ La sesión activa no coincide con el proyecto seleccionado, así que se reinició. Usa /sessions para elegir una o /new para crear una nueva.",
43
43
  "bot.prompt_send_error": "No se pudo enviar la solicitud a OpenCode.",
44
44
  "bot.session_error": "🔴 OpenCode devolvió un error: {message}",
45
- "bot.session_retry": "🔁 {message}\n\nEl proveedor devuelve el mismo error en intentos repetidos. Usa /stop para detenerlo.",
45
+ "bot.session_retry": "🔁 {message}\n\nEl proveedor devuelve el mismo error en intentos repetidos. Usa /abort para detenerlo.",
46
46
  "bot.unknown_command": "⚠️ Comando desconocido: {command}. Usa /help para ver los comandos disponibles.",
47
47
  "bot.photo_downloading": "⏳ Descargando foto...",
48
48
  "bot.photo_too_large": "⚠️ La foto es demasiado grande (max {maxSizeMb}MB)",
@@ -105,13 +105,13 @@ export const es = {
105
105
  "new.create_error": "🔴 OpenCode Server no está disponible u ocurrió un error al crear la sesión.",
106
106
  "stop.no_active_session": "🛑 El agente no se inició\n\nCrea una sesión con /new o selecciona una con /sessions.",
107
107
  "stop.in_progress": "🛑 Flujo de eventos detenido; enviando señal de aborto...\n\nEsperando a que el agente se detenga.",
108
- "stop.warn_unconfirmed": "⚠️ Flujo de eventos detenido, pero el servidor no confirmó el aborto.\n\nRevisa /status y vuelve a intentar /stop en unos segundos.",
108
+ "stop.warn_unconfirmed": "⚠️ Flujo de eventos detenido, pero el servidor no confirmó el aborto.\n\nRevisa /status y vuelve a intentar /abort en unos segundos.",
109
109
  "stop.warn_maybe_finished": "⚠️ Flujo de eventos detenido, pero el agente podría haber terminado ya.",
110
110
  "stop.success": "✅ Acción del agente interrumpida. No se enviarán más mensajes de esta ejecución.",
111
111
  "stop.warn_still_busy": "⚠️ Señal enviada, pero el agente sigue ocupado.\n\nEl flujo de eventos ya está deshabilitado, así que no se enviarán mensajes intermedios.",
112
- "stop.warn_timeout": "⚠️ Tiempo de espera agotado al solicitar el aborto.\n\nEl flujo de eventos ya está deshabilitado; vuelve a intentar /stop en unos segundos.",
112
+ "stop.warn_timeout": "⚠️ Tiempo de espera agotado al solicitar el aborto.\n\nEl flujo de eventos ya está deshabilitado; vuelve a intentar /abort en unos segundos.",
113
113
  "stop.warn_local_only": "⚠️ Flujo de eventos detenido localmente, pero el aborto en el servidor falló.",
114
- "stop.error": "🔴 No se pudo detener la acción.\n\nEl flujo de eventos está detenido; prueba /stop otra vez.",
114
+ "stop.error": "🔴 No se pudo detener la acción.\n\nEl flujo de eventos está detenido; prueba /abort otra vez.",
115
115
  "opencode_start.already_running_managed": "⚠️ OpenCode Server ya está en ejecución\n\nPID: {pid}\nTiempo activo: {seconds} segundos",
116
116
  "opencode_start.already_running_external": "✅ OpenCode Server ya está en ejecución como un proceso externo\n\nVersión: {version}\n\nEste servidor no fue iniciado por el bot, por lo que /opencode-stop no puede detenerlo.",
117
117
  "opencode_start.starting": "🔄 Iniciando OpenCode Server...",
package/dist/i18n/ru.js CHANGED
@@ -38,11 +38,11 @@ export const ru = {
38
38
  "bot.creating_session": "🔄 Создаю новую сессию...",
39
39
  "bot.create_session_error": "🔴 Не удалось создать сессию. Попробуйте команду /new или проверьте статус сервера /status.",
40
40
  "bot.session_created": "✅ Сессия создана: {title}",
41
- "bot.session_busy": "⏳ Агент уже выполняет задачу. Дождитесь завершения или используйте /stop, чтобы прервать текущий запуск.",
41
+ "bot.session_busy": "⏳ Агент уже выполняет задачу. Дождитесь завершения или используйте /abort, чтобы прервать текущий запуск.",
42
42
  "bot.session_reset_project_mismatch": "⚠️ Активная сессия не соответствует выбранному проекту, поэтому была сброшена. Используйте /sessions для выбора или /new для создания новой сессии.",
43
43
  "bot.prompt_send_error": "Не удалось отправить запрос в OpenCode.",
44
44
  "bot.session_error": "🔴 OpenCode вернул ошибку: {message}",
45
- "bot.session_retry": "🔁 {message}\n\nПровайдер возвращает одну и ту же ошибку при повторных запросах. Используйте /stop для остановки.",
45
+ "bot.session_retry": "🔁 {message}\n\nПровайдер возвращает одну и ту же ошибку при повторных запросах. Используйте /abort для остановки.",
46
46
  "bot.unknown_command": "⚠️ Неизвестная команда: {command}. Используйте /help для списка команд.",
47
47
  "bot.photo_downloading": "⏳ Скачиваю фото...",
48
48
  "bot.photo_too_large": "⚠️ Фото слишком большое (макс. {maxSizeMb}МБ)",
@@ -105,13 +105,13 @@ export const ru = {
105
105
  "new.create_error": "🔴 OpenCode Server недоступен или произошла ошибка при создании сессии.",
106
106
  "stop.no_active_session": "🛑 Агент не был запущен\n\nСначала создайте сессию командой /new или выберите существующую через /sessions.",
107
107
  "stop.in_progress": "🛑 Отключил поток событий и отправляю сигнал прерывания...\n\nОжидание остановки агента.",
108
- "stop.warn_unconfirmed": "⚠️ Поток событий остановлен, но сервер не подтвердил прерывание.\n\nПроверьте /status и повторите /stop через пару секунд.",
108
+ "stop.warn_unconfirmed": "⚠️ Поток событий остановлен, но сервер не подтвердил прерывание.\n\nПроверьте /status и повторите /abort через пару секунд.",
109
109
  "stop.warn_maybe_finished": "⚠️ Поток событий остановлен, но агент мог уже завершиться к моменту запроса.",
110
110
  "stop.success": "✅ Действие агента прервано. Новые сообщения от текущего запуска больше не придут.",
111
111
  "stop.warn_still_busy": "⚠️ Сигнал отправлен, но агент еще busy.\n\nПоток событий уже отключен, поэтому бот не будет присылать промежуточные сообщения.",
112
- "stop.warn_timeout": "⚠️ Таймаут запроса на прерывание.\n\nПоток событий уже отключен, повторите /stop через пару секунд.",
112
+ "stop.warn_timeout": "⚠️ Таймаут запроса на прерывание.\n\nПоток событий уже отключен, повторите /abort через пару секунд.",
113
113
  "stop.warn_local_only": "⚠️ Поток событий остановлен локально, но при прерывании на сервере произошла ошибка.",
114
- "stop.error": "🔴 Ошибка при прерывании действия.\n\nПоток событий остановлен, попробуйте /stop еще раз.",
114
+ "stop.error": "🔴 Ошибка при прерывании действия.\n\nПоток событий остановлен, попробуйте /abort еще раз.",
115
115
  "opencode_start.already_running_managed": "⚠️ OpenCode Server уже запущен\n\nPID: {pid}\nUptime: {seconds} секунд",
116
116
  "opencode_start.already_running_external": "✅ OpenCode Server уже запущен внешним процессом\n\nВерсия: {version}\n\nЭтот сервер не был запущен через бота, поэтому команда /opencode-stop не сможет его остановить.",
117
117
  "opencode_start.starting": "🔄 Запускаю OpenCode Server...",
package/dist/i18n/zh.js CHANGED
@@ -38,11 +38,11 @@ export const zh = {
38
38
  "bot.creating_session": "🔄 正在创建新会话...",
39
39
  "bot.create_session_error": "🔴 创建会话失败。请重试 /new,或使用 /status 检查服务器状态。",
40
40
  "bot.session_created": "✅ 会话已创建:{title}",
41
- "bot.session_busy": "⏳ 代理正在执行任务。请等待完成,或使用 /stop 中断当前运行。",
41
+ "bot.session_busy": "⏳ 代理正在执行任务。请等待完成,或使用 /abort 中断当前运行。",
42
42
  "bot.session_reset_project_mismatch": "⚠️ 活动会话与所选项目不匹配,因此已重置。使用 /sessions 选择一个会话,或 /new 创建新会话。",
43
43
  "bot.prompt_send_error": "向 OpenCode 发送请求失败。",
44
44
  "bot.session_error": "🔴 OpenCode 返回错误:{message}",
45
- "bot.session_retry": "🔁 {message}\n\n提供方在重复重试时持续返回同一错误。使用 /stop 可停止。",
45
+ "bot.session_retry": "🔁 {message}\n\n提供方在重复重试时持续返回同一错误。使用 /abort 可停止。",
46
46
  "bot.unknown_command": "⚠️ 未知命令:{command}。使用 /help 查看可用命令。",
47
47
  "bot.photo_downloading": "⏳ 正在下载照片...",
48
48
  "bot.photo_too_large": "⚠️ 照片过大(最大 {maxSizeMb}MB)",
@@ -105,13 +105,13 @@ export const zh = {
105
105
  "new.create_error": "🔴 OpenCode 服务器不可用,或创建会话时发生错误。",
106
106
  "stop.no_active_session": "🛑 代理尚未启动\n\n使用 /new 创建会话,或通过 /sessions 选择一个。",
107
107
  "stop.in_progress": "🛑 已停止事件流,正在发送中止信号...\n\n等待代理停止。",
108
- "stop.warn_unconfirmed": "⚠️ 事件流已停止,但服务器未确认中止。\n\n检查 /status,并在几秒后重试 /stop。",
108
+ "stop.warn_unconfirmed": "⚠️ 事件流已停止,但服务器未确认中止。\n\n检查 /status,并在几秒后重试 /abort。",
109
109
  "stop.warn_maybe_finished": "⚠️ 事件流已停止,但代理可能已完成。",
110
110
  "stop.success": "✅ 代理操作已中断。本次运行的后续消息将不再发送。",
111
111
  "stop.warn_still_busy": "⚠️ 信号已发送,但代理仍在忙。\n\n事件流已禁用,因此不会发送中间消息。",
112
- "stop.warn_timeout": "⚠️ 中止请求超时。\n\n事件流已禁用,请在几秒后重试 /stop。",
112
+ "stop.warn_timeout": "⚠️ 中止请求超时。\n\n事件流已禁用,请在几秒后重试 /abort。",
113
113
  "stop.warn_local_only": "⚠️ 已在本地停止事件流,但服务器端中止失败。",
114
- "stop.error": "🔴 停止操作失败。\n\n事件流已停止,请再次尝试 /stop。",
114
+ "stop.error": "🔴 停止操作失败。\n\n事件流已停止,请再次尝试 /abort。",
115
115
  "opencode_start.already_running_managed": "⚠️ OpenCode 服务器已在运行\n\nPID:{pid}\n运行时间:{seconds} 秒",
116
116
  "opencode_start.already_running_external": "✅ OpenCode 服务器正作为外部进程运行\n\n版本:{version}\n\n该服务器不是由机器人启动,因此 /opencode-stop 无法停止它。",
117
117
  "opencode_start.starting": "🔄 正在启动 OpenCode 服务器...",
@@ -73,6 +73,9 @@ export function resolveInteractionGuardDecision(ctx) {
73
73
  return createBlockDecision(inputType, state, "expired", command);
74
74
  }
75
75
  if (inputType === "command") {
76
+ if (command === "/start") {
77
+ return createAllowDecision(inputType, state, command);
78
+ }
76
79
  if (command && state.allowedCommands.includes(command)) {
77
80
  return createAllowDecision(inputType, state, command);
78
81
  }
@@ -1,5 +1,5 @@
1
1
  import { logger } from "../utils/logger.js";
2
- export const DEFAULT_ALLOWED_INTERACTION_COMMANDS = ["/help", "/status", "/stop"];
2
+ export const DEFAULT_ALLOWED_INTERACTION_COMMANDS = ["/help", "/status", "/abort"];
3
3
  function normalizeCommand(command) {
4
4
  const trimmed = command.trim().toLowerCase();
5
5
  if (!trimmed) {
@@ -1,7 +1,15 @@
1
1
  import { getCurrentModel, setCurrentModel } from "../settings/manager.js";
2
2
  import { config } from "../config.js";
3
+ import { opencodeClient } from "../opencode/client.js";
3
4
  import { logger } from "../utils/logger.js";
4
5
  import path from "node:path";
6
+ const MODEL_CATALOG_CACHE_TTL_MS = 10 * 60 * 1000;
7
+ let cachedValidModelKeys = null;
8
+ let modelCatalogCacheExpiresAt = 0;
9
+ let modelCatalogFetchInFlight = null;
10
+ function getModelKey(providerID, modelID) {
11
+ return `${providerID}/${modelID}`;
12
+ }
5
13
  function getEnvDefaultModel() {
6
14
  const providerID = config.opencode.model.provider;
7
15
  const modelID = config.opencode.model.modelId;
@@ -20,6 +28,58 @@ function dedupeModels(models) {
20
28
  }
21
29
  return Array.from(unique.values());
22
30
  }
31
+ function filterModelsByCatalog(models, validModelKeys) {
32
+ if (!validModelKeys) {
33
+ return models;
34
+ }
35
+ return models.filter((model) => validModelKeys.has(getModelKey(model.providerID, model.modelID)));
36
+ }
37
+ async function getValidModelKeys() {
38
+ if (cachedValidModelKeys && Date.now() < modelCatalogCacheExpiresAt) {
39
+ logger.debug(`[ModelManager] Model catalog cache hit: models=${cachedValidModelKeys.size}, ttlMs=${modelCatalogCacheExpiresAt - Date.now()}`);
40
+ return cachedValidModelKeys;
41
+ }
42
+ if (modelCatalogFetchInFlight) {
43
+ logger.debug("[ModelManager] Awaiting in-flight model catalog refresh");
44
+ return modelCatalogFetchInFlight;
45
+ }
46
+ modelCatalogFetchInFlight = (async () => {
47
+ try {
48
+ logger.debug("[ModelManager] Refreshing model catalog from OpenCode API");
49
+ const response = await opencodeClient.config.providers();
50
+ if (response.error || !response.data) {
51
+ logger.warn("[ModelManager] Failed to refresh model catalog:", response.error);
52
+ if (cachedValidModelKeys) {
53
+ logger.warn("[ModelManager] Using stale model catalog cache after refresh failure");
54
+ return cachedValidModelKeys;
55
+ }
56
+ return null;
57
+ }
58
+ const validModelKeys = new Set();
59
+ for (const provider of response.data.providers) {
60
+ for (const modelID of Object.keys(provider.models)) {
61
+ validModelKeys.add(getModelKey(provider.id, modelID));
62
+ }
63
+ }
64
+ cachedValidModelKeys = validModelKeys;
65
+ modelCatalogCacheExpiresAt = Date.now() + MODEL_CATALOG_CACHE_TTL_MS;
66
+ logger.debug(`[ModelManager] Model catalog refreshed: providers=${response.data.providers.length}, models=${validModelKeys.size}`);
67
+ return cachedValidModelKeys;
68
+ }
69
+ catch (err) {
70
+ logger.warn("[ModelManager] Error refreshing model catalog:", err);
71
+ if (cachedValidModelKeys) {
72
+ logger.warn("[ModelManager] Using stale model catalog cache after refresh exception");
73
+ return cachedValidModelKeys;
74
+ }
75
+ return null;
76
+ }
77
+ finally {
78
+ modelCatalogFetchInFlight = null;
79
+ }
80
+ })();
81
+ return modelCatalogFetchInFlight;
82
+ }
23
83
  function normalizeFavoriteModels(state) {
24
84
  if (!Array.isArray(state.favorite)) {
25
85
  return [];
@@ -68,17 +128,27 @@ export async function getModelSelectionLists() {
68
128
  const content = await fs.readFile(stateFilePath, "utf-8");
69
129
  const state = JSON.parse(content);
70
130
  const rawFavorites = normalizeFavoriteModels(state);
131
+ const rawRecent = normalizeRecentModels(state);
132
+ const shouldValidateWithCatalog = rawFavorites.length > 0 || rawRecent.length > 0;
133
+ const validModelKeys = shouldValidateWithCatalog ? await getValidModelKeys() : null;
134
+ const validatedFavorites = filterModelsByCatalog(rawFavorites, validModelKeys);
135
+ const validatedRecent = filterModelsByCatalog(rawRecent, validModelKeys);
71
136
  const favorites = envDefaultModel
72
- ? dedupeModels([...rawFavorites, envDefaultModel])
73
- : rawFavorites;
137
+ ? dedupeModels([...validatedFavorites, envDefaultModel])
138
+ : validatedFavorites;
74
139
  if (rawFavorites.length === 0 && envDefaultModel) {
75
140
  logger.info(`[ModelManager] No favorites in ${stateFilePath}, using config model as favorite`);
76
141
  }
77
142
  if (favorites.length === 0) {
78
143
  logger.warn(`[ModelManager] No favorites in ${stateFilePath}`);
79
144
  }
80
- const favoriteKeys = new Set(favorites.map((model) => `${model.providerID}/${model.modelID}`));
81
- const recent = dedupeModels(normalizeRecentModels(state)).filter((model) => !favoriteKeys.has(`${model.providerID}/${model.modelID}`));
145
+ const filteredOutFavorites = rawFavorites.length - validatedFavorites.length;
146
+ const filteredOutRecent = rawRecent.length - validatedRecent.length;
147
+ if (filteredOutFavorites > 0 || filteredOutRecent > 0) {
148
+ logger.info(`[ModelManager] Filtered unavailable models from OpenCode state: favoritesRemoved=${filteredOutFavorites}, recentRemoved=${filteredOutRecent}`);
149
+ }
150
+ const favoriteKeys = new Set(favorites.map((model) => getModelKey(model.providerID, model.modelID)));
151
+ const recent = dedupeModels(validatedRecent).filter((model) => !favoriteKeys.has(getModelKey(model.providerID, model.modelID)));
82
152
  logger.debug(`[ModelManager] Loaded model selection lists from ${stateFilePath}: favorites=${favorites.length}, recent=${recent.length}`);
83
153
  return { favorites, recent };
84
154
  }
@@ -97,6 +167,42 @@ export async function getModelSelectionLists() {
97
167
  };
98
168
  }
99
169
  }
170
+ /**
171
+ * Validate stored selected model against OpenCode providers catalog.
172
+ * If selected model is unavailable, fallback to env default model.
173
+ */
174
+ export async function reconcileStoredModelSelection() {
175
+ const currentModel = getCurrentModel();
176
+ if (!currentModel?.providerID || !currentModel.modelID) {
177
+ return;
178
+ }
179
+ const validModelKeys = await getValidModelKeys();
180
+ if (!validModelKeys) {
181
+ logger.warn("[ModelManager] Skipping stored model validation: model catalog unavailable");
182
+ return;
183
+ }
184
+ const currentModelKey = getModelKey(currentModel.providerID, currentModel.modelID);
185
+ if (validModelKeys.has(currentModelKey)) {
186
+ return;
187
+ }
188
+ const envDefaultModel = getEnvDefaultModel();
189
+ if (!envDefaultModel) {
190
+ logger.warn(`[ModelManager] Stored model ${currentModelKey} is unavailable and env default model is missing`);
191
+ return;
192
+ }
193
+ const fallbackKey = getModelKey(envDefaultModel.providerID, envDefaultModel.modelID);
194
+ logger.warn(`[ModelManager] Stored model ${currentModelKey} is unavailable, falling back to ${fallbackKey}`);
195
+ setCurrentModel({
196
+ providerID: envDefaultModel.providerID,
197
+ modelID: envDefaultModel.modelID,
198
+ variant: "default",
199
+ });
200
+ }
201
+ export function __resetModelCatalogCacheForTests() {
202
+ cachedValidModelKeys = null;
203
+ modelCatalogCacheExpiresAt = 0;
204
+ modelCatalogFetchInFlight = null;
205
+ }
100
206
  /**
101
207
  * Get list of favorite models from OpenCode local state file
102
208
  * Falls back to env default model if file is unavailable or empty
@@ -128,7 +128,7 @@ class PinnedMessageManager {
128
128
  */
129
129
  async onMessageComplete(tokens) {
130
130
  // Ensure context limit is available even if session was restored
131
- // without a fresh onSessionChange call (for example after /stop + continue).
131
+ // without a fresh onSessionChange call (for example after /abort + continue).
132
132
  if (this.getContextLimit() === 0) {
133
133
  await this.fetchContextLimit();
134
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grinev/opencode-telegram-bot",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "Telegram bot client for OpenCode to run and monitor coding tasks from chat.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",