@grinev/opencode-telegram-bot 0.19.2 → 0.20.0
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/.env.example +5 -0
- package/README.md +5 -0
- package/dist/app/start-bot-app.js +4 -5
- package/dist/attach/service.js +34 -8
- package/dist/background-session/tracker.js +137 -0
- package/dist/bot/commands/definitions.js +2 -0
- package/dist/bot/commands/detach.js +54 -0
- package/dist/bot/commands/ls.js +445 -0
- package/dist/bot/commands/open.js +9 -4
- package/dist/bot/commands/opencode-start.js +3 -3
- package/dist/bot/commands/projects.js +6 -2
- package/dist/bot/commands/sessions.js +233 -82
- package/dist/bot/commands/worktree.js +7 -2
- package/dist/bot/handlers/inline-menu.js +1 -0
- package/dist/bot/index.js +86 -16
- package/dist/bot/utils/external-user-input.js +10 -2
- package/dist/bot/utils/send-downloaded-file.js +57 -0
- package/dist/bot/utils/switch-project.js +9 -14
- package/dist/config.js +1 -0
- package/dist/i18n/de.js +26 -0
- package/dist/i18n/en.js +26 -0
- package/dist/i18n/es.js +26 -0
- package/dist/i18n/fr.js +26 -0
- package/dist/i18n/ru.js +26 -0
- package/dist/i18n/zh.js +26 -0
- package/dist/interaction/busy.js +1 -1
- package/dist/interaction/manager.js +1 -1
- package/dist/model/context-limit.js +13 -2
- package/dist/opencode/auto-restart.js +4 -3
- package/dist/opencode/events.js +13 -2
- package/dist/opencode/ready-lifecycle.js +45 -0
- package/dist/opencode/ready-refresh.js +19 -1
- package/dist/pinned/manager.js +43 -7
- package/dist/scheduled-task/executor.js +3 -0
- package/dist/scheduled-task/runtime.js +2 -0
- package/dist/scheduled-task/schedule-parser.js +6 -0
- package/dist/scheduled-task/session-ignore.js +53 -0
- package/dist/settings/manager.js +12 -0
- package/dist/utils/opencode-error.js +13 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/pinned/manager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -2,6 +2,7 @@ import { config } from "../config.js";
|
|
|
2
2
|
import { t } from "../i18n/index.js";
|
|
3
3
|
import { opencodeClient } from "../opencode/client.js";
|
|
4
4
|
import { logger } from "../utils/logger.js";
|
|
5
|
+
import { cleanupScheduledTaskSessionIgnores, registerScheduledTaskSessionIgnore, } from "./session-ignore.js";
|
|
5
6
|
export const SCHEDULED_TASK_AGENT = "build";
|
|
6
7
|
const SCHEDULED_TASK_SESSION_TITLE = "Scheduled task run";
|
|
7
8
|
const EXECUTION_POLL_INTERVAL_MS = 2000;
|
|
@@ -298,6 +299,7 @@ export async function executeScheduledTask(task) {
|
|
|
298
299
|
let sessionId = null;
|
|
299
300
|
let deleteTemporarySession = true;
|
|
300
301
|
try {
|
|
302
|
+
await cleanupScheduledTaskSessionIgnores();
|
|
301
303
|
const { data: session, error: createError } = await opencodeClient.session.create({
|
|
302
304
|
directory: task.projectWorktree,
|
|
303
305
|
title: SCHEDULED_TASK_SESSION_TITLE,
|
|
@@ -306,6 +308,7 @@ export async function executeScheduledTask(task) {
|
|
|
306
308
|
throw createError || new Error("Failed to create temporary scheduled task session");
|
|
307
309
|
}
|
|
308
310
|
sessionId = session.id;
|
|
311
|
+
await registerScheduledTaskSessionIgnore(session.id);
|
|
309
312
|
const promptOptions = {
|
|
310
313
|
sessionID: session.id,
|
|
311
314
|
directory: session.directory,
|
|
@@ -7,6 +7,7 @@ import { sendBotText } from "../bot/utils/telegram-text.js";
|
|
|
7
7
|
import { formatAssistantRunFooter } from "../bot/utils/assistant-run-footer.js";
|
|
8
8
|
import { executeScheduledTask, SCHEDULED_TASK_AGENT } from "./executor.js";
|
|
9
9
|
import { foregroundSessionState } from "./foreground-state.js";
|
|
10
|
+
import { cleanupScheduledTaskSessionIgnores } from "./session-ignore.js";
|
|
10
11
|
import { computeNextRunAt, isTaskDue } from "./next-run.js";
|
|
11
12
|
import { getScheduledTask, listScheduledTasks, removeScheduledTask, replaceScheduledTasks, updateScheduledTask, } from "./store.js";
|
|
12
13
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
@@ -96,6 +97,7 @@ export class ScheduledTaskRuntime {
|
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
98
99
|
this.initialized = true;
|
|
100
|
+
await cleanupScheduledTaskSessionIgnores();
|
|
99
101
|
await this.recoverTasksOnStartup();
|
|
100
102
|
await this.flushDeferredDeliveries();
|
|
101
103
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { opencodeClient } from "../opencode/client.js";
|
|
2
2
|
import { logger } from "../utils/logger.js";
|
|
3
|
+
import { cleanupScheduledTaskSessionIgnores, registerScheduledTaskSessionIgnore, } from "./session-ignore.js";
|
|
3
4
|
const SCHEDULE_PARSE_SESSION_TITLE = "Scheduled task schedule parser";
|
|
4
5
|
function getLocalTimezone() {
|
|
5
6
|
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
@@ -133,6 +134,8 @@ export async function parseTaskSchedule(scheduleText, directory) {
|
|
|
133
134
|
const timezone = getLocalTimezone();
|
|
134
135
|
let sessionId = null;
|
|
135
136
|
try {
|
|
137
|
+
logger.debug(`[ScheduledTaskScheduleParser] Parsing schedule: directory=${trimmedDirectory}, textLength=${trimmedScheduleText.length}`);
|
|
138
|
+
await cleanupScheduledTaskSessionIgnores();
|
|
136
139
|
const { data: session, error: createError } = await opencodeClient.session.create({
|
|
137
140
|
directory: trimmedDirectory,
|
|
138
141
|
title: SCHEDULE_PARSE_SESSION_TITLE,
|
|
@@ -141,6 +144,8 @@ export async function parseTaskSchedule(scheduleText, directory) {
|
|
|
141
144
|
throw createError || new Error("Failed to create temporary schedule parser session");
|
|
142
145
|
}
|
|
143
146
|
sessionId = session.id;
|
|
147
|
+
await registerScheduledTaskSessionIgnore(session.id);
|
|
148
|
+
logger.debug(`[ScheduledTaskScheduleParser] Created temporary session: sessionId=${session.id}`);
|
|
144
149
|
const { data: response, error: promptError } = await opencodeClient.session.prompt({
|
|
145
150
|
sessionID: session.id,
|
|
146
151
|
directory: session.directory,
|
|
@@ -151,6 +156,7 @@ export async function parseTaskSchedule(scheduleText, directory) {
|
|
|
151
156
|
throw promptError || new Error("Failed to parse schedule");
|
|
152
157
|
}
|
|
153
158
|
const responseText = collectResponseText(response.parts);
|
|
159
|
+
logger.debug(`[ScheduledTaskScheduleParser] Received parser response: sessionId=${session.id}, textLength=${responseText.length}`);
|
|
154
160
|
if (!responseText) {
|
|
155
161
|
throw new Error("Schedule parser returned an empty response");
|
|
156
162
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getScheduledTaskSessionIgnores, setScheduledTaskSessionIgnores, } from "../settings/manager.js";
|
|
2
|
+
const SCHEDULED_TASK_SESSION_IGNORE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
3
|
+
let mutationQueue = Promise.resolve();
|
|
4
|
+
function isFreshIgnore(ignore, nowMs) {
|
|
5
|
+
const createdAtMs = Date.parse(ignore.createdAt);
|
|
6
|
+
return !Number.isNaN(createdAtMs) && nowMs - createdAtMs < SCHEDULED_TASK_SESSION_IGNORE_TTL_MS;
|
|
7
|
+
}
|
|
8
|
+
function pruneExpiredIgnores(ignores, nowMs) {
|
|
9
|
+
return ignores.filter((ignore) => isFreshIgnore(ignore, nowMs));
|
|
10
|
+
}
|
|
11
|
+
async function mutateIgnores(mutator) {
|
|
12
|
+
const runMutation = async () => {
|
|
13
|
+
const currentIgnores = getScheduledTaskSessionIgnores();
|
|
14
|
+
const { ignores, result } = mutator(currentIgnores);
|
|
15
|
+
await setScheduledTaskSessionIgnores(ignores);
|
|
16
|
+
return result;
|
|
17
|
+
};
|
|
18
|
+
const mutationPromise = mutationQueue.then(runMutation, runMutation);
|
|
19
|
+
mutationQueue = mutationPromise.catch(() => undefined);
|
|
20
|
+
return mutationPromise;
|
|
21
|
+
}
|
|
22
|
+
export function isScheduledTaskSessionIgnored(sessionId, now = new Date()) {
|
|
23
|
+
const nowMs = now.getTime();
|
|
24
|
+
return getScheduledTaskSessionIgnores().some((ignore) => ignore.sessionId === sessionId && isFreshIgnore(ignore, nowMs));
|
|
25
|
+
}
|
|
26
|
+
export async function registerScheduledTaskSessionIgnore(sessionId, createdAt = new Date()) {
|
|
27
|
+
await mutateIgnores((ignores) => {
|
|
28
|
+
const nowMs = createdAt.getTime();
|
|
29
|
+
const nextIgnores = pruneExpiredIgnores(ignores, nowMs).filter((ignore) => ignore.sessionId !== sessionId);
|
|
30
|
+
return {
|
|
31
|
+
ignores: [...nextIgnores, { sessionId, createdAt: createdAt.toISOString() }],
|
|
32
|
+
result: undefined,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export async function removeScheduledTaskSessionIgnore(sessionId) {
|
|
37
|
+
await mutateIgnores((ignores) => ({
|
|
38
|
+
ignores: ignores.filter((ignore) => ignore.sessionId !== sessionId),
|
|
39
|
+
result: undefined,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
export async function cleanupScheduledTaskSessionIgnores(now = new Date()) {
|
|
43
|
+
return mutateIgnores((ignores) => {
|
|
44
|
+
const nextIgnores = pruneExpiredIgnores(ignores, now.getTime());
|
|
45
|
+
return {
|
|
46
|
+
ignores: nextIgnores,
|
|
47
|
+
result: ignores.length - nextIgnores.length,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function __resetScheduledTaskSessionIgnoreForTests() {
|
|
52
|
+
mutationQueue = Promise.resolve();
|
|
53
|
+
}
|
package/dist/settings/manager.js
CHANGED
|
@@ -5,6 +5,9 @@ import { logger } from "../utils/logger.js";
|
|
|
5
5
|
function cloneScheduledTasks(tasks) {
|
|
6
6
|
return tasks?.map((task) => cloneScheduledTask(task));
|
|
7
7
|
}
|
|
8
|
+
function cloneScheduledTaskSessionIgnores(ignores) {
|
|
9
|
+
return ignores?.map((ignore) => ({ ...ignore }));
|
|
10
|
+
}
|
|
8
11
|
function getSettingsFilePath() {
|
|
9
12
|
return getRuntimePaths().settingsFilePath;
|
|
10
13
|
}
|
|
@@ -121,6 +124,13 @@ export function setScheduledTasks(tasks) {
|
|
|
121
124
|
currentSettings.scheduledTasks = cloneScheduledTasks(tasks);
|
|
122
125
|
return writeSettingsFile(currentSettings);
|
|
123
126
|
}
|
|
127
|
+
export function getScheduledTaskSessionIgnores() {
|
|
128
|
+
return cloneScheduledTaskSessionIgnores(currentSettings.scheduledTaskSessionIgnores) ?? [];
|
|
129
|
+
}
|
|
130
|
+
export function setScheduledTaskSessionIgnores(ignores) {
|
|
131
|
+
currentSettings.scheduledTaskSessionIgnores = cloneScheduledTaskSessionIgnores(ignores);
|
|
132
|
+
return writeSettingsFile(currentSettings);
|
|
133
|
+
}
|
|
124
134
|
export function __resetSettingsForTests() {
|
|
125
135
|
currentSettings = {};
|
|
126
136
|
settingsWriteQueue = Promise.resolve();
|
|
@@ -138,6 +148,8 @@ export async function loadSettings() {
|
|
|
138
148
|
}
|
|
139
149
|
currentSettings = loadedSettings;
|
|
140
150
|
currentSettings.scheduledTasks = cloneScheduledTasks(loadedSettings.scheduledTasks) ?? [];
|
|
151
|
+
currentSettings.scheduledTaskSessionIgnores =
|
|
152
|
+
cloneScheduledTaskSessionIgnores(loadedSettings.scheduledTaskSessionIgnores) ?? [];
|
|
141
153
|
if (requiresRewrite) {
|
|
142
154
|
void writeSettingsFile(currentSettings);
|
|
143
155
|
}
|
|
@@ -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
|
+
}
|