@grinev/opencode-telegram-bot 0.19.2 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
3
3
  import { cleanupBotRuntime, createBot } from "../bot/index.js";
4
4
  import { config } from "../config.js";
5
5
  import { opencodeAutoRestartService } from "../opencode/auto-restart.js";
6
- import { refreshSessionCacheIfOpencodeReady } from "../opencode/ready-refresh.js";
6
+ import { notifyOpencodeReadyIfHealthy, registerOpenCodeReadyRefreshHandler, } from "../opencode/ready-refresh.js";
7
7
  import { loadSettings } from "../settings/manager.js";
8
8
  import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
9
9
  import { reconcileStoredModelSelection } from "../model/manager.js";
@@ -40,12 +40,11 @@ export async function startBotApp() {
40
40
  logger.debug(`[Runtime] Application start mode: ${mode}`);
41
41
  await loadSettings();
42
42
  await reconcileStoredModelSelection();
43
- const autoRestartHandledStartup = await opencodeAutoRestartService.start();
44
- if (!autoRestartHandledStartup) {
45
- await refreshSessionCacheIfOpencodeReady("startup");
46
- }
43
+ registerOpenCodeReadyRefreshHandler();
47
44
  const bot = createBot();
48
45
  await scheduledTaskRuntime.initialize(bot);
46
+ await opencodeAutoRestartService.start();
47
+ await notifyOpencodeReadyIfHealthy("startup");
49
48
  let shutdownStarted = false;
50
49
  let serviceStateCleared = false;
51
50
  let shutdownTimeout = null;
@@ -1,5 +1,6 @@
1
1
  import { opencodeClient } from "../opencode/client.js";
2
2
  import { stopEventListening } from "../opencode/events.js";
3
+ import { isOpencodeServerHealthy } from "../opencode/ready-refresh.js";
3
4
  import { summaryAggregator } from "../summary/aggregator.js";
4
5
  import { pinnedMessageManager } from "../pinned/manager.js";
5
6
  import { keyboardManager } from "../keyboard/manager.js";
@@ -11,6 +12,7 @@ import { getCurrentSession } from "../session/manager.js";
11
12
  import { getCurrentProject } from "../settings/manager.js";
12
13
  import { attachManager } from "./manager.js";
13
14
  import { logger } from "../utils/logger.js";
15
+ import { isExpectedOpencodeUnavailableError } from "../utils/opencode-error.js";
14
16
  function getAttachBusyStatus(sessionId, statuses) {
15
17
  if (!statuses || typeof statuses !== "object") {
16
18
  return false;
@@ -18,13 +20,16 @@ function getAttachBusyStatus(sessionId, statuses) {
18
20
  const sessionStatus = statuses[sessionId];
19
21
  return sessionStatus?.type === "busy";
20
22
  }
21
- async function ensureAttachPinnedSession({ api, chatId, session, }) {
23
+ async function ensureAttachPinnedSession({ api, chatId, session, forceFullRestore = false, }) {
22
24
  if (!pinnedMessageManager.isInitialized()) {
23
25
  pinnedMessageManager.initialize(api, chatId);
24
26
  }
25
27
  keyboardManager.initialize(api, chatId);
26
28
  const pinnedState = pinnedMessageManager.getState();
27
29
  if (pinnedState.sessionId === session.id && pinnedState.messageId) {
30
+ if (forceFullRestore) {
31
+ await pinnedMessageManager.loadContextFromHistory(session.id, session.directory);
32
+ }
28
33
  return;
29
34
  }
30
35
  if (pinnedState.messageId && pinnedState.sessionId === null) {
@@ -51,7 +56,12 @@ async function restorePendingQuestion(bot, chatId, sessionId, directory) {
51
56
  directory,
52
57
  });
53
58
  if (error || !data) {
54
- logger.warn("[Attach] Failed to load pending questions during attach:", error);
59
+ if (isExpectedOpencodeUnavailableError(error)) {
60
+ logger.warn("[Attach] OpenCode server unavailable; skipping pending question restore");
61
+ }
62
+ else {
63
+ logger.warn("[Attach] Failed to load pending questions during attach:", error);
64
+ }
55
65
  return false;
56
66
  }
57
67
  const pendingQuestion = data.find((request) => request.sessionID === sessionId);
@@ -67,7 +77,12 @@ async function restorePendingPermissions(bot, chatId, sessionId, directory) {
67
77
  directory,
68
78
  });
69
79
  if (error || !data) {
70
- logger.warn("[Attach] Failed to load pending permissions during attach:", error);
80
+ if (isExpectedOpencodeUnavailableError(error)) {
81
+ logger.warn("[Attach] OpenCode server unavailable; skipping pending permission restore");
82
+ }
83
+ else {
84
+ logger.warn("[Attach] Failed to load pending permissions during attach:", error);
85
+ }
71
86
  return 0;
72
87
  }
73
88
  const pendingPermissions = data.filter((request) => request.sessionID === sessionId);
@@ -77,12 +92,13 @@ async function restorePendingPermissions(bot, chatId, sessionId, directory) {
77
92
  return pendingPermissions.length;
78
93
  }
79
94
  export async function attachToSession(deps) {
80
- const { bot, chatId, session, ensureEventSubscription } = deps;
95
+ const { bot, chatId, session, ensureEventSubscription, forceFullRestore = false } = deps;
81
96
  const alreadyAttached = attachManager.isAttachedSession(session.id, session.directory);
82
97
  await ensureAttachPinnedSession({
83
98
  api: bot.api,
84
99
  chatId,
85
100
  session,
101
+ forceFullRestore,
86
102
  });
87
103
  if (!alreadyAttached) {
88
104
  await ensureEventSubscription(session.directory);
@@ -98,7 +114,12 @@ export async function attachToSession(deps) {
98
114
  directory: session.directory,
99
115
  });
100
116
  if (statusesError) {
101
- logger.warn("[Attach] Failed to load session status during attach:", statusesError);
117
+ if (isExpectedOpencodeUnavailableError(statusesError)) {
118
+ logger.warn("[Attach] OpenCode server unavailable; skipping session status restore");
119
+ }
120
+ else {
121
+ logger.warn("[Attach] Failed to load session status during attach:", statusesError);
122
+ }
102
123
  }
103
124
  const busy = getAttachBusyStatus(session.id, statuses);
104
125
  if (busy) {
@@ -110,7 +131,9 @@ export async function attachToSession(deps) {
110
131
  await syncPinnedAttachState();
111
132
  let restoredQuestion = false;
112
133
  let restoredPermissions = 0;
113
- if (!alreadyAttached && !questionManager.isActive() && !permissionManager.isActive()) {
134
+ if ((!alreadyAttached || forceFullRestore) &&
135
+ !questionManager.isActive() &&
136
+ !permissionManager.isActive()) {
114
137
  restoredQuestion = await restorePendingQuestion(bot, chatId, session.id, session.directory);
115
138
  if (!restoredQuestion) {
116
139
  restoredPermissions = await restorePendingPermissions(bot, chatId, session.id, session.directory);
@@ -134,11 +157,16 @@ export async function restoreAttachedCurrentSession(deps) {
134
157
  return false;
135
158
  }
136
159
  try {
160
+ if (!(await isOpencodeServerHealthy())) {
161
+ logger.warn(`[Attach] OpenCode server is unavailable; skipping followed session restore: session=${currentSession.id}, directory=${currentSession.directory}`);
162
+ return false;
163
+ }
137
164
  await attachToSession({
138
165
  bot: deps.bot,
139
166
  chatId: deps.chatId,
140
167
  session: currentSession,
141
168
  ensureEventSubscription: deps.ensureEventSubscription,
169
+ forceFullRestore: deps.forceFullRestore,
142
170
  });
143
171
  logger.info(`[Attach] Restored followed session on startup: session=${currentSession.id}, directory=${currentSession.directory}`);
144
172
  return true;
@@ -1,7 +1,7 @@
1
1
  import { config } from "../../config.js";
2
2
  import { opencodeClient } from "../../opencode/client.js";
3
3
  import { resolveLocalOpencodeTarget, startLocalOpencodeServer } from "../../opencode/process.js";
4
- import { refreshSessionCacheAfterOpencodeReady } from "../../opencode/ready-refresh.js";
4
+ import { opencodeReadyLifecycle } from "../../opencode/ready-lifecycle.js";
5
5
  import { logger } from "../../utils/logger.js";
6
6
  import { t } from "../../i18n/index.js";
7
7
  import { editBotText } from "../utils/telegram-text.js";
@@ -43,7 +43,7 @@ export async function opencodeStartCommand(ctx) {
43
43
  const { data, error } = await opencodeClient.global.health();
44
44
  if (!error && data?.healthy) {
45
45
  await ctx.reply(t("opencode_start.already_running", { version: data.version || t("common.unknown") }));
46
- await refreshSessionCacheAfterOpencodeReady("opencode_start_already_running");
46
+ await opencodeReadyLifecycle.notifyReady("opencode_start_already_running");
47
47
  return;
48
48
  }
49
49
  }
@@ -90,7 +90,7 @@ export async function opencodeStartCommand(ctx) {
90
90
  }),
91
91
  });
92
92
  logger.info(`[Bot] OpenCode server started successfully, PID=${pid}, port=${localTarget.port}`);
93
- await refreshSessionCacheAfterOpencodeReady("opencode_start_success");
93
+ await opencodeReadyLifecycle.notifyReady("opencode_start_success");
94
94
  }
95
95
  catch (err) {
96
96
  logger.error("[Bot] Error in /opencode-start command:", err);
package/dist/bot/index.js CHANGED
@@ -40,6 +40,7 @@ import { interactionManager } from "../interaction/manager.js";
40
40
  import { clearAllInteractionState } from "../interaction/cleanup.js";
41
41
  import { keyboardManager } from "../keyboard/manager.js";
42
42
  import { stopEventListening, subscribeToEvents } from "../opencode/events.js";
43
+ import { opencodeReadyLifecycle } from "../opencode/ready-lifecycle.js";
43
44
  import { summaryAggregator } from "../summary/aggregator.js";
44
45
  import { formatToolInfo } from "../summary/formatter.js";
45
46
  import { renderSubagentCards } from "../summary/subagent-formatter.js";
@@ -76,6 +77,7 @@ let botInstance = null;
76
77
  let chatIdInstance = null;
77
78
  let commandsInitialized = false;
78
79
  let heartbeatTimer = null;
80
+ let unsubscribeReadyRestore = null;
79
81
  const TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH = 1024;
80
82
  const RESPONSE_STREAM_THROTTLE_MS = config.bot.responseStreamThrottleMs;
81
83
  const RESPONSE_STREAM_TEXT_LIMIT = 3800;
@@ -809,6 +811,18 @@ export function createBot() {
809
811
  const bot = new Bot(config.telegram.token, botOptions);
810
812
  botInstance = bot;
811
813
  chatIdInstance = config.telegram.allowedUserId;
814
+ unsubscribeReadyRestore?.();
815
+ unsubscribeReadyRestore = opencodeReadyLifecycle.onReady(async (reason) => {
816
+ const restored = await restoreAttachedCurrentSession({
817
+ bot,
818
+ chatId: config.telegram.allowedUserId,
819
+ ensureEventSubscription,
820
+ forceFullRestore: true,
821
+ });
822
+ if (restored) {
823
+ logger.info(`[Bot] Restored followed session after OpenCode ready: reason=${reason}`);
824
+ }
825
+ });
812
826
  // Heartbeat for diagnostics: verify the event loop is not blocked
813
827
  let heartbeatCounter = 0;
814
828
  heartbeatTimer = setInterval(() => {
@@ -1024,14 +1038,6 @@ export function createBot() {
1024
1038
  });
1025
1039
  // Voice and audio message handlers (STT transcription -> prompt)
1026
1040
  const voicePromptDeps = { bot, ensureEventSubscription };
1027
- safeBackgroundTask({
1028
- taskName: "bot.restoreFollowedSession",
1029
- task: () => restoreAttachedCurrentSession({
1030
- bot,
1031
- chatId: config.telegram.allowedUserId,
1032
- ensureEventSubscription,
1033
- }),
1034
- });
1035
1041
  bot.on("message:voice", async (ctx) => {
1036
1042
  logger.debug(`[Bot] Received voice message, chatId=${ctx.chat.id}`);
1037
1043
  botInstance = bot;
@@ -1146,6 +1152,8 @@ export function createBot() {
1146
1152
  return bot;
1147
1153
  }
1148
1154
  export function cleanupBotRuntime(reason) {
1155
+ unsubscribeReadyRestore?.();
1156
+ unsubscribeReadyRestore = null;
1149
1157
  stopEventListening();
1150
1158
  summaryAggregator.clear();
1151
1159
  responseStreamer.clearAll(reason);
@@ -1,9 +1,16 @@
1
1
  import { t } from "../../i18n/index.js";
2
2
  import { escapePlainTextForTelegramMarkdownV2 } from "../../summary/formatter.js";
3
3
  import { sendBotText } from "./telegram-text.js";
4
+ const EXTERNAL_USER_INPUT_MAX_DISPLAY_LENGTH = 2000;
4
5
  function normalizeExternalUserInputText(text) {
5
6
  return text.replace(/\r\n/g, "\n").trim();
6
7
  }
8
+ function truncateExternalUserInputText(text) {
9
+ if (text.length <= EXTERNAL_USER_INPUT_MAX_DISPLAY_LENGTH) {
10
+ return text;
11
+ }
12
+ return `${text.slice(0, EXTERNAL_USER_INPUT_MAX_DISPLAY_LENGTH - 3)}...`;
13
+ }
7
14
  function buildQuotedPlainText(text) {
8
15
  return text
9
16
  .split("\n")
@@ -21,10 +28,11 @@ export function buildExternalUserInputNotification(text) {
21
28
  if (!normalizedText) {
22
29
  return null;
23
30
  }
31
+ const displayText = truncateExternalUserInputText(normalizedText);
24
32
  const title = `👤 ${t("bot.external_user_input")}`;
25
33
  return {
26
- text: `${escapePlainTextForTelegramMarkdownV2(title)}\n\n${buildQuotedMarkdownText(normalizedText)}`,
27
- rawFallbackText: `${title}\n\n${buildQuotedPlainText(normalizedText)}`,
34
+ text: `${escapePlainTextForTelegramMarkdownV2(title)}\n\n${buildQuotedMarkdownText(displayText)}`,
35
+ rawFallbackText: `${title}\n\n${buildQuotedPlainText(displayText)}`,
28
36
  };
29
37
  }
30
38
  export async function deliverExternalUserInputNotification({ api, chatId, currentSessionId, sessionId, text, consumeSuppressedInput, }) {
@@ -1,6 +1,7 @@
1
1
  import { opencodeClient } from "../opencode/client.js";
2
2
  import { logger } from "../utils/logger.js";
3
3
  import { DEFAULT_CONTEXT_LIMIT } from "../pinned/format.js";
4
+ import { isExpectedOpencodeUnavailableError } from "../utils/opencode-error.js";
4
5
  const PROVIDER_CACHE_TTL_MS = 10 * 60 * 1000;
5
6
  const contextLimitCache = new Map();
6
7
  let providersCacheExpiresAt = 0;
@@ -20,7 +21,12 @@ async function refreshContextLimitCache() {
20
21
  try {
21
22
  const { data, error } = await opencodeClient.config.providers();
22
23
  if (error || !data) {
23
- logger.warn("[ModelContextLimit] Failed to fetch providers:", error);
24
+ if (isExpectedOpencodeUnavailableError(error)) {
25
+ logger.warn("[ModelContextLimit] OpenCode server unavailable; using default context limit");
26
+ }
27
+ else {
28
+ logger.warn("[ModelContextLimit] Failed to fetch providers:", error);
29
+ }
24
30
  return;
25
31
  }
26
32
  contextLimitCache.clear();
@@ -35,7 +41,12 @@ async function refreshContextLimitCache() {
35
41
  logger.debug(`[ModelContextLimit] Cached limits for ${contextLimitCache.size} provider/model pairs`);
36
42
  }
37
43
  catch (error) {
38
- logger.warn("[ModelContextLimit] Error refreshing providers cache:", error);
44
+ if (isExpectedOpencodeUnavailableError(error)) {
45
+ logger.warn("[ModelContextLimit] OpenCode server unavailable; using default context limit");
46
+ }
47
+ else {
48
+ logger.warn("[ModelContextLimit] Error refreshing providers cache:", error);
49
+ }
39
50
  }
40
51
  finally {
41
52
  providersFetchInFlight = null;
@@ -1,8 +1,8 @@
1
1
  import { config } from "../config.js";
2
2
  import { logger } from "../utils/logger.js";
3
3
  import { opencodeClient } from "./client.js";
4
+ import { opencodeReadyLifecycle } from "./ready-lifecycle.js";
4
5
  import { resolveLocalOpencodeTarget, startLocalOpencodeServer, } from "./process.js";
5
- import { refreshSessionCacheAfterOpencodeReady } from "./ready-refresh.js";
6
6
  const SERVER_READY_TIMEOUT_MS = 10000;
7
7
  const SERVER_READY_POLL_INTERVAL_MS = 500;
8
8
  function sleep(ms) {
@@ -71,11 +71,12 @@ export class OpencodeAutoRestartService {
71
71
  logger.debug(`[OpenCodeAutoRestart] Health-check succeeded: reason=${reason}`);
72
72
  if (!this.serverWasHealthy) {
73
73
  this.serverWasHealthy = true;
74
- await refreshSessionCacheAfterOpencodeReady(`auto_restart_${reason}`);
74
+ await opencodeReadyLifecycle.notifyReady(`auto_restart_${reason}`);
75
75
  }
76
76
  return;
77
77
  }
78
78
  this.serverWasHealthy = false;
79
+ opencodeReadyLifecycle.notifyUnavailable(`auto_restart_${reason}`);
79
80
  logger.warn(`[OpenCodeAutoRestart] OpenCode server is unavailable, starting local server: reason=${reason}, port=${this.localTarget.port}`);
80
81
  const childProcess = startLocalOpencodeServer(this.localTarget);
81
82
  childProcess.once("error", (error) => {
@@ -90,7 +91,7 @@ export class OpencodeAutoRestartService {
90
91
  }
91
92
  logger.info(`[OpenCodeAutoRestart] OpenCode server recovered: pid=${pid ?? "unknown"}, port=${this.localTarget.port}`);
92
93
  this.serverWasHealthy = true;
93
- await refreshSessionCacheAfterOpencodeReady(`auto_restart_${reason}`);
94
+ await opencodeReadyLifecycle.notifyReady(`auto_restart_${reason}`);
94
95
  }
95
96
  catch (error) {
96
97
  logger.error("[OpenCodeAutoRestart] Failed to check or restart OpenCode server", error);
@@ -1,5 +1,6 @@
1
1
  import { opencodeClient } from "./client.js";
2
2
  import { logger } from "../utils/logger.js";
3
+ import { isExpectedOpencodeUnavailableError } from "../utils/opencode-error.js";
3
4
  const RECONNECT_BASE_DELAY_MS = 1000;
4
5
  const RECONNECT_MAX_DELAY_MS = 15000;
5
6
  const FATAL_NO_STREAM_ERROR = "No stream returned from event subscription";
@@ -108,7 +109,12 @@ export async function subscribeToEvents(directory, callback) {
108
109
  }
109
110
  reconnectAttempt++;
110
111
  const reconnectDelay = getReconnectDelayMs(reconnectAttempt);
111
- logger.error(`Event stream error for ${directory}, reconnecting in ${reconnectDelay}ms (attempt=${reconnectAttempt})`, error);
112
+ if (isExpectedOpencodeUnavailableError(error)) {
113
+ logger.warn(`Event stream unavailable for ${directory}, reconnecting in ${reconnectDelay}ms (attempt=${reconnectAttempt})`);
114
+ }
115
+ else {
116
+ logger.error(`Event stream error for ${directory}, reconnecting in ${reconnectDelay}ms (attempt=${reconnectAttempt})`, error);
117
+ }
112
118
  const shouldContinue = await waitWithAbort(reconnectDelay, controller.signal);
113
119
  if (!shouldContinue) {
114
120
  break;
@@ -121,7 +127,12 @@ export async function subscribeToEvents(directory, callback) {
121
127
  logger.info("Event listener aborted");
122
128
  return;
123
129
  }
124
- logger.error("Event stream error:", error);
130
+ if (isExpectedOpencodeUnavailableError(error)) {
131
+ logger.warn("Event stream unavailable; listener stopped");
132
+ }
133
+ else {
134
+ logger.error("Event stream error:", error);
135
+ }
125
136
  isListening = false;
126
137
  activeDirectory = null;
127
138
  streamAbortController = null;
@@ -0,0 +1,45 @@
1
+ import { logger } from "../utils/logger.js";
2
+ class OpencodeReadyLifecycle {
3
+ ready = false;
4
+ handlers = new Set();
5
+ onReady(handler) {
6
+ this.handlers.add(handler);
7
+ return () => {
8
+ this.handlers.delete(handler);
9
+ };
10
+ }
11
+ isReady() {
12
+ return this.ready;
13
+ }
14
+ async notifyReady(reason) {
15
+ if (this.ready) {
16
+ logger.debug(`[OpenCodeReady] Ready notification ignored: reason=${reason}`);
17
+ return false;
18
+ }
19
+ this.ready = true;
20
+ logger.info(`[OpenCodeReady] OpenCode server is ready: reason=${reason}`);
21
+ for (const handler of this.handlers) {
22
+ try {
23
+ await handler(reason);
24
+ }
25
+ catch (error) {
26
+ logger.warn(`[OpenCodeReady] Ready handler failed: reason=${reason}`, error);
27
+ }
28
+ }
29
+ return true;
30
+ }
31
+ notifyUnavailable(reason) {
32
+ if (!this.ready) {
33
+ logger.debug(`[OpenCodeReady] Unavailable notification ignored: reason=${reason}`);
34
+ return false;
35
+ }
36
+ this.ready = false;
37
+ logger.warn(`[OpenCodeReady] OpenCode server became unavailable: reason=${reason}`);
38
+ return true;
39
+ }
40
+ __resetForTests() {
41
+ this.ready = false;
42
+ this.handlers.clear();
43
+ }
44
+ }
45
+ export const opencodeReadyLifecycle = new OpencodeReadyLifecycle();
@@ -2,7 +2,9 @@ import { reconcileStoredModelSelection } from "../model/manager.js";
2
2
  import { warmupSessionDirectoryCache } from "../session/cache-manager.js";
3
3
  import { logger } from "../utils/logger.js";
4
4
  import { opencodeClient } from "./client.js";
5
- async function isOpencodeServerHealthy() {
5
+ import { opencodeReadyLifecycle } from "./ready-lifecycle.js";
6
+ let readyRefreshRegistered = false;
7
+ export async function isOpencodeServerHealthy() {
6
8
  try {
7
9
  const { data, error } = await opencodeClient.global.health();
8
10
  return !error && data?.healthy === true;
@@ -29,9 +31,25 @@ export async function refreshSessionCacheAfterOpencodeReady(reason) {
29
31
  }
30
32
  export async function refreshSessionCacheIfOpencodeReady(reason) {
31
33
  if (!(await isOpencodeServerHealthy())) {
34
+ opencodeReadyLifecycle.notifyUnavailable(reason);
32
35
  logger.warn(`[OpenCodeReady] OpenCode server is not running; skipping session cache refresh: reason=${reason}`);
33
36
  return false;
34
37
  }
35
38
  await refreshSessionCacheAfterOpencodeReady(reason);
36
39
  return true;
37
40
  }
41
+ export function registerOpenCodeReadyRefreshHandler() {
42
+ if (readyRefreshRegistered) {
43
+ return;
44
+ }
45
+ readyRefreshRegistered = true;
46
+ opencodeReadyLifecycle.onReady((reason) => refreshSessionCacheAfterOpencodeReady(reason));
47
+ }
48
+ export async function notifyOpencodeReadyIfHealthy(reason) {
49
+ if (!(await isOpencodeServerHealthy())) {
50
+ opencodeReadyLifecycle.notifyUnavailable(reason);
51
+ logger.warn(`[OpenCodeReady] OpenCode server is not running: reason=${reason}`);
52
+ return false;
53
+ }
54
+ return opencodeReadyLifecycle.notifyReady(reason);
55
+ }
@@ -5,6 +5,7 @@ import { getCurrentSession } from "../session/manager.js";
5
5
  import { getCurrentProject, getPinnedMessageId, setPinnedMessageId, clearPinnedMessageId, } from "../settings/manager.js";
6
6
  import { getStoredModel } from "../model/manager.js";
7
7
  import { getModelContextLimit } from "../model/context-limit.js";
8
+ import { isExpectedOpencodeUnavailableError } from "../utils/opencode-error.js";
8
9
  import { t } from "../i18n/index.js";
9
10
  import { DEFAULT_CONTEXT_LIMIT, formatContextLine, formatCostLine, formatModelDisplayName, } from "./format.js";
10
11
  class PinnedMessageManager {
@@ -128,7 +129,12 @@ class PinnedMessageManager {
128
129
  directory,
129
130
  });
130
131
  if (error || !messagesData) {
131
- logger.warn("[PinnedManager] Failed to load session history:", error);
132
+ if (isExpectedOpencodeUnavailableError(error)) {
133
+ logger.warn("[PinnedManager] OpenCode server unavailable; skipping session history load");
134
+ }
135
+ else {
136
+ logger.warn("[PinnedManager] Failed to load session history:", error);
137
+ }
132
138
  return;
133
139
  }
134
140
  // Get the maximum context size and total cost from session history
@@ -164,7 +170,12 @@ class PinnedMessageManager {
164
170
  await this.updatePinnedMessage();
165
171
  }
166
172
  catch (err) {
167
- logger.error("[PinnedManager] Error loading context from history:", err);
173
+ if (isExpectedOpencodeUnavailableError(err)) {
174
+ logger.warn("[PinnedManager] OpenCode server unavailable; skipping session history load");
175
+ }
176
+ else {
177
+ logger.error("[PinnedManager] Error loading context from history:", err);
178
+ }
168
179
  }
169
180
  }
170
181
  /**
@@ -337,7 +348,12 @@ class PinnedMessageManager {
337
348
  await this.loadDiffsFromMessages(sessionId, project.worktree);
338
349
  }
339
350
  catch (err) {
340
- logger.debug("[PinnedManager] Could not load diffs from API:", err);
351
+ if (isExpectedOpencodeUnavailableError(err)) {
352
+ logger.debug("[PinnedManager] OpenCode server unavailable; skipping diff restore");
353
+ }
354
+ else {
355
+ logger.debug("[PinnedManager] Could not load diffs from API:", err);
356
+ }
341
357
  }
342
358
  }
343
359
  /**
@@ -351,7 +367,12 @@ class PinnedMessageManager {
351
367
  directory,
352
368
  });
353
369
  if (error || !messagesData) {
354
- logger.debug(`[PinnedManager] loadDiffsFromMessages: error or no data`);
370
+ if (isExpectedOpencodeUnavailableError(error)) {
371
+ logger.debug("[PinnedManager] OpenCode server unavailable; skipping diff message restore");
372
+ }
373
+ else {
374
+ logger.debug(`[PinnedManager] loadDiffsFromMessages: error or no data`);
375
+ }
355
376
  return;
356
377
  }
357
378
  logger.debug(`[PinnedManager] loadDiffsFromMessages: ${messagesData.length} messages`);
@@ -422,7 +443,12 @@ class PinnedMessageManager {
422
443
  }
423
444
  }
424
445
  catch (err) {
425
- logger.debug("[PinnedManager] Could not load diffs from messages:", err);
446
+ if (isExpectedOpencodeUnavailableError(err)) {
447
+ logger.debug("[PinnedManager] OpenCode server unavailable; skipping diff message restore");
448
+ }
449
+ else {
450
+ logger.debug("[PinnedManager] Could not load diffs from messages:", err);
451
+ }
426
452
  }
427
453
  }
428
454
  /**
@@ -445,7 +471,12 @@ class PinnedMessageManager {
445
471
  }
446
472
  }
447
473
  catch (err) {
448
- logger.debug("[PinnedManager] Could not refresh session title:", err);
474
+ if (isExpectedOpencodeUnavailableError(err)) {
475
+ logger.debug("[PinnedManager] OpenCode server unavailable; skipping session title refresh");
476
+ }
477
+ else {
478
+ logger.debug("[PinnedManager] Could not refresh session title:", err);
479
+ }
449
480
  }
450
481
  }
451
482
  /**
@@ -523,7 +554,12 @@ class PinnedMessageManager {
523
554
  logger.debug(`[PinnedManager] Context limit: ${this.contextLimit}`);
524
555
  }
525
556
  catch (err) {
526
- logger.error("[PinnedManager] Error fetching context limit:", err);
557
+ if (isExpectedOpencodeUnavailableError(err)) {
558
+ logger.warn("[PinnedManager] OpenCode server unavailable; using default context limit");
559
+ }
560
+ else {
561
+ logger.error("[PinnedManager] Error fetching context limit:", err);
562
+ }
527
563
  this.contextLimit = DEFAULT_CONTEXT_LIMIT;
528
564
  this.state.tokensLimit = this.contextLimit;
529
565
  }
@@ -0,0 +1,13 @@
1
+ export function isExpectedOpencodeUnavailableError(error) {
2
+ if (!error) {
3
+ return false;
4
+ }
5
+ const message = error instanceof Error ? error.message : String(error);
6
+ const normalized = message.toLowerCase();
7
+ return (normalized.includes("fetch failed") ||
8
+ normalized.includes("failed to fetch") ||
9
+ normalized.includes("econnrefused") ||
10
+ normalized.includes("econnreset") ||
11
+ normalized.includes("enotfound") ||
12
+ normalized.includes("connectex"));
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grinev/opencode-telegram-bot",
3
- "version": "0.19.2",
3
+ "version": "0.19.3",
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",