cc-claw 0.28.0 → 0.29.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/cli.js +871 -414
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -33,7 +33,7 @@ var VERSION;
|
|
|
33
33
|
var init_version = __esm({
|
|
34
34
|
"src/version.ts"() {
|
|
35
35
|
"use strict";
|
|
36
|
-
VERSION = true ? "0.
|
|
36
|
+
VERSION = true ? "0.29.1" : (() => {
|
|
37
37
|
try {
|
|
38
38
|
return JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
|
|
39
39
|
} catch {
|
|
@@ -7559,6 +7559,380 @@ var init_html = __esm({
|
|
|
7559
7559
|
}
|
|
7560
7560
|
});
|
|
7561
7561
|
|
|
7562
|
+
// src/channels/telegram-throttle.ts
|
|
7563
|
+
var telegram_throttle_exports = {};
|
|
7564
|
+
__export(telegram_throttle_exports, {
|
|
7565
|
+
CircuitState: () => CircuitState,
|
|
7566
|
+
Priority: () => Priority,
|
|
7567
|
+
TelegramThrottle: () => TelegramThrottle,
|
|
7568
|
+
getThrottleState: () => getThrottleState
|
|
7569
|
+
});
|
|
7570
|
+
import { GrammyError } from "grammy";
|
|
7571
|
+
function perChatInterval(chatId) {
|
|
7572
|
+
return parseInt(chatId) < 0 ? PER_GROUP_INTERVAL_MS : PER_DM_INTERVAL_MS;
|
|
7573
|
+
}
|
|
7574
|
+
function getThrottleState() {
|
|
7575
|
+
if (!_activeThrottle) return null;
|
|
7576
|
+
return _activeThrottle.getState();
|
|
7577
|
+
}
|
|
7578
|
+
function is429(err) {
|
|
7579
|
+
return err instanceof GrammyError && err.error_code === 429;
|
|
7580
|
+
}
|
|
7581
|
+
function sleep(ms) {
|
|
7582
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
7583
|
+
}
|
|
7584
|
+
var PER_DM_INTERVAL_MS, PER_GROUP_INTERVAL_MS, GLOBAL_INTERVAL_MS, MAX_RETRIES2, RETRY_DELAY_MS, MAX_QUEUE_SIZE, EDIT_PRESSURE_THRESHOLD, MAX_PER_CHAT_QUEUE, MAX_TOTAL_PAUSE_MS, CIRCUIT_TRIP_THRESHOLD, CIRCUIT_TRIP_WINDOW_MS, CIRCUIT_COOLDOWN_STEP_SEC, CIRCUIT_RESET_WINDOW_MS, CircuitState, Priority, _activeThrottle, TelegramThrottle;
|
|
7585
|
+
var init_telegram_throttle = __esm({
|
|
7586
|
+
"src/channels/telegram-throttle.ts"() {
|
|
7587
|
+
"use strict";
|
|
7588
|
+
init_log();
|
|
7589
|
+
PER_DM_INTERVAL_MS = 1e3;
|
|
7590
|
+
PER_GROUP_INTERVAL_MS = 3500;
|
|
7591
|
+
GLOBAL_INTERVAL_MS = 100;
|
|
7592
|
+
MAX_RETRIES2 = 2;
|
|
7593
|
+
RETRY_DELAY_MS = 1e3;
|
|
7594
|
+
MAX_QUEUE_SIZE = 60;
|
|
7595
|
+
EDIT_PRESSURE_THRESHOLD = MAX_QUEUE_SIZE / 2;
|
|
7596
|
+
MAX_PER_CHAT_QUEUE = 15;
|
|
7597
|
+
MAX_TOTAL_PAUSE_MS = 5 * 60 * 1e3;
|
|
7598
|
+
CIRCUIT_TRIP_THRESHOLD = 3;
|
|
7599
|
+
CIRCUIT_TRIP_WINDOW_MS = 5 * 60 * 1e3;
|
|
7600
|
+
CIRCUIT_COOLDOWN_STEP_SEC = 5;
|
|
7601
|
+
CIRCUIT_RESET_WINDOW_MS = 5 * 60 * 1e3;
|
|
7602
|
+
CircuitState = /* @__PURE__ */ ((CircuitState2) => {
|
|
7603
|
+
CircuitState2["CLOSED"] = "closed";
|
|
7604
|
+
CircuitState2["OPEN"] = "open";
|
|
7605
|
+
CircuitState2["HALF_OPEN"] = "half_open";
|
|
7606
|
+
return CircuitState2;
|
|
7607
|
+
})(CircuitState || {});
|
|
7608
|
+
Priority = /* @__PURE__ */ ((Priority2) => {
|
|
7609
|
+
Priority2[Priority2["P0_CRITICAL"] = 0] = "P0_CRITICAL";
|
|
7610
|
+
Priority2[Priority2["P1_NORMAL"] = 1] = "P1_NORMAL";
|
|
7611
|
+
Priority2[Priority2["P2_COSMETIC"] = 2] = "P2_COSMETIC";
|
|
7612
|
+
return Priority2;
|
|
7613
|
+
})(Priority || {});
|
|
7614
|
+
_activeThrottle = null;
|
|
7615
|
+
TelegramThrottle = class {
|
|
7616
|
+
queue = [];
|
|
7617
|
+
processing = false;
|
|
7618
|
+
lastSendPerChat = /* @__PURE__ */ new Map();
|
|
7619
|
+
perChatQueueCount = /* @__PURE__ */ new Map();
|
|
7620
|
+
// O(1) per-chat depth lookup
|
|
7621
|
+
lastGlobalSend = 0;
|
|
7622
|
+
// Pause state
|
|
7623
|
+
pausedUntil = 0;
|
|
7624
|
+
pauseStartedAt = 0;
|
|
7625
|
+
// Per-chat cosmetic backoff — tryBestEffort() 429s only affect future
|
|
7626
|
+
// best-effort calls for the SAME chat, never triggering a global pause.
|
|
7627
|
+
cosmeticPausedUntil = /* @__PURE__ */ new Map();
|
|
7628
|
+
// Circuit breaker state — tracks repeated 429s and manages recovery
|
|
7629
|
+
circuitState = "closed" /* CLOSED */;
|
|
7630
|
+
circuitTrips = [];
|
|
7631
|
+
// timestamps of recent 429s
|
|
7632
|
+
circuitCooldownUntil = 0;
|
|
7633
|
+
// when OPEN cooldown expires
|
|
7634
|
+
lastSuccessfulSend = 0;
|
|
7635
|
+
// for resetting trip count after 5min of success
|
|
7636
|
+
constructor() {
|
|
7637
|
+
_activeThrottle = this;
|
|
7638
|
+
}
|
|
7639
|
+
/** Enqueue a Telegram API call with automatic pacing and 429 handling.
|
|
7640
|
+
* Priority controls queue insertion order:
|
|
7641
|
+
* P0_CRITICAL — keyboard responses, finalize edits, /stop — always first
|
|
7642
|
+
* P1_NORMAL — agent responses, cron deliveries — default
|
|
7643
|
+
* P2_COSMETIC — live-status streaming edits — dropped first on overflow
|
|
7644
|
+
*
|
|
7645
|
+
* Accepts `Priority | boolean` for backward compatibility during migration.
|
|
7646
|
+
* `true` maps to P0_CRITICAL, `false`/`undefined` maps to P1_NORMAL. */
|
|
7647
|
+
async send(chatId, label2, fn, priority) {
|
|
7648
|
+
const prio = priority === true ? 0 /* P0_CRITICAL */ : typeof priority === "number" ? priority : 1 /* P1_NORMAL */;
|
|
7649
|
+
if (prio === 2 /* P2_COSMETIC */) {
|
|
7650
|
+
if (this.isPaused()) {
|
|
7651
|
+
throw new Error("Throttle paused (rate limit active) \u2014 edit skipped");
|
|
7652
|
+
}
|
|
7653
|
+
if (this.queue.length >= EDIT_PRESSURE_THRESHOLD) {
|
|
7654
|
+
throw new Error("Throttle queue pressured \u2014 edit skipped");
|
|
7655
|
+
}
|
|
7656
|
+
}
|
|
7657
|
+
return new Promise((resolve3, reject) => {
|
|
7658
|
+
if (this.queue.length >= MAX_QUEUE_SIZE) {
|
|
7659
|
+
let dropIdx = -1;
|
|
7660
|
+
for (let i = this.queue.length - 1; i >= 0; i--) {
|
|
7661
|
+
if (this.queue[i].priority === 2 /* P2_COSMETIC */) {
|
|
7662
|
+
dropIdx = i;
|
|
7663
|
+
break;
|
|
7664
|
+
}
|
|
7665
|
+
}
|
|
7666
|
+
if (dropIdx === -1) {
|
|
7667
|
+
for (let i = this.queue.length - 1; i >= 0; i--) {
|
|
7668
|
+
if (this.queue[i].priority === 1 /* P1_NORMAL */) {
|
|
7669
|
+
dropIdx = i;
|
|
7670
|
+
break;
|
|
7671
|
+
}
|
|
7672
|
+
}
|
|
7673
|
+
}
|
|
7674
|
+
if (dropIdx === -1) dropIdx = this.queue.length - 1;
|
|
7675
|
+
const dropped = this.queue.splice(dropIdx, 1)[0];
|
|
7676
|
+
if (dropped) {
|
|
7677
|
+
this.decrementChatCount(dropped.chatId);
|
|
7678
|
+
warn(`[throttle] Queue full (${MAX_QUEUE_SIZE}), dropping P${dropped.priority}: ${dropped.label}`);
|
|
7679
|
+
dropped.reject(new Error("Dropped from send queue (overflow)"));
|
|
7680
|
+
}
|
|
7681
|
+
}
|
|
7682
|
+
const chatQueueCount = this.perChatQueueCount.get(chatId) ?? 0;
|
|
7683
|
+
if (chatQueueCount >= MAX_PER_CHAT_QUEUE && prio !== 0 /* P0_CRITICAL */) {
|
|
7684
|
+
if (prio === 2 /* P2_COSMETIC */) {
|
|
7685
|
+
reject(new Error(`Per-chat queue limit (${MAX_PER_CHAT_QUEUE}) reached \u2014 cosmetic edit dropped`));
|
|
7686
|
+
return;
|
|
7687
|
+
}
|
|
7688
|
+
const p2Idx = this.queue.findIndex((q) => q.chatId === chatId && q.priority === 2 /* P2_COSMETIC */);
|
|
7689
|
+
if (p2Idx >= 0) {
|
|
7690
|
+
const dropped = this.queue.splice(p2Idx, 1)[0];
|
|
7691
|
+
this.decrementChatCount(dropped.chatId);
|
|
7692
|
+
dropped.reject(new Error("Dropped (per-chat P2 eviction)"));
|
|
7693
|
+
} else {
|
|
7694
|
+
reject(new Error(`Per-chat queue limit (${MAX_PER_CHAT_QUEUE}) reached \u2014 normal send dropped`));
|
|
7695
|
+
return;
|
|
7696
|
+
}
|
|
7697
|
+
}
|
|
7698
|
+
const item = { chatId, label: label2, priority: prio, fn, resolve: resolve3, reject };
|
|
7699
|
+
const insertIdx = this.queue.findIndex((q) => q.priority > prio);
|
|
7700
|
+
if (insertIdx === -1) {
|
|
7701
|
+
this.queue.push(item);
|
|
7702
|
+
} else {
|
|
7703
|
+
this.queue.splice(insertIdx, 0, item);
|
|
7704
|
+
}
|
|
7705
|
+
this.perChatQueueCount.set(chatId, (this.perChatQueueCount.get(chatId) ?? 0) + 1);
|
|
7706
|
+
this.drain();
|
|
7707
|
+
});
|
|
7708
|
+
}
|
|
7709
|
+
/**
|
|
7710
|
+
* Best-effort send — drops silently if throttle is paused or queue is pressured.
|
|
7711
|
+
* Used for cosmetic calls (typing indicators, reactions) that should count toward
|
|
7712
|
+
* rate limits but must never queue up or amplify 429 spirals.
|
|
7713
|
+
*/
|
|
7714
|
+
async tryBestEffort(chatId, label2, fn, opts) {
|
|
7715
|
+
if (this.isPaused()) return void 0;
|
|
7716
|
+
if (this.queue.length > 10) return void 0;
|
|
7717
|
+
const cosmeticUntil = this.cosmeticPausedUntil.get(chatId) ?? 0;
|
|
7718
|
+
if (Date.now() < cosmeticUntil) return void 0;
|
|
7719
|
+
if (cosmeticUntil > 0) this.cosmeticPausedUntil.delete(chatId);
|
|
7720
|
+
if (!opts?.skipRecord) {
|
|
7721
|
+
const lastChat = this.lastSendPerChat.get(chatId) ?? 0;
|
|
7722
|
+
if (Date.now() - lastChat < perChatInterval(chatId)) return void 0;
|
|
7723
|
+
if (Date.now() - this.lastGlobalSend < GLOBAL_INTERVAL_MS) return void 0;
|
|
7724
|
+
}
|
|
7725
|
+
try {
|
|
7726
|
+
const result = await fn();
|
|
7727
|
+
if (!opts?.skipRecord) this.recordSend(chatId);
|
|
7728
|
+
return result;
|
|
7729
|
+
} catch (err) {
|
|
7730
|
+
if (is429(err)) {
|
|
7731
|
+
const retrySec = err.parameters?.retry_after ?? 10;
|
|
7732
|
+
this.cosmeticPausedUntil.set(chatId, Date.now() + retrySec * 1e3);
|
|
7733
|
+
warn(`[throttle] 429 event (cosmetic)`, JSON.stringify({
|
|
7734
|
+
method: label2,
|
|
7735
|
+
chatId,
|
|
7736
|
+
retry_after: retrySec,
|
|
7737
|
+
queue_depth: this.queue.length,
|
|
7738
|
+
circuit_state: this.circuitState,
|
|
7739
|
+
type: "best_effort"
|
|
7740
|
+
}));
|
|
7741
|
+
}
|
|
7742
|
+
return void 0;
|
|
7743
|
+
}
|
|
7744
|
+
}
|
|
7745
|
+
/** Check whether the throttle is currently paused (rate-limited). */
|
|
7746
|
+
isPaused() {
|
|
7747
|
+
return Date.now() < this.pausedUntil;
|
|
7748
|
+
}
|
|
7749
|
+
/** Get structured state for diagnostics / health checks. */
|
|
7750
|
+
getState() {
|
|
7751
|
+
const now = Date.now();
|
|
7752
|
+
const paused = now < this.pausedUntil;
|
|
7753
|
+
return {
|
|
7754
|
+
isPaused: paused,
|
|
7755
|
+
queueDepth: this.queue.length,
|
|
7756
|
+
pausedUntilMs: this.pausedUntil,
|
|
7757
|
+
pauseRemainingSec: paused ? Math.ceil((this.pausedUntil - now) / 1e3) : 0,
|
|
7758
|
+
circuitState: this.circuitState
|
|
7759
|
+
};
|
|
7760
|
+
}
|
|
7761
|
+
// ── Queue processor ─────────────────────────────────────────────────
|
|
7762
|
+
async drain() {
|
|
7763
|
+
if (this.processing) return;
|
|
7764
|
+
this.processing = true;
|
|
7765
|
+
try {
|
|
7766
|
+
while (this.queue.length > 0) {
|
|
7767
|
+
while (this.isPaused()) {
|
|
7768
|
+
if (this.pauseStartedAt > 0 && Date.now() - this.pauseStartedAt > MAX_TOTAL_PAUSE_MS) {
|
|
7769
|
+
warn(`[throttle] Max pause duration exceeded (${MAX_TOTAL_PAUSE_MS / 6e4}min), dropping ${this.queue.length} items`);
|
|
7770
|
+
this.flushQueueWithError("Telegram rate limit exceeded max wait time");
|
|
7771
|
+
this.pausedUntil = 0;
|
|
7772
|
+
this.pauseStartedAt = 0;
|
|
7773
|
+
break;
|
|
7774
|
+
}
|
|
7775
|
+
const waitMs = Math.min(this.pausedUntil - Date.now(), 5e3);
|
|
7776
|
+
if (waitMs > 0) await sleep(waitMs);
|
|
7777
|
+
}
|
|
7778
|
+
if (this.queue.length === 0) break;
|
|
7779
|
+
this.updateCircuitState();
|
|
7780
|
+
const item = this.selectNextItem();
|
|
7781
|
+
if (!item) {
|
|
7782
|
+
await sleep(1e3);
|
|
7783
|
+
continue;
|
|
7784
|
+
}
|
|
7785
|
+
const lastChat = this.lastSendPerChat.get(item.chatId) ?? 0;
|
|
7786
|
+
const chatWait = perChatInterval(item.chatId) - (Date.now() - lastChat);
|
|
7787
|
+
if (chatWait > 0) await sleep(chatWait);
|
|
7788
|
+
const globalWait = GLOBAL_INTERVAL_MS - (Date.now() - this.lastGlobalSend);
|
|
7789
|
+
if (globalWait > 0) await sleep(globalWait);
|
|
7790
|
+
try {
|
|
7791
|
+
const result = await this.execWithRetry(item.label, item.fn);
|
|
7792
|
+
this.recordSend(item.chatId);
|
|
7793
|
+
this.pauseStartedAt = 0;
|
|
7794
|
+
this.onSuccessfulSend();
|
|
7795
|
+
item.resolve(result);
|
|
7796
|
+
} catch (err) {
|
|
7797
|
+
if (is429(err)) {
|
|
7798
|
+
const retrySec = err.parameters?.retry_after ?? 10;
|
|
7799
|
+
this.enterPause(retrySec, item);
|
|
7800
|
+
continue;
|
|
7801
|
+
}
|
|
7802
|
+
item.reject(err);
|
|
7803
|
+
}
|
|
7804
|
+
}
|
|
7805
|
+
} finally {
|
|
7806
|
+
this.processing = false;
|
|
7807
|
+
}
|
|
7808
|
+
}
|
|
7809
|
+
/**
|
|
7810
|
+
* Select the next queue item to process, respecting circuit breaker state.
|
|
7811
|
+
* Returns the item (already removed from queue) or null if nothing processable.
|
|
7812
|
+
*/
|
|
7813
|
+
selectNextItem() {
|
|
7814
|
+
if (this.circuitState === "closed" /* CLOSED */) {
|
|
7815
|
+
return this.dequeue();
|
|
7816
|
+
}
|
|
7817
|
+
if (this.circuitState === "open" /* OPEN */) {
|
|
7818
|
+
while (this.queue.length > 0 && this.queue[0].priority === 2 /* P2_COSMETIC */) {
|
|
7819
|
+
const dropped = this.dequeue();
|
|
7820
|
+
warn(`[throttle] Circuit OPEN \u2014 dropping P2: ${dropped.label}`);
|
|
7821
|
+
dropped.reject(new Error("Circuit breaker OPEN \u2014 cosmetic item dropped"));
|
|
7822
|
+
}
|
|
7823
|
+
if (this.queue.length > 0 && this.queue[0].priority === 0 /* P0_CRITICAL */) {
|
|
7824
|
+
return this.dequeue();
|
|
7825
|
+
}
|
|
7826
|
+
return null;
|
|
7827
|
+
}
|
|
7828
|
+
if (this.circuitState === "half_open" /* HALF_OPEN */) {
|
|
7829
|
+
return this.dequeue();
|
|
7830
|
+
}
|
|
7831
|
+
return this.dequeue();
|
|
7832
|
+
}
|
|
7833
|
+
/**
|
|
7834
|
+
* Check if circuit breaker should transition states.
|
|
7835
|
+
* OPEN → HALF_OPEN when cooldown expires.
|
|
7836
|
+
*/
|
|
7837
|
+
updateCircuitState() {
|
|
7838
|
+
if (this.circuitState === "open" /* OPEN */ && Date.now() >= this.circuitCooldownUntil) {
|
|
7839
|
+
this.circuitState = "half_open" /* HALF_OPEN */;
|
|
7840
|
+
log(`[throttle] Circuit breaker: OPEN \u2192 HALF_OPEN (cooldown expired, probing)`);
|
|
7841
|
+
}
|
|
7842
|
+
}
|
|
7843
|
+
/**
|
|
7844
|
+
* Handle successful send — manage circuit breaker recovery.
|
|
7845
|
+
*/
|
|
7846
|
+
onSuccessfulSend() {
|
|
7847
|
+
this.lastSuccessfulSend = Date.now();
|
|
7848
|
+
if (this.circuitState === "half_open" /* HALF_OPEN */) {
|
|
7849
|
+
this.circuitState = "closed" /* CLOSED */;
|
|
7850
|
+
log(`[throttle] Circuit breaker: HALF_OPEN \u2192 CLOSED (probe succeeded)`);
|
|
7851
|
+
}
|
|
7852
|
+
if (this.circuitTrips.length > 0) {
|
|
7853
|
+
const lastTrip = this.circuitTrips[this.circuitTrips.length - 1];
|
|
7854
|
+
if (Date.now() - lastTrip > CIRCUIT_RESET_WINDOW_MS) {
|
|
7855
|
+
this.circuitTrips.length = 0;
|
|
7856
|
+
this.circuitTrips = [];
|
|
7857
|
+
log(`[throttle] Circuit breaker: trip count reset after ${CIRCUIT_RESET_WINDOW_MS / 6e4}min of success`);
|
|
7858
|
+
}
|
|
7859
|
+
}
|
|
7860
|
+
}
|
|
7861
|
+
// ── Retry logic (non-429 errors only) ───────────────────────────────
|
|
7862
|
+
async execWithRetry(label2, fn) {
|
|
7863
|
+
for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
|
|
7864
|
+
try {
|
|
7865
|
+
return await fn();
|
|
7866
|
+
} catch (err) {
|
|
7867
|
+
if (is429(err)) throw err;
|
|
7868
|
+
if (attempt < MAX_RETRIES2 && err instanceof GrammyError) {
|
|
7869
|
+
warn(`[throttle] ${label2} attempt ${attempt + 1}/${MAX_RETRIES2} failed (${err.error_code}), retrying`);
|
|
7870
|
+
await sleep(RETRY_DELAY_MS);
|
|
7871
|
+
continue;
|
|
7872
|
+
}
|
|
7873
|
+
throw err;
|
|
7874
|
+
}
|
|
7875
|
+
}
|
|
7876
|
+
throw new Error("unreachable");
|
|
7877
|
+
}
|
|
7878
|
+
// ── Pause management ────────────────────────────────────────────────
|
|
7879
|
+
enterPause(retrySec, failedItem) {
|
|
7880
|
+
this.queue.unshift(failedItem);
|
|
7881
|
+
const now = Date.now();
|
|
7882
|
+
this.circuitTrips.push(now);
|
|
7883
|
+
this.circuitTrips = this.circuitTrips.filter((t) => now - t < CIRCUIT_TRIP_WINDOW_MS);
|
|
7884
|
+
const bufferSec = this.circuitTrips.length * CIRCUIT_COOLDOWN_STEP_SEC;
|
|
7885
|
+
const totalPauseSec = retrySec + bufferSec;
|
|
7886
|
+
this.pausedUntil = now + totalPauseSec * 1e3;
|
|
7887
|
+
if (this.pauseStartedAt === 0) this.pauseStartedAt = now;
|
|
7888
|
+
if (this.circuitTrips.length >= CIRCUIT_TRIP_THRESHOLD && this.circuitState !== "open" /* OPEN */) {
|
|
7889
|
+
this.circuitState = "open" /* OPEN */;
|
|
7890
|
+
this.circuitCooldownUntil = now + totalPauseSec * 1e3;
|
|
7891
|
+
warn(`[throttle] Circuit breaker TRIPPED \u2192 OPEN (${this.circuitTrips.length} 429s in ${CIRCUIT_TRIP_WINDOW_MS / 6e4}min)`);
|
|
7892
|
+
} else if (this.circuitState === "half_open" /* HALF_OPEN */) {
|
|
7893
|
+
this.circuitState = "open" /* OPEN */;
|
|
7894
|
+
this.circuitCooldownUntil = now + totalPauseSec * 1e3;
|
|
7895
|
+
warn(`[throttle] Circuit breaker probe FAILED \u2192 OPEN (retry_after=${retrySec}s + ${bufferSec}s buffer)`);
|
|
7896
|
+
}
|
|
7897
|
+
warn(`[throttle] 429 event`, JSON.stringify({
|
|
7898
|
+
method: failedItem.label,
|
|
7899
|
+
chatId: failedItem.chatId,
|
|
7900
|
+
priority: failedItem.priority,
|
|
7901
|
+
retry_after: retrySec,
|
|
7902
|
+
buffer: bufferSec,
|
|
7903
|
+
total_pause: totalPauseSec,
|
|
7904
|
+
queue_depth: this.queue.length,
|
|
7905
|
+
circuit_state: this.circuitState,
|
|
7906
|
+
circuit_trip_count: this.circuitTrips.length
|
|
7907
|
+
}));
|
|
7908
|
+
}
|
|
7909
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
7910
|
+
recordSend(chatId) {
|
|
7911
|
+
const now = Date.now();
|
|
7912
|
+
this.lastSendPerChat.set(chatId, now);
|
|
7913
|
+
this.lastGlobalSend = now;
|
|
7914
|
+
}
|
|
7915
|
+
/** Remove and return the first item from the queue, updating per-chat counter. */
|
|
7916
|
+
dequeue() {
|
|
7917
|
+
const item = this.queue.shift();
|
|
7918
|
+
if (item) this.decrementChatCount(item.chatId);
|
|
7919
|
+
return item ?? null;
|
|
7920
|
+
}
|
|
7921
|
+
decrementChatCount(chatId) {
|
|
7922
|
+
const count = (this.perChatQueueCount.get(chatId) ?? 1) - 1;
|
|
7923
|
+
if (count <= 0) this.perChatQueueCount.delete(chatId);
|
|
7924
|
+
else this.perChatQueueCount.set(chatId, count);
|
|
7925
|
+
}
|
|
7926
|
+
flushQueueWithError(message) {
|
|
7927
|
+
while (this.queue.length > 0) {
|
|
7928
|
+
const item = this.dequeue();
|
|
7929
|
+
item.reject(new Error(message));
|
|
7930
|
+
}
|
|
7931
|
+
}
|
|
7932
|
+
};
|
|
7933
|
+
}
|
|
7934
|
+
});
|
|
7935
|
+
|
|
7562
7936
|
// src/dashboard/routes/system.ts
|
|
7563
7937
|
var handleHealth, handleJobs, handleMemories, handleStats, handleAgents, handleTasks, handleOrchestrations;
|
|
7564
7938
|
var init_system = __esm({
|
|
@@ -7570,8 +7944,10 @@ var init_system = __esm({
|
|
|
7570
7944
|
init_store5();
|
|
7571
7945
|
init_store();
|
|
7572
7946
|
init_version();
|
|
7947
|
+
init_telegram_throttle();
|
|
7573
7948
|
handleHealth = (_req, res) => {
|
|
7574
|
-
|
|
7949
|
+
const throttle = getThrottleState();
|
|
7950
|
+
jsonResponse(res, { status: "ok", version: VERSION, uptime: process.uptime(), throttle: throttle ?? void 0 });
|
|
7575
7951
|
};
|
|
7576
7952
|
handleJobs = (_req, res) => {
|
|
7577
7953
|
jsonResponse(res, listJobs());
|
|
@@ -7936,7 +8312,7 @@ function buildContextPrefix(msg) {
|
|
|
7936
8312
|
}
|
|
7937
8313
|
async function sendOrEditKeyboard(chatId, channel, messageId, text, buttons) {
|
|
7938
8314
|
if (messageId && typeof channel.editKeyboard === "function") {
|
|
7939
|
-
const ok = await channel.editKeyboard(chatId, messageId, text, buttons, { priority:
|
|
8315
|
+
const ok = await channel.editKeyboard(chatId, messageId, text, buttons, { priority: 0 /* P0_CRITICAL */ });
|
|
7940
8316
|
if (ok) return messageId;
|
|
7941
8317
|
}
|
|
7942
8318
|
if (typeof channel.sendKeyboard === "function") {
|
|
@@ -7949,6 +8325,7 @@ var TONE_PATTERNS, BLOCKED_PATH_PATTERNS, CLI_INSTALL_HINTS, PERM_MODES, VERBOSE
|
|
|
7949
8325
|
var init_helpers = __esm({
|
|
7950
8326
|
"src/router/helpers.ts"() {
|
|
7951
8327
|
"use strict";
|
|
8328
|
+
init_telegram_throttle();
|
|
7952
8329
|
init_store5();
|
|
7953
8330
|
init_backends();
|
|
7954
8331
|
TONE_PATTERNS = [
|
|
@@ -8728,8 +9105,8 @@ function checkBudget(db3, orchestrationId) {
|
|
|
8728
9105
|
}
|
|
8729
9106
|
const percentUsed = totalCost / budgetLimit * 100;
|
|
8730
9107
|
const exceeded = totalCost >= budgetLimit;
|
|
8731
|
-
const
|
|
8732
|
-
return { totalCost, budgetLimit, percentUsed, exceeded, warning:
|
|
9108
|
+
const warning3 = percentUsed >= BUDGET_WARNING_PERCENT * 100;
|
|
9109
|
+
return { totalCost, budgetLimit, percentUsed, exceeded, warning: warning3 };
|
|
8733
9110
|
}
|
|
8734
9111
|
function recordAgentCost(db3, orchestrationId, agentCost) {
|
|
8735
9112
|
updateOrchestrationCost(db3, orchestrationId, agentCost);
|
|
@@ -16392,251 +16769,6 @@ var init_health2 = __esm({
|
|
|
16392
16769
|
}
|
|
16393
16770
|
});
|
|
16394
16771
|
|
|
16395
|
-
// src/channels/telegram-throttle.ts
|
|
16396
|
-
var telegram_throttle_exports = {};
|
|
16397
|
-
__export(telegram_throttle_exports, {
|
|
16398
|
-
TelegramThrottle: () => TelegramThrottle,
|
|
16399
|
-
getThrottleState: () => getThrottleState
|
|
16400
|
-
});
|
|
16401
|
-
import { GrammyError } from "grammy";
|
|
16402
|
-
function isEditLabel(label2) {
|
|
16403
|
-
return label2.startsWith("editText") || label2.startsWith("editKeyboard");
|
|
16404
|
-
}
|
|
16405
|
-
function perChatInterval(chatId) {
|
|
16406
|
-
return parseInt(chatId) < 0 ? PER_GROUP_INTERVAL_MS : PER_DM_INTERVAL_MS;
|
|
16407
|
-
}
|
|
16408
|
-
function getThrottleState() {
|
|
16409
|
-
if (!_activeThrottle) return null;
|
|
16410
|
-
return _activeThrottle.getState();
|
|
16411
|
-
}
|
|
16412
|
-
function is429(err) {
|
|
16413
|
-
return err instanceof GrammyError && err.error_code === 429;
|
|
16414
|
-
}
|
|
16415
|
-
function sleep(ms) {
|
|
16416
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
16417
|
-
}
|
|
16418
|
-
var PER_DM_INTERVAL_MS, PER_GROUP_INTERVAL_MS, GLOBAL_INTERVAL_MS, MAX_RETRIES2, RETRY_DELAY_MS, MAX_QUEUE_SIZE, EDIT_PRESSURE_THRESHOLD, MAX_TOTAL_PAUSE_MS, _activeThrottle, TelegramThrottle;
|
|
16419
|
-
var init_telegram_throttle = __esm({
|
|
16420
|
-
"src/channels/telegram-throttle.ts"() {
|
|
16421
|
-
"use strict";
|
|
16422
|
-
init_log();
|
|
16423
|
-
PER_DM_INTERVAL_MS = 1e3;
|
|
16424
|
-
PER_GROUP_INTERVAL_MS = 3500;
|
|
16425
|
-
GLOBAL_INTERVAL_MS = 100;
|
|
16426
|
-
MAX_RETRIES2 = 2;
|
|
16427
|
-
RETRY_DELAY_MS = 1e3;
|
|
16428
|
-
MAX_QUEUE_SIZE = 100;
|
|
16429
|
-
EDIT_PRESSURE_THRESHOLD = MAX_QUEUE_SIZE / 2;
|
|
16430
|
-
MAX_TOTAL_PAUSE_MS = 30 * 60 * 1e3;
|
|
16431
|
-
_activeThrottle = null;
|
|
16432
|
-
TelegramThrottle = class {
|
|
16433
|
-
queue = [];
|
|
16434
|
-
processing = false;
|
|
16435
|
-
lastSendPerChat = /* @__PURE__ */ new Map();
|
|
16436
|
-
lastGlobalSend = 0;
|
|
16437
|
-
// Pause state
|
|
16438
|
-
pausedUntil = 0;
|
|
16439
|
-
pauseStartedAt = 0;
|
|
16440
|
-
chatsPendingNotification = /* @__PURE__ */ new Set();
|
|
16441
|
-
resumeNotifier;
|
|
16442
|
-
constructor() {
|
|
16443
|
-
_activeThrottle = this;
|
|
16444
|
-
}
|
|
16445
|
-
/**
|
|
16446
|
-
* Register a callback that fires when the throttle resumes after a pause.
|
|
16447
|
-
* The callback should send a message directly via bot.api (NOT through the throttle).
|
|
16448
|
-
*/
|
|
16449
|
-
setResumeNotifier(fn) {
|
|
16450
|
-
this.resumeNotifier = fn;
|
|
16451
|
-
}
|
|
16452
|
-
/** Enqueue a Telegram API call with automatic pacing and 429 handling.
|
|
16453
|
-
* When `priority` is true the item jumps to the front of the queue —
|
|
16454
|
-
* used by fast-path commands (/status, /stop, etc.) so their responses
|
|
16455
|
-
* aren't delayed behind tool-notification or error-message floods. */
|
|
16456
|
-
async send(chatId, label2, fn, priority) {
|
|
16457
|
-
if (isEditLabel(label2)) {
|
|
16458
|
-
if (this.isPaused()) {
|
|
16459
|
-
throw new Error("Throttle paused (rate limit active) \u2014 edit skipped");
|
|
16460
|
-
}
|
|
16461
|
-
if (this.queue.length >= EDIT_PRESSURE_THRESHOLD) {
|
|
16462
|
-
throw new Error("Throttle queue pressured \u2014 edit skipped");
|
|
16463
|
-
}
|
|
16464
|
-
}
|
|
16465
|
-
return new Promise((resolve3, reject) => {
|
|
16466
|
-
if (this.queue.length >= MAX_QUEUE_SIZE) {
|
|
16467
|
-
const editIdx = this.queue.findIndex((q) => isEditLabel(q.label));
|
|
16468
|
-
const dropIdx = editIdx >= 0 ? editIdx : 0;
|
|
16469
|
-
const dropped = this.queue.splice(dropIdx, 1)[0];
|
|
16470
|
-
if (dropped) {
|
|
16471
|
-
warn(`[throttle] Queue full (${MAX_QUEUE_SIZE}), dropping: ${dropped.label}`);
|
|
16472
|
-
dropped.reject(new Error("Dropped from send queue (overflow)"));
|
|
16473
|
-
}
|
|
16474
|
-
}
|
|
16475
|
-
const item = { chatId, label: label2, fn, resolve: resolve3, reject };
|
|
16476
|
-
if (priority) {
|
|
16477
|
-
this.queue.unshift(item);
|
|
16478
|
-
} else {
|
|
16479
|
-
this.queue.push(item);
|
|
16480
|
-
}
|
|
16481
|
-
this.drain();
|
|
16482
|
-
});
|
|
16483
|
-
}
|
|
16484
|
-
/**
|
|
16485
|
-
* Best-effort send — drops silently if throttle is paused or queue is pressured.
|
|
16486
|
-
* Used for cosmetic calls (typing indicators, reactions) that should count toward
|
|
16487
|
-
* rate limits but must never queue up or amplify 429 spirals.
|
|
16488
|
-
*/
|
|
16489
|
-
async tryBestEffort(chatId, label2, fn, opts) {
|
|
16490
|
-
if (this.isPaused()) return void 0;
|
|
16491
|
-
if (this.queue.length > 10) return void 0;
|
|
16492
|
-
if (!opts?.skipRecord) {
|
|
16493
|
-
const lastChat = this.lastSendPerChat.get(chatId) ?? 0;
|
|
16494
|
-
if (Date.now() - lastChat < perChatInterval(chatId)) return void 0;
|
|
16495
|
-
if (Date.now() - this.lastGlobalSend < GLOBAL_INTERVAL_MS) return void 0;
|
|
16496
|
-
}
|
|
16497
|
-
try {
|
|
16498
|
-
const result = await fn();
|
|
16499
|
-
if (!opts?.skipRecord) this.recordSend(chatId);
|
|
16500
|
-
return result;
|
|
16501
|
-
} catch (err) {
|
|
16502
|
-
if (is429(err)) {
|
|
16503
|
-
const retrySec = err.parameters?.retry_after ?? 10;
|
|
16504
|
-
this.pausedUntil = Date.now() + retrySec * 1e3;
|
|
16505
|
-
if (this.pauseStartedAt === 0) this.pauseStartedAt = Date.now();
|
|
16506
|
-
warn(`[throttle] Best-effort ${label2} hit 429, pausing for ${retrySec}s`);
|
|
16507
|
-
}
|
|
16508
|
-
return void 0;
|
|
16509
|
-
}
|
|
16510
|
-
}
|
|
16511
|
-
/** Check whether the throttle is currently paused (rate-limited). */
|
|
16512
|
-
isPaused() {
|
|
16513
|
-
return Date.now() < this.pausedUntil;
|
|
16514
|
-
}
|
|
16515
|
-
/** Get structured state for diagnostics / health checks. */
|
|
16516
|
-
getState() {
|
|
16517
|
-
const now = Date.now();
|
|
16518
|
-
const paused = now < this.pausedUntil;
|
|
16519
|
-
return {
|
|
16520
|
-
isPaused: paused,
|
|
16521
|
-
queueDepth: this.queue.length,
|
|
16522
|
-
pausedUntilMs: this.pausedUntil,
|
|
16523
|
-
pauseRemainingSec: paused ? Math.ceil((this.pausedUntil - now) / 1e3) : 0
|
|
16524
|
-
};
|
|
16525
|
-
}
|
|
16526
|
-
// ── Queue processor ─────────────────────────────────────────────────
|
|
16527
|
-
async drain() {
|
|
16528
|
-
if (this.processing) return;
|
|
16529
|
-
this.processing = true;
|
|
16530
|
-
try {
|
|
16531
|
-
while (this.queue.length > 0) {
|
|
16532
|
-
while (this.isPaused()) {
|
|
16533
|
-
if (this.pauseStartedAt > 0 && Date.now() - this.pauseStartedAt > MAX_TOTAL_PAUSE_MS) {
|
|
16534
|
-
warn(`[throttle] Max pause duration exceeded (${MAX_TOTAL_PAUSE_MS / 6e4}min), dropping ${this.queue.length} items`);
|
|
16535
|
-
this.flushQueueWithError("Telegram rate limit exceeded max wait time");
|
|
16536
|
-
this.pausedUntil = 0;
|
|
16537
|
-
this.pauseStartedAt = 0;
|
|
16538
|
-
this.chatsPendingNotification.clear();
|
|
16539
|
-
break;
|
|
16540
|
-
}
|
|
16541
|
-
const waitMs = Math.min(this.pausedUntil - Date.now(), 5e3);
|
|
16542
|
-
if (waitMs > 0) await sleep(waitMs);
|
|
16543
|
-
}
|
|
16544
|
-
if (this.queue.length === 0) break;
|
|
16545
|
-
if (this.chatsPendingNotification.size > 0) {
|
|
16546
|
-
await this.sendResumeNotifications();
|
|
16547
|
-
}
|
|
16548
|
-
const item = this.queue[0];
|
|
16549
|
-
const lastChat = this.lastSendPerChat.get(item.chatId) ?? 0;
|
|
16550
|
-
const chatWait = perChatInterval(item.chatId) - (Date.now() - lastChat);
|
|
16551
|
-
if (chatWait > 0) await sleep(chatWait);
|
|
16552
|
-
const globalWait = GLOBAL_INTERVAL_MS - (Date.now() - this.lastGlobalSend);
|
|
16553
|
-
if (globalWait > 0) await sleep(globalWait);
|
|
16554
|
-
this.queue.shift();
|
|
16555
|
-
try {
|
|
16556
|
-
const result = await this.execWithRetry(item.label, item.fn);
|
|
16557
|
-
this.recordSend(item.chatId);
|
|
16558
|
-
this.pauseStartedAt = 0;
|
|
16559
|
-
item.resolve(result);
|
|
16560
|
-
} catch (err) {
|
|
16561
|
-
if (is429(err)) {
|
|
16562
|
-
const retrySec = err.parameters?.retry_after ?? 10;
|
|
16563
|
-
this.enterPause(retrySec, item);
|
|
16564
|
-
continue;
|
|
16565
|
-
}
|
|
16566
|
-
item.reject(err);
|
|
16567
|
-
}
|
|
16568
|
-
}
|
|
16569
|
-
} finally {
|
|
16570
|
-
this.processing = false;
|
|
16571
|
-
}
|
|
16572
|
-
}
|
|
16573
|
-
// ── Retry logic (non-429 errors only) ───────────────────────────────
|
|
16574
|
-
async execWithRetry(label2, fn) {
|
|
16575
|
-
for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
|
|
16576
|
-
try {
|
|
16577
|
-
return await fn();
|
|
16578
|
-
} catch (err) {
|
|
16579
|
-
if (is429(err)) throw err;
|
|
16580
|
-
if (attempt < MAX_RETRIES2 && err instanceof GrammyError) {
|
|
16581
|
-
warn(`[throttle] ${label2} attempt ${attempt + 1}/${MAX_RETRIES2} failed (${err.error_code}), retrying`);
|
|
16582
|
-
await sleep(RETRY_DELAY_MS);
|
|
16583
|
-
continue;
|
|
16584
|
-
}
|
|
16585
|
-
throw err;
|
|
16586
|
-
}
|
|
16587
|
-
}
|
|
16588
|
-
throw new Error("unreachable");
|
|
16589
|
-
}
|
|
16590
|
-
// ── Pause management ────────────────────────────────────────────────
|
|
16591
|
-
enterPause(retrySec, failedItem) {
|
|
16592
|
-
this.queue.unshift(failedItem);
|
|
16593
|
-
const bufferedSec = Math.ceil(retrySec * 1.5);
|
|
16594
|
-
this.pausedUntil = Date.now() + bufferedSec * 1e3;
|
|
16595
|
-
if (this.pauseStartedAt === 0) this.pauseStartedAt = Date.now();
|
|
16596
|
-
for (const qi of this.queue) {
|
|
16597
|
-
this.chatsPendingNotification.add(qi.chatId);
|
|
16598
|
-
}
|
|
16599
|
-
warn(`[throttle] 429 \u2014 pausing ALL sends for ${bufferedSec}s (retry_after=${retrySec}s + 50% buffer, ${this.queue.length} items queued)`);
|
|
16600
|
-
}
|
|
16601
|
-
async sendResumeNotifications() {
|
|
16602
|
-
const chats2 = new Set(this.chatsPendingNotification);
|
|
16603
|
-
this.chatsPendingNotification.clear();
|
|
16604
|
-
if (!this.resumeNotifier) return;
|
|
16605
|
-
const pausedSec = this.pauseStartedAt > 0 ? Math.round((Date.now() - this.pauseStartedAt) / 1e3) : 0;
|
|
16606
|
-
for (const chatId of chats2) {
|
|
16607
|
-
const queuedForChat = this.queue.filter((q) => q.chatId === chatId).length;
|
|
16608
|
-
if (queuedForChat === 0) continue;
|
|
16609
|
-
try {
|
|
16610
|
-
await this.resumeNotifier(chatId, pausedSec, queuedForChat);
|
|
16611
|
-
this.recordSend(chatId);
|
|
16612
|
-
} catch (err) {
|
|
16613
|
-
if (is429(err)) {
|
|
16614
|
-
const retrySec = err.parameters?.retry_after ?? 10;
|
|
16615
|
-
this.pausedUntil = Date.now() + retrySec * 1e3;
|
|
16616
|
-
warn(`[throttle] Resume notification hit 429, re-pausing for ${retrySec}s (skipping further notifications)`);
|
|
16617
|
-
return;
|
|
16618
|
-
}
|
|
16619
|
-
warn(`[throttle] Resume notification failed for chat ${chatId}: ${err}`);
|
|
16620
|
-
}
|
|
16621
|
-
}
|
|
16622
|
-
this.pauseStartedAt = 0;
|
|
16623
|
-
}
|
|
16624
|
-
// ── Helpers ─────────────────────────────────────────────────────────
|
|
16625
|
-
recordSend(chatId) {
|
|
16626
|
-
const now = Date.now();
|
|
16627
|
-
this.lastSendPerChat.set(chatId, now);
|
|
16628
|
-
this.lastGlobalSend = now;
|
|
16629
|
-
}
|
|
16630
|
-
flushQueueWithError(message) {
|
|
16631
|
-
while (this.queue.length > 0) {
|
|
16632
|
-
const item = this.queue.shift();
|
|
16633
|
-
item.reject(new Error(message));
|
|
16634
|
-
}
|
|
16635
|
-
}
|
|
16636
|
-
};
|
|
16637
|
-
}
|
|
16638
|
-
});
|
|
16639
|
-
|
|
16640
16772
|
// src/health/checks.ts
|
|
16641
16773
|
import { existsSync as existsSync16, statSync as statSync5, readFileSync as readFileSync11 } from "fs";
|
|
16642
16774
|
import { execFileSync as execFileSync2, execSync as execSync2 } from "child_process";
|
|
@@ -17978,38 +18110,32 @@ async function sendThinkingKeyboard(chatId, channel, messageId, forModelId) {
|
|
|
17978
18110
|
const currentModel = forModelId ?? getModel(chatId) ?? adapter.defaultModel;
|
|
17979
18111
|
const modelInfo = adapter.availableModels[currentModel];
|
|
17980
18112
|
const currentLevel = getThinkingLevel(chatId) || "auto";
|
|
17981
|
-
if (!modelInfo || modelInfo.thinking !== "adjustable" || !modelInfo.thinkingLevels) {
|
|
17982
|
-
await sendOrEditKeyboard(
|
|
17983
|
-
chatId,
|
|
17984
|
-
channel,
|
|
17985
|
-
messageId,
|
|
17986
|
-
`Model ${shortModelName(currentModel)} uses fixed thinking \u2014 no adjustment needed.`,
|
|
17987
|
-
[[{ label: "\u2190 Back to Model", data: "menu:model" }]]
|
|
17988
|
-
);
|
|
17989
|
-
return;
|
|
17990
|
-
}
|
|
17991
18113
|
const showThinkingUi = getShowThinkingUi(chatId);
|
|
17992
|
-
const
|
|
17993
|
-
|
|
17994
|
-
|
|
17995
|
-
|
|
17996
|
-
|
|
18114
|
+
const canAdjust = modelInfo?.thinking === "adjustable" && modelInfo.thinkingLevels;
|
|
18115
|
+
const buttons = [];
|
|
18116
|
+
if (canAdjust) {
|
|
18117
|
+
for (const level of modelInfo.thinkingLevels) {
|
|
18118
|
+
buttons.push([{
|
|
18119
|
+
label: `${level === currentLevel ? "\u2713 " : ""}${level === "auto" ? "Auto" : capitalize(level)}`,
|
|
18120
|
+
data: `thinking:${level}`,
|
|
18121
|
+
...level === currentLevel ? { style: "primary" } : {}
|
|
18122
|
+
}]);
|
|
18123
|
+
}
|
|
18124
|
+
}
|
|
17997
18125
|
buttons.push([{
|
|
17998
18126
|
label: `${showThinkingUi ? "\u2713 " : ""}\u{1F4AD} Show Thinking`,
|
|
17999
18127
|
data: "thinking_show_ui:toggle",
|
|
18000
18128
|
...showThinkingUi ? { style: "primary" } : {}
|
|
18001
18129
|
}]);
|
|
18002
|
-
|
|
18003
|
-
chatId,
|
|
18004
|
-
channel,
|
|
18005
|
-
messageId,
|
|
18006
|
-
`\u{1F4AD} Thinking Level \u2014 ${shortModelName(currentModel)}
|
|
18130
|
+
const header2 = canAdjust ? `\u{1F4AD} Thinking Level \u2014 ${shortModelName(currentModel)}
|
|
18007
18131
|
Current: ${capitalize(currentLevel)}
|
|
18008
|
-
Show thinking tokens: ${showThinkingUi ? "On" : "Off"}
|
|
18132
|
+
Show thinking tokens: ${showThinkingUi ? "On" : "Off"}` : `\u{1F4AD} Thinking \u2014 ${shortModelName(currentModel)}
|
|
18133
|
+
Level: Fixed
|
|
18134
|
+
Show thinking tokens: ${showThinkingUi ? "On" : "Off"}`;
|
|
18135
|
+
const note = adapter.id === "cursor" ? `
|
|
18009
18136
|
|
|
18010
|
-
\u26A0\uFE0F ${adapter.displayName} doesn't expose thinking tokens` : ""
|
|
18011
|
-
|
|
18012
|
-
);
|
|
18137
|
+
\u26A0\uFE0F ${adapter.displayName} doesn't expose thinking tokens` : "";
|
|
18138
|
+
await sendOrEditKeyboard(chatId, channel, messageId, `${header2}${note}`, buttons);
|
|
18013
18139
|
}
|
|
18014
18140
|
async function sendSkillsPage(chatId, channel, skills2, page, messageId) {
|
|
18015
18141
|
const approved = skills2.filter((s) => s.status === "approved");
|
|
@@ -18810,7 +18936,8 @@ async function sendBackendModelPicker(chatId, backendId, channel, messageId) {
|
|
|
18810
18936
|
const summary = backendConfigSummary(chatId, backendId, false);
|
|
18811
18937
|
if (adapter.type === "api") {
|
|
18812
18938
|
const apiModels = getApiModels(backendId);
|
|
18813
|
-
|
|
18939
|
+
const adapterModelCount = Object.keys(adapter.availableModels).length;
|
|
18940
|
+
if (apiModels.length === 0 && adapterModelCount === 0) {
|
|
18814
18941
|
await sendOrEditKeyboard(
|
|
18815
18942
|
chatId,
|
|
18816
18943
|
channel,
|
|
@@ -18825,23 +18952,25 @@ No models configured. Add one with \u2795`,
|
|
|
18825
18952
|
);
|
|
18826
18953
|
return;
|
|
18827
18954
|
}
|
|
18828
|
-
|
|
18829
|
-
|
|
18830
|
-
const
|
|
18831
|
-
|
|
18832
|
-
|
|
18833
|
-
|
|
18834
|
-
|
|
18835
|
-
|
|
18836
|
-
|
|
18837
|
-
|
|
18838
|
-
|
|
18839
|
-
|
|
18955
|
+
if (apiModels.length > 0) {
|
|
18956
|
+
const modelButtons2 = [];
|
|
18957
|
+
for (const m of apiModels) {
|
|
18958
|
+
const isActive = m.modelId === currentModel;
|
|
18959
|
+
const freeTag = m.isFree && !m.displayName.toLowerCase().includes("free") ? " (free)" : "";
|
|
18960
|
+
modelButtons2.push([
|
|
18961
|
+
{
|
|
18962
|
+
label: `${isActive ? "\u2713 " : ""}${m.displayName}${freeTag}`,
|
|
18963
|
+
data: `apimodel:sel:${m.id}`,
|
|
18964
|
+
...isActive ? { style: "primary" } : {}
|
|
18965
|
+
},
|
|
18966
|
+
{ label: "\u{1F5D1}", data: `apimodel:del:${m.id}` }
|
|
18967
|
+
]);
|
|
18968
|
+
}
|
|
18969
|
+
modelButtons2.push([{ label: "\u2795 Add Model", data: `apimodel:add:${backendId}` }]);
|
|
18970
|
+
modelButtons2.push([{ label: "\u2190 Back", data: `bconf:panel:${backendId}` }]);
|
|
18971
|
+
await sendOrEditKeyboard(chatId, channel, messageId, summary, modelButtons2);
|
|
18972
|
+
return;
|
|
18840
18973
|
}
|
|
18841
|
-
modelButtons2.push([{ label: "\u2795 Add Model", data: `apimodel:add:${backendId}` }]);
|
|
18842
|
-
modelButtons2.push([{ label: "\u2190 Back", data: `bconf:panel:${backendId}` }]);
|
|
18843
|
-
await sendOrEditKeyboard(chatId, channel, messageId, summary, modelButtons2);
|
|
18844
|
-
return;
|
|
18845
18974
|
}
|
|
18846
18975
|
const modelButtons = Object.entries(adapter.availableModels).map(([id, info]) => [{
|
|
18847
18976
|
label: `${id === currentModel ? "\u2713 " : ""}${info.label}`,
|
|
@@ -22467,18 +22596,26 @@ var init_ollama2 = __esm({
|
|
|
22467
22596
|
*/
|
|
22468
22597
|
async streamDirect(prompt, model2, opts) {
|
|
22469
22598
|
const cleanPrompt = stripForLocalModel(prompt);
|
|
22470
|
-
let
|
|
22599
|
+
let disableThinking = false;
|
|
22471
22600
|
try {
|
|
22472
22601
|
const { OllamaStore } = (init_ollama(), __toCommonJS(ollama_exports));
|
|
22473
22602
|
const modelRecord = OllamaStore.getModelByName(model2);
|
|
22474
|
-
|
|
22603
|
+
if (modelRecord?.forceThinkOff) {
|
|
22604
|
+
disableThinking = true;
|
|
22605
|
+
} else if (opts?.thinkingLevel === "off") {
|
|
22606
|
+
disableThinking = true;
|
|
22607
|
+
}
|
|
22475
22608
|
} catch {
|
|
22476
22609
|
}
|
|
22477
22610
|
const apiOpts = {
|
|
22478
22611
|
timeoutMs: opts?.timeoutMs,
|
|
22479
22612
|
onStream: opts?.onStream,
|
|
22480
22613
|
signal: opts?.signal,
|
|
22481
|
-
|
|
22614
|
+
messageHistory: opts?.messageHistory,
|
|
22615
|
+
permMode: opts?.permMode,
|
|
22616
|
+
thinkingLevel: opts?.thinkingLevel,
|
|
22617
|
+
onThinking: opts?.onThinking,
|
|
22618
|
+
...disableThinking ? { providerOptions: { ollama: { think: false } } } : {}
|
|
22482
22619
|
};
|
|
22483
22620
|
const result = await this.streamDirectWithHistory(
|
|
22484
22621
|
cleanPrompt,
|
|
@@ -22503,9 +22640,10 @@ var init_ollama2 = __esm({
|
|
|
22503
22640
|
const { OllamaStore } = (init_ollama(), __toCommonJS(ollama_exports));
|
|
22504
22641
|
const models = OllamaStore.getAvailableModels();
|
|
22505
22642
|
for (const m of models) {
|
|
22643
|
+
const isThinkingCapable = m.capability === "thinking" && !m.forceThinkOff;
|
|
22506
22644
|
this.availableModels[m.name] = {
|
|
22507
22645
|
label: `${m.name}${m.parameterSize ? ` (${m.parameterSize})` : ""}`,
|
|
22508
|
-
thinking: "none"
|
|
22646
|
+
thinking: isThinkingCapable ? "adjustable" : "none"
|
|
22509
22647
|
};
|
|
22510
22648
|
this.pricing[m.name] = { in: 0, out: 0, cache: 0 };
|
|
22511
22649
|
this.contextWindow[m.name] = m.contextWindow ?? 4096;
|
|
@@ -22800,7 +22938,7 @@ var init_backends = __esm({
|
|
|
22800
22938
|
ollama: new OllamaAdapter(),
|
|
22801
22939
|
openrouter: openRouterAdapter
|
|
22802
22940
|
};
|
|
22803
|
-
CHAT_BACKEND_IDS = ["claude", "gemini", "codex", "cursor", "openrouter"];
|
|
22941
|
+
CHAT_BACKEND_IDS = ["claude", "gemini", "codex", "cursor", "openrouter", "ollama"];
|
|
22804
22942
|
availableSet = /* @__PURE__ */ new Set();
|
|
22805
22943
|
}
|
|
22806
22944
|
});
|
|
@@ -24397,13 +24535,152 @@ var init_session_log2 = __esm({
|
|
|
24397
24535
|
}
|
|
24398
24536
|
});
|
|
24399
24537
|
|
|
24400
|
-
// src/
|
|
24401
|
-
|
|
24402
|
-
|
|
24538
|
+
// src/channels/edit-coordinator.ts
|
|
24539
|
+
var edit_coordinator_exports = {};
|
|
24540
|
+
__export(edit_coordinator_exports, {
|
|
24541
|
+
getEditCoordinator: () => getEditCoordinator,
|
|
24542
|
+
resetEditCoordinator: () => resetEditCoordinator
|
|
24543
|
+
});
|
|
24544
|
+
function getEditCoordinator() {
|
|
24545
|
+
return EditCoordinator.getInstance();
|
|
24403
24546
|
}
|
|
24404
|
-
function
|
|
24405
|
-
|
|
24547
|
+
function resetEditCoordinator() {
|
|
24548
|
+
EditCoordinator.resetInstance();
|
|
24406
24549
|
}
|
|
24550
|
+
var TICK_INTERVAL_MS, MAX_EDITS_PER_WINDOW, EDIT_WINDOW_MS, EditCoordinator;
|
|
24551
|
+
var init_edit_coordinator = __esm({
|
|
24552
|
+
"src/channels/edit-coordinator.ts"() {
|
|
24553
|
+
"use strict";
|
|
24554
|
+
init_log();
|
|
24555
|
+
TICK_INTERVAL_MS = 1e3;
|
|
24556
|
+
MAX_EDITS_PER_WINDOW = 4;
|
|
24557
|
+
EDIT_WINDOW_MS = 6e4;
|
|
24558
|
+
EditCoordinator = class _EditCoordinator {
|
|
24559
|
+
static instance = null;
|
|
24560
|
+
/** Active streams indexed by messageId. */
|
|
24561
|
+
activeStreams = /* @__PURE__ */ new Map();
|
|
24562
|
+
/** Per-message edit tracking for the sliding window cap. */
|
|
24563
|
+
perMessageEditCount = /* @__PURE__ */ new Map();
|
|
24564
|
+
/** Single flush timer shared across all streams. */
|
|
24565
|
+
flushTimer = null;
|
|
24566
|
+
/** Round-robin index — cycles through stream keys. */
|
|
24567
|
+
roundRobinIndex = 0;
|
|
24568
|
+
/** Ordered list of stream keys for round-robin iteration.
|
|
24569
|
+
* Rebuilt on register/unregister to avoid iterator invalidation. */
|
|
24570
|
+
streamKeys = [];
|
|
24571
|
+
constructor() {
|
|
24572
|
+
}
|
|
24573
|
+
static getInstance() {
|
|
24574
|
+
if (!_EditCoordinator.instance) {
|
|
24575
|
+
_EditCoordinator.instance = new _EditCoordinator();
|
|
24576
|
+
}
|
|
24577
|
+
return _EditCoordinator.instance;
|
|
24578
|
+
}
|
|
24579
|
+
/** Reset the singleton (for testing only). */
|
|
24580
|
+
static resetInstance() {
|
|
24581
|
+
if (_EditCoordinator.instance) {
|
|
24582
|
+
_EditCoordinator.instance.shutdown();
|
|
24583
|
+
_EditCoordinator.instance = null;
|
|
24584
|
+
}
|
|
24585
|
+
}
|
|
24586
|
+
/** Register a stream to be managed by the coordinator.
|
|
24587
|
+
* Creates the flush timer if this is the first stream. */
|
|
24588
|
+
register(messageId, stream) {
|
|
24589
|
+
this.activeStreams.set(messageId, stream);
|
|
24590
|
+
this.rebuildKeys();
|
|
24591
|
+
log(`[edit-coordinator] registered stream ${messageId} (${this.activeStreams.size} active)`);
|
|
24592
|
+
if (!this.flushTimer) {
|
|
24593
|
+
this.flushTimer = setInterval(() => this.tick(), TICK_INTERVAL_MS);
|
|
24594
|
+
log(`[edit-coordinator] timer started (${TICK_INTERVAL_MS}ms tick)`);
|
|
24595
|
+
}
|
|
24596
|
+
}
|
|
24597
|
+
/** Unregister a stream (called on finalization).
|
|
24598
|
+
* Cleans up per-message edit tracking. Stops timer if no streams remain. */
|
|
24599
|
+
unregister(messageId) {
|
|
24600
|
+
this.activeStreams.delete(messageId);
|
|
24601
|
+
this.perMessageEditCount.delete(messageId);
|
|
24602
|
+
this.rebuildKeys();
|
|
24603
|
+
log(`[edit-coordinator] unregistered stream ${messageId} (${this.activeStreams.size} remaining)`);
|
|
24604
|
+
if (this.activeStreams.size === 0 && this.flushTimer) {
|
|
24605
|
+
clearInterval(this.flushTimer);
|
|
24606
|
+
this.flushTimer = null;
|
|
24607
|
+
this.roundRobinIndex = 0;
|
|
24608
|
+
log(`[edit-coordinator] timer stopped (no active streams)`);
|
|
24609
|
+
}
|
|
24610
|
+
}
|
|
24611
|
+
/** Shut down the coordinator — clear timer, remove all streams. */
|
|
24612
|
+
shutdown() {
|
|
24613
|
+
if (this.flushTimer) {
|
|
24614
|
+
clearInterval(this.flushTimer);
|
|
24615
|
+
this.flushTimer = null;
|
|
24616
|
+
}
|
|
24617
|
+
this.activeStreams.clear();
|
|
24618
|
+
this.perMessageEditCount.clear();
|
|
24619
|
+
this.streamKeys = [];
|
|
24620
|
+
this.roundRobinIndex = 0;
|
|
24621
|
+
}
|
|
24622
|
+
/** Get the number of active streams (for diagnostics). */
|
|
24623
|
+
get streamCount() {
|
|
24624
|
+
return this.activeStreams.size;
|
|
24625
|
+
}
|
|
24626
|
+
/** Check whether a message can be edited (under the per-message cap). */
|
|
24627
|
+
canEditMessage(messageId) {
|
|
24628
|
+
const record = this.perMessageEditCount.get(messageId);
|
|
24629
|
+
if (!record) return true;
|
|
24630
|
+
const now = Date.now();
|
|
24631
|
+
if (now - record.windowStart >= EDIT_WINDOW_MS) {
|
|
24632
|
+
return true;
|
|
24633
|
+
}
|
|
24634
|
+
return record.count < MAX_EDITS_PER_WINDOW;
|
|
24635
|
+
}
|
|
24636
|
+
/** Record that an edit was made to a message. */
|
|
24637
|
+
recordEdit(messageId) {
|
|
24638
|
+
const now = Date.now();
|
|
24639
|
+
const record = this.perMessageEditCount.get(messageId);
|
|
24640
|
+
if (!record || now - record.windowStart >= EDIT_WINDOW_MS) {
|
|
24641
|
+
this.perMessageEditCount.set(messageId, { count: 1, windowStart: now });
|
|
24642
|
+
} else {
|
|
24643
|
+
record.count++;
|
|
24644
|
+
}
|
|
24645
|
+
}
|
|
24646
|
+
// ── Internal ──────────────────────────────────────────────────────────
|
|
24647
|
+
/** Rebuild the ordered keys array after registration changes. */
|
|
24648
|
+
rebuildKeys() {
|
|
24649
|
+
this.streamKeys = Array.from(this.activeStreams.keys());
|
|
24650
|
+
if (this.streamKeys.length > 0) {
|
|
24651
|
+
this.roundRobinIndex = this.roundRobinIndex % this.streamKeys.length;
|
|
24652
|
+
} else {
|
|
24653
|
+
this.roundRobinIndex = 0;
|
|
24654
|
+
}
|
|
24655
|
+
}
|
|
24656
|
+
/** Timer tick — pick the next stream via round-robin and flush it.
|
|
24657
|
+
* If the selected stream is at its per-message edit cap, try the next one. */
|
|
24658
|
+
async tick() {
|
|
24659
|
+
if (this.streamKeys.length === 0) return;
|
|
24660
|
+
const startIdx = this.roundRobinIndex;
|
|
24661
|
+
let tried = 0;
|
|
24662
|
+
while (tried < this.streamKeys.length) {
|
|
24663
|
+
const idx = (startIdx + tried) % this.streamKeys.length;
|
|
24664
|
+
const messageId = this.streamKeys[idx];
|
|
24665
|
+
const stream = this.activeStreams.get(messageId);
|
|
24666
|
+
if (stream && this.canEditMessage(messageId)) {
|
|
24667
|
+
this.roundRobinIndex = (idx + 1) % this.streamKeys.length;
|
|
24668
|
+
try {
|
|
24669
|
+
await stream.flush();
|
|
24670
|
+
this.recordEdit(messageId);
|
|
24671
|
+
} catch {
|
|
24672
|
+
}
|
|
24673
|
+
return;
|
|
24674
|
+
}
|
|
24675
|
+
tried++;
|
|
24676
|
+
}
|
|
24677
|
+
this.roundRobinIndex = (startIdx + 1) % this.streamKeys.length;
|
|
24678
|
+
}
|
|
24679
|
+
};
|
|
24680
|
+
}
|
|
24681
|
+
});
|
|
24682
|
+
|
|
24683
|
+
// src/router/live-status.ts
|
|
24407
24684
|
function dedupThinking(entries) {
|
|
24408
24685
|
const out = [];
|
|
24409
24686
|
for (const e of entries) {
|
|
@@ -24467,18 +24744,15 @@ function makeLiveStatus(chatId, channel, modelLabel, verboseLevel, showThinking)
|
|
|
24467
24744
|
};
|
|
24468
24745
|
return { liveStatus, toolCb };
|
|
24469
24746
|
}
|
|
24470
|
-
var
|
|
24747
|
+
var MAX_THINKING_CHARS, TRIM_THRESHOLD, MAX_ENTRIES, SPINNER_FRAMES, HEARTBEAT_TEXTS, LiveStatusMessage;
|
|
24471
24748
|
var init_live_status = __esm({
|
|
24472
24749
|
"src/router/live-status.ts"() {
|
|
24473
24750
|
"use strict";
|
|
24474
24751
|
init_log();
|
|
24475
24752
|
init_helpers();
|
|
24476
24753
|
init_telegram_throttle();
|
|
24477
|
-
|
|
24478
|
-
FLUSH_INTERVAL_GROUP_MS = 5e3;
|
|
24754
|
+
init_edit_coordinator();
|
|
24479
24755
|
MAX_THINKING_CHARS = 800;
|
|
24480
|
-
GLOBAL_MIN_GAP_MS = 1e3;
|
|
24481
|
-
globalLastFlushAt = 0;
|
|
24482
24756
|
TRIM_THRESHOLD = 3500;
|
|
24483
24757
|
MAX_ENTRIES = 200;
|
|
24484
24758
|
SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
@@ -24494,7 +24768,6 @@ var init_live_status = __esm({
|
|
|
24494
24768
|
messageId = null;
|
|
24495
24769
|
entries = [];
|
|
24496
24770
|
startTime = Date.now();
|
|
24497
|
-
flushTimer = null;
|
|
24498
24771
|
lastRendered = "";
|
|
24499
24772
|
finalized = false;
|
|
24500
24773
|
/** Earliest time the next flush is allowed (set after 429 backoff) */
|
|
@@ -24509,18 +24782,12 @@ var init_live_status = __esm({
|
|
|
24509
24782
|
/** Spinner frame counter — advances on each flush for animation. */
|
|
24510
24783
|
spinnerFrame = 0;
|
|
24511
24784
|
/** Timestamp of last successful edit — used for heartbeat force-through. */
|
|
24512
|
-
lastSuccessfulFlushAt = Date.now();
|
|
24513
24785
|
/** Callback to restart typing indicator as fallback. */
|
|
24514
24786
|
onTypingFallback;
|
|
24515
24787
|
/** Set a callback that restarts the typing indicator loop as a fallback. */
|
|
24516
24788
|
setTypingFallback(cb) {
|
|
24517
24789
|
this.onTypingFallback = cb;
|
|
24518
24790
|
}
|
|
24519
|
-
/** Resolve flush interval based on chat type (group chats are rate-limited more aggressively). */
|
|
24520
|
-
get flushIntervalMs() {
|
|
24521
|
-
const numericId = parseInt(this.chatId);
|
|
24522
|
-
return numericId < 0 ? FLUSH_INTERVAL_GROUP_MS : FLUSH_INTERVAL_DM_MS;
|
|
24523
|
-
}
|
|
24524
24791
|
/** Send the initial status message. Must be called before adding entries. */
|
|
24525
24792
|
async init() {
|
|
24526
24793
|
if (!this.channel.sendTextReturningId) return;
|
|
@@ -24528,9 +24795,7 @@ var init_live_status = __esm({
|
|
|
24528
24795
|
const initial = `\u23F3 ${this.modelLabel} \xB7 Processing\u2026`;
|
|
24529
24796
|
this.messageId = await this.channel.sendTextReturningId(this.chatId, initial, "plain") ?? null;
|
|
24530
24797
|
if (this.messageId) {
|
|
24531
|
-
this.
|
|
24532
|
-
this.flushTimer = setInterval(() => this.flush().catch(() => {
|
|
24533
|
-
}), this.flushIntervalMs);
|
|
24798
|
+
getEditCoordinator().register(this.messageId, this);
|
|
24534
24799
|
}
|
|
24535
24800
|
} catch (err) {
|
|
24536
24801
|
log(`[live-status] init failed: ${err}`);
|
|
@@ -24582,21 +24847,22 @@ var init_live_status = __esm({
|
|
|
24582
24847
|
}
|
|
24583
24848
|
/**
|
|
24584
24849
|
* Finalize the status message: replace the spinner with ✅ and elapsed time.
|
|
24585
|
-
*
|
|
24850
|
+
* Unregisters from EditCoordinator and sends one final P0_CRITICAL edit that
|
|
24851
|
+
* bypasses the coordinator entirely (finalization must never be skipped).
|
|
24852
|
+
* No-op if no message was created (channel doesn't support editing).
|
|
24586
24853
|
*/
|
|
24587
24854
|
async finalize(elapsedMs) {
|
|
24588
24855
|
this.finalized = true;
|
|
24589
24856
|
this.pendingTools.clear();
|
|
24590
|
-
if (this.
|
|
24591
|
-
|
|
24592
|
-
this.flushTimer = null;
|
|
24857
|
+
if (this.messageId) {
|
|
24858
|
+
getEditCoordinator().unregister(this.messageId);
|
|
24593
24859
|
}
|
|
24594
24860
|
if (!this.messageId || !this.channel.editText) return;
|
|
24595
24861
|
const elapsedSec = (elapsedMs / 1e3).toFixed(1);
|
|
24596
24862
|
const deduped = dedupThinking(this.entries);
|
|
24597
24863
|
const body = renderFinal(deduped, this.modelLabel, elapsedSec, this.hasTrimmed);
|
|
24598
24864
|
try {
|
|
24599
|
-
await this.channel.editText(this.chatId, this.messageId, body, "plain");
|
|
24865
|
+
await this.channel.editText(this.chatId, this.messageId, body, "plain", 0 /* P0_CRITICAL */);
|
|
24600
24866
|
} catch (err) {
|
|
24601
24867
|
log(`[live-status] finalize edit failed: ${err}`);
|
|
24602
24868
|
}
|
|
@@ -24605,7 +24871,16 @@ var init_live_status = __esm({
|
|
|
24605
24871
|
getMessageId() {
|
|
24606
24872
|
return this.messageId;
|
|
24607
24873
|
}
|
|
24608
|
-
// ──
|
|
24874
|
+
// ── FlushableStream interface ──────────────────────────────────────────
|
|
24875
|
+
/** Return the chatId this stream belongs to (FlushableStream interface). */
|
|
24876
|
+
getChatId() {
|
|
24877
|
+
return this.chatId;
|
|
24878
|
+
}
|
|
24879
|
+
/**
|
|
24880
|
+
* Flush the current status to Telegram via editMessageText.
|
|
24881
|
+
* Called by the EditCoordinator on each round-robin tick.
|
|
24882
|
+
* Public to satisfy FlushableStream interface — do not call directly.
|
|
24883
|
+
*/
|
|
24609
24884
|
async flush() {
|
|
24610
24885
|
if (this.finalized || !this.messageId || !this.channel.editText) return;
|
|
24611
24886
|
if (this.consecutiveEditFailures >= _LiveStatusMessage.MAX_EDIT_FAILURES) {
|
|
@@ -24618,10 +24893,9 @@ var init_live_status = __esm({
|
|
|
24618
24893
|
if (Date.now() < this.nextFlushAllowedAt) return;
|
|
24619
24894
|
const throttleState = getThrottleState();
|
|
24620
24895
|
if (throttleState?.isPaused) return;
|
|
24621
|
-
if (!canFlushGlobally()) return;
|
|
24622
24896
|
this.spinnerFrame++;
|
|
24623
24897
|
const deduped = dedupThinking(this.entries);
|
|
24624
|
-
|
|
24898
|
+
let body = renderEntries(
|
|
24625
24899
|
deduped,
|
|
24626
24900
|
this.modelLabel,
|
|
24627
24901
|
Date.now() - this.startTime,
|
|
@@ -24629,17 +24903,21 @@ var init_live_status = __esm({
|
|
|
24629
24903
|
this.pendingTools,
|
|
24630
24904
|
this.spinnerFrame
|
|
24631
24905
|
);
|
|
24906
|
+
if (throttleState && throttleState.queueDepth > 30) {
|
|
24907
|
+
body += `
|
|
24908
|
+
(queue: ${throttleState.queueDepth})`;
|
|
24909
|
+
}
|
|
24632
24910
|
if (body === this.lastRendered) return;
|
|
24633
24911
|
this.lastRendered = body;
|
|
24634
|
-
markGlobalFlush();
|
|
24635
24912
|
try {
|
|
24636
|
-
await this.channel.editText(this.chatId, this.messageId, body, "plain");
|
|
24913
|
+
await this.channel.editText(this.chatId, this.messageId, body, "plain", 2 /* P2_COSMETIC */);
|
|
24637
24914
|
this.consecutiveEditFailures = 0;
|
|
24638
24915
|
this.lastSuccessfulFlushAt = Date.now();
|
|
24639
24916
|
} catch (err) {
|
|
24640
24917
|
this.handleRateLimit(err);
|
|
24641
24918
|
}
|
|
24642
24919
|
}
|
|
24920
|
+
// ── Internal ──────────────────────────────────────────────────────────
|
|
24643
24921
|
/**
|
|
24644
24922
|
* Trim entries from the BEGINNING when the rendered body exceeds the threshold.
|
|
24645
24923
|
* This is the core of the single-message pattern: always show the most recent
|
|
@@ -25070,6 +25348,88 @@ var init_response = __esm({
|
|
|
25070
25348
|
}
|
|
25071
25349
|
});
|
|
25072
25350
|
|
|
25351
|
+
// src/channels/typing-manager.ts
|
|
25352
|
+
var typing_manager_exports = {};
|
|
25353
|
+
__export(typing_manager_exports, {
|
|
25354
|
+
getTypingManager: () => getTypingManager,
|
|
25355
|
+
resetTypingManager: () => resetTypingManager
|
|
25356
|
+
});
|
|
25357
|
+
function getTypingManager() {
|
|
25358
|
+
return TypingManager.getInstance();
|
|
25359
|
+
}
|
|
25360
|
+
function resetTypingManager() {
|
|
25361
|
+
TypingManager.resetInstance();
|
|
25362
|
+
}
|
|
25363
|
+
var TypingManager;
|
|
25364
|
+
var init_typing_manager = __esm({
|
|
25365
|
+
"src/channels/typing-manager.ts"() {
|
|
25366
|
+
"use strict";
|
|
25367
|
+
TypingManager = class _TypingManager {
|
|
25368
|
+
static instance = null;
|
|
25369
|
+
activeChats = /* @__PURE__ */ new Map();
|
|
25370
|
+
static getInstance() {
|
|
25371
|
+
if (!_TypingManager.instance) {
|
|
25372
|
+
_TypingManager.instance = new _TypingManager();
|
|
25373
|
+
}
|
|
25374
|
+
return _TypingManager.instance;
|
|
25375
|
+
}
|
|
25376
|
+
/**
|
|
25377
|
+
* Start showing typing indicator for this chat.
|
|
25378
|
+
* If already showing (another agent acquired), just increments refCount.
|
|
25379
|
+
*/
|
|
25380
|
+
acquire(chatId, channel) {
|
|
25381
|
+
const entry = this.activeChats.get(chatId);
|
|
25382
|
+
if (entry) {
|
|
25383
|
+
entry.refCount++;
|
|
25384
|
+
return;
|
|
25385
|
+
}
|
|
25386
|
+
channel.sendTyping?.(chatId).catch(() => {
|
|
25387
|
+
});
|
|
25388
|
+
const timer = setInterval(() => {
|
|
25389
|
+
channel.sendTyping?.(chatId).catch(() => {
|
|
25390
|
+
});
|
|
25391
|
+
}, 4e3);
|
|
25392
|
+
this.activeChats.set(chatId, { refCount: 1, timer });
|
|
25393
|
+
}
|
|
25394
|
+
/**
|
|
25395
|
+
* Stop showing typing for this agent's perspective.
|
|
25396
|
+
* Only stops the timer when refCount reaches 0.
|
|
25397
|
+
*/
|
|
25398
|
+
release(chatId) {
|
|
25399
|
+
const entry = this.activeChats.get(chatId);
|
|
25400
|
+
if (!entry) return;
|
|
25401
|
+
entry.refCount--;
|
|
25402
|
+
if (entry.refCount <= 0) {
|
|
25403
|
+
clearInterval(entry.timer);
|
|
25404
|
+
this.activeChats.delete(chatId);
|
|
25405
|
+
}
|
|
25406
|
+
}
|
|
25407
|
+
/** Clean shutdown — clear all timers. */
|
|
25408
|
+
shutdown() {
|
|
25409
|
+
for (const [, entry] of this.activeChats) {
|
|
25410
|
+
clearInterval(entry.timer);
|
|
25411
|
+
}
|
|
25412
|
+
this.activeChats.clear();
|
|
25413
|
+
}
|
|
25414
|
+
/** Expose active chat count for testing/diagnostics. */
|
|
25415
|
+
get size() {
|
|
25416
|
+
return this.activeChats.size;
|
|
25417
|
+
}
|
|
25418
|
+
/** Get ref count for a chat (for testing). */
|
|
25419
|
+
getRefCount(chatId) {
|
|
25420
|
+
return this.activeChats.get(chatId)?.refCount ?? 0;
|
|
25421
|
+
}
|
|
25422
|
+
/** Reset singleton (for testing only). */
|
|
25423
|
+
static resetInstance() {
|
|
25424
|
+
if (_TypingManager.instance) {
|
|
25425
|
+
_TypingManager.instance.shutdown();
|
|
25426
|
+
_TypingManager.instance = null;
|
|
25427
|
+
}
|
|
25428
|
+
}
|
|
25429
|
+
};
|
|
25430
|
+
}
|
|
25431
|
+
});
|
|
25432
|
+
|
|
25073
25433
|
// src/shell/exec.ts
|
|
25074
25434
|
import { execFile as execFile4 } from "child_process";
|
|
25075
25435
|
function resolveShell() {
|
|
@@ -25685,18 +26045,7 @@ async function handleSideQuest(parentChatId, msg, channel) {
|
|
|
25685
26045
|
[{ label: "\u274C Cancel", data: `sq:cancel:${sqId}` }]
|
|
25686
26046
|
]);
|
|
25687
26047
|
const startTime = Date.now();
|
|
25688
|
-
|
|
25689
|
-
const typingLoop = async () => {
|
|
25690
|
-
while (typingActive) {
|
|
25691
|
-
try {
|
|
25692
|
-
await channel.sendTyping?.(parentChatId);
|
|
25693
|
-
} catch {
|
|
25694
|
-
}
|
|
25695
|
-
await new Promise((r) => setTimeout(r, 4e3));
|
|
25696
|
-
}
|
|
25697
|
-
};
|
|
25698
|
-
typingLoop().catch(() => {
|
|
25699
|
-
});
|
|
26048
|
+
getTypingManager().acquire(parentChatId, channel);
|
|
25700
26049
|
try {
|
|
25701
26050
|
const backend2 = getBackend(parentChatId);
|
|
25702
26051
|
const model2 = getModel(parentChatId);
|
|
@@ -25712,7 +26061,6 @@ async function handleSideQuest(parentChatId, msg, channel) {
|
|
|
25712
26061
|
entityType: "main",
|
|
25713
26062
|
bootstrapProfile: "interactive"
|
|
25714
26063
|
});
|
|
25715
|
-
typingActive = false;
|
|
25716
26064
|
const userText = msg.text ?? "";
|
|
25717
26065
|
const truncated = userText.length > 60 ? userText.slice(0, 57) + "\u2026" : userText;
|
|
25718
26066
|
const header2 = `\u{1F5FA} <b>Side quest: "${truncated}"</b>
|
|
@@ -25736,10 +26084,9 @@ async function handleSideQuest(parentChatId, msg, channel) {
|
|
|
25736
26084
|
log(`[reflection] Side quest signal detection error: ${e}`);
|
|
25737
26085
|
}
|
|
25738
26086
|
} catch (err) {
|
|
25739
|
-
typingActive = false;
|
|
25740
26087
|
await channel.sendText(parentChatId, `\u{1F5FA} Side quest failed: ${err.message}`, { parseMode: "plain" });
|
|
25741
26088
|
} finally {
|
|
25742
|
-
|
|
26089
|
+
getTypingManager().release(parentChatId);
|
|
25743
26090
|
const activeSet = activeSideQuests.get(parentChatId);
|
|
25744
26091
|
if (activeSet) {
|
|
25745
26092
|
activeSet.delete(sqId);
|
|
@@ -25751,6 +26098,7 @@ async function handleSideQuest(parentChatId, msg, channel) {
|
|
|
25751
26098
|
var init_sidequest = __esm({
|
|
25752
26099
|
"src/router/sidequest.ts"() {
|
|
25753
26100
|
"use strict";
|
|
26101
|
+
init_typing_manager();
|
|
25754
26102
|
init_agent();
|
|
25755
26103
|
init_log();
|
|
25756
26104
|
init_store5();
|
|
@@ -26192,19 +26540,27 @@ async function handleEvolveCallback(chatId, data, channel, messageId) {
|
|
|
26192
26540
|
break;
|
|
26193
26541
|
}
|
|
26194
26542
|
case "apply": {
|
|
26195
|
-
const
|
|
26196
|
-
|
|
26197
|
-
|
|
26198
|
-
|
|
26199
|
-
|
|
26200
|
-
|
|
26201
|
-
|
|
26202
|
-
|
|
26203
|
-
|
|
26204
|
-
|
|
26205
|
-
|
|
26206
|
-
|
|
26207
|
-
|
|
26543
|
+
const id = parseInt(idStr, 10);
|
|
26544
|
+
if (processingInsights.has(id)) break;
|
|
26545
|
+
processingInsights.add(id);
|
|
26546
|
+
await sendOrEditKeyboard(chatId, channel, messageId, "\u23F3 Applying...", []);
|
|
26547
|
+
try {
|
|
26548
|
+
const { applyInsight: applyInsight2 } = await Promise.resolve().then(() => (init_apply(), apply_exports));
|
|
26549
|
+
const { advanceReviewSession: advanceReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
|
|
26550
|
+
const result = await applyInsight2(id);
|
|
26551
|
+
advanceReviewSession2(getDb(), chatId, id, "applied");
|
|
26552
|
+
await sendOrEditKeyboard(
|
|
26553
|
+
chatId,
|
|
26554
|
+
channel,
|
|
26555
|
+
messageId,
|
|
26556
|
+
`\u2705 ${result.message}`,
|
|
26557
|
+
[]
|
|
26558
|
+
);
|
|
26559
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
26560
|
+
await sendCurrentProposalInPlace(chatId, channel, messageId);
|
|
26561
|
+
} finally {
|
|
26562
|
+
processingInsights.delete(id);
|
|
26563
|
+
}
|
|
26208
26564
|
break;
|
|
26209
26565
|
}
|
|
26210
26566
|
case "skip": {
|
|
@@ -26214,10 +26570,17 @@ async function handleEvolveCallback(chatId, data, channel, messageId) {
|
|
|
26214
26570
|
break;
|
|
26215
26571
|
}
|
|
26216
26572
|
case "reject": {
|
|
26217
|
-
const
|
|
26218
|
-
|
|
26219
|
-
|
|
26220
|
-
|
|
26573
|
+
const rejId = parseInt(idStr, 10);
|
|
26574
|
+
if (processingInsights.has(rejId)) break;
|
|
26575
|
+
processingInsights.add(rejId);
|
|
26576
|
+
try {
|
|
26577
|
+
const { updateInsightStatus: updateInsightStatus2, advanceReviewSession: advanceReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
|
|
26578
|
+
updateInsightStatus2(getDb(), rejId, "rejected");
|
|
26579
|
+
advanceReviewSession2(getDb(), chatId, rejId, "rejected");
|
|
26580
|
+
await sendCurrentProposalInPlace(chatId, channel, messageId);
|
|
26581
|
+
} finally {
|
|
26582
|
+
processingInsights.delete(rejId);
|
|
26583
|
+
}
|
|
26221
26584
|
break;
|
|
26222
26585
|
}
|
|
26223
26586
|
case "discuss": {
|
|
@@ -26464,11 +26827,13 @@ async function handleReflectCallback(chatId, data, channel, messageId) {
|
|
|
26464
26827
|
);
|
|
26465
26828
|
}
|
|
26466
26829
|
}
|
|
26830
|
+
var processingInsights;
|
|
26467
26831
|
var init_evolve2 = __esm({
|
|
26468
26832
|
"src/router/evolve.ts"() {
|
|
26469
26833
|
"use strict";
|
|
26470
26834
|
init_store5();
|
|
26471
26835
|
init_helpers();
|
|
26836
|
+
processingInsights = /* @__PURE__ */ new Set();
|
|
26472
26837
|
}
|
|
26473
26838
|
});
|
|
26474
26839
|
|
|
@@ -28637,12 +29002,12 @@ async function handleStopCommand(chatId, commandArgs, msg, channel) {
|
|
|
28637
29002
|
await channel.sendText(
|
|
28638
29003
|
chatId,
|
|
28639
29004
|
stopped ? "Stopping current task..." : "Nothing is running.",
|
|
28640
|
-
{ parseMode: "plain", priority:
|
|
29005
|
+
{ parseMode: "plain", priority: 0 /* P0_CRITICAL */ }
|
|
28641
29006
|
);
|
|
28642
29007
|
if (stopped && typeof channel.sendKeyboard === "function") {
|
|
28643
29008
|
await channel.sendKeyboard(chatId, "", [
|
|
28644
29009
|
[{ label: "\u{1F195} New Chat", data: "menu:newchat" }]
|
|
28645
|
-
], { priority:
|
|
29010
|
+
], { priority: 0 /* P0_CRITICAL */ });
|
|
28646
29011
|
}
|
|
28647
29012
|
}
|
|
28648
29013
|
async function handleVoiceCommand(chatId, commandArgs, msg, channel) {
|
|
@@ -29009,7 +29374,7 @@ async function handleClearCommand(chatId, _commandArgs, _msg, channel) {
|
|
|
29009
29374
|
clearChatPaidSlots(chatId);
|
|
29010
29375
|
setSessionStartedAt(chatId);
|
|
29011
29376
|
logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: "Session cleared (no summary)", detail: { field: "session", action: "clear" } });
|
|
29012
|
-
await channel.sendText(chatId, "\u{1F9FD} Session cleared. No summary saved.", { parseMode: "plain", priority:
|
|
29377
|
+
await channel.sendText(chatId, "\u{1F9FD} Session cleared. No summary saved.", { parseMode: "plain", priority: 0 /* P0_CRITICAL */ });
|
|
29013
29378
|
}
|
|
29014
29379
|
async function handleSummarizeCommand(chatId, commandArgs, msg, channel) {
|
|
29015
29380
|
if (commandArgs?.toLowerCase() === "all") {
|
|
@@ -29192,9 +29557,9 @@ async function handleStatusCommand(chatId, commandArgs, msg, channel) {
|
|
|
29192
29557
|
{ label: "Change Mode", data: "menu:permissions" },
|
|
29193
29558
|
{ label: "Change Style", data: "menu:style" }
|
|
29194
29559
|
]
|
|
29195
|
-
], { priority:
|
|
29560
|
+
], { priority: 0 /* P0_CRITICAL */ });
|
|
29196
29561
|
} else {
|
|
29197
|
-
await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain", priority:
|
|
29562
|
+
await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain", priority: 0 /* P0_CRITICAL */ });
|
|
29198
29563
|
}
|
|
29199
29564
|
}
|
|
29200
29565
|
async function handleBackendCommand2(chatId, commandArgs, msg, channel) {
|
|
@@ -30202,6 +30567,7 @@ async function handleCouncilCommand(chatId, commandArgs, msg, channel) {
|
|
|
30202
30567
|
var init_command_handlers = __esm({
|
|
30203
30568
|
"src/router/command-handlers.ts"() {
|
|
30204
30569
|
"use strict";
|
|
30570
|
+
init_telegram_throttle();
|
|
30205
30571
|
init_format();
|
|
30206
30572
|
init_log();
|
|
30207
30573
|
init_format_time();
|
|
@@ -33194,7 +33560,9 @@ function withThread(channel, threadId) {
|
|
|
33194
33560
|
// These operate on existing messages — no threadId needed
|
|
33195
33561
|
editText: channel.editText?.bind(channel),
|
|
33196
33562
|
editKeyboard: channel.editKeyboard?.bind(channel),
|
|
33197
|
-
reactToMessage: channel.reactToMessage?.bind(channel)
|
|
33563
|
+
reactToMessage: channel.reactToMessage?.bind(channel),
|
|
33564
|
+
isDraftCapable: channel.isDraftCapable?.bind(channel),
|
|
33565
|
+
sendMessageDraft: channel.sendMessageDraft?.bind(channel)
|
|
33198
33566
|
};
|
|
33199
33567
|
}
|
|
33200
33568
|
var init_thread_wrapper = __esm({
|
|
@@ -34116,18 +34484,7 @@ You're still in discussion mode \u2014 try again or click a button to exit.`, {
|
|
|
34116
34484
|
isSideQuest: hasSqPrefix
|
|
34117
34485
|
})) {
|
|
34118
34486
|
const planDirective = buildPlanningDirective();
|
|
34119
|
-
|
|
34120
|
-
const typingLoop2 = async () => {
|
|
34121
|
-
while (typingActive2) {
|
|
34122
|
-
try {
|
|
34123
|
-
await channel.sendTyping?.(chatId);
|
|
34124
|
-
} catch {
|
|
34125
|
-
}
|
|
34126
|
-
await new Promise((r) => setTimeout(r, 4e3));
|
|
34127
|
-
}
|
|
34128
|
-
};
|
|
34129
|
-
typingLoop2().catch(() => {
|
|
34130
|
-
});
|
|
34487
|
+
getTypingManager().acquire(chatId, channel);
|
|
34131
34488
|
try {
|
|
34132
34489
|
const planResponse = await askAgent(chatId, cleanText || text, {
|
|
34133
34490
|
cwd: settings.getCwd(),
|
|
@@ -34137,7 +34494,7 @@ You're still in discussion mode \u2014 try again or click a button to exit.`, {
|
|
|
34137
34494
|
agentMode: effectiveAgentMode,
|
|
34138
34495
|
planningDirective: planDirective
|
|
34139
34496
|
});
|
|
34140
|
-
|
|
34497
|
+
getTypingManager().release(chatId);
|
|
34141
34498
|
if (planResponse.text) {
|
|
34142
34499
|
let planText = planResponse.text.replace(/\[REACT:.+?\]/g, "").replace(/\[SEND_FILE:.+?\]/g, "").replace(/\[GENERATE_IMAGE:.+?\]/g, "").replace(/\[HISTORY_SEARCH:[^\]]+\]/g, "").trim();
|
|
34143
34500
|
const PLAN_DISPLAY_LIMIT = 3500;
|
|
@@ -34162,23 +34519,12 @@ You're still in discussion mode \u2014 try again or click a button to exit.`, {
|
|
|
34162
34519
|
await channel.sendText(chatId, "(No plan generated \u2014 proceeding normally)", { parseMode: "plain" });
|
|
34163
34520
|
}
|
|
34164
34521
|
} catch (err) {
|
|
34165
|
-
|
|
34522
|
+
getTypingManager().release(chatId);
|
|
34166
34523
|
await channel.sendText(chatId, `\u26A0\uFE0F Planning error: ${err.message}`, { parseMode: "plain" });
|
|
34167
34524
|
}
|
|
34168
34525
|
return;
|
|
34169
34526
|
}
|
|
34170
|
-
|
|
34171
|
-
const typingLoop = async () => {
|
|
34172
|
-
while (typingActive) {
|
|
34173
|
-
try {
|
|
34174
|
-
await channel.sendTyping?.(chatId);
|
|
34175
|
-
} catch {
|
|
34176
|
-
}
|
|
34177
|
-
await new Promise((r) => setTimeout(r, 4e3));
|
|
34178
|
-
}
|
|
34179
|
-
};
|
|
34180
|
-
typingLoop().catch(() => {
|
|
34181
|
-
});
|
|
34527
|
+
getTypingManager().acquire(chatId, channel);
|
|
34182
34528
|
try {
|
|
34183
34529
|
const tMode = settings.getMode();
|
|
34184
34530
|
const tVerbose = settings.getVerboseLevel();
|
|
@@ -34222,6 +34568,28 @@ You're still in discussion mode \u2014 try again or click a button to exit.`, {
|
|
|
34222
34568
|
}
|
|
34223
34569
|
};
|
|
34224
34570
|
}
|
|
34571
|
+
const DRAFT_FLUSH_INTERVAL_MS = 500;
|
|
34572
|
+
const useDraftStreaming = channel.sendMessageDraft && channel.isDraftCapable?.(chatId);
|
|
34573
|
+
const draftState = useDraftStreaming ? { accumulated: "", draftId: Date.now() & 2147483647, dirty: false, flushTimer: null } : null;
|
|
34574
|
+
let onStreamCb;
|
|
34575
|
+
if (draftState) {
|
|
34576
|
+
draftState.flushTimer = setInterval(() => {
|
|
34577
|
+
if (draftState.dirty) {
|
|
34578
|
+
draftState.dirty = false;
|
|
34579
|
+
channel.sendMessageDraft(chatId, draftState.draftId, draftState.accumulated);
|
|
34580
|
+
}
|
|
34581
|
+
}, DRAFT_FLUSH_INTERVAL_MS);
|
|
34582
|
+
onStreamCb = (chunk) => {
|
|
34583
|
+
draftState.accumulated += chunk;
|
|
34584
|
+
draftState.dirty = true;
|
|
34585
|
+
};
|
|
34586
|
+
}
|
|
34587
|
+
const stopDraftTimer2 = () => {
|
|
34588
|
+
if (draftState?.flushTimer) {
|
|
34589
|
+
clearInterval(draftState.flushTimer);
|
|
34590
|
+
draftState.flushTimer = null;
|
|
34591
|
+
}
|
|
34592
|
+
};
|
|
34225
34593
|
let toolUseCount = 0;
|
|
34226
34594
|
const sigT0 = Date.now();
|
|
34227
34595
|
const response = await askAgent(chatId, cleanText || text, {
|
|
@@ -34234,6 +34602,7 @@ You're still in discussion mode \u2014 try again or click a button to exit.`, {
|
|
|
34234
34602
|
agentMode: effectiveAgentMode,
|
|
34235
34603
|
...effectiveThinking ? { thinkingLevel: effectiveThinking } : {},
|
|
34236
34604
|
chatContext: { chatTitle: msg.chatTitle, threadId: msg.threadId },
|
|
34605
|
+
onStream: onStreamCb,
|
|
34237
34606
|
onThinking: liveStatus || sessionLog ? (chunk) => {
|
|
34238
34607
|
if (liveStatus) liveStatus.addThinking(chunk);
|
|
34239
34608
|
if (sessionLog) sessionLog.logThinking(chunk);
|
|
@@ -34271,6 +34640,7 @@ You're still in discussion mode \u2014 try again or click a button to exit.`, {
|
|
|
34271
34640
|
});
|
|
34272
34641
|
}
|
|
34273
34642
|
});
|
|
34643
|
+
stopDraftTimer2();
|
|
34274
34644
|
const elapsedMs = Date.now() - sigT0;
|
|
34275
34645
|
const elapsedSec = (elapsedMs / 1e3).toFixed(1);
|
|
34276
34646
|
if (liveStatus && response.thinkingText?.trim()) {
|
|
@@ -34436,7 +34806,8 @@ Approve paid usage for this session?`,
|
|
|
34436
34806
|
const userMsg = diagnoseAgentError(errMsg, chatId);
|
|
34437
34807
|
await channel.sendText(chatId, userMsg, { parseMode: "plain" });
|
|
34438
34808
|
} finally {
|
|
34439
|
-
|
|
34809
|
+
stopDraftTimer();
|
|
34810
|
+
getTypingManager().release(chatId);
|
|
34440
34811
|
const pending = pendingInterrupts.get(chatId);
|
|
34441
34812
|
if (pending) {
|
|
34442
34813
|
pendingInterrupts.delete(chatId);
|
|
@@ -34471,6 +34842,7 @@ var init_router2 = __esm({
|
|
|
34471
34842
|
init_gate();
|
|
34472
34843
|
init_helpers();
|
|
34473
34844
|
init_response();
|
|
34845
|
+
init_typing_manager();
|
|
34474
34846
|
init_shell();
|
|
34475
34847
|
init_ui();
|
|
34476
34848
|
init_api_models();
|
|
@@ -35765,6 +36137,11 @@ function isFastPathMessage(msg) {
|
|
|
35765
36137
|
function sanitizeForTelegram(text) {
|
|
35766
36138
|
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFFFD\uFFFE\uFFFF]/g, "");
|
|
35767
36139
|
}
|
|
36140
|
+
function isDraftCapable(chatId) {
|
|
36141
|
+
if (isSyntheticChatId(chatId)) return false;
|
|
36142
|
+
const numId = numericChatId(chatId);
|
|
36143
|
+
return !isNaN(numId) && numId > 0;
|
|
36144
|
+
}
|
|
35768
36145
|
function numericChatId(chatId) {
|
|
35769
36146
|
if (chatId.startsWith("sq:") || chatId.startsWith("cron:")) {
|
|
35770
36147
|
throw new Error(`Synthetic chatId "${chatId}" passed to Telegram API`);
|
|
@@ -35780,6 +36157,7 @@ var init_telegram2 = __esm({
|
|
|
35780
36157
|
init_log();
|
|
35781
36158
|
init_health3();
|
|
35782
36159
|
init_store5();
|
|
36160
|
+
init_agent();
|
|
35783
36161
|
init_telegram_throttle();
|
|
35784
36162
|
FAST_PATH_COMMANDS = /* @__PURE__ */ new Set(["stop", "status", "new", "newchat", "clear"]);
|
|
35785
36163
|
TelegramChannel = class _TelegramChannel {
|
|
@@ -35798,18 +36176,23 @@ var init_telegram2 = __esm({
|
|
|
35798
36176
|
mediaGroupBuffer = /* @__PURE__ */ new Map();
|
|
35799
36177
|
static MEDIA_GROUP_DEBOUNCE_MS = 500;
|
|
35800
36178
|
// ── Polling health tracking ─────────────────────────────────────────
|
|
35801
|
-
/** Timestamp of last
|
|
35802
|
-
|
|
36179
|
+
/** Timestamp of last successful API keepalive ping (updated every 2 minutes) */
|
|
36180
|
+
lastPollingCheckAt = 0;
|
|
35803
36181
|
/** True while polling is expected to be active (between start() and stop()) */
|
|
35804
36182
|
pollingExpected = false;
|
|
35805
36183
|
/** Watchdog interval that detects silent polling death */
|
|
35806
36184
|
pollingWatchdog = null;
|
|
35807
|
-
/**
|
|
35808
|
-
|
|
35809
|
-
|
|
36185
|
+
/** Keepalive interval: pings bot.api.getMe() to confirm API connection is alive */
|
|
36186
|
+
keepaliveInterval = null;
|
|
36187
|
+
/** Max time without a successful keepalive ping before we consider polling dead (ms) */
|
|
36188
|
+
static POLLING_SILENCE_THRESHOLD_MS = 10 * 60 * 1e3;
|
|
36189
|
+
// 10 minutes
|
|
35810
36190
|
/** How often the watchdog checks for polling health (ms) */
|
|
35811
36191
|
static POLLING_WATCHDOG_INTERVAL_MS = 60 * 1e3;
|
|
35812
36192
|
// 60 seconds
|
|
36193
|
+
/** How often to ping bot.api.getMe() as a keepalive (ms) */
|
|
36194
|
+
static POLLING_KEEPALIVE_INTERVAL_MS = 2 * 60 * 1e3;
|
|
36195
|
+
// 2 minutes
|
|
35813
36196
|
constructor() {
|
|
35814
36197
|
const token = process.env.TELEGRAM_BOT_TOKEN;
|
|
35815
36198
|
if (!token) {
|
|
@@ -35927,7 +36310,6 @@ var init_telegram2 = __esm({
|
|
|
35927
36310
|
{ command: "council", description: "Multi-model debate (select models, anonymous rounds)" }
|
|
35928
36311
|
]);
|
|
35929
36312
|
this.bot.on("message", async (ctx) => {
|
|
35930
|
-
this.lastUpdateAt = Date.now();
|
|
35931
36313
|
const chatId = ctx.chat.id.toString();
|
|
35932
36314
|
const senderId = ctx.from?.id?.toString() ?? "";
|
|
35933
36315
|
const authorized = this.isAuthorized(chatId) || this.isAuthorized(senderId);
|
|
@@ -35968,7 +36350,6 @@ var init_telegram2 = __esm({
|
|
|
35968
36350
|
});
|
|
35969
36351
|
});
|
|
35970
36352
|
this.bot.on("callback_query:data", (ctx) => {
|
|
35971
|
-
this.lastUpdateAt = Date.now();
|
|
35972
36353
|
const userId = ctx.from.id.toString();
|
|
35973
36354
|
const chatId = ctx.callbackQuery.message?.chat?.id?.toString() ?? userId;
|
|
35974
36355
|
log(`[telegram] Callback from user ${userId} in chat ${chatId}: ${ctx.callbackQuery.data}`);
|
|
@@ -35996,7 +36377,6 @@ var init_telegram2 = __esm({
|
|
|
35996
36377
|
});
|
|
35997
36378
|
});
|
|
35998
36379
|
this.bot.on("message_reaction", async (ctx) => {
|
|
35999
|
-
this.lastUpdateAt = Date.now();
|
|
36000
36380
|
const chatId = String(ctx.chat.id);
|
|
36001
36381
|
const messageId = ctx.messageReaction.message_id;
|
|
36002
36382
|
if (!this.agentMessageIds.has(messageId)) return;
|
|
@@ -36013,7 +36393,6 @@ var init_telegram2 = __esm({
|
|
|
36013
36393
|
}
|
|
36014
36394
|
});
|
|
36015
36395
|
this.bot.on("inline_query", (ctx) => {
|
|
36016
|
-
this.lastUpdateAt = Date.now();
|
|
36017
36396
|
if (!this.isAuthorized(ctx.from.id.toString())) return;
|
|
36018
36397
|
this.handleInlineQuery(ctx).catch((err) => {
|
|
36019
36398
|
error("[telegram] Inline query error:", err);
|
|
@@ -36028,7 +36407,16 @@ var init_telegram2 = __esm({
|
|
|
36028
36407
|
}
|
|
36029
36408
|
});
|
|
36030
36409
|
this.pollingExpected = true;
|
|
36031
|
-
this.
|
|
36410
|
+
this.lastPollingCheckAt = Date.now();
|
|
36411
|
+
this.keepaliveInterval = setInterval(async () => {
|
|
36412
|
+
if (!this.pollingExpected) return;
|
|
36413
|
+
try {
|
|
36414
|
+
await this.bot.api.getMe();
|
|
36415
|
+
this.lastPollingCheckAt = Date.now();
|
|
36416
|
+
} catch (err) {
|
|
36417
|
+
error("[telegram] Keepalive ping failed:", err);
|
|
36418
|
+
}
|
|
36419
|
+
}, _TelegramChannel.POLLING_KEEPALIVE_INTERVAL_MS);
|
|
36032
36420
|
const pollingPromise = this.bot.start({
|
|
36033
36421
|
allowed_updates: [...API_CONSTANTS.ALL_UPDATE_TYPES],
|
|
36034
36422
|
onStart: () => log("[telegram] Polling for messages...")
|
|
@@ -36051,13 +36439,13 @@ var init_telegram2 = __esm({
|
|
|
36051
36439
|
);
|
|
36052
36440
|
this.pollingWatchdog = setInterval(() => {
|
|
36053
36441
|
if (!this.pollingExpected) return;
|
|
36054
|
-
const silenceMs = Date.now() - this.
|
|
36442
|
+
const silenceMs = Date.now() - this.lastPollingCheckAt;
|
|
36055
36443
|
if (silenceMs > _TelegramChannel.POLLING_SILENCE_THRESHOLD_MS) {
|
|
36056
36444
|
log(
|
|
36057
|
-
`[telegram] No
|
|
36445
|
+
`[telegram] No polling confirmation for ${Math.round(silenceMs / 1e3)}s \u2014 triggering reconnect`
|
|
36058
36446
|
);
|
|
36059
|
-
markChannelDown("telegram", `No
|
|
36060
|
-
this.
|
|
36447
|
+
markChannelDown("telegram", `No polling confirmation for ${Math.round(silenceMs / 1e3)}s`);
|
|
36448
|
+
this.lastPollingCheckAt = Date.now();
|
|
36061
36449
|
}
|
|
36062
36450
|
}, _TelegramChannel.POLLING_WATCHDOG_INTERVAL_MS);
|
|
36063
36451
|
}
|
|
@@ -36067,6 +36455,10 @@ var init_telegram2 = __esm({
|
|
|
36067
36455
|
clearInterval(this.pollingWatchdog);
|
|
36068
36456
|
this.pollingWatchdog = null;
|
|
36069
36457
|
}
|
|
36458
|
+
if (this.keepaliveInterval) {
|
|
36459
|
+
clearInterval(this.keepaliveInterval);
|
|
36460
|
+
this.keepaliveInterval = null;
|
|
36461
|
+
}
|
|
36070
36462
|
try {
|
|
36071
36463
|
await this.bot.stop();
|
|
36072
36464
|
} catch {
|
|
@@ -36180,15 +36572,18 @@ var init_telegram2 = __esm({
|
|
|
36180
36572
|
return void 0;
|
|
36181
36573
|
}
|
|
36182
36574
|
}
|
|
36183
|
-
async editText(chatId, messageId, text, parseMode) {
|
|
36575
|
+
async editText(chatId, messageId, text, parseMode, priority) {
|
|
36184
36576
|
const formatted = sanitizeForTelegram(parseMode === "html" ? text : formatForTelegram(text));
|
|
36577
|
+
const isCritical = priority === true || priority === 0 /* P0_CRITICAL */;
|
|
36578
|
+
const label2 = isCritical ? "finalizeStatus" : "editText:html";
|
|
36185
36579
|
try {
|
|
36186
36580
|
await this.throttle.send(
|
|
36187
36581
|
chatId,
|
|
36188
|
-
|
|
36582
|
+
label2,
|
|
36189
36583
|
() => this.bot.api.editMessageText(numericChatId(chatId), parseInt(messageId), formatted, {
|
|
36190
36584
|
parse_mode: "HTML"
|
|
36191
|
-
})
|
|
36585
|
+
}),
|
|
36586
|
+
priority
|
|
36192
36587
|
);
|
|
36193
36588
|
return true;
|
|
36194
36589
|
} catch (err) {
|
|
@@ -36201,12 +36596,13 @@ var init_telegram2 = __esm({
|
|
|
36201
36596
|
try {
|
|
36202
36597
|
await this.throttle.send(
|
|
36203
36598
|
chatId,
|
|
36204
|
-
"editText:fallback",
|
|
36599
|
+
priority ? "finalizeStatus:fallback" : "editText:fallback",
|
|
36205
36600
|
() => this.bot.api.editMessageText(
|
|
36206
36601
|
numericChatId(chatId),
|
|
36207
36602
|
parseInt(messageId),
|
|
36208
36603
|
formatted.replace(/<[^>]+>/g, "")
|
|
36209
|
-
)
|
|
36604
|
+
),
|
|
36605
|
+
priority
|
|
36210
36606
|
);
|
|
36211
36607
|
return true;
|
|
36212
36608
|
} catch (err2) {
|
|
@@ -36352,6 +36748,44 @@ var init_telegram2 = __esm({
|
|
|
36352
36748
|
log(`[telegram] reactToMessage failed (chat=${chatId} msg=${messageId}): ${err}`);
|
|
36353
36749
|
}
|
|
36354
36750
|
}
|
|
36751
|
+
/**
|
|
36752
|
+
* Check whether a chat supports native draft streaming.
|
|
36753
|
+
* sendMessageDraft only works in private/DM chats (positive chat IDs).
|
|
36754
|
+
*/
|
|
36755
|
+
isDraftCapable(chatId) {
|
|
36756
|
+
return isDraftCapable(chatId);
|
|
36757
|
+
}
|
|
36758
|
+
/**
|
|
36759
|
+
* Send a streaming draft update to a DM chat using Telegram's native
|
|
36760
|
+
* sendMessageDraft API (Bot API 9.3+). The draft shows an animated
|
|
36761
|
+
* typing bubble with the text content, replacing editMessageText for
|
|
36762
|
+
* DM streaming. Subsequent calls with the same draft_id produce smooth
|
|
36763
|
+
* animated transitions.
|
|
36764
|
+
*
|
|
36765
|
+
* Draft updates are cosmetic — if one gets dropped, the next update
|
|
36766
|
+
* will contain the full accumulated text. Uses tryBestEffort so drafts
|
|
36767
|
+
* never block critical sends.
|
|
36768
|
+
*
|
|
36769
|
+
* @param chatId - Private chat ID (must be positive / draft-capable)
|
|
36770
|
+
* @param draftId - Non-zero integer, consistent per response for smooth animation
|
|
36771
|
+
* @param text - Plain text content (no parse_mode during streaming)
|
|
36772
|
+
*/
|
|
36773
|
+
async sendMessageDraft(chatId, draftId, text) {
|
|
36774
|
+
if (!this.isDraftCapable(chatId)) {
|
|
36775
|
+
log(`[telegram] sendMessageDraft skipped \u2014 chat ${chatId} is not draft-capable`);
|
|
36776
|
+
return;
|
|
36777
|
+
}
|
|
36778
|
+
try {
|
|
36779
|
+
await this.throttle.tryBestEffort(
|
|
36780
|
+
chatId,
|
|
36781
|
+
"draft",
|
|
36782
|
+
() => this.bot.api.sendMessageDraft(numericChatId(chatId), draftId, sanitizeForTelegram(text))
|
|
36783
|
+
);
|
|
36784
|
+
log(`[telegram] sendMessageDraft sent (chat=${chatId}, draftId=${draftId}, len=${text.length})`);
|
|
36785
|
+
} catch (err) {
|
|
36786
|
+
log(`[telegram] sendMessageDraft failed (chat=${chatId}): ${err}`);
|
|
36787
|
+
}
|
|
36788
|
+
}
|
|
36355
36789
|
/** Get the underlying Grammy Bot instance (for scheduler, etc.) */
|
|
36356
36790
|
getBot() {
|
|
36357
36791
|
return this.bot;
|
|
@@ -37571,6 +38005,16 @@ ${lines.join("\n")}`;
|
|
|
37571
38005
|
} catch {
|
|
37572
38006
|
}
|
|
37573
38007
|
;
|
|
38008
|
+
try {
|
|
38009
|
+
const { getEditCoordinator: getEditCoordinator2 } = await Promise.resolve().then(() => (init_edit_coordinator(), edit_coordinator_exports));
|
|
38010
|
+
getEditCoordinator2().shutdown();
|
|
38011
|
+
} catch {
|
|
38012
|
+
}
|
|
38013
|
+
try {
|
|
38014
|
+
const { getTypingManager: getTypingManager2 } = await Promise.resolve().then(() => (init_typing_manager(), typing_manager_exports));
|
|
38015
|
+
getTypingManager2().shutdown();
|
|
38016
|
+
} catch {
|
|
38017
|
+
}
|
|
37574
38018
|
shutdownOrchestrator();
|
|
37575
38019
|
shutdownScheduler();
|
|
37576
38020
|
flushMemoryHalfLifeUpdates();
|
|
@@ -38130,6 +38574,7 @@ async function statusCommand(globalOpts, localOpts) {
|
|
|
38130
38574
|
const dbStat = existsSync35(DB_PATH) ? statSync10(DB_PATH) : null;
|
|
38131
38575
|
let daemonRunning = false;
|
|
38132
38576
|
let daemonInfo = {};
|
|
38577
|
+
let throttleData;
|
|
38133
38578
|
try {
|
|
38134
38579
|
const { apiGet: apiGet2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
|
|
38135
38580
|
const healthRes = await apiGet2("/api/health");
|
|
@@ -38137,6 +38582,9 @@ async function statusCommand(globalOpts, localOpts) {
|
|
|
38137
38582
|
if (healthRes.ok && healthRes.data?.uptime) {
|
|
38138
38583
|
daemonInfo.uptime_seconds = Math.floor(healthRes.data.uptime);
|
|
38139
38584
|
}
|
|
38585
|
+
if (healthRes.ok && healthRes.data?.throttle) {
|
|
38586
|
+
throttleData = healthRes.data.throttle;
|
|
38587
|
+
}
|
|
38140
38588
|
} catch {
|
|
38141
38589
|
}
|
|
38142
38590
|
const contextUsed = (usageRow?.last_input_tokens ?? 0) + (usageRow?.last_cache_read_tokens ?? 0);
|
|
@@ -38158,7 +38606,8 @@ async function statusCommand(globalOpts, localOpts) {
|
|
|
38158
38606
|
output_tokens: usageRow?.output_tokens ?? 0,
|
|
38159
38607
|
cache_read_tokens: usageRow?.cache_read_tokens ?? 0
|
|
38160
38608
|
},
|
|
38161
|
-
db: { path: DB_PATH, sizeBytes: dbStat?.size ?? 0, exists: !!dbStat }
|
|
38609
|
+
db: { path: DB_PATH, sizeBytes: dbStat?.size ?? 0, exists: !!dbStat },
|
|
38610
|
+
throttle: throttleData
|
|
38162
38611
|
};
|
|
38163
38612
|
try {
|
|
38164
38613
|
const { OllamaStore } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
|
|
@@ -38190,6 +38639,14 @@ async function statusCommand(globalOpts, localOpts) {
|
|
|
38190
38639
|
if (localOpts.deep) {
|
|
38191
38640
|
lines.push(kvLine("Daemon", s.daemon.running ? success(`running${s.daemon.uptime_seconds ? ` (uptime ${formatUptime2(s.daemon.uptime_seconds)})` : ""}`) : error2("offline")));
|
|
38192
38641
|
}
|
|
38642
|
+
if (s.throttle) {
|
|
38643
|
+
const t = s.throttle;
|
|
38644
|
+
const queueStr = t.queueDepth > 0 ? warning(`${t.queueDepth} queued`) : success("0 queued");
|
|
38645
|
+
const pauseStr = t.isPaused ? error2(`PAUSED (${t.pauseRemainingSec}s remaining)`) : "";
|
|
38646
|
+
const circuitStr = t.circuitState !== "closed" ? warning(t.circuitState.toUpperCase()) : "";
|
|
38647
|
+
const parts = [queueStr, pauseStr, circuitStr].filter(Boolean).join(", ");
|
|
38648
|
+
lines.push(kvLine("Throttle", parts));
|
|
38649
|
+
}
|
|
38193
38650
|
lines.push(
|
|
38194
38651
|
"",
|
|
38195
38652
|
divider("Usage (this session)"),
|
|
@@ -38585,9 +39042,9 @@ async function sessionLogsList(opts) {
|
|
|
38585
39042
|
`));
|
|
38586
39043
|
console.log(` ${"Filename".padEnd(55)} ${"Size".padStart(8)} Chat ID`);
|
|
38587
39044
|
console.log(` ${"\u2500".repeat(55)} ${"\u2500".repeat(8)} ${"\u2500".repeat(15)}`);
|
|
38588
|
-
for (const
|
|
38589
|
-
const size =
|
|
38590
|
-
console.log(` ${
|
|
39045
|
+
for (const log5 of logs) {
|
|
39046
|
+
const size = log5.sizeBytes < 1024 ? `${log5.sizeBytes}B` : log5.sizeBytes < 1024 * 1024 ? `${(log5.sizeBytes / 1024).toFixed(1)}K` : `${(log5.sizeBytes / 1024 / 1024).toFixed(1)}M`;
|
|
39047
|
+
console.log(` ${log5.filename.padEnd(55)} ${size.padStart(8)} ${log5.chatId}`);
|
|
38591
39048
|
}
|
|
38592
39049
|
console.log(muted(`
|
|
38593
39050
|
Path: ${SESSION_LOGS_PATH}`));
|