opencode-gateway 0.2.3 → 0.2.4

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 (75) hide show
  1. package/dist/cli.js +0 -0
  2. package/dist/index.js +20907 -52
  3. package/package.json +1 -1
  4. package/dist/binding/execution.js +0 -1
  5. package/dist/binding/gateway.js +0 -1
  6. package/dist/binding/index.js +0 -4
  7. package/dist/binding/opencode.js +0 -1
  8. package/dist/cli/args.js +0 -53
  9. package/dist/cli/doctor.js +0 -49
  10. package/dist/cli/init.js +0 -40
  11. package/dist/cli/opencode-config-file.js +0 -18
  12. package/dist/cli/opencode-config.js +0 -194
  13. package/dist/cli/paths.js +0 -22
  14. package/dist/cli/templates.js +0 -41
  15. package/dist/config/cron.js +0 -52
  16. package/dist/config/gateway.js +0 -148
  17. package/dist/config/memory.js +0 -105
  18. package/dist/config/paths.js +0 -39
  19. package/dist/config/telegram.js +0 -91
  20. package/dist/cron/runtime.js +0 -402
  21. package/dist/delivery/telegram.js +0 -75
  22. package/dist/delivery/text.js +0 -175
  23. package/dist/gateway.js +0 -117
  24. package/dist/host/file-sender.js +0 -59
  25. package/dist/host/logger.js +0 -53
  26. package/dist/host/transport.js +0 -35
  27. package/dist/mailbox/router.js +0 -16
  28. package/dist/media/mime.js +0 -45
  29. package/dist/memory/prompt.js +0 -122
  30. package/dist/opencode/adapter.js +0 -340
  31. package/dist/opencode/driver-hub.js +0 -82
  32. package/dist/opencode/event-normalize.js +0 -48
  33. package/dist/opencode/event-stream.js +0 -65
  34. package/dist/opencode/events.js +0 -1
  35. package/dist/questions/client.js +0 -36
  36. package/dist/questions/format.js +0 -36
  37. package/dist/questions/normalize.js +0 -45
  38. package/dist/questions/parser.js +0 -96
  39. package/dist/questions/runtime.js +0 -195
  40. package/dist/questions/types.js +0 -1
  41. package/dist/runtime/attachments.js +0 -12
  42. package/dist/runtime/conversation-coordinator.js +0 -22
  43. package/dist/runtime/executor.js +0 -407
  44. package/dist/runtime/mailbox.js +0 -112
  45. package/dist/runtime/opencode-runner.js +0 -79
  46. package/dist/runtime/runtime-singleton.js +0 -28
  47. package/dist/session/context.js +0 -23
  48. package/dist/session/conversation-key.js +0 -3
  49. package/dist/session/switcher.js +0 -59
  50. package/dist/session/system-prompt.js +0 -52
  51. package/dist/store/migrations.js +0 -197
  52. package/dist/store/sqlite.js +0 -777
  53. package/dist/telegram/client.js +0 -180
  54. package/dist/telegram/media.js +0 -65
  55. package/dist/telegram/normalize.js +0 -119
  56. package/dist/telegram/poller.js +0 -166
  57. package/dist/telegram/runtime.js +0 -157
  58. package/dist/telegram/state.js +0 -149
  59. package/dist/telegram/types.js +0 -1
  60. package/dist/tools/channel-new-session.js +0 -27
  61. package/dist/tools/channel-send-file.js +0 -27
  62. package/dist/tools/channel-target.js +0 -34
  63. package/dist/tools/cron-run.js +0 -20
  64. package/dist/tools/cron-upsert.js +0 -51
  65. package/dist/tools/gateway-dispatch-cron.js +0 -33
  66. package/dist/tools/gateway-status.js +0 -25
  67. package/dist/tools/schedule-cancel.js +0 -12
  68. package/dist/tools/schedule-format.js +0 -48
  69. package/dist/tools/schedule-list.js +0 -17
  70. package/dist/tools/schedule-once.js +0 -43
  71. package/dist/tools/schedule-status.js +0 -23
  72. package/dist/tools/telegram-send-test.js +0 -26
  73. package/dist/tools/telegram-status.js +0 -49
  74. package/dist/tools/time.js +0 -25
  75. package/dist/utils/error.js +0 -57
@@ -1,180 +0,0 @@
1
- import { formatError } from "../utils/error";
2
- export class TelegramApiError extends Error {
3
- retryable;
4
- constructor(message, retryable) {
5
- super(message);
6
- this.retryable = retryable;
7
- this.name = "TelegramApiError";
8
- }
9
- }
10
- export class TelegramBotClient {
11
- botToken;
12
- constructor(botToken) {
13
- this.botToken = botToken;
14
- }
15
- async getUpdates(offset, timeoutSeconds, signal) {
16
- return this.call("getUpdates", {
17
- offset,
18
- timeout: timeoutSeconds,
19
- allowed_updates: ["message", "callback_query"],
20
- }, signal);
21
- }
22
- async getMe() {
23
- return this.call("getMe", {});
24
- }
25
- async getChat(chatId) {
26
- return this.call("getChat", {
27
- chat_id: chatId,
28
- });
29
- }
30
- async getFile(fileId) {
31
- return this.call("getFile", {
32
- file_id: fileId,
33
- });
34
- }
35
- async downloadFile(remotePath, localPath) {
36
- let response;
37
- try {
38
- response = await fetch(`https://api.telegram.org/file/bot${this.botToken}/${remotePath}`);
39
- }
40
- catch (error) {
41
- throw new TelegramApiError(`Telegram file download failed: ${formatError(error)}`, true);
42
- }
43
- if (!response.ok) {
44
- throw new TelegramApiError(`Telegram file download failed (${response.status}): ${response.statusText}`, isRetryableError(response.status, response.status));
45
- }
46
- await Bun.write(localPath, await response.bytes());
47
- }
48
- async sendMessage(chatId, text, messageThreadId) {
49
- await this.call("sendMessage", {
50
- chat_id: chatId,
51
- text,
52
- message_thread_id: parseMessageThreadId(messageThreadId),
53
- });
54
- }
55
- async sendInteractiveMessage(chatId, text, messageThreadId, replyMarkup) {
56
- return await this.call("sendMessage", {
57
- chat_id: chatId,
58
- text,
59
- message_thread_id: parseMessageThreadId(messageThreadId),
60
- reply_markup: replyMarkup,
61
- });
62
- }
63
- async sendPhoto(chatId, filePath, caption, messageThreadId, mimeType) {
64
- const form = new FormData();
65
- form.set("chat_id", chatId);
66
- setOptionalFormField(form, "caption", caption);
67
- setOptionalFormField(form, "message_thread_id", formatMessageThreadId(messageThreadId));
68
- form.set("photo", Bun.file(filePath, { type: mimeType }));
69
- await this.callMultipart("sendPhoto", form);
70
- }
71
- async sendDocument(chatId, filePath, caption, messageThreadId, mimeType) {
72
- const form = new FormData();
73
- form.set("chat_id", chatId);
74
- setOptionalFormField(form, "caption", caption);
75
- setOptionalFormField(form, "message_thread_id", formatMessageThreadId(messageThreadId));
76
- form.set("document", Bun.file(filePath, { type: mimeType }));
77
- await this.callMultipart("sendDocument", form);
78
- }
79
- async sendChatAction(chatId, action, messageThreadId) {
80
- await this.call("sendChatAction", {
81
- chat_id: chatId,
82
- action,
83
- message_thread_id: parseMessageThreadId(messageThreadId),
84
- });
85
- }
86
- async sendMessageDraft(chatId, draftId, text, messageThreadId) {
87
- if (!Number.isSafeInteger(draftId) || draftId === 0) {
88
- throw new Error(`invalid Telegram draft id: ${draftId}`);
89
- }
90
- await this.call("sendMessageDraft", {
91
- chat_id: chatId,
92
- draft_id: draftId,
93
- text,
94
- message_thread_id: parseMessageThreadId(messageThreadId),
95
- });
96
- }
97
- async answerCallbackQuery(callbackQueryId, text) {
98
- await this.call("answerCallbackQuery", {
99
- callback_query_id: callbackQueryId,
100
- text,
101
- });
102
- }
103
- async call(method, body, signal) {
104
- let response;
105
- try {
106
- response = await fetch(`https://api.telegram.org/bot${this.botToken}/${method}`, {
107
- method: "POST",
108
- headers: {
109
- "content-type": "application/json",
110
- },
111
- body: JSON.stringify(stripUndefined(body)),
112
- signal,
113
- });
114
- }
115
- catch (error) {
116
- throw new TelegramApiError(`Telegram ${method} request failed: ${formatError(error)}`, true);
117
- }
118
- const payload = (await response.json());
119
- if (payload.ok) {
120
- return payload.result;
121
- }
122
- const description = payload.description ?? `HTTP ${response.status}`;
123
- const errorCode = payload.error_code ?? response.status;
124
- throw new TelegramApiError(`Telegram ${method} failed (${errorCode}): ${description}`, isRetryableError(errorCode, response.status));
125
- }
126
- async callMultipart(method, body) {
127
- let response;
128
- try {
129
- response = await fetch(`https://api.telegram.org/bot${this.botToken}/${method}`, {
130
- method: "POST",
131
- body,
132
- });
133
- }
134
- catch (error) {
135
- throw new TelegramApiError(`Telegram ${method} request failed: ${formatError(error)}`, true);
136
- }
137
- const payload = (await response.json());
138
- if (payload.ok) {
139
- return payload.result;
140
- }
141
- const description = payload.description ?? `HTTP ${response.status}`;
142
- const errorCode = payload.error_code ?? response.status;
143
- throw new TelegramApiError(`Telegram ${method} failed (${errorCode}): ${description}`, isRetryableError(errorCode, response.status));
144
- }
145
- }
146
- function parseMessageThreadId(value) {
147
- if (value == null) {
148
- return undefined;
149
- }
150
- const normalized = value.trim();
151
- if (normalized.length === 0 || normalized === "undefined") {
152
- return undefined;
153
- }
154
- const parsed = Number.parseInt(normalized, 10);
155
- if (!Number.isSafeInteger(parsed) || parsed <= 0) {
156
- throw new Error(`invalid Telegram topic id: ${value}`);
157
- }
158
- return parsed;
159
- }
160
- function formatMessageThreadId(value) {
161
- const parsed = parseMessageThreadId(value);
162
- return parsed === undefined ? null : String(parsed);
163
- }
164
- function stripUndefined(body) {
165
- return Object.fromEntries(Object.entries(body).filter((entry) => entry[1] !== undefined));
166
- }
167
- function setOptionalFormField(form, key, value) {
168
- if (value != null && value.trim().length > 0) {
169
- form.set(key, value);
170
- }
171
- }
172
- function isRetryableError(errorCode, httpStatus) {
173
- if (errorCode === 401 || errorCode === 403 || errorCode === 404) {
174
- return false;
175
- }
176
- if (httpStatus >= 500 || httpStatus === 429) {
177
- return true;
178
- }
179
- return errorCode !== 400;
180
- }
@@ -1,65 +0,0 @@
1
- import { mkdir } from "node:fs/promises";
2
- import { basename, extname, join } from "node:path";
3
- import { inferLocalFileMimeType } from "../media/mime";
4
- export class TelegramInboundMediaStore {
5
- client;
6
- mediaRootPath;
7
- constructor(client, mediaRootPath) {
8
- this.client = client;
9
- this.mediaRootPath = mediaRootPath;
10
- }
11
- async materializeInboundMessage(message, sourceKind, externalId) {
12
- return {
13
- deliveryTarget: message.deliveryTarget,
14
- sender: message.sender,
15
- text: message.text,
16
- attachments: await Promise.all(message.attachments.map((attachment, index) => this.materializeAttachment(attachment, sourceKind, externalId, index))),
17
- mailboxKey: message.mailboxKey,
18
- };
19
- }
20
- async materializeAttachment(attachment, sourceKind, externalId, ordinal) {
21
- switch (attachment.kind) {
22
- case "image":
23
- return await this.materializeImageAttachment(attachment, sourceKind, externalId, ordinal);
24
- }
25
- }
26
- async materializeImageAttachment(attachment, sourceKind, externalId, ordinal) {
27
- const file = await this.client.getFile(attachment.fileId);
28
- const remotePath = file.file_path?.trim();
29
- if (!remotePath) {
30
- throw new Error(`Telegram file ${attachment.fileId} did not include file_path`);
31
- }
32
- const fileName = normalizeOptionalFileName(attachment.fileName) ??
33
- normalizeOptionalFileName(basename(remotePath)) ??
34
- `telegram-image-${ordinal + 1}${extensionFromRemotePath(remotePath)}`;
35
- const localPath = join(this.mediaRootPath, "telegram", sanitizePathSegment(sourceKind), sanitizePathSegment(externalId), `${ordinal}-${sanitizePathSegment(fileName)}`);
36
- await mkdir(join(this.mediaRootPath, "telegram", sanitizePathSegment(sourceKind), sanitizePathSegment(externalId)), {
37
- recursive: true,
38
- });
39
- await this.client.downloadFile(remotePath, localPath);
40
- return {
41
- kind: "image",
42
- mimeType: attachment.mimeType ?? (await inferLocalFileMimeType(localPath)),
43
- fileName,
44
- localPath,
45
- };
46
- }
47
- }
48
- function sanitizePathSegment(value) {
49
- return (value
50
- .trim()
51
- .replace(/[^a-zA-Z0-9._-]+/g, "_")
52
- .replace(/^_+|_+$/g, "")
53
- .slice(0, 120) || "file");
54
- }
55
- function normalizeOptionalFileName(value) {
56
- if (value === null) {
57
- return null;
58
- }
59
- const trimmed = value.trim();
60
- return trimmed.length === 0 ? null : trimmed;
61
- }
62
- function extensionFromRemotePath(remotePath) {
63
- const extension = extname(remotePath);
64
- return extension.length === 0 ? ".bin" : extension;
65
- }
@@ -1,119 +0,0 @@
1
- export function buildTelegramAllowlist(config) {
2
- return {
3
- allowedChats: new Set(config.allowedChats),
4
- allowedUsers: new Set(config.allowedUsers),
5
- };
6
- }
7
- export function normalizeTelegramUpdate(update, allowlist, mailboxRouter) {
8
- if (update.callback_query) {
9
- return normalizeTelegramCallbackQuery(update.callback_query, allowlist);
10
- }
11
- const message = update.message;
12
- if (!message) {
13
- return ignored("unsupported update type");
14
- }
15
- if (!message.from) {
16
- return ignored("message sender is missing");
17
- }
18
- if (message.from.is_bot === true) {
19
- return ignored("message sender is a bot");
20
- }
21
- const chatId = String(message.chat.id);
22
- const userId = String(message.from.id);
23
- if (!isAllowed(chatId, userId, allowlist)) {
24
- return ignored("message is not allowlisted");
25
- }
26
- const deliveryTarget = {
27
- channel: "telegram",
28
- target: chatId,
29
- topic: message.message_thread_id === undefined ? null : String(message.message_thread_id),
30
- };
31
- const attachments = extractAttachments(message.photo, message.document);
32
- const text = normalizeOptionalText(message.text ?? message.caption ?? null);
33
- if (text === null && attachments.length === 0) {
34
- return ignored("message has no supported content");
35
- }
36
- return {
37
- kind: "message",
38
- chatType: message.chat.type,
39
- message: {
40
- mailboxKey: mailboxRouter?.resolve(deliveryTarget) ?? null,
41
- deliveryTarget,
42
- sender: `telegram:${userId}`,
43
- text,
44
- attachments,
45
- },
46
- };
47
- }
48
- function normalizeTelegramCallbackQuery(callbackQuery, allowlist) {
49
- const message = callbackQuery.message;
50
- if (!message) {
51
- return ignored("callback query message is missing");
52
- }
53
- const chatId = String(message.chat.id);
54
- const userId = String(callbackQuery.from.id);
55
- if (!isAllowed(chatId, userId, allowlist)) {
56
- return ignored("callback query is not allowlisted");
57
- }
58
- return {
59
- kind: "callbackQuery",
60
- callbackQuery: {
61
- callbackQueryId: callbackQuery.id,
62
- sender: `telegram:${userId}`,
63
- deliveryTarget: {
64
- channel: "telegram",
65
- target: chatId,
66
- topic: message.message_thread_id === undefined ? null : String(message.message_thread_id),
67
- },
68
- messageId: message.message_id,
69
- data: normalizeOptionalText(callbackQuery.data ?? null),
70
- },
71
- };
72
- }
73
- function extractAttachments(photo, document) {
74
- const photoAttachment = selectLargestPhoto(photo);
75
- if (photoAttachment !== null) {
76
- return [photoAttachment];
77
- }
78
- if (document?.mime_type?.startsWith("image/") === true) {
79
- return [
80
- {
81
- kind: "image",
82
- fileId: document.file_id,
83
- fileUniqueId: document.file_unique_id ?? null,
84
- mimeType: document.mime_type,
85
- fileName: normalizeOptionalText(document.file_name ?? null),
86
- },
87
- ];
88
- }
89
- return [];
90
- }
91
- function selectLargestPhoto(photo) {
92
- if (!Array.isArray(photo) || photo.length === 0) {
93
- return null;
94
- }
95
- const largest = photo[photo.length - 1];
96
- return {
97
- kind: "image",
98
- fileId: largest.file_id,
99
- fileUniqueId: largest.file_unique_id ?? null,
100
- mimeType: null,
101
- fileName: null,
102
- };
103
- }
104
- function isAllowed(chatId, userId, allowlist) {
105
- return allowlist.allowedChats.has(chatId) || allowlist.allowedUsers.has(userId);
106
- }
107
- function ignored(reason) {
108
- return {
109
- kind: "ignore",
110
- reason,
111
- };
112
- }
113
- function normalizeOptionalText(value) {
114
- if (value === null) {
115
- return null;
116
- }
117
- const trimmed = value.trim();
118
- return trimmed.length === 0 ? null : trimmed;
119
- }
@@ -1,166 +0,0 @@
1
- import { formatError } from "../utils/error";
2
- import { TelegramApiError } from "./client";
3
- import { buildTelegramAllowlist, normalizeTelegramUpdate } from "./normalize";
4
- import { recordTelegramChatType, recordTelegramPollCompleted, recordTelegramPollFailure, recordTelegramPollStarted, recordTelegramPollSuccess, recordTelegramPollTimeout, } from "./state";
5
- const POLL_TIMEOUT_FLOOR_MS = 15_000;
6
- const POLL_TIMEOUT_GRACE_MS = 10_000;
7
- const POLL_STALL_GRACE_MS = 5_000;
8
- export class TelegramPollingService {
9
- client;
10
- mailbox;
11
- store;
12
- logger;
13
- config;
14
- mailboxRouter;
15
- mediaStore;
16
- questions;
17
- allowlist;
18
- timing;
19
- running = false;
20
- inFlightStartedAtMs = null;
21
- consecutiveFailures = 0;
22
- recoveredAtMs = null;
23
- constructor(client, mailbox, store, logger, config, mailboxRouter, mediaStore, questions, timing) {
24
- this.client = client;
25
- this.mailbox = mailbox;
26
- this.store = store;
27
- this.logger = logger;
28
- this.config = config;
29
- this.mailboxRouter = mailboxRouter;
30
- this.mediaStore = mediaStore;
31
- this.questions = questions;
32
- this.allowlist = buildTelegramAllowlist(config);
33
- this.timing = {
34
- timeoutFloorMs: timing?.timeoutFloorMs ?? POLL_TIMEOUT_FLOOR_MS,
35
- timeoutGraceMs: timing?.timeoutGraceMs ?? POLL_TIMEOUT_GRACE_MS,
36
- stallGraceMs: timing?.stallGraceMs ?? POLL_STALL_GRACE_MS,
37
- };
38
- }
39
- start() {
40
- if (this.running) {
41
- return;
42
- }
43
- this.running = true;
44
- void this.runLoop().finally(() => {
45
- this.running = false;
46
- });
47
- }
48
- isRunning() {
49
- return this.running;
50
- }
51
- currentPollStartedAtMs() {
52
- return this.inFlightStartedAtMs;
53
- }
54
- requestTimeoutMs() {
55
- return Math.max(this.config.pollTimeoutSeconds * 1_000 + this.timing.timeoutGraceMs, this.timing.timeoutFloorMs);
56
- }
57
- recoveryRecordedAtMs() {
58
- return this.recoveredAtMs;
59
- }
60
- async runLoop() {
61
- let offset = this.store.getTelegramUpdateOffset();
62
- let retryDelayMs = 1_000;
63
- for (;;) {
64
- const pollStartedAtMs = Date.now();
65
- recordTelegramPollStarted(this.store, pollStartedAtMs);
66
- const controller = new AbortController();
67
- const timeoutHandle = setTimeout(() => {
68
- controller.abort();
69
- }, this.requestTimeoutMs() + this.timing.stallGraceMs);
70
- this.inFlightStartedAtMs = pollStartedAtMs;
71
- try {
72
- const updates = await this.client.getUpdates(offset, this.config.pollTimeoutSeconds, controller.signal);
73
- const recordedAtMs = Date.now();
74
- recordTelegramPollCompleted(this.store, recordedAtMs);
75
- recordTelegramPollSuccess(this.store, recordedAtMs);
76
- if (this.consecutiveFailures > 0) {
77
- this.recoveredAtMs = recordedAtMs;
78
- this.logger.log("info", `telegram poller recovered after ${this.consecutiveFailures} consecutive failure(s)`);
79
- }
80
- for (const update of updates) {
81
- const nextOffset = update.update_id + 1;
82
- const normalized = normalizeTelegramUpdate(update, this.allowlist, this.mailboxRouter);
83
- if (normalized.kind === "ignore") {
84
- this.logger.log("debug", `ignoring telegram update ${update.update_id}: ${normalized.reason}`);
85
- offset = this.advanceOffset(nextOffset);
86
- continue;
87
- }
88
- if (this.store.hasMailboxEntry("telegram_update", String(update.update_id))) {
89
- offset = this.advanceOffset(nextOffset);
90
- continue;
91
- }
92
- if (normalized.kind === "callbackQuery") {
93
- await this.questions.handleTelegramCallbackQuery(normalized.callbackQuery);
94
- offset = this.advanceOffset(nextOffset);
95
- continue;
96
- }
97
- recordTelegramChatType(this.store, normalized.message.deliveryTarget.target, normalized.chatType, Date.now());
98
- await this.mailbox.enqueueInboundMessage(await this.mediaStore.materializeInboundMessage(normalized.message, "telegram_update", String(update.update_id)), "telegram_update", String(update.update_id));
99
- offset = this.advanceOffset(nextOffset);
100
- }
101
- this.consecutiveFailures = 0;
102
- retryDelayMs = 1_000;
103
- }
104
- catch (error) {
105
- const recordedAtMs = Date.now();
106
- recordTelegramPollCompleted(this.store, recordedAtMs);
107
- const pollError = classifyTelegramPollError(error, this.requestTimeoutMs());
108
- this.consecutiveFailures += 1;
109
- if (pollError.kind === "timeout") {
110
- recordTelegramPollTimeout(this.store, pollError.message, recordedAtMs);
111
- }
112
- else {
113
- recordTelegramPollFailure(this.store, pollError.message, recordedAtMs);
114
- }
115
- if (isPermanentTelegramFailure(error)) {
116
- this.logger.log("error", pollError.message);
117
- return;
118
- }
119
- this.logger.log("warn", pollError.message);
120
- await sleep(retryDelayMs);
121
- retryDelayMs = Math.min(retryDelayMs * 2, 15_000);
122
- }
123
- finally {
124
- clearTimeout(timeoutHandle);
125
- this.inFlightStartedAtMs = null;
126
- }
127
- }
128
- }
129
- advanceOffset(offset) {
130
- const recordedAtMs = Date.now();
131
- this.store.putTelegramUpdateOffset(offset, recordedAtMs);
132
- return offset;
133
- }
134
- }
135
- function isPermanentTelegramFailure(error) {
136
- return error instanceof TelegramApiError && !error.retryable;
137
- }
138
- function formatTelegramPollerError(error) {
139
- return `telegram poller failure: ${formatError(error)}`;
140
- }
141
- function classifyTelegramPollError(error, requestTimeoutMs) {
142
- if (isAbortError(error)) {
143
- return {
144
- kind: "timeout",
145
- message: `telegram poller timeout after ${requestTimeoutMs}ms`,
146
- };
147
- }
148
- return {
149
- kind: "error",
150
- message: formatTelegramPollerError(error),
151
- };
152
- }
153
- function isAbortError(error) {
154
- if (error instanceof DOMException) {
155
- return error.name === "AbortError";
156
- }
157
- if (error instanceof Error) {
158
- return error.name === "AbortError";
159
- }
160
- return false;
161
- }
162
- function sleep(durationMs) {
163
- return new Promise((resolve) => {
164
- setTimeout(resolve, durationMs);
165
- });
166
- }
@@ -1,157 +0,0 @@
1
- import { formatUnixMsAsUtc } from "../tools/time";
2
- import { formatError } from "../utils/error";
3
- import { readTelegramHealthSnapshot, recordTelegramProbeFailure, recordTelegramProbeSuccess, recordTelegramSendFailure, } from "./state";
4
- const RECENT_RECOVERY_WINDOW_MS = 60_000;
5
- const POLL_STALLED_GRACE_MS = 5_000;
6
- export class GatewayTelegramRuntime {
7
- client;
8
- delivery;
9
- store;
10
- logger;
11
- config;
12
- polling;
13
- opencodeEvents;
14
- constructor(client, delivery, store, logger, config, polling, opencodeEvents) {
15
- this.client = client;
16
- this.delivery = delivery;
17
- this.store = store;
18
- this.logger = logger;
19
- this.config = config;
20
- this.polling = polling;
21
- this.opencodeEvents = opencodeEvents;
22
- }
23
- isEnabled() {
24
- return this.config.enabled;
25
- }
26
- isPolling() {
27
- return this.polling?.isRunning() ?? false;
28
- }
29
- allowlistMode() {
30
- return this.config.enabled ? "explicit" : "disabled";
31
- }
32
- start() {
33
- this.polling?.start();
34
- }
35
- async status() {
36
- const snapshot = readTelegramHealthSnapshot(this.store);
37
- if (!this.config.enabled || this.client === null) {
38
- return {
39
- ...snapshot,
40
- enabled: false,
41
- polling: false,
42
- pollState: "disabled",
43
- allowlistMode: "disabled",
44
- allowedChatsCount: 0,
45
- allowedUsersCount: 0,
46
- liveProbe: "disabled",
47
- liveProbeError: null,
48
- liveBotId: null,
49
- liveBotUsername: null,
50
- streamingEnabled: false,
51
- opencodeEventStreamConnected: this.opencodeEvents.isConnected(),
52
- lastEventStreamError: this.opencodeEvents.lastStreamError(),
53
- };
54
- }
55
- try {
56
- const bot = await this.client.getMe();
57
- const recordedAtMs = Date.now();
58
- recordTelegramProbeSuccess(this.store, bot, recordedAtMs);
59
- return buildEnabledStatus(this.config, readTelegramHealthSnapshot(this.store), this.polling, "ok", null, bot, this.opencodeEvents);
60
- }
61
- catch (error) {
62
- const message = formatError(error);
63
- const recordedAtMs = Date.now();
64
- recordTelegramProbeFailure(this.store, message, recordedAtMs);
65
- this.logger.log("warn", `telegram live probe failed: ${message}`);
66
- return buildEnabledStatus(this.config, readTelegramHealthSnapshot(this.store), this.polling, "failed", message, null, this.opencodeEvents);
67
- }
68
- }
69
- async sendTest(chatId, topic, text, mode) {
70
- const normalizedChatId = normalizeRequiredField(chatId, "chat_id");
71
- const normalizedTopic = normalizeOptionalField(topic);
72
- const body = normalizeOptionalField(text) ?? defaultTestMessage();
73
- if (!this.config.enabled || this.client === null) {
74
- throw new Error("telegram is not enabled");
75
- }
76
- try {
77
- const sentAtMs = Date.now();
78
- const result = await this.delivery.sendTest({
79
- channel: "telegram",
80
- target: normalizedChatId,
81
- topic: normalizedTopic,
82
- }, body, mode);
83
- if (!result.delivered) {
84
- throw new Error("telegram test delivery produced no final message");
85
- }
86
- return {
87
- chatId: normalizedChatId,
88
- topic: normalizedTopic,
89
- text: body,
90
- sentAtMs,
91
- mode: result.mode,
92
- };
93
- }
94
- catch (error) {
95
- const message = formatError(error);
96
- recordTelegramSendFailure(this.store, message, Date.now());
97
- this.logger.log("warn", `telegram_send_test failed: ${message}`);
98
- throw error;
99
- }
100
- }
101
- }
102
- function buildEnabledStatus(config, snapshot, polling, liveProbe, liveProbeError, bot, opencodeEvents) {
103
- const pollingEnabled = polling?.isRunning() ?? false;
104
- return {
105
- ...snapshot,
106
- enabled: true,
107
- polling: pollingEnabled,
108
- pollState: resolvePollState(snapshot, polling),
109
- allowlistMode: "explicit",
110
- allowedChatsCount: config.allowedChats.length,
111
- allowedUsersCount: config.allowedUsers.length,
112
- liveProbe,
113
- liveProbeError,
114
- liveBotId: bot ? String(bot.id) : null,
115
- liveBotUsername: bot?.username ?? null,
116
- streamingEnabled: true,
117
- opencodeEventStreamConnected: opencodeEvents.isConnected(),
118
- lastEventStreamError: opencodeEvents.lastStreamError(),
119
- };
120
- }
121
- function resolvePollState(snapshot, polling) {
122
- if (polling === null || !polling.isRunning()) {
123
- return "idle";
124
- }
125
- const now = Date.now();
126
- const inFlightStartedAtMs = polling.currentPollStartedAtMs();
127
- if (inFlightStartedAtMs !== null &&
128
- now - inFlightStartedAtMs > polling.requestTimeoutMs() + POLL_STALLED_GRACE_MS) {
129
- return "stalled";
130
- }
131
- const recoveredAtMs = polling.recoveryRecordedAtMs();
132
- if (recoveredAtMs !== null && now - recoveredAtMs <= RECENT_RECOVERY_WINDOW_MS) {
133
- return "recovering";
134
- }
135
- if (snapshot.lastPollStartedMs !== null) {
136
- return "running";
137
- }
138
- return "idle";
139
- }
140
- function normalizeRequiredField(value, field) {
141
- const trimmed = value.trim();
142
- if (trimmed.length === 0) {
143
- throw new Error(`${field} must not be empty`);
144
- }
145
- return trimmed;
146
- }
147
- function normalizeOptionalField(value) {
148
- if (value === null) {
149
- return null;
150
- }
151
- const trimmed = value.trim();
152
- return trimmed.length === 0 ? null : trimmed;
153
- }
154
- function defaultTestMessage() {
155
- const recordedAtMs = Date.now();
156
- return `opencode-gateway telegram_send_test at ${formatUnixMsAsUtc(recordedAtMs)}`;
157
- }