@grinev/opencode-telegram-bot 0.13.1 → 0.14.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 +0 -8
- package/README.md +25 -26
- package/dist/bot/commands/sessions.js +1 -0
- package/dist/bot/index.js +136 -29
- package/dist/bot/streaming/response-streamer.js +6 -4
- package/dist/bot/streaming/tool-call-streamer.js +285 -0
- package/dist/bot/utils/finalize-assistant-response.js +20 -11
- package/dist/bot/utils/send-with-markdown-fallback.js +13 -2
- package/dist/bot/utils/thinking-message.js +1 -5
- package/dist/config.js +0 -16
- package/dist/i18n/de.js +13 -1
- package/dist/i18n/en.js +13 -1
- package/dist/i18n/es.js +13 -1
- package/dist/i18n/fr.js +13 -1
- package/dist/i18n/ru.js +13 -1
- package/dist/i18n/zh.js +13 -1
- package/dist/model/context-limit.js +57 -0
- package/dist/pinned/format.js +29 -0
- package/dist/pinned/manager.js +30 -55
- package/dist/scheduled-task/runtime.js +27 -21
- package/dist/summary/aggregator.js +435 -16
- package/dist/summary/formatter.js +84 -12
- package/dist/summary/subagent-formatter.js +63 -0
- package/dist/summary/tool-message-batcher.js +14 -209
- package/package.json +1 -1
package/dist/pinned/manager.js
CHANGED
|
@@ -3,7 +3,9 @@ import { opencodeClient } from "../opencode/client.js";
|
|
|
3
3
|
import { getCurrentSession } from "../session/manager.js";
|
|
4
4
|
import { getCurrentProject, getPinnedMessageId, setPinnedMessageId, clearPinnedMessageId, } from "../settings/manager.js";
|
|
5
5
|
import { getStoredModel } from "../model/manager.js";
|
|
6
|
+
import { getModelContextLimit } from "../model/context-limit.js";
|
|
6
7
|
import { t } from "../i18n/index.js";
|
|
8
|
+
import { DEFAULT_CONTEXT_LIMIT, formatContextLine, formatCostLine, formatModelDisplayName, } from "./format.js";
|
|
7
9
|
class PinnedMessageManager {
|
|
8
10
|
api = null;
|
|
9
11
|
chatId = null;
|
|
@@ -147,6 +149,22 @@ class PinnedMessageManager {
|
|
|
147
149
|
await this.refreshSessionTitle();
|
|
148
150
|
await this.updatePinnedMessage();
|
|
149
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Update tokens in memory without triggering an API call.
|
|
154
|
+
* Used for intermediate (non-completed) message.updated events
|
|
155
|
+
* to keep pinned state in sync with keyboardManager.
|
|
156
|
+
*/
|
|
157
|
+
updateTokensSilent(tokens) {
|
|
158
|
+
this.state.tokensUsed = tokens.input + tokens.cacheRead;
|
|
159
|
+
logger.debug(`[PinnedManager] Tokens updated (silent): ${this.state.tokensUsed}/${this.state.tokensLimit}`);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Refresh the pinned message with current in-memory state.
|
|
163
|
+
* Used at thinking time to push accumulated silent updates to Telegram.
|
|
164
|
+
*/
|
|
165
|
+
async refresh() {
|
|
166
|
+
await this.updatePinnedMessage();
|
|
167
|
+
}
|
|
150
168
|
/**
|
|
151
169
|
* Called when cost info is received from SSE events
|
|
152
170
|
*/
|
|
@@ -162,6 +180,12 @@ class PinnedMessageManager {
|
|
|
162
180
|
setOnKeyboardUpdate(callback) {
|
|
163
181
|
this.onKeyboardUpdateCallback = callback;
|
|
164
182
|
logger.debug("[PinnedManager] Keyboard update callback registered");
|
|
183
|
+
// Fire immediately with current state to fix race condition:
|
|
184
|
+
// onSessionChange may have already run before this callback was registered.
|
|
185
|
+
const limit = this.state.tokensLimit > 0 ? this.state.tokensLimit : this.contextLimit || 0;
|
|
186
|
+
if (limit > 0) {
|
|
187
|
+
callback(this.state.tokensUsed, limit);
|
|
188
|
+
}
|
|
165
189
|
}
|
|
166
190
|
/**
|
|
167
191
|
* Get current context information
|
|
@@ -412,38 +436,13 @@ class PinnedMessageManager {
|
|
|
412
436
|
async fetchContextLimit() {
|
|
413
437
|
try {
|
|
414
438
|
const model = getStoredModel();
|
|
415
|
-
|
|
416
|
-
logger.warn("[PinnedManager] No model configured, using default limit");
|
|
417
|
-
this.contextLimit = 200000;
|
|
418
|
-
this.state.tokensLimit = this.contextLimit;
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
const { data: providersData, error } = await opencodeClient.config.providers();
|
|
422
|
-
if (error || !providersData) {
|
|
423
|
-
logger.warn("[PinnedManager] Failed to fetch providers, using default limit");
|
|
424
|
-
this.contextLimit = 200000;
|
|
425
|
-
this.state.tokensLimit = this.contextLimit;
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
// Find the model in providers
|
|
429
|
-
for (const provider of providersData.providers) {
|
|
430
|
-
if (provider.id === model.providerID) {
|
|
431
|
-
const modelInfo = provider.models[model.modelID];
|
|
432
|
-
if (modelInfo?.limit?.context) {
|
|
433
|
-
this.contextLimit = modelInfo.limit.context;
|
|
434
|
-
this.state.tokensLimit = this.contextLimit;
|
|
435
|
-
logger.debug(`[PinnedManager] Context limit: ${this.contextLimit}`);
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
logger.warn("[PinnedManager] Model not found in providers, using default limit");
|
|
441
|
-
this.contextLimit = 200000;
|
|
439
|
+
this.contextLimit = await getModelContextLimit(model.providerID, model.modelID);
|
|
442
440
|
this.state.tokensLimit = this.contextLimit;
|
|
441
|
+
logger.debug(`[PinnedManager] Context limit: ${this.contextLimit}`);
|
|
443
442
|
}
|
|
444
443
|
catch (err) {
|
|
445
444
|
logger.error("[PinnedManager] Error fetching context limit:", err);
|
|
446
|
-
this.contextLimit =
|
|
445
|
+
this.contextLimit = DEFAULT_CONTEXT_LIMIT;
|
|
447
446
|
this.state.tokensLimit = this.contextLimit;
|
|
448
447
|
}
|
|
449
448
|
}
|
|
@@ -451,28 +450,16 @@ class PinnedMessageManager {
|
|
|
451
450
|
* Format the pinned message text
|
|
452
451
|
*/
|
|
453
452
|
formatMessage() {
|
|
454
|
-
const percentage = this.state.tokensLimit > 0
|
|
455
|
-
? Math.round((this.state.tokensUsed / this.state.tokensLimit) * 100)
|
|
456
|
-
: 0;
|
|
457
|
-
const tokensFormatted = this.formatTokenCount(this.state.tokensUsed);
|
|
458
|
-
const limitFormatted = this.formatTokenCount(this.state.tokensLimit);
|
|
459
|
-
// Get current model info
|
|
460
453
|
const currentModel = getStoredModel();
|
|
461
|
-
const modelName = currentModel.providerID
|
|
462
|
-
? `${currentModel.providerID}/${currentModel.modelID}`
|
|
463
|
-
: t("pinned.unknown");
|
|
454
|
+
const modelName = formatModelDisplayName(currentModel.providerID, currentModel.modelID);
|
|
464
455
|
const lines = [
|
|
465
456
|
`${this.state.sessionTitle}`,
|
|
466
457
|
t("pinned.line.project", { project: this.state.projectName }),
|
|
467
458
|
t("pinned.line.model", { model: modelName }),
|
|
468
|
-
|
|
469
|
-
used: tokensFormatted,
|
|
470
|
-
limit: limitFormatted,
|
|
471
|
-
percent: percentage,
|
|
472
|
-
}),
|
|
459
|
+
formatContextLine(this.state.tokensUsed, this.state.tokensLimit),
|
|
473
460
|
];
|
|
474
461
|
if (this.state.cost !== undefined && this.state.cost !== null) {
|
|
475
|
-
lines.push(
|
|
462
|
+
lines.push(formatCostLine(this.state.cost));
|
|
476
463
|
}
|
|
477
464
|
if (this.state.changedFiles.length > 0) {
|
|
478
465
|
const maxFiles = 10;
|
|
@@ -496,18 +483,6 @@ class PinnedMessageManager {
|
|
|
496
483
|
}
|
|
497
484
|
return lines.join("\n");
|
|
498
485
|
}
|
|
499
|
-
/**
|
|
500
|
-
* Format token count (e.g., 150000 -> "150K")
|
|
501
|
-
*/
|
|
502
|
-
formatTokenCount(count) {
|
|
503
|
-
if (count >= 1000000) {
|
|
504
|
-
return `${(count / 1000000).toFixed(1)}M`;
|
|
505
|
-
}
|
|
506
|
-
else if (count >= 1000) {
|
|
507
|
-
return `${Math.round(count / 1000)}K`;
|
|
508
|
-
}
|
|
509
|
-
return count.toString();
|
|
510
|
-
}
|
|
511
486
|
/**
|
|
512
487
|
* Create and pin a new status message
|
|
513
488
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { config } from "../config.js";
|
|
2
|
+
import { escapePlainTextForTelegramMarkdownV2, formatSummaryWithMode, } from "../summary/formatter.js";
|
|
2
3
|
import { t } from "../i18n/index.js";
|
|
3
4
|
import { logger } from "../utils/logger.js";
|
|
4
5
|
import { safeBackgroundTask } from "../utils/safe-background-task.js";
|
|
@@ -7,28 +8,30 @@ import { executeScheduledTask } from "./executor.js";
|
|
|
7
8
|
import { foregroundSessionState } from "./foreground-state.js";
|
|
8
9
|
import { computeNextRunAt, isTaskDue } from "./next-run.js";
|
|
9
10
|
import { getScheduledTask, listScheduledTasks, removeScheduledTask, replaceScheduledTasks, updateScheduledTask, } from "./store.js";
|
|
10
|
-
const TELEGRAM_MESSAGE_LIMIT = 4096;
|
|
11
11
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
12
|
+
const TELEGRAM_MESSAGE_LIMIT = 4096;
|
|
12
13
|
const TASK_DESCRIPTION_PREVIEW_LENGTH = 64;
|
|
13
14
|
const RESTART_INTERRUPTED_ERROR = "Interrupted by bot restart during scheduled task execution.";
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
function getScheduledTaskDeliveryFormat() {
|
|
16
|
+
return config.bot.messageFormatMode === "markdown" ? "markdown_v2" : "raw";
|
|
17
|
+
}
|
|
18
|
+
function buildScheduledTaskSuccessMessageParts(delivery) {
|
|
19
|
+
if (!delivery.resultText) {
|
|
20
|
+
return [delivery.notificationText];
|
|
17
21
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
parts.push(remaining.slice(0, splitIndex).trim());
|
|
26
|
-
remaining = remaining.slice(splitIndex).trimStart();
|
|
22
|
+
if (config.bot.messageFormatMode !== "markdown") {
|
|
23
|
+
return formatSummaryWithMode(`${delivery.notificationText}\n\n${delivery.resultText}`, config.bot.messageFormatMode);
|
|
24
|
+
}
|
|
25
|
+
const header = escapePlainTextForTelegramMarkdownV2(delivery.notificationText);
|
|
26
|
+
const resultParts = formatSummaryWithMode(delivery.resultText, config.bot.messageFormatMode);
|
|
27
|
+
if (resultParts.length === 0) {
|
|
28
|
+
return [header];
|
|
27
29
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
const firstPart = `${header}\n\n${resultParts[0]}`;
|
|
31
|
+
if (firstPart.length <= TELEGRAM_MESSAGE_LIMIT) {
|
|
32
|
+
return [firstPart, ...resultParts.slice(1)];
|
|
30
33
|
}
|
|
31
|
-
return
|
|
34
|
+
return [header, ...resultParts];
|
|
32
35
|
}
|
|
33
36
|
function normalizeTaskPrompt(prompt) {
|
|
34
37
|
const normalized = prompt.replace(/\s+/g, " ").trim();
|
|
@@ -44,10 +47,10 @@ function buildSuccessDelivery(task, runAt, resultText) {
|
|
|
44
47
|
prompt: task.prompt,
|
|
45
48
|
runAt,
|
|
46
49
|
status: "success",
|
|
47
|
-
|
|
50
|
+
notificationText: t("task.run.success", {
|
|
48
51
|
description: normalizeTaskPrompt(task.prompt),
|
|
49
|
-
result: resultText,
|
|
50
52
|
}),
|
|
53
|
+
resultText,
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
56
|
function buildErrorDelivery(task, runAt, errorMessage) {
|
|
@@ -57,7 +60,7 @@ function buildErrorDelivery(task, runAt, errorMessage) {
|
|
|
57
60
|
prompt: task.prompt,
|
|
58
61
|
runAt,
|
|
59
62
|
status: "error",
|
|
60
|
-
|
|
63
|
+
notificationText: t("task.run.error", {
|
|
61
64
|
description: normalizeTaskPrompt(task.prompt),
|
|
62
65
|
error: errorMessage,
|
|
63
66
|
}),
|
|
@@ -342,13 +345,16 @@ export class ScheduledTaskRuntime {
|
|
|
342
345
|
return false;
|
|
343
346
|
}
|
|
344
347
|
try {
|
|
345
|
-
const messageParts =
|
|
348
|
+
const messageParts = delivery.status === "success"
|
|
349
|
+
? buildScheduledTaskSuccessMessageParts(delivery)
|
|
350
|
+
: [delivery.notificationText];
|
|
351
|
+
const format = delivery.status === "success" ? getScheduledTaskDeliveryFormat() : "raw";
|
|
346
352
|
for (const part of messageParts) {
|
|
347
353
|
await sendBotText({
|
|
348
354
|
api: this.botApi,
|
|
349
355
|
chatId: this.chatId,
|
|
350
356
|
text: part,
|
|
351
|
-
format
|
|
357
|
+
format,
|
|
352
358
|
});
|
|
353
359
|
}
|
|
354
360
|
return true;
|