cc-claw 0.20.14 → 0.20.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +502 -192
  2. 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.20.14" : (() => {
36
+ VERSION = true ? "0.20.15" : (() => {
37
37
  try {
38
38
  return JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
39
39
  } catch {
@@ -1333,7 +1333,8 @@ function initSchema(db3) {
1333
1333
  last_run_at TEXT,
1334
1334
  next_run_at TEXT,
1335
1335
  consecutive_failures INTEGER NOT NULL DEFAULT 0,
1336
- allow_paid_slots INTEGER NOT NULL DEFAULT 0
1336
+ allow_paid_slots INTEGER NOT NULL DEFAULT 0,
1337
+ credential_slot_id INTEGER
1337
1338
  );
1338
1339
  `);
1339
1340
  try {
@@ -1493,6 +1494,10 @@ function initSchema(db3) {
1493
1494
  db3.exec("ALTER TABLE jobs ADD COLUMN allow_paid_slots INTEGER NOT NULL DEFAULT 0");
1494
1495
  } catch {
1495
1496
  }
1497
+ try {
1498
+ db3.exec("ALTER TABLE jobs ADD COLUMN credential_slot_id INTEGER");
1499
+ } catch {
1500
+ }
1496
1501
  try {
1497
1502
  db3.exec("UPDATE jobs SET job_type = 'reflection' WHERE description LIKE '%reflection analysis%' AND job_type = 'normal'");
1498
1503
  } catch {
@@ -3317,8 +3322,8 @@ function insertJob(params) {
3317
3322
  const db3 = getDb();
3318
3323
  const result = db3.prepare(`
3319
3324
  INSERT INTO jobs (schedule_type, cron, at_time, every_ms, title, description, chat_id,
3320
- backend, model, thinking, timeout, fallbacks, session_type, channel, target, delivery_mode, timezone, job_type, allow_paid_slots)
3321
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3325
+ backend, model, thinking, timeout, fallbacks, session_type, channel, target, delivery_mode, timezone, job_type, allow_paid_slots, credential_slot_id)
3326
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3322
3327
  `).run(
3323
3328
  params.scheduleType,
3324
3329
  params.cron ?? null,
@@ -3338,7 +3343,8 @@ function insertJob(params) {
3338
3343
  params.deliveryMode ?? "announce",
3339
3344
  params.timezone ?? "UTC",
3340
3345
  params.jobType ?? "normal",
3341
- params.allowPaidSlots ? 1 : 0
3346
+ params.allowPaidSlots ? 1 : 0,
3347
+ params.credentialSlotId ?? null
3342
3348
  );
3343
3349
  return getJobById(Number(result.lastInsertRowid));
3344
3350
  }
@@ -3346,6 +3352,7 @@ function mapJobRow(row) {
3346
3352
  if (!row) return void 0;
3347
3353
  row.fallbacks = row.fallbacks ? JSON.parse(row.fallbacks) : [];
3348
3354
  row.allowPaidSlots = !!row.allowPaidSlots;
3355
+ row.credentialSlotId = row.credentialSlotId ?? null;
3349
3356
  return row;
3350
3357
  }
3351
3358
  function getJobById(id) {
@@ -3400,7 +3407,8 @@ function updateJob(id, fields) {
3400
3407
  deliveryMode: "delivery_mode",
3401
3408
  timezone: "timezone",
3402
3409
  jobType: "job_type",
3403
- allowPaidSlots: "allow_paid_slots"
3410
+ allowPaidSlots: "allow_paid_slots",
3411
+ credentialSlotId: "credential_slot_id"
3404
3412
  };
3405
3413
  for (const [key, val] of Object.entries(fields)) {
3406
3414
  const col = fieldMap[key];
@@ -3473,7 +3481,7 @@ var init_jobs = __esm({
3473
3481
  session_type as sessionType, channel, target, delivery_mode as deliveryMode,
3474
3482
  timezone, job_type as jobType, enabled, active, created_at as createdAt, last_run_at as lastRunAt,
3475
3483
  next_run_at as nextRunAt, consecutive_failures as consecutiveFailures,
3476
- allow_paid_slots as allowPaidSlots
3484
+ allow_paid_slots as allowPaidSlots, credential_slot_id as credentialSlotId
3477
3485
  FROM jobs
3478
3486
  `;
3479
3487
  }
@@ -4141,6 +4149,24 @@ var init_store5 = __esm({
4141
4149
  }
4142
4150
  });
4143
4151
 
4152
+ // src/backends/types.ts
4153
+ function isBackendId(value) {
4154
+ return Object.values(BACKEND).includes(value);
4155
+ }
4156
+ var BACKEND;
4157
+ var init_types = __esm({
4158
+ "src/backends/types.ts"() {
4159
+ "use strict";
4160
+ BACKEND = {
4161
+ CLAUDE: "claude",
4162
+ GEMINI: "gemini",
4163
+ CODEX: "codex",
4164
+ CURSOR: "cursor",
4165
+ OLLAMA: "ollama"
4166
+ };
4167
+ }
4168
+ });
4169
+
4144
4170
  // src/env.ts
4145
4171
  import { homedir as homedir2 } from "os";
4146
4172
  function stripProxyVars(env) {
@@ -4223,6 +4249,11 @@ var init_strip_thinking = __esm({
4223
4249
  });
4224
4250
 
4225
4251
  // src/backends/resolve-executable.ts
4252
+ var resolve_executable_exports = {};
4253
+ __export(resolve_executable_exports, {
4254
+ clearExecutableCache: () => clearExecutableCache,
4255
+ resolveExecutable: () => resolveExecutable
4256
+ });
4226
4257
  import { existsSync as existsSync2 } from "fs";
4227
4258
  import { spawnSync } from "child_process";
4228
4259
  function resolveExecutable(config2) {
@@ -4259,6 +4290,9 @@ function resolveExecutable(config2) {
4259
4290
  cache.set(config2.binaryName, fallback);
4260
4291
  return fallback;
4261
4292
  }
4293
+ function clearExecutableCache() {
4294
+ cache.clear();
4295
+ }
4262
4296
  var cache;
4263
4297
  var init_resolve_executable = __esm({
4264
4298
  "src/backends/resolve-executable.ts"() {
@@ -6494,24 +6528,6 @@ var init_ollama2 = __esm({
6494
6528
  }
6495
6529
  });
6496
6530
 
6497
- // src/backends/types.ts
6498
- function isBackendId(value) {
6499
- return Object.values(BACKEND).includes(value);
6500
- }
6501
- var BACKEND;
6502
- var init_types = __esm({
6503
- "src/backends/types.ts"() {
6504
- "use strict";
6505
- BACKEND = {
6506
- CLAUDE: "claude",
6507
- GEMINI: "gemini",
6508
- CODEX: "codex",
6509
- CURSOR: "cursor",
6510
- OLLAMA: "ollama"
6511
- };
6512
- }
6513
- });
6514
-
6515
6531
  // src/backends/index.ts
6516
6532
  var backends_exports = {};
6517
6533
  __export(backends_exports, {
@@ -14978,8 +14994,8 @@ var init_telegram_throttle = __esm({
14978
14994
  "src/channels/telegram-throttle.ts"() {
14979
14995
  "use strict";
14980
14996
  init_log();
14981
- PER_CHAT_INTERVAL_MS = 350;
14982
- GLOBAL_INTERVAL_MS = 50;
14997
+ PER_CHAT_INTERVAL_MS = 1e3;
14998
+ GLOBAL_INTERVAL_MS = 100;
14983
14999
  MAX_RETRIES2 = 2;
14984
15000
  RETRY_DELAY_MS = 1e3;
14985
15001
  MAX_QUEUE_SIZE = 100;
@@ -15007,6 +15023,9 @@ var init_telegram_throttle = __esm({
15007
15023
  }
15008
15024
  /** Enqueue a Telegram API call with automatic pacing and 429 handling. */
15009
15025
  async send(chatId, label2, fn) {
15026
+ if (this.isPaused() && label2.startsWith("editText")) {
15027
+ throw new Error("Throttle paused (rate limit active) \u2014 edit skipped");
15028
+ }
15010
15029
  return new Promise((resolve, reject) => {
15011
15030
  if (this.queue.length >= MAX_QUEUE_SIZE) {
15012
15031
  const dropped = this.queue.shift();
@@ -15019,6 +15038,31 @@ var init_telegram_throttle = __esm({
15019
15038
  this.drain();
15020
15039
  });
15021
15040
  }
15041
+ /**
15042
+ * Best-effort send — drops silently if throttle is paused or queue is pressured.
15043
+ * Used for cosmetic calls (typing indicators, reactions) that should count toward
15044
+ * rate limits but must never queue up or amplify 429 spirals.
15045
+ */
15046
+ async tryBestEffort(chatId, label2, fn) {
15047
+ if (this.isPaused()) return void 0;
15048
+ if (this.queue.length > 10) return void 0;
15049
+ const lastChat = this.lastSendPerChat.get(chatId) ?? 0;
15050
+ if (Date.now() - lastChat < PER_CHAT_INTERVAL_MS) return void 0;
15051
+ if (Date.now() - this.lastGlobalSend < GLOBAL_INTERVAL_MS) return void 0;
15052
+ try {
15053
+ const result = await fn();
15054
+ this.recordSend(chatId);
15055
+ return result;
15056
+ } catch (err) {
15057
+ if (is429(err)) {
15058
+ const retrySec = err.parameters?.retry_after ?? 10;
15059
+ this.pausedUntil = Date.now() + retrySec * 1e3;
15060
+ if (this.pauseStartedAt === 0) this.pauseStartedAt = Date.now();
15061
+ warn(`[throttle] Best-effort ${label2} hit 429, pausing for ${retrySec}s`);
15062
+ }
15063
+ return void 0;
15064
+ }
15065
+ }
15022
15066
  /** Check whether the throttle is currently paused (rate-limited). */
15023
15067
  isPaused() {
15024
15068
  return Date.now() < this.pausedUntil;
@@ -15101,12 +15145,13 @@ var init_telegram_throttle = __esm({
15101
15145
  // ── Pause management ────────────────────────────────────────────────
15102
15146
  enterPause(retrySec, failedItem) {
15103
15147
  this.queue.unshift(failedItem);
15104
- this.pausedUntil = Date.now() + retrySec * 1e3;
15148
+ const bufferedSec = Math.ceil(retrySec * 1.5);
15149
+ this.pausedUntil = Date.now() + bufferedSec * 1e3;
15105
15150
  if (this.pauseStartedAt === 0) this.pauseStartedAt = Date.now();
15106
15151
  for (const qi of this.queue) {
15107
15152
  this.chatsPendingNotification.add(qi.chatId);
15108
15153
  }
15109
- warn(`[throttle] 429 \u2014 pausing ALL sends for ${retrySec}s (${this.queue.length} items queued)`);
15154
+ warn(`[throttle] 429 \u2014 pausing ALL sends for ${bufferedSec}s (retry_after=${retrySec}s + 50% buffer, ${this.queue.length} items queued)`);
15110
15155
  }
15111
15156
  async sendResumeNotifications() {
15112
15157
  const chats2 = new Set(this.chatsPendingNotification);
@@ -15739,6 +15784,7 @@ var init_profile = __esm({
15739
15784
  var classify_exports = {};
15740
15785
  __export(classify_exports, {
15741
15786
  classifyIntent: () => classifyIntent,
15787
+ classifyIntentAsync: () => classifyIntentAsync,
15742
15788
  getIntentStats: () => getIntentStats,
15743
15789
  resetIntentStats: () => resetIntentStats
15744
15790
  });
@@ -15749,12 +15795,17 @@ function resetIntentStats() {
15749
15795
  intentCounts.chat = 0;
15750
15796
  intentCounts.agentic = 0;
15751
15797
  }
15752
- function classifyIntent(text, chatId) {
15798
+ function classifyIntentFast(text, chatId) {
15753
15799
  const trimmed = text.trim();
15754
15800
  if (trimmed.startsWith(">>")) return "agentic";
15755
15801
  if (trimmed.startsWith("/")) return "agentic";
15756
15802
  const lower = trimmed.toLowerCase();
15757
15803
  const normalized = trimmed.replace(/^["'\u201C\u201D\u2018\u2019`\s]+|["'\u201C\u201D\u2018\u2019`\s]+$/g, "");
15804
+ const lowerNoPunct = lower.replace(/[?!.,…]+$/, "").trim();
15805
+ if (CHAT_EXACT.has(lowerNoPunct) || CHAT_EXACT.has(lower)) {
15806
+ log(`[intent] "${lower}" -> chat (exact match)`);
15807
+ return "chat";
15808
+ }
15758
15809
  const sessionId = getSessionId(chatId);
15759
15810
  if (sessionId) {
15760
15811
  const lastTs = getLastMessageTimestamp(chatId);
@@ -15762,47 +15813,148 @@ function classifyIntent(text, chatId) {
15762
15813
  const elapsed = Date.now() - lastTs;
15763
15814
  if (elapsed < 12e4 && trimmed.length < 30) {
15764
15815
  log(`[intent] "${trimmed.slice(0, 30)}" -> agentic (active session, ${(elapsed / 1e3).toFixed(0)}s ago)`);
15765
- intentCounts.agentic++;
15766
15816
  return "agentic";
15767
15817
  }
15768
15818
  }
15769
15819
  }
15770
- if (CHAT_EXACT.has(lower)) {
15771
- log(`[intent] "${lower}" -> chat (exact match)`);
15772
- intentCounts.chat++;
15773
- return "chat";
15774
- }
15775
15820
  if (trimmed.length <= 4 && /^[\p{Emoji}\s]+$/u.test(trimmed)) {
15776
15821
  log(`[intent] "${trimmed}" -> chat (emoji-only)`);
15777
- intentCounts.chat++;
15778
15822
  return "chat";
15779
15823
  }
15780
15824
  for (const pattern of STRUCTURAL_PATTERNS) {
15781
15825
  if (pattern.test(normalized)) {
15782
15826
  log(`[intent] "${trimmed.slice(0, 40)}..." -> agentic (structural: ${pattern})`);
15783
- intentCounts.agentic++;
15784
15827
  return "agentic";
15785
15828
  }
15786
15829
  }
15787
15830
  for (const pattern of MUTATION_PATTERNS) {
15788
15831
  if (pattern.test(normalized)) {
15789
15832
  log(`[intent] "${trimmed.slice(0, 40)}..." -> agentic (mutation: ${pattern})`);
15790
- intentCounts.agentic++;
15791
15833
  return "agentic";
15792
15834
  }
15793
15835
  }
15794
15836
  for (const pattern of CHAT_QUESTION_PATTERNS) {
15795
15837
  if (pattern.test(normalized)) {
15796
15838
  log(`[intent] "${trimmed.slice(0, 40)}..." -> chat (question: ${pattern})`);
15797
- intentCounts.chat++;
15798
15839
  return "chat";
15799
15840
  }
15800
15841
  }
15801
- log(`[intent] "${trimmed.slice(0, 40)}..." -> agentic (default)`);
15842
+ return null;
15843
+ }
15844
+ async function classifyWithLlm(text) {
15845
+ try {
15846
+ const ollamaResult = await classifyWithOllama(text);
15847
+ if (ollamaResult) return ollamaResult;
15848
+ } catch {
15849
+ }
15850
+ try {
15851
+ const cliResult = await classifyWithSummarizerCli(text);
15852
+ if (cliResult) return cliResult;
15853
+ } catch {
15854
+ }
15855
+ return null;
15856
+ }
15857
+ async function classifyWithOllama(text) {
15858
+ const ollamaService = await Promise.resolve().then(() => (init_service(), service_exports));
15859
+ const ollamaClient = await Promise.resolve().then(() => (init_client(), client_exports));
15860
+ const servers = ollamaService.listServers();
15861
+ const onlineServer = servers.find((s) => s.status === "online");
15862
+ if (!onlineServer) return null;
15863
+ const models = ollamaService.listModels(onlineServer.name);
15864
+ if (models.length === 0) return null;
15865
+ const sorted = [...models].sort(
15866
+ (a, b) => (a.sizeBytes ?? Infinity) - (b.sizeBytes ?? Infinity)
15867
+ );
15868
+ const model2 = sorted[0].name;
15869
+ const result = await ollamaClient.chat(
15870
+ onlineServer.baseUrl,
15871
+ model2,
15872
+ [{ role: "user", content: LLM_CLASSIFY_PROMPT + text.slice(0, 500) }],
15873
+ { timeoutMs: LLM_CLASSIFY_TIMEOUT_MS, maxTokens: 5, temperature: 0 }
15874
+ );
15875
+ return parseClassifyResponse(result.text);
15876
+ }
15877
+ async function classifyWithSummarizerCli(text) {
15878
+ const { getSummarizer: getSummarizer3 } = await Promise.resolve().then(() => (init_chat_settings(), chat_settings_exports));
15879
+ const { getAdapter: getAdapter4, getAllAdapters: getAllAdapters5 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
15880
+ const config2 = getSummarizer3("__global__");
15881
+ let backendId = config2.backend;
15882
+ let modelName = config2.model;
15883
+ if (!backendId) {
15884
+ const adapters2 = getAllAdapters5();
15885
+ const cheapAdapter = adapters2.find((a) => a.summarizerModel);
15886
+ if (!cheapAdapter) return null;
15887
+ backendId = cheapAdapter.id;
15888
+ modelName = cheapAdapter.summarizerModel;
15889
+ }
15890
+ const adapter = getAdapter4(backendId);
15891
+ if (!adapter) return null;
15892
+ const model2 = modelName ?? adapter.summarizerModel;
15893
+ const { spawn: spawn8 } = await import("child_process");
15894
+ const { resolveExecutable: resolveExecutable4 } = await Promise.resolve().then(() => (init_resolve_executable(), resolve_executable_exports));
15895
+ const exe = resolveExecutable4(adapter.id);
15896
+ if (!exe) return null;
15897
+ return new Promise((resolve) => {
15898
+ const timeout = setTimeout(() => {
15899
+ proc.kill("SIGKILL");
15900
+ resolve(null);
15901
+ }, LLM_CLASSIFY_TIMEOUT_MS);
15902
+ const args = adapter.id === "claude" ? ["-p", LLM_CLASSIFY_PROMPT + text.slice(0, 500), "--model", model2, "--no-input"] : ["-p", LLM_CLASSIFY_PROMPT + text.slice(0, 500), "--model", model2];
15903
+ const proc = spawn8(exe, args, {
15904
+ stdio: ["ignore", "pipe", "ignore"],
15905
+ timeout: LLM_CLASSIFY_TIMEOUT_MS + 1e3
15906
+ });
15907
+ let output2 = "";
15908
+ proc.stdout?.on("data", (chunk) => {
15909
+ output2 += chunk.toString();
15910
+ });
15911
+ proc.on("close", () => {
15912
+ clearTimeout(timeout);
15913
+ resolve(parseClassifyResponse(output2));
15914
+ });
15915
+ proc.on("error", () => {
15916
+ clearTimeout(timeout);
15917
+ resolve(null);
15918
+ });
15919
+ });
15920
+ }
15921
+ function parseClassifyResponse(text) {
15922
+ const lower = text.trim().toLowerCase();
15923
+ if (lower.includes("chat")) return "chat";
15924
+ if (lower.includes("task")) return "agentic";
15925
+ return null;
15926
+ }
15927
+ function classifyIntent(text, chatId) {
15928
+ const fast = classifyIntentFast(text, chatId);
15929
+ if (fast) {
15930
+ intentCounts[fast]++;
15931
+ return fast;
15932
+ }
15933
+ log(`[intent] "${text.slice(0, 40)}..." -> agentic (default)`);
15934
+ intentCounts.agentic++;
15935
+ return "agentic";
15936
+ }
15937
+ async function classifyIntentAsync(text, chatId) {
15938
+ const fast = classifyIntentFast(text, chatId);
15939
+ if (fast) {
15940
+ intentCounts[fast]++;
15941
+ return fast;
15942
+ }
15943
+ try {
15944
+ const llmResult = await classifyWithLlm(text);
15945
+ if (llmResult) {
15946
+ log(`[intent] "${text.slice(0, 40)}..." -> ${llmResult} (LLM)`);
15947
+ intentCounts[llmResult]++;
15948
+ return llmResult;
15949
+ }
15950
+ } catch (err) {
15951
+ warn(`[intent] LLM classification failed: ${err instanceof Error ? err.message : err}`);
15952
+ }
15953
+ log(`[intent] "${text.slice(0, 40)}..." -> agentic (default, LLM unavailable)`);
15802
15954
  intentCounts.agentic++;
15803
15955
  return "agentic";
15804
15956
  }
15805
- var intentCounts, CHAT_EXACT, MUTATION_PATTERNS, CHAT_QUESTION_PATTERNS, STRUCTURAL_PATTERNS;
15957
+ var intentCounts, CHAT_EXACT, MUTATION_PATTERNS, CHAT_QUESTION_PATTERNS, STRUCTURAL_PATTERNS, LLM_CLASSIFY_PROMPT, LLM_CLASSIFY_TIMEOUT_MS;
15806
15958
  var init_classify = __esm({
15807
15959
  "src/intent/classify.ts"() {
15808
15960
  "use strict";
@@ -15898,6 +16050,13 @@ var init_classify = __esm({
15898
16050
  /\b(error|bug|crash|fail|broken|issue|problem|exception|stack\s?trace)\b/i,
15899
16051
  /\b(function|class|const|let|var|import|export|return|async|await)\b/i
15900
16052
  ];
16053
+ LLM_CLASSIFY_PROMPT = `Classify this message as either "chat" or "task".
16054
+ - "chat" = greeting, small talk, acknowledgment, question, opinion, or anything conversational
16055
+ - "task" = request to DO something (research, draft, analyze, code, create, fix, etc.)
16056
+ Reply with ONLY the word "chat" or "task", nothing else.
16057
+
16058
+ Message: `;
16059
+ LLM_CLASSIFY_TIMEOUT_MS = 3e3;
15901
16060
  }
15902
16061
  });
15903
16062
 
@@ -16124,8 +16283,8 @@ async function handleWizardText(chatId, text, channel) {
16124
16283
  case "thinking": {
16125
16284
  const level = text.trim().toLowerCase();
16126
16285
  pending.thinking = level || "auto";
16127
- pending.step = "timeout";
16128
- await promptTimeout(chatId, channel);
16286
+ pending.step = "account";
16287
+ await promptAccount(chatId, channel);
16129
16288
  break;
16130
16289
  }
16131
16290
  case "timeout": {
@@ -16218,6 +16377,17 @@ async function handleWizardCallback(chatId, data, channel) {
16218
16377
  await promptThinking(chatId, channel);
16219
16378
  } else if (data.startsWith("sched:thinking:")) {
16220
16379
  pending.thinking = data.slice(15);
16380
+ pending.step = "account";
16381
+ await promptAccount(chatId, channel);
16382
+ } else if (data.startsWith("sched:slot:")) {
16383
+ const val = data.slice(11);
16384
+ if (val === "auto") {
16385
+ pending.credentialSlotId = null;
16386
+ } else {
16387
+ pending.credentialSlotId = parseInt(val, 10);
16388
+ }
16389
+ const slotLabel = resolveSlotLabel(pending.backend, pending.credentialSlotId);
16390
+ await channel.sendText(chatId, `Account: ${slotLabel}`, { parseMode: "plain" });
16221
16391
  pending.step = "timeout";
16222
16392
  await promptTimeout(chatId, channel);
16223
16393
  } else if (data.startsWith("sched:timeout:")) {
@@ -16323,9 +16493,52 @@ async function promptThinking(chatId, channel) {
16323
16493
  await channel.sendKeyboard(chatId, "Thinking/effort level for this job?", buttons);
16324
16494
  } else {
16325
16495
  pending.thinking = "auto";
16496
+ pending.step = "account";
16497
+ await promptAccount(chatId, channel);
16498
+ }
16499
+ }
16500
+ async function promptAccount(chatId, channel) {
16501
+ const pending = pendingJobs.get(chatId);
16502
+ if (!pending?.backend) {
16503
+ if (pending) pending.step = "timeout";
16504
+ await promptTimeout(chatId, channel);
16505
+ return;
16506
+ }
16507
+ const isGemini = pending.backend === BACKEND.GEMINI;
16508
+ const slots = isGemini ? getGeminiSlots() : getBackendSlots(pending.backend);
16509
+ const enabledSlots = slots.filter((s) => s.enabled);
16510
+ if (enabledSlots.length === 0) {
16511
+ pending.credentialSlotId = null;
16326
16512
  pending.step = "timeout";
16327
16513
  await promptTimeout(chatId, channel);
16514
+ return;
16515
+ }
16516
+ if (typeof channel.sendKeyboard !== "function") {
16517
+ await channel.sendText(chatId, `Enter account slot ID, or "auto" for rotation:`, { parseMode: "plain" });
16518
+ return;
16328
16519
  }
16520
+ const buttons = [
16521
+ [{ label: "\u{1F504} Auto (rotate)", data: "sched:slot:auto" }]
16522
+ ];
16523
+ for (const slot of enabledSlots) {
16524
+ const s = slot;
16525
+ const icon = s.slotType === "api_key" ? "\u{1F511}" : "\u{1F4E7}";
16526
+ const label2 = s.label || s.email || `Slot #${s.id}`;
16527
+ const type = s.slotType === "api_key" ? "API key" : "OAuth";
16528
+ buttons.push([{ label: `${icon} ${label2} (${type})`, data: `sched:slot:${s.id}` }]);
16529
+ }
16530
+ await channel.sendKeyboard(chatId, "Which account should this job use?", buttons);
16531
+ }
16532
+ function resolveSlotLabel(backend2, slotId) {
16533
+ if (!slotId || !backend2) return "Auto (rotate)";
16534
+ const isGemini = backend2 === BACKEND.GEMINI;
16535
+ const slots = isGemini ? getGeminiSlots() : getBackendSlots(backend2);
16536
+ const slot = slots.find((s) => s.id === slotId);
16537
+ if (!slot) return `Slot #${slotId} (unknown)`;
16538
+ const icon = slot.slotType === "api_key" ? "\u{1F511}" : "\u{1F4E7}";
16539
+ const label2 = slot.label || slot.email || `Slot #${slot.id}`;
16540
+ const type = slot.slotType === "api_key" ? "API key" : "OAuth";
16541
+ return `${icon} ${label2} (${type})`;
16329
16542
  }
16330
16543
  async function promptTimeout(chatId, channel) {
16331
16544
  if (typeof channel.sendKeyboard !== "function") {
@@ -16380,6 +16593,7 @@ async function promptConfirm(chatId, channel) {
16380
16593
  if (!pending) return;
16381
16594
  const backendName = pending.backend ? getAdapter(pending.backend).displayName : "default";
16382
16595
  const timeoutLabel = pending.timeout ? `${pending.timeout}s (${Math.round(pending.timeout / 60)} min)` : `Default (${TIMEOUT_DEFAULT_SECONDS}s)`;
16596
+ const accountLabel = resolveSlotLabel(pending.backend, pending.credentialSlotId);
16383
16597
  const lines = [
16384
16598
  "Job configuration:",
16385
16599
  "",
@@ -16388,6 +16602,7 @@ async function promptConfirm(chatId, channel) {
16388
16602
  ` Timezone: ${pending.timezone ?? "UTC"}`,
16389
16603
  ` Backend: ${backendName}`,
16390
16604
  ` Model: ${pending.model ?? "default"}`,
16605
+ ` Account: ${accountLabel}`,
16391
16606
  ` Thinking: ${pending.thinking ?? "auto"}`,
16392
16607
  ` Timeout: ${timeoutLabel}`,
16393
16608
  ` Session: ${pending.sessionType ?? "isolated"}`,
@@ -16445,7 +16660,8 @@ ${pending.task}`, {
16445
16660
  channel: pending.channel ?? null,
16446
16661
  target: pending.target ?? null,
16447
16662
  deliveryMode: pending.deliveryMode ?? "announce",
16448
- timezone: pending.timezone ?? "UTC"
16663
+ timezone: pending.timezone ?? "UTC",
16664
+ credentialSlotId: pending.credentialSlotId ?? null
16449
16665
  };
16450
16666
  try {
16451
16667
  if (editJobId) {
@@ -16507,7 +16723,8 @@ async function startEditWizard(chatId, jobId, channel) {
16507
16723
  sessionType: job.sessionType,
16508
16724
  deliveryMode: job.deliveryMode,
16509
16725
  channel: job.channel ?? void 0,
16510
- target: job.target ?? void 0
16726
+ target: job.target ?? void 0,
16727
+ credentialSlotId: job.credentialSlotId ?? void 0
16511
16728
  };
16512
16729
  if (pendingJobs.has(chatId)) cancelWizard(chatId);
16513
16730
  pendingJobs.set(chatId, pending);
@@ -17273,10 +17490,11 @@ var init_live_status = __esm({
17273
17490
  "use strict";
17274
17491
  init_log();
17275
17492
  init_helpers();
17276
- FLUSH_INTERVAL_DM_MS = 1e3;
17277
- FLUSH_INTERVAL_GROUP_MS = 3e3;
17493
+ init_telegram_throttle();
17494
+ FLUSH_INTERVAL_DM_MS = 2e3;
17495
+ FLUSH_INTERVAL_GROUP_MS = 5e3;
17278
17496
  MAX_THINKING_CHARS = 800;
17279
- GLOBAL_MIN_GAP_MS = 500;
17497
+ GLOBAL_MIN_GAP_MS = 1e3;
17280
17498
  globalLastFlushAt = 0;
17281
17499
  TRIM_THRESHOLD = 3500;
17282
17500
  MAX_ENTRIES = 200;
@@ -17382,6 +17600,8 @@ var init_live_status = __esm({
17382
17600
  if (this.consecutiveEditFailures >= _LiveStatusMessage.MAX_EDIT_FAILURES) return;
17383
17601
  if (Date.now() < this.nextFlushAllowedAt) return;
17384
17602
  if (!canFlushGlobally()) return;
17603
+ const throttleState = getThrottleState();
17604
+ if (throttleState?.isPaused) return;
17385
17605
  const deduped = dedupThinking(this.entries);
17386
17606
  const body = renderEntries(deduped, this.modelLabel, Date.now() - this.startTime, this.hasTrimmed);
17387
17607
  if (body === this.lastRendered) return;
@@ -19850,15 +20070,20 @@ async function sendJobDetail(chatId, jobId, channel, messageId) {
19850
20070
  \u26A0\uFE0F ${job.consecutiveFailures} consecutive failures` : "";
19851
20071
  const runs = getJobRuns(jobId, 1);
19852
20072
  const lastRunStatus = runs.length > 0 ? runs[0].status : null;
20073
+ const thinking2 = job.thinking ?? "auto";
20074
+ const accountLabel = resolveSlotLabel(job.backend ?? void 0, job.credentialSlotId);
19853
20075
  const lines = [
19854
20076
  `Job #${job.id}: ${job.title ?? job.description}`,
19855
20077
  buildSectionHeader("", 22),
19856
20078
  ...job.title ? [`Task: ${job.description}`] : [],
19857
20079
  `Runs: ${schedule2}${tz}`,
19858
- `Backend: ${backend2} | Model: ${model2}`,
20080
+ "",
19859
20081
  `Last run: ${lastRun}${lastRunStatus ? ` (${lastRunStatus})` : ""}`,
19860
20082
  `Next run: ${nextRun}`,
19861
- `Status: ${status}${failures}`
20083
+ `Status: ${status}${failures}`,
20084
+ "",
20085
+ `\u{1F3F7} ${backend2} \xB7 ${model2} \xB7 ${thinking2}`,
20086
+ `\u{1F464} ${accountLabel}`
19862
20087
  ];
19863
20088
  const text = lines.join("\n");
19864
20089
  if (typeof channel.sendKeyboard !== "function") {
@@ -20071,6 +20296,7 @@ var init_ui = __esm({
20071
20296
  init_format_time();
20072
20297
  init_cron();
20073
20298
  init_humanize();
20299
+ init_wizard();
20074
20300
  init_stt();
20075
20301
  init_helpers();
20076
20302
  ROTATION_MODE_LABELS = {
@@ -25937,7 +26163,7 @@ async function handleText(msg, channel) {
25937
26163
  await channel.sendText(chatId, limitMsg, { parseMode: "plain" });
25938
26164
  return;
25939
26165
  }
25940
- let intent = classifyIntent(text, chatId);
26166
+ let intent = await classifyIntentAsync(text, chatId);
25941
26167
  const cleanText = text.startsWith(">>") ? text.slice(2).trim() : text;
25942
26168
  let bootstrapTier = intent === "chat" ? "chat" : void 0;
25943
26169
  let maxTurns = void 0;
@@ -25981,9 +26207,10 @@ async function handleText(msg, channel) {
25981
26207
  agentMode: effectiveAgentMode
25982
26208
  });
25983
26209
  if (response.text) {
25984
- storePendingPlan(chatId, response.text, text);
26210
+ const revisedPlan = response.text.replace(/\[REACT:.+?\]/g, "").replace(/\[SEND_FILE:.+?\]/g, "").replace(/\[GENERATE_IMAGE:.+?\]/g, "").replace(/\[HISTORY_SEARCH:[^\]]+\]/g, "").trim();
26211
+ storePendingPlan(chatId, revisedPlan, text);
25985
26212
  if (typeof channel.sendKeyboard === "function") {
25986
- await channel.sendKeyboard(chatId, `\u{1F50D} ${response.text}`, [
26213
+ await channel.sendKeyboard(chatId, `\u{1F50D} ${revisedPlan}`, [
25987
26214
  [
25988
26215
  { label: "\u2705 Approve", data: "exec:approve", style: "success" },
25989
26216
  { label: "\u274C Reject", data: "exec:reject", style: "danger" }
@@ -26119,7 +26346,7 @@ Debating: "${question.slice(0, 100)}${question.length > 100 ? "\u2026" : ""}"`,
26119
26346
  })) {
26120
26347
  const planDirective = buildPlanningDirective();
26121
26348
  let typingActive2 = true;
26122
- const typingLoop2 = async () => {
26349
+ const typingLoop = async () => {
26123
26350
  while (typingActive2) {
26124
26351
  try {
26125
26352
  await channel.sendTyping?.(chatId);
@@ -26128,7 +26355,7 @@ Debating: "${question.slice(0, 100)}${question.length > 100 ? "\u2026" : ""}"`,
26128
26355
  await new Promise((r) => setTimeout(r, 4e3));
26129
26356
  }
26130
26357
  };
26131
- typingLoop2().catch(() => {
26358
+ typingLoop().catch(() => {
26132
26359
  });
26133
26360
  try {
26134
26361
  const planResponse = await askAgent(chatId, cleanText || text, {
@@ -26169,18 +26396,23 @@ Debating: "${question.slice(0, 100)}${question.length > 100 ? "\u2026" : ""}"`,
26169
26396
  }
26170
26397
  return;
26171
26398
  }
26172
- let typingActive = true;
26173
- const typingLoop = async () => {
26174
- while (typingActive) {
26175
- try {
26176
- await channel.sendTyping?.(chatId);
26177
- } catch {
26399
+ const verboseForTyping = settings.getVerboseLevel();
26400
+ const showThinkingForTyping = settings.getShowThinkingUi();
26401
+ const needsLiveStatusForTyping = verboseForTyping !== "off" || showThinkingForTyping;
26402
+ let typingActive = !needsLiveStatusForTyping;
26403
+ if (typingActive) {
26404
+ const typingLoop = async () => {
26405
+ while (typingActive) {
26406
+ try {
26407
+ await channel.sendTyping?.(chatId);
26408
+ } catch {
26409
+ }
26410
+ await new Promise((r) => setTimeout(r, 4e3));
26178
26411
  }
26179
- await new Promise((r) => setTimeout(r, 4e3));
26180
- }
26181
- };
26182
- typingLoop().catch(() => {
26183
- });
26412
+ };
26413
+ typingLoop().catch(() => {
26414
+ });
26415
+ }
26184
26416
  try {
26185
26417
  const tMode = settings.getMode();
26186
26418
  const tVerbose = settings.getVerboseLevel();
@@ -26803,6 +27035,13 @@ async function runWithRetry(job, model2, runId, t0) {
26803
27035
  const cronBackend = currentBackend;
26804
27036
  setAllowPaidSlots(chatId, cronBackend);
26805
27037
  }
27038
+ if (job.credentialSlotId) {
27039
+ if (currentBackend === BACKEND.GEMINI) {
27040
+ pinChatGeminiSlot(chatId, job.credentialSlotId);
27041
+ } else {
27042
+ pinChatBackendSlot(chatId, currentBackend, job.credentialSlotId);
27043
+ }
27044
+ }
26806
27045
  const response = await askAgent(chatId, job.description, {
26807
27046
  model: currentModel,
26808
27047
  backend: currentBackend,
@@ -26874,6 +27113,7 @@ var init_cron = __esm({
26874
27113
  "src/scheduler/cron.ts"() {
26875
27114
  "use strict";
26876
27115
  init_store5();
27116
+ init_types();
26877
27117
  init_backends();
26878
27118
  init_agent();
26879
27119
  init_log();
@@ -27564,6 +27804,115 @@ ${body.replace(/<[^>]*>/g, "").trim()}</code>
27564
27804
  }
27565
27805
  });
27566
27806
 
27807
+ // src/channels/health.ts
27808
+ function trackChannel(channel) {
27809
+ healthState.set(channel.name, {
27810
+ status: "healthy",
27811
+ startedAt: Date.now(),
27812
+ lastHealthyAt: Date.now(),
27813
+ lastErrorAt: null,
27814
+ lastError: null,
27815
+ reconnectAttempts: 0
27816
+ });
27817
+ }
27818
+ function markChannelHealthy(name) {
27819
+ const state = healthState.get(name);
27820
+ if (state) {
27821
+ state.status = "healthy";
27822
+ state.lastHealthyAt = Date.now();
27823
+ state.reconnectAttempts = 0;
27824
+ }
27825
+ }
27826
+ function markChannelDown(name, error3) {
27827
+ const state = healthState.get(name);
27828
+ if (state) {
27829
+ state.status = "down";
27830
+ state.lastErrorAt = Date.now();
27831
+ state.lastError = error3;
27832
+ warn(`[channel-health] ${name} marked as down: ${error3}`);
27833
+ }
27834
+ }
27835
+ function getChannelHealth(name) {
27836
+ const state = healthState.get(name);
27837
+ if (!state) return null;
27838
+ return {
27839
+ name,
27840
+ status: state.status,
27841
+ lastHealthyAt: state.lastHealthyAt,
27842
+ lastErrorAt: state.lastErrorAt,
27843
+ lastError: state.lastError,
27844
+ reconnectAttempts: state.reconnectAttempts,
27845
+ uptimeMs: state.status === "healthy" ? Date.now() - state.startedAt : 0
27846
+ };
27847
+ }
27848
+ async function attemptReconnect(channel, handler) {
27849
+ const state = healthState.get(channel.name);
27850
+ if (!state) return false;
27851
+ if (reconnecting.has(channel.name)) {
27852
+ log(`[channel-health] ${channel.name}: reconnect already in progress, skipping`);
27853
+ return false;
27854
+ }
27855
+ if (state.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
27856
+ error(`[channel-health] ${channel.name}: max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached`);
27857
+ return false;
27858
+ }
27859
+ reconnecting.add(channel.name);
27860
+ state.reconnectAttempts++;
27861
+ const backoffMs = RECONNECT_BASE_MS * Math.pow(2, state.reconnectAttempts - 1);
27862
+ log(`[channel-health] ${channel.name}: reconnect attempt ${state.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS} in ${backoffMs}ms`);
27863
+ await new Promise((r) => setTimeout(r, backoffMs));
27864
+ try {
27865
+ await channel.stop().catch(() => {
27866
+ });
27867
+ await channel.start(handler);
27868
+ markChannelHealthy(channel.name);
27869
+ log(`[channel-health] ${channel.name}: reconnected successfully`);
27870
+ return true;
27871
+ } catch (err) {
27872
+ const msg = err instanceof Error ? err.message : String(err);
27873
+ markChannelDown(channel.name, msg);
27874
+ return false;
27875
+ } finally {
27876
+ reconnecting.delete(channel.name);
27877
+ }
27878
+ }
27879
+ function startHealthMonitor3(channels, handler) {
27880
+ registeredHandler = handler;
27881
+ for (const ch of channels) {
27882
+ trackChannel(ch);
27883
+ }
27884
+ healthInterval = setInterval(() => {
27885
+ for (const ch of channels) {
27886
+ const health = getChannelHealth(ch.name);
27887
+ if (health?.status === "down" && registeredHandler) {
27888
+ attemptReconnect(ch, registeredHandler).catch(() => {
27889
+ });
27890
+ }
27891
+ }
27892
+ }, HEALTH_CHECK_INTERVAL_MS);
27893
+ log(`[channel-health] Monitoring ${channels.length} channel(s)`);
27894
+ }
27895
+ function stopHealthMonitor3() {
27896
+ if (healthInterval) {
27897
+ clearInterval(healthInterval);
27898
+ healthInterval = null;
27899
+ }
27900
+ }
27901
+ var healthState, MAX_RECONNECT_ATTEMPTS, RECONNECT_BASE_MS, HEALTH_CHECK_INTERVAL_MS, healthInterval, registeredHandler, reconnecting;
27902
+ var init_health3 = __esm({
27903
+ "src/channels/health.ts"() {
27904
+ "use strict";
27905
+ init_log();
27906
+ healthState = /* @__PURE__ */ new Map();
27907
+ MAX_RECONNECT_ATTEMPTS = 5;
27908
+ RECONNECT_BASE_MS = 5e3;
27909
+ HEALTH_CHECK_INTERVAL_MS = 15e3;
27910
+ healthInterval = null;
27911
+ registeredHandler = null;
27912
+ reconnecting = /* @__PURE__ */ new Set();
27913
+ }
27914
+ });
27915
+
27567
27916
  // src/channels/telegram.ts
27568
27917
  import { API_CONSTANTS, Bot, GrammyError as GrammyError2, InlineKeyboard, InputFile } from "grammy";
27569
27918
  function isFastPathMessage(msg) {
@@ -27594,10 +27943,11 @@ var init_telegram2 = __esm({
27594
27943
  "use strict";
27595
27944
  init_telegram();
27596
27945
  init_log();
27946
+ init_health3();
27597
27947
  init_store5();
27598
27948
  init_telegram_throttle();
27599
27949
  FAST_PATH_COMMANDS = /* @__PURE__ */ new Set(["stop", "status", "new", "newchat"]);
27600
- TelegramChannel = class {
27950
+ TelegramChannel = class _TelegramChannel {
27601
27951
  name = "telegram";
27602
27952
  bot;
27603
27953
  allowedChatIds;
@@ -27607,6 +27957,19 @@ var init_telegram2 = __esm({
27607
27957
  // messageId → chatId
27608
27958
  reactionHandlers = [];
27609
27959
  throttle;
27960
+ // ── Polling health tracking ─────────────────────────────────────────
27961
+ /** Timestamp of last update received from Telegram (message, callback, reaction) */
27962
+ lastUpdateAt = 0;
27963
+ /** True while polling is expected to be active (between start() and stop()) */
27964
+ pollingExpected = false;
27965
+ /** Watchdog interval that detects silent polling death */
27966
+ pollingWatchdog = null;
27967
+ /** Max time without any update before we consider polling dead (ms) */
27968
+ static POLLING_SILENCE_THRESHOLD_MS = 2 * 60 * 1e3;
27969
+ // 2 minutes
27970
+ /** How often the watchdog checks for polling health (ms) */
27971
+ static POLLING_WATCHDOG_INTERVAL_MS = 60 * 1e3;
27972
+ // 60 seconds
27610
27973
  constructor() {
27611
27974
  const token = process.env.TELEGRAM_BOT_TOKEN;
27612
27975
  if (!token) {
@@ -27624,6 +27987,10 @@ var init_telegram2 = __esm({
27624
27987
  this.bot = new Bot(token);
27625
27988
  this.throttle = new TelegramThrottle();
27626
27989
  this.throttle.setResumeNotifier(async (chatId, pausedSec, queuedCount) => {
27990
+ if (pausedSec > 60) {
27991
+ log(`[telegram] Skipping resume notification (paused ${pausedSec}s \u2014 too long, would risk another 429)`);
27992
+ return;
27993
+ }
27627
27994
  try {
27628
27995
  await this.bot.api.sendMessage(
27629
27996
  numericChatId(chatId),
@@ -27735,6 +28102,7 @@ var init_telegram2 = __esm({
27735
28102
  { command: "council", description: "Multi-model debate (select models, anonymous rounds)" }
27736
28103
  ]);
27737
28104
  this.bot.on("message", async (ctx) => {
28105
+ this.lastUpdateAt = Date.now();
27738
28106
  const chatId = ctx.chat.id.toString();
27739
28107
  const senderId = ctx.from?.id?.toString() ?? "";
27740
28108
  const authorized = this.isAuthorized(chatId) || this.isAuthorized(senderId);
@@ -27762,6 +28130,7 @@ var init_telegram2 = __esm({
27762
28130
  });
27763
28131
  });
27764
28132
  this.bot.on("callback_query:data", (ctx) => {
28133
+ this.lastUpdateAt = Date.now();
27765
28134
  const userId = ctx.from.id.toString();
27766
28135
  const chatId = ctx.callbackQuery.message?.chat?.id?.toString() ?? userId;
27767
28136
  log(`[telegram] Callback from user ${userId} in chat ${chatId}: ${ctx.callbackQuery.data}`);
@@ -27789,6 +28158,7 @@ var init_telegram2 = __esm({
27789
28158
  });
27790
28159
  });
27791
28160
  this.bot.on("message_reaction", async (ctx) => {
28161
+ this.lastUpdateAt = Date.now();
27792
28162
  const chatId = String(ctx.chat.id);
27793
28163
  const messageId = ctx.messageReaction.message_id;
27794
28164
  if (!this.agentMessageIds.has(messageId)) return;
@@ -27805,6 +28175,7 @@ var init_telegram2 = __esm({
27805
28175
  }
27806
28176
  });
27807
28177
  this.bot.on("inline_query", (ctx) => {
28178
+ this.lastUpdateAt = Date.now();
27808
28179
  if (!this.isAuthorized(ctx.from.id.toString())) return;
27809
28180
  this.handleInlineQuery(ctx).catch((err) => {
27810
28181
  error("[telegram] Inline query error:", err);
@@ -27818,25 +28189,64 @@ var init_telegram2 = __esm({
27818
28189
  error("[telegram] Unhandled error:", err);
27819
28190
  }
27820
28191
  });
27821
- this.bot.start({
28192
+ this.pollingExpected = true;
28193
+ this.lastUpdateAt = Date.now();
28194
+ const pollingPromise = this.bot.start({
27822
28195
  allowed_updates: [...API_CONSTANTS.ALL_UPDATE_TYPES],
27823
28196
  onStart: () => log("[telegram] Polling for messages...")
27824
- }).catch((err) => {
27825
- error("[telegram] Fatal: bot.start() failed:", err);
27826
- error("[telegram] Check TELEGRAM_BOT_TOKEN in ~/.cc-claw/.env \u2014 it may be invalid or revoked.");
27827
- error("[telegram] To regenerate: message @BotFather on Telegram, use /revoke, then 'cc-claw setup'");
27828
- process.exit(1);
27829
28197
  });
28198
+ pollingPromise.then(
28199
+ () => {
28200
+ if (this.pollingExpected) {
28201
+ error("[telegram] CRITICAL: Polling loop exited unexpectedly (resolved without stop)");
28202
+ markChannelDown("telegram", "Polling loop exited unexpectedly");
28203
+ }
28204
+ },
28205
+ (err) => {
28206
+ if (this.pollingExpected) {
28207
+ error("[telegram] Fatal: bot.start() failed:", err);
28208
+ error("[telegram] Check TELEGRAM_BOT_TOKEN in ~/.cc-claw/.env \u2014 it may be invalid or revoked.");
28209
+ error("[telegram] To regenerate: message @BotFather on Telegram, use /revoke, then 'cc-claw setup'");
28210
+ markChannelDown("telegram", `Polling error: ${err instanceof Error ? err.message : String(err)}`);
28211
+ }
28212
+ }
28213
+ );
28214
+ this.pollingWatchdog = setInterval(() => {
28215
+ if (!this.pollingExpected) return;
28216
+ const silenceMs = Date.now() - this.lastUpdateAt;
28217
+ if (silenceMs > _TelegramChannel.POLLING_SILENCE_THRESHOLD_MS) {
28218
+ error(
28219
+ `[telegram] CRITICAL: No updates received for ${Math.round(silenceMs / 1e3)}s \u2014 polling likely dead, triggering reconnect`
28220
+ );
28221
+ markChannelDown("telegram", `No updates for ${Math.round(silenceMs / 1e3)}s`);
28222
+ this.lastUpdateAt = Date.now();
28223
+ }
28224
+ }, _TelegramChannel.POLLING_WATCHDOG_INTERVAL_MS);
27830
28225
  }
27831
28226
  async stop() {
27832
- await this.bot.stop();
28227
+ this.pollingExpected = false;
28228
+ if (this.pollingWatchdog) {
28229
+ clearInterval(this.pollingWatchdog);
28230
+ this.pollingWatchdog = null;
28231
+ }
28232
+ try {
28233
+ await this.bot.stop();
28234
+ } catch {
28235
+ }
28236
+ const token = process.env.TELEGRAM_BOT_TOKEN;
28237
+ if (token) {
28238
+ this.bot = new Bot(token);
28239
+ }
27833
28240
  }
27834
28241
  async sendTyping(chatId, threadId) {
27835
- if (this.throttle.isPaused()) return;
27836
28242
  try {
27837
- await this.bot.api.sendChatAction(numericChatId(chatId), "typing", {
27838
- ...threadId ? { message_thread_id: threadId } : {}
27839
- });
28243
+ await this.throttle.tryBestEffort(
28244
+ chatId,
28245
+ "typing",
28246
+ () => this.bot.api.sendChatAction(numericChatId(chatId), "typing", {
28247
+ ...threadId ? { message_thread_id: threadId } : {}
28248
+ })
28249
+ );
27840
28250
  } catch {
27841
28251
  }
27842
28252
  }
@@ -27941,7 +28351,12 @@ var init_telegram2 = __esm({
27941
28351
  );
27942
28352
  return true;
27943
28353
  } catch (err) {
27944
- warn("[telegram] editText HTML failed, trying plain fallback:", err instanceof Error ? err.message : err);
28354
+ const errMsg = err instanceof Error ? err.message : String(err);
28355
+ if (errMsg.includes("overflow") || errMsg.includes("rate limit") || errMsg.includes("max wait")) {
28356
+ warn("[telegram] editText skipped fallback (throttle overload):", errMsg);
28357
+ return false;
28358
+ }
28359
+ warn("[telegram] editText HTML failed, trying plain fallback:", errMsg);
27945
28360
  try {
27946
28361
  await this.throttle.send(
27947
28362
  chatId,
@@ -28079,9 +28494,13 @@ var init_telegram2 = __esm({
28079
28494
  }
28080
28495
  async reactToMessage(chatId, messageId, emoji) {
28081
28496
  try {
28082
- await this.bot.api.setMessageReaction(numericChatId(chatId), parseInt(messageId), [
28083
- { type: "emoji", emoji }
28084
- ]);
28497
+ await this.throttle.tryBestEffort(
28498
+ chatId,
28499
+ "reaction",
28500
+ () => this.bot.api.setMessageReaction(numericChatId(chatId), parseInt(messageId), [
28501
+ { type: "emoji", emoji }
28502
+ ])
28503
+ );
28085
28504
  } catch (err) {
28086
28505
  log(`[telegram] reactToMessage failed (chat=${chatId} msg=${messageId}): ${err}`);
28087
28506
  }
@@ -28499,115 +28918,6 @@ var init_bootstrap2 = __esm({
28499
28918
  }
28500
28919
  });
28501
28920
 
28502
- // src/channels/health.ts
28503
- function trackChannel(channel) {
28504
- healthState.set(channel.name, {
28505
- status: "healthy",
28506
- startedAt: Date.now(),
28507
- lastHealthyAt: Date.now(),
28508
- lastErrorAt: null,
28509
- lastError: null,
28510
- reconnectAttempts: 0
28511
- });
28512
- }
28513
- function markChannelHealthy(name) {
28514
- const state = healthState.get(name);
28515
- if (state) {
28516
- state.status = "healthy";
28517
- state.lastHealthyAt = Date.now();
28518
- state.reconnectAttempts = 0;
28519
- }
28520
- }
28521
- function markChannelDown(name, error3) {
28522
- const state = healthState.get(name);
28523
- if (state) {
28524
- state.status = "down";
28525
- state.lastErrorAt = Date.now();
28526
- state.lastError = error3;
28527
- warn(`[channel-health] ${name} marked as down: ${error3}`);
28528
- }
28529
- }
28530
- function getChannelHealth(name) {
28531
- const state = healthState.get(name);
28532
- if (!state) return null;
28533
- return {
28534
- name,
28535
- status: state.status,
28536
- lastHealthyAt: state.lastHealthyAt,
28537
- lastErrorAt: state.lastErrorAt,
28538
- lastError: state.lastError,
28539
- reconnectAttempts: state.reconnectAttempts,
28540
- uptimeMs: state.status === "healthy" ? Date.now() - state.startedAt : 0
28541
- };
28542
- }
28543
- async function attemptReconnect(channel, handler) {
28544
- const state = healthState.get(channel.name);
28545
- if (!state) return false;
28546
- if (reconnecting.has(channel.name)) {
28547
- log(`[channel-health] ${channel.name}: reconnect already in progress, skipping`);
28548
- return false;
28549
- }
28550
- if (state.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
28551
- error(`[channel-health] ${channel.name}: max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached`);
28552
- return false;
28553
- }
28554
- reconnecting.add(channel.name);
28555
- state.reconnectAttempts++;
28556
- const backoffMs = RECONNECT_BASE_MS * Math.pow(2, state.reconnectAttempts - 1);
28557
- log(`[channel-health] ${channel.name}: reconnect attempt ${state.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS} in ${backoffMs}ms`);
28558
- await new Promise((r) => setTimeout(r, backoffMs));
28559
- try {
28560
- await channel.stop().catch(() => {
28561
- });
28562
- await channel.start(handler);
28563
- markChannelHealthy(channel.name);
28564
- log(`[channel-health] ${channel.name}: reconnected successfully`);
28565
- return true;
28566
- } catch (err) {
28567
- const msg = err instanceof Error ? err.message : String(err);
28568
- markChannelDown(channel.name, msg);
28569
- return false;
28570
- } finally {
28571
- reconnecting.delete(channel.name);
28572
- }
28573
- }
28574
- function startHealthMonitor3(channels, handler) {
28575
- registeredHandler = handler;
28576
- for (const ch of channels) {
28577
- trackChannel(ch);
28578
- }
28579
- healthInterval = setInterval(() => {
28580
- for (const ch of channels) {
28581
- const health = getChannelHealth(ch.name);
28582
- if (health?.status === "down" && registeredHandler) {
28583
- attemptReconnect(ch, registeredHandler).catch(() => {
28584
- });
28585
- }
28586
- }
28587
- }, HEALTH_CHECK_INTERVAL_MS);
28588
- log(`[channel-health] Monitoring ${channels.length} channel(s)`);
28589
- }
28590
- function stopHealthMonitor3() {
28591
- if (healthInterval) {
28592
- clearInterval(healthInterval);
28593
- healthInterval = null;
28594
- }
28595
- }
28596
- var healthState, MAX_RECONNECT_ATTEMPTS, RECONNECT_BASE_MS, HEALTH_CHECK_INTERVAL_MS, healthInterval, registeredHandler, reconnecting;
28597
- var init_health3 = __esm({
28598
- "src/channels/health.ts"() {
28599
- "use strict";
28600
- init_log();
28601
- healthState = /* @__PURE__ */ new Map();
28602
- MAX_RECONNECT_ATTEMPTS = 5;
28603
- RECONNECT_BASE_MS = 5e3;
28604
- HEALTH_CHECK_INTERVAL_MS = 15e3;
28605
- healthInterval = null;
28606
- registeredHandler = null;
28607
- reconnecting = /* @__PURE__ */ new Set();
28608
- }
28609
- });
28610
-
28611
28921
  // src/cli/commands/ai-skill.ts
28612
28922
  var ai_skill_exports = {};
28613
28923
  __export(ai_skill_exports, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-claw",
3
- "version": "0.20.14",
3
+ "version": "0.20.15",
4
4
  "description": "CC-Claw: Personal AI assistant on Telegram — multi-backend (Claude, Gemini, Codex, Cursor), sub-agent orchestration, MCP management",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",