cc-claw 0.20.13 → 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 +519 -201
  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.13" : (() => {
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, {
@@ -8088,8 +8104,11 @@ function revokeSubAgentToken(agentId) {
8088
8104
  function authenticateRequest(req, url) {
8089
8105
  const authHeader = req.headers.authorization ?? "";
8090
8106
  const bearerToken = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
8091
- const isMainToken = bearerToken === DASHBOARD_TOKEN;
8092
- const subEntry = subAgentTokens.get(bearerToken);
8107
+ const queryToken = url.searchParams.get("token") ?? "";
8108
+ const isBrowserRoute = BROWSER_ROUTES.has(url.pathname) || url.pathname.startsWith("/files");
8109
+ const effectiveToken = bearerToken || (isBrowserRoute ? queryToken : "");
8110
+ const isMainToken = effectiveToken === DASHBOARD_TOKEN;
8111
+ const subEntry = subAgentTokens.get(effectiveToken);
8093
8112
  const isSubAgentToken = !!subEntry && subEntry.expiresAt > Date.now();
8094
8113
  if (!isMainToken && !isSubAgentToken) {
8095
8114
  return { authenticated: false, isSubAgent: false };
@@ -8136,7 +8155,7 @@ function validateAgentIdentity(req, body) {
8136
8155
  throw new Error(`IDENTITY_MISMATCH: Token bound to agent ${boundAgentId}, but request claims ${callerAgentId}`);
8137
8156
  }
8138
8157
  }
8139
- var PORT, DASHBOARD_TOKEN, SUB_AGENT_TOKEN_TTL_MS, subAgentTokens, SUB_AGENT_ALLOWED_PATHS, MAX_BODY_BYTES;
8158
+ var PORT, DASHBOARD_TOKEN, SUB_AGENT_TOKEN_TTL_MS, subAgentTokens, SUB_AGENT_ALLOWED_PATHS, BROWSER_ROUTES, MAX_BODY_BYTES;
8140
8159
  var init_middleware = __esm({
8141
8160
  "src/dashboard/middleware.ts"() {
8142
8161
  "use strict";
@@ -8172,6 +8191,7 @@ var init_middleware = __esm({
8172
8191
  "/api/memory/history",
8173
8192
  "/api/memory/summaries"
8174
8193
  ]);
8194
+ BROWSER_ROUTES = /* @__PURE__ */ new Set(["/", "/index.html", "/upload"]);
8175
8195
  MAX_BODY_BYTES = 1048576;
8176
8196
  }
8177
8197
  });
@@ -14974,8 +14994,8 @@ var init_telegram_throttle = __esm({
14974
14994
  "src/channels/telegram-throttle.ts"() {
14975
14995
  "use strict";
14976
14996
  init_log();
14977
- PER_CHAT_INTERVAL_MS = 350;
14978
- GLOBAL_INTERVAL_MS = 50;
14997
+ PER_CHAT_INTERVAL_MS = 1e3;
14998
+ GLOBAL_INTERVAL_MS = 100;
14979
14999
  MAX_RETRIES2 = 2;
14980
15000
  RETRY_DELAY_MS = 1e3;
14981
15001
  MAX_QUEUE_SIZE = 100;
@@ -15003,6 +15023,9 @@ var init_telegram_throttle = __esm({
15003
15023
  }
15004
15024
  /** Enqueue a Telegram API call with automatic pacing and 429 handling. */
15005
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
+ }
15006
15029
  return new Promise((resolve, reject) => {
15007
15030
  if (this.queue.length >= MAX_QUEUE_SIZE) {
15008
15031
  const dropped = this.queue.shift();
@@ -15015,6 +15038,31 @@ var init_telegram_throttle = __esm({
15015
15038
  this.drain();
15016
15039
  });
15017
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
+ }
15018
15066
  /** Check whether the throttle is currently paused (rate-limited). */
15019
15067
  isPaused() {
15020
15068
  return Date.now() < this.pausedUntil;
@@ -15097,12 +15145,13 @@ var init_telegram_throttle = __esm({
15097
15145
  // ── Pause management ────────────────────────────────────────────────
15098
15146
  enterPause(retrySec, failedItem) {
15099
15147
  this.queue.unshift(failedItem);
15100
- this.pausedUntil = Date.now() + retrySec * 1e3;
15148
+ const bufferedSec = Math.ceil(retrySec * 1.5);
15149
+ this.pausedUntil = Date.now() + bufferedSec * 1e3;
15101
15150
  if (this.pauseStartedAt === 0) this.pauseStartedAt = Date.now();
15102
15151
  for (const qi of this.queue) {
15103
15152
  this.chatsPendingNotification.add(qi.chatId);
15104
15153
  }
15105
- 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)`);
15106
15155
  }
15107
15156
  async sendResumeNotifications() {
15108
15157
  const chats2 = new Set(this.chatsPendingNotification);
@@ -15735,6 +15784,7 @@ var init_profile = __esm({
15735
15784
  var classify_exports = {};
15736
15785
  __export(classify_exports, {
15737
15786
  classifyIntent: () => classifyIntent,
15787
+ classifyIntentAsync: () => classifyIntentAsync,
15738
15788
  getIntentStats: () => getIntentStats,
15739
15789
  resetIntentStats: () => resetIntentStats
15740
15790
  });
@@ -15745,12 +15795,17 @@ function resetIntentStats() {
15745
15795
  intentCounts.chat = 0;
15746
15796
  intentCounts.agentic = 0;
15747
15797
  }
15748
- function classifyIntent(text, chatId) {
15798
+ function classifyIntentFast(text, chatId) {
15749
15799
  const trimmed = text.trim();
15750
15800
  if (trimmed.startsWith(">>")) return "agentic";
15751
15801
  if (trimmed.startsWith("/")) return "agentic";
15752
15802
  const lower = trimmed.toLowerCase();
15753
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
+ }
15754
15809
  const sessionId = getSessionId(chatId);
15755
15810
  if (sessionId) {
15756
15811
  const lastTs = getLastMessageTimestamp(chatId);
@@ -15758,47 +15813,148 @@ function classifyIntent(text, chatId) {
15758
15813
  const elapsed = Date.now() - lastTs;
15759
15814
  if (elapsed < 12e4 && trimmed.length < 30) {
15760
15815
  log(`[intent] "${trimmed.slice(0, 30)}" -> agentic (active session, ${(elapsed / 1e3).toFixed(0)}s ago)`);
15761
- intentCounts.agentic++;
15762
15816
  return "agentic";
15763
15817
  }
15764
15818
  }
15765
15819
  }
15766
- if (CHAT_EXACT.has(lower)) {
15767
- log(`[intent] "${lower}" -> chat (exact match)`);
15768
- intentCounts.chat++;
15769
- return "chat";
15770
- }
15771
15820
  if (trimmed.length <= 4 && /^[\p{Emoji}\s]+$/u.test(trimmed)) {
15772
15821
  log(`[intent] "${trimmed}" -> chat (emoji-only)`);
15773
- intentCounts.chat++;
15774
15822
  return "chat";
15775
15823
  }
15776
15824
  for (const pattern of STRUCTURAL_PATTERNS) {
15777
15825
  if (pattern.test(normalized)) {
15778
15826
  log(`[intent] "${trimmed.slice(0, 40)}..." -> agentic (structural: ${pattern})`);
15779
- intentCounts.agentic++;
15780
15827
  return "agentic";
15781
15828
  }
15782
15829
  }
15783
15830
  for (const pattern of MUTATION_PATTERNS) {
15784
15831
  if (pattern.test(normalized)) {
15785
15832
  log(`[intent] "${trimmed.slice(0, 40)}..." -> agentic (mutation: ${pattern})`);
15786
- intentCounts.agentic++;
15787
15833
  return "agentic";
15788
15834
  }
15789
15835
  }
15790
15836
  for (const pattern of CHAT_QUESTION_PATTERNS) {
15791
15837
  if (pattern.test(normalized)) {
15792
15838
  log(`[intent] "${trimmed.slice(0, 40)}..." -> chat (question: ${pattern})`);
15793
- intentCounts.chat++;
15794
15839
  return "chat";
15795
15840
  }
15796
15841
  }
15797
- 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)`);
15798
15954
  intentCounts.agentic++;
15799
15955
  return "agentic";
15800
15956
  }
15801
- 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;
15802
15958
  var init_classify = __esm({
15803
15959
  "src/intent/classify.ts"() {
15804
15960
  "use strict";
@@ -15894,6 +16050,13 @@ var init_classify = __esm({
15894
16050
  /\b(error|bug|crash|fail|broken|issue|problem|exception|stack\s?trace)\b/i,
15895
16051
  /\b(function|class|const|let|var|import|export|return|async|await)\b/i
15896
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;
15897
16060
  }
15898
16061
  });
15899
16062
 
@@ -16120,8 +16283,8 @@ async function handleWizardText(chatId, text, channel) {
16120
16283
  case "thinking": {
16121
16284
  const level = text.trim().toLowerCase();
16122
16285
  pending.thinking = level || "auto";
16123
- pending.step = "timeout";
16124
- await promptTimeout(chatId, channel);
16286
+ pending.step = "account";
16287
+ await promptAccount(chatId, channel);
16125
16288
  break;
16126
16289
  }
16127
16290
  case "timeout": {
@@ -16214,6 +16377,17 @@ async function handleWizardCallback(chatId, data, channel) {
16214
16377
  await promptThinking(chatId, channel);
16215
16378
  } else if (data.startsWith("sched:thinking:")) {
16216
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" });
16217
16391
  pending.step = "timeout";
16218
16392
  await promptTimeout(chatId, channel);
16219
16393
  } else if (data.startsWith("sched:timeout:")) {
@@ -16319,9 +16493,52 @@ async function promptThinking(chatId, channel) {
16319
16493
  await channel.sendKeyboard(chatId, "Thinking/effort level for this job?", buttons);
16320
16494
  } else {
16321
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;
16322
16512
  pending.step = "timeout";
16323
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;
16324
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})`;
16325
16542
  }
16326
16543
  async function promptTimeout(chatId, channel) {
16327
16544
  if (typeof channel.sendKeyboard !== "function") {
@@ -16376,6 +16593,7 @@ async function promptConfirm(chatId, channel) {
16376
16593
  if (!pending) return;
16377
16594
  const backendName = pending.backend ? getAdapter(pending.backend).displayName : "default";
16378
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);
16379
16597
  const lines = [
16380
16598
  "Job configuration:",
16381
16599
  "",
@@ -16384,6 +16602,7 @@ async function promptConfirm(chatId, channel) {
16384
16602
  ` Timezone: ${pending.timezone ?? "UTC"}`,
16385
16603
  ` Backend: ${backendName}`,
16386
16604
  ` Model: ${pending.model ?? "default"}`,
16605
+ ` Account: ${accountLabel}`,
16387
16606
  ` Thinking: ${pending.thinking ?? "auto"}`,
16388
16607
  ` Timeout: ${timeoutLabel}`,
16389
16608
  ` Session: ${pending.sessionType ?? "isolated"}`,
@@ -16441,7 +16660,8 @@ ${pending.task}`, {
16441
16660
  channel: pending.channel ?? null,
16442
16661
  target: pending.target ?? null,
16443
16662
  deliveryMode: pending.deliveryMode ?? "announce",
16444
- timezone: pending.timezone ?? "UTC"
16663
+ timezone: pending.timezone ?? "UTC",
16664
+ credentialSlotId: pending.credentialSlotId ?? null
16445
16665
  };
16446
16666
  try {
16447
16667
  if (editJobId) {
@@ -16503,7 +16723,8 @@ async function startEditWizard(chatId, jobId, channel) {
16503
16723
  sessionType: job.sessionType,
16504
16724
  deliveryMode: job.deliveryMode,
16505
16725
  channel: job.channel ?? void 0,
16506
- target: job.target ?? void 0
16726
+ target: job.target ?? void 0,
16727
+ credentialSlotId: job.credentialSlotId ?? void 0
16507
16728
  };
16508
16729
  if (pendingJobs.has(chatId)) cancelWizard(chatId);
16509
16730
  pendingJobs.set(chatId, pending);
@@ -17269,10 +17490,11 @@ var init_live_status = __esm({
17269
17490
  "use strict";
17270
17491
  init_log();
17271
17492
  init_helpers();
17272
- FLUSH_INTERVAL_DM_MS = 1e3;
17273
- FLUSH_INTERVAL_GROUP_MS = 3e3;
17493
+ init_telegram_throttle();
17494
+ FLUSH_INTERVAL_DM_MS = 2e3;
17495
+ FLUSH_INTERVAL_GROUP_MS = 5e3;
17274
17496
  MAX_THINKING_CHARS = 800;
17275
- GLOBAL_MIN_GAP_MS = 500;
17497
+ GLOBAL_MIN_GAP_MS = 1e3;
17276
17498
  globalLastFlushAt = 0;
17277
17499
  TRIM_THRESHOLD = 3500;
17278
17500
  MAX_ENTRIES = 200;
@@ -17378,6 +17600,8 @@ var init_live_status = __esm({
17378
17600
  if (this.consecutiveEditFailures >= _LiveStatusMessage.MAX_EDIT_FAILURES) return;
17379
17601
  if (Date.now() < this.nextFlushAllowedAt) return;
17380
17602
  if (!canFlushGlobally()) return;
17603
+ const throttleState = getThrottleState();
17604
+ if (throttleState?.isPaused) return;
17381
17605
  const deduped = dedupThinking(this.entries);
17382
17606
  const body = renderEntries(deduped, this.modelLabel, Date.now() - this.startTime, this.hasTrimmed);
17383
17607
  if (body === this.lastRendered) return;
@@ -17871,7 +18095,7 @@ function makeToolActionCallback(chatId, channel, level) {
17871
18095
  };
17872
18096
  }
17873
18097
  async function processFileSends2(chatId, channel, text) {
17874
- const fileSendPattern = /\[SEND_FILE:(.+?)\]/g;
18098
+ const fileSendPattern = /\[\s*SEND_FILE:\s*(.+?)\s*\]/g;
17875
18099
  const filePaths = [];
17876
18100
  for (const match of text.matchAll(fileSendPattern)) {
17877
18101
  filePaths.push(match[1].trim());
@@ -17892,7 +18116,7 @@ async function processFileSends2(chatId, channel, text) {
17892
18116
  return text.replace(fileSendPattern, "").trim();
17893
18117
  }
17894
18118
  async function processImageGenerations(chatId, channel, text) {
17895
- const pattern = /\[GENERATE_IMAGE:(.+?)\]/g;
18119
+ const pattern = /\[\s*GENERATE_IMAGE:\s*(.+?)\s*\]/g;
17896
18120
  const prompts = [];
17897
18121
  for (const match of text.matchAll(pattern)) {
17898
18122
  prompts.push(match[1].trim());
@@ -17920,7 +18144,7 @@ async function processImageGenerations(chatId, channel, text) {
17920
18144
  return text.replace(pattern, "").trim();
17921
18145
  }
17922
18146
  async function processReaction(chatId, channel, text, messageId) {
17923
- const reactPatternGlobal = /\[REACT:(.+?)\]/g;
18147
+ const reactPatternGlobal = /\[\s*REACT:\s*(.+?)\s*\]/g;
17924
18148
  if (!reactPatternGlobal.test(text)) return text;
17925
18149
  let reacted = false;
17926
18150
  reactPatternGlobal.lastIndex = 0;
@@ -17934,7 +18158,7 @@ async function processReaction(chatId, channel, text, messageId) {
17934
18158
  reacted = true;
17935
18159
  }
17936
18160
  }
17937
- return text.replace(/\[REACT:(.+?)\]/g, "").trim();
18161
+ return text.replace(/\[\s*REACT:\s*(.+?)\s*\]/g, "").trim();
17938
18162
  }
17939
18163
  async function sendResponse(chatId, channel, text, messageId, replyToMessageId) {
17940
18164
  text = await processReaction(chatId, channel, text, messageId);
@@ -17947,9 +18171,9 @@ async function sendResponse(chatId, channel, text, messageId, replyToMessageId)
17947
18171
  }
17948
18172
  }
17949
18173
  let afterHistory = afterUpdates;
17950
- const historySearchMatch = afterHistory.match(/\[HISTORY_SEARCH:([^\]]+)\]/);
18174
+ const historySearchMatch = afterHistory.match(/\[\s*HISTORY_SEARCH:\s*([^\]]+?)\s*\]/);
17951
18175
  if (historySearchMatch) {
17952
- afterHistory = afterHistory.replace(/\[HISTORY_SEARCH:[^\]]+\]/g, "").trim();
18176
+ afterHistory = afterHistory.replace(/\[\s*HISTORY_SEARCH:\s*[^\]]+?\s*\]/g, "").trim();
17953
18177
  const hsQuery = historySearchMatch[1].trim();
17954
18178
  const hsResults = searchMessageLog(chatId, hsQuery, 10);
17955
18179
  if (hsResults.length > 0) {
@@ -19846,15 +20070,20 @@ async function sendJobDetail(chatId, jobId, channel, messageId) {
19846
20070
  \u26A0\uFE0F ${job.consecutiveFailures} consecutive failures` : "";
19847
20071
  const runs = getJobRuns(jobId, 1);
19848
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);
19849
20075
  const lines = [
19850
20076
  `Job #${job.id}: ${job.title ?? job.description}`,
19851
20077
  buildSectionHeader("", 22),
19852
20078
  ...job.title ? [`Task: ${job.description}`] : [],
19853
20079
  `Runs: ${schedule2}${tz}`,
19854
- `Backend: ${backend2} | Model: ${model2}`,
20080
+ "",
19855
20081
  `Last run: ${lastRun}${lastRunStatus ? ` (${lastRunStatus})` : ""}`,
19856
20082
  `Next run: ${nextRun}`,
19857
- `Status: ${status}${failures}`
20083
+ `Status: ${status}${failures}`,
20084
+ "",
20085
+ `\u{1F3F7} ${backend2} \xB7 ${model2} \xB7 ${thinking2}`,
20086
+ `\u{1F464} ${accountLabel}`
19858
20087
  ];
19859
20088
  const text = lines.join("\n");
19860
20089
  if (typeof channel.sendKeyboard !== "function") {
@@ -20067,6 +20296,7 @@ var init_ui = __esm({
20067
20296
  init_format_time();
20068
20297
  init_cron();
20069
20298
  init_humanize();
20299
+ init_wizard();
20070
20300
  init_stt();
20071
20301
  init_helpers();
20072
20302
  ROTATION_MODE_LABELS = {
@@ -25933,7 +26163,7 @@ async function handleText(msg, channel) {
25933
26163
  await channel.sendText(chatId, limitMsg, { parseMode: "plain" });
25934
26164
  return;
25935
26165
  }
25936
- let intent = classifyIntent(text, chatId);
26166
+ let intent = await classifyIntentAsync(text, chatId);
25937
26167
  const cleanText = text.startsWith(">>") ? text.slice(2).trim() : text;
25938
26168
  let bootstrapTier = intent === "chat" ? "chat" : void 0;
25939
26169
  let maxTurns = void 0;
@@ -25977,9 +26207,10 @@ async function handleText(msg, channel) {
25977
26207
  agentMode: effectiveAgentMode
25978
26208
  });
25979
26209
  if (response.text) {
25980
- 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);
25981
26212
  if (typeof channel.sendKeyboard === "function") {
25982
- await channel.sendKeyboard(chatId, `\u{1F50D} ${response.text}`, [
26213
+ await channel.sendKeyboard(chatId, `\u{1F50D} ${revisedPlan}`, [
25983
26214
  [
25984
26215
  { label: "\u2705 Approve", data: "exec:approve", style: "success" },
25985
26216
  { label: "\u274C Reject", data: "exec:reject", style: "danger" }
@@ -26115,7 +26346,7 @@ Debating: "${question.slice(0, 100)}${question.length > 100 ? "\u2026" : ""}"`,
26115
26346
  })) {
26116
26347
  const planDirective = buildPlanningDirective();
26117
26348
  let typingActive2 = true;
26118
- const typingLoop2 = async () => {
26349
+ const typingLoop = async () => {
26119
26350
  while (typingActive2) {
26120
26351
  try {
26121
26352
  await channel.sendTyping?.(chatId);
@@ -26124,7 +26355,7 @@ Debating: "${question.slice(0, 100)}${question.length > 100 ? "\u2026" : ""}"`,
26124
26355
  await new Promise((r) => setTimeout(r, 4e3));
26125
26356
  }
26126
26357
  };
26127
- typingLoop2().catch(() => {
26358
+ typingLoop().catch(() => {
26128
26359
  });
26129
26360
  try {
26130
26361
  const planResponse = await askAgent(chatId, cleanText || text, {
@@ -26165,18 +26396,23 @@ Debating: "${question.slice(0, 100)}${question.length > 100 ? "\u2026" : ""}"`,
26165
26396
  }
26166
26397
  return;
26167
26398
  }
26168
- let typingActive = true;
26169
- const typingLoop = async () => {
26170
- while (typingActive) {
26171
- try {
26172
- await channel.sendTyping?.(chatId);
26173
- } 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));
26174
26411
  }
26175
- await new Promise((r) => setTimeout(r, 4e3));
26176
- }
26177
- };
26178
- typingLoop().catch(() => {
26179
- });
26412
+ };
26413
+ typingLoop().catch(() => {
26414
+ });
26415
+ }
26180
26416
  try {
26181
26417
  const tMode = settings.getMode();
26182
26418
  const tVerbose = settings.getVerboseLevel();
@@ -26799,6 +27035,13 @@ async function runWithRetry(job, model2, runId, t0) {
26799
27035
  const cronBackend = currentBackend;
26800
27036
  setAllowPaidSlots(chatId, cronBackend);
26801
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
+ }
26802
27045
  const response = await askAgent(chatId, job.description, {
26803
27046
  model: currentModel,
26804
27047
  backend: currentBackend,
@@ -26870,6 +27113,7 @@ var init_cron = __esm({
26870
27113
  "src/scheduler/cron.ts"() {
26871
27114
  "use strict";
26872
27115
  init_store5();
27116
+ init_types();
26873
27117
  init_backends();
26874
27118
  init_agent();
26875
27119
  init_log();
@@ -27560,6 +27804,115 @@ ${body.replace(/<[^>]*>/g, "").trim()}</code>
27560
27804
  }
27561
27805
  });
27562
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
+
27563
27916
  // src/channels/telegram.ts
27564
27917
  import { API_CONSTANTS, Bot, GrammyError as GrammyError2, InlineKeyboard, InputFile } from "grammy";
27565
27918
  function isFastPathMessage(msg) {
@@ -27590,10 +27943,11 @@ var init_telegram2 = __esm({
27590
27943
  "use strict";
27591
27944
  init_telegram();
27592
27945
  init_log();
27946
+ init_health3();
27593
27947
  init_store5();
27594
27948
  init_telegram_throttle();
27595
27949
  FAST_PATH_COMMANDS = /* @__PURE__ */ new Set(["stop", "status", "new", "newchat"]);
27596
- TelegramChannel = class {
27950
+ TelegramChannel = class _TelegramChannel {
27597
27951
  name = "telegram";
27598
27952
  bot;
27599
27953
  allowedChatIds;
@@ -27603,6 +27957,19 @@ var init_telegram2 = __esm({
27603
27957
  // messageId → chatId
27604
27958
  reactionHandlers = [];
27605
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
27606
27973
  constructor() {
27607
27974
  const token = process.env.TELEGRAM_BOT_TOKEN;
27608
27975
  if (!token) {
@@ -27620,6 +27987,10 @@ var init_telegram2 = __esm({
27620
27987
  this.bot = new Bot(token);
27621
27988
  this.throttle = new TelegramThrottle();
27622
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
+ }
27623
27994
  try {
27624
27995
  await this.bot.api.sendMessage(
27625
27996
  numericChatId(chatId),
@@ -27660,6 +28031,8 @@ var init_telegram2 = __esm({
27660
28031
  { command: "newchat", description: "Start a fresh conversation" },
27661
28032
  { command: "summarize", description: "Save session to memory (or 'all' for pre-restart)" },
27662
28033
  { command: "stop", description: "Cancel the current running task" },
28034
+ { command: "debug", description: "Toggle session debug logging" },
28035
+ { command: "imagine", description: "Generate an image from a prompt" },
27663
28036
  // Backend & model
27664
28037
  { command: "backend", description: "Switch AI backend (Claude/Gemini/Codex/Cursor)" },
27665
28038
  { command: "claude", description: "Switch to Claude backend" },
@@ -27708,6 +28081,8 @@ var init_telegram2 = __esm({
27708
28081
  { command: "mcp", description: "List MCP servers across all backends" },
27709
28082
  // Skills & profile
27710
28083
  { command: "skills", description: "List and invoke skills" },
28084
+ { command: "extract_skill", description: "Extract a reusable skill from this session" },
28085
+ { command: "skill_install", description: "Install a skill from GitHub" },
27711
28086
  { command: "voice", description: "Toggle voice responses" },
27712
28087
  { command: "voice_config", description: "Configure voice provider and voice" },
27713
28088
  { command: "response_style", description: "Set the AI response style (concise/normal/detailed)" },
@@ -27727,6 +28102,7 @@ var init_telegram2 = __esm({
27727
28102
  { command: "council", description: "Multi-model debate (select models, anonymous rounds)" }
27728
28103
  ]);
27729
28104
  this.bot.on("message", async (ctx) => {
28105
+ this.lastUpdateAt = Date.now();
27730
28106
  const chatId = ctx.chat.id.toString();
27731
28107
  const senderId = ctx.from?.id?.toString() ?? "";
27732
28108
  const authorized = this.isAuthorized(chatId) || this.isAuthorized(senderId);
@@ -27754,6 +28130,7 @@ var init_telegram2 = __esm({
27754
28130
  });
27755
28131
  });
27756
28132
  this.bot.on("callback_query:data", (ctx) => {
28133
+ this.lastUpdateAt = Date.now();
27757
28134
  const userId = ctx.from.id.toString();
27758
28135
  const chatId = ctx.callbackQuery.message?.chat?.id?.toString() ?? userId;
27759
28136
  log(`[telegram] Callback from user ${userId} in chat ${chatId}: ${ctx.callbackQuery.data}`);
@@ -27781,6 +28158,7 @@ var init_telegram2 = __esm({
27781
28158
  });
27782
28159
  });
27783
28160
  this.bot.on("message_reaction", async (ctx) => {
28161
+ this.lastUpdateAt = Date.now();
27784
28162
  const chatId = String(ctx.chat.id);
27785
28163
  const messageId = ctx.messageReaction.message_id;
27786
28164
  if (!this.agentMessageIds.has(messageId)) return;
@@ -27797,6 +28175,7 @@ var init_telegram2 = __esm({
27797
28175
  }
27798
28176
  });
27799
28177
  this.bot.on("inline_query", (ctx) => {
28178
+ this.lastUpdateAt = Date.now();
27800
28179
  if (!this.isAuthorized(ctx.from.id.toString())) return;
27801
28180
  this.handleInlineQuery(ctx).catch((err) => {
27802
28181
  error("[telegram] Inline query error:", err);
@@ -27810,25 +28189,64 @@ var init_telegram2 = __esm({
27810
28189
  error("[telegram] Unhandled error:", err);
27811
28190
  }
27812
28191
  });
27813
- this.bot.start({
28192
+ this.pollingExpected = true;
28193
+ this.lastUpdateAt = Date.now();
28194
+ const pollingPromise = this.bot.start({
27814
28195
  allowed_updates: [...API_CONSTANTS.ALL_UPDATE_TYPES],
27815
28196
  onStart: () => log("[telegram] Polling for messages...")
27816
- }).catch((err) => {
27817
- error("[telegram] Fatal: bot.start() failed:", err);
27818
- error("[telegram] Check TELEGRAM_BOT_TOKEN in ~/.cc-claw/.env \u2014 it may be invalid or revoked.");
27819
- error("[telegram] To regenerate: message @BotFather on Telegram, use /revoke, then 'cc-claw setup'");
27820
- process.exit(1);
27821
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);
27822
28225
  }
27823
28226
  async stop() {
27824
- 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
+ }
27825
28240
  }
27826
28241
  async sendTyping(chatId, threadId) {
27827
- if (this.throttle.isPaused()) return;
27828
28242
  try {
27829
- await this.bot.api.sendChatAction(numericChatId(chatId), "typing", {
27830
- ...threadId ? { message_thread_id: threadId } : {}
27831
- });
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
+ );
27832
28250
  } catch {
27833
28251
  }
27834
28252
  }
@@ -27933,7 +28351,12 @@ var init_telegram2 = __esm({
27933
28351
  );
27934
28352
  return true;
27935
28353
  } catch (err) {
27936
- 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);
27937
28360
  try {
27938
28361
  await this.throttle.send(
27939
28362
  chatId,
@@ -28071,9 +28494,13 @@ var init_telegram2 = __esm({
28071
28494
  }
28072
28495
  async reactToMessage(chatId, messageId, emoji) {
28073
28496
  try {
28074
- await this.bot.api.setMessageReaction(numericChatId(chatId), parseInt(messageId), [
28075
- { type: "emoji", emoji }
28076
- ]);
28497
+ await this.throttle.tryBestEffort(
28498
+ chatId,
28499
+ "reaction",
28500
+ () => this.bot.api.setMessageReaction(numericChatId(chatId), parseInt(messageId), [
28501
+ { type: "emoji", emoji }
28502
+ ])
28503
+ );
28077
28504
  } catch (err) {
28078
28505
  log(`[telegram] reactToMessage failed (chat=${chatId} msg=${messageId}): ${err}`);
28079
28506
  }
@@ -28491,115 +28918,6 @@ var init_bootstrap2 = __esm({
28491
28918
  }
28492
28919
  });
28493
28920
 
28494
- // src/channels/health.ts
28495
- function trackChannel(channel) {
28496
- healthState.set(channel.name, {
28497
- status: "healthy",
28498
- startedAt: Date.now(),
28499
- lastHealthyAt: Date.now(),
28500
- lastErrorAt: null,
28501
- lastError: null,
28502
- reconnectAttempts: 0
28503
- });
28504
- }
28505
- function markChannelHealthy(name) {
28506
- const state = healthState.get(name);
28507
- if (state) {
28508
- state.status = "healthy";
28509
- state.lastHealthyAt = Date.now();
28510
- state.reconnectAttempts = 0;
28511
- }
28512
- }
28513
- function markChannelDown(name, error3) {
28514
- const state = healthState.get(name);
28515
- if (state) {
28516
- state.status = "down";
28517
- state.lastErrorAt = Date.now();
28518
- state.lastError = error3;
28519
- warn(`[channel-health] ${name} marked as down: ${error3}`);
28520
- }
28521
- }
28522
- function getChannelHealth(name) {
28523
- const state = healthState.get(name);
28524
- if (!state) return null;
28525
- return {
28526
- name,
28527
- status: state.status,
28528
- lastHealthyAt: state.lastHealthyAt,
28529
- lastErrorAt: state.lastErrorAt,
28530
- lastError: state.lastError,
28531
- reconnectAttempts: state.reconnectAttempts,
28532
- uptimeMs: state.status === "healthy" ? Date.now() - state.startedAt : 0
28533
- };
28534
- }
28535
- async function attemptReconnect(channel, handler) {
28536
- const state = healthState.get(channel.name);
28537
- if (!state) return false;
28538
- if (reconnecting.has(channel.name)) {
28539
- log(`[channel-health] ${channel.name}: reconnect already in progress, skipping`);
28540
- return false;
28541
- }
28542
- if (state.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
28543
- error(`[channel-health] ${channel.name}: max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached`);
28544
- return false;
28545
- }
28546
- reconnecting.add(channel.name);
28547
- state.reconnectAttempts++;
28548
- const backoffMs = RECONNECT_BASE_MS * Math.pow(2, state.reconnectAttempts - 1);
28549
- log(`[channel-health] ${channel.name}: reconnect attempt ${state.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS} in ${backoffMs}ms`);
28550
- await new Promise((r) => setTimeout(r, backoffMs));
28551
- try {
28552
- await channel.stop().catch(() => {
28553
- });
28554
- await channel.start(handler);
28555
- markChannelHealthy(channel.name);
28556
- log(`[channel-health] ${channel.name}: reconnected successfully`);
28557
- return true;
28558
- } catch (err) {
28559
- const msg = err instanceof Error ? err.message : String(err);
28560
- markChannelDown(channel.name, msg);
28561
- return false;
28562
- } finally {
28563
- reconnecting.delete(channel.name);
28564
- }
28565
- }
28566
- function startHealthMonitor3(channels, handler) {
28567
- registeredHandler = handler;
28568
- for (const ch of channels) {
28569
- trackChannel(ch);
28570
- }
28571
- healthInterval = setInterval(() => {
28572
- for (const ch of channels) {
28573
- const health = getChannelHealth(ch.name);
28574
- if (health?.status === "down" && registeredHandler) {
28575
- attemptReconnect(ch, registeredHandler).catch(() => {
28576
- });
28577
- }
28578
- }
28579
- }, HEALTH_CHECK_INTERVAL_MS);
28580
- log(`[channel-health] Monitoring ${channels.length} channel(s)`);
28581
- }
28582
- function stopHealthMonitor3() {
28583
- if (healthInterval) {
28584
- clearInterval(healthInterval);
28585
- healthInterval = null;
28586
- }
28587
- }
28588
- var healthState, MAX_RECONNECT_ATTEMPTS, RECONNECT_BASE_MS, HEALTH_CHECK_INTERVAL_MS, healthInterval, registeredHandler, reconnecting;
28589
- var init_health3 = __esm({
28590
- "src/channels/health.ts"() {
28591
- "use strict";
28592
- init_log();
28593
- healthState = /* @__PURE__ */ new Map();
28594
- MAX_RECONNECT_ATTEMPTS = 5;
28595
- RECONNECT_BASE_MS = 5e3;
28596
- HEALTH_CHECK_INTERVAL_MS = 15e3;
28597
- healthInterval = null;
28598
- registeredHandler = null;
28599
- reconnecting = /* @__PURE__ */ new Set();
28600
- }
28601
- });
28602
-
28603
28921
  // src/cli/commands/ai-skill.ts
28604
28922
  var ai_skill_exports = {};
28605
28923
  __export(ai_skill_exports, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-claw",
3
- "version": "0.20.13",
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",