@grinev/opencode-telegram-bot 0.13.2 → 0.14.1

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/dist/i18n/de.js CHANGED
@@ -217,6 +217,18 @@ export const de = {
217
217
  "pinned.line.model": "Modell: {model}",
218
218
  "pinned.line.context": "Kontext: {used} / {limit} ({percent}%)",
219
219
  "pinned.line.cost": "Kosten: {cost} ausgegeben",
220
+ "subagent.header": "Subagent {agent}: {description}",
221
+ "subagent.line.status": "Status: {status}",
222
+ "subagent.line.task": "Aufgabe: {task}",
223
+ "subagent.line.agent": "Agent: {agent}",
224
+ "subagent.working": "Arbeitet...",
225
+ "subagent.working_with_details": "Arbeitet: {details}",
226
+ "subagent.completed": "Abgeschlossen",
227
+ "subagent.failed": "Aufgabe fehlgeschlagen",
228
+ "subagent.status.pending": "ausstehend",
229
+ "subagent.status.running": "laeuft",
230
+ "subagent.status.completed": "abgeschlossen",
231
+ "subagent.status.error": "Fehler",
220
232
  "pinned.files.title": "Dateien ({count}):",
221
233
  "pinned.files.item": " {path}{diff}",
222
234
  "pinned.files.more": " ... und {count} mehr",
package/dist/i18n/en.js CHANGED
@@ -217,6 +217,18 @@ export const en = {
217
217
  "pinned.line.model": "Model: {model}",
218
218
  "pinned.line.context": "Context: {used} / {limit} ({percent}%)",
219
219
  "pinned.line.cost": "Cost: {cost} spent",
220
+ "subagent.header": "Subagent {agent}: {description}",
221
+ "subagent.line.status": "Status: {status}",
222
+ "subagent.line.task": "Task: {task}",
223
+ "subagent.line.agent": "Agent: {agent}",
224
+ "subagent.working": "Working...",
225
+ "subagent.working_with_details": "Working: {details}",
226
+ "subagent.completed": "Completed",
227
+ "subagent.failed": "Task failed",
228
+ "subagent.status.pending": "pending",
229
+ "subagent.status.running": "running",
230
+ "subagent.status.completed": "completed",
231
+ "subagent.status.error": "error",
220
232
  "pinned.files.title": "Files ({count}):",
221
233
  "pinned.files.item": " {path}{diff}",
222
234
  "pinned.files.more": " ... and {count} more",
package/dist/i18n/es.js CHANGED
@@ -217,6 +217,18 @@ export const es = {
217
217
  "pinned.line.model": "Modelo: {model}",
218
218
  "pinned.line.context": "Contexto: {used} / {limit} ({percent}%)",
219
219
  "pinned.line.cost": "Costo: {cost} gastado",
220
+ "subagent.header": "Subagente {agent}: {description}",
221
+ "subagent.line.status": "Estado: {status}",
222
+ "subagent.line.task": "Tarea: {task}",
223
+ "subagent.line.agent": "Agente: {agent}",
224
+ "subagent.working": "Trabajando...",
225
+ "subagent.working_with_details": "Trabajando: {details}",
226
+ "subagent.completed": "Completada",
227
+ "subagent.failed": "Error de tarea",
228
+ "subagent.status.pending": "pendiente",
229
+ "subagent.status.running": "en ejecucion",
230
+ "subagent.status.completed": "completado",
231
+ "subagent.status.error": "error",
220
232
  "pinned.files.title": "Archivos ({count}):",
221
233
  "pinned.files.item": " {path}{diff}",
222
234
  "pinned.files.more": " ... y {count} más",
package/dist/i18n/fr.js CHANGED
@@ -217,6 +217,18 @@ export const fr = {
217
217
  "pinned.line.model": "Modèle : {model}",
218
218
  "pinned.line.context": "Contexte : {used} / {limit} ({percent}%)",
219
219
  "pinned.line.cost": "Coût : {cost} dépensé",
220
+ "subagent.header": "Sous-agent {agent} : {description}",
221
+ "subagent.line.status": "Statut : {status}",
222
+ "subagent.line.task": "Tache : {task}",
223
+ "subagent.line.agent": "Agent : {agent}",
224
+ "subagent.working": "En cours...",
225
+ "subagent.working_with_details": "En cours : {details}",
226
+ "subagent.completed": "Terminee",
227
+ "subagent.failed": "Echec de la tache",
228
+ "subagent.status.pending": "en attente",
229
+ "subagent.status.running": "en cours",
230
+ "subagent.status.completed": "termine",
231
+ "subagent.status.error": "erreur",
220
232
  "pinned.files.title": "Fichiers ({count}) :",
221
233
  "pinned.files.item": " {path}{diff}",
222
234
  "pinned.files.more": " ... et encore {count}",
package/dist/i18n/ru.js CHANGED
@@ -217,6 +217,18 @@ export const ru = {
217
217
  "pinned.line.model": "Модель: {model}",
218
218
  "pinned.line.context": "Контекст: {used} / {limit} ({percent}%)",
219
219
  "pinned.line.cost": "Стоимость: {cost} потрачено",
220
+ "subagent.header": "Сабагент {agent}: {description}",
221
+ "subagent.line.status": "Статус: {status}",
222
+ "subagent.line.task": "Задача: {task}",
223
+ "subagent.line.agent": "Агент: {agent}",
224
+ "subagent.working": "В работе...",
225
+ "subagent.working_with_details": "В работе: {details}",
226
+ "subagent.completed": "Завершена",
227
+ "subagent.failed": "Ошибка задачи",
228
+ "subagent.status.pending": "ожидание",
229
+ "subagent.status.running": "в работе",
230
+ "subagent.status.completed": "завершен",
231
+ "subagent.status.error": "ошибка",
220
232
  "pinned.files.title": "Файлы ({count}):",
221
233
  "pinned.files.item": " {path}{diff}",
222
234
  "pinned.files.more": " ... и еще {count}",
package/dist/i18n/zh.js CHANGED
@@ -217,6 +217,18 @@ export const zh = {
217
217
  "pinned.line.model": "模型: {model}",
218
218
  "pinned.line.context": "上下文: {used} / {limit} ({percent}%)",
219
219
  "pinned.line.cost": "费用: {cost}",
220
+ "subagent.header": "子代理 {agent}: {description}",
221
+ "subagent.line.status": "状态: {status}",
222
+ "subagent.line.task": "任务: {task}",
223
+ "subagent.line.agent": "代理: {agent}",
224
+ "subagent.working": "执行中...",
225
+ "subagent.working_with_details": "执行中: {details}",
226
+ "subagent.completed": "已完成",
227
+ "subagent.failed": "任务失败",
228
+ "subagent.status.pending": "等待中",
229
+ "subagent.status.running": "运行中",
230
+ "subagent.status.completed": "已完成",
231
+ "subagent.status.error": "错误",
220
232
  "pinned.files.title": "文件({count}):",
221
233
  "pinned.files.item": " {path}{diff}",
222
234
  "pinned.files.more": " ... 还有 {count} 个",
@@ -0,0 +1,57 @@
1
+ import { opencodeClient } from "../opencode/client.js";
2
+ import { logger } from "../utils/logger.js";
3
+ import { DEFAULT_CONTEXT_LIMIT } from "../pinned/format.js";
4
+ const PROVIDER_CACHE_TTL_MS = 10 * 60 * 1000;
5
+ const contextLimitCache = new Map();
6
+ let providersCacheExpiresAt = 0;
7
+ let providersFetchInFlight = null;
8
+ function getModelKey(providerID, modelID) {
9
+ return `${providerID}/${modelID}`;
10
+ }
11
+ async function refreshContextLimitCache() {
12
+ if (Date.now() < providersCacheExpiresAt) {
13
+ return;
14
+ }
15
+ if (providersFetchInFlight) {
16
+ await providersFetchInFlight;
17
+ return;
18
+ }
19
+ providersFetchInFlight = (async () => {
20
+ try {
21
+ const { data, error } = await opencodeClient.config.providers();
22
+ if (error || !data) {
23
+ logger.warn("[ModelContextLimit] Failed to fetch providers:", error);
24
+ return;
25
+ }
26
+ contextLimitCache.clear();
27
+ for (const provider of data.providers) {
28
+ for (const [modelID, model] of Object.entries(provider.models)) {
29
+ if (model?.limit?.context) {
30
+ contextLimitCache.set(getModelKey(provider.id, modelID), model.limit.context);
31
+ }
32
+ }
33
+ }
34
+ providersCacheExpiresAt = Date.now() + PROVIDER_CACHE_TTL_MS;
35
+ logger.debug(`[ModelContextLimit] Cached limits for ${contextLimitCache.size} provider/model pairs`);
36
+ }
37
+ catch (error) {
38
+ logger.warn("[ModelContextLimit] Error refreshing providers cache:", error);
39
+ }
40
+ finally {
41
+ providersFetchInFlight = null;
42
+ }
43
+ })();
44
+ await providersFetchInFlight;
45
+ }
46
+ export async function getModelContextLimit(providerID, modelID) {
47
+ if (!providerID || !modelID) {
48
+ return DEFAULT_CONTEXT_LIMIT;
49
+ }
50
+ const cacheKey = getModelKey(providerID, modelID);
51
+ const cachedLimit = contextLimitCache.get(cacheKey);
52
+ if (cachedLimit) {
53
+ return cachedLimit;
54
+ }
55
+ await refreshContextLimitCache();
56
+ return contextLimitCache.get(cacheKey) ?? DEFAULT_CONTEXT_LIMIT;
57
+ }
@@ -0,0 +1,29 @@
1
+ import { t } from "../i18n/index.js";
2
+ export const DEFAULT_CONTEXT_LIMIT = 200000;
3
+ export function formatTokenCount(count) {
4
+ if (count >= 1000000) {
5
+ return `${(count / 1000000).toFixed(1)}M`;
6
+ }
7
+ if (count >= 1000) {
8
+ return `${Math.round(count / 1000)}K`;
9
+ }
10
+ return count.toString();
11
+ }
12
+ export function formatModelDisplayName(providerID, modelID) {
13
+ if (providerID && modelID) {
14
+ return `${providerID}/${modelID}`;
15
+ }
16
+ return t("pinned.unknown");
17
+ }
18
+ export function formatContextLine(tokensUsed, tokensLimit) {
19
+ const safeLimit = typeof tokensLimit === "number" && tokensLimit > 0 ? tokensLimit : null;
20
+ const percentage = safeLimit ? Math.round((tokensUsed / safeLimit) * 100) : 0;
21
+ return t("pinned.line.context", {
22
+ used: formatTokenCount(tokensUsed),
23
+ limit: safeLimit ? formatTokenCount(safeLimit) : t("pinned.unknown"),
24
+ percent: percentage,
25
+ });
26
+ }
27
+ export function formatCostLine(cost) {
28
+ return t("pinned.line.cost", { cost: `$${cost.toFixed(2)}` });
29
+ }
@@ -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;
@@ -21,6 +23,11 @@ class PinnedMessageManager {
21
23
  };
22
24
  contextLimit = null;
23
25
  onKeyboardUpdateCallback;
26
+ updateDebounceTimer = null;
27
+ updateTask = null;
28
+ pendingUpdate = false;
29
+ pendingForceUpdate = false;
30
+ lastRenderedMessageText = null;
24
31
  /**
25
32
  * Initialize manager with bot API and chat ID
26
33
  */
@@ -56,6 +63,9 @@ class PinnedMessageManager {
56
63
  }
57
64
  // Reset changed files for new session
58
65
  this.state.changedFiles = [];
66
+ this.lastRenderedMessageText = null;
67
+ this.pendingUpdate = false;
68
+ this.pendingForceUpdate = false;
59
69
  // Unpin old message and create new one
60
70
  await this.unpinOldMessage();
61
71
  await this.createPinnedMessage();
@@ -147,10 +157,30 @@ class PinnedMessageManager {
147
157
  await this.refreshSessionTitle();
148
158
  await this.updatePinnedMessage();
149
159
  }
160
+ /**
161
+ * Update tokens in memory without triggering an API call.
162
+ * Used for intermediate (non-completed) message.updated events
163
+ * to keep pinned state in sync with keyboardManager.
164
+ */
165
+ updateTokensSilent(tokens) {
166
+ this.state.tokensUsed = tokens.input + tokens.cacheRead;
167
+ logger.debug(`[PinnedManager] Tokens updated (silent): ${this.state.tokensUsed}/${this.state.tokensLimit}`);
168
+ }
169
+ /**
170
+ * Refresh the pinned message with current in-memory state.
171
+ * Used at thinking time to push accumulated silent updates to Telegram.
172
+ */
173
+ async refresh() {
174
+ await this.updatePinnedMessage(true);
175
+ }
150
176
  /**
151
177
  * Called when cost info is received from SSE events
152
178
  */
153
179
  async onCostUpdate(cost) {
180
+ if (!Number.isFinite(cost) || cost === 0) {
181
+ logger.debug("[PinnedManager] Ignoring non-impacting cost update");
182
+ return;
183
+ }
154
184
  const currentCost = this.state.cost || 0;
155
185
  this.state.cost = currentCost + cost;
156
186
  logger.debug(`[PinnedManager] Cost added: $${cost.toFixed(2)}, total session: $${(this.state.cost || 0).toFixed(2)}`);
@@ -162,6 +192,12 @@ class PinnedMessageManager {
162
192
  setOnKeyboardUpdate(callback) {
163
193
  this.onKeyboardUpdateCallback = callback;
164
194
  logger.debug("[PinnedManager] Keyboard update callback registered");
195
+ // Fire immediately with current state to fix race condition:
196
+ // onSessionChange may have already run before this callback was registered.
197
+ const limit = this.state.tokensLimit > 0 ? this.state.tokensLimit : this.contextLimit || 0;
198
+ if (limit > 0) {
199
+ callback(this.state.tokensUsed, limit);
200
+ }
165
201
  }
166
202
  /**
167
203
  * Get current context information
@@ -199,6 +235,10 @@ class PinnedMessageManager {
199
235
  logger.debug("[PinnedManager] Ignoring empty session.diff, keeping tool-collected data");
200
236
  return;
201
237
  }
238
+ if (this.areFileDiffsEqual(this.state.changedFiles, diffs)) {
239
+ logger.debug("[PinnedManager] Ignoring unchanged session.diff");
240
+ return;
241
+ }
202
242
  this.state.changedFiles = diffs;
203
243
  logger.debug(`[PinnedManager] Session diff updated: ${diffs.length} files`);
204
244
  await this.updatePinnedMessage();
@@ -219,15 +259,14 @@ class PinnedMessageManager {
219
259
  // Schedule debounced update (avoid spamming Telegram API on rapid tool events)
220
260
  this.scheduleDebouncedUpdate();
221
261
  }
222
- updateDebounceTimer = null;
223
262
  scheduleDebouncedUpdate() {
224
263
  if (this.updateDebounceTimer) {
225
264
  clearTimeout(this.updateDebounceTimer);
226
265
  }
227
266
  this.updateDebounceTimer = setTimeout(() => {
228
267
  this.updateDebounceTimer = null;
229
- this.updatePinnedMessage();
230
- }, 500);
268
+ void this.updatePinnedMessage();
269
+ }, 1000);
231
270
  }
232
271
  /**
233
272
  * Load file diffs from API for current session.
@@ -406,44 +445,34 @@ class PinnedMessageManager {
406
445
  return normalized;
407
446
  return ".../" + segments.slice(-3).join("/");
408
447
  }
448
+ areFileDiffsEqual(current, next) {
449
+ if (current.length !== next.length) {
450
+ return false;
451
+ }
452
+ for (let index = 0; index < current.length; index++) {
453
+ const left = current[index];
454
+ const right = next[index];
455
+ if (left.file !== right.file ||
456
+ left.additions !== right.additions ||
457
+ left.deletions !== right.deletions) {
458
+ return false;
459
+ }
460
+ }
461
+ return true;
462
+ }
409
463
  /**
410
464
  * Fetch context limit from current model configuration
411
465
  */
412
466
  async fetchContextLimit() {
413
467
  try {
414
468
  const model = getStoredModel();
415
- if (!model.providerID || !model.modelID) {
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;
469
+ this.contextLimit = await getModelContextLimit(model.providerID, model.modelID);
442
470
  this.state.tokensLimit = this.contextLimit;
471
+ logger.debug(`[PinnedManager] Context limit: ${this.contextLimit}`);
443
472
  }
444
473
  catch (err) {
445
474
  logger.error("[PinnedManager] Error fetching context limit:", err);
446
- this.contextLimit = 200000;
475
+ this.contextLimit = DEFAULT_CONTEXT_LIMIT;
447
476
  this.state.tokensLimit = this.contextLimit;
448
477
  }
449
478
  }
@@ -451,28 +480,16 @@ class PinnedMessageManager {
451
480
  * Format the pinned message text
452
481
  */
453
482
  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
483
  const currentModel = getStoredModel();
461
- const modelName = currentModel.providerID && currentModel.modelID
462
- ? `${currentModel.providerID}/${currentModel.modelID}`
463
- : t("pinned.unknown");
484
+ const modelName = formatModelDisplayName(currentModel.providerID, currentModel.modelID);
464
485
  const lines = [
465
486
  `${this.state.sessionTitle}`,
466
487
  t("pinned.line.project", { project: this.state.projectName }),
467
488
  t("pinned.line.model", { model: modelName }),
468
- t("pinned.line.context", {
469
- used: tokensFormatted,
470
- limit: limitFormatted,
471
- percent: percentage,
472
- }),
489
+ formatContextLine(this.state.tokensUsed, this.state.tokensLimit),
473
490
  ];
474
491
  if (this.state.cost !== undefined && this.state.cost !== null) {
475
- lines.push(t("pinned.line.cost", { cost: `$${this.state.cost.toFixed(2)}` }));
492
+ lines.push(formatCostLine(this.state.cost));
476
493
  }
477
494
  if (this.state.changedFiles.length > 0) {
478
495
  const maxFiles = 10;
@@ -496,18 +513,6 @@ class PinnedMessageManager {
496
513
  }
497
514
  return lines.join("\n");
498
515
  }
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
516
  /**
512
517
  * Create and pin a new status message
513
518
  */
@@ -523,6 +528,7 @@ class PinnedMessageManager {
523
528
  this.state.messageId = sentMessage.message_id;
524
529
  this.state.chatId = this.chatId;
525
530
  this.state.lastUpdated = Date.now();
531
+ this.lastRenderedMessageText = text;
526
532
  // Save to settings for persistence
527
533
  setPinnedMessageId(sentMessage.message_id);
528
534
  // Pin the message (silently)
@@ -538,36 +544,67 @@ class PinnedMessageManager {
538
544
  /**
539
545
  * Update existing pinned message text
540
546
  */
541
- async updatePinnedMessage() {
547
+ async updatePinnedMessage(forceUpdate = false) {
542
548
  if (!this.api || !this.chatId || !this.state.messageId) {
543
549
  return;
544
550
  }
545
- try {
546
- const text = this.formatMessage();
547
- await this.api.editMessageText(this.chatId, this.state.messageId, text);
548
- this.state.lastUpdated = Date.now();
549
- logger.debug(`[PinnedManager] Updated pinned message: ${this.state.messageId}`);
550
- // Trigger keyboard update callback
551
- if (this.onKeyboardUpdateCallback && this.state.tokensLimit > 0) {
552
- setImmediate(() => {
553
- this.onKeyboardUpdateCallback(this.state.tokensUsed, this.state.tokensLimit);
554
- });
555
- }
551
+ this.pendingUpdate = true;
552
+ if (forceUpdate) {
553
+ this.pendingForceUpdate = true;
556
554
  }
557
- catch (err) {
558
- // Handle "message is not modified" error silently
559
- if (err instanceof Error && err.message.includes("message is not modified")) {
555
+ if (this.updateTask) {
556
+ await this.updateTask;
557
+ return;
558
+ }
559
+ this.updateTask = this.flushPendingPinnedUpdates().finally(() => {
560
+ this.updateTask = null;
561
+ });
562
+ await this.updateTask;
563
+ }
564
+ async flushPendingPinnedUpdates() {
565
+ while (this.pendingUpdate) {
566
+ this.pendingUpdate = false;
567
+ const shouldForceUpdate = this.pendingForceUpdate;
568
+ this.pendingForceUpdate = false;
569
+ if (!this.api || !this.chatId || !this.state.messageId) {
560
570
  return;
561
571
  }
562
- // Handle "message to edit not found" - recreate
563
- if (err instanceof Error && err.message.includes("message to edit not found")) {
564
- logger.warn("[PinnedManager] Pinned message was deleted, recreating...");
565
- this.state.messageId = null;
566
- clearPinnedMessageId();
567
- await this.createPinnedMessage();
568
- return;
572
+ const text = this.formatMessage();
573
+ if (!shouldForceUpdate && text === this.lastRenderedMessageText) {
574
+ logger.debug("[PinnedManager] Skipping pinned update: message content unchanged");
575
+ continue;
576
+ }
577
+ try {
578
+ await this.api.editMessageText(this.chatId, this.state.messageId, text);
579
+ this.state.lastUpdated = Date.now();
580
+ this.lastRenderedMessageText = text;
581
+ logger.debug(`[PinnedManager] Updated pinned message: ${this.state.messageId}`);
582
+ // Trigger keyboard update callback
583
+ if (this.onKeyboardUpdateCallback && this.state.tokensLimit > 0) {
584
+ setImmediate(() => {
585
+ this.onKeyboardUpdateCallback(this.state.tokensUsed, this.state.tokensLimit);
586
+ });
587
+ }
588
+ }
589
+ catch (err) {
590
+ const errorMessage = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
591
+ // Handle "message is not modified" error silently
592
+ if (errorMessage.includes("message is not modified")) {
593
+ this.lastRenderedMessageText = text;
594
+ continue;
595
+ }
596
+ // Handle "message to edit not found" - recreate
597
+ if (errorMessage.includes("message to edit not found")) {
598
+ logger.warn("[PinnedManager] Pinned message was deleted, recreating...");
599
+ this.state.messageId = null;
600
+ this.lastRenderedMessageText = null;
601
+ this.pendingForceUpdate = false;
602
+ clearPinnedMessageId();
603
+ await this.createPinnedMessage();
604
+ continue;
605
+ }
606
+ logger.error("[PinnedManager] Error updating pinned message:", err);
569
607
  }
570
- logger.error("[PinnedManager] Error updating pinned message:", err);
571
608
  }
572
609
  }
573
610
  /**
@@ -581,6 +618,9 @@ class PinnedMessageManager {
581
618
  // Unpin all messages (ensures clean state)
582
619
  await this.api.unpinAllChatMessages(this.chatId).catch(() => { });
583
620
  this.state.messageId = null;
621
+ this.lastRenderedMessageText = null;
622
+ this.pendingUpdate = false;
623
+ this.pendingForceUpdate = false;
584
624
  clearPinnedMessageId();
585
625
  logger.debug("[PinnedManager] Unpinned old messages");
586
626
  }
@@ -611,6 +651,9 @@ class PinnedMessageManager {
611
651
  this.state.tokensUsed = 0;
612
652
  this.state.tokensLimit = 0;
613
653
  this.state.changedFiles = [];
654
+ this.lastRenderedMessageText = null;
655
+ this.pendingUpdate = false;
656
+ this.pendingForceUpdate = false;
614
657
  clearPinnedMessageId();
615
658
  return;
616
659
  }
@@ -625,6 +668,9 @@ class PinnedMessageManager {
625
668
  this.state.tokensUsed = 0;
626
669
  this.state.tokensLimit = 0;
627
670
  this.state.changedFiles = [];
671
+ this.lastRenderedMessageText = null;
672
+ this.pendingUpdate = false;
673
+ this.pendingForceUpdate = false;
628
674
  clearPinnedMessageId();
629
675
  logger.info("[PinnedManager] Cleared pinned message state");
630
676
  }