cc-claw 0.20.10 → 0.20.12

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 +895 -646
  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.10" : (() => {
36
+ VERSION = true ? "0.20.12" : (() => {
37
37
  try {
38
38
  return JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
39
39
  } catch {
@@ -2721,15 +2721,16 @@ function toggleShowThinkingUi(chatId) {
2721
2721
  function getSkillSuggestionsEnabled(chatId) {
2722
2722
  const row = getDb().prepare(
2723
2723
  "SELECT value FROM chat_skill_suggestions WHERE chat_id = ?"
2724
- ).get(chatId);
2724
+ ).get(SKILL_SUGGESTIONS_GLOBAL_KEY);
2725
2725
  return (row?.value ?? 1) === 1;
2726
2726
  }
2727
- function setSkillSuggestionsEnabled(chatId, enabled) {
2727
+ function setSkillSuggestionsEnabled(chatIdOrEnabled, enabled) {
2728
+ const value = typeof chatIdOrEnabled === "boolean" ? chatIdOrEnabled : enabled;
2728
2729
  getDb().prepare(`
2729
2730
  INSERT INTO chat_skill_suggestions (chat_id, value)
2730
2731
  VALUES (?, ?)
2731
2732
  ON CONFLICT(chat_id) DO UPDATE SET value = ?
2732
- `).run(chatId, enabled ? 1 : 0, enabled ? 1 : 0);
2733
+ `).run(SKILL_SUGGESTIONS_GLOBAL_KEY, value ? 1 : 0, value ? 1 : 0);
2733
2734
  }
2734
2735
  function getMode(chatId) {
2735
2736
  const row = getDb().prepare(
@@ -2933,12 +2934,13 @@ function clearChatPaidSlots(chatId) {
2933
2934
  function clearAllPaidSlots() {
2934
2935
  getDb().prepare("DELETE FROM chat_allow_paid_slots").run();
2935
2936
  }
2936
- var pendingEscalations, ESCALATION_TTL_MS, ALL_TOOLS, GLOBAL_SUMMARIZER_SENTINEL, ChatSettingsSnapshot;
2937
+ var SKILL_SUGGESTIONS_GLOBAL_KEY, pendingEscalations, ESCALATION_TTL_MS, ALL_TOOLS, GLOBAL_SUMMARIZER_SENTINEL, ChatSettingsSnapshot;
2937
2938
  var init_chat_settings = __esm({
2938
2939
  "src/memory/chat-settings.ts"() {
2939
2940
  "use strict";
2940
2941
  init_store5();
2941
2942
  init_sessions();
2943
+ SKILL_SUGGESTIONS_GLOBAL_KEY = "__global__";
2942
2944
  pendingEscalations = /* @__PURE__ */ new Map();
2943
2945
  ESCALATION_TTL_MS = 5 * 60 * 1e3;
2944
2946
  ALL_TOOLS = ["Read", "Glob", "Grep", "Bash", "Write", "Edit", "WebFetch", "WebSearch", "Agent", "AskUserQuestion"];
@@ -3293,6 +3295,24 @@ var init_summaries = __esm({
3293
3295
  });
3294
3296
 
3295
3297
  // src/memory/jobs.ts
3298
+ var jobs_exports = {};
3299
+ __export(jobs_exports, {
3300
+ cancelJobById: () => cancelJobById,
3301
+ completeJobRun: () => completeJobRun,
3302
+ getActiveJobs: () => getActiveJobs,
3303
+ getAllJobs: () => getAllJobs,
3304
+ getJobById: () => getJobById,
3305
+ getJobRuns: () => getJobRuns,
3306
+ incrementJobFailures: () => incrementJobFailures,
3307
+ insertJob: () => insertJob,
3308
+ insertJobRun: () => insertJobRun,
3309
+ pruneJobRuns: () => pruneJobRuns,
3310
+ resetJobFailures: () => resetJobFailures,
3311
+ updateJob: () => updateJob,
3312
+ updateJobEnabled: () => updateJobEnabled,
3313
+ updateJobLastRun: () => updateJobLastRun,
3314
+ updateJobNextRun: () => updateJobNextRun
3315
+ });
3296
3316
  function insertJob(params) {
3297
3317
  const db3 = getDb();
3298
3318
  const result = db3.prepare(`
@@ -5892,6 +5912,10 @@ async function routeModelForTask(task, preferredModel) {
5892
5912
  if (!server) return void 0;
5893
5913
  return { model: best.model, server, reason: best.reason };
5894
5914
  }
5915
+ function getEligibleModelsForTask(task) {
5916
+ const available = getAvailableModels();
5917
+ return filterByTask(available, task).map((m) => m.name);
5918
+ }
5895
5919
  function routeModelForTaskSync(task, preferredModel) {
5896
5920
  if (preferredModel) {
5897
5921
  const model2 = getModelByName(preferredModel);
@@ -6128,6 +6152,7 @@ var service_exports = {};
6128
6152
  __export(service_exports, {
6129
6153
  addServer: () => addServer2,
6130
6154
  discoverModels: () => discoverModels,
6155
+ getEligibleModelsForTask: () => getEligibleModelsForTask,
6131
6156
  getLoadedModels: () => getLoadedModels,
6132
6157
  getModelForTask: () => getModelForTask,
6133
6158
  getModelForTaskAsync: () => getModelForTaskAsync,
@@ -6199,8 +6224,8 @@ async function healthCheck(serverName) {
6199
6224
  }
6200
6225
  if (needsCatalogRefresh) {
6201
6226
  try {
6202
- const { getAdapter: getAdapter5 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
6203
- const adapter = getAdapter5("ollama");
6227
+ const { getAdapter: getAdapter4 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
6228
+ const adapter = getAdapter4("ollama");
6204
6229
  if ("refreshModelCatalog" in adapter) {
6205
6230
  adapter.refreshModelCatalog();
6206
6231
  }
@@ -9580,8 +9605,8 @@ async function spawnSubAgent(chatId, opts) {
9580
9605
  if (!runner) throw new Error(`Unknown runner: ${opts.runner}`);
9581
9606
  if (opts.model) {
9582
9607
  try {
9583
- const { getAdapter: getAdapter5 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
9584
- const adapter = getAdapter5(opts.runner);
9608
+ const { getAdapter: getAdapter4 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
9609
+ const adapter = getAdapter4(opts.runner);
9585
9610
  if (adapter.availableModels && !adapter.availableModels[opts.model]) {
9586
9611
  const validModels = Object.keys(adapter.availableModels).join(", ");
9587
9612
  throw new Error(`Unknown model "${opts.model}" for ${opts.runner}. Available: ${validModels}`);
@@ -9962,8 +9987,8 @@ async function handleAgentComplete(agentId, chatId, resultText, usage2, mcpsAdde
9962
9987
  const runner = getRunner(agent.runnerId);
9963
9988
  if (runner) {
9964
9989
  try {
9965
- const { getAdapter: getAdapter5 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
9966
- const adapter = getAdapter5(agent.runnerId);
9990
+ const { getAdapter: getAdapter4 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
9991
+ const adapter = getAdapter4(agent.runnerId);
9967
9992
  const model2 = agent.model ?? adapter.defaultModel;
9968
9993
  const pricing = adapter.pricing[model2];
9969
9994
  if (pricing) {
@@ -10712,7 +10737,7 @@ var init_chat = __esm({
10712
10737
  }
10713
10738
  const { askAgent: askAgent3 } = await Promise.resolve().then(() => (init_agent(), agent_exports));
10714
10739
  const { getMode: getMode3, getCwd: getCwd3, getModel: getModel3, addUsage: addUsage3, getBackend: getBackend3 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
10715
- const { getAdapterForChat: getAdapterForChat3 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
10740
+ const { getAdapterForChat: getAdapterForChat2 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
10716
10741
  const chatId = body.chatId;
10717
10742
  const backend2 = body.backend;
10718
10743
  const PERM_LEVEL = { plan: 0, safe: 1, yolo: 2 };
@@ -10722,7 +10747,7 @@ var init_chat = __esm({
10722
10747
  const cwd = body.cwd ?? getCwd3(chatId);
10723
10748
  const model2 = body.model ?? getModel3(chatId) ?? (() => {
10724
10749
  try {
10725
- return getAdapterForChat3(chatId).defaultModel;
10750
+ return getAdapterForChat2(chatId).defaultModel;
10726
10751
  } catch {
10727
10752
  return void 0;
10728
10753
  }
@@ -11358,8 +11383,8 @@ var init_config = __esm({
11358
11383
  handleHeartbeatSet = async (req, res) => {
11359
11384
  try {
11360
11385
  const body = JSON.parse(await readBody(req));
11361
- const { setHeartbeatConfig: setHeartbeatConfig3 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
11362
- setHeartbeatConfig3(body.chatId, body);
11386
+ const { setHeartbeatConfig: setHeartbeatConfig4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
11387
+ setHeartbeatConfig4(body.chatId, body);
11363
11388
  jsonResponse(res, { success: true });
11364
11389
  } catch (err) {
11365
11390
  jsonResponse(res, { error: errorMessage(err) }, 400);
@@ -13292,9 +13317,10 @@ function spawnQuery(adapter, config2, model2, cancelState, thinkingLevel, timeou
13292
13317
  const env = opts?.envOverride ? thinkingConfig?.envOverrides ? { ...opts.envOverride, ...thinkingConfig.envOverrides } : opts.envOverride : adapter.getEnv(thinkingConfig?.envOverrides);
13293
13318
  const finalArgs = thinkingConfig?.extraArgs ? [...config2.args, ...thinkingConfig.extraArgs] : config2.args;
13294
13319
  log(`[agent:spawn] backend=${adapter.id} exe=${config2.executable} model=${model2} timeout=${effectiveTimeout / 1e3}s cwd=${config2.cwd ?? "(inherited)"}`);
13320
+ const stdinMode = adapter.id === "codex" ? "pipe" : "ignore";
13295
13321
  const proc = spawn6(config2.executable, finalArgs, {
13296
13322
  env,
13297
- stdio: ["ignore", "pipe", "pipe"],
13323
+ stdio: [stdinMode, "pipe", "pipe"],
13298
13324
  detached: true,
13299
13325
  ...config2.cwd ? { cwd: config2.cwd } : {}
13300
13326
  });
@@ -14561,9 +14587,636 @@ var init_propose = __esm({
14561
14587
  }
14562
14588
  });
14563
14589
 
14564
- // src/bootstrap/profile.ts
14565
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync14 } from "fs";
14590
+ // src/channels/telegram-throttle.ts
14591
+ var telegram_throttle_exports = {};
14592
+ __export(telegram_throttle_exports, {
14593
+ TelegramThrottle: () => TelegramThrottle,
14594
+ getThrottleState: () => getThrottleState
14595
+ });
14596
+ import { GrammyError } from "grammy";
14597
+ function getThrottleState() {
14598
+ if (!_activeThrottle) return null;
14599
+ return _activeThrottle.getState();
14600
+ }
14601
+ function is429(err) {
14602
+ return err instanceof GrammyError && err.error_code === 429;
14603
+ }
14604
+ function sleep(ms) {
14605
+ return new Promise((r) => setTimeout(r, ms));
14606
+ }
14607
+ var PER_CHAT_INTERVAL_MS, GLOBAL_INTERVAL_MS, MAX_RETRIES2, RETRY_DELAY_MS, MAX_QUEUE_SIZE, MAX_TOTAL_PAUSE_MS, _activeThrottle, TelegramThrottle;
14608
+ var init_telegram_throttle = __esm({
14609
+ "src/channels/telegram-throttle.ts"() {
14610
+ "use strict";
14611
+ init_log();
14612
+ PER_CHAT_INTERVAL_MS = 350;
14613
+ GLOBAL_INTERVAL_MS = 50;
14614
+ MAX_RETRIES2 = 2;
14615
+ RETRY_DELAY_MS = 1e3;
14616
+ MAX_QUEUE_SIZE = 100;
14617
+ MAX_TOTAL_PAUSE_MS = 30 * 60 * 1e3;
14618
+ _activeThrottle = null;
14619
+ TelegramThrottle = class {
14620
+ queue = [];
14621
+ processing = false;
14622
+ lastSendPerChat = /* @__PURE__ */ new Map();
14623
+ lastGlobalSend = 0;
14624
+ // Pause state
14625
+ pausedUntil = 0;
14626
+ pauseStartedAt = 0;
14627
+ chatsPendingNotification = /* @__PURE__ */ new Set();
14628
+ resumeNotifier;
14629
+ constructor() {
14630
+ _activeThrottle = this;
14631
+ }
14632
+ /**
14633
+ * Register a callback that fires when the throttle resumes after a pause.
14634
+ * The callback should send a message directly via bot.api (NOT through the throttle).
14635
+ */
14636
+ setResumeNotifier(fn) {
14637
+ this.resumeNotifier = fn;
14638
+ }
14639
+ /** Enqueue a Telegram API call with automatic pacing and 429 handling. */
14640
+ async send(chatId, label2, fn) {
14641
+ return new Promise((resolve, reject) => {
14642
+ if (this.queue.length >= MAX_QUEUE_SIZE) {
14643
+ const dropped = this.queue.shift();
14644
+ if (dropped) {
14645
+ warn(`[throttle] Queue full (${MAX_QUEUE_SIZE}), dropping oldest: ${dropped.label}`);
14646
+ dropped.reject(new Error("Dropped from send queue (overflow)"));
14647
+ }
14648
+ }
14649
+ this.queue.push({ chatId, label: label2, fn, resolve, reject });
14650
+ this.drain();
14651
+ });
14652
+ }
14653
+ /** Check whether the throttle is currently paused (rate-limited). */
14654
+ isPaused() {
14655
+ return Date.now() < this.pausedUntil;
14656
+ }
14657
+ /** Get structured state for diagnostics / health checks. */
14658
+ getState() {
14659
+ const now = Date.now();
14660
+ const paused = now < this.pausedUntil;
14661
+ return {
14662
+ isPaused: paused,
14663
+ queueDepth: this.queue.length,
14664
+ pausedUntilMs: this.pausedUntil,
14665
+ pauseRemainingSec: paused ? Math.ceil((this.pausedUntil - now) / 1e3) : 0
14666
+ };
14667
+ }
14668
+ // ── Queue processor ─────────────────────────────────────────────────
14669
+ async drain() {
14670
+ if (this.processing) return;
14671
+ this.processing = true;
14672
+ try {
14673
+ while (this.queue.length > 0) {
14674
+ while (this.isPaused()) {
14675
+ if (this.pauseStartedAt > 0 && Date.now() - this.pauseStartedAt > MAX_TOTAL_PAUSE_MS) {
14676
+ warn(`[throttle] Max pause duration exceeded (${MAX_TOTAL_PAUSE_MS / 6e4}min), dropping ${this.queue.length} items`);
14677
+ this.flushQueueWithError("Telegram rate limit exceeded max wait time");
14678
+ this.pausedUntil = 0;
14679
+ this.pauseStartedAt = 0;
14680
+ this.chatsPendingNotification.clear();
14681
+ break;
14682
+ }
14683
+ const waitMs = Math.min(this.pausedUntil - Date.now(), 5e3);
14684
+ if (waitMs > 0) await sleep(waitMs);
14685
+ }
14686
+ if (this.queue.length === 0) break;
14687
+ if (this.chatsPendingNotification.size > 0) {
14688
+ await this.sendResumeNotifications();
14689
+ }
14690
+ const item = this.queue[0];
14691
+ const lastChat = this.lastSendPerChat.get(item.chatId) ?? 0;
14692
+ const chatWait = PER_CHAT_INTERVAL_MS - (Date.now() - lastChat);
14693
+ if (chatWait > 0) await sleep(chatWait);
14694
+ const globalWait = GLOBAL_INTERVAL_MS - (Date.now() - this.lastGlobalSend);
14695
+ if (globalWait > 0) await sleep(globalWait);
14696
+ this.queue.shift();
14697
+ try {
14698
+ const result = await this.execWithRetry(item.label, item.fn);
14699
+ this.recordSend(item.chatId);
14700
+ this.pauseStartedAt = 0;
14701
+ item.resolve(result);
14702
+ } catch (err) {
14703
+ if (is429(err)) {
14704
+ const retrySec = err.parameters?.retry_after ?? 10;
14705
+ this.enterPause(retrySec, item);
14706
+ continue;
14707
+ }
14708
+ item.reject(err);
14709
+ }
14710
+ }
14711
+ } finally {
14712
+ this.processing = false;
14713
+ }
14714
+ }
14715
+ // ── Retry logic (non-429 errors only) ───────────────────────────────
14716
+ async execWithRetry(label2, fn) {
14717
+ for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
14718
+ try {
14719
+ return await fn();
14720
+ } catch (err) {
14721
+ if (is429(err)) throw err;
14722
+ if (attempt < MAX_RETRIES2 && err instanceof GrammyError) {
14723
+ warn(`[throttle] ${label2} attempt ${attempt + 1}/${MAX_RETRIES2} failed (${err.error_code}), retrying`);
14724
+ await sleep(RETRY_DELAY_MS);
14725
+ continue;
14726
+ }
14727
+ throw err;
14728
+ }
14729
+ }
14730
+ throw new Error("unreachable");
14731
+ }
14732
+ // ── Pause management ────────────────────────────────────────────────
14733
+ enterPause(retrySec, failedItem) {
14734
+ this.queue.unshift(failedItem);
14735
+ this.pausedUntil = Date.now() + retrySec * 1e3;
14736
+ if (this.pauseStartedAt === 0) this.pauseStartedAt = Date.now();
14737
+ for (const qi of this.queue) {
14738
+ this.chatsPendingNotification.add(qi.chatId);
14739
+ }
14740
+ warn(`[throttle] 429 \u2014 pausing ALL sends for ${retrySec}s (${this.queue.length} items queued)`);
14741
+ }
14742
+ async sendResumeNotifications() {
14743
+ const chats2 = new Set(this.chatsPendingNotification);
14744
+ this.chatsPendingNotification.clear();
14745
+ if (!this.resumeNotifier) return;
14746
+ const pausedSec = this.pauseStartedAt > 0 ? Math.round((Date.now() - this.pauseStartedAt) / 1e3) : 0;
14747
+ for (const chatId of chats2) {
14748
+ const queuedForChat = this.queue.filter((q) => q.chatId === chatId).length;
14749
+ if (queuedForChat === 0) continue;
14750
+ try {
14751
+ await this.resumeNotifier(chatId, pausedSec, queuedForChat);
14752
+ this.recordSend(chatId);
14753
+ } catch (err) {
14754
+ if (is429(err)) {
14755
+ const retrySec = err.parameters?.retry_after ?? 10;
14756
+ this.pausedUntil = Date.now() + retrySec * 1e3;
14757
+ warn(`[throttle] Resume notification hit 429, re-pausing for ${retrySec}s (skipping further notifications)`);
14758
+ return;
14759
+ }
14760
+ warn(`[throttle] Resume notification failed for chat ${chatId}: ${err}`);
14761
+ }
14762
+ }
14763
+ this.pauseStartedAt = 0;
14764
+ }
14765
+ // ── Helpers ─────────────────────────────────────────────────────────
14766
+ recordSend(chatId) {
14767
+ const now = Date.now();
14768
+ this.lastSendPerChat.set(chatId, now);
14769
+ this.lastGlobalSend = now;
14770
+ }
14771
+ flushQueueWithError(message) {
14772
+ while (this.queue.length > 0) {
14773
+ const item = this.queue.shift();
14774
+ item.reject(new Error(message));
14775
+ }
14776
+ }
14777
+ };
14778
+ }
14779
+ });
14780
+
14781
+ // src/health/checks.ts
14782
+ import { existsSync as existsSync14, statSync as statSync6, readFileSync as readFileSync8 } from "fs";
14783
+ import { execFileSync, execSync as execSync3 } from "child_process";
14784
+ function getRecentErrors() {
14785
+ if (!existsSync14(ERROR_LOG_PATH)) return null;
14786
+ const logContent = readFileSync8(ERROR_LOG_PATH, "utf-8");
14787
+ const allLines = logContent.split("\n").filter(Boolean).slice(-500);
14788
+ const last24h = Date.now() - 864e5;
14789
+ const lines = allLines.filter((line) => {
14790
+ const match = line.match(/^\[(\d{4}-\d{2}-\d{2})\s+([\d:+-]+)/);
14791
+ if (match) return (/* @__PURE__ */ new Date(`${match[1]}T${match[2]}`)).getTime() > last24h;
14792
+ return false;
14793
+ });
14794
+ if (lines.length === 0) return null;
14795
+ const classified = { rate429: [], contentSilence: [], spawnTimeout: [], other: [], total: 0 };
14796
+ for (const line of lines) {
14797
+ if (/429|rate.?limit/i.test(line)) classified.rate429.push(line);
14798
+ else if (/content silence/i.test(line)) classified.contentSilence.push(line);
14799
+ else if (/spawn timeout|timeout after \d+s/i.test(line)) classified.spawnTimeout.push(line);
14800
+ else classified.other.push(line);
14801
+ }
14802
+ classified.total = lines.length;
14803
+ return classified;
14804
+ }
14805
+ function checkDatabase() {
14806
+ const checks = [];
14807
+ if (existsSync14(DB_PATH)) {
14808
+ const size = statSync6(DB_PATH).size;
14809
+ checks.push({ name: "Database", status: "ok", message: `${(size / 1024).toFixed(0)}KB` });
14810
+ } else {
14811
+ checks.push({ name: "Database", status: "error", message: "Not found", fix: "cc-claw setup" });
14812
+ }
14813
+ return checks;
14814
+ }
14815
+ function checkDiskSpace() {
14816
+ try {
14817
+ const dfOutput = execSync3("df -k " + DATA_PATH, { encoding: "utf-8" });
14818
+ const lines = dfOutput.trim().split("\n");
14819
+ if (lines.length >= 2) {
14820
+ const parts = lines[1].split(/\s+/);
14821
+ const availKB = parseInt(parts[3], 10);
14822
+ if (availKB < 1e5) {
14823
+ return { name: "Disk space", status: "warning", message: `${(availKB / 1024).toFixed(0)}MB available` };
14824
+ }
14825
+ return { name: "Disk space", status: "ok", message: `${(availKB / 1024 / 1024).toFixed(1)}GB available` };
14826
+ }
14827
+ } catch {
14828
+ }
14829
+ return null;
14830
+ }
14831
+ function checkErrorLog() {
14832
+ const checks = [];
14833
+ const errors = getRecentErrors();
14834
+ if (!errors) {
14835
+ checks.push({ name: "Recent errors", status: "ok", message: "none in last 24h" });
14836
+ return checks;
14837
+ }
14838
+ if (errors.rate429.length > 10) {
14839
+ checks.push({ name: "Rate limits", status: "error", message: `${errors.rate429.length} rate-limit (429) errors in last 24h` });
14840
+ } else if (errors.rate429.length > 0) {
14841
+ checks.push({ name: "Rate limits", status: "warning", message: `${errors.rate429.length} rate-limit errors in last 24h` });
14842
+ }
14843
+ if (errors.contentSilence.length > 0) {
14844
+ checks.push({ name: "Content silence", status: "warning", message: `${errors.contentSilence.length} silence timeout(s) in last 24h` });
14845
+ }
14846
+ if (errors.spawnTimeout.length > 0) {
14847
+ checks.push({ name: "Spawn timeouts", status: "warning", message: `${errors.spawnTimeout.length} backend timeout(s) in last 24h` });
14848
+ }
14849
+ if (errors.other.length > 0) {
14850
+ checks.push({ name: "Other errors", status: "warning", message: `${errors.other.length} error(s) in last 24h` });
14851
+ }
14852
+ if (checks.length === 0) {
14853
+ checks.push({ name: "Recent errors", status: "ok", message: "none in last 24h" });
14854
+ }
14855
+ return checks;
14856
+ }
14857
+ function checkBackendCLIs() {
14858
+ const checks = [];
14859
+ const CLI_BINARIES = { claude: "claude", gemini: "gemini", codex: "codex", cursor: "agent" };
14860
+ let installed = 0;
14861
+ for (const [label2, binary] of Object.entries(CLI_BINARIES)) {
14862
+ try {
14863
+ const path = execFileSync("which", [binary], { encoding: "utf-8", timeout: 5e3 }).trim();
14864
+ if (path) {
14865
+ checks.push({ name: `${label2} CLI`, status: "ok", message: path });
14866
+ installed++;
14867
+ }
14868
+ } catch {
14869
+ }
14870
+ }
14871
+ if (installed === 0) {
14872
+ checks.push({ name: "Backend CLIs", status: "error", message: "No backend CLIs found" });
14873
+ }
14874
+ return checks;
14875
+ }
14876
+ function checkOllamaServers() {
14877
+ try {
14878
+ const { OllamaStore } = (init_ollama(), __toCommonJS(ollama_exports));
14879
+ const servers = OllamaStore.listServers();
14880
+ if (servers.length > 0) {
14881
+ const online = servers.filter((s) => s.status === "online");
14882
+ const models = OllamaStore.getAvailableModels();
14883
+ if (online.length === servers.length) {
14884
+ return { name: "Ollama", status: "ok", message: `${online.length} server(s) online, ${models.length} model(s)` };
14885
+ } else if (online.length > 0) {
14886
+ return { name: "Ollama", status: "warning", message: `${online.length}/${servers.length} server(s) online` };
14887
+ }
14888
+ return { name: "Ollama", status: "warning", message: `${servers.length} server(s) configured, all offline`, fix: "ollama serve" };
14889
+ }
14890
+ } catch {
14891
+ }
14892
+ return null;
14893
+ }
14894
+ function checkBackendLimitsAll() {
14895
+ const checks = [];
14896
+ try {
14897
+ const { getAllBackendIds: getAllBackendIds3 } = (init_backends(), __toCommonJS(backends_exports));
14898
+ const { checkBackendLimits: checkBackendLimits2 } = (init_store5(), __toCommonJS(store_exports5));
14899
+ for (const backend2 of getAllBackendIds3()) {
14900
+ const limitMsg = checkBackendLimits2(backend2);
14901
+ if (limitMsg) {
14902
+ checks.push({ name: `${backend2} limits`, status: "warning", message: limitMsg });
14903
+ }
14904
+ }
14905
+ } catch {
14906
+ }
14907
+ return checks;
14908
+ }
14909
+ function checkSchedulerHealth() {
14910
+ const checks = [];
14911
+ try {
14912
+ const { getHealthReport: getHealthReport2 } = (init_health2(), __toCommonJS(health_exports2));
14913
+ const report = getHealthReport2();
14914
+ if (report.failingJobs.length > 0) {
14915
+ for (const j of report.failingJobs) {
14916
+ checks.push({
14917
+ name: `Job #${j.id}`,
14918
+ status: report.failingJobs.length > 2 ? "error" : "warning",
14919
+ message: `"${j.description.slice(0, 40)}": ${j.failures} consecutive failures`
14920
+ });
14921
+ }
14922
+ }
14923
+ checks.push({
14924
+ name: "Scheduler",
14925
+ status: report.status === "healthy" ? "ok" : "warning",
14926
+ message: `${report.activeJobs} jobs active, ${report.failingJobs.length} failing`
14927
+ });
14928
+ } catch {
14929
+ }
14930
+ return checks;
14931
+ }
14932
+ function checkTelegramThrottle() {
14933
+ try {
14934
+ const { getThrottleState: getThrottleState2 } = (init_telegram_throttle(), __toCommonJS(telegram_throttle_exports));
14935
+ const state = getThrottleState2();
14936
+ if (!state) return null;
14937
+ if (state.isPaused) {
14938
+ return {
14939
+ name: "Telegram send queue",
14940
+ status: "error",
14941
+ message: `PAUSED \u2014 rate-limited, ${state.pauseRemainingSec}s remaining, ${state.queueDepth} message(s) queued`
14942
+ };
14943
+ }
14944
+ if (state.queueDepth > 5) {
14945
+ return {
14946
+ name: "Telegram send queue",
14947
+ status: "warning",
14948
+ message: `${state.queueDepth} message(s) queued (pacing)`
14949
+ };
14950
+ }
14951
+ return null;
14952
+ } catch {
14953
+ return null;
14954
+ }
14955
+ }
14956
+ function runAllHealthChecks() {
14957
+ const checks = [];
14958
+ checks.push(...checkDatabase());
14959
+ const disk = checkDiskSpace();
14960
+ if (disk) checks.push(disk);
14961
+ checks.push(...checkErrorLog());
14962
+ const throttle = checkTelegramThrottle();
14963
+ if (throttle) checks.push(throttle);
14964
+ checks.push(...checkBackendLimitsAll());
14965
+ checks.push(...checkSchedulerHealth());
14966
+ const ollama2 = checkOllamaServers();
14967
+ if (ollama2) checks.push(ollama2);
14968
+ const errors = checks.filter((c) => c.status === "error").length;
14969
+ const warnings = checks.filter((c) => c.status === "warning").length;
14970
+ let statusLine;
14971
+ if (errors === 0 && warnings === 0) {
14972
+ statusLine = "\u2705 All systems OK";
14973
+ } else {
14974
+ const parts = [];
14975
+ if (errors > 0) parts.push(`${errors} error(s)`);
14976
+ if (warnings > 0) parts.push(`${warnings} warning(s)`);
14977
+ statusLine = `\u26A0\uFE0F ${parts.join(", ")}`;
14978
+ }
14979
+ return { checks, errors, warnings, statusLine };
14980
+ }
14981
+ function formatHealthForPrompt(summary) {
14982
+ const lines = [];
14983
+ for (const check of summary.checks) {
14984
+ const icon = check.status === "ok" ? "\u2705" : check.status === "warning" ? "\u26A0\uFE0F" : "\u274C";
14985
+ lines.push(`${icon} ${check.name}: ${check.message}`);
14986
+ }
14987
+ return lines.join("\n");
14988
+ }
14989
+ var init_checks = __esm({
14990
+ "src/health/checks.ts"() {
14991
+ "use strict";
14992
+ init_paths();
14993
+ }
14994
+ });
14995
+
14996
+ // src/bootstrap/heartbeat.ts
14997
+ var heartbeat_exports = {};
14998
+ __export(heartbeat_exports, {
14999
+ HEARTBEAT_JOB_TYPE: () => HEARTBEAT_JOB_TYPE,
15000
+ HEARTBEAT_OK: () => HEARTBEAT_OK,
15001
+ assembleHeartbeatPrompt: () => assembleHeartbeatPrompt,
15002
+ disableHeartbeat: () => disableHeartbeat,
15003
+ enableHeartbeat: () => enableHeartbeat,
15004
+ findHeartbeatJob: () => findHeartbeatJob,
15005
+ formatHeartbeatStatus: () => formatHeartbeatStatus,
15006
+ initHeartbeat: () => initHeartbeat,
15007
+ parseHeartbeatCommand: () => parseHeartbeatCommand,
15008
+ runHeartbeatNow: () => runHeartbeatNow,
15009
+ startAllHeartbeats: () => startAllHeartbeats,
15010
+ startHeartbeatForChat: () => startHeartbeatForChat,
15011
+ stopAllHeartbeats: () => stopAllHeartbeats,
15012
+ stopHeartbeatForChat: () => stopHeartbeatForChat,
15013
+ updateHeartbeatConfig: () => updateHeartbeatConfig
15014
+ });
15015
+ import { readFileSync as readFileSync9, existsSync as existsSync15 } from "fs";
14566
15016
  import { join as join14 } from "path";
15017
+ function findHeartbeatJob() {
15018
+ try {
15019
+ const { getDb: getDb2 } = (init_store5(), __toCommonJS(store_exports5));
15020
+ const db3 = getDb2();
15021
+ const row = db3.prepare(
15022
+ "SELECT * FROM jobs WHERE job_type = 'heartbeat' AND active = 1 ORDER BY id LIMIT 1"
15023
+ ).get();
15024
+ if (!row) return null;
15025
+ const { getJobById: getJobById3 } = (init_store5(), __toCommonJS(store_exports5));
15026
+ return getJobById3(row.id) ?? null;
15027
+ } catch {
15028
+ return null;
15029
+ }
15030
+ }
15031
+ function enableHeartbeat(chatId, opts) {
15032
+ const existing = findHeartbeatJob();
15033
+ if (existing) {
15034
+ if (!existing.enabled) {
15035
+ const { resumeJob: resumeJob2 } = (init_cron(), __toCommonJS(cron_exports));
15036
+ resumeJob2(existing.id);
15037
+ log(`[heartbeat] Resumed job #${existing.id}`);
15038
+ }
15039
+ return existing.id;
15040
+ }
15041
+ const { insertJob: insertJob2 } = (init_jobs(), __toCommonJS(jobs_exports));
15042
+ const { startSingleJob: startSingleJob2 } = (init_cron(), __toCommonJS(cron_exports));
15043
+ const intervalMs = opts?.intervalMs ?? DEFAULT_INTERVAL_MS;
15044
+ const job = insertJob2({
15045
+ scheduleType: "every",
15046
+ everyMs: intervalMs,
15047
+ description: DEFAULT_DESCRIPTION,
15048
+ chatId,
15049
+ backend: opts?.backend ?? null,
15050
+ model: opts?.model ?? null,
15051
+ sessionType: "isolated",
15052
+ deliveryMode: "announce",
15053
+ channel: opts?.channel ?? "telegram",
15054
+ target: opts?.target ?? chatId,
15055
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
15056
+ jobType: HEARTBEAT_JOB_TYPE,
15057
+ timeout: 120
15058
+ });
15059
+ startSingleJob2(job);
15060
+ log(`[heartbeat] Created job #${job.id} (every ${intervalMs / 6e4}min)`);
15061
+ return job.id;
15062
+ }
15063
+ function disableHeartbeat() {
15064
+ const job = findHeartbeatJob();
15065
+ if (!job) return false;
15066
+ const { pauseJob: pauseJob2 } = (init_cron(), __toCommonJS(cron_exports));
15067
+ pauseJob2(job.id);
15068
+ log(`[heartbeat] Paused job #${job.id}`);
15069
+ return true;
15070
+ }
15071
+ async function runHeartbeatNow() {
15072
+ const job = findHeartbeatJob();
15073
+ if (!job) return;
15074
+ const { triggerJob: triggerJob2 } = (init_cron(), __toCommonJS(cron_exports));
15075
+ await triggerJob2(job.id);
15076
+ }
15077
+ function updateHeartbeatConfig(updates) {
15078
+ const job = findHeartbeatJob();
15079
+ if (!job) return false;
15080
+ const { getDb: getDb2 } = (init_store5(), __toCommonJS(store_exports5));
15081
+ const db3 = getDb2();
15082
+ const sets = [];
15083
+ const values = [];
15084
+ if (updates.intervalMs !== void 0) {
15085
+ sets.push("every_ms = ?");
15086
+ values.push(updates.intervalMs);
15087
+ }
15088
+ if (updates.backend !== void 0) {
15089
+ sets.push("backend = ?");
15090
+ values.push(updates.backend);
15091
+ }
15092
+ if (updates.model !== void 0) {
15093
+ sets.push("model = ?");
15094
+ values.push(updates.model);
15095
+ }
15096
+ if (updates.target !== void 0) {
15097
+ sets.push("target = ?");
15098
+ values.push(updates.target);
15099
+ }
15100
+ if (updates.channel !== void 0) {
15101
+ sets.push("channel = ?");
15102
+ values.push(updates.channel);
15103
+ }
15104
+ if (sets.length === 0) return false;
15105
+ values.push(job.id);
15106
+ db3.prepare(`UPDATE jobs SET ${sets.join(", ")} WHERE id = ?`).run(...values);
15107
+ if (job.enabled) {
15108
+ const { stopJobTimer: stopJobTimer2, startSingleJob: startSingleJob2, listJobs: listJobs3 } = (init_cron(), __toCommonJS(cron_exports));
15109
+ const { getJobById: getJobById3 } = (init_store5(), __toCommonJS(store_exports5));
15110
+ stopJobTimer2(job.id);
15111
+ const refreshed = getJobById3(job.id);
15112
+ if (refreshed) startSingleJob2(refreshed);
15113
+ }
15114
+ return true;
15115
+ }
15116
+ function initHeartbeat(_channelReg) {
15117
+ }
15118
+ function startAllHeartbeats() {
15119
+ }
15120
+ function stopAllHeartbeats() {
15121
+ }
15122
+ function startHeartbeatForChat(chatId) {
15123
+ enableHeartbeat(chatId);
15124
+ }
15125
+ function stopHeartbeatForChat(_chatId) {
15126
+ disableHeartbeat();
15127
+ }
15128
+ function assembleHeartbeatPrompt(chatId) {
15129
+ cleanExpiredWatches();
15130
+ const sections = [];
15131
+ sections.push(
15132
+ "You are running a periodic heartbeat check. Review the following system health data and active watches. If nothing needs the user's attention, respond with exactly HEARTBEAT_OK and nothing else. Only alert on genuine issues \u2014 do NOT report that everything is fine."
15133
+ );
15134
+ const healthSummary = runAllHealthChecks();
15135
+ const healthText = formatHealthForPrompt(healthSummary);
15136
+ sections.push(`[System Health Report]
15137
+ ${healthText}`);
15138
+ const watches = getActiveWatches(chatId);
15139
+ if (watches.length > 0) {
15140
+ const watchLines = watches.map((w, i) => {
15141
+ const expiry = w.expiresAt ? ` (expires: ${w.expiresAt})` : "";
15142
+ return `${i + 1}. ${w.description}${expiry}`;
15143
+ });
15144
+ sections.push(`[Active Watches \u2014 execute these checks using your tools]
15145
+ ${watchLines.join("\n")}`);
15146
+ }
15147
+ if (existsSync15(HEARTBEAT_MD_PATH)) {
15148
+ try {
15149
+ const custom = readFileSync9(HEARTBEAT_MD_PATH, "utf-8").trim();
15150
+ if (custom) {
15151
+ sections.push(`[Custom checks from HEARTBEAT.md]
15152
+ ${custom}`);
15153
+ }
15154
+ } catch {
15155
+ }
15156
+ }
15157
+ return sections.join("\n\n");
15158
+ }
15159
+ function formatHeartbeatStatus(chatId) {
15160
+ const job = findHeartbeatJob();
15161
+ const watches = getActiveWatches(chatId);
15162
+ if (!job) {
15163
+ const lines2 = ["Heartbeat: not configured", "Use /heartbeat on to enable."];
15164
+ if (watches.length > 0) {
15165
+ lines2.push("", `Active watches (${watches.length}):`);
15166
+ for (const w of watches) {
15167
+ const expiry = w.expiresAt ? ` (until ${w.expiresAt})` : "";
15168
+ lines2.push(` - ${w.description}${expiry}`);
15169
+ }
15170
+ }
15171
+ return lines2.join("\n");
15172
+ }
15173
+ const lines = [
15174
+ `Heartbeat: ${job.enabled ? "ON" : "PAUSED"} (job #${job.id})`,
15175
+ `Interval: ${(job.everyMs ?? DEFAULT_INTERVAL_MS) / 6e4} minutes`,
15176
+ `Last run: ${job.lastRunAt ?? "never"}`,
15177
+ `Next run: ${job.nextRunAt ?? "N/A"}`
15178
+ ];
15179
+ if (job.backend || job.model) {
15180
+ lines.push(`Backend: ${job.backend ?? "default"} | Model: ${job.model ?? "default"}`);
15181
+ }
15182
+ if (job.target && job.target !== chatId) {
15183
+ lines.push(`Delivery: ${job.target}`);
15184
+ }
15185
+ if (job.fallbacks?.length > 0) {
15186
+ lines.push(`Fallbacks: ${job.fallbacks.map((f) => `${f.backend}${f.model ? `:${f.model}` : ""}`).join(", ")}`);
15187
+ }
15188
+ if (watches.length > 0) {
15189
+ lines.push("", `Active watches (${watches.length}):`);
15190
+ for (const w of watches) {
15191
+ const expiry = w.expiresAt ? ` (until ${w.expiresAt})` : "";
15192
+ lines.push(` - ${w.description}${expiry}`);
15193
+ }
15194
+ }
15195
+ return lines.join("\n");
15196
+ }
15197
+ function parseHeartbeatCommand(_chatId, args) {
15198
+ const parts = args.trim().toLowerCase().split(/\s+/);
15199
+ return { action: parts[0] ?? "status", value: parts.slice(1).join(" ") };
15200
+ }
15201
+ var HEARTBEAT_MD_PATH, HEARTBEAT_OK, HEARTBEAT_JOB_TYPE, DEFAULT_INTERVAL_MS, DEFAULT_DESCRIPTION;
15202
+ var init_heartbeat2 = __esm({
15203
+ "src/bootstrap/heartbeat.ts"() {
15204
+ "use strict";
15205
+ init_paths();
15206
+ init_store5();
15207
+ init_checks();
15208
+ init_log();
15209
+ HEARTBEAT_MD_PATH = join14(WORKSPACE_PATH, "HEARTBEAT.md");
15210
+ HEARTBEAT_OK = "HEARTBEAT_OK";
15211
+ HEARTBEAT_JOB_TYPE = "heartbeat";
15212
+ DEFAULT_INTERVAL_MS = 30 * 60 * 1e3;
15213
+ DEFAULT_DESCRIPTION = "System heartbeat \u2014 health checks and watch tasks";
15214
+ }
15215
+ });
15216
+
15217
+ // src/bootstrap/profile.ts
15218
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, existsSync as existsSync16 } from "fs";
15219
+ import { join as join15 } from "path";
14567
15220
  function hasActiveProfile(chatId) {
14568
15221
  return activeProfiles.has(chatId);
14569
15222
  }
@@ -14692,8 +15345,8 @@ function extractUserUpdates(text) {
14692
15345
  return { cleanText, updates };
14693
15346
  }
14694
15347
  function appendToUserProfile(key, value) {
14695
- if (!existsSync14(USER_PATH2)) return;
14696
- const content = readFileSync8(USER_PATH2, "utf-8");
15348
+ if (!existsSync16(USER_PATH2)) return;
15349
+ const content = readFileSync10(USER_PATH2, "utf-8");
14697
15350
  const line = `- **${key}**: ${value}`;
14698
15351
  if (content.includes(line)) return;
14699
15352
  const updated = content.trimEnd() + `
@@ -14708,7 +15361,7 @@ var init_profile = __esm({
14708
15361
  "use strict";
14709
15362
  init_paths();
14710
15363
  init_log();
14711
- USER_PATH2 = join14(IDENTITY_PATH, "USER.md");
15364
+ USER_PATH2 = join15(IDENTITY_PATH, "USER.md");
14712
15365
  activeProfiles = /* @__PURE__ */ new Map();
14713
15366
  }
14714
15367
  });
@@ -15745,8 +16398,8 @@ async function handleAdd(chatId, channel, name, host, port) {
15745
16398
  }
15746
16399
  const models = await OllamaService.discoverModels(name);
15747
16400
  try {
15748
- const { getAdapter: getAdapter5 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
15749
- const adapter = getAdapter5("ollama");
16401
+ const { getAdapter: getAdapter4 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
16402
+ const adapter = getAdapter4("ollama");
15750
16403
  if ("refreshModelCatalog" in adapter) {
15751
16404
  adapter.refreshModelCatalog();
15752
16405
  }
@@ -15799,8 +16452,8 @@ async function sendDiscover(chatId, channel, serverName) {
15799
16452
  const { OllamaService } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
15800
16453
  const models = await OllamaService.discoverModels(serverName);
15801
16454
  try {
15802
- const { getAdapter: getAdapter5 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
15803
- const adapter = getAdapter5("ollama");
16455
+ const { getAdapter: getAdapter4 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
16456
+ const adapter = getAdapter4("ollama");
15804
16457
  if ("refreshModelCatalog" in adapter) {
15805
16458
  adapter.refreshModelCatalog();
15806
16459
  }
@@ -16005,8 +16658,8 @@ __export(session_log_exports2, {
16005
16658
  startSessionLogCleanupTimer: () => startSessionLogCleanupTimer,
16006
16659
  tailSessionLog: () => tailSessionLog
16007
16660
  });
16008
- import { existsSync as existsSync15, mkdirSync as mkdirSync8, appendFileSync, readdirSync as readdirSync9, unlinkSync as unlinkSync6, statSync as statSync6, createReadStream } from "fs";
16009
- import { join as join15, basename } from "path";
16661
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, appendFileSync, readdirSync as readdirSync9, unlinkSync as unlinkSync6, statSync as statSync7, createReadStream } from "fs";
16662
+ import { join as join16, basename } from "path";
16010
16663
  import { createInterface as createInterface6 } from "readline";
16011
16664
  function getRetentionDays() {
16012
16665
  const env = process.env.SESSION_LOG_RETENTION_DAYS;
@@ -16018,15 +16671,15 @@ function getRetentionDays() {
16018
16671
  }
16019
16672
  function cleanupSessionLogs(retentionDays) {
16020
16673
  const days = retentionDays ?? getRetentionDays();
16021
- if (!existsSync15(SESSION_LOGS_PATH)) return 0;
16674
+ if (!existsSync17(SESSION_LOGS_PATH)) return 0;
16022
16675
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
16023
16676
  let cleaned = 0;
16024
16677
  try {
16025
16678
  for (const file of readdirSync9(SESSION_LOGS_PATH)) {
16026
16679
  if (!file.startsWith("session-") || !file.endsWith(".log")) continue;
16027
- const filePath = join15(SESSION_LOGS_PATH, file);
16680
+ const filePath = join16(SESSION_LOGS_PATH, file);
16028
16681
  try {
16029
- const { mtimeMs } = statSync6(filePath);
16682
+ const { mtimeMs } = statSync7(filePath);
16030
16683
  if (mtimeMs < cutoff) {
16031
16684
  unlinkSync6(filePath);
16032
16685
  cleaned++;
@@ -16050,13 +16703,13 @@ function startSessionLogCleanupTimer() {
16050
16703
  return timer;
16051
16704
  }
16052
16705
  function listSessionLogs() {
16053
- if (!existsSync15(SESSION_LOGS_PATH)) return [];
16706
+ if (!existsSync17(SESSION_LOGS_PATH)) return [];
16054
16707
  const logs = [];
16055
16708
  for (const file of readdirSync9(SESSION_LOGS_PATH)) {
16056
16709
  if (!file.startsWith("session-") || !file.endsWith(".log")) continue;
16057
- const filePath = join15(SESSION_LOGS_PATH, file);
16710
+ const filePath = join16(SESSION_LOGS_PATH, file);
16058
16711
  try {
16059
- const stat3 = statSync6(filePath);
16712
+ const stat3 = statSync7(filePath);
16060
16713
  const match = file.match(/^session-(.+?)-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})\.log$/);
16061
16714
  logs.push({
16062
16715
  filename: file,
@@ -16073,7 +16726,7 @@ function listSessionLogs() {
16073
16726
  return logs;
16074
16727
  }
16075
16728
  async function* tailSessionLog(filePath, lines = 50) {
16076
- if (!existsSync15(filePath)) {
16729
+ if (!existsSync17(filePath)) {
16077
16730
  yield `File not found: ${filePath}`;
16078
16731
  return;
16079
16732
  }
@@ -16101,12 +16754,12 @@ var init_session_log2 = __esm({
16101
16754
  constructor(chatId, backend2, model2) {
16102
16755
  this.backend = backend2;
16103
16756
  this.model = model2;
16104
- if (!existsSync15(SESSION_LOGS_PATH)) {
16757
+ if (!existsSync17(SESSION_LOGS_PATH)) {
16105
16758
  mkdirSync8(SESSION_LOGS_PATH, { recursive: true });
16106
16759
  }
16107
16760
  const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
16108
16761
  const sanitizedChatId = chatId.replace(/[^a-zA-Z0-9_-]/g, "_");
16109
- this.filePath = join15(SESSION_LOGS_PATH, `session-${sanitizedChatId}-${ts2}.log`);
16762
+ this.filePath = join16(SESSION_LOGS_PATH, `session-${sanitizedChatId}-${ts2}.log`);
16110
16763
  const header2 = [
16111
16764
  "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
16112
16765
  `CC-Claw Agent Session \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}`,
@@ -16508,14 +17161,14 @@ var init_gate = __esm({
16508
17161
 
16509
17162
  // src/voice/stt.ts
16510
17163
  import crypto from "crypto";
16511
- import { execFile as execFile2, execFileSync } from "child_process";
17164
+ import { execFile as execFile2, execFileSync as execFileSync2 } from "child_process";
16512
17165
  import { readFile as readFile2, unlink } from "fs/promises";
16513
17166
  import { promisify as promisify2 } from "util";
16514
17167
  function ensureFfmpeg() {
16515
17168
  if (ffmpegAvailable === true) return;
16516
17169
  if (ffmpegAvailable === false) throw new Error("ffmpeg is required for voice replies. Install it: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)");
16517
17170
  try {
16518
- execFileSync("ffmpeg", ["-version"], { stdio: "ignore" });
17171
+ execFileSync2("ffmpeg", ["-version"], { stdio: "ignore" });
16519
17172
  ffmpegAvailable = true;
16520
17173
  } catch {
16521
17174
  ffmpegAvailable = false;
@@ -16714,9 +17367,9 @@ var init_stt = __esm({
16714
17367
  });
16715
17368
 
16716
17369
  // src/media/image-gen.ts
16717
- import { mkdirSync as mkdirSync9, existsSync as existsSync16, unlink as unlink2, readdir, stat } from "fs";
17370
+ import { mkdirSync as mkdirSync9, existsSync as existsSync18, unlink as unlink2, readdir, stat } from "fs";
16718
17371
  import { writeFile } from "fs/promises";
16719
- import { join as join16 } from "path";
17372
+ import { join as join17 } from "path";
16720
17373
  async function generateImage(prompt) {
16721
17374
  const apiKey = process.env.GEMINI_API_KEY;
16722
17375
  if (!apiKey) {
@@ -16763,12 +17416,12 @@ async function generateImage(prompt) {
16763
17416
  if (!imageData) {
16764
17417
  throw new Error(textResponse ?? "Gemini did not generate an image. The prompt may have been filtered.");
16765
17418
  }
16766
- if (!existsSync16(IMAGE_OUTPUT_DIR)) {
17419
+ if (!existsSync18(IMAGE_OUTPUT_DIR)) {
16767
17420
  mkdirSync9(IMAGE_OUTPUT_DIR, { recursive: true });
16768
17421
  }
16769
17422
  const ext = mimeType.includes("jpeg") || mimeType.includes("jpg") ? "jpg" : "png";
16770
17423
  const filename = `img_${Date.now()}.${ext}`;
16771
- const filePath = join16(IMAGE_OUTPUT_DIR, filename);
17424
+ const filePath = join17(IMAGE_OUTPUT_DIR, filename);
16772
17425
  const buffer = Buffer.from(imageData, "base64");
16773
17426
  await writeFile(filePath, buffer);
16774
17427
  log(`[image-gen] Saved ${buffer.length} bytes to ${filePath}`);
@@ -16786,7 +17439,7 @@ function cleanupGeneratedImage(filePath) {
16786
17439
  function pruneImageCache() {
16787
17440
  readdir(IMAGE_OUTPUT_DIR, (err, files) => {
16788
17441
  if (err || !files) return;
16789
- const imageFiles = files.filter((f) => /\.(png|jpg)$/.test(f)).map((f) => join16(IMAGE_OUTPUT_DIR, f));
17442
+ const imageFiles = files.filter((f) => /\.(png|jpg)$/.test(f)).map((f) => join17(IMAGE_OUTPUT_DIR, f));
16790
17443
  if (imageFiles.length === 0) return;
16791
17444
  const now = Date.now();
16792
17445
  let statsPending = imageFiles.length;
@@ -16818,8 +17471,8 @@ var init_image_gen = __esm({
16818
17471
  MAX_GENERATED_IMAGES = 20;
16819
17472
  IMAGE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
16820
17473
  IMAGE_MODEL = "gemini-3.1-flash-image-preview";
16821
- IMAGE_OUTPUT_DIR = join16(
16822
- process.env.CC_CLAW_HOME ?? join16(process.env.HOME ?? "/tmp", ".cc-claw"),
17474
+ IMAGE_OUTPUT_DIR = join17(
17475
+ process.env.CC_CLAW_HOME ?? join17(process.env.HOME ?? "/tmp", ".cc-claw"),
16823
17476
  "data",
16824
17477
  "images"
16825
17478
  );
@@ -17276,7 +17929,7 @@ var init_video = __esm({
17276
17929
  });
17277
17930
 
17278
17931
  // src/router/media.ts
17279
- import { join as join17 } from "path";
17932
+ import { join as join18 } from "path";
17280
17933
  import { mkdir, writeFile as writeFile2, readdir as readdir2, stat as stat2, unlink as unlink3 } from "fs/promises";
17281
17934
  function getMediaRetentionMs() {
17282
17935
  const hours = parseInt(process.env.MEDIA_RETENTION_HOURS ?? "24", 10);
@@ -17285,7 +17938,7 @@ function getMediaRetentionMs() {
17285
17938
  async function saveMedia(buffer, prefix, ext) {
17286
17939
  await mkdir(MEDIA_INCOMING_PATH, { recursive: true });
17287
17940
  const filename = `${prefix}-${Date.now()}.${ext}`;
17288
- const fullPath = join17(MEDIA_INCOMING_PATH, filename);
17941
+ const fullPath = join18(MEDIA_INCOMING_PATH, filename);
17289
17942
  await writeFile2(fullPath, buffer);
17290
17943
  return fullPath;
17291
17944
  }
@@ -17299,7 +17952,7 @@ async function cleanupOldMedia() {
17299
17952
  let removed = 0;
17300
17953
  for (const file of files) {
17301
17954
  try {
17302
- const filePath = join17(MEDIA_INCOMING_PATH, file);
17955
+ const filePath = join18(MEDIA_INCOMING_PATH, file);
17303
17956
  const s = await stat2(filePath);
17304
17957
  if (now - s.mtimeMs > retentionMs) {
17305
17958
  await unlink3(filePath);
@@ -17535,7 +18188,7 @@ var init_media = __esm({
17535
18188
  init_helpers();
17536
18189
  init_response();
17537
18190
  init_live_status();
17538
- MEDIA_INCOMING_PATH = join17(MEDIA_PATH, "incoming");
18191
+ MEDIA_INCOMING_PATH = join18(MEDIA_PATH, "incoming");
17539
18192
  }
17540
18193
  });
17541
18194
 
@@ -17848,9 +18501,9 @@ __export(install_exports, {
17848
18501
  installSkillFromGitHub: () => installSkillFromGitHub
17849
18502
  });
17850
18503
  import { mkdir as mkdir2, readdir as readdir3, readFile as readFile4, cp } from "fs/promises";
17851
- import { existsSync as existsSync17 } from "fs";
17852
- import { join as join18, basename as basename2 } from "path";
17853
- import { execSync as execSync3 } from "child_process";
18504
+ import { existsSync as existsSync19 } from "fs";
18505
+ import { join as join19, basename as basename2 } from "path";
18506
+ import { execSync as execSync4 } from "child_process";
17854
18507
  async function installSkillFromGitHub(urlOrShorthand) {
17855
18508
  let repoUrl;
17856
18509
  let subPath;
@@ -17860,36 +18513,36 @@ async function installSkillFromGitHub(urlOrShorthand) {
17860
18513
  }
17861
18514
  repoUrl = parsed.cloneUrl;
17862
18515
  subPath = parsed.subPath;
17863
- const tmpDir = join18("/tmp", `cc-claw-skill-${Date.now()}`);
18516
+ const tmpDir = join19("/tmp", `cc-claw-skill-${Date.now()}`);
17864
18517
  try {
17865
18518
  log(`[skill-install] Cloning ${repoUrl} to ${tmpDir}`);
17866
- execSync3(`git clone --depth 1 ${repoUrl} ${tmpDir}`, {
18519
+ execSync4(`git clone --depth 1 ${repoUrl} ${tmpDir}`, {
17867
18520
  stdio: "pipe",
17868
18521
  timeout: 3e4
17869
18522
  });
17870
- if (!existsSync17(join18(tmpDir, ".git"))) {
18523
+ if (!existsSync19(join19(tmpDir, ".git"))) {
17871
18524
  return { success: false, error: "Git clone failed: no .git directory produced" };
17872
18525
  }
17873
- const searchRoot = subPath ? join18(tmpDir, subPath) : tmpDir;
18526
+ const searchRoot = subPath ? join19(tmpDir, subPath) : tmpDir;
17874
18527
  const skillDir = await findSkillDir(searchRoot);
17875
18528
  if (!skillDir) {
17876
18529
  return { success: false, error: "No SKILL.md found in the repository." };
17877
18530
  }
17878
18531
  const skillFolderName = basename2(skillDir);
17879
- const destDir = join18(SKILLS_PATH, skillFolderName);
17880
- if (existsSync17(destDir)) {
18532
+ const destDir = join19(SKILLS_PATH, skillFolderName);
18533
+ if (existsSync19(destDir)) {
17881
18534
  log(`[skill-install] Overwriting existing skill at ${destDir}`);
17882
18535
  }
17883
18536
  await mkdir2(destDir, { recursive: true });
17884
18537
  await cp(skillDir, destDir, { recursive: true });
17885
18538
  let skillName = skillFolderName;
17886
18539
  try {
17887
- const content = await readFile4(join18(destDir, "SKILL.md"), "utf-8");
18540
+ const content = await readFile4(join19(destDir, "SKILL.md"), "utf-8");
17888
18541
  const nameMatch = content.match(/^name:\s*(.+)$/m);
17889
18542
  if (nameMatch) skillName = nameMatch[1].trim().replace(/^["']|["']$/g, "");
17890
18543
  } catch {
17891
18544
  try {
17892
- const content = await readFile4(join18(destDir, "skill.md"), "utf-8");
18545
+ const content = await readFile4(join19(destDir, "skill.md"), "utf-8");
17893
18546
  const nameMatch = content.match(/^name:\s*(.+)$/m);
17894
18547
  if (nameMatch) skillName = nameMatch[1].trim().replace(/^["']|["']$/g, "");
17895
18548
  } catch {
@@ -17902,7 +18555,7 @@ async function installSkillFromGitHub(urlOrShorthand) {
17902
18555
  return { success: false, error: errorMessage(err) };
17903
18556
  } finally {
17904
18557
  try {
17905
- execSync3(`rm -rf ${tmpDir}`, { stdio: "pipe" });
18558
+ execSync4(`rm -rf ${tmpDir}`, { stdio: "pipe" });
17906
18559
  } catch {
17907
18560
  }
17908
18561
  }
@@ -17924,15 +18577,15 @@ function parseGitHubUrl(input) {
17924
18577
  async function findSkillDir(root) {
17925
18578
  const candidates = ["SKILL.md", "skill.md"];
17926
18579
  for (const c of candidates) {
17927
- if (existsSync17(join18(root, c))) return root;
18580
+ if (existsSync19(join19(root, c))) return root;
17928
18581
  }
17929
18582
  try {
17930
18583
  const entries = await readdir3(root, { withFileTypes: true });
17931
18584
  for (const entry of entries) {
17932
18585
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
17933
18586
  for (const c of candidates) {
17934
- if (existsSync17(join18(root, entry.name, c))) {
17935
- return join18(root, entry.name);
18587
+ if (existsSync19(join19(root, entry.name, c))) {
18588
+ return join19(root, entry.name);
17936
18589
  }
17937
18590
  }
17938
18591
  }
@@ -17944,15 +18597,15 @@ async function findSkillDir(root) {
17944
18597
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
17945
18598
  let subEntries;
17946
18599
  try {
17947
- subEntries = await readdir3(join18(root, entry.name), { withFileTypes: true });
18600
+ subEntries = await readdir3(join19(root, entry.name), { withFileTypes: true });
17948
18601
  } catch {
17949
18602
  continue;
17950
18603
  }
17951
18604
  for (const sub of subEntries) {
17952
18605
  if (!sub.isDirectory() || sub.name.startsWith(".")) continue;
17953
18606
  for (const c of candidates) {
17954
- if (existsSync17(join18(root, entry.name, sub.name, c))) {
17955
- return join18(root, entry.name, sub.name);
18607
+ if (existsSync19(join19(root, entry.name, sub.name, c))) {
18608
+ return join19(root, entry.name, sub.name);
17956
18609
  }
17957
18610
  }
17958
18611
  }
@@ -17969,444 +18622,6 @@ var init_install = __esm({
17969
18622
  }
17970
18623
  });
17971
18624
 
17972
- // src/health/checks.ts
17973
- import { existsSync as existsSync18, statSync as statSync7, readFileSync as readFileSync9 } from "fs";
17974
- import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
17975
- function getRecentErrors() {
17976
- if (!existsSync18(ERROR_LOG_PATH)) return null;
17977
- const logContent = readFileSync9(ERROR_LOG_PATH, "utf-8");
17978
- const allLines = logContent.split("\n").filter(Boolean).slice(-500);
17979
- const last24h = Date.now() - 864e5;
17980
- const lines = allLines.filter((line) => {
17981
- const match = line.match(/^\[(\d{4}-\d{2}-\d{2})\s+([\d:+-]+)/);
17982
- if (match) return (/* @__PURE__ */ new Date(`${match[1]}T${match[2]}`)).getTime() > last24h;
17983
- return false;
17984
- });
17985
- if (lines.length === 0) return null;
17986
- const classified = { rate429: [], contentSilence: [], spawnTimeout: [], other: [], total: 0 };
17987
- for (const line of lines) {
17988
- if (/429|rate.?limit/i.test(line)) classified.rate429.push(line);
17989
- else if (/content silence/i.test(line)) classified.contentSilence.push(line);
17990
- else if (/spawn timeout|timeout after \d+s/i.test(line)) classified.spawnTimeout.push(line);
17991
- else classified.other.push(line);
17992
- }
17993
- classified.total = lines.length;
17994
- return classified;
17995
- }
17996
- function checkDatabase() {
17997
- const checks = [];
17998
- if (existsSync18(DB_PATH)) {
17999
- const size = statSync7(DB_PATH).size;
18000
- checks.push({ name: "Database", status: "ok", message: `${(size / 1024).toFixed(0)}KB` });
18001
- } else {
18002
- checks.push({ name: "Database", status: "error", message: "Not found", fix: "cc-claw setup" });
18003
- }
18004
- return checks;
18005
- }
18006
- function checkDiskSpace() {
18007
- try {
18008
- const dfOutput = execSync4("df -k " + DATA_PATH, { encoding: "utf-8" });
18009
- const lines = dfOutput.trim().split("\n");
18010
- if (lines.length >= 2) {
18011
- const parts = lines[1].split(/\s+/);
18012
- const availKB = parseInt(parts[3], 10);
18013
- if (availKB < 1e5) {
18014
- return { name: "Disk space", status: "warning", message: `${(availKB / 1024).toFixed(0)}MB available` };
18015
- }
18016
- return { name: "Disk space", status: "ok", message: `${(availKB / 1024 / 1024).toFixed(1)}GB available` };
18017
- }
18018
- } catch {
18019
- }
18020
- return null;
18021
- }
18022
- function checkErrorLog() {
18023
- const checks = [];
18024
- const errors = getRecentErrors();
18025
- if (!errors) {
18026
- checks.push({ name: "Recent errors", status: "ok", message: "none in last 24h" });
18027
- return checks;
18028
- }
18029
- if (errors.rate429.length > 10) {
18030
- checks.push({ name: "Rate limits", status: "error", message: `${errors.rate429.length} rate-limit (429) errors in last 24h` });
18031
- } else if (errors.rate429.length > 0) {
18032
- checks.push({ name: "Rate limits", status: "warning", message: `${errors.rate429.length} rate-limit errors in last 24h` });
18033
- }
18034
- if (errors.contentSilence.length > 0) {
18035
- checks.push({ name: "Content silence", status: "warning", message: `${errors.contentSilence.length} silence timeout(s) in last 24h` });
18036
- }
18037
- if (errors.spawnTimeout.length > 0) {
18038
- checks.push({ name: "Spawn timeouts", status: "warning", message: `${errors.spawnTimeout.length} backend timeout(s) in last 24h` });
18039
- }
18040
- if (errors.other.length > 0) {
18041
- checks.push({ name: "Other errors", status: "warning", message: `${errors.other.length} error(s) in last 24h` });
18042
- }
18043
- if (checks.length === 0) {
18044
- checks.push({ name: "Recent errors", status: "ok", message: "none in last 24h" });
18045
- }
18046
- return checks;
18047
- }
18048
- function checkBackendCLIs() {
18049
- const checks = [];
18050
- const CLI_BINARIES = { claude: "claude", gemini: "gemini", codex: "codex", cursor: "agent" };
18051
- let installed = 0;
18052
- for (const [label2, binary] of Object.entries(CLI_BINARIES)) {
18053
- try {
18054
- const path = execFileSync2("which", [binary], { encoding: "utf-8", timeout: 5e3 }).trim();
18055
- if (path) {
18056
- checks.push({ name: `${label2} CLI`, status: "ok", message: path });
18057
- installed++;
18058
- }
18059
- } catch {
18060
- }
18061
- }
18062
- if (installed === 0) {
18063
- checks.push({ name: "Backend CLIs", status: "error", message: "No backend CLIs found" });
18064
- }
18065
- return checks;
18066
- }
18067
- function checkOllamaServers() {
18068
- try {
18069
- const { OllamaStore } = (init_ollama(), __toCommonJS(ollama_exports));
18070
- const servers = OllamaStore.listServers();
18071
- if (servers.length > 0) {
18072
- const online = servers.filter((s) => s.status === "online");
18073
- const models = OllamaStore.getAvailableModels();
18074
- if (online.length === servers.length) {
18075
- return { name: "Ollama", status: "ok", message: `${online.length} server(s) online, ${models.length} model(s)` };
18076
- } else if (online.length > 0) {
18077
- return { name: "Ollama", status: "warning", message: `${online.length}/${servers.length} server(s) online` };
18078
- }
18079
- return { name: "Ollama", status: "warning", message: `${servers.length} server(s) configured, all offline`, fix: "ollama serve" };
18080
- }
18081
- } catch {
18082
- }
18083
- return null;
18084
- }
18085
- function checkBackendLimitsAll() {
18086
- const checks = [];
18087
- try {
18088
- const { getAllBackendIds: getAllBackendIds3 } = (init_backends(), __toCommonJS(backends_exports));
18089
- const { checkBackendLimits: checkBackendLimits2 } = (init_store5(), __toCommonJS(store_exports5));
18090
- for (const backend2 of getAllBackendIds3()) {
18091
- const limitMsg = checkBackendLimits2(backend2);
18092
- if (limitMsg) {
18093
- checks.push({ name: `${backend2} limits`, status: "warning", message: limitMsg });
18094
- }
18095
- }
18096
- } catch {
18097
- }
18098
- return checks;
18099
- }
18100
- function checkSchedulerHealth() {
18101
- const checks = [];
18102
- try {
18103
- const { getHealthReport: getHealthReport2 } = (init_health2(), __toCommonJS(health_exports2));
18104
- const report = getHealthReport2();
18105
- if (report.failingJobs.length > 0) {
18106
- for (const j of report.failingJobs) {
18107
- checks.push({
18108
- name: `Job #${j.id}`,
18109
- status: report.failingJobs.length > 2 ? "error" : "warning",
18110
- message: `"${j.description.slice(0, 40)}": ${j.failures} consecutive failures`
18111
- });
18112
- }
18113
- }
18114
- checks.push({
18115
- name: "Scheduler",
18116
- status: report.status === "healthy" ? "ok" : "warning",
18117
- message: `${report.activeJobs} jobs active, ${report.failingJobs.length} failing`
18118
- });
18119
- } catch {
18120
- }
18121
- return checks;
18122
- }
18123
- function runAllHealthChecks() {
18124
- const checks = [];
18125
- checks.push(...checkDatabase());
18126
- const disk = checkDiskSpace();
18127
- if (disk) checks.push(disk);
18128
- checks.push(...checkErrorLog());
18129
- checks.push(...checkBackendLimitsAll());
18130
- checks.push(...checkSchedulerHealth());
18131
- const ollama2 = checkOllamaServers();
18132
- if (ollama2) checks.push(ollama2);
18133
- const errors = checks.filter((c) => c.status === "error").length;
18134
- const warnings = checks.filter((c) => c.status === "warning").length;
18135
- let statusLine;
18136
- if (errors === 0 && warnings === 0) {
18137
- statusLine = "\u2705 All systems OK";
18138
- } else {
18139
- const parts = [];
18140
- if (errors > 0) parts.push(`${errors} error(s)`);
18141
- if (warnings > 0) parts.push(`${warnings} warning(s)`);
18142
- statusLine = `\u26A0\uFE0F ${parts.join(", ")}`;
18143
- }
18144
- return { checks, errors, warnings, statusLine };
18145
- }
18146
- function formatHealthForPrompt(summary) {
18147
- const lines = [];
18148
- for (const check of summary.checks) {
18149
- const icon = check.status === "ok" ? "\u2705" : check.status === "warning" ? "\u26A0\uFE0F" : "\u274C";
18150
- lines.push(`${icon} ${check.name}: ${check.message}`);
18151
- }
18152
- return lines.join("\n");
18153
- }
18154
- var init_checks = __esm({
18155
- "src/health/checks.ts"() {
18156
- "use strict";
18157
- init_paths();
18158
- }
18159
- });
18160
-
18161
- // src/bootstrap/heartbeat.ts
18162
- import { readFileSync as readFileSync10, existsSync as existsSync19 } from "fs";
18163
- import { join as join19 } from "path";
18164
- function initHeartbeat(channelReg) {
18165
- registry2 = channelReg;
18166
- }
18167
- function startHeartbeatForChat(chatId) {
18168
- stopHeartbeatForChat(chatId);
18169
- const config2 = getHeartbeatConfig(chatId);
18170
- if (!config2 || !config2.enabled) return;
18171
- let running = false;
18172
- const timer = setInterval(async () => {
18173
- if (running) {
18174
- log(`[heartbeat] Skipping tick for ${chatId}: previous heartbeat still running`);
18175
- return;
18176
- }
18177
- running = true;
18178
- try {
18179
- await runHeartbeat(chatId, config2);
18180
- } finally {
18181
- running = false;
18182
- }
18183
- }, config2.intervalMs);
18184
- activeTimers.set(chatId, timer);
18185
- const nextBeat = new Date(Date.now() + config2.intervalMs).toISOString();
18186
- updateHeartbeatTimestamps(chatId, config2.lastBeatAt ?? "", nextBeat);
18187
- log(`[heartbeat] Started for chat ${chatId} (every ${config2.intervalMs / 6e4}min)`);
18188
- }
18189
- function stopHeartbeatForChat(chatId) {
18190
- const timer = activeTimers.get(chatId);
18191
- if (timer) {
18192
- clearInterval(timer);
18193
- activeTimers.delete(chatId);
18194
- }
18195
- }
18196
- function stopAllHeartbeats() {
18197
- for (const [chatId] of activeTimers) {
18198
- stopHeartbeatForChat(chatId);
18199
- }
18200
- }
18201
- function startAllHeartbeats() {
18202
- try {
18203
- const db3 = getDb();
18204
- const allowedIds = (process.env.ALLOWED_CHAT_ID ?? "").split(",").map((s) => s.trim()).filter(Boolean);
18205
- for (const chatId of allowedIds) {
18206
- const existing = getHeartbeatConfig(chatId);
18207
- if (!existing) {
18208
- setHeartbeatConfig(chatId, { enabled: true });
18209
- log(`[heartbeat] Enabled by default for chat ${chatId}`);
18210
- }
18211
- }
18212
- const rows = db3.prepare(
18213
- "SELECT chat_id FROM chat_heartbeat WHERE enabled = 1"
18214
- ).all();
18215
- for (const row of rows) {
18216
- startHeartbeatForChat(row.chat_id);
18217
- }
18218
- if (rows.length > 0) {
18219
- log(`[heartbeat] Started ${rows.length} heartbeat(s)`);
18220
- }
18221
- } catch {
18222
- }
18223
- }
18224
- async function runHeartbeatNow(chatId) {
18225
- const config2 = getHeartbeatConfig(chatId);
18226
- if (!config2) return;
18227
- await runHeartbeat(chatId, config2);
18228
- }
18229
- async function runHeartbeat(chatId, config2) {
18230
- if (!isWithinActiveHours(config2.activeStart, config2.activeEnd)) {
18231
- log(`[heartbeat] Skipping for ${chatId}: outside active hours (${config2.activeStart}-${config2.activeEnd})`);
18232
- return;
18233
- }
18234
- cleanExpiredWatches();
18235
- const prompt = assembleHeartbeatPrompt(chatId);
18236
- const resolvedBackend = resolveBackendId(chatId, config2);
18237
- const resolvedModel = config2.model ?? void 0;
18238
- try {
18239
- const response = await askAgent(chatId, prompt, {
18240
- bootstrapTier: "heartbeat",
18241
- backend: resolvedBackend,
18242
- model: resolvedModel,
18243
- timeoutMs: 12e4
18244
- });
18245
- if (response.usage) {
18246
- const heartbeatModel = resolvedModel ?? getModel(chatId) ?? resolvedBackend;
18247
- addUsage(chatId, response.usage.input, response.usage.output, response.usage.cacheRead, heartbeatModel, void 0, response.usage.contextSize);
18248
- }
18249
- const now = (/* @__PURE__ */ new Date()).toISOString();
18250
- const next = new Date(Date.now() + config2.intervalMs).toISOString();
18251
- updateHeartbeatTimestamps(chatId, now, next);
18252
- const trimmed = response.text.trim();
18253
- if (trimmed === HEARTBEAT_OK || trimmed.startsWith(HEARTBEAT_OK)) {
18254
- log(`[heartbeat] ${chatId}: nothing to report`);
18255
- return;
18256
- }
18257
- await deliverHeartbeatMessage(chatId, config2, response.text);
18258
- } catch (err) {
18259
- error(`[heartbeat] Error for ${chatId}: ${errorMessage(err)}`);
18260
- const fallbacks = parseHeartbeatFallbacks(config2.fallbacks);
18261
- if (fallbacks.length > 0) {
18262
- for (const fb of fallbacks) {
18263
- try {
18264
- log(`[heartbeat] Trying fallback: ${fb.backend}${fb.model ? `:${fb.model}` : ""}`);
18265
- const fbResponse = await askAgent(chatId, prompt, {
18266
- bootstrapTier: "heartbeat",
18267
- backend: fb.backend,
18268
- model: fb.model,
18269
- timeoutMs: 12e4
18270
- });
18271
- const now = (/* @__PURE__ */ new Date()).toISOString();
18272
- const next = new Date(Date.now() + config2.intervalMs).toISOString();
18273
- updateHeartbeatTimestamps(chatId, now, next);
18274
- const trimmed = fbResponse.text.trim();
18275
- if (trimmed === HEARTBEAT_OK || trimmed.startsWith(HEARTBEAT_OK)) {
18276
- log(`[heartbeat] ${chatId}: nothing to report (via fallback ${fb.backend})`);
18277
- return;
18278
- }
18279
- await deliverHeartbeatMessage(chatId, config2, fbResponse.text);
18280
- return;
18281
- } catch (fbErr) {
18282
- error(`[heartbeat] Fallback ${fb.backend} failed: ${errorMessage(fbErr)}`);
18283
- }
18284
- }
18285
- }
18286
- }
18287
- }
18288
- async function deliverHeartbeatMessage(chatId, config2, text) {
18289
- const channelName = config2.channel ?? "telegram";
18290
- const channel = registry2?.get(channelName);
18291
- if (!channel) return;
18292
- const targetChatId = config2.target ?? chatId;
18293
- if (channelName === "telegram") {
18294
- const topicMatch = targetChatId.match(/^(.+):topic:(\d+)$/);
18295
- if (topicMatch) {
18296
- await channel.sendText(topicMatch[1], text, { threadId: parseInt(topicMatch[2], 10) });
18297
- } else {
18298
- await channel.sendText(targetChatId, text);
18299
- }
18300
- } else {
18301
- await channel.sendText(targetChatId, text);
18302
- }
18303
- }
18304
- function resolveBackendId(chatId, config2) {
18305
- if (config2.backend) {
18306
- const available = getAvailableBackendIds();
18307
- if (available.includes(config2.backend)) {
18308
- return config2.backend;
18309
- }
18310
- }
18311
- return void 0;
18312
- }
18313
- function isWithinActiveHours(start, end) {
18314
- try {
18315
- const now = /* @__PURE__ */ new Date();
18316
- const [startH, startM] = start.split(":").map(Number);
18317
- const [endH, endM] = end.split(":").map(Number);
18318
- const currentMinutes = now.getHours() * 60 + now.getMinutes();
18319
- const startMinutes = startH * 60 + (startM || 0);
18320
- const endMinutes = endH * 60 + (endM || 0);
18321
- if (startMinutes <= endMinutes) {
18322
- return currentMinutes >= startMinutes && currentMinutes <= endMinutes;
18323
- }
18324
- return currentMinutes >= startMinutes || currentMinutes <= endMinutes;
18325
- } catch {
18326
- return true;
18327
- }
18328
- }
18329
- function assembleHeartbeatPrompt(chatId) {
18330
- const sections = [];
18331
- sections.push("You are running a periodic heartbeat check. Review the following system health data and active watches. If nothing needs the user's attention, respond with exactly HEARTBEAT_OK and nothing else. Only alert on genuine issues \u2014 do NOT report that everything is fine.");
18332
- const healthSummary = runAllHealthChecks();
18333
- const healthText = formatHealthForPrompt(healthSummary);
18334
- sections.push(`[System Health Report]
18335
- ${healthText}`);
18336
- const watches = getActiveWatches(chatId);
18337
- if (watches.length > 0) {
18338
- const watchLines = watches.map((w, i) => {
18339
- const expiry = w.expiresAt ? ` (expires: ${w.expiresAt})` : "";
18340
- return `${i + 1}. ${w.description}${expiry}`;
18341
- });
18342
- sections.push(`[Active Watches \u2014 execute these checks using your tools]
18343
- ${watchLines.join("\n")}`);
18344
- }
18345
- if (existsSync19(HEARTBEAT_MD_PATH)) {
18346
- try {
18347
- const custom = readFileSync10(HEARTBEAT_MD_PATH, "utf-8").trim();
18348
- if (custom) {
18349
- sections.push(`[Custom checks from HEARTBEAT.md]
18350
- ${custom}`);
18351
- }
18352
- } catch {
18353
- }
18354
- }
18355
- return sections.join("\n\n");
18356
- }
18357
- function formatHeartbeatStatus(chatId) {
18358
- const config2 = getHeartbeatConfig(chatId);
18359
- if (!config2) {
18360
- return "Heartbeat: not configured\nUse /heartbeat on to enable.";
18361
- }
18362
- const watches = getActiveWatches(chatId);
18363
- const lines = [
18364
- `Heartbeat: ${config2.enabled ? "ON" : "OFF"}`,
18365
- `Interval: ${config2.intervalMs / 6e4} minutes`,
18366
- `Active hours: ${config2.activeStart} - ${config2.activeEnd}`,
18367
- `Last beat: ${config2.lastBeatAt ?? "never"}`,
18368
- `Next beat: ${config2.nextBeatAt ?? "N/A"}`
18369
- ];
18370
- if (config2.backend || config2.model) {
18371
- lines.push(`Backend: ${config2.backend ?? "default"} | Model: ${config2.model ?? "default"}`);
18372
- }
18373
- if (config2.target) {
18374
- lines.push(`Delivery: ${config2.target}`);
18375
- }
18376
- const fallbacks = parseHeartbeatFallbacks(config2.fallbacks);
18377
- if (fallbacks.length > 0) {
18378
- lines.push(`Fallbacks: ${fallbacks.map((f) => `${f.backend}${f.model ? `:${f.model}` : ""}`).join(", ")}`);
18379
- }
18380
- if (watches.length > 0) {
18381
- lines.push("", `Active watches (${watches.length}):`);
18382
- for (const w of watches) {
18383
- const expiry = w.expiresAt ? ` (until ${w.expiresAt})` : "";
18384
- lines.push(` - ${w.description}${expiry}`);
18385
- }
18386
- }
18387
- return lines.join("\n");
18388
- }
18389
- function parseHeartbeatCommand(chatId, args) {
18390
- const parts = args.trim().toLowerCase().split(/\s+/);
18391
- return { action: parts[0] ?? "status", value: parts.slice(1).join(" ") };
18392
- }
18393
- var HEARTBEAT_MD_PATH, HEARTBEAT_OK, registry2, activeTimers;
18394
- var init_heartbeat2 = __esm({
18395
- "src/bootstrap/heartbeat.ts"() {
18396
- "use strict";
18397
- init_paths();
18398
- init_agent();
18399
- init_store5();
18400
- init_backends();
18401
- init_checks();
18402
- init_log();
18403
- HEARTBEAT_MD_PATH = join19(WORKSPACE_PATH, "HEARTBEAT.md");
18404
- HEARTBEAT_OK = "HEARTBEAT_OK";
18405
- registry2 = null;
18406
- activeTimers = /* @__PURE__ */ new Map();
18407
- }
18408
- });
18409
-
18410
18625
  // src/skills/discover.ts
18411
18626
  var discover_exports = {};
18412
18627
  __export(discover_exports, {
@@ -22302,12 +22517,44 @@ async function handleSummarizerCommand(chatId, commandArgs, msg, channel) {
22302
22517
  [{ label: `Off (disable)${isOff ? " \u2713" : ""}`, data: "summarizer:off", ...isOff ? { style: "primary" } : {} }]
22303
22518
  ];
22304
22519
  for (const a of getAvailableAdapters()) {
22305
- const pinned = current.backend === a.id;
22306
- buttons.push([{
22307
- label: `${pinned ? "\u2713 " : ""}${a.displayName}: ${a.summarizerModel}`,
22308
- data: `summarizer:${a.id}:${a.summarizerModel}`,
22309
- ...pinned ? { style: "primary" } : {}
22310
- }]);
22520
+ if (a.id === "ollama") {
22521
+ try {
22522
+ const { OllamaService } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
22523
+ const eligible = OllamaService.getEligibleModelsForTask("summarize");
22524
+ for (const modelName of eligible) {
22525
+ const pinned = current.backend === "ollama" && current.model === modelName;
22526
+ buttons.push([{
22527
+ label: `${pinned ? "\u2713 " : ""}Ollama: ${modelName}`,
22528
+ data: `summarizer:ollama:${modelName}`,
22529
+ ...pinned ? { style: "primary" } : {}
22530
+ }]);
22531
+ }
22532
+ if (eligible.length === 0 && a.summarizerModel) {
22533
+ const pinned = current.backend === a.id;
22534
+ buttons.push([{
22535
+ label: `${pinned ? "\u2713 " : ""}${a.displayName}: ${a.summarizerModel}`,
22536
+ data: `summarizer:${a.id}:${a.summarizerModel}`,
22537
+ ...pinned ? { style: "primary" } : {}
22538
+ }]);
22539
+ }
22540
+ } catch {
22541
+ if (a.summarizerModel) {
22542
+ const pinned = current.backend === a.id;
22543
+ buttons.push([{
22544
+ label: `${pinned ? "\u2713 " : ""}${a.displayName}: ${a.summarizerModel}`,
22545
+ data: `summarizer:${a.id}:${a.summarizerModel}`,
22546
+ ...pinned ? { style: "primary" } : {}
22547
+ }]);
22548
+ }
22549
+ }
22550
+ } else {
22551
+ const pinned = current.backend === a.id;
22552
+ buttons.push([{
22553
+ label: `${pinned ? "\u2713 " : ""}${a.displayName}: ${a.summarizerModel}`,
22554
+ data: `summarizer:${a.id}:${a.summarizerModel}`,
22555
+ ...pinned ? { style: "primary" } : {}
22556
+ }]);
22557
+ }
22311
22558
  }
22312
22559
  await channel.sendKeyboard(chatId, `Session summarizer (current: ${currentLabel}):`, buttons);
22313
22560
  } else {
@@ -23275,7 +23522,21 @@ function parseExtractedSkill(llmResponse) {
23275
23522
  let content = llmResponse;
23276
23523
  const fenceMatch = llmResponse.match(/```(?:markdown|md)?\s*\n([\s\S]*?)```/);
23277
23524
  if (fenceMatch) content = fenceMatch[1].trim();
23278
- const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
23525
+ let fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
23526
+ if (!fmMatch) {
23527
+ const greedyFence = llmResponse.match(/```(?:markdown|md)?\s*\n([\s\S]*)```/);
23528
+ if (greedyFence) {
23529
+ content = greedyFence[1].trim();
23530
+ fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
23531
+ }
23532
+ }
23533
+ if (!fmMatch) {
23534
+ fmMatch = llmResponse.match(/---\s*\n([\s\S]*?)\n---/);
23535
+ if (fmMatch) {
23536
+ const fmStart = llmResponse.indexOf(fmMatch[0]);
23537
+ content = llmResponse.slice(fmStart).replace(/```\s*$/, "").trim();
23538
+ }
23539
+ }
23279
23540
  if (!fmMatch) {
23280
23541
  warn("[auto-skill] No frontmatter found in extracted skill");
23281
23542
  return null;
@@ -24027,8 +24288,8 @@ ${rotationNote}`, { parseMode: "html" });
24027
24288
  if (action === "toggle") {
24028
24289
  const backend2 = parts[2];
24029
24290
  const model2 = parts[3];
24030
- const { getAdapter: getAdapter5 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
24031
- const toggleAdapter = getAdapter5(backend2);
24291
+ const { getAdapter: getAdapter4 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
24292
+ const toggleAdapter = getAdapter4(backend2);
24032
24293
  const label2 = toggleAdapter.availableModels[model2]?.label ?? model2;
24033
24294
  const { toggleParticipant: toggleParticipant2, buildSelectKeyboard: buildSelectKeyboard2, hasPendingCouncil: hasPendingCouncil2 } = await Promise.resolve().then(() => (init_wizard2(), wizard_exports));
24034
24295
  if (!hasPendingCouncil2(chatId)) {
@@ -24274,54 +24535,38 @@ Salience: ${memory2.salience.toFixed(2)} | Created: ${memory2.created_at.slice(0
24274
24535
  }
24275
24536
  return;
24276
24537
  } else if (data.startsWith("hb:")) {
24538
+ const { enableHeartbeat: enableHeartbeat2, disableHeartbeat: disableHeartbeat2, runHeartbeatNow: triggerHb, updateHeartbeatConfig: updateHeartbeatConfig2, findHeartbeatJob: findHeartbeatJob2 } = await Promise.resolve().then(() => (init_heartbeat2(), heartbeat_exports));
24277
24539
  const rest = data.slice(3);
24278
24540
  if (rest === "on") {
24279
- setHeartbeatConfig(chatId, { enabled: true });
24280
- startHeartbeatForChat(chatId);
24541
+ enableHeartbeat2(chatId);
24281
24542
  await sendHeartbeatKeyboard(chatId, channel, messageId);
24282
24543
  } else if (rest === "off") {
24283
- setHeartbeatConfig(chatId, { enabled: false });
24284
- stopHeartbeatForChat(chatId);
24285
- await channel.sendText(chatId, "\u26A0\uFE0F Heartbeat disabled. You won't receive proactive health alerts or watch notifications.\nRe-enable with /heartbeat on.", { parseMode: "plain" });
24544
+ disableHeartbeat2();
24545
+ await channel.sendText(chatId, "\u26A0\uFE0F Heartbeat paused. Re-enable with /heartbeat on.", { parseMode: "plain" });
24286
24546
  await sendHeartbeatKeyboard(chatId, channel, messageId);
24287
24547
  } else if (rest.startsWith("interval:")) {
24288
24548
  const min = parseInt(rest.slice(9), 10);
24289
24549
  if (isNaN(min) || min < 1) return;
24290
- const ms = min * 6e4;
24291
- setHeartbeatConfig(chatId, { intervalMs: ms });
24292
- stopHeartbeatForChat(chatId);
24293
- const hbConf = getHeartbeatConfig(chatId);
24294
- if (hbConf?.enabled) startHeartbeatForChat(chatId);
24550
+ updateHeartbeatConfig2({ intervalMs: min * 6e4 });
24295
24551
  await sendHeartbeatKeyboard(chatId, channel, messageId);
24296
24552
  } else if (rest.startsWith("backend:")) {
24297
24553
  const bid = rest.slice(8);
24298
24554
  if (bid === "default") {
24299
- updateHeartbeatField(chatId, "backend", null);
24300
- updateHeartbeatField(chatId, "model", null);
24555
+ updateHeartbeatConfig2({ backend: null, model: null });
24301
24556
  } else {
24302
- updateHeartbeatField(chatId, "backend", bid);
24303
- updateHeartbeatField(chatId, "model", null);
24557
+ updateHeartbeatConfig2({ backend: bid, model: null });
24304
24558
  }
24305
24559
  await sendHeartbeatKeyboard(chatId, channel, messageId);
24306
24560
  } else if (rest.startsWith("model:")) {
24307
24561
  const model2 = rest.slice(6);
24308
- if (model2 === "default") {
24309
- updateHeartbeatField(chatId, "model", null);
24310
- } else {
24311
- updateHeartbeatField(chatId, "model", model2);
24312
- }
24562
+ updateHeartbeatConfig2({ model: model2 === "default" ? null : model2 });
24313
24563
  await sendHeartbeatKeyboard(chatId, channel, messageId);
24314
24564
  } else if (rest === "thinking") {
24315
- const config2 = getHeartbeatConfig(chatId);
24316
- const current = config2?.thinking ?? "off";
24317
- const cycle = ["off", "low", "medium", "high"];
24318
- const next = cycle[(cycle.indexOf(current) + 1) % cycle.length];
24319
- updateHeartbeatField(chatId, "thinking", next === "off" ? null : next);
24320
- await sendHeartbeatKeyboard(chatId, channel, messageId);
24565
+ await channel.sendText(chatId, "Thinking level for heartbeat: use /editjob to configure.", { parseMode: "plain" });
24321
24566
  } else if (rest === "run") {
24322
24567
  await channel.sendText(chatId, "\u23F3 Running heartbeat check...", { parseMode: "plain" });
24323
24568
  try {
24324
- await runHeartbeatNow(chatId);
24569
+ await triggerHb();
24325
24570
  await channel.sendText(chatId, "\u2705 Heartbeat check complete.", { parseMode: "plain" });
24326
24571
  } catch (err) {
24327
24572
  await channel.sendText(chatId, `\u274C Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`, { parseMode: "plain" });
@@ -24571,7 +24816,6 @@ var init_callbacks = __esm({
24571
24816
  init_discover();
24572
24817
  init_profile();
24573
24818
  init_profile();
24574
- init_heartbeat2();
24575
24819
  init_store5();
24576
24820
  init_summarize();
24577
24821
  init_inject();
@@ -25748,7 +25992,7 @@ function startSingleJob(job) {
25748
25992
  }
25749
25993
  }
25750
25994
  function stopJobTimer(id) {
25751
- const timer = activeTimers2.get(id);
25995
+ const timer = activeTimers.get(id);
25752
25996
  if (!timer) return;
25753
25997
  if (timer instanceof Cron) {
25754
25998
  timer.stop();
@@ -25756,7 +26000,7 @@ function stopJobTimer(id) {
25756
26000
  clearTimeout(timer);
25757
26001
  clearInterval(timer);
25758
26002
  }
25759
- activeTimers2.delete(id);
26003
+ activeTimers.delete(id);
25760
26004
  }
25761
26005
  function cancelJob(id) {
25762
26006
  stopJobTimer(id);
@@ -25803,7 +26047,7 @@ function startCronJob(job) {
25803
26047
  await executeJob(fresh);
25804
26048
  }
25805
26049
  });
25806
- activeTimers2.set(job.id, cronInstance);
26050
+ activeTimers.set(job.id, cronInstance);
25807
26051
  const nextRun = cronInstance.nextRun();
25808
26052
  if (nextRun) {
25809
26053
  updateJobNextRun(job.id, nextRun.toISOString());
@@ -25824,9 +26068,9 @@ function startOneShot(job) {
25824
26068
  await executeJob(fresh);
25825
26069
  }
25826
26070
  updateJobEnabled(jobId, false);
25827
- activeTimers2.delete(jobId);
26071
+ activeTimers.delete(jobId);
25828
26072
  }, Math.max(0, delay));
25829
- activeTimers2.set(job.id, timer);
26073
+ activeTimers.set(job.id, timer);
25830
26074
  updateJobNextRun(job.id, job.atTime);
25831
26075
  }
25832
26076
  function startInterval(job) {
@@ -25839,7 +26083,7 @@ function startInterval(job) {
25839
26083
  updateJobNextRun(jobId, new Date(Date.now() + everyMs).toISOString());
25840
26084
  }
25841
26085
  }, everyMs);
25842
- activeTimers2.set(job.id, interval);
26086
+ activeTimers.set(job.id, interval);
25843
26087
  updateJobNextRun(job.id, new Date(Date.now() + everyMs).toISOString());
25844
26088
  }
25845
26089
  async function executeJob(job) {
@@ -25850,7 +26094,7 @@ async function executeJob(job) {
25850
26094
  return;
25851
26095
  }
25852
26096
  runningJobs.add(job.id);
25853
- const timer = activeTimers2.get(job.id);
26097
+ const timer = activeTimers.get(job.id);
25854
26098
  if (timer instanceof Cron) {
25855
26099
  const nextRun = timer.nextRun();
25856
26100
  if (nextRun) updateJobNextRun(job.id, nextRun.toISOString());
@@ -25944,6 +26188,24 @@ async function runWithRetry(job, model2, runId, t0) {
25944
26188
  }
25945
26189
  return { text: formatNightlySummary2(allInsights, totalPending) };
25946
26190
  }
26191
+ if (job.jobType === "heartbeat") {
26192
+ const { assembleHeartbeatPrompt: assembleHeartbeatPrompt2, HEARTBEAT_OK: HEARTBEAT_OK2 } = await Promise.resolve().then(() => (init_heartbeat2(), heartbeat_exports));
26193
+ const hbPrompt = assembleHeartbeatPrompt2(job.chatId);
26194
+ const hbTimeoutMs = job.timeout ? job.timeout * 1e3 : 12e4;
26195
+ const response = await askAgent(chatId, hbPrompt, {
26196
+ bootstrapTier: "heartbeat",
26197
+ backend: resolveJobBackendId(job),
26198
+ model: model2,
26199
+ timeoutMs: hbTimeoutMs
26200
+ });
26201
+ const trimmed = response.text.trim();
26202
+ if (trimmed === HEARTBEAT_OK2 || trimmed.startsWith(HEARTBEAT_OK2)) {
26203
+ log(`[heartbeat] Job #${job.id}: nothing to report`);
26204
+ return { text: "", usage: response.usage };
26205
+ }
26206
+ const cleanText = response.text.replace(/\[REACT:[^\]]*\]/g, "").replace(/\[SEND_FILE:[^\]]*\]/g, "").replace(/\[GENERATE_IMAGE:[^\]]*\]/g, "").replace(/\[HISTORY_SEARCH:[^\]]*\]/g, "").replace(/\[UPDATE_USER:[^\]]*\]/g, "").trim();
26207
+ return { text: cleanText, usage: response.usage };
26208
+ }
25947
26209
  if (job.thinking && job.thinking !== "auto") {
25948
26210
  setThinkingLevel(chatId, job.thinking);
25949
26211
  }
@@ -26037,13 +26299,13 @@ function resolveJobModel(job) {
26037
26299
  }
26038
26300
  }
26039
26301
  function shutdownScheduler() {
26040
- for (const [id] of activeTimers2) {
26302
+ for (const [id] of activeTimers) {
26041
26303
  stopJobTimer(id);
26042
26304
  }
26043
26305
  stopHealthMonitor2();
26044
26306
  log("[scheduler] Shutdown complete");
26045
26307
  }
26046
- var activeTimers2, runningJobs;
26308
+ var activeTimers, runningJobs;
26047
26309
  var init_cron = __esm({
26048
26310
  "src/scheduler/cron.ts"() {
26049
26311
  "use strict";
@@ -26054,7 +26316,7 @@ var init_cron = __esm({
26054
26316
  init_delivery();
26055
26317
  init_retry();
26056
26318
  init_health2();
26057
- activeTimers2 = /* @__PURE__ */ new Map();
26319
+ activeTimers = /* @__PURE__ */ new Map();
26058
26320
  runningJobs = /* @__PURE__ */ new Set();
26059
26321
  }
26060
26322
  });
@@ -26200,6 +26462,16 @@ var init_wrap_backend = __esm({
26200
26462
  maxConcurrentSessions: 4,
26201
26463
  specialties: ["multi-provider", "code-generation", "analysis", "planning", "debugging"],
26202
26464
  mcpInjection: "config-file"
26465
+ },
26466
+ ollama: {
26467
+ supportsSessionResume: false,
26468
+ supportsMcp: false,
26469
+ // HTTP-based adapter — no CLI MCP injection
26470
+ supportsNdjson: false,
26471
+ supportsPermissionModes: false,
26472
+ maxConcurrentSessions: 8,
26473
+ specialties: ["local-inference", "summarization"],
26474
+ mcpInjection: "add-remove"
26203
26475
  }
26204
26476
  };
26205
26477
  }
@@ -26729,52 +27001,7 @@ ${body.replace(/<[^>]*>/g, "").trim()}</code>
26729
27001
  });
26730
27002
 
26731
27003
  // src/channels/telegram.ts
26732
- import { API_CONSTANTS, Bot, GrammyError, InlineKeyboard, InputFile } from "grammy";
26733
- function tripCircuitBreaker(retrySec) {
26734
- const until = Date.now() + retrySec * 1e3;
26735
- if (until > circuitBreakerUntil) {
26736
- circuitBreakerUntil = until;
26737
- warn(`[telegram] Circuit breaker tripped \u2014 blocking ALL Telegram API calls for ${retrySec}s`);
26738
- }
26739
- }
26740
- function isCircuitBreakerActive() {
26741
- return Date.now() < circuitBreakerUntil;
26742
- }
26743
- function isRateLimitError(err) {
26744
- return err instanceof GrammyError && err.error_code === 429;
26745
- }
26746
- async function withRetry(label2, fn) {
26747
- for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
26748
- if (isCircuitBreakerActive()) {
26749
- throw new GrammyError(
26750
- `Circuit breaker active \u2014 skipping ${label2}`,
26751
- { ok: false, error_code: 429, description: "Rate limited (circuit breaker)" },
26752
- label2,
26753
- {}
26754
- );
26755
- }
26756
- try {
26757
- return await fn();
26758
- } catch (err) {
26759
- if (err instanceof GrammyError && err.error_code === 429) {
26760
- const retrySec = err.parameters?.retry_after ?? FALLBACK_RETRY_SEC;
26761
- tripCircuitBreaker(retrySec);
26762
- if (retrySec > GIVE_UP_THRESHOLD_SEC) {
26763
- warn(`[telegram] 429 on ${label2} \u2014 retry_after ${retrySec}s exceeds threshold, giving up`);
26764
- throw err;
26765
- }
26766
- if (attempt < MAX_RETRIES2) {
26767
- warn(`[telegram] 429 on ${label2} (attempt ${attempt + 1}/${MAX_RETRIES2}) \u2014 retrying in ${retrySec}s`);
26768
- await new Promise((r) => setTimeout(r, retrySec * 1e3));
26769
- continue;
26770
- }
26771
- warn(`[telegram] 429 on ${label2} \u2014 exhausted ${MAX_RETRIES2} retries, giving up`);
26772
- }
26773
- throw err;
26774
- }
26775
- }
26776
- throw new Error(`withRetry: unreachable`);
26777
- }
27004
+ import { API_CONSTANTS, Bot, GrammyError as GrammyError2, InlineKeyboard, InputFile } from "grammy";
26778
27005
  function isFastPathMessage(msg) {
26779
27006
  if (msg.type === "command" && msg.command && FAST_PATH_COMMANDS.has(msg.command)) {
26780
27007
  return true;
@@ -26797,17 +27024,14 @@ function numericChatId(chatId) {
26797
27024
  const raw = chatId.includes(":") ? chatId.split(":").pop() : chatId;
26798
27025
  return parseInt(raw);
26799
27026
  }
26800
- var MAX_RETRIES2, FALLBACK_RETRY_SEC, GIVE_UP_THRESHOLD_SEC, circuitBreakerUntil, FAST_PATH_COMMANDS, TelegramChannel;
27027
+ var FAST_PATH_COMMANDS, TelegramChannel;
26801
27028
  var init_telegram2 = __esm({
26802
27029
  "src/channels/telegram.ts"() {
26803
27030
  "use strict";
26804
27031
  init_telegram();
26805
27032
  init_log();
26806
27033
  init_store5();
26807
- MAX_RETRIES2 = 2;
26808
- FALLBACK_RETRY_SEC = 3;
26809
- GIVE_UP_THRESHOLD_SEC = 30;
26810
- circuitBreakerUntil = 0;
27034
+ init_telegram_throttle();
26811
27035
  FAST_PATH_COMMANDS = /* @__PURE__ */ new Set(["stop", "status", "new", "newchat"]);
26812
27036
  TelegramChannel = class {
26813
27037
  name = "telegram";
@@ -26818,6 +27042,7 @@ var init_telegram2 = __esm({
26818
27042
  agentMessageIds = /* @__PURE__ */ new Map();
26819
27043
  // messageId → chatId
26820
27044
  reactionHandlers = [];
27045
+ throttle;
26821
27046
  constructor() {
26822
27047
  const token = process.env.TELEGRAM_BOT_TOKEN;
26823
27048
  if (!token) {
@@ -26833,6 +27058,17 @@ var init_telegram2 = __esm({
26833
27058
  this.primaryChatId = ids[0];
26834
27059
  this.allowedChatIds = new Set(ids);
26835
27060
  this.bot = new Bot(token);
27061
+ this.throttle = new TelegramThrottle();
27062
+ this.throttle.setResumeNotifier(async (chatId, pausedSec, queuedCount) => {
27063
+ try {
27064
+ await this.bot.api.sendMessage(
27065
+ numericChatId(chatId),
27066
+ `\u26A0\uFE0F Rate-limited by Telegram for ${pausedSec}s \u2014 delivering ${queuedCount} pending message(s) now.`
27067
+ );
27068
+ } catch (err) {
27069
+ warn("[telegram] Resume notification failed:", err instanceof Error ? err.message : err);
27070
+ }
27071
+ });
26836
27072
  }
26837
27073
  /** The first ID in ALLOWED_CHAT_ID — used for heartbeat, notifications, etc. */
26838
27074
  getPrimaryChatId() {
@@ -26968,11 +27204,17 @@ var init_telegram2 = __esm({
26968
27204
  }
26969
27205
  const data = ctx.callbackQuery.data;
26970
27206
  const messageId = ctx.callbackQuery.message?.message_id?.toString();
27207
+ const threadId = ctx.callbackQuery.message?.message_thread_id;
26971
27208
  ctx.answerCallbackQuery().catch(() => {
26972
27209
  });
26973
27210
  (async () => {
27211
+ let ch = this;
27212
+ if (threadId) {
27213
+ const { withThread: withThread2 } = await Promise.resolve().then(() => (init_thread_wrapper(), thread_wrapper_exports));
27214
+ ch = withThread2(this, threadId);
27215
+ }
26974
27216
  for (const handler2 of this.callbackHandlers) {
26975
- await handler2(chatId, data, this, messageId);
27217
+ await handler2(chatId, data, ch, messageId);
26976
27218
  }
26977
27219
  })().catch((err) => {
26978
27220
  error("[telegram] Callback handler error:", err);
@@ -27002,7 +27244,7 @@ var init_telegram2 = __esm({
27002
27244
  });
27003
27245
  this.bot.catch((botError) => {
27004
27246
  const err = botError.error;
27005
- if (err instanceof GrammyError) {
27247
+ if (err instanceof GrammyError2) {
27006
27248
  error(`[telegram] Grammy error ${err.error_code}: ${err.description}`);
27007
27249
  } else {
27008
27250
  error("[telegram] Unhandled error:", err);
@@ -27022,6 +27264,7 @@ var init_telegram2 = __esm({
27022
27264
  await this.bot.stop();
27023
27265
  }
27024
27266
  async sendTyping(chatId, threadId) {
27267
+ if (this.throttle.isPaused()) return;
27025
27268
  try {
27026
27269
  await this.bot.api.sendChatAction(numericChatId(chatId), "typing", {
27027
27270
  ...threadId ? { message_thread_id: threadId } : {}
@@ -27036,7 +27279,8 @@ var init_telegram2 = __esm({
27036
27279
  if (parseMode === "plain") {
27037
27280
  const plainChunks = splitMessage(sanitizeForTelegram(text));
27038
27281
  for (const chunk of plainChunks) {
27039
- const sent = await withRetry(
27282
+ const sent = await this.throttle.send(
27283
+ chatId,
27040
27284
  "sendText:plain",
27041
27285
  () => this.bot.api.sendMessage(numericChatId(chatId), chunk, { ...threadOpts, ...replyOpts })
27042
27286
  );
@@ -27048,7 +27292,8 @@ var init_telegram2 = __esm({
27048
27292
  const chunks = splitMessage(formatted);
27049
27293
  for (const chunk of chunks) {
27050
27294
  try {
27051
- const sent = await withRetry(
27295
+ const sent = await this.throttle.send(
27296
+ chatId,
27052
27297
  "sendText:html",
27053
27298
  () => this.bot.api.sendMessage(numericChatId(chatId), chunk, {
27054
27299
  parse_mode: "HTML",
@@ -27058,9 +27303,9 @@ var init_telegram2 = __esm({
27058
27303
  );
27059
27304
  this.trackAgentMessage(sent.message_id, chatId);
27060
27305
  } catch (err) {
27061
- if (isRateLimitError(err)) throw err;
27062
27306
  warn("[telegram] sendText HTML failed, falling back to plain:", err instanceof Error ? err.message : err);
27063
- const sent = await withRetry(
27307
+ const sent = await this.throttle.send(
27308
+ chatId,
27064
27309
  "sendText:fallback",
27065
27310
  () => this.bot.api.sendMessage(
27066
27311
  numericChatId(chatId),
@@ -27073,7 +27318,8 @@ var init_telegram2 = __esm({
27073
27318
  }
27074
27319
  }
27075
27320
  async sendVoice(chatId, audioBuffer, fileName, threadId) {
27076
- await withRetry(
27321
+ await this.throttle.send(
27322
+ chatId,
27077
27323
  "sendVoice",
27078
27324
  () => this.bot.api.sendVoice(
27079
27325
  numericChatId(chatId),
@@ -27083,7 +27329,8 @@ var init_telegram2 = __esm({
27083
27329
  );
27084
27330
  }
27085
27331
  async sendFile(chatId, buffer, fileName, _mimeType, threadId) {
27086
- await withRetry(
27332
+ await this.throttle.send(
27333
+ chatId,
27087
27334
  "sendFile",
27088
27335
  () => this.bot.api.sendDocument(
27089
27336
  numericChatId(chatId),
@@ -27103,7 +27350,8 @@ var init_telegram2 = __esm({
27103
27350
  const formatted = sanitizeForTelegram(parseMode === "html" ? text : parseMode === "plain" ? text : formatForTelegram(text));
27104
27351
  const threadOpts = threadId ? { message_thread_id: threadId } : {};
27105
27352
  const opts = parseMode === "plain" ? { ...threadOpts } : { parse_mode: "HTML", ...threadOpts };
27106
- const msg = await withRetry(
27353
+ const msg = await this.throttle.send(
27354
+ chatId,
27107
27355
  "sendTextReturningId",
27108
27356
  () => this.bot.api.sendMessage(numericChatId(chatId), formatted, opts)
27109
27357
  );
@@ -27116,7 +27364,8 @@ var init_telegram2 = __esm({
27116
27364
  async editText(chatId, messageId, text, parseMode) {
27117
27365
  const formatted = sanitizeForTelegram(parseMode === "html" ? text : formatForTelegram(text));
27118
27366
  try {
27119
- await withRetry(
27367
+ await this.throttle.send(
27368
+ chatId,
27120
27369
  "editText:html",
27121
27370
  () => this.bot.api.editMessageText(numericChatId(chatId), parseInt(messageId), formatted, {
27122
27371
  parse_mode: "HTML"
@@ -27124,10 +27373,10 @@ var init_telegram2 = __esm({
27124
27373
  );
27125
27374
  return true;
27126
27375
  } catch (err) {
27127
- if (isRateLimitError(err)) return false;
27128
27376
  warn("[telegram] editText HTML failed, trying plain fallback:", err instanceof Error ? err.message : err);
27129
27377
  try {
27130
- await withRetry(
27378
+ await this.throttle.send(
27379
+ chatId,
27131
27380
  "editText:fallback",
27132
27381
  () => this.bot.api.editMessageText(
27133
27382
  numericChatId(chatId),
@@ -27155,7 +27404,8 @@ var init_telegram2 = __esm({
27155
27404
  }
27156
27405
  const formatted = sanitizeForTelegram(formatForTelegram(text));
27157
27406
  try {
27158
- await withRetry(
27407
+ await this.throttle.send(
27408
+ chatId,
27159
27409
  "editKeyboard:html",
27160
27410
  () => this.bot.api.editMessageText(numericChatId(chatId), parseInt(messageId), formatted, {
27161
27411
  parse_mode: "HTML",
@@ -27164,9 +27414,9 @@ var init_telegram2 = __esm({
27164
27414
  );
27165
27415
  return true;
27166
27416
  } catch (err) {
27167
- if (isRateLimitError(err)) return false;
27168
27417
  try {
27169
- await withRetry(
27418
+ await this.throttle.send(
27419
+ chatId,
27170
27420
  "editKeyboard:plain",
27171
27421
  () => this.bot.api.editMessageText(
27172
27422
  numericChatId(chatId),
@@ -27206,7 +27456,8 @@ var init_telegram2 = __esm({
27206
27456
  const formatted = sanitizeForTelegram(formatForTelegram(safeText));
27207
27457
  const threadOpts = threadId ? { message_thread_id: threadId } : {};
27208
27458
  try {
27209
- const msg = await withRetry(
27459
+ const msg = await this.throttle.send(
27460
+ chatId,
27210
27461
  "sendKeyboard",
27211
27462
  () => this.bot.api.sendMessage(numericChatId(chatId), formatted, {
27212
27463
  parse_mode: "HTML",
@@ -27216,14 +27467,11 @@ var init_telegram2 = __esm({
27216
27467
  );
27217
27468
  return msg.message_id.toString();
27218
27469
  } catch (err) {
27219
- if (isRateLimitError(err)) {
27220
- warn(`[telegram] sendKeyboard rate-limited, skipping all fallbacks`);
27221
- return void 0;
27222
- }
27223
27470
  error(`[telegram] sendKeyboard failed (chat=${chatId}, textLen=${text.length}):`, err);
27224
27471
  try {
27225
27472
  const escaped = safeText.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
27226
- const retryMsg = await withRetry(
27473
+ const retryMsg = await this.throttle.send(
27474
+ chatId,
27227
27475
  "sendKeyboard:plain",
27228
27476
  () => this.bot.api.sendMessage(numericChatId(chatId), escaped, {
27229
27477
  reply_markup: keyboard,
@@ -27232,13 +27480,13 @@ var init_telegram2 = __esm({
27232
27480
  );
27233
27481
  return retryMsg.message_id.toString();
27234
27482
  } catch (plainErr) {
27235
- if (isRateLimitError(plainErr)) return void 0;
27236
27483
  error(`[telegram] sendKeyboard plain retry also failed:`, plainErr);
27237
27484
  }
27238
27485
  try {
27239
27486
  const plainText = safeText.replace(/<[^>]+>/g, "");
27240
27487
  if (plainText.trim()) {
27241
- await withRetry(
27488
+ await this.throttle.send(
27489
+ chatId,
27242
27490
  "sendKeyboard:text-rescue",
27243
27491
  () => this.bot.api.sendMessage(numericChatId(chatId), plainText, { ...threadOpts })
27244
27492
  );
@@ -27246,7 +27494,8 @@ var init_telegram2 = __esm({
27246
27494
  } catch {
27247
27495
  }
27248
27496
  try {
27249
- const fallbackMsg = await withRetry(
27497
+ const fallbackMsg = await this.throttle.send(
27498
+ chatId,
27250
27499
  "sendKeyboard:fallback",
27251
27500
  () => this.bot.api.sendMessage(numericChatId(chatId), "\u2B06\uFE0F (see above for details)", {
27252
27501
  reply_markup: keyboard,
@@ -27580,7 +27829,7 @@ var init_bootstrap = __esm({
27580
27829
  });
27581
27830
 
27582
27831
  // src/memory/migrate-embeddings.ts
27583
- function sleep(ms) {
27832
+ function sleep2(ms) {
27584
27833
  return new Promise((resolve) => setTimeout(resolve, ms));
27585
27834
  }
27586
27835
  async function migrateEmbeddings() {
@@ -27607,7 +27856,7 @@ async function migrateEmbeddings() {
27607
27856
  }
27608
27857
  }
27609
27858
  log(`[migrate-embeddings] Embedded ${memoriesEmbedded} memories so far...`);
27610
- if (batch.length === BATCH_SIZE) await sleep(BATCH_DELAY_MS);
27859
+ if (batch.length === BATCH_SIZE) await sleep2(BATCH_DELAY_MS);
27611
27860
  }
27612
27861
  while (true) {
27613
27862
  const batch = getSessionSummariesWithoutEmbeddings(BATCH_SIZE);
@@ -27625,7 +27874,7 @@ async function migrateEmbeddings() {
27625
27874
  }
27626
27875
  }
27627
27876
  log(`[migrate-embeddings] Embedded ${sessionsEmbedded} session summaries so far...`);
27628
- if (batch.length === BATCH_SIZE) await sleep(BATCH_DELAY_MS);
27877
+ if (batch.length === BATCH_SIZE) await sleep2(BATCH_DELAY_MS);
27629
27878
  }
27630
27879
  log(`[migrate-embeddings] Migration complete: ${memoriesEmbedded} memories, ${sessionsEmbedded} session summaries`);
27631
27880
  return { memories: memoriesEmbedded, sessions: sessionsEmbedded };
@@ -28334,11 +28583,11 @@ async function main() {
28334
28583
  } catch {
28335
28584
  }
28336
28585
  try {
28337
- const { getAdapter: getAdapter5, probeBackendAvailability: probeBackendAvailability2 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
28586
+ const { getAdapter: getAdapter4, probeBackendAvailability: probeBackendAvailability2 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
28338
28587
  await probeBackendAvailability2();
28339
- const claude = getAdapter5("claude");
28588
+ const claude = getAdapter4("claude");
28340
28589
  if ("getAuthMode" in claude) claude.getAuthMode();
28341
- const cursor = getAdapter5("cursor");
28590
+ const cursor = getAdapter4("cursor");
28342
28591
  if ("probeTier" in cursor) cursor.probeTier();
28343
28592
  } catch {
28344
28593
  }
@@ -28358,8 +28607,8 @@ async function main() {
28358
28607
  }
28359
28608
  if (modelCount > 0) {
28360
28609
  try {
28361
- const { getAdapter: getAdapter5 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
28362
- const adapter = getAdapter5("ollama");
28610
+ const { getAdapter: getAdapter4 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
28611
+ const adapter = getAdapter4("ollama");
28363
28612
  if ("refreshModelCatalog" in adapter) {
28364
28613
  adapter.refreshModelCatalog();
28365
28614
  }
@@ -29144,7 +29393,7 @@ async function statusCommand(globalOpts, localOpts) {
29144
29393
  const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
29145
29394
  const readDb = openDatabaseReadOnly2();
29146
29395
  const chatId = resolveChatId2(globalOpts);
29147
- const { getAdapterForChat: getAdapterForChat3, getAdapter: getAdapter5, getAllAdapters: getAllAdapters5 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
29396
+ const { getAdapterForChat: getAdapterForChat2, getAdapter: getAdapter4, getAllAdapters: getAllAdapters5 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
29148
29397
  let backend2 = null;
29149
29398
  let modelName = "not set";
29150
29399
  let contextMax = 2e5;
@@ -29152,7 +29401,7 @@ async function statusCommand(globalOpts, localOpts) {
29152
29401
  const backendRow = readDb.prepare("SELECT backend FROM chat_backend WHERE chat_id = ?").get(chatId);
29153
29402
  if (backendRow) {
29154
29403
  try {
29155
- const a = getAdapter5(backendRow.backend);
29404
+ const a = getAdapter4(backendRow.backend);
29156
29405
  backend2 = { id: a.id, displayName: a.displayName };
29157
29406
  } catch {
29158
29407
  backend2 = { id: backendRow.backend, displayName: backendRow.backend };
@@ -29165,7 +29414,7 @@ async function statusCommand(globalOpts, localOpts) {
29165
29414
  if (modelRow) modelName = modelRow.model;
29166
29415
  else if (backend2) {
29167
29416
  try {
29168
- modelName = getAdapter5(backend2.id).defaultModel;
29417
+ modelName = getAdapter4(backend2.id).defaultModel;
29169
29418
  } catch {
29170
29419
  }
29171
29420
  }
@@ -29632,9 +29881,9 @@ async function sessionLogsList(opts) {
29632
29881
  `));
29633
29882
  console.log(` ${"Filename".padEnd(55)} ${"Size".padStart(8)} Chat ID`);
29634
29883
  console.log(` ${"\u2500".repeat(55)} ${"\u2500".repeat(8)} ${"\u2500".repeat(15)}`);
29635
- for (const log4 of logs) {
29636
- const size = log4.sizeBytes < 1024 ? `${log4.sizeBytes}B` : log4.sizeBytes < 1024 * 1024 ? `${(log4.sizeBytes / 1024).toFixed(1)}K` : `${(log4.sizeBytes / 1024 / 1024).toFixed(1)}M`;
29637
- console.log(` ${log4.filename.padEnd(55)} ${size.padStart(8)} ${log4.chatId}`);
29884
+ for (const log5 of logs) {
29885
+ const size = log5.sizeBytes < 1024 ? `${log5.sizeBytes}B` : log5.sizeBytes < 1024 * 1024 ? `${(log5.sizeBytes / 1024).toFixed(1)}K` : `${(log5.sizeBytes / 1024 / 1024).toFixed(1)}M`;
29886
+ console.log(` ${log5.filename.padEnd(55)} ${size.padStart(8)} ${log5.chatId}`);
29638
29887
  }
29639
29888
  console.log(muted(`
29640
29889
  Path: ${SESSION_LOGS_PATH}`));
@@ -30842,7 +31091,7 @@ import { existsSync as existsSync38 } from "fs";
30842
31091
  async function modelList(globalOpts) {
30843
31092
  const chatId = resolveChatId2(globalOpts);
30844
31093
  const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
30845
- const { getAdapter: getAdapter5, getAllAdapters: getAllAdapters5 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
31094
+ const { getAdapter: getAdapter4, getAllAdapters: getAllAdapters5 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
30846
31095
  let backendId = "claude";
30847
31096
  if (existsSync38(DB_PATH)) {
30848
31097
  const readDb = openDatabaseReadOnly2();
@@ -30854,7 +31103,7 @@ async function modelList(globalOpts) {
30854
31103
  readDb.close();
30855
31104
  }
30856
31105
  try {
30857
- const adapter = getAdapter5(backendId);
31106
+ const adapter = getAdapter4(backendId);
30858
31107
  const models = Object.entries(adapter.availableModels).map(([id, info]) => ({
30859
31108
  id,
30860
31109
  label: info.label,
@@ -32178,8 +32427,8 @@ var init_voice = __esm({
32178
32427
  });
32179
32428
 
32180
32429
  // src/cli/commands/heartbeat.ts
32181
- var heartbeat_exports = {};
32182
- __export(heartbeat_exports, {
32430
+ var heartbeat_exports2 = {};
32431
+ __export(heartbeat_exports2, {
32183
32432
  heartbeatGet: () => heartbeatGet,
32184
32433
  heartbeatSet: () => heartbeatSet
32185
32434
  });
@@ -34626,11 +34875,11 @@ voice.command("set <on|off>").description("Set voice on or off").action(async (v
34626
34875
  });
34627
34876
  var heartbeat = program.command("heartbeat").description("Proactive awareness");
34628
34877
  heartbeat.command("get").description("Show heartbeat status and config").action(async () => {
34629
- const { heartbeatGet: heartbeatGet2 } = await Promise.resolve().then(() => (init_heartbeat3(), heartbeat_exports));
34878
+ const { heartbeatGet: heartbeatGet2 } = await Promise.resolve().then(() => (init_heartbeat3(), heartbeat_exports2));
34630
34879
  await heartbeatGet2(program.opts());
34631
34880
  });
34632
34881
  heartbeat.command("set <subcommand> [value]").description("Configure heartbeat (on/off/interval/hours/backend/model/thinking/fallbacks)").action(async (subcommand, value) => {
34633
- const { heartbeatSet: heartbeatSet2 } = await Promise.resolve().then(() => (init_heartbeat3(), heartbeat_exports));
34882
+ const { heartbeatSet: heartbeatSet2 } = await Promise.resolve().then(() => (init_heartbeat3(), heartbeat_exports2));
34634
34883
  await heartbeatSet2(program.opts(), subcommand, value);
34635
34884
  });
34636
34885
  var summarizer = program.command("summarizer").description("Session summarizer config");