iosm-cli 0.3.3 → 0.3.5
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/CHANGELOG.md +20 -0
- package/README.md +5 -6
- package/dist/core/background-processes.d.ts.map +1 -1
- package/dist/core/background-processes.js +3 -2
- package/dist/core/background-processes.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +3 -2
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +5 -2
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/modes/telegram/telegram-bridge-mode.d.ts.map +1 -1
- package/dist/modes/telegram/telegram-bridge-mode.js +153 -32
- package/dist/modes/telegram/telegram-bridge-mode.js.map +1 -1
- package/dist/utils/shell.d.ts +8 -0
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +45 -0
- package/dist/utils/shell.js.map +1 -1
- package/docs/configuration.md +2 -0
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ const COMMAND_MENU_TTL_MS = 5 * 60 * 1000;
|
|
|
7
7
|
const MODELS_PAGE_SIZE = 8;
|
|
8
8
|
const MODEL_MENU_TTL_MS = 2 * 60 * 1000;
|
|
9
9
|
const LIVE_STATUS_ANIMATION_MAX_THROTTLE_MS = 850;
|
|
10
|
+
const TELEGRAM_API_MAX_429_RETRIES = 4;
|
|
10
11
|
const INPUT_BUTTON_HUB = "🧭 Hub";
|
|
11
12
|
const INPUT_BUTTON_START = "▶️ Start";
|
|
12
13
|
const INPUT_BUTTON_NEW = "🆕 New";
|
|
@@ -128,29 +129,77 @@ class TelegramBotApi {
|
|
|
128
129
|
return this.callMultipart("sendDocument", form);
|
|
129
130
|
}
|
|
130
131
|
async call(method, payload) {
|
|
131
|
-
|
|
132
|
-
method
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
132
|
+
for (let attempt = 0;; attempt++) {
|
|
133
|
+
const response = await fetch(`${this.endpoint}/${method}`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: {
|
|
136
|
+
"content-type": "application/json",
|
|
137
|
+
},
|
|
138
|
+
body: JSON.stringify(payload),
|
|
139
|
+
});
|
|
140
|
+
const envelope = await this.readEnvelope(response);
|
|
141
|
+
if (response.ok && envelope.ok && envelope.result !== undefined) {
|
|
142
|
+
return envelope.result;
|
|
143
|
+
}
|
|
144
|
+
const errorCode = envelope.error_code ?? response.status;
|
|
145
|
+
const retryAfterMs = this.extractRetryAfterMs(response, envelope.description);
|
|
146
|
+
if (errorCode === 429 && retryAfterMs !== undefined && attempt < TELEGRAM_API_MAX_429_RETRIES) {
|
|
147
|
+
console.warn(`[telegram] ${method} rate-limited, retrying in ${Math.ceil(retryAfterMs / 1000)}s (${attempt + 1}/${TELEGRAM_API_MAX_429_RETRIES})`);
|
|
148
|
+
await sleepTimeout(retryAfterMs);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`Telegram API ${method} failed: ${envelope.description ?? response.statusText} (${errorCode})`);
|
|
141
152
|
}
|
|
142
|
-
return envelope.result;
|
|
143
153
|
}
|
|
144
154
|
async callMultipart(method, form) {
|
|
145
|
-
|
|
146
|
-
method
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
for (let attempt = 0;; attempt++) {
|
|
156
|
+
const response = await fetch(`${this.endpoint}/${method}`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
body: form,
|
|
159
|
+
});
|
|
160
|
+
const envelope = await this.readEnvelope(response);
|
|
161
|
+
if (response.ok && envelope.ok && envelope.result !== undefined) {
|
|
162
|
+
return envelope.result;
|
|
163
|
+
}
|
|
164
|
+
const errorCode = envelope.error_code ?? response.status;
|
|
165
|
+
const retryAfterMs = this.extractRetryAfterMs(response, envelope.description);
|
|
166
|
+
if (errorCode === 429 && retryAfterMs !== undefined && attempt < TELEGRAM_API_MAX_429_RETRIES) {
|
|
167
|
+
console.warn(`[telegram] ${method} rate-limited, retrying in ${Math.ceil(retryAfterMs / 1000)}s (${attempt + 1}/${TELEGRAM_API_MAX_429_RETRIES})`);
|
|
168
|
+
await sleepTimeout(retryAfterMs);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
throw new Error(`Telegram API ${method} failed: ${envelope.description ?? response.statusText} (${errorCode})`);
|
|
152
172
|
}
|
|
153
|
-
|
|
173
|
+
}
|
|
174
|
+
async readEnvelope(response) {
|
|
175
|
+
try {
|
|
176
|
+
return (await response.json());
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
description: response.statusText,
|
|
182
|
+
error_code: response.status,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
extractRetryAfterMs(response, description) {
|
|
187
|
+
const headerValue = response.headers.get("retry-after");
|
|
188
|
+
if (headerValue) {
|
|
189
|
+
const headerSeconds = Number.parseInt(headerValue, 10);
|
|
190
|
+
if (Number.isFinite(headerSeconds) && headerSeconds > 0) {
|
|
191
|
+
return headerSeconds * 1000;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!description)
|
|
195
|
+
return undefined;
|
|
196
|
+
const match = /retry after\s+(\d+)/i.exec(description);
|
|
197
|
+
if (!match)
|
|
198
|
+
return undefined;
|
|
199
|
+
const seconds = Number.parseInt(match[1] ?? "", 10);
|
|
200
|
+
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
201
|
+
return undefined;
|
|
202
|
+
return seconds * 1000;
|
|
154
203
|
}
|
|
155
204
|
}
|
|
156
205
|
export async function runTelegramBridgeMode(options) {
|
|
@@ -218,13 +267,19 @@ class TelegramBridgeRuntime {
|
|
|
218
267
|
void shutdown("SIGTERM");
|
|
219
268
|
});
|
|
220
269
|
this.rpcClient.onEvent((event) => {
|
|
221
|
-
void this.onRpcEvent(event)
|
|
270
|
+
void this.onRpcEvent(event).catch((error) => {
|
|
271
|
+
console.error(`[telegram] rpc event handler error: ${error instanceof Error ? error.message : String(error)}`);
|
|
272
|
+
});
|
|
222
273
|
});
|
|
223
274
|
this.rpcClient.onExtensionUIRequest((request) => {
|
|
224
|
-
void this.onRpcExtensionUiRequest(request)
|
|
275
|
+
void this.onRpcExtensionUiRequest(request).catch((error) => {
|
|
276
|
+
console.error(`[telegram] rpc extension ui handler error: ${error instanceof Error ? error.message : String(error)}`);
|
|
277
|
+
});
|
|
225
278
|
});
|
|
226
279
|
this.rpcClient.onRequiresConfirmation((event) => {
|
|
227
|
-
void this.onRpcConfirmationRequest(event)
|
|
280
|
+
void this.onRpcConfirmationRequest(event).catch((error) => {
|
|
281
|
+
console.error(`[telegram] rpc confirmation handler error: ${error instanceof Error ? error.message : String(error)}`);
|
|
282
|
+
});
|
|
228
283
|
});
|
|
229
284
|
// Keep polling forever.
|
|
230
285
|
for (;;) {
|
|
@@ -2030,6 +2085,53 @@ class TelegramBridgeRuntime {
|
|
|
2030
2085
|
formatStartingStatus(prompt) {
|
|
2031
2086
|
return [`⏳ ${APP_NAME} · start · 0s · q${this.queue.length}`, `“${this.formatPromptPreview(prompt, 56)}”`].join("\n");
|
|
2032
2087
|
}
|
|
2088
|
+
isStatusMessageInvalidError(error) {
|
|
2089
|
+
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
2090
|
+
return (message.includes("message_id_invalid") ||
|
|
2091
|
+
message.includes("message to edit not found") ||
|
|
2092
|
+
message.includes("message can't be edited"));
|
|
2093
|
+
}
|
|
2094
|
+
extractTelegramRetryAfterMs(error) {
|
|
2095
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2096
|
+
const match = /retry after\s+(\d+)/i.exec(message);
|
|
2097
|
+
if (!match)
|
|
2098
|
+
return undefined;
|
|
2099
|
+
const seconds = Number.parseInt(match[1] ?? "", 10);
|
|
2100
|
+
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
2101
|
+
return undefined;
|
|
2102
|
+
return seconds * 1000;
|
|
2103
|
+
}
|
|
2104
|
+
scheduleStatusEditRetry(turnId, retryAfterMs) {
|
|
2105
|
+
if (!this.activeTurn || this.activeTurn.turnId !== turnId)
|
|
2106
|
+
return;
|
|
2107
|
+
if (this.activeTurn.statusEditPending)
|
|
2108
|
+
return;
|
|
2109
|
+
this.activeTurn.statusEditPending = true;
|
|
2110
|
+
this.activeTurn.statusEditTimer = setTimeout(() => {
|
|
2111
|
+
if (this.activeTurn && this.activeTurn.turnId === turnId) {
|
|
2112
|
+
this.activeTurn.statusEditPending = false;
|
|
2113
|
+
void this.editLiveStatus(true);
|
|
2114
|
+
}
|
|
2115
|
+
}, retryAfterMs);
|
|
2116
|
+
}
|
|
2117
|
+
async recreateLiveStatusMessage(target, statusText) {
|
|
2118
|
+
// Status message may be removed by user or become invalid in Telegram storage.
|
|
2119
|
+
// Recreate it and continue editing against the new message_id.
|
|
2120
|
+
if (!this.activeTurn || this.activeTurn.turnId !== target.turnId)
|
|
2121
|
+
return false;
|
|
2122
|
+
try {
|
|
2123
|
+
const fresh = await this.bot.sendMessage(target.chatId, statusText, {
|
|
2124
|
+
replyMarkup: { inline_keyboard: [] },
|
|
2125
|
+
});
|
|
2126
|
+
target.statusMessageId = fresh.message_id;
|
|
2127
|
+
target.lastStatusEditAt = Date.now();
|
|
2128
|
+
return true;
|
|
2129
|
+
}
|
|
2130
|
+
catch (error) {
|
|
2131
|
+
console.warn(`[telegram] status recreation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2132
|
+
return false;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2033
2135
|
formatLiveStatus(turn = this.activeTurn) {
|
|
2034
2136
|
if (!turn)
|
|
2035
2137
|
return "No active task.";
|
|
@@ -2074,10 +2176,21 @@ class TelegramBridgeRuntime {
|
|
|
2074
2176
|
.then(() => {
|
|
2075
2177
|
target.lastStatusEditAt = Date.now();
|
|
2076
2178
|
})
|
|
2077
|
-
.catch((error) => {
|
|
2179
|
+
.catch(async (error) => {
|
|
2078
2180
|
const message = error instanceof Error ? error.message : String(error);
|
|
2079
2181
|
// Ignore no-op edit errors and transient "message is not modified".
|
|
2080
2182
|
if (!message.toLowerCase().includes("message is not modified")) {
|
|
2183
|
+
const retryAfterMs = this.extractTelegramRetryAfterMs(error);
|
|
2184
|
+
if (retryAfterMs) {
|
|
2185
|
+
console.warn(`[telegram] status edit rate-limited, retrying in ${Math.ceil(retryAfterMs / 1000)}s`);
|
|
2186
|
+
this.scheduleStatusEditRetry(turnId, retryAfterMs);
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
if (this.isStatusMessageInvalidError(error)) {
|
|
2190
|
+
const recovered = await this.recreateLiveStatusMessage(target, statusText);
|
|
2191
|
+
if (recovered)
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2081
2194
|
console.warn(`[telegram] status edit failed: ${message}`);
|
|
2082
2195
|
}
|
|
2083
2196
|
})
|
|
@@ -2129,17 +2242,25 @@ class TelegramBridgeRuntime {
|
|
|
2129
2242
|
replyMarkup: { inline_keyboard: [] },
|
|
2130
2243
|
});
|
|
2131
2244
|
}
|
|
2132
|
-
catch {
|
|
2133
|
-
// Best-effort.
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2245
|
+
catch (error) {
|
|
2246
|
+
// Best-effort, but recover from invalid status message ids to avoid losing final state.
|
|
2247
|
+
if (this.isStatusMessageInvalidError(error)) {
|
|
2248
|
+
await this.bot.sendMessage(finishedTurn.chatId, statusLabel).catch(() => { });
|
|
2249
|
+
}
|
|
2137
2250
|
}
|
|
2138
|
-
|
|
2139
|
-
|
|
2251
|
+
try {
|
|
2252
|
+
if (options?.error) {
|
|
2253
|
+
await this.bot.sendMessage(finishedTurn.chatId, `Task failed: ${options.error}`);
|
|
2254
|
+
}
|
|
2255
|
+
else if (finalText && finalText.trim().length > 0) {
|
|
2256
|
+
await this.sendFinalOutput(finishedTurn.chatId, finalText);
|
|
2257
|
+
}
|
|
2258
|
+
else if (!finishedTurn.aborted) {
|
|
2259
|
+
await this.bot.sendMessage(finishedTurn.chatId, "Task completed with no assistant text output.");
|
|
2260
|
+
}
|
|
2140
2261
|
}
|
|
2141
|
-
|
|
2142
|
-
|
|
2262
|
+
catch (error) {
|
|
2263
|
+
console.warn(`[telegram] final output delivery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2143
2264
|
}
|
|
2144
2265
|
await this.drainQueue();
|
|
2145
2266
|
}
|