cc-claw 0.29.1 → 0.29.3

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 +304 -87
  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.29.1" : (() => {
36
+ VERSION = true ? "0.29.3" : (() => {
37
37
  try {
38
38
  return JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
39
39
  } catch {
@@ -2813,6 +2813,7 @@ __export(chat_settings_exports, {
2813
2813
  GLOBAL_SUMMARIZER_SENTINEL: () => GLOBAL_SUMMARIZER_SENTINEL,
2814
2814
  clearAgentMode: () => clearAgentMode,
2815
2815
  clearAllPaidSlots: () => clearAllPaidSlots,
2816
+ clearAllSummarizerOverrides: () => clearAllSummarizerOverrides,
2816
2817
  clearChatPaidSlots: () => clearChatPaidSlots,
2817
2818
  clearCwd: () => clearCwd,
2818
2819
  clearExecMode: () => clearExecMode,
@@ -2820,6 +2821,7 @@ __export(chat_settings_exports, {
2820
2821
  clearModelMap: () => clearModelMap,
2821
2822
  clearSummarizer: () => clearSummarizer,
2822
2823
  clearThinkingLevel: () => clearThinkingLevel,
2824
+ countSummarizerOverrides: () => countSummarizerOverrides,
2823
2825
  deleteBookmark: () => deleteBookmark,
2824
2826
  determineEscalationTarget: () => determineEscalationTarget,
2825
2827
  findBookmarksByPrefix: () => findBookmarksByPrefix,
@@ -2840,6 +2842,7 @@ __export(chat_settings_exports, {
2840
2842
  getShowThinkingUi: () => getShowThinkingUi,
2841
2843
  getSkillSuggestionsEnabled: () => getSkillSuggestionsEnabled,
2842
2844
  getSummarizer: () => getSummarizer,
2845
+ getSummarizerWithSource: () => getSummarizerWithSource,
2843
2846
  getThinkingLevel: () => getThinkingLevel,
2844
2847
  getToolsMap: () => getToolsMap,
2845
2848
  getVerboseLevel: () => getVerboseLevel,
@@ -3184,6 +3187,35 @@ function setSummarizer(chatId, backend2, model2) {
3184
3187
  function clearSummarizer(chatId) {
3185
3188
  getDb().prepare("DELETE FROM chat_summarizer WHERE chat_id = ?").run(chatId);
3186
3189
  }
3190
+ function clearAllSummarizerOverrides() {
3191
+ const result = getDb().prepare(
3192
+ "DELETE FROM chat_summarizer WHERE chat_id != ?"
3193
+ ).run(GLOBAL_SUMMARIZER_SENTINEL);
3194
+ return result.changes;
3195
+ }
3196
+ function getSummarizerWithSource(chatId) {
3197
+ const perChat = getDb().prepare(
3198
+ "SELECT backend, model FROM chat_summarizer WHERE chat_id = ?"
3199
+ ).get(chatId);
3200
+ const globalRow = getDb().prepare(
3201
+ "SELECT backend, model FROM chat_summarizer WHERE chat_id = ?"
3202
+ ).get(GLOBAL_SUMMARIZER_SENTINEL);
3203
+ const hasPerChat = perChat && (perChat.backend || perChat.model);
3204
+ const globalConfig = globalRow && (globalRow.backend || globalRow.model) ? globalRow : { backend: null, model: null };
3205
+ if (hasPerChat) {
3206
+ return { config: perChat, source: "per-chat", globalConfig };
3207
+ }
3208
+ if (globalConfig.backend || globalConfig.model) {
3209
+ return { config: globalConfig, source: "global", globalConfig };
3210
+ }
3211
+ return { config: { backend: null, model: null }, source: "auto", globalConfig };
3212
+ }
3213
+ function countSummarizerOverrides() {
3214
+ const row = getDb().prepare(
3215
+ "SELECT COUNT(*) as cnt FROM chat_summarizer WHERE chat_id != ?"
3216
+ ).get(GLOBAL_SUMMARIZER_SENTINEL);
3217
+ return row.cnt;
3218
+ }
3187
3219
  function getAgentMode(chatId) {
3188
3220
  const row = getDb().prepare("SELECT mode FROM chat_agent_mode WHERE chat_id = ?").get(chatId);
3189
3221
  return row?.mode ?? "auto";
@@ -3325,7 +3357,7 @@ function getUsage(chatId) {
3325
3357
  }
3326
3358
  function addUsage(chatId, input, output2, cacheRead, model2, backend2, contextSize) {
3327
3359
  const db3 = getDb();
3328
- const finalContextSize = contextSize ?? input + cacheRead;
3360
+ const finalContextSize = contextSize === null ? 0 : contextSize ?? input + cacheRead;
3329
3361
  db3.prepare(`
3330
3362
  INSERT INTO chat_usage (chat_id, input_tokens, output_tokens, cache_read_tokens, request_count, last_input_tokens, last_cache_read_tokens, context_size, updated_at)
3331
3363
  VALUES (?, ?, ?, ?, 1, ?, ?, ?, datetime('now'))
@@ -4593,6 +4625,12 @@ var init_session_log = __esm({
4593
4625
  });
4594
4626
 
4595
4627
  // src/memory/api-context.ts
4628
+ var api_context_exports = {};
4629
+ __export(api_context_exports, {
4630
+ buildApiMessages: () => buildApiMessages,
4631
+ estimateContextUsage: () => estimateContextUsage,
4632
+ estimateTokens: () => estimateTokens
4633
+ });
4596
4634
  import { getEncoding } from "js-tiktoken";
4597
4635
  function estimateTokens(text) {
4598
4636
  return enc.encode(text).length;
@@ -4621,7 +4659,7 @@ async function buildApiMessages(chatId, userMessage, systemPrompt, contextWindow
4621
4659
  return { role: "assistant", content: entry.text };
4622
4660
  });
4623
4661
  const currentUserMessage = { role: "user", content: userMessage };
4624
- const tokenBudget = Math.floor(contextWindow * 0.85);
4662
+ const tokenBudget = Math.floor(contextWindow * 0.95);
4625
4663
  const fixedMessages = [systemMessage, currentUserMessage];
4626
4664
  const fixedTokens = fixedMessages.reduce((sum, m) => sum + enc.encode(typeof m.content === "string" ? m.content : JSON.stringify(m.content)).length, 0);
4627
4665
  const historyBudget = tokenBudget - fixedTokens;
@@ -4637,12 +4675,30 @@ async function buildApiMessages(chatId, userMessage, systemPrompt, contextWindow
4637
4675
  }
4638
4676
  return [systemMessage, ...truncatedHistory, currentUserMessage];
4639
4677
  }
4640
- var enc;
4678
+ function estimateContextUsage(chatId, contextWindow) {
4679
+ const logEntries = getLog(chatId);
4680
+ if (logEntries.length === 0) {
4681
+ estimateCache.delete(chatId);
4682
+ return { estimatedTokens: 0, contextWindow, percentage: 0 };
4683
+ }
4684
+ const cached = estimateCache.get(chatId);
4685
+ if (cached && cached.logSize === logEntries.length) {
4686
+ const percentage2 = contextWindow > 0 ? cached.tokens / contextWindow * 100 : 0;
4687
+ return { estimatedTokens: cached.tokens, contextWindow, percentage: percentage2 };
4688
+ }
4689
+ const text = logEntries.map((e) => e.text).join("\n");
4690
+ const estimatedTokens = estimateTokens(text);
4691
+ estimateCache.set(chatId, { logSize: logEntries.length, tokens: estimatedTokens });
4692
+ const percentage = contextWindow > 0 ? estimatedTokens / contextWindow * 100 : 0;
4693
+ return { estimatedTokens, contextWindow, percentage };
4694
+ }
4695
+ var enc, estimateCache;
4641
4696
  var init_api_context = __esm({
4642
4697
  "src/memory/api-context.ts"() {
4643
4698
  "use strict";
4644
4699
  init_session_log();
4645
4700
  enc = getEncoding("cl100k_base");
4701
+ estimateCache = /* @__PURE__ */ new Map();
4646
4702
  }
4647
4703
  });
4648
4704
 
@@ -4838,6 +4894,7 @@ __export(store_exports5, {
4838
4894
  clearAgentMode: () => clearAgentMode,
4839
4895
  clearAllPaidSlots: () => clearAllPaidSlots,
4840
4896
  clearAllSessions: () => clearAllSessions,
4897
+ clearAllSummarizerOverrides: () => clearAllSummarizerOverrides,
4841
4898
  clearBackendLimit: () => clearBackendLimit,
4842
4899
  clearChatBackendSlot: () => clearChatBackendSlot,
4843
4900
  clearChatGeminiSlot: () => clearChatGeminiSlot,
@@ -4852,6 +4909,7 @@ __export(store_exports5, {
4852
4909
  clearThinkingLevel: () => clearThinkingLevel,
4853
4910
  clearUsage: () => clearUsage,
4854
4911
  completeJobRun: () => completeJobRun,
4912
+ countSummarizerOverrides: () => countSummarizerOverrides,
4855
4913
  deleteBookmark: () => deleteBookmark,
4856
4914
  deleteMemoryById: () => deleteMemoryById,
4857
4915
  deleteSessionSummary: () => deleteSessionSummary,
@@ -4923,6 +4981,7 @@ __export(store_exports5, {
4923
4981
  getShowThinkingUi: () => getShowThinkingUi,
4924
4982
  getSkillSuggestionsEnabled: () => getSkillSuggestionsEnabled,
4925
4983
  getSummarizer: () => getSummarizer,
4984
+ getSummarizerWithSource: () => getSummarizerWithSource,
4926
4985
  getThinkingLevel: () => getThinkingLevel,
4927
4986
  getToolsMap: () => getToolsMap,
4928
4987
  getUnsummarizedChatIds: () => getUnsummarizedChatIds,
@@ -7581,19 +7640,20 @@ function is429(err) {
7581
7640
  function sleep(ms) {
7582
7641
  return new Promise((r) => setTimeout(r, ms));
7583
7642
  }
7584
- var PER_DM_INTERVAL_MS, PER_GROUP_INTERVAL_MS, GLOBAL_INTERVAL_MS, MAX_RETRIES2, RETRY_DELAY_MS, MAX_QUEUE_SIZE, EDIT_PRESSURE_THRESHOLD, MAX_PER_CHAT_QUEUE, MAX_TOTAL_PAUSE_MS, CIRCUIT_TRIP_THRESHOLD, CIRCUIT_TRIP_WINDOW_MS, CIRCUIT_COOLDOWN_STEP_SEC, CIRCUIT_RESET_WINDOW_MS, CircuitState, Priority, _activeThrottle, TelegramThrottle;
7643
+ var PER_DM_INTERVAL_MS, PER_GROUP_INTERVAL_MS, P0_PACING_MS, GLOBAL_INTERVAL_MS, MAX_RETRIES2, RETRY_DELAY_MS, MAX_QUEUE_SIZE, EDIT_PRESSURE_THRESHOLD, MAX_PER_CHAT_QUEUE, MAX_TOTAL_PAUSE_MS, CIRCUIT_TRIP_THRESHOLD, CIRCUIT_TRIP_WINDOW_MS, CIRCUIT_COOLDOWN_STEP_SEC, CIRCUIT_RESET_WINDOW_MS, CircuitState, Priority, _activeThrottle, TelegramThrottle;
7585
7644
  var init_telegram_throttle = __esm({
7586
7645
  "src/channels/telegram-throttle.ts"() {
7587
7646
  "use strict";
7588
7647
  init_log();
7589
7648
  PER_DM_INTERVAL_MS = 1e3;
7590
7649
  PER_GROUP_INTERVAL_MS = 3500;
7650
+ P0_PACING_MS = 150;
7591
7651
  GLOBAL_INTERVAL_MS = 100;
7592
7652
  MAX_RETRIES2 = 2;
7593
7653
  RETRY_DELAY_MS = 1e3;
7594
7654
  MAX_QUEUE_SIZE = 60;
7595
7655
  EDIT_PRESSURE_THRESHOLD = MAX_QUEUE_SIZE / 2;
7596
- MAX_PER_CHAT_QUEUE = 15;
7656
+ MAX_PER_CHAT_QUEUE = 30;
7597
7657
  MAX_TOTAL_PAUSE_MS = 5 * 60 * 1e3;
7598
7658
  CIRCUIT_TRIP_THRESHOLD = 3;
7599
7659
  CIRCUIT_TRIP_WINDOW_MS = 5 * 60 * 1e3;
@@ -7766,16 +7826,17 @@ var init_telegram_throttle = __esm({
7766
7826
  while (this.queue.length > 0) {
7767
7827
  while (this.isPaused()) {
7768
7828
  if (this.pauseStartedAt > 0 && Date.now() - this.pauseStartedAt > MAX_TOTAL_PAUSE_MS) {
7769
- warn(`[throttle] Max pause duration exceeded (${MAX_TOTAL_PAUSE_MS / 6e4}min), dropping ${this.queue.length} items`);
7829
+ warn(`[throttle] Max pause duration exceeded (${MAX_TOTAL_PAUSE_MS / 6e4}min), dropping ${this.queue.length} items (pause remains until ${new Date(this.pausedUntil).toISOString()})`);
7770
7830
  this.flushQueueWithError("Telegram rate limit exceeded max wait time");
7771
- this.pausedUntil = 0;
7772
- this.pauseStartedAt = 0;
7773
7831
  break;
7774
7832
  }
7775
7833
  const waitMs = Math.min(this.pausedUntil - Date.now(), 5e3);
7776
7834
  if (waitMs > 0) await sleep(waitMs);
7777
7835
  }
7778
7836
  if (this.queue.length === 0) break;
7837
+ if (!this.isPaused() && this.pauseStartedAt > 0) {
7838
+ this.pauseStartedAt = 0;
7839
+ }
7779
7840
  this.updateCircuitState();
7780
7841
  const item = this.selectNextItem();
7781
7842
  if (!item) {
@@ -7783,7 +7844,8 @@ var init_telegram_throttle = __esm({
7783
7844
  continue;
7784
7845
  }
7785
7846
  const lastChat = this.lastSendPerChat.get(item.chatId) ?? 0;
7786
- const chatWait = perChatInterval(item.chatId) - (Date.now() - lastChat);
7847
+ const interval = item.priority === 0 /* P0_CRITICAL */ ? P0_PACING_MS : perChatInterval(item.chatId);
7848
+ const chatWait = interval - (Date.now() - lastChat);
7787
7849
  if (chatWait > 0) await sleep(chatWait);
7788
7850
  const globalWait = GLOBAL_INTERVAL_MS - (Date.now() - this.lastGlobalSend);
7789
7851
  if (globalWait > 0) await sleep(globalWait);
@@ -7865,6 +7927,7 @@ var init_telegram_throttle = __esm({
7865
7927
  return await fn();
7866
7928
  } catch (err) {
7867
7929
  if (is429(err)) throw err;
7930
+ if (err instanceof GrammyError && err.error_code >= 400 && err.error_code < 500) throw err;
7868
7931
  if (attempt < MAX_RETRIES2 && err instanceof GrammyError) {
7869
7932
  warn(`[throttle] ${label2} attempt ${attempt + 1}/${MAX_RETRIES2} failed (${err.error_code}), retrying`);
7870
7933
  await sleep(RETRY_DELAY_MS);
@@ -13382,7 +13445,7 @@ async function summarizeWithFallbackChain(chatId, targetBackendId, excludeBacken
13382
13445
  const cap = getOllamaTranscriptCap(ollamaModel);
13383
13446
  const key = `${ollamaAdapter.id}:${ollamaModel}`;
13384
13447
  tried.add(key);
13385
- const directFn = (prompt) => ollamaAdapter.streamDirect(prompt, ollamaModel);
13448
+ const directFn = (prompt) => ollamaAdapter.streamDirect(prompt, ollamaModel, { thinkingLevel: "off" });
13386
13449
  const result = await attemptSummarizeDirect(chatId, directFn, "ollama", ollamaModel, entries, cap);
13387
13450
  if (result.success) {
13388
13451
  await extractAndLogSignals(result.rawText, chatId, "ollama", ollamaModel);
@@ -13400,7 +13463,7 @@ async function summarizeWithFallbackChain(chatId, targetBackendId, excludeBacken
13400
13463
  const key = `${targetAdapter.id}:${model2}`;
13401
13464
  if (!tried.has(key)) {
13402
13465
  tried.add(key);
13403
- const result = targetAdapter.streamDirect ? await attemptSummarizeDirect(chatId, (p) => targetAdapter.streamDirect(p, model2), targetAdapter.id, model2, entries, getTranscriptCap(model2)) : await attemptSummarize(chatId, targetAdapter, model2, entries);
13466
+ const result = targetAdapter.streamDirect ? await attemptSummarizeDirect(chatId, (p) => targetAdapter.streamDirect(p, model2, { thinkingLevel: "off" }), targetAdapter.id, model2, entries, getTranscriptCap(model2)) : await attemptSummarize(chatId, targetAdapter, model2, entries);
13404
13467
  if (result.success) {
13405
13468
  await extractAndLogSignals(result.rawText, chatId, targetAdapter.id, model2);
13406
13469
  if (clearLogAfter) clearLog(chatId);
@@ -13418,7 +13481,7 @@ async function summarizeWithFallbackChain(chatId, targetBackendId, excludeBacken
13418
13481
  const key = `${adapter.id}:${model2}`;
13419
13482
  if (!tried.has(key)) {
13420
13483
  tried.add(key);
13421
- const result = adapter.streamDirect ? await attemptSummarizeDirect(chatId, (p) => adapter.streamDirect(p, model2), adapter.id, model2, entries, adapter.id === "ollama" ? getOllamaTranscriptCap(model2) : getTranscriptCap(model2)) : await attemptSummarize(chatId, adapter, model2, entries);
13484
+ const result = adapter.streamDirect ? await attemptSummarizeDirect(chatId, (p) => adapter.streamDirect(p, model2, { thinkingLevel: "off" }), adapter.id, model2, entries, adapter.id === "ollama" ? getOllamaTranscriptCap(model2) : getTranscriptCap(model2)) : await attemptSummarize(chatId, adapter, model2, entries);
13422
13485
  if (result.success) {
13423
13486
  await extractAndLogSignals(result.rawText, chatId, adapter.id, model2);
13424
13487
  if (clearLogAfter) clearLog(chatId);
@@ -13440,7 +13503,7 @@ async function summarizeWithFallbackChain(chatId, targetBackendId, excludeBacken
13440
13503
  let result;
13441
13504
  if (adapter.streamDirect) {
13442
13505
  const cap = adapter.id === "ollama" ? getOllamaTranscriptCap(model2) : getTranscriptCap(model2);
13443
- const directFn = (prompt) => adapter.streamDirect(prompt, model2);
13506
+ const directFn = (prompt) => adapter.streamDirect(prompt, model2, { thinkingLevel: "off" });
13444
13507
  result = await attemptSummarizeDirect(chatId, directFn, adapter.id, model2, entries, cap);
13445
13508
  } else {
13446
13509
  result = await attemptSummarize(chatId, adapter, model2, entries);
@@ -13953,6 +14016,17 @@ function killProcessGroup(proc, signal = "SIGTERM") {
13953
14016
  }
13954
14017
  }
13955
14018
  }
14019
+ function runCompaction(chatId, reason, onCompaction) {
14020
+ return summarizeWithFallbackChain(chatId).then((saved) => {
14021
+ if (saved) {
14022
+ clearSession(chatId);
14023
+ clearUsage(chatId);
14024
+ onCompaction?.(chatId);
14025
+ }
14026
+ }).catch((err) => {
14027
+ warn(`[agent] Compaction failed for ${chatId} (${reason}): ${err}`);
14028
+ });
14029
+ }
13956
14030
  function sweepStaleChatEntries() {
13957
14031
  for (const [chatId, state] of activeChats) {
13958
14032
  if (state.process && state.process.exitCode !== null) {
@@ -14088,18 +14162,24 @@ function spawnQuery(adapter, config2, model2, cancelState, thinkingLevel, timeou
14088
14162
  let contentSilenceTimer;
14089
14163
  const silenceTimeoutMs = CONTENT_SILENCE_TIMEOUT_MS;
14090
14164
  let silenceResetCount = 0;
14091
- const MAX_SILENCE_RESETS = 20;
14165
+ const MAX_SILENCE_RESETS = 3;
14092
14166
  function resetContentSilenceTimer() {
14093
14167
  if (silenceTimeoutMs <= 0) return;
14094
14168
  if (contentSilenceTimer) clearTimeout(contentSilenceTimer);
14095
14169
  contentSilenceTimer = setTimeout(() => {
14096
14170
  if (cancelState.cancelled || timedOut) return;
14097
14171
  if (pendingTools.size > 0 && silenceResetCount < MAX_SILENCE_RESETS) {
14098
- silenceResetCount++;
14099
- const tools2 = Array.from(pendingTools.values()).map((t) => typeof t === "string" ? t : t.name).join(", ");
14100
- log(`[agent] Content silence timer fired but ${pendingTools.size} tool(s) still running (${tools2}) \u2014 resetting (${silenceResetCount}/${MAX_SILENCE_RESETS})`);
14101
- resetContentSilenceTimer();
14102
- return;
14172
+ const now = Date.now();
14173
+ const hungTool = Array.from(pendingTools.values()).find((t) => now - t.startedAt > silenceTimeoutMs);
14174
+ if (hungTool) {
14175
+ warn(`[agent] Tool "${hungTool.name}" has been running for ${Math.round((now - hungTool.startedAt) / 1e3)}s \u2014 treating as hung, killing`);
14176
+ } else {
14177
+ silenceResetCount++;
14178
+ const tools2 = Array.from(pendingTools.values()).map((t) => `${t.name} (${Math.round((now - t.startedAt) / 1e3)}s)`).join(", ");
14179
+ log(`[agent] Content silence timer fired but ${pendingTools.size} tool(s) still running (${tools2}) \u2014 resetting (${silenceResetCount}/${MAX_SILENCE_RESETS})`);
14180
+ resetContentSilenceTimer();
14181
+ return;
14182
+ }
14103
14183
  }
14104
14184
  warn(`[agent] Content silence timeout after ${silenceTimeoutMs / 1e3}s for ${adapter.id} \u2014 no content events, killing`);
14105
14185
  timedOut = true;
@@ -14178,7 +14258,7 @@ function spawnQuery(adapter, config2, model2, cancelState, thinkingLevel, timeou
14178
14258
  sawToolEvents = true;
14179
14259
  if (opts?.onToolAction && ev.toolName) {
14180
14260
  const toolInput = ev.toolInput ?? {};
14181
- if (ev.toolId) pendingTools.set(ev.toolId, { name: ev.toolName, input: toolInput });
14261
+ if (ev.toolId) pendingTools.set(ev.toolId, { name: ev.toolName, input: toolInput, startedAt: Date.now() });
14182
14262
  opts.onToolAction(ev.toolName, toolInput, void 0, ev.toolId).catch((err) => {
14183
14263
  error("[agent] tool action error:", err);
14184
14264
  });
@@ -14222,7 +14302,6 @@ function spawnQuery(adapter, config2, model2, cancelState, thinkingLevel, timeou
14222
14302
  input += ev.usage.input;
14223
14303
  output2 += ev.usage.output;
14224
14304
  cacheRead += ev.usage.cacheRead;
14225
- contextSize = ev.usage.input + (ev.usage.cacheRead ?? 0);
14226
14305
  }
14227
14306
  break;
14228
14307
  case "result":
@@ -14240,7 +14319,6 @@ function spawnQuery(adapter, config2, model2, cancelState, thinkingLevel, timeou
14240
14319
  input = ev.usage.input;
14241
14320
  output2 = ev.usage.output;
14242
14321
  cacheRead = ev.usage.cacheRead;
14243
- contextSize = ev.usage.input + (ev.usage.cacheRead ?? 0);
14244
14322
  }
14245
14323
  if (adapter.shouldKillOnResult()) {
14246
14324
  try {
@@ -14323,7 +14401,7 @@ Partial output: ${accumulatedText.slice(-500)}`;
14323
14401
  return;
14324
14402
  }
14325
14403
  const cleanedResult = stripThinkingContent(resultText || accumulatedText);
14326
- resolve3({ resultText: cleanedResult, thinkingText: accumulatedThinking, sessionId, input, output: output2, cacheRead, contextSize, sawToolEvents, sawResultEvent });
14404
+ resolve3({ resultText: cleanedResult, thinkingText: accumulatedThinking, sessionId, input, output: output2, cacheRead, contextSize: null, sawToolEvents, sawResultEvent });
14327
14405
  });
14328
14406
  });
14329
14407
  }
@@ -14542,9 +14620,12 @@ async function askAgentImpl(chatId, userMessage, opts) {
14542
14620
  activeChats.set(chatId, cancelState2);
14543
14621
  try {
14544
14622
  let messageHistory;
14623
+ let apiContextSize;
14545
14624
  if (adapter.type === "api") {
14546
14625
  const contextWindow = adapter.contextWindow[resolvedModel2] ?? 8192;
14547
- messageHistory = await buildApiMessages(chatId, userMessage, fullPrompt, contextWindow);
14626
+ const { buildApiMessages: buildMsgs, estimateContextUsage: estimateContextUsage2 } = await Promise.resolve().then(() => (init_api_context(), api_context_exports));
14627
+ messageHistory = await buildMsgs(chatId, userMessage, fullPrompt, contextWindow);
14628
+ apiContextSize = estimateContextUsage2(chatId, contextWindow).estimatedTokens;
14548
14629
  }
14549
14630
  const sdResult = await adapter.streamDirect(fullPrompt, resolvedModel2, {
14550
14631
  timeoutMs: timeoutMs ?? 3e5,
@@ -14560,23 +14641,29 @@ async function askAgentImpl(chatId, userMessage, opts) {
14560
14641
  });
14561
14642
  if (!isSyntheticChatId(chatId)) {
14562
14643
  appendToLog(chatId, userMessage, sdResult.text, adapter.id, resolvedModel2, null);
14563
- const AUTO_SUMMARIZE_THRESHOLD = 30;
14564
- const pairCount = profile !== "chat" ? getMessagePairCount(chatId) : 0;
14565
- if (pairCount >= AUTO_SUMMARIZE_THRESHOLD) {
14566
- log(`[agent] Auto-summarizing chat ${chatId} after ${pairCount} turns`);
14567
- summarizeWithFallbackChain(chatId).then((saved) => {
14568
- if (saved) {
14569
- clearSession(chatId);
14570
- opts?.onCompaction?.(chatId);
14571
- }
14572
- }).catch((err) => {
14573
- warn(`[agent] Auto-summarize failed for chat ${chatId}: ${err}`);
14574
- });
14644
+ if (apiContextSize && adapter.type === "api" && !compactionInFlight.has(chatId)) {
14645
+ const contextWindow = adapter.contextWindow[resolvedModel2] ?? 8192;
14646
+ const contextPct = apiContextSize / contextWindow * 100;
14647
+ if (contextPct >= 85) {
14648
+ compactionInFlight.add(chatId);
14649
+ log(`[agent] Context at ${contextPct.toFixed(0)}% for ${chatId} \u2014 triggering background compaction`);
14650
+ opts?.onCompaction?.(chatId, "triggered");
14651
+ runCompaction(chatId, "context-85%", opts?.onCompaction).finally(() => {
14652
+ compactionInFlight.delete(chatId);
14653
+ });
14654
+ }
14655
+ }
14656
+ if (adapter.type !== "api" || !compactionInFlight.has(chatId)) {
14657
+ const pairCount = profile !== "chat" ? getMessagePairCount(chatId) : 0;
14658
+ if (pairCount >= 30) {
14659
+ log(`[agent] Auto-summarizing chat ${chatId} after ${pairCount} turns`);
14660
+ runCompaction(chatId, "30-pair-threshold", opts?.onCompaction);
14661
+ }
14575
14662
  }
14576
14663
  }
14577
14664
  const sdUsage = sdResult.usage ?? { input: 0, output: 0 };
14578
14665
  if (sdUsage.input + sdUsage.output > 0) {
14579
- addUsage(chatId, sdUsage.input, sdUsage.output, 0, resolvedModel2);
14666
+ addUsage(chatId, sdUsage.input, sdUsage.output, 0, resolvedModel2, adapter.id, apiContextSize);
14580
14667
  }
14581
14668
  if (cancelState2.cancelled) {
14582
14669
  return { text: "Stopped.", usage: { input: sdUsage.input, output: sdUsage.output, cacheRead: 0 } };
@@ -14878,18 +14965,10 @@ async function askAgentImpl(chatId, userMessage, opts) {
14878
14965
  }
14879
14966
  if (result.resultText && !isSyntheticChatId(chatId)) {
14880
14967
  appendToLog(chatId, userMessage, result.resultText, adapter.id, model2 ?? null, result.sessionId ?? null);
14881
- const AUTO_SUMMARIZE_THRESHOLD = 30;
14882
14968
  const pairCount = profile !== "chat" ? getMessagePairCount(chatId) : 0;
14883
- if (pairCount >= AUTO_SUMMARIZE_THRESHOLD) {
14969
+ if (pairCount >= 30) {
14884
14970
  log(`[agent] Auto-summarizing chat ${chatId} after ${pairCount} turns`);
14885
- summarizeWithFallbackChain(chatId).then((saved) => {
14886
- if (saved) {
14887
- clearSession(chatId);
14888
- opts?.onCompaction?.(chatId);
14889
- }
14890
- }).catch((err) => {
14891
- warn(`[agent] Auto-summarize failed for chat ${chatId}: ${err}`);
14892
- });
14971
+ runCompaction(chatId, "30-pair-threshold", opts?.onCompaction);
14893
14972
  }
14894
14973
  }
14895
14974
  return {
@@ -14917,7 +14996,7 @@ function injectMcpConfig(adapterId, args, mcpConfigPath) {
14917
14996
  if (!flag) return args;
14918
14997
  return [...args, ...flag, mcpConfigPath, "--strict-mcp-config"];
14919
14998
  }
14920
- var activeChats, staleSweepTimer, chatLocks, SPAWN_TIMEOUT_MS, FIRST_RESPONSE_TIMEOUT_MS, CONTENT_SILENCE_TIMEOUT_MS, CONTENT_SILENCE_TIMEOUT_ERROR, FIRST_RESPONSE_TIMEOUT_ERROR, FREE_SLOTS_EXHAUSTED, GEMINI_FALLBACK_CHAIN, GEMINI_DOWNGRADE_MODELS, MCP_CONFIG_FLAG;
14999
+ var activeChats, compactionInFlight, staleSweepTimer, chatLocks, SPAWN_TIMEOUT_MS, FIRST_RESPONSE_TIMEOUT_MS, CONTENT_SILENCE_TIMEOUT_MS, CONTENT_SILENCE_TIMEOUT_ERROR, FIRST_RESPONSE_TIMEOUT_ERROR, FREE_SLOTS_EXHAUSTED, GEMINI_FALLBACK_CHAIN, GEMINI_DOWNGRADE_MODELS, MCP_CONFIG_FLAG;
14921
15000
  var init_agent = __esm({
14922
15001
  "src/agent.ts"() {
14923
15002
  "use strict";
@@ -14934,7 +15013,6 @@ var init_agent = __esm({
14934
15013
  init_strip_thinking();
14935
15014
  init_text_utils();
14936
15015
  init_session_log();
14937
- init_api_context();
14938
15016
  init_summarize();
14939
15017
  init_quota();
14940
15018
  init_store5();
@@ -14945,6 +15023,7 @@ var init_agent = __esm({
14945
15023
  init_unified_config();
14946
15024
  init_mcp_config();
14947
15025
  activeChats = /* @__PURE__ */ new Map();
15026
+ compactionInFlight = /* @__PURE__ */ new Set();
14948
15027
  chatLocks = /* @__PURE__ */ new Map();
14949
15028
  SPAWN_TIMEOUT_MS = 10 * 60 * 1e3;
14950
15029
  FIRST_RESPONSE_TIMEOUT_MS = parseInt(process.env.GEMINI_FIRST_RESPONSE_TIMEOUT_MS ?? "30000", 10);
@@ -15112,7 +15191,9 @@ async function runWeeklySweep(chatId, channel, backendId, model2) {
15112
15191
  buttons.push([{ label: "Review Now", data: "mem:opt:start", style: "success" }]);
15113
15192
  }
15114
15193
  buttons.push([{ label: "Dismiss", data: "mem:sweep:dismiss" }]);
15115
- await sendOrEditKeyboard(chatId, channel, void 0, lines.join("\n"), buttons);
15194
+ if (channel) {
15195
+ await sendOrEditKeyboard(chatId, channel, void 0, lines.join("\n"), buttons);
15196
+ }
15116
15197
  return { suggestionsCount, cleanedUp };
15117
15198
  } catch (err) {
15118
15199
  const msg = errorMessage(err);
@@ -22597,6 +22678,7 @@ var init_ollama2 = __esm({
22597
22678
  async streamDirect(prompt, model2, opts) {
22598
22679
  const cleanPrompt = stripForLocalModel(prompt);
22599
22680
  let disableThinking = false;
22681
+ let modelContextWindow;
22600
22682
  try {
22601
22683
  const { OllamaStore } = (init_ollama(), __toCommonJS(ollama_exports));
22602
22684
  const modelRecord = OllamaStore.getModelByName(model2);
@@ -22605,8 +22687,14 @@ var init_ollama2 = __esm({
22605
22687
  } else if (opts?.thinkingLevel === "off") {
22606
22688
  disableThinking = true;
22607
22689
  }
22690
+ if (modelRecord?.contextWindow && modelRecord.contextWindow > 4096) {
22691
+ modelContextWindow = modelRecord.contextWindow;
22692
+ }
22608
22693
  } catch {
22609
22694
  }
22695
+ const ollamaProviderOpts = {};
22696
+ if (disableThinking) ollamaProviderOpts.think = false;
22697
+ if (modelContextWindow) ollamaProviderOpts.num_ctx = modelContextWindow;
22610
22698
  const apiOpts = {
22611
22699
  timeoutMs: opts?.timeoutMs,
22612
22700
  onStream: opts?.onStream,
@@ -22615,7 +22703,7 @@ var init_ollama2 = __esm({
22615
22703
  permMode: opts?.permMode,
22616
22704
  thinkingLevel: opts?.thinkingLevel,
22617
22705
  onThinking: opts?.onThinking,
22618
- ...disableThinking ? { providerOptions: { ollama: { think: false } } } : {}
22706
+ ...Object.keys(ollamaProviderOpts).length > 0 ? { providerOptions: { ollama: ollamaProviderOpts } } : {}
22619
22707
  };
22620
22708
  const result = await this.streamDirectWithHistory(
22621
22709
  cleanPrompt,
@@ -22701,7 +22789,28 @@ var init_openrouter = __esm({
22701
22789
  }
22702
22790
  summarizerModel = DEFAULT_FREE_MODEL;
22703
22791
  pricing = {};
22704
- contextWindow = {};
22792
+ _contextWindowCache = null;
22793
+ _contextWindowCacheSize = 0;
22794
+ get contextWindow() {
22795
+ try {
22796
+ const { getApiModels: getApiModels2 } = (init_api_models(), __toCommonJS(api_models_exports));
22797
+ const models = getApiModels2("openrouter");
22798
+ if (this._contextWindowCache && models.length === this._contextWindowCacheSize) {
22799
+ return this._contextWindowCache;
22800
+ }
22801
+ const result = {};
22802
+ for (const m of models) {
22803
+ if (m.contextWindow) {
22804
+ result[m.modelId] = m.contextWindow;
22805
+ }
22806
+ }
22807
+ this._contextWindowCache = result;
22808
+ this._contextWindowCacheSize = models.length;
22809
+ return result;
22810
+ } catch {
22811
+ return {};
22812
+ }
22813
+ }
22705
22814
  // ── Vercel AI SDK provider ────────────────────────────────────────
22706
22815
  /**
22707
22816
  * Create the Vercel AI SDK LanguageModel for a given model ID.
@@ -24547,7 +24656,7 @@ function getEditCoordinator() {
24547
24656
  function resetEditCoordinator() {
24548
24657
  EditCoordinator.resetInstance();
24549
24658
  }
24550
- var TICK_INTERVAL_MS, MAX_EDITS_PER_WINDOW, EDIT_WINDOW_MS, EditCoordinator;
24659
+ var TICK_INTERVAL_MS, MAX_EDITS_PER_WINDOW, EDIT_WINDOW_MS, MIN_FLUSH_GAP_DM_MS, MIN_FLUSH_GAP_GROUP_MS, EditCoordinator;
24551
24660
  var init_edit_coordinator = __esm({
24552
24661
  "src/channels/edit-coordinator.ts"() {
24553
24662
  "use strict";
@@ -24555,10 +24664,14 @@ var init_edit_coordinator = __esm({
24555
24664
  TICK_INTERVAL_MS = 1e3;
24556
24665
  MAX_EDITS_PER_WINDOW = 4;
24557
24666
  EDIT_WINDOW_MS = 6e4;
24667
+ MIN_FLUSH_GAP_DM_MS = 2e3;
24668
+ MIN_FLUSH_GAP_GROUP_MS = 4e3;
24558
24669
  EditCoordinator = class _EditCoordinator {
24559
24670
  static instance = null;
24560
24671
  /** Active streams indexed by messageId. */
24561
24672
  activeStreams = /* @__PURE__ */ new Map();
24673
+ /** Last flush timestamp per stream — prevents flushing faster than the throttle can drain. */
24674
+ lastFlushAt = /* @__PURE__ */ new Map();
24562
24675
  /** Per-message edit tracking for the sliding window cap. */
24563
24676
  perMessageEditCount = /* @__PURE__ */ new Map();
24564
24677
  /** Single flush timer shared across all streams. */
@@ -24599,6 +24712,7 @@ var init_edit_coordinator = __esm({
24599
24712
  unregister(messageId) {
24600
24713
  this.activeStreams.delete(messageId);
24601
24714
  this.perMessageEditCount.delete(messageId);
24715
+ this.lastFlushAt.delete(messageId);
24602
24716
  this.rebuildKeys();
24603
24717
  log(`[edit-coordinator] unregistered stream ${messageId} (${this.activeStreams.size} remaining)`);
24604
24718
  if (this.activeStreams.size === 0 && this.flushTimer) {
@@ -24616,6 +24730,7 @@ var init_edit_coordinator = __esm({
24616
24730
  }
24617
24731
  this.activeStreams.clear();
24618
24732
  this.perMessageEditCount.clear();
24733
+ this.lastFlushAt.clear();
24619
24734
  this.streamKeys = [];
24620
24735
  this.roundRobinIndex = 0;
24621
24736
  }
@@ -24644,6 +24759,17 @@ var init_edit_coordinator = __esm({
24644
24759
  }
24645
24760
  }
24646
24761
  // ── Internal ──────────────────────────────────────────────────────────
24762
+ /** Check whether enough time has passed since the last flush for this stream.
24763
+ * Group chats need longer gaps (4s) to match the throttle's group pacing (3.5s).
24764
+ * Without this, the coordinator pushes edits faster than the throttle drains them,
24765
+ * causing per-chat queue buildup. */
24766
+ canFlushStream(messageId, stream) {
24767
+ const last = this.lastFlushAt.get(messageId);
24768
+ if (last === void 0) return true;
24769
+ const chatId = stream.getChatId();
24770
+ const minGap = parseInt(chatId) < 0 ? MIN_FLUSH_GAP_GROUP_MS : MIN_FLUSH_GAP_DM_MS;
24771
+ return Date.now() - last >= minGap;
24772
+ }
24647
24773
  /** Rebuild the ordered keys array after registration changes. */
24648
24774
  rebuildKeys() {
24649
24775
  this.streamKeys = Array.from(this.activeStreams.keys());
@@ -24663,11 +24789,12 @@ var init_edit_coordinator = __esm({
24663
24789
  const idx = (startIdx + tried) % this.streamKeys.length;
24664
24790
  const messageId = this.streamKeys[idx];
24665
24791
  const stream = this.activeStreams.get(messageId);
24666
- if (stream && this.canEditMessage(messageId)) {
24792
+ if (stream && this.canEditMessage(messageId) && this.canFlushStream(messageId, stream)) {
24667
24793
  this.roundRobinIndex = (idx + 1) % this.streamKeys.length;
24668
24794
  try {
24669
24795
  await stream.flush();
24670
24796
  this.recordEdit(messageId);
24797
+ this.lastFlushAt.set(messageId, Date.now());
24671
24798
  } catch {
24672
24799
  }
24673
24800
  return;
@@ -24782,6 +24909,7 @@ var init_live_status = __esm({
24782
24909
  /** Spinner frame counter — advances on each flush for animation. */
24783
24910
  spinnerFrame = 0;
24784
24911
  /** Timestamp of last successful edit — used for heartbeat force-through. */
24912
+ lastSuccessfulFlushAt = 0;
24785
24913
  /** Callback to restart typing indicator as fallback. */
24786
24914
  onTypingFallback;
24787
24915
  /** Set a callback that restarts the typing indicator loop as a fallback. */
@@ -25360,10 +25488,12 @@ function getTypingManager() {
25360
25488
  function resetTypingManager() {
25361
25489
  TypingManager.resetInstance();
25362
25490
  }
25363
- var TypingManager;
25491
+ var MAX_TYPING_DURATION_MS, TypingManager;
25364
25492
  var init_typing_manager = __esm({
25365
25493
  "src/channels/typing-manager.ts"() {
25366
25494
  "use strict";
25495
+ init_log();
25496
+ MAX_TYPING_DURATION_MS = 15 * 60 * 1e3;
25367
25497
  TypingManager = class _TypingManager {
25368
25498
  static instance = null;
25369
25499
  activeChats = /* @__PURE__ */ new Map();
@@ -25385,11 +25515,17 @@ var init_typing_manager = __esm({
25385
25515
  }
25386
25516
  channel.sendTyping?.(chatId).catch(() => {
25387
25517
  });
25518
+ const acquiredAt = Date.now();
25388
25519
  const timer = setInterval(() => {
25520
+ if (Date.now() - acquiredAt > MAX_TYPING_DURATION_MS) {
25521
+ warn(`[typing-manager] Auto-releasing chat ${chatId} after ${MAX_TYPING_DURATION_MS / 6e4}min (likely leak)`);
25522
+ this.forceRelease(chatId);
25523
+ return;
25524
+ }
25389
25525
  channel.sendTyping?.(chatId).catch(() => {
25390
25526
  });
25391
25527
  }, 4e3);
25392
- this.activeChats.set(chatId, { refCount: 1, timer });
25528
+ this.activeChats.set(chatId, { refCount: 1, timer, acquiredAt });
25393
25529
  }
25394
25530
  /**
25395
25531
  * Stop showing typing for this agent's perspective.
@@ -25404,6 +25540,13 @@ var init_typing_manager = __esm({
25404
25540
  this.activeChats.delete(chatId);
25405
25541
  }
25406
25542
  }
25543
+ /** Force-release a chat regardless of refCount (for leak recovery). */
25544
+ forceRelease(chatId) {
25545
+ const entry = this.activeChats.get(chatId);
25546
+ if (!entry) return;
25547
+ clearInterval(entry.timer);
25548
+ this.activeChats.delete(chatId);
25549
+ }
25407
25550
  /** Clean shutdown — clear all timers. */
25408
25551
  shutdown() {
25409
25552
  for (const [, entry] of this.activeChats) {
@@ -29319,6 +29462,7 @@ async function handleNewchatCommand(chatId, commandArgs, msg, channel) {
29319
29462
  const summarized = await summarizeSession(chatId);
29320
29463
  clearSession(chatId);
29321
29464
  clearChatPaidSlots(chatId);
29465
+ clearUsage(chatId);
29322
29466
  setSessionStartedAt(chatId);
29323
29467
  logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: "New session started", detail: { field: "session", action: "reset", summarized } });
29324
29468
  if (typeof channel.sendKeyboard === "function" && oldSessionId) {
@@ -29372,6 +29516,7 @@ async function handleClearCommand(chatId, _commandArgs, _msg, channel) {
29372
29516
  stopAllSideQuests(chatId);
29373
29517
  clearSession(chatId);
29374
29518
  clearChatPaidSlots(chatId);
29519
+ clearUsage(chatId);
29375
29520
  setSessionStartedAt(chatId);
29376
29521
  logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: "Session cleared (no summary)", detail: { field: "session", action: "clear" } });
29377
29522
  await channel.sendText(chatId, "\u{1F9FD} Session cleared. No summary saved.", { parseMode: "plain", priority: 0 /* P0_CRITICAL */ });
@@ -29448,6 +29593,17 @@ async function handleSummarizeCommand(chatId, commandArgs, msg, channel) {
29448
29593
  }
29449
29594
  }
29450
29595
  }
29596
+ function formatSummarizerLabel(backend2, model2, fallbackModel) {
29597
+ if (backend2 === "off") return "Off";
29598
+ if (backend2) return `${backend2}: ${model2 ?? "default"}`;
29599
+ return `Auto (${fallbackModel ?? "default"})`;
29600
+ }
29601
+ function summarizerStatusLine(chatId, adapter) {
29602
+ const { config: config2, source } = getSummarizerWithSource(chatId);
29603
+ const label2 = formatSummarizerLabel(config2.backend, config2.model, adapter?.summarizerModel);
29604
+ if (source === "auto") return label2.toLowerCase();
29605
+ return `${label2} (${source === "global" ? "global" : "per-chat"})`;
29606
+ }
29451
29607
  async function handleStatusCommand(chatId, commandArgs, msg, channel) {
29452
29608
  const sessionId = getSessionId(chatId);
29453
29609
  const cwd = getCwd(chatId);
@@ -29464,12 +29620,21 @@ async function handleStatusCommand(chatId, commandArgs, msg, channel) {
29464
29620
  const thinking2 = getThinkingLevel(chatId);
29465
29621
  const mode = getMode(chatId);
29466
29622
  const modelSig = getModelSignature(chatId);
29467
- const contextMax = adapter?.contextWindow[model2] ?? 2e5;
29468
- const contextUsed = usage2.context_size;
29469
- const contextPct = contextMax > 0 ? contextUsed / contextMax * 100 : 0;
29470
- const ctxBar = buildBar(contextPct);
29471
- const usedK = (contextUsed / 1e3).toFixed(1);
29472
- const maxK = (contextMax / 1e3).toFixed(0);
29623
+ let contextLine;
29624
+ if (adapter?.type === "api") {
29625
+ const { estimateContextUsage: estimateContextUsage2 } = await Promise.resolve().then(() => (init_api_context(), api_context_exports));
29626
+ const contextMax = adapter.contextWindow[model2] ?? 8192;
29627
+ const ctxEst = estimateContextUsage2(chatId, contextMax);
29628
+ const ctxBar = buildBar(ctxEst.percentage);
29629
+ const usedK = (ctxEst.estimatedTokens / 1e3).toFixed(1);
29630
+ const maxK = (contextMax / 1e3).toFixed(0);
29631
+ contextLine = `\u{1F4D0} Context: ${ctxBar} ${usedK}K/${maxK}K (${ctxEst.percentage.toFixed(1)}%) \xB7 compacts at 85%`;
29632
+ } else {
29633
+ const pairCount = getMessagePairCount(chatId);
29634
+ const threshold = 30;
29635
+ const remaining = Math.max(0, threshold - pairCount);
29636
+ contextLine = `\u{1F4D0} Session: ${pairCount}/${threshold} messages \xB7 compacts in ${remaining}`;
29637
+ }
29473
29638
  const bootRow = getDb().prepare("SELECT value FROM meta WHERE key = 'boot_time'").get();
29474
29639
  let uptimeStr = "unknown";
29475
29640
  if (bootRow) {
@@ -29532,11 +29697,12 @@ async function handleStatusCommand(chatId, commandArgs, msg, channel) {
29532
29697
  `\u{1F4AD} Think: ${thinking2} \xB7 Mode: ${mode}`,
29533
29698
  `\u{1F916} Agents: ${getAgentMode(chatId)}`,
29534
29699
  `\u{1F507} Voice: ${voice2 ? "on" : "off"} \xB7 Sig: ${modelSig}`,
29700
+ `\u{1F4DD} Summarizer: ${summarizerStatusLine(chatId, adapter)}`,
29535
29701
  ``,
29536
29702
  buildSectionHeader("Session"),
29537
29703
  `\u{1F4CB} ${sessionId ?? "no active session"}`,
29538
29704
  `\u{1F4C1} ${cwd ?? "default workspace"}`,
29539
- `\u{1F4D0} Context: ${ctxBar} ${usedK}K/${maxK}K (${contextPct.toFixed(1)}%)`,
29705
+ contextLine,
29540
29706
  ...sqCount > 0 ? [`\u{1F5FA} Side quests: ${sqCount} active`] : [],
29541
29707
  ``,
29542
29708
  buildSectionHeader("Usage"),
@@ -29947,8 +30113,17 @@ async function handleSummarizerCommand(chatId, commandArgs, msg, channel) {
29947
30113
  } catch {
29948
30114
  adapter = null;
29949
30115
  }
29950
- const current = getSummarizer(chatId);
29951
- const currentLabel = current.backend === "off" ? "Off" : current.backend ? `${current.backend}:${current.model ?? "default"}` : `Auto (${adapter?.summarizerModel ?? "default"})`;
30116
+ const { config: current, source, globalConfig } = getSummarizerWithSource(chatId);
30117
+ const overrideCount = countSummarizerOverrides();
30118
+ const currentLabel = formatSummarizerLabel(current.backend, current.model, adapter?.summarizerModel);
30119
+ const sourceTag = source === "per-chat" ? "per-chat override" : source === "global" ? "global default" : "auto";
30120
+ const headerLines = [`Summarizer: ${currentLabel} (${sourceTag})`];
30121
+ if (globalConfig.backend) {
30122
+ headerLines.push(`Global default: ${formatSummarizerLabel(globalConfig.backend, globalConfig.model)}`);
30123
+ }
30124
+ if (overrideCount > 0) {
30125
+ headerLines.push(`${overrideCount} chat${overrideCount === 1 ? "" : "s"} with per-chat overrides`);
30126
+ }
29952
30127
  if (typeof channel.sendKeyboard === "function") {
29953
30128
  const isAuto = !current.backend && current.backend !== "off";
29954
30129
  const isOff = current.backend === "off";
@@ -29996,7 +30171,14 @@ async function handleSummarizerCommand(chatId, commandArgs, msg, channel) {
29996
30171
  }]);
29997
30172
  }
29998
30173
  }
29999
- await channel.sendKeyboard(chatId, `Session summarizer (current: ${currentLabel}):`, buttons);
30174
+ if (overrideCount > 0) {
30175
+ buttons.push([{
30176
+ label: `Clear All Overrides (${overrideCount})`,
30177
+ data: "summarizer:clearall",
30178
+ style: "danger"
30179
+ }]);
30180
+ }
30181
+ await channel.sendKeyboard(chatId, headerLines.join("\n"), buttons);
30000
30182
  } else {
30001
30183
  await channel.sendText(chatId, `Summarizer: ${currentLabel}
30002
30184
 
@@ -31519,14 +31701,40 @@ ${value ? "Full tool inputs/results will be saved to ~/.cc-claw/logs/sessions/"
31519
31701
  if (rest === "auto") {
31520
31702
  clearSummarizer(chatId);
31521
31703
  await channel.sendText(chatId, "Summarizer set to auto (uses active backend).", { parseMode: "plain" });
31522
- } else if (rest === "off") {
31523
- setSummarizer(chatId, "off", null);
31524
- await channel.sendText(chatId, "Session summarization disabled.", { parseMode: "plain" });
31704
+ } else if (rest === "clearall") {
31705
+ const cleared = clearAllSummarizerOverrides();
31706
+ await channel.sendText(chatId, `Cleared ${cleared} per-chat override${cleared === 1 ? "" : "s"}. All chats now use the global default.`, { parseMode: "plain" });
31707
+ } else if (rest.startsWith("promote:")) {
31708
+ const promoteValue = rest.slice(8);
31709
+ const db3 = getDb();
31710
+ const promote = db3.transaction(() => {
31711
+ if (promoteValue === "off") {
31712
+ setGlobalSummarizer("off", null);
31713
+ } else {
31714
+ const [bk, ...modelParts] = promoteValue.split(":");
31715
+ setGlobalSummarizer(bk, modelParts.join(":") || null);
31716
+ }
31717
+ return clearAllSummarizerOverrides();
31718
+ });
31719
+ const cleared = promote();
31720
+ const label2 = promoteValue === "off" ? "Off" : promoteValue;
31721
+ const clearedNote = cleared > 0 ? ` Cleared ${cleared} per-chat override${cleared === 1 ? "" : "s"}.` : "";
31722
+ await channel.sendText(chatId, `Global summarizer set to ${label2}.${clearedNote} All chats now use this default.`, { parseMode: "plain" });
31525
31723
  } else {
31526
- const [bk, ...modelParts] = rest.split(":");
31527
- const mdl = modelParts.join(":") || null;
31724
+ const isOff = rest === "off";
31725
+ const bk = isOff ? "off" : rest.split(":")[0];
31726
+ const mdl = isOff ? null : rest.split(":").slice(1).join(":") || null;
31528
31727
  setSummarizer(chatId, bk, mdl);
31529
- await channel.sendText(chatId, `Summarizer pinned to ${bk}:${mdl ?? "default"}.`, { parseMode: "plain" });
31728
+ const displayLabel = isOff ? "Off" : `${bk}:${mdl ?? "default"}`;
31729
+ const confirmMsg = isOff ? "Session summarization disabled for this chat." : `Summarizer pinned to ${displayLabel} for this chat.`;
31730
+ const promoteData = `summarizer:promote:${rest}`;
31731
+ if (typeof channel.sendKeyboard === "function") {
31732
+ await channel.sendKeyboard(chatId, confirmMsg, [
31733
+ [{ label: "Set as Global Default", data: promoteData }]
31734
+ ]);
31735
+ } else {
31736
+ await channel.sendText(chatId, confirmMsg, { parseMode: "plain" });
31737
+ }
31530
31738
  }
31531
31739
  } else if (data.startsWith("perms:")) {
31532
31740
  let chosen = data.slice(6);
@@ -34525,6 +34733,8 @@ You're still in discussion mode \u2014 try again or click a button to exit.`, {
34525
34733
  return;
34526
34734
  }
34527
34735
  getTypingManager().acquire(chatId, channel);
34736
+ let stopDraftTimer = () => {
34737
+ };
34528
34738
  try {
34529
34739
  const tMode = settings.getMode();
34530
34740
  const tVerbose = settings.getVerboseLevel();
@@ -34584,7 +34794,7 @@ You're still in discussion mode \u2014 try again or click a button to exit.`, {
34584
34794
  draftState.dirty = true;
34585
34795
  };
34586
34796
  }
34587
- const stopDraftTimer2 = () => {
34797
+ stopDraftTimer = () => {
34588
34798
  if (draftState?.flushTimer) {
34589
34799
  clearInterval(draftState.flushTimer);
34590
34800
  draftState.flushTimer = null;
@@ -34620,9 +34830,14 @@ You're still in discussion mode \u2014 try again or click a button to exit.`, {
34620
34830
  } catch {
34621
34831
  }
34622
34832
  },
34623
- onCompaction: (cid) => {
34624
- channel.sendText(cid, "\u{1F4BE} Context saved to memory.").catch(() => {
34625
- });
34833
+ onCompaction: (cid, phase) => {
34834
+ if (phase === "triggered") {
34835
+ channel.sendText(cid, "\u{1F4BE} Context compaction triggered \u2014 saving conversation to memory...").catch(() => {
34836
+ });
34837
+ } else {
34838
+ channel.sendText(cid, "\u{1F4BE} Context saved to memory.").catch(() => {
34839
+ });
34840
+ }
34626
34841
  },
34627
34842
  onSlotRotation: (cid, from, to) => {
34628
34843
  const slots = getGeminiSlots();
@@ -34640,7 +34855,7 @@ You're still in discussion mode \u2014 try again or click a button to exit.`, {
34640
34855
  });
34641
34856
  }
34642
34857
  });
34643
- stopDraftTimer2();
34858
+ stopDraftTimer();
34644
34859
  const elapsedMs = Date.now() - sigT0;
34645
34860
  const elapsedSec = (elapsedMs / 1e3).toFixed(1);
34646
34861
  if (liveStatus && response.thinkingText?.trim()) {
@@ -36230,7 +36445,7 @@ var init_telegram2 = __esm({
36230
36445
  };
36231
36446
  }
36232
36447
  async start(handler) {
36233
- await this.bot.api.setMyCommands([
36448
+ await this.throttle.send(this.primaryChatId, "setMyCommands", () => this.bot.api.setMyCommands([
36234
36449
  // Core
36235
36450
  { command: "menu", description: "Home screen \u2014 quick-access keyboard" },
36236
36451
  { command: "m", description: "Home screen (alias for /menu)" },
@@ -36308,7 +36523,7 @@ var init_telegram2 = __esm({
36308
36523
  // Context & info
36309
36524
  { command: "info", description: "Current chat context (ID, topic, sender, settings)" },
36310
36525
  { command: "council", description: "Multi-model debate (select models, anonymous rounds)" }
36311
- ]);
36526
+ ]));
36312
36527
  this.bot.on("message", async (ctx) => {
36313
36528
  const chatId = ctx.chat.id.toString();
36314
36529
  const senderId = ctx.from?.id?.toString() ?? "";
@@ -36354,15 +36569,13 @@ var init_telegram2 = __esm({
36354
36569
  const chatId = ctx.callbackQuery.message?.chat?.id?.toString() ?? userId;
36355
36570
  log(`[telegram] Callback from user ${userId} in chat ${chatId}: ${ctx.callbackQuery.data}`);
36356
36571
  if (!this.isAuthorized(userId) && !this.isAuthorized(chatId)) {
36357
- ctx.answerCallbackQuery("Unauthorized").catch(() => {
36358
- });
36572
+ this.throttle.tryBestEffort(chatId, "answerCallbackQuery:unauth", () => ctx.answerCallbackQuery("Unauthorized"));
36359
36573
  return;
36360
36574
  }
36361
36575
  const data = ctx.callbackQuery.data;
36362
36576
  const messageId = ctx.callbackQuery.message?.message_id?.toString();
36363
36577
  const threadId = ctx.callbackQuery.message?.message_thread_id;
36364
- ctx.answerCallbackQuery().catch(() => {
36365
- });
36578
+ this.throttle.tryBestEffort(chatId, "answerCallbackQuery", () => ctx.answerCallbackQuery());
36366
36579
  (async () => {
36367
36580
  let ch = this;
36368
36581
  if (threadId) {
@@ -36411,7 +36624,7 @@ var init_telegram2 = __esm({
36411
36624
  this.keepaliveInterval = setInterval(async () => {
36412
36625
  if (!this.pollingExpected) return;
36413
36626
  try {
36414
- await this.bot.api.getMe();
36627
+ await this.throttle.tryBestEffort(this.primaryChatId, "getMe:keepalive", () => this.bot.api.getMe());
36415
36628
  this.lastPollingCheckAt = Date.now();
36416
36629
  } catch (err) {
36417
36630
  error("[telegram] Keepalive ping failed:", err);
@@ -36551,7 +36764,7 @@ var init_telegram2 = __esm({
36551
36764
  );
36552
36765
  }
36553
36766
  async downloadFile(fileId) {
36554
- const file = await this.bot.api.getFile(fileId);
36767
+ const file = await this.throttle.send(this.primaryChatId, "getFile", () => this.bot.api.getFile(fileId));
36555
36768
  const fileUrl = `https://api.telegram.org/file/bot${process.env.TELEGRAM_BOT_TOKEN}/${file.file_path}`;
36556
36769
  const response = await fetch(fileUrl);
36557
36770
  return Buffer.from(await response.arrayBuffer());
@@ -36840,7 +37053,11 @@ var init_telegram2 = __esm({
36840
37053
  }
36841
37054
  }
36842
37055
  }
36843
- await ctx.answerInlineQuery(results.slice(0, 10), { cache_time: 30 });
37056
+ await this.throttle.tryBestEffort(
37057
+ this.primaryChatId,
37058
+ "answerInlineQuery",
37059
+ () => ctx.answerInlineQuery(results.slice(0, 10), { cache_time: 30 })
37060
+ );
36844
37061
  }
36845
37062
  trackAgentMessage(messageId, chatId) {
36846
37063
  if (this.agentMessageIds.size >= 1e4) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-claw",
3
- "version": "0.29.1",
3
+ "version": "0.29.3",
4
4
  "description": "CC-Claw: Personal AI assistant on Telegram — multi-backend (Claude, Gemini, Codex, Cursor), sub-agent orchestration, MCP management",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",