cc-claw 0.27.2 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js 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.27.2" : (() => {
36
+ VERSION = true ? "0.29.0" : (() => {
37
37
  try {
38
38
  return JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
39
39
  } catch {
@@ -62,7 +62,8 @@ __export(paths_exports, {
62
62
  SKILLS_PATH: () => SKILLS_PATH,
63
63
  WHISPER_MODELS_PATH: () => WHISPER_MODELS_PATH,
64
64
  WORKSPACE_PATH: () => WORKSPACE_PATH,
65
- readApiToken: () => readApiToken
65
+ readApiToken: () => readApiToken,
66
+ resolveRealHome: () => resolveRealHome
66
67
  });
67
68
  import { homedir, userInfo } from "os";
68
69
  import { join as join2 } from "path";
@@ -1209,6 +1210,64 @@ var init_store4 = __esm({
1209
1210
  }
1210
1211
  });
1211
1212
 
1213
+ // src/memory/classify.ts
1214
+ function classifyMemory(_trigger, content) {
1215
+ for (const { category, patterns } of CATEGORY_PATTERNS) {
1216
+ for (const pattern of patterns) {
1217
+ if (pattern.test(content)) {
1218
+ return category;
1219
+ }
1220
+ }
1221
+ }
1222
+ return "uncategorized";
1223
+ }
1224
+ var CATEGORY_PATTERNS;
1225
+ var init_classify = __esm({
1226
+ "src/memory/classify.ts"() {
1227
+ "use strict";
1228
+ CATEGORY_PATTERNS = [
1229
+ {
1230
+ category: "decision",
1231
+ patterns: [
1232
+ /\b(decided|chose|picked|selected|opted)\b/i,
1233
+ /\b(switched to|migrated to|moved to|changed to)\b/i,
1234
+ /\b(replaced .+ with|went with|settled on)\b/i,
1235
+ /\b(over|instead of|rather than)\b.*\b(because|due to|since)\b/i
1236
+ ]
1237
+ },
1238
+ {
1239
+ category: "preference",
1240
+ patterns: [
1241
+ /\b(prefer(?:s|red|ring)?)\b/i,
1242
+ /\b(likes?|loved?|enjoys?|hates?|dislikes?)\b/i,
1243
+ /\b(always uses?|never uses?|favorite|favourite)\b/i,
1244
+ /\b(wants? to|doesn't want|rather|instead of)\b/i
1245
+ ]
1246
+ },
1247
+ {
1248
+ category: "event",
1249
+ patterns: [
1250
+ /\b(yesterday|today|this morning|last week|last month)\b/i,
1251
+ /\b(on (monday|tuesday|wednesday|thursday|friday|saturday|sunday))\b/i,
1252
+ /\b(happened|occurred|took place|attended|deployed|launched|released)\b/i,
1253
+ /\b(had a (meeting|call|chat|discussion|session))\b/i
1254
+ ]
1255
+ },
1256
+ {
1257
+ category: "fact",
1258
+ patterns: [
1259
+ /\b(works? at|employed at|job is)\b/i,
1260
+ /\b(lives? in|located in|based in)\b/i,
1261
+ /\b(is a|is an|role is|position is)\b/i,
1262
+ /\b(uses?|runs? on|built with|powered by|written in)\b/i,
1263
+ /\b(name is|called|known as)\b/i,
1264
+ /\b(timezone|time zone)\b/i
1265
+ ]
1266
+ }
1267
+ ];
1268
+ }
1269
+ });
1270
+
1212
1271
  // src/memory/schema.ts
1213
1272
  function initSchema(db3) {
1214
1273
  db3.exec(`
@@ -1628,6 +1687,10 @@ function initSchema(db3) {
1628
1687
  db3.exec(`ALTER TABLE chat_voice ADD COLUMN stt_model TEXT DEFAULT 'small.en'`);
1629
1688
  } catch {
1630
1689
  }
1690
+ try {
1691
+ db3.exec(`ALTER TABLE chat_voice ADD COLUMN stt_echo INTEGER DEFAULT 0`);
1692
+ } catch {
1693
+ }
1631
1694
  db3.exec(`
1632
1695
  CREATE TABLE IF NOT EXISTS backend_limits (
1633
1696
  backend TEXT NOT NULL,
@@ -1754,6 +1817,44 @@ function initSchema(db3) {
1754
1817
  db3.exec("ALTER TABLE session_summaries ADD COLUMN embedding TEXT");
1755
1818
  } catch {
1756
1819
  }
1820
+ try {
1821
+ db3.exec("ALTER TABLE memories ADD COLUMN category TEXT NOT NULL DEFAULT 'uncategorized'");
1822
+ } catch {
1823
+ }
1824
+ try {
1825
+ db3.exec("ALTER TABLE memories ADD COLUMN half_life_days REAL NOT NULL DEFAULT 14.0");
1826
+ } catch {
1827
+ }
1828
+ try {
1829
+ db3.exec("ALTER TABLE memories ADD COLUMN superseded_by INTEGER DEFAULT NULL");
1830
+ } catch {
1831
+ }
1832
+ try {
1833
+ db3.exec("ALTER TABLE memories ADD COLUMN superseded_at TEXT DEFAULT NULL");
1834
+ } catch {
1835
+ }
1836
+ try {
1837
+ const alreadyDone = db3.prepare("SELECT value FROM meta WHERE key = 'schema_category_backfill_done'").get();
1838
+ if (!alreadyDone) {
1839
+ const allUncategorized = db3.prepare("SELECT id, trigger, content FROM memories WHERE category = 'uncategorized'").all();
1840
+ if (allUncategorized.length > 0) {
1841
+ const updateStmt = db3.prepare("UPDATE memories SET category = ? WHERE id = ?");
1842
+ const backfillTxn = db3.transaction(() => {
1843
+ for (const mem of allUncategorized) {
1844
+ const category = classifyMemory(mem.trigger, mem.content);
1845
+ if (category !== "uncategorized") {
1846
+ updateStmt.run(category, mem.id);
1847
+ }
1848
+ }
1849
+ });
1850
+ backfillTxn();
1851
+ log(`[schema] Backfilled categories for ${allUncategorized.length} memories`);
1852
+ }
1853
+ db3.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run("schema_category_backfill_done", "1");
1854
+ }
1855
+ } catch (err) {
1856
+ warn(`[schema] Category backfill skipped: ${err}`);
1857
+ }
1757
1858
  try {
1758
1859
  db3.exec("ALTER TABLE message_log ADD COLUMN message_type TEXT DEFAULT 'text'");
1759
1860
  } catch {
@@ -1975,32 +2076,32 @@ function applySalienceDecay(db3) {
1975
2076
  if (lastDecay) {
1976
2077
  const daysSinceLastDecay = (Date.now() - (/* @__PURE__ */ new Date(lastDecay.replace(" ", "T") + "Z")).getTime()) / (1e3 * 60 * 60 * 24);
1977
2078
  if (daysSinceLastDecay < 0.01) return;
1978
- db3.prepare(`
1979
- UPDATE memories
1980
- SET salience = salience * POWER(0.98, ?)
1981
- WHERE salience >= 0.1
1982
- `).run(daysSinceLastDecay);
1983
2079
  db3.prepare(`
1984
2080
  UPDATE session_summaries
1985
2081
  SET salience = salience * POWER(0.995, ?)
1986
2082
  WHERE salience >= 0.1
1987
2083
  `).run(daysSinceLastDecay);
1988
2084
  } else {
1989
- db3.prepare(`
1990
- UPDATE memories
1991
- SET salience = salience * POWER(0.98, julianday('now') - julianday(last_accessed))
1992
- WHERE salience >= 0.1
1993
- `).run();
1994
2085
  db3.prepare(`
1995
2086
  UPDATE session_summaries
1996
2087
  SET salience = salience * POWER(0.995, julianday('now') - julianday(created_at))
1997
2088
  WHERE salience >= 0.1
1998
2089
  `).run();
1999
2090
  }
2000
- db3.prepare("DELETE FROM memories WHERE salience < 0.1").run();
2001
2091
  db3.prepare("DELETE FROM session_summaries WHERE salience < 0.1").run();
2002
2092
  db3.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('last_salience_decay', ?)").run(now);
2003
2093
  }
2094
+ function getMetaValue(key) {
2095
+ const { getDb: getDb2 } = (init_store5(), __toCommonJS(store_exports5));
2096
+ const db3 = getDb2();
2097
+ const row = db3.prepare("SELECT value FROM meta WHERE key = ?").get(key);
2098
+ return row?.value;
2099
+ }
2100
+ function setMetaValue(key, value) {
2101
+ const { getDb: getDb2 } = (init_store5(), __toCommonJS(store_exports5));
2102
+ const db3 = getDb2();
2103
+ db3.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run(key, value);
2104
+ }
2004
2105
  function backfillJobTitles(database) {
2005
2106
  const rows = database.prepare(
2006
2107
  "SELECT id, description FROM jobs WHERE active = 1 AND (title IS NULL OR title LIKE 'No thinking%' OR title LIKE 'You must%' OR title LIKE 'You MUST%')"
@@ -2032,6 +2133,8 @@ var init_schema = __esm({
2032
2133
  init_store3();
2033
2134
  init_identity();
2034
2135
  init_store4();
2136
+ init_log();
2137
+ init_classify();
2035
2138
  }
2036
2139
  });
2037
2140
 
@@ -2311,33 +2414,32 @@ var init_embeddings = __esm({
2311
2414
  });
2312
2415
 
2313
2416
  // src/memory/memories.ts
2314
- function queueMemorySalienceBoost(memoryId, boost) {
2315
- const current = pendingMemorySalienceUpdates.get(memoryId) ?? 0;
2316
- pendingMemorySalienceUpdates.set(memoryId, current + boost);
2317
- if (!memorySalienceFlushTimer) {
2318
- memorySalienceFlushTimer = setTimeout(flushMemorySalienceUpdates, 6e4);
2417
+ function queueHalfLifeExtension(memoryId) {
2418
+ pendingHalfLifeExtensions.set(memoryId, (pendingHalfLifeExtensions.get(memoryId) ?? 0) + 1);
2419
+ if (!memoryHalfLifeFlushTimer) {
2420
+ memoryHalfLifeFlushTimer = setTimeout(flushMemoryHalfLifeUpdates, 6e4);
2319
2421
  }
2320
2422
  }
2321
- function flushMemorySalienceUpdates() {
2322
- if (pendingMemorySalienceUpdates.size === 0) {
2323
- memorySalienceFlushTimer = null;
2423
+ function flushMemoryHalfLifeUpdates() {
2424
+ if (pendingHalfLifeExtensions.size === 0) {
2425
+ memoryHalfLifeFlushTimer = null;
2324
2426
  return;
2325
2427
  }
2326
2428
  try {
2327
2429
  const db3 = getDb();
2328
2430
  const stmt = db3.prepare(
2329
- "UPDATE memories SET salience = MAX(0, MIN(1.0, salience + ?)), access_count = access_count + 1, last_accessed = datetime('now') WHERE id = ?"
2431
+ "UPDATE memories SET half_life_days = MIN(90, half_life_days + 2), access_count = access_count + 1, last_accessed = datetime('now') WHERE id = ?"
2330
2432
  );
2331
2433
  const txn = db3.transaction(() => {
2332
- for (const [id, boost] of pendingMemorySalienceUpdates) {
2333
- stmt.run(boost, id);
2434
+ for (const [id] of pendingHalfLifeExtensions) {
2435
+ stmt.run(id);
2334
2436
  }
2335
2437
  });
2336
2438
  txn();
2337
2439
  } catch {
2338
2440
  }
2339
- pendingMemorySalienceUpdates.clear();
2340
- memorySalienceFlushTimer = null;
2441
+ pendingHalfLifeExtensions.clear();
2442
+ memoryHalfLifeFlushTimer = null;
2341
2443
  }
2342
2444
  function invalidateMemoryEmbeddingCache() {
2343
2445
  embeddingCache = null;
@@ -2361,7 +2463,7 @@ function getMemoriesWithoutEmbeddings(limit = 100) {
2361
2463
  }
2362
2464
  function getAllMemoriesWithEmbeddingsUncached() {
2363
2465
  return getDb().prepare(
2364
- "SELECT * FROM memories WHERE embedding IS NOT NULL AND salience >= 0.1"
2466
+ "SELECT * FROM memories WHERE embedding IS NOT NULL AND salience >= 0.1 AND superseded_by IS NULL"
2365
2467
  ).all();
2366
2468
  }
2367
2469
  function getAllMemoriesWithEmbeddings() {
@@ -2373,8 +2475,13 @@ function getAllMemoriesWithEmbeddings() {
2373
2475
  embeddingCache = { data, timestamp: now };
2374
2476
  return data;
2375
2477
  }
2376
- function saveMemoryWithEmbedding(trigger, content, type = "semantic") {
2377
- const id = saveMemory(trigger, content, type);
2478
+ function saveMemoryWithEmbedding(trigger, content, type = "semantic", category = "uncategorized", halfLifeDays = 14) {
2479
+ const db3 = getDb();
2480
+ const result = db3.prepare(
2481
+ "INSERT INTO memories (trigger, content, type, category, half_life_days) VALUES (?, ?, ?, ?, ?)"
2482
+ ).run(trigger, content, type, category, halfLifeDays);
2483
+ const id = Number(result.lastInsertRowid);
2484
+ invalidateMemoryEmbeddingCache();
2378
2485
  const textToEmbed = `${trigger}: ${content}`;
2379
2486
  embedForStorage(textToEmbed).then((emb) => {
2380
2487
  if (emb) {
@@ -2402,33 +2509,45 @@ function searchMemories(queryText, limit = 3) {
2402
2509
  SELECT m.*, fts.rank AS _ftsRank FROM memories m
2403
2510
  JOIN memories_fts fts ON m.id = fts.rowid
2404
2511
  WHERE memories_fts MATCH ?
2512
+ AND m.superseded_by IS NULL
2405
2513
  ORDER BY fts.rank
2406
2514
  LIMIT ?
2407
2515
  `).all(ftsQuery, limit);
2408
2516
  for (const mem of ftsResults) {
2409
- queueMemorySalienceBoost(mem.id, 0.1);
2517
+ queueHalfLifeExtension(mem.id);
2410
2518
  }
2411
2519
  return ftsResults;
2412
2520
  }
2413
2521
  function searchMemoriesReadOnly(readDb, queryText, limit = 3) {
2414
2522
  const ftsQuery = toFts5Query(queryText);
2415
2523
  if (!ftsQuery) return [];
2416
- return readDb.prepare(`
2417
- SELECT m.*, fts.rank AS _ftsRank FROM memories m
2418
- JOIN memories_fts fts ON m.id = fts.rowid
2419
- WHERE memories_fts MATCH ?
2420
- ORDER BY fts.rank
2421
- LIMIT ?
2422
- `).all(ftsQuery, limit);
2524
+ try {
2525
+ return readDb.prepare(`
2526
+ SELECT m.*, fts.rank AS _ftsRank FROM memories m
2527
+ JOIN memories_fts fts ON m.id = fts.rowid
2528
+ WHERE memories_fts MATCH ?
2529
+ AND m.superseded_by IS NULL
2530
+ ORDER BY fts.rank
2531
+ LIMIT ?
2532
+ `).all(ftsQuery, limit);
2533
+ } catch {
2534
+ return readDb.prepare(`
2535
+ SELECT m.*, fts.rank AS _ftsRank FROM memories m
2536
+ JOIN memories_fts fts ON m.id = fts.rowid
2537
+ WHERE memories_fts MATCH ?
2538
+ ORDER BY fts.rank
2539
+ LIMIT ?
2540
+ `).all(ftsQuery, limit);
2541
+ }
2423
2542
  }
2424
2543
  function getRecentMemories(limit = 5) {
2425
2544
  return getDb().prepare(
2426
- "SELECT * FROM memories ORDER BY last_accessed DESC LIMIT ?"
2545
+ "SELECT * FROM memories WHERE superseded_by IS NULL ORDER BY last_accessed DESC LIMIT ?"
2427
2546
  ).all(limit);
2428
2547
  }
2429
2548
  function listMemories() {
2430
2549
  return getDb().prepare(
2431
- "SELECT * FROM memories WHERE salience >= 0.1 ORDER BY salience DESC LIMIT 50"
2550
+ "SELECT * FROM memories WHERE salience >= 0.1 AND superseded_by IS NULL ORDER BY salience DESC LIMIT 50"
2432
2551
  ).all();
2433
2552
  }
2434
2553
  function forgetMemory(keyword) {
@@ -2449,17 +2568,52 @@ function deleteMemoryById(id) {
2449
2568
  }
2450
2569
  function getAllMemoriesForOptimization() {
2451
2570
  return getDb().prepare(
2452
- "SELECT * FROM memories WHERE salience >= 0.1 ORDER BY salience DESC"
2571
+ "SELECT * FROM memories WHERE salience >= 0.1 AND superseded_by IS NULL ORDER BY salience DESC"
2453
2572
  ).all();
2454
2573
  }
2455
2574
  function restoreMemory(memory2) {
2456
- getDb().prepare(`
2457
- INSERT OR REPLACE INTO memories (id, trigger, content, type, salience, access_count, created_at, last_accessed, embedding)
2458
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
2459
- `).run(memory2.id, memory2.trigger, memory2.content, memory2.type, memory2.salience, memory2.access_count, memory2.created_at, memory2.last_accessed, memory2.embedding);
2575
+ const db3 = getDb();
2576
+ db3.prepare(
2577
+ `INSERT OR REPLACE INTO memories
2578
+ (id, trigger, content, type, salience, access_count, created_at, last_accessed, embedding,
2579
+ category, half_life_days, superseded_by, superseded_at)
2580
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2581
+ ).run(
2582
+ memory2.id,
2583
+ memory2.trigger,
2584
+ memory2.content,
2585
+ memory2.type,
2586
+ memory2.salience,
2587
+ memory2.access_count,
2588
+ memory2.created_at,
2589
+ memory2.last_accessed,
2590
+ memory2.embedding,
2591
+ memory2.category ?? "uncategorized",
2592
+ memory2.half_life_days ?? 14,
2593
+ memory2.superseded_by ?? null,
2594
+ memory2.superseded_at ?? null
2595
+ );
2460
2596
  invalidateMemoryEmbeddingCache();
2461
2597
  }
2462
- var embeddingCache, EMBEDDING_CACHE_TTL_MS, pendingMemorySalienceUpdates, memorySalienceFlushTimer, STOP_WORDS;
2598
+ function markSuperseded(memoryId, supersededById) {
2599
+ if (memoryId === supersededById) return;
2600
+ const db3 = getDb();
2601
+ db3.prepare(
2602
+ `UPDATE memories SET superseded_by = ?, superseded_at = datetime('now') WHERE id = ?`
2603
+ ).run(supersededById, memoryId);
2604
+ }
2605
+ function cleanupSuperseded(gracePeriodDays = 14) {
2606
+ const db3 = getDb();
2607
+ const result = db3.prepare(
2608
+ `DELETE FROM memories WHERE superseded_by IS NOT NULL AND superseded_at < datetime('now', '-' || ? || ' days')`
2609
+ ).run(gracePeriodDays);
2610
+ return result.changes;
2611
+ }
2612
+ function updateMemoryCategory(id, category) {
2613
+ const db3 = getDb();
2614
+ db3.prepare("UPDATE memories SET category = ? WHERE id = ?").run(category, id);
2615
+ }
2616
+ var embeddingCache, EMBEDDING_CACHE_TTL_MS, pendingHalfLifeExtensions, memoryHalfLifeFlushTimer, STOP_WORDS;
2463
2617
  var init_memories = __esm({
2464
2618
  "src/memory/memories.ts"() {
2465
2619
  "use strict";
@@ -2468,8 +2622,8 @@ var init_memories = __esm({
2468
2622
  init_log();
2469
2623
  embeddingCache = null;
2470
2624
  EMBEDDING_CACHE_TTL_MS = 3e4;
2471
- pendingMemorySalienceUpdates = /* @__PURE__ */ new Map();
2472
- memorySalienceFlushTimer = null;
2625
+ pendingHalfLifeExtensions = /* @__PURE__ */ new Map();
2626
+ memoryHalfLifeFlushTimer = null;
2473
2627
  STOP_WORDS = /* @__PURE__ */ new Set([
2474
2628
  "a",
2475
2629
  "an",
@@ -4492,6 +4646,175 @@ var init_api_context = __esm({
4492
4646
  }
4493
4647
  });
4494
4648
 
4649
+ // src/text-utils.ts
4650
+ var text_utils_exports = {};
4651
+ __export(text_utils_exports, {
4652
+ appendTextChunk: () => appendTextChunk,
4653
+ jaccardSimilarity: () => jaccardSimilarity,
4654
+ tokenizeText: () => tokenizeText
4655
+ });
4656
+ function appendTextChunk(accumulated, chunk) {
4657
+ if (!accumulated) return chunk;
4658
+ if (!chunk) return accumulated;
4659
+ return accumulated + chunk;
4660
+ }
4661
+ function tokenizeText(text, minLen = 3) {
4662
+ return new Set(
4663
+ text.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length >= minLen)
4664
+ );
4665
+ }
4666
+ function jaccardSimilarity(a, b) {
4667
+ if (a.size === 0 && b.size === 0) return 0;
4668
+ let intersection = 0;
4669
+ for (const term of a) {
4670
+ if (b.has(term)) intersection++;
4671
+ }
4672
+ const union = a.size + b.size - intersection;
4673
+ return union === 0 ? 0 : intersection / union;
4674
+ }
4675
+ var init_text_utils = __esm({
4676
+ "src/text-utils.ts"() {
4677
+ "use strict";
4678
+ }
4679
+ });
4680
+
4681
+ // src/memory/supersede.ts
4682
+ function detectSupersession(newTrigger, newContent, newCategory, existingMemories, newEmbedding) {
4683
+ if (existingMemories.length === 0) return null;
4684
+ const hasChangeSignal = CHANGE_SIGNALS.some((re) => re.test(newContent));
4685
+ const newTerms = tokenizeText(newContent);
4686
+ let bestId = null;
4687
+ let bestScore = 0;
4688
+ for (const mem of existingMemories) {
4689
+ if (mem.superseded_by != null) continue;
4690
+ let score = 0;
4691
+ if (normalizeTrigger(mem.trigger) === normalizeTrigger(newTrigger)) {
4692
+ score += 3;
4693
+ }
4694
+ if (newCategory !== "uncategorized" && mem.category !== "uncategorized") {
4695
+ if (newCategory === mem.category) {
4696
+ score += 1;
4697
+ } else {
4698
+ continue;
4699
+ }
4700
+ }
4701
+ if (newEmbedding && mem.embedding) {
4702
+ try {
4703
+ const memEmbedding = JSON.parse(mem.embedding);
4704
+ const sim = cosineSimilarity(newEmbedding, memEmbedding);
4705
+ if (sim >= EMBEDDING_THRESHOLD) {
4706
+ score += 3;
4707
+ }
4708
+ } catch {
4709
+ }
4710
+ }
4711
+ const memTerms = tokenizeText(mem.content);
4712
+ const overlap = jaccardSimilarity(newTerms, memTerms);
4713
+ if (overlap >= KEYWORD_OVERLAP_THRESHOLD) {
4714
+ score += 2;
4715
+ }
4716
+ if (hasChangeSignal) {
4717
+ score += 2;
4718
+ }
4719
+ if (score > bestScore) {
4720
+ bestScore = score;
4721
+ bestId = mem.id;
4722
+ }
4723
+ }
4724
+ return bestScore >= SUPERSESSION_SCORE_THRESHOLD ? bestId : null;
4725
+ }
4726
+ function normalizeTrigger(trigger) {
4727
+ return trigger.toLowerCase().trim().replace(/[\s_-]+/g, "-");
4728
+ }
4729
+ var CHANGE_SIGNALS, EMBEDDING_THRESHOLD, KEYWORD_OVERLAP_THRESHOLD, SUPERSESSION_SCORE_THRESHOLD;
4730
+ var init_supersede = __esm({
4731
+ "src/memory/supersede.ts"() {
4732
+ "use strict";
4733
+ init_embeddings();
4734
+ init_text_utils();
4735
+ CHANGE_SIGNALS = [
4736
+ /\bswitched to\b/i,
4737
+ /\bchanged to\b/i,
4738
+ /\bnow uses?\b/i,
4739
+ /\bno longer\b/i,
4740
+ /\breplaced\b/i,
4741
+ /\bmigrated to\b/i,
4742
+ /\bmoved to\b/i,
4743
+ /\bstopped using\b/i,
4744
+ /\bupdated to\b/i
4745
+ ];
4746
+ EMBEDDING_THRESHOLD = 0.75;
4747
+ KEYWORD_OVERLAP_THRESHOLD = 0.4;
4748
+ SUPERSESSION_SCORE_THRESHOLD = 4;
4749
+ }
4750
+ });
4751
+
4752
+ // src/memory/engine.ts
4753
+ var engine_exports = {};
4754
+ __export(engine_exports, {
4755
+ cleanup: () => cleanup,
4756
+ forget: () => forget,
4757
+ list: () => list,
4758
+ recall: () => recall,
4759
+ remember: () => remember
4760
+ });
4761
+ async function remember(tag, content, opts = {}) {
4762
+ const category = opts.category ?? classifyMemory(tag, content);
4763
+ const type = opts.type ?? "semantic";
4764
+ const candidates = getAllMemoriesWithEmbeddings();
4765
+ const id = saveMemoryWithEmbedding(tag, content, type, category, DEFAULT_HALF_LIFE);
4766
+ let supersededId = null;
4767
+ try {
4768
+ const queryEmbedding = await embedQuery(content);
4769
+ supersededId = detectSupersession(tag, content, category, candidates, queryEmbedding);
4770
+ if (supersededId != null) {
4771
+ markSuperseded(supersededId, id);
4772
+ log(`[engine] Memory #${supersededId} superseded by #${id} ("${tag}")`);
4773
+ }
4774
+ } catch (err) {
4775
+ log(`[engine] Supersession check skipped: ${err}`);
4776
+ }
4777
+ const result = { id, category };
4778
+ if (supersededId != null) result.superseded = supersededId;
4779
+ return result;
4780
+ }
4781
+ function recall(query, opts = {}) {
4782
+ const limit = opts.limit ?? 5;
4783
+ let results = searchMemories(query, limit);
4784
+ if (opts.category) {
4785
+ results = results.filter((m) => m.category === opts.category);
4786
+ }
4787
+ return results;
4788
+ }
4789
+ function list(_opts = {}) {
4790
+ return listMemories();
4791
+ }
4792
+ function forget(keywordOrId) {
4793
+ if (typeof keywordOrId === "number") {
4794
+ return deleteMemoryById(keywordOrId) ? 1 : 0;
4795
+ }
4796
+ return forgetMemory(keywordOrId);
4797
+ }
4798
+ function cleanup(gracePeriodDays = 14) {
4799
+ const deleted = cleanupSuperseded(gracePeriodDays);
4800
+ if (deleted > 0) {
4801
+ log(`[engine] Cleaned up ${deleted} superseded memories past ${gracePeriodDays}-day grace period`);
4802
+ }
4803
+ return deleted;
4804
+ }
4805
+ var DEFAULT_HALF_LIFE;
4806
+ var init_engine = __esm({
4807
+ "src/memory/engine.ts"() {
4808
+ "use strict";
4809
+ init_memories();
4810
+ init_classify();
4811
+ init_supersede();
4812
+ init_embeddings();
4813
+ init_log();
4814
+ DEFAULT_HALF_LIFE = 14;
4815
+ }
4816
+ });
4817
+
4495
4818
  // src/memory/store.ts
4496
4819
  var store_exports5 = {};
4497
4820
  __export(store_exports5, {
@@ -4508,7 +4831,10 @@ __export(store_exports5, {
4508
4831
  buildApiMessages: () => buildApiMessages,
4509
4832
  cancelJobById: () => cancelJobById,
4510
4833
  checkBackendLimits: () => checkBackendLimits,
4834
+ classifyMemory: () => classifyMemory,
4511
4835
  cleanExpiredWatches: () => cleanExpiredWatches,
4836
+ cleanup: () => cleanup,
4837
+ cleanupSuperseded: () => cleanupSuperseded,
4512
4838
  clearAgentMode: () => clearAgentMode,
4513
4839
  clearAllPaidSlots: () => clearAllPaidSlots,
4514
4840
  clearAllSessions: () => clearAllSessions,
@@ -4529,11 +4855,13 @@ __export(store_exports5, {
4529
4855
  deleteBookmark: () => deleteBookmark,
4530
4856
  deleteMemoryById: () => deleteMemoryById,
4531
4857
  deleteSessionSummary: () => deleteSessionSummary,
4858
+ detectSupersession: () => detectSupersession,
4532
4859
  determineEscalationTarget: () => determineEscalationTarget,
4533
4860
  estimateTokens: () => estimateTokens,
4534
4861
  findBookmarksByPrefix: () => findBookmarksByPrefix,
4535
- flushMemorySalienceUpdates: () => flushMemorySalienceUpdates,
4862
+ flushMemoryHalfLifeUpdates: () => flushMemoryHalfLifeUpdates,
4536
4863
  flushSummarySalienceUpdates: () => flushSummarySalienceUpdates,
4864
+ forget: () => forget,
4537
4865
  forgetMemory: () => forgetMemory,
4538
4866
  getActiveJobs: () => getActiveJobs,
4539
4867
  getActiveWatches: () => getActiveWatches,
@@ -4576,6 +4904,7 @@ __export(store_exports5, {
4576
4904
  getMemoryById: () => getMemoryById,
4577
4905
  getMessagePairById: () => getMessagePairById,
4578
4906
  getMessagePairs: () => getMessagePairs,
4907
+ getMetaValue: () => getMetaValue,
4579
4908
  getMode: () => getMode,
4580
4909
  getModel: () => getModel,
4581
4910
  getModelSignature: () => getModelSignature,
@@ -4606,6 +4935,7 @@ __export(store_exports5, {
4606
4935
  insertJobRun: () => insertJobRun,
4607
4936
  invalidateMemoryEmbeddingCache: () => invalidateMemoryEmbeddingCache,
4608
4937
  invalidateSummaryEmbeddingCache: () => invalidateSummaryEmbeddingCache,
4938
+ list: () => list,
4609
4939
  listMemories: () => listMemories,
4610
4940
  listSessionSummaries: () => listSessionSummaries,
4611
4941
  markBackendSlotExhausted: () => markBackendSlotExhausted,
@@ -4613,14 +4943,18 @@ __export(store_exports5, {
4613
4943
  markMessageLogSummarized: () => markMessageLogSummarized,
4614
4944
  markSlotExhausted: () => markSlotExhausted,
4615
4945
  markSlotSuccess: () => markSlotSuccess,
4946
+ markSuperseded: () => markSuperseded,
4616
4947
  openDatabaseReadOnly: () => openDatabaseReadOnly,
4617
4948
  parseHeartbeatFallbacks: () => parseHeartbeatFallbacks,
4618
4949
  pinChatBackendSlot: () => pinChatBackendSlot,
4619
4950
  pinChatGeminiSlot: () => pinChatGeminiSlot,
4620
4951
  pruneJobRuns: () => pruneJobRuns,
4621
4952
  pruneMessageLog: () => pruneMessageLog,
4953
+ queueHalfLifeExtension: () => queueHalfLifeExtension,
4954
+ recall: () => recall,
4622
4955
  reenableBackendSlot: () => reenableBackendSlot,
4623
4956
  reenableSlot: () => reenableSlot,
4957
+ remember: () => remember,
4624
4958
  removeApiModel: () => removeApiModel,
4625
4959
  removeApiModelById: () => removeApiModelById,
4626
4960
  removeBackendSlot: () => removeBackendSlot,
@@ -4659,6 +4993,7 @@ __export(store_exports5, {
4659
4993
  setGeminiSlotEnabled: () => setGeminiSlotEnabled,
4660
4994
  setGlobalSummarizer: () => setGlobalSummarizer,
4661
4995
  setHeartbeatConfig: () => setHeartbeatConfig,
4996
+ setMetaValue: () => setMetaValue,
4662
4997
  setMode: () => setMode,
4663
4998
  setModel: () => setModel,
4664
4999
  setModelSignature: () => setModelSignature,
@@ -4683,6 +5018,7 @@ __export(store_exports5, {
4683
5018
  updateJobEnabled: () => updateJobEnabled,
4684
5019
  updateJobLastRun: () => updateJobLastRun,
4685
5020
  updateJobNextRun: () => updateJobNextRun,
5021
+ updateMemoryCategory: () => updateMemoryCategory,
4686
5022
  updateMemoryEmbedding: () => updateMemoryEmbedding,
4687
5023
  updateSessionSummaryEmbedding: () => updateSessionSummaryEmbedding,
4688
5024
  upsertBookmark: () => upsertBookmark
@@ -4727,6 +5063,10 @@ var init_store5 = __esm({
4727
5063
  init_backend_slots();
4728
5064
  init_api_models();
4729
5065
  init_api_context();
5066
+ init_schema();
5067
+ init_classify();
5068
+ init_supersede();
5069
+ init_engine();
4730
5070
  _testDbPath = null;
4731
5071
  }
4732
5072
  });
@@ -4945,6 +5285,11 @@ var init_resolve_executable = __esm({
4945
5285
  });
4946
5286
 
4947
5287
  // src/backends/claude.ts
5288
+ var claude_exports = {};
5289
+ __export(claude_exports, {
5290
+ ClaudeAdapter: () => ClaudeAdapter,
5291
+ tryRefreshOAuthSlot: () => tryRefreshOAuthSlot
5292
+ });
4948
5293
  import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync } from "fs";
4949
5294
  import { execFile as execFileCb, spawnSync as spawnSync2 } from "child_process";
4950
5295
  import { promisify } from "util";
@@ -5021,6 +5366,14 @@ async function getValidOAuthToken(configHome) {
5021
5366
  }
5022
5367
  return accessToken;
5023
5368
  }
5369
+ async function tryRefreshOAuthSlot(slot) {
5370
+ if (slot.slotType !== "oauth" || !slot.configHome) return false;
5371
+ const credsPath = join4(slot.configHome, ".claude", ".credentials.json");
5372
+ const oauth = readOAuthCredentials(credsPath);
5373
+ if (!oauth?.refreshToken) return false;
5374
+ const fresh = await refreshOAuthToken(credsPath, oauth.refreshToken, oauth.scopes);
5375
+ return fresh !== null;
5376
+ }
5024
5377
  var execFileAsync, OAUTH_TOKEN_ENDPOINT, OAUTH_CLIENT_ID, REFRESH_THRESHOLD_MS, REFRESH_COOLDOWN_MS, lastRefreshAttempt, ADAPTIVE_MODELS, ClaudeAdapter;
5025
5378
  var init_claude = __esm({
5026
5379
  "src/backends/claude.ts"() {
@@ -6446,7 +6799,8 @@ function isSensitivePath(filePath) {
6446
6799
  if (SENSITIVE_EXACT.has(normalized)) return true;
6447
6800
  return SENSITIVE_PATTERNS.some((pattern) => pattern.test(normalized));
6448
6801
  }
6449
- function createRestrictedBashTool(chatId, permMode, whitelist) {
6802
+ function createRestrictedBashTool(chatId, permMode, opts) {
6803
+ const { whitelist, shellTimeoutMs } = opts ?? {};
6450
6804
  const isYolo = permMode === "yolo";
6451
6805
  return tool({
6452
6806
  description: isYolo ? `Run any shell command. Use this for ALL external CLI tools. No restrictions. Format: restrictedBash({command: 'gsearch "query" --type news'})` : "Run a whitelisted shell command. Use this for ANY external CLI tool \u2014 do NOT call CLIs as separate tools by name, always route them through restrictedBash. Format: restrictedBash({command: 'toolname args...'})",
@@ -6460,12 +6814,18 @@ function createRestrictedBashTool(chatId, permMode, whitelist) {
6460
6814
  return { error: "Empty command" };
6461
6815
  }
6462
6816
  if (!isYolo) {
6463
- const list = whitelist ?? getApiCliWhitelist(chatId);
6464
- const entry = list.find((w) => w.cli === firstWord);
6817
+ const list2 = whitelist ?? getApiCliWhitelist(chatId);
6818
+ const entry = list2.find((w) => w.cli === firstWord);
6465
6819
  if (!entry) {
6466
- const allowed = list.map((w) => w.cli).join(", ") || "(none)";
6820
+ const allowed = list2.map((w) => w.cli).join(", ") || "(none)";
6467
6821
  return {
6468
- error: `Command "${firstWord}" is not whitelisted. Allowed commands: ${allowed}. IMPORTANT: Stop retrying. Tell the user exactly this: "I tried to run '${firstWord}' but it's not in your approved tools list. Go to /tools \u2192 Add Command \u2192 type '${firstWord}' to allow it, then ask me again."`
6822
+ blockedCommand: firstWord,
6823
+ allowedCommands: allowed,
6824
+ userMessage: `\u274C \`${firstWord}\` is not in your approved tools list.
6825
+ Allowed: ${allowed}
6826
+
6827
+ To fix: go to /tools \u2192 Add Command \u2192 type \`${firstWord}\``,
6828
+ instruction: "STOP. Do not retry. Reply to the user with ONLY the userMessage field above, verbatim. Do not add explanation."
6469
6829
  };
6470
6830
  }
6471
6831
  const dangerousPatterns = /[;|&`$(){}]|>>|<<|\n/;
@@ -6474,13 +6834,18 @@ function createRestrictedBashTool(chatId, permMode, whitelist) {
6474
6834
  }
6475
6835
  }
6476
6836
  log(`[api-tools] Executing whitelisted command: ${trimmed.slice(0, 100)}`);
6837
+ const effectiveTimeoutMs = shellTimeoutMs ?? RESTRICTED_BASH_TIMEOUT_MS;
6477
6838
  try {
6478
- const result = await executeShell(trimmed, { timeoutMs: RESTRICTED_BASH_TIMEOUT_MS });
6839
+ const result = await executeShell(trimmed, { timeoutMs: effectiveTimeoutMs });
6479
6840
  return result;
6480
6841
  } catch (err) {
6481
6842
  const msg = err instanceof Error ? err.message : String(err);
6482
6843
  warn(`[api-tools] Shell exec failed: ${msg}`);
6483
- return { error: `Command failed: ${msg}` };
6844
+ const isTimeout = msg.includes("timed out");
6845
+ return isTimeout ? {
6846
+ error: `Command timed out after ${Math.round(effectiveTimeoutMs / 1e3)}s`,
6847
+ suggestion: "Tell the user the command took too long and timed out. They can increase the job timeout via /editjob."
6848
+ } : { error: `Command failed: ${msg}` };
6484
6849
  }
6485
6850
  }
6486
6851
  });
@@ -6642,7 +7007,7 @@ function createWebSearchTool() {
6642
7007
  }
6643
7008
  });
6644
7009
  }
6645
- function buildApiTools(chatId, permMode, mcpTools, webSearchEnabled) {
7010
+ function buildApiTools(chatId, permMode, mcpTools, webSearchEnabled, shellTimeoutMs) {
6646
7011
  const searchEnabled = webSearchEnabled ?? getApiWebSearchEnabled(chatId);
6647
7012
  const readOnly = {
6648
7013
  readFile: createReadFileTool(),
@@ -6654,14 +7019,14 @@ function buildApiTools(chatId, permMode, mcpTools, webSearchEnabled) {
6654
7019
  }
6655
7020
  switch (permMode) {
6656
7021
  case "yolo": {
6657
- const all = { ...readOnly, ...mcpTools ?? {}, restrictedBash: createRestrictedBashTool(chatId, permMode) };
7022
+ const all = { ...readOnly, ...mcpTools ?? {}, restrictedBash: createRestrictedBashTool(chatId, permMode, { shellTimeoutMs }) };
6658
7023
  return all;
6659
7024
  }
6660
7025
  case "safe": {
6661
7026
  const whitelist = getApiCliWhitelist(chatId);
6662
7027
  const base = { ...readOnly, ...mcpTools ?? {} };
6663
7028
  if (whitelist.length === 0) return base;
6664
- return { ...base, restrictedBash: createRestrictedBashTool(chatId, permMode, whitelist) };
7029
+ return { ...base, restrictedBash: createRestrictedBashTool(chatId, permMode, { whitelist, shellTimeoutMs }) };
6665
7030
  }
6666
7031
  case "plan":
6667
7032
  return readOnly;
@@ -6710,7 +7075,7 @@ var init_api_tools = __esm({
6710
7075
  normalize(resolve(DATA_PATH, "api-token")),
6711
7076
  normalize(resolve(CC_CLAW_HOME, ".env"))
6712
7077
  ]);
6713
- RESTRICTED_BASH_TIMEOUT_MS = 12e4;
7078
+ RESTRICTED_BASH_TIMEOUT_MS = 3e5;
6714
7079
  MAX_READ_BYTES = 1e5;
6715
7080
  lastDdgSearchAt = 0;
6716
7081
  DDG_MIN_GAP_MS = 3e3;
@@ -7194,6 +7559,380 @@ var init_html = __esm({
7194
7559
  }
7195
7560
  });
7196
7561
 
7562
+ // src/channels/telegram-throttle.ts
7563
+ var telegram_throttle_exports = {};
7564
+ __export(telegram_throttle_exports, {
7565
+ CircuitState: () => CircuitState,
7566
+ Priority: () => Priority,
7567
+ TelegramThrottle: () => TelegramThrottle,
7568
+ getThrottleState: () => getThrottleState
7569
+ });
7570
+ import { GrammyError } from "grammy";
7571
+ function perChatInterval(chatId) {
7572
+ return parseInt(chatId) < 0 ? PER_GROUP_INTERVAL_MS : PER_DM_INTERVAL_MS;
7573
+ }
7574
+ function getThrottleState() {
7575
+ if (!_activeThrottle) return null;
7576
+ return _activeThrottle.getState();
7577
+ }
7578
+ function is429(err) {
7579
+ return err instanceof GrammyError && err.error_code === 429;
7580
+ }
7581
+ function sleep(ms) {
7582
+ return new Promise((r) => setTimeout(r, ms));
7583
+ }
7584
+ var PER_DM_INTERVAL_MS, PER_GROUP_INTERVAL_MS, GLOBAL_INTERVAL_MS, MAX_RETRIES2, RETRY_DELAY_MS, MAX_QUEUE_SIZE, EDIT_PRESSURE_THRESHOLD, MAX_PER_CHAT_QUEUE, MAX_TOTAL_PAUSE_MS, CIRCUIT_TRIP_THRESHOLD, CIRCUIT_TRIP_WINDOW_MS, CIRCUIT_COOLDOWN_STEP_SEC, CIRCUIT_RESET_WINDOW_MS, CircuitState, Priority, _activeThrottle, TelegramThrottle;
7585
+ var init_telegram_throttle = __esm({
7586
+ "src/channels/telegram-throttle.ts"() {
7587
+ "use strict";
7588
+ init_log();
7589
+ PER_DM_INTERVAL_MS = 1e3;
7590
+ PER_GROUP_INTERVAL_MS = 3500;
7591
+ GLOBAL_INTERVAL_MS = 100;
7592
+ MAX_RETRIES2 = 2;
7593
+ RETRY_DELAY_MS = 1e3;
7594
+ MAX_QUEUE_SIZE = 60;
7595
+ EDIT_PRESSURE_THRESHOLD = MAX_QUEUE_SIZE / 2;
7596
+ MAX_PER_CHAT_QUEUE = 15;
7597
+ MAX_TOTAL_PAUSE_MS = 5 * 60 * 1e3;
7598
+ CIRCUIT_TRIP_THRESHOLD = 3;
7599
+ CIRCUIT_TRIP_WINDOW_MS = 5 * 60 * 1e3;
7600
+ CIRCUIT_COOLDOWN_STEP_SEC = 5;
7601
+ CIRCUIT_RESET_WINDOW_MS = 5 * 60 * 1e3;
7602
+ CircuitState = /* @__PURE__ */ ((CircuitState2) => {
7603
+ CircuitState2["CLOSED"] = "closed";
7604
+ CircuitState2["OPEN"] = "open";
7605
+ CircuitState2["HALF_OPEN"] = "half_open";
7606
+ return CircuitState2;
7607
+ })(CircuitState || {});
7608
+ Priority = /* @__PURE__ */ ((Priority2) => {
7609
+ Priority2[Priority2["P0_CRITICAL"] = 0] = "P0_CRITICAL";
7610
+ Priority2[Priority2["P1_NORMAL"] = 1] = "P1_NORMAL";
7611
+ Priority2[Priority2["P2_COSMETIC"] = 2] = "P2_COSMETIC";
7612
+ return Priority2;
7613
+ })(Priority || {});
7614
+ _activeThrottle = null;
7615
+ TelegramThrottle = class {
7616
+ queue = [];
7617
+ processing = false;
7618
+ lastSendPerChat = /* @__PURE__ */ new Map();
7619
+ perChatQueueCount = /* @__PURE__ */ new Map();
7620
+ // O(1) per-chat depth lookup
7621
+ lastGlobalSend = 0;
7622
+ // Pause state
7623
+ pausedUntil = 0;
7624
+ pauseStartedAt = 0;
7625
+ // Per-chat cosmetic backoff — tryBestEffort() 429s only affect future
7626
+ // best-effort calls for the SAME chat, never triggering a global pause.
7627
+ cosmeticPausedUntil = /* @__PURE__ */ new Map();
7628
+ // Circuit breaker state — tracks repeated 429s and manages recovery
7629
+ circuitState = "closed" /* CLOSED */;
7630
+ circuitTrips = [];
7631
+ // timestamps of recent 429s
7632
+ circuitCooldownUntil = 0;
7633
+ // when OPEN cooldown expires
7634
+ lastSuccessfulSend = 0;
7635
+ // for resetting trip count after 5min of success
7636
+ constructor() {
7637
+ _activeThrottle = this;
7638
+ }
7639
+ /** Enqueue a Telegram API call with automatic pacing and 429 handling.
7640
+ * Priority controls queue insertion order:
7641
+ * P0_CRITICAL — keyboard responses, finalize edits, /stop — always first
7642
+ * P1_NORMAL — agent responses, cron deliveries — default
7643
+ * P2_COSMETIC — live-status streaming edits — dropped first on overflow
7644
+ *
7645
+ * Accepts `Priority | boolean` for backward compatibility during migration.
7646
+ * `true` maps to P0_CRITICAL, `false`/`undefined` maps to P1_NORMAL. */
7647
+ async send(chatId, label2, fn, priority) {
7648
+ const prio = priority === true ? 0 /* P0_CRITICAL */ : typeof priority === "number" ? priority : 1 /* P1_NORMAL */;
7649
+ if (prio === 2 /* P2_COSMETIC */) {
7650
+ if (this.isPaused()) {
7651
+ throw new Error("Throttle paused (rate limit active) \u2014 edit skipped");
7652
+ }
7653
+ if (this.queue.length >= EDIT_PRESSURE_THRESHOLD) {
7654
+ throw new Error("Throttle queue pressured \u2014 edit skipped");
7655
+ }
7656
+ }
7657
+ return new Promise((resolve3, reject) => {
7658
+ if (this.queue.length >= MAX_QUEUE_SIZE) {
7659
+ let dropIdx = -1;
7660
+ for (let i = this.queue.length - 1; i >= 0; i--) {
7661
+ if (this.queue[i].priority === 2 /* P2_COSMETIC */) {
7662
+ dropIdx = i;
7663
+ break;
7664
+ }
7665
+ }
7666
+ if (dropIdx === -1) {
7667
+ for (let i = this.queue.length - 1; i >= 0; i--) {
7668
+ if (this.queue[i].priority === 1 /* P1_NORMAL */) {
7669
+ dropIdx = i;
7670
+ break;
7671
+ }
7672
+ }
7673
+ }
7674
+ if (dropIdx === -1) dropIdx = this.queue.length - 1;
7675
+ const dropped = this.queue.splice(dropIdx, 1)[0];
7676
+ if (dropped) {
7677
+ this.decrementChatCount(dropped.chatId);
7678
+ warn(`[throttle] Queue full (${MAX_QUEUE_SIZE}), dropping P${dropped.priority}: ${dropped.label}`);
7679
+ dropped.reject(new Error("Dropped from send queue (overflow)"));
7680
+ }
7681
+ }
7682
+ const chatQueueCount = this.perChatQueueCount.get(chatId) ?? 0;
7683
+ if (chatQueueCount >= MAX_PER_CHAT_QUEUE && prio !== 0 /* P0_CRITICAL */) {
7684
+ if (prio === 2 /* P2_COSMETIC */) {
7685
+ reject(new Error(`Per-chat queue limit (${MAX_PER_CHAT_QUEUE}) reached \u2014 cosmetic edit dropped`));
7686
+ return;
7687
+ }
7688
+ const p2Idx = this.queue.findIndex((q) => q.chatId === chatId && q.priority === 2 /* P2_COSMETIC */);
7689
+ if (p2Idx >= 0) {
7690
+ const dropped = this.queue.splice(p2Idx, 1)[0];
7691
+ this.decrementChatCount(dropped.chatId);
7692
+ dropped.reject(new Error("Dropped (per-chat P2 eviction)"));
7693
+ } else {
7694
+ reject(new Error(`Per-chat queue limit (${MAX_PER_CHAT_QUEUE}) reached \u2014 normal send dropped`));
7695
+ return;
7696
+ }
7697
+ }
7698
+ const item = { chatId, label: label2, priority: prio, fn, resolve: resolve3, reject };
7699
+ const insertIdx = this.queue.findIndex((q) => q.priority > prio);
7700
+ if (insertIdx === -1) {
7701
+ this.queue.push(item);
7702
+ } else {
7703
+ this.queue.splice(insertIdx, 0, item);
7704
+ }
7705
+ this.perChatQueueCount.set(chatId, (this.perChatQueueCount.get(chatId) ?? 0) + 1);
7706
+ this.drain();
7707
+ });
7708
+ }
7709
+ /**
7710
+ * Best-effort send — drops silently if throttle is paused or queue is pressured.
7711
+ * Used for cosmetic calls (typing indicators, reactions) that should count toward
7712
+ * rate limits but must never queue up or amplify 429 spirals.
7713
+ */
7714
+ async tryBestEffort(chatId, label2, fn, opts) {
7715
+ if (this.isPaused()) return void 0;
7716
+ if (this.queue.length > 10) return void 0;
7717
+ const cosmeticUntil = this.cosmeticPausedUntil.get(chatId) ?? 0;
7718
+ if (Date.now() < cosmeticUntil) return void 0;
7719
+ if (cosmeticUntil > 0) this.cosmeticPausedUntil.delete(chatId);
7720
+ if (!opts?.skipRecord) {
7721
+ const lastChat = this.lastSendPerChat.get(chatId) ?? 0;
7722
+ if (Date.now() - lastChat < perChatInterval(chatId)) return void 0;
7723
+ if (Date.now() - this.lastGlobalSend < GLOBAL_INTERVAL_MS) return void 0;
7724
+ }
7725
+ try {
7726
+ const result = await fn();
7727
+ if (!opts?.skipRecord) this.recordSend(chatId);
7728
+ return result;
7729
+ } catch (err) {
7730
+ if (is429(err)) {
7731
+ const retrySec = err.parameters?.retry_after ?? 10;
7732
+ this.cosmeticPausedUntil.set(chatId, Date.now() + retrySec * 1e3);
7733
+ warn(`[throttle] 429 event (cosmetic)`, JSON.stringify({
7734
+ method: label2,
7735
+ chatId,
7736
+ retry_after: retrySec,
7737
+ queue_depth: this.queue.length,
7738
+ circuit_state: this.circuitState,
7739
+ type: "best_effort"
7740
+ }));
7741
+ }
7742
+ return void 0;
7743
+ }
7744
+ }
7745
+ /** Check whether the throttle is currently paused (rate-limited). */
7746
+ isPaused() {
7747
+ return Date.now() < this.pausedUntil;
7748
+ }
7749
+ /** Get structured state for diagnostics / health checks. */
7750
+ getState() {
7751
+ const now = Date.now();
7752
+ const paused = now < this.pausedUntil;
7753
+ return {
7754
+ isPaused: paused,
7755
+ queueDepth: this.queue.length,
7756
+ pausedUntilMs: this.pausedUntil,
7757
+ pauseRemainingSec: paused ? Math.ceil((this.pausedUntil - now) / 1e3) : 0,
7758
+ circuitState: this.circuitState
7759
+ };
7760
+ }
7761
+ // ── Queue processor ─────────────────────────────────────────────────
7762
+ async drain() {
7763
+ if (this.processing) return;
7764
+ this.processing = true;
7765
+ try {
7766
+ while (this.queue.length > 0) {
7767
+ while (this.isPaused()) {
7768
+ if (this.pauseStartedAt > 0 && Date.now() - this.pauseStartedAt > MAX_TOTAL_PAUSE_MS) {
7769
+ warn(`[throttle] Max pause duration exceeded (${MAX_TOTAL_PAUSE_MS / 6e4}min), dropping ${this.queue.length} items`);
7770
+ this.flushQueueWithError("Telegram rate limit exceeded max wait time");
7771
+ this.pausedUntil = 0;
7772
+ this.pauseStartedAt = 0;
7773
+ break;
7774
+ }
7775
+ const waitMs = Math.min(this.pausedUntil - Date.now(), 5e3);
7776
+ if (waitMs > 0) await sleep(waitMs);
7777
+ }
7778
+ if (this.queue.length === 0) break;
7779
+ this.updateCircuitState();
7780
+ const item = this.selectNextItem();
7781
+ if (!item) {
7782
+ await sleep(1e3);
7783
+ continue;
7784
+ }
7785
+ const lastChat = this.lastSendPerChat.get(item.chatId) ?? 0;
7786
+ const chatWait = perChatInterval(item.chatId) - (Date.now() - lastChat);
7787
+ if (chatWait > 0) await sleep(chatWait);
7788
+ const globalWait = GLOBAL_INTERVAL_MS - (Date.now() - this.lastGlobalSend);
7789
+ if (globalWait > 0) await sleep(globalWait);
7790
+ try {
7791
+ const result = await this.execWithRetry(item.label, item.fn);
7792
+ this.recordSend(item.chatId);
7793
+ this.pauseStartedAt = 0;
7794
+ this.onSuccessfulSend();
7795
+ item.resolve(result);
7796
+ } catch (err) {
7797
+ if (is429(err)) {
7798
+ const retrySec = err.parameters?.retry_after ?? 10;
7799
+ this.enterPause(retrySec, item);
7800
+ continue;
7801
+ }
7802
+ item.reject(err);
7803
+ }
7804
+ }
7805
+ } finally {
7806
+ this.processing = false;
7807
+ }
7808
+ }
7809
+ /**
7810
+ * Select the next queue item to process, respecting circuit breaker state.
7811
+ * Returns the item (already removed from queue) or null if nothing processable.
7812
+ */
7813
+ selectNextItem() {
7814
+ if (this.circuitState === "closed" /* CLOSED */) {
7815
+ return this.dequeue();
7816
+ }
7817
+ if (this.circuitState === "open" /* OPEN */) {
7818
+ while (this.queue.length > 0 && this.queue[0].priority === 2 /* P2_COSMETIC */) {
7819
+ const dropped = this.dequeue();
7820
+ warn(`[throttle] Circuit OPEN \u2014 dropping P2: ${dropped.label}`);
7821
+ dropped.reject(new Error("Circuit breaker OPEN \u2014 cosmetic item dropped"));
7822
+ }
7823
+ if (this.queue.length > 0 && this.queue[0].priority === 0 /* P0_CRITICAL */) {
7824
+ return this.dequeue();
7825
+ }
7826
+ return null;
7827
+ }
7828
+ if (this.circuitState === "half_open" /* HALF_OPEN */) {
7829
+ return this.dequeue();
7830
+ }
7831
+ return this.dequeue();
7832
+ }
7833
+ /**
7834
+ * Check if circuit breaker should transition states.
7835
+ * OPEN → HALF_OPEN when cooldown expires.
7836
+ */
7837
+ updateCircuitState() {
7838
+ if (this.circuitState === "open" /* OPEN */ && Date.now() >= this.circuitCooldownUntil) {
7839
+ this.circuitState = "half_open" /* HALF_OPEN */;
7840
+ log(`[throttle] Circuit breaker: OPEN \u2192 HALF_OPEN (cooldown expired, probing)`);
7841
+ }
7842
+ }
7843
+ /**
7844
+ * Handle successful send — manage circuit breaker recovery.
7845
+ */
7846
+ onSuccessfulSend() {
7847
+ this.lastSuccessfulSend = Date.now();
7848
+ if (this.circuitState === "half_open" /* HALF_OPEN */) {
7849
+ this.circuitState = "closed" /* CLOSED */;
7850
+ log(`[throttle] Circuit breaker: HALF_OPEN \u2192 CLOSED (probe succeeded)`);
7851
+ }
7852
+ if (this.circuitTrips.length > 0) {
7853
+ const lastTrip = this.circuitTrips[this.circuitTrips.length - 1];
7854
+ if (Date.now() - lastTrip > CIRCUIT_RESET_WINDOW_MS) {
7855
+ this.circuitTrips.length = 0;
7856
+ this.circuitTrips = [];
7857
+ log(`[throttle] Circuit breaker: trip count reset after ${CIRCUIT_RESET_WINDOW_MS / 6e4}min of success`);
7858
+ }
7859
+ }
7860
+ }
7861
+ // ── Retry logic (non-429 errors only) ───────────────────────────────
7862
+ async execWithRetry(label2, fn) {
7863
+ for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
7864
+ try {
7865
+ return await fn();
7866
+ } catch (err) {
7867
+ if (is429(err)) throw err;
7868
+ if (attempt < MAX_RETRIES2 && err instanceof GrammyError) {
7869
+ warn(`[throttle] ${label2} attempt ${attempt + 1}/${MAX_RETRIES2} failed (${err.error_code}), retrying`);
7870
+ await sleep(RETRY_DELAY_MS);
7871
+ continue;
7872
+ }
7873
+ throw err;
7874
+ }
7875
+ }
7876
+ throw new Error("unreachable");
7877
+ }
7878
+ // ── Pause management ────────────────────────────────────────────────
7879
+ enterPause(retrySec, failedItem) {
7880
+ this.queue.unshift(failedItem);
7881
+ const now = Date.now();
7882
+ this.circuitTrips.push(now);
7883
+ this.circuitTrips = this.circuitTrips.filter((t) => now - t < CIRCUIT_TRIP_WINDOW_MS);
7884
+ const bufferSec = this.circuitTrips.length * CIRCUIT_COOLDOWN_STEP_SEC;
7885
+ const totalPauseSec = retrySec + bufferSec;
7886
+ this.pausedUntil = now + totalPauseSec * 1e3;
7887
+ if (this.pauseStartedAt === 0) this.pauseStartedAt = now;
7888
+ if (this.circuitTrips.length >= CIRCUIT_TRIP_THRESHOLD && this.circuitState !== "open" /* OPEN */) {
7889
+ this.circuitState = "open" /* OPEN */;
7890
+ this.circuitCooldownUntil = now + totalPauseSec * 1e3;
7891
+ warn(`[throttle] Circuit breaker TRIPPED \u2192 OPEN (${this.circuitTrips.length} 429s in ${CIRCUIT_TRIP_WINDOW_MS / 6e4}min)`);
7892
+ } else if (this.circuitState === "half_open" /* HALF_OPEN */) {
7893
+ this.circuitState = "open" /* OPEN */;
7894
+ this.circuitCooldownUntil = now + totalPauseSec * 1e3;
7895
+ warn(`[throttle] Circuit breaker probe FAILED \u2192 OPEN (retry_after=${retrySec}s + ${bufferSec}s buffer)`);
7896
+ }
7897
+ warn(`[throttle] 429 event`, JSON.stringify({
7898
+ method: failedItem.label,
7899
+ chatId: failedItem.chatId,
7900
+ priority: failedItem.priority,
7901
+ retry_after: retrySec,
7902
+ buffer: bufferSec,
7903
+ total_pause: totalPauseSec,
7904
+ queue_depth: this.queue.length,
7905
+ circuit_state: this.circuitState,
7906
+ circuit_trip_count: this.circuitTrips.length
7907
+ }));
7908
+ }
7909
+ // ── Helpers ─────────────────────────────────────────────────────────
7910
+ recordSend(chatId) {
7911
+ const now = Date.now();
7912
+ this.lastSendPerChat.set(chatId, now);
7913
+ this.lastGlobalSend = now;
7914
+ }
7915
+ /** Remove and return the first item from the queue, updating per-chat counter. */
7916
+ dequeue() {
7917
+ const item = this.queue.shift();
7918
+ if (item) this.decrementChatCount(item.chatId);
7919
+ return item ?? null;
7920
+ }
7921
+ decrementChatCount(chatId) {
7922
+ const count = (this.perChatQueueCount.get(chatId) ?? 1) - 1;
7923
+ if (count <= 0) this.perChatQueueCount.delete(chatId);
7924
+ else this.perChatQueueCount.set(chatId, count);
7925
+ }
7926
+ flushQueueWithError(message) {
7927
+ while (this.queue.length > 0) {
7928
+ const item = this.dequeue();
7929
+ item.reject(new Error(message));
7930
+ }
7931
+ }
7932
+ };
7933
+ }
7934
+ });
7935
+
7197
7936
  // src/dashboard/routes/system.ts
7198
7937
  var handleHealth, handleJobs, handleMemories, handleStats, handleAgents, handleTasks, handleOrchestrations;
7199
7938
  var init_system = __esm({
@@ -7205,8 +7944,10 @@ var init_system = __esm({
7205
7944
  init_store5();
7206
7945
  init_store();
7207
7946
  init_version();
7947
+ init_telegram_throttle();
7208
7948
  handleHealth = (_req, res) => {
7209
- jsonResponse(res, { status: "ok", version: VERSION, uptime: process.uptime() });
7949
+ const throttle = getThrottleState();
7950
+ jsonResponse(res, { status: "ok", version: VERSION, uptime: process.uptime(), throttle: throttle ?? void 0 });
7210
7951
  };
7211
7952
  handleJobs = (_req, res) => {
7212
7953
  jsonResponse(res, listJobs());
@@ -7571,7 +8312,7 @@ function buildContextPrefix(msg) {
7571
8312
  }
7572
8313
  async function sendOrEditKeyboard(chatId, channel, messageId, text, buttons) {
7573
8314
  if (messageId && typeof channel.editKeyboard === "function") {
7574
- const ok = await channel.editKeyboard(chatId, messageId, text, buttons);
8315
+ const ok = await channel.editKeyboard(chatId, messageId, text, buttons, { priority: 0 /* P0_CRITICAL */ });
7575
8316
  if (ok) return messageId;
7576
8317
  }
7577
8318
  if (typeof channel.sendKeyboard === "function") {
@@ -7584,6 +8325,7 @@ var TONE_PATTERNS, BLOCKED_PATH_PATTERNS, CLI_INSTALL_HINTS, PERM_MODES, VERBOSE
7584
8325
  var init_helpers = __esm({
7585
8326
  "src/router/helpers.ts"() {
7586
8327
  "use strict";
8328
+ init_telegram_throttle();
7587
8329
  init_store5();
7588
8330
  init_backends();
7589
8331
  TONE_PATTERNS = [
@@ -7767,6 +8509,21 @@ You have access to cc-claw orchestrator tools via MCP:
7767
8509
  });
7768
8510
 
7769
8511
  // src/bootstrap/templates/shared.ts
8512
+ function buildCliEnvironment() {
8513
+ return [
8514
+ "## CLI Execution Environment",
8515
+ "",
8516
+ `Your user's real HOME directory is: \`${REAL_HOME}\``,
8517
+ "",
8518
+ "CC-Claw may redirect HOME for credential isolation when spawning backends.",
8519
+ "If you execute any CLI tool (cc-claw, claude, gemini, codex, python3, npm, etc.),",
8520
+ "always prefix the command with the real HOME:",
8521
+ "",
8522
+ `\`HOME=${REAL_HOME} <command>\``,
8523
+ "",
8524
+ "Without this, CLI tools will fail to find their configurations or prompt for login."
8525
+ ].join("\n");
8526
+ }
7770
8527
  function getCurrentDate() {
7771
8528
  return (/* @__PURE__ */ new Date()).toLocaleDateString("en-CA");
7772
8529
  }
@@ -7849,10 +8606,12 @@ function buildHandoffContract() {
7849
8606
  "**Open questions:** Anything unresolved or needing the main agent's attention."
7850
8607
  ].join("\n");
7851
8608
  }
7852
- var TASK_WORKER_SKILL_BUDGET;
8609
+ var REAL_HOME, TASK_WORKER_SKILL_BUDGET;
7853
8610
  var init_shared = __esm({
7854
8611
  "src/bootstrap/templates/shared.ts"() {
7855
8612
  "use strict";
8613
+ init_paths();
8614
+ REAL_HOME = resolveRealHome();
7856
8615
  TASK_WORKER_SKILL_BUDGET = 12e3;
7857
8616
  }
7858
8617
  });
@@ -8346,8 +9105,8 @@ function checkBudget(db3, orchestrationId) {
8346
9105
  }
8347
9106
  const percentUsed = totalCost / budgetLimit * 100;
8348
9107
  const exceeded = totalCost >= budgetLimit;
8349
- const warning4 = percentUsed >= BUDGET_WARNING_PERCENT * 100;
8350
- return { totalCost, budgetLimit, percentUsed, exceeded, warning: warning4 };
9108
+ const warning3 = percentUsed >= BUDGET_WARNING_PERCENT * 100;
9109
+ return { totalCost, budgetLimit, percentUsed, exceeded, warning: warning3 };
8351
9110
  }
8352
9111
  function recordAgentCost(db3, orchestrationId, agentCost) {
8353
9112
  updateOrchestrationCost(db3, orchestrationId, agentCost);
@@ -8737,15 +9496,15 @@ import { existsSync as existsSync12 } from "fs";
8737
9496
  async function withRunnerLock(runnerId, fn) {
8738
9497
  const prev = runnerLocks.get(runnerId) ?? Promise.resolve();
8739
9498
  const next = prev.then(fn, () => fn());
8740
- const cleanup = next.then(
9499
+ const cleanup2 = next.then(
8741
9500
  () => {
8742
- if (runnerLocks.get(runnerId) === cleanup) runnerLocks.delete(runnerId);
9501
+ if (runnerLocks.get(runnerId) === cleanup2) runnerLocks.delete(runnerId);
8743
9502
  },
8744
9503
  () => {
8745
- if (runnerLocks.get(runnerId) === cleanup) runnerLocks.delete(runnerId);
9504
+ if (runnerLocks.get(runnerId) === cleanup2) runnerLocks.delete(runnerId);
8746
9505
  }
8747
9506
  );
8748
- runnerLocks.set(runnerId, cleanup);
9507
+ runnerLocks.set(runnerId, cleanup2);
8749
9508
  return next;
8750
9509
  }
8751
9510
  function safeParseJsonArray(json) {
@@ -10060,6 +10819,9 @@ var init_ndjson = __esm({
10060
10819
  });
10061
10820
 
10062
10821
  // src/memory/inject.ts
10822
+ function memoryDecayRate(halfLifeDays) {
10823
+ return 1 - Math.pow(0.5, 1 / halfLifeDays);
10824
+ }
10063
10825
  function getTopK(query) {
10064
10826
  const wordCount2 = query.split(/\s+/).length;
10065
10827
  const scale = wordCount2 <= 3 ? 0.5 : wordCount2 <= 8 ? 0.75 : 1;
@@ -10094,13 +10856,14 @@ function vectorSearch(queryEmbedding, items, topK) {
10094
10856
  scored.sort((a, b) => b.score - a.score);
10095
10857
  return scored.slice(0, topK);
10096
10858
  }
10097
- function mergeAndScore(allItems, vectorScores, ftsScores, getDays, decayRate, topK) {
10859
+ function mergeAndScore(allItems, vectorScores, ftsScores, getDays, getDecayRate, topK) {
10098
10860
  const vw = getVectorWeight();
10099
10861
  const results = [];
10100
10862
  for (const [id, item] of allItems) {
10101
10863
  const vs = vectorScores.get(id) ?? 0;
10102
10864
  const fs = ftsScores.get(id) ?? 0;
10103
10865
  if (vs === 0 && fs === 0) continue;
10866
+ const decayRate = typeof getDecayRate === "function" ? getDecayRate(item) : getDecayRate;
10104
10867
  const score = hybridScore({
10105
10868
  id,
10106
10869
  vectorScore: vs,
@@ -10122,10 +10885,11 @@ function consumeContextBridge(chatId) {
10122
10885
  pendingContextBridges.delete(chatId);
10123
10886
  return bridge;
10124
10887
  }
10125
- function buildContextBridge(chatId, pairs = 15) {
10888
+ function buildContextBridge(chatId, pairs = 15, interrupted = false) {
10126
10889
  const rows = getRecentMessageLog(chatId, pairs * 2).reverse();
10127
10890
  if (rows.length === 0) return null;
10128
- const lines = [`[Conversation history \u2014 continued from previous session]`];
10891
+ const header2 = interrupted ? `[Previous session was stopped by the user mid-task \u2014 for context only]` : `[Conversation history \u2014 continued from previous session]`;
10892
+ const lines = [header2];
10129
10893
  let totalChars = lines[0].length;
10130
10894
  for (const row of rows) {
10131
10895
  const backendLabel = row.backend ?? "unknown";
@@ -10143,7 +10907,8 @@ function buildContextBridge(chatId, pairs = 15) {
10143
10907
  lines.push(line);
10144
10908
  totalChars += line.length;
10145
10909
  }
10146
- lines.push(`[End of recent history \u2014 you are continuing this conversation]`);
10910
+ const footer = interrupted ? `[End of stopped session \u2014 do NOT continue the previous approach; wait for the user's new instruction]` : `[End of recent history \u2014 you are continuing this conversation]`;
10911
+ lines.push(footer);
10147
10912
  return lines.join("\n");
10148
10913
  }
10149
10914
  function formatToolSummary(toolName, toolInput, toolOutput) {
@@ -10209,7 +10974,7 @@ async function injectMemoryContext(userMessage, chatId) {
10209
10974
  memVectorScores,
10210
10975
  memFtsScores,
10211
10976
  (m) => daysSince(m.last_accessed),
10212
- MEMORY_DECAY_RATE,
10977
+ (m) => memoryDecayRate(m.half_life_days ?? 14),
10213
10978
  FINAL_TOP_K_MEMORIES
10214
10979
  );
10215
10980
  const topSessions = mergeAndScore(
@@ -10235,10 +11000,13 @@ async function injectMemoryContext(userMessage, chatId) {
10235
11000
  }
10236
11001
  combinedSessions = ftsSessions.slice(0, FINAL_TOP_K_SESSIONS);
10237
11002
  }
11003
+ for (const mem of combinedMemories) {
11004
+ queueHalfLifeExtension(mem.id);
11005
+ }
10238
11006
  if (combinedMemories.length === 0 && combinedSessions.length === 0) return null;
10239
11007
  const lines = [];
10240
11008
  for (const m of combinedMemories) {
10241
- let text = `- [${m.type}] ${m.trigger}: ${m.content}`;
11009
+ let text = `- [${m.category}] ${m.trigger}: ${m.content}`;
10242
11010
  if (text.length > MAX_MEMORY_CHARS) text = text.slice(0, MAX_MEMORY_CHARS) + "\u2026";
10243
11011
  lines.push(text);
10244
11012
  }
@@ -10252,13 +11020,12 @@ async function injectMemoryContext(userMessage, chatId) {
10252
11020
  ${lines.join("\n")}
10253
11021
  [End memory context]`;
10254
11022
  }
10255
- var MEMORY_DECAY_RATE, SESSION_DECAY_RATE, BASE_VECTOR_TOP_K, BASE_FTS_TOP_K, FINAL_TOP_K_MEMORIES, FINAL_TOP_K_SESSIONS, MAX_MEMORY_CHARS, MAX_SESSION_CHARS, MAX_BRIDGE_CHARS, pendingContextBridges, TOOL_SUMMARY_MAX_CHARS;
11023
+ var SESSION_DECAY_RATE, BASE_VECTOR_TOP_K, BASE_FTS_TOP_K, FINAL_TOP_K_MEMORIES, FINAL_TOP_K_SESSIONS, MAX_MEMORY_CHARS, MAX_SESSION_CHARS, MAX_BRIDGE_CHARS, pendingContextBridges, TOOL_SUMMARY_MAX_CHARS;
10256
11024
  var init_inject = __esm({
10257
11025
  "src/memory/inject.ts"() {
10258
11026
  "use strict";
10259
11027
  init_store5();
10260
11028
  init_embeddings();
10261
- MEMORY_DECAY_RATE = parseFloat(process.env.CC_CLAW_MEMORY_DECAY_RATE ?? "0.02");
10262
11029
  SESSION_DECAY_RATE = parseFloat(process.env.CC_CLAW_SESSION_DECAY_RATE ?? "0.005");
10263
11030
  BASE_VECTOR_TOP_K = 20;
10264
11031
  BASE_FTS_TOP_K = 20;
@@ -10448,6 +11215,7 @@ ${buildToolUsageRules()}
10448
11215
 
10449
11216
  <platform_reference>
10450
11217
  ${buildSystemCapabilities()}
11218
+ ${buildCliEnvironment()}
10451
11219
  ${buildPlatformQuickReference()}
10452
11220
  </platform_reference>
10453
11221
 
@@ -10493,6 +11261,8 @@ ${user}
10493
11261
 
10494
11262
  ${buildSystemCapabilities()}
10495
11263
 
11264
+ ${buildCliEnvironment()}
11265
+
10496
11266
  ${buildPlatformQuickReference()}
10497
11267
 
10498
11268
  ${buildToolUsageRules()}
@@ -10588,6 +11358,7 @@ ${buildDatabaseSafetyBoundary()}
10588
11358
 
10589
11359
  <platform_reference>
10590
11360
  ${buildSystemCapabilities()}
11361
+ ${buildCliEnvironment()}
10591
11362
  ${buildPlatformQuickReference()}
10592
11363
  </platform_reference>`;
10593
11364
  }
@@ -10636,6 +11407,7 @@ ${buildDatabaseSafetyBoundary()}
10636
11407
 
10637
11408
  <platform_reference>
10638
11409
  ${buildSystemCapabilities()}
11410
+ ${buildCliEnvironment()}
10639
11411
  ${buildPlatformQuickReference()}
10640
11412
  </platform_reference>
10641
11413
 
@@ -10676,6 +11448,10 @@ impossible scenarios.
10676
11448
  </avoid_overengineering>
10677
11449
  </tool_guidance>
10678
11450
 
11451
+ <cli_environment>
11452
+ ${buildCliEnvironment()}
11453
+ </cli_environment>
11454
+
10679
11455
  <safety_boundary>
10680
11456
  Take local, reversible actions freely. Confirm before: deleting files, force-pushing,
10681
11457
  amending published commits, posting to external services, or modifying shared infrastructure.
@@ -10742,6 +11518,10 @@ Before finalizing any response, silently check:
10742
11518
  Never invent library APIs, file paths, or documentation. If uncertain, say so.
10743
11519
  </grounding_rules>
10744
11520
 
11521
+ <cli_environment>
11522
+ ${buildCliEnvironment()}
11523
+ </cli_environment>
11524
+
10745
11525
  <safety_boundary>
10746
11526
  Confirm before: deleting files, posting to external services, database schema changes.
10747
11527
  Do not access ~/.cc-claw/data/cc-claw.db directly.
@@ -11011,14 +11791,16 @@ async function assembleBootstrapPrompt(userMessage, entityType = "main", profile
11011
11791
  if (permMode && permMode !== "yolo") {
11012
11792
  sections.push(buildPermissionNotice(permMode));
11013
11793
  }
11014
- if (backendType === "api" && profile !== "minimal") {
11794
+ if (backendType === "api") {
11015
11795
  sections.push(`[API Backend \u2014 Tool Usage]
11016
11796
  You are operating as a direct API model (not a CLI like Claude Code or Gemini CLI).
11017
11797
  External tools (gsearch, pwm, gws, gemcli, nlm, curl, python3, etc.) are accessed ONLY via the \`restrictedBash\` tool.
11018
11798
  NEVER call external CLIs as direct tools by name \u2014 they are not registered as native tools.
11019
11799
  Correct: restrictedBash({"command": "gsearch \\"query\\" --type news"})
11020
11800
  Incorrect: gsearch({"query": "..."}) \u2190 this will fail silently
11021
- If a skill or instruction says to use a CLI tool, always route it through restrictedBash.`);
11801
+ If a skill or instruction says to use a CLI tool, always route it through restrictedBash.
11802
+
11803
+ ${buildCliEnvironment()}`);
11022
11804
  }
11023
11805
  if (responseStyle) {
11024
11806
  if (responseStyle === "concise") {
@@ -11185,6 +11967,12 @@ Use cc_claw_memory for ALL memory operations. Do NOT use your native memory syst
11185
11967
  - To search: cc_claw_memory(action: "recall", query: "...")
11186
11968
  - To list: cc_claw_memory(action: "list")
11187
11969
  - To search history: cc_claw_memory(action: "history", query: "...")
11970
+ When using cc_claw_memory remember, include the category parameter when the memory type is clear:
11971
+ - "fact" \u2014 biographical or situational truths (name, role, location, tools used, tech stack)
11972
+ - "preference" \u2014 likes, dislikes, habits, working style, stated preferences
11973
+ - "event" \u2014 something that happened at a specific time (meetings, deployments, milestones)
11974
+ - "decision" \u2014 a deliberate choice with reasoning (architecture choices, tool selections)
11975
+ Omit category if uncertain \u2014 the system will auto-classify.
11188
11976
  For scheduling: cc_claw_schedule(action: "create", schedule: "...", task: "...")
11189
11977
  If an action is not available via MCP tools, fall back to the cc-claw CLI.`;
11190
11978
  if (agentMode === "claw") {
@@ -11220,6 +12008,7 @@ var init_loader2 = __esm({
11220
12008
  init_paths();
11221
12009
  init_inject();
11222
12010
  init_init();
12011
+ init_shared();
11223
12012
  init_log();
11224
12013
  init_store3();
11225
12014
  init_store5();
@@ -11258,22 +12047,6 @@ var init_types3 = __esm({
11258
12047
  }
11259
12048
  });
11260
12049
 
11261
- // src/text-utils.ts
11262
- var text_utils_exports = {};
11263
- __export(text_utils_exports, {
11264
- appendTextChunk: () => appendTextChunk
11265
- });
11266
- function appendTextChunk(accumulated, chunk) {
11267
- if (!accumulated) return chunk;
11268
- if (!chunk) return accumulated;
11269
- return accumulated + chunk;
11270
- }
11271
- var init_text_utils = __esm({
11272
- "src/text-utils.ts"() {
11273
- "use strict";
11274
- }
11275
- });
11276
-
11277
12050
  // src/services/ollama/client.ts
11278
12051
  var client_exports = {};
11279
12052
  __export(client_exports, {
@@ -13567,6 +14340,7 @@ async function spawnWithSlotRotation(chatId, adapter, baseConfig, configWithSess
13567
14340
  }
13568
14341
  const maxAttempts = slots.length;
13569
14342
  let lastError;
14343
+ const oauthRefreshRetried = /* @__PURE__ */ new Set();
13570
14344
  for (let i = 0; i < maxAttempts; i++) {
13571
14345
  const slot = getNextBackendSlot(chatId, adapter.id);
13572
14346
  if (!slot) break;
@@ -13584,6 +14358,21 @@ async function spawnWithSlotRotation(chatId, adapter, baseConfig, configWithSess
13584
14358
  if (/rate.?limit|too many requests|429|503|timeout/i.test(errMsg)) {
13585
14359
  throw err;
13586
14360
  }
14361
+ const isOAuthExpiry = slot.slotType === "oauth" && /OAuth token has expired/i.test(errMsg);
14362
+ if (isOAuthExpiry && !oauthRefreshRetried.has(slot.id)) {
14363
+ oauthRefreshRetried.add(slot.id);
14364
+ try {
14365
+ const { tryRefreshOAuthSlot: tryRefreshOAuthSlot2 } = await Promise.resolve().then(() => (init_claude(), claude_exports));
14366
+ const refreshed = await tryRefreshOAuthSlot2(slot);
14367
+ if (refreshed) {
14368
+ log(`[agent:${adapter.id}-rotation] OAuth token refreshed for ${slotLabel} \u2014 retrying`);
14369
+ i--;
14370
+ continue;
14371
+ }
14372
+ } catch {
14373
+ }
14374
+ warn(`[agent:${adapter.id}-rotation] OAuth refresh failed for ${slotLabel} \u2014 rotating`);
14375
+ }
13587
14376
  const isExhausted = /quota|exhausted|exceeded|insufficient|credit|billing|unauthorized|forbidden|401|403/i.test(errMsg);
13588
14377
  if (isExhausted) {
13589
14378
  warn(`[agent:${adapter.id}-rotation] Slot ${slotLabel} exhausted: ${errMsg.slice(0, 200)}`);
@@ -14272,6 +15061,335 @@ var init_format = __esm({
14272
15061
  }
14273
15062
  });
14274
15063
 
15064
+ // src/memory/sweep.ts
15065
+ var sweep_exports = {};
15066
+ __export(sweep_exports, {
15067
+ SWEEP_DEFAULT_CRON: () => SWEEP_DEFAULT_CRON,
15068
+ SWEEP_ENABLED_KEY: () => SWEEP_ENABLED_KEY,
15069
+ SWEEP_JOB_TYPE: () => SWEEP_JOB_TYPE,
15070
+ disableSweep: () => disableSweep,
15071
+ enableSweep: () => enableSweep,
15072
+ findSweepJob: () => findSweepJob,
15073
+ runWeeklySweep: () => runWeeklySweep
15074
+ });
15075
+ async function runWeeklySweep(chatId, channel, backendId, model2) {
15076
+ log("[sweep] Starting weekly memory sweep");
15077
+ const cleanedUp = cleanup();
15078
+ let suggestionsCount = 0;
15079
+ try {
15080
+ const suggestions = await runMemoryAnalysis(chatId, backendId, model2);
15081
+ suggestionsCount = suggestions.length;
15082
+ if (suggestionsCount === 0 && cleanedUp === 0) {
15083
+ log("[sweep] Memory bank healthy, no action needed");
15084
+ return { suggestionsCount: 0, cleanedUp };
15085
+ }
15086
+ const lines = [
15087
+ "\u{1F9E0} Weekly Memory Health",
15088
+ "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"
15089
+ ];
15090
+ if (cleanedUp > 0) {
15091
+ lines.push(`\u{1F5D1} Cleaned up ${cleanedUp} expired superseded memories`);
15092
+ }
15093
+ if (suggestionsCount > 0) {
15094
+ const counts = {};
15095
+ for (const s of suggestions) {
15096
+ counts[s.type] = (counts[s.type] ?? 0) + 1;
15097
+ }
15098
+ const parts = [];
15099
+ if (counts.superseded) parts.push(`${counts.superseded} contradiction${counts.superseded > 1 ? "s" : ""}`);
15100
+ if (counts.duplicate) parts.push(`${counts.duplicate} duplicate${counts.duplicate > 1 ? "s" : ""}`);
15101
+ if (counts.merge) parts.push(`${counts.merge} merge opportunit${counts.merge > 1 ? "ies" : "y"}`);
15102
+ if (counts.stale) parts.push(`${counts.stale} stale`);
15103
+ if (counts.categorize) parts.push(`${counts.categorize} uncategorized`);
15104
+ if (counts.reclassify) parts.push(`${counts.reclassify} misclassified`);
15105
+ lines.push("", `Found: ${parts.join(", ")}`, "");
15106
+ lines.push("Review these suggestions to keep your memory clean.");
15107
+ } else {
15108
+ lines.push("", "\u2705 Memory bank is healthy!");
15109
+ }
15110
+ const buttons = [];
15111
+ if (suggestionsCount > 0) {
15112
+ buttons.push([{ label: "Review Now", data: "mem:opt:start", style: "success" }]);
15113
+ }
15114
+ buttons.push([{ label: "Dismiss", data: "mem:sweep:dismiss" }]);
15115
+ await sendOrEditKeyboard(chatId, channel, void 0, lines.join("\n"), buttons);
15116
+ return { suggestionsCount, cleanedUp };
15117
+ } catch (err) {
15118
+ const msg = errorMessage(err);
15119
+ warn(`[sweep] Weekly sweep failed: ${msg}`);
15120
+ return { suggestionsCount: 0, cleanedUp, error: msg };
15121
+ }
15122
+ }
15123
+ function findSweepJob() {
15124
+ try {
15125
+ const { getDb: getDb2, getJobById: getJobById3 } = (init_store5(), __toCommonJS(store_exports5));
15126
+ const row = getDb2().prepare(
15127
+ "SELECT id FROM jobs WHERE job_type = ? AND active = 1 ORDER BY id LIMIT 1"
15128
+ ).get(SWEEP_JOB_TYPE);
15129
+ if (!row) return null;
15130
+ return getJobById3(row.id) ?? null;
15131
+ } catch {
15132
+ return null;
15133
+ }
15134
+ }
15135
+ function enableSweep(chatId, opts) {
15136
+ const existing = findSweepJob();
15137
+ const { setMetaValue: setMetaValue2 } = (init_store5(), __toCommonJS(store_exports5));
15138
+ setMetaValue2(SWEEP_ENABLED_KEY, "1");
15139
+ if (existing) {
15140
+ if (!existing.enabled) {
15141
+ const { resumeJob: resumeJob2 } = (init_cron(), __toCommonJS(cron_exports));
15142
+ resumeJob2(existing.id);
15143
+ log(`[sweep] Resumed job #${existing.id}`);
15144
+ }
15145
+ return existing.id;
15146
+ }
15147
+ const { insertJob: insertJob2 } = (init_jobs(), __toCommonJS(jobs_exports));
15148
+ const { startSingleJob: startSingleJob2 } = (init_cron(), __toCommonJS(cron_exports));
15149
+ const cron2 = opts?.cron ?? SWEEP_DEFAULT_CRON;
15150
+ const timezone = opts?.timezone ?? (Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC");
15151
+ const backend2 = opts?.backend ?? resolveDefaultBackend(chatId);
15152
+ const model2 = opts?.model ?? resolveDefaultModel(backend2);
15153
+ const job = insertJob2({
15154
+ scheduleType: "cron",
15155
+ cron: cron2,
15156
+ description: "Weekly memory health sweep \u2014 cleanup and optimization",
15157
+ title: "Weekly Memory Sweep",
15158
+ chatId,
15159
+ backend: backend2,
15160
+ model: model2,
15161
+ sessionType: "isolated",
15162
+ deliveryMode: "none",
15163
+ channel: opts?.channel ?? "telegram",
15164
+ target: opts?.target ?? chatId,
15165
+ timezone,
15166
+ jobType: SWEEP_JOB_TYPE,
15167
+ timeout: 300
15168
+ });
15169
+ startSingleJob2(job);
15170
+ log(`[sweep] Created job #${job.id} (cron="${cron2}", backend=${backend2}, model=${model2})`);
15171
+ return job.id;
15172
+ }
15173
+ function disableSweep() {
15174
+ const { setMetaValue: setMetaValue2 } = (init_store5(), __toCommonJS(store_exports5));
15175
+ setMetaValue2(SWEEP_ENABLED_KEY, "0");
15176
+ const job = findSweepJob();
15177
+ if (!job) return false;
15178
+ const { pauseJob: pauseJob2 } = (init_cron(), __toCommonJS(cron_exports));
15179
+ pauseJob2(job.id);
15180
+ log(`[sweep] Paused job #${job.id}`);
15181
+ return true;
15182
+ }
15183
+ function resolveDefaultBackend(chatId) {
15184
+ try {
15185
+ const { getAdapterForChat: getAdapterForChat2 } = (init_backends(), __toCommonJS(backends_exports));
15186
+ return getAdapterForChat2(chatId).id;
15187
+ } catch {
15188
+ return "claude";
15189
+ }
15190
+ }
15191
+ function resolveDefaultModel(backend2) {
15192
+ try {
15193
+ const { getAdapter: getAdapter3 } = (init_backends(), __toCommonJS(backends_exports));
15194
+ return getAdapter3(backend2).defaultModel;
15195
+ } catch {
15196
+ return "claude-sonnet-4-6";
15197
+ }
15198
+ }
15199
+ var SWEEP_ENABLED_KEY, SWEEP_DEFAULT_CRON, SWEEP_JOB_TYPE;
15200
+ var init_sweep = __esm({
15201
+ "src/memory/sweep.ts"() {
15202
+ "use strict";
15203
+ init_optimize();
15204
+ init_engine();
15205
+ init_log();
15206
+ init_helpers();
15207
+ SWEEP_ENABLED_KEY = "memory_sweep_enabled";
15208
+ SWEEP_DEFAULT_CRON = "0 9 * * 0";
15209
+ SWEEP_JOB_TYPE = "memory_sweep";
15210
+ }
15211
+ });
15212
+
15213
+ // src/router/state.ts
15214
+ var state_exports = {};
15215
+ __export(state_exports, {
15216
+ activeSideQuests: () => activeSideQuests,
15217
+ addForceStoppedChat: () => addForceStoppedChat,
15218
+ bypassBusyCheck: () => bypassBusyCheck,
15219
+ clearHistoryFilter: () => clearHistoryFilter,
15220
+ clearPendingCliAddition: () => clearPendingCliAddition,
15221
+ clearPendingModelResults: () => clearPendingModelResults,
15222
+ clearPendingModelSearch: () => clearPendingModelSearch,
15223
+ consumeForceStoppedChat: () => consumeForceStoppedChat,
15224
+ councilResults: () => councilResults,
15225
+ dashboardClawWarnings: () => dashboardClawWarnings,
15226
+ getActiveSideQuestCount: () => getActiveSideQuestCount,
15227
+ historyFilters: () => historyFilters,
15228
+ parseSideQuestPrefix: () => parseSideQuestPrefix,
15229
+ pendingCliAdditions: () => pendingCliAdditions,
15230
+ pendingInterrupts: () => pendingInterrupts,
15231
+ pendingMcpImports: () => pendingMcpImports,
15232
+ pendingModelResults: () => pendingModelResults,
15233
+ pendingModelSearch: () => pendingModelSearch,
15234
+ pendingNewchatUndo: () => pendingNewchatUndo,
15235
+ pendingSummaryUndo: () => pendingSummaryUndo,
15236
+ setCouncilResult: () => setCouncilResult,
15237
+ setHistoryFilter: () => setHistoryFilter,
15238
+ setPendingCliAddition: () => setPendingCliAddition,
15239
+ setPendingModelResults: () => setPendingModelResults,
15240
+ setPendingModelSearch: () => setPendingModelSearch,
15241
+ startStateSweep: () => startStateSweep,
15242
+ stopAllSideQuests: () => stopAllSideQuests,
15243
+ stopStateSweep: () => stopStateSweep
15244
+ });
15245
+ function addForceStoppedChat(chatId) {
15246
+ _forceStoppedChats.add(chatId);
15247
+ _forceStoppedTimestamps.set(chatId, Date.now());
15248
+ }
15249
+ function consumeForceStoppedChat(chatId) {
15250
+ _forceStoppedTimestamps.delete(chatId);
15251
+ return _forceStoppedChats.delete(chatId);
15252
+ }
15253
+ function setHistoryFilter(chatId, filter) {
15254
+ historyFilters.set(chatId, filter);
15255
+ historyFilterTimestamps.set(chatId, Date.now());
15256
+ }
15257
+ function clearHistoryFilter(chatId) {
15258
+ historyFilters.delete(chatId);
15259
+ historyFilterTimestamps.delete(chatId);
15260
+ }
15261
+ function setCouncilResult(chatId, result) {
15262
+ councilResults.set(chatId, result);
15263
+ councilResultTimestamps.set(chatId, Date.now());
15264
+ }
15265
+ function setPendingModelSearch(chatId, state) {
15266
+ pendingModelSearch.set(chatId, state);
15267
+ pendingModelSearchTimestamps.set(chatId, Date.now());
15268
+ }
15269
+ function clearPendingModelSearch(chatId) {
15270
+ pendingModelSearch.delete(chatId);
15271
+ pendingModelSearchTimestamps.delete(chatId);
15272
+ }
15273
+ function setPendingModelResults(chatId, results) {
15274
+ pendingModelResults.set(chatId, results);
15275
+ }
15276
+ function clearPendingModelResults(chatId) {
15277
+ pendingModelResults.delete(chatId);
15278
+ }
15279
+ function parseSideQuestPrefix(text) {
15280
+ const match = text.match(/^(?:sq|btw):\s*/i);
15281
+ if (match) return { isSideQuest: true, cleanText: text.slice(match[0].length) };
15282
+ return { isSideQuest: false, cleanText: text };
15283
+ }
15284
+ function getActiveSideQuestCount(chatId) {
15285
+ return activeSideQuests.get(chatId)?.size ?? 0;
15286
+ }
15287
+ function stopAllSideQuests(chatId) {
15288
+ const active = activeSideQuests.get(chatId);
15289
+ if (active) {
15290
+ for (const sqId of active) {
15291
+ stopAgent(sqId);
15292
+ }
15293
+ }
15294
+ }
15295
+ function startStateSweep() {
15296
+ if (sweepTimer) return;
15297
+ sweepTimer = setInterval(() => {
15298
+ const now = Date.now();
15299
+ for (const [chatId, ts2] of dashboardClawWarnings) {
15300
+ if (now - ts2 > STALE_THRESHOLD_MS) dashboardClawWarnings.delete(chatId);
15301
+ }
15302
+ for (const [cid, ts2] of historyFilterTimestamps) {
15303
+ if (now - ts2 > STALE_THRESHOLD_MS) {
15304
+ historyFilters.delete(cid);
15305
+ historyFilterTimestamps.delete(cid);
15306
+ }
15307
+ }
15308
+ for (const [cid, ts2] of councilResultTimestamps) {
15309
+ if (now - ts2 > STALE_THRESHOLD_MS) {
15310
+ councilResults.delete(cid);
15311
+ councilResultTimestamps.delete(cid);
15312
+ }
15313
+ }
15314
+ for (const [cid, ts2] of pendingModelSearchTimestamps) {
15315
+ if (now - ts2 > STALE_THRESHOLD_MS) {
15316
+ pendingModelSearch.delete(cid);
15317
+ pendingModelSearchTimestamps.delete(cid);
15318
+ }
15319
+ }
15320
+ for (const chatId of pendingInterrupts.keys()) {
15321
+ if (!_interruptSeen.has(chatId)) {
15322
+ _interruptSeen.add(chatId);
15323
+ } else {
15324
+ pendingInterrupts.delete(chatId);
15325
+ _interruptSeen.delete(chatId);
15326
+ }
15327
+ }
15328
+ for (const chatId of _interruptSeen) {
15329
+ if (!pendingInterrupts.has(chatId)) _interruptSeen.delete(chatId);
15330
+ }
15331
+ for (const [cid, ts2] of pendingCliTimestamps) {
15332
+ if (now - ts2 > STALE_THRESHOLD_MS) {
15333
+ pendingCliAdditions.delete(cid);
15334
+ pendingCliTimestamps.delete(cid);
15335
+ }
15336
+ }
15337
+ for (const [cid, state] of pendingMcpImports) {
15338
+ if (now - state.startedAt > STALE_THRESHOLD_MS) pendingMcpImports.delete(cid);
15339
+ }
15340
+ for (const [cid, ts2] of _forceStoppedTimestamps) {
15341
+ if (now - ts2 > STALE_THRESHOLD_MS) {
15342
+ _forceStoppedChats.delete(cid);
15343
+ _forceStoppedTimestamps.delete(cid);
15344
+ }
15345
+ }
15346
+ }, SWEEP_INTERVAL_MS);
15347
+ sweepTimer.unref();
15348
+ }
15349
+ function stopStateSweep() {
15350
+ if (sweepTimer) {
15351
+ clearInterval(sweepTimer);
15352
+ sweepTimer = null;
15353
+ }
15354
+ }
15355
+ function setPendingCliAddition(chatId, messageId) {
15356
+ pendingCliAdditions.set(chatId, messageId);
15357
+ pendingCliTimestamps.set(chatId, Date.now());
15358
+ }
15359
+ function clearPendingCliAddition(chatId) {
15360
+ pendingCliAdditions.delete(chatId);
15361
+ pendingCliTimestamps.delete(chatId);
15362
+ }
15363
+ var pendingInterrupts, bypassBusyCheck, _forceStoppedChats, _forceStoppedTimestamps, activeSideQuests, dashboardClawWarnings, pendingSummaryUndo, pendingNewchatUndo, historyFilters, historyFilterTimestamps, councilResults, councilResultTimestamps, pendingModelSearch, pendingModelSearchTimestamps, pendingModelResults, SWEEP_INTERVAL_MS, STALE_THRESHOLD_MS, sweepTimer, _interruptSeen, pendingMcpImports, pendingCliAdditions, pendingCliTimestamps;
15364
+ var init_state = __esm({
15365
+ "src/router/state.ts"() {
15366
+ "use strict";
15367
+ init_agent();
15368
+ pendingInterrupts = /* @__PURE__ */ new Map();
15369
+ bypassBusyCheck = /* @__PURE__ */ new Set();
15370
+ _forceStoppedChats = /* @__PURE__ */ new Set();
15371
+ _forceStoppedTimestamps = /* @__PURE__ */ new Map();
15372
+ activeSideQuests = /* @__PURE__ */ new Map();
15373
+ dashboardClawWarnings = /* @__PURE__ */ new Map();
15374
+ pendingSummaryUndo = /* @__PURE__ */ new Map();
15375
+ pendingNewchatUndo = /* @__PURE__ */ new Map();
15376
+ historyFilters = /* @__PURE__ */ new Map();
15377
+ historyFilterTimestamps = /* @__PURE__ */ new Map();
15378
+ councilResults = /* @__PURE__ */ new Map();
15379
+ councilResultTimestamps = /* @__PURE__ */ new Map();
15380
+ pendingModelSearch = /* @__PURE__ */ new Map();
15381
+ pendingModelSearchTimestamps = /* @__PURE__ */ new Map();
15382
+ pendingModelResults = /* @__PURE__ */ new Map();
15383
+ SWEEP_INTERVAL_MS = 30 * 60 * 1e3;
15384
+ STALE_THRESHOLD_MS = 30 * 60 * 1e3;
15385
+ sweepTimer = null;
15386
+ _interruptSeen = /* @__PURE__ */ new Set();
15387
+ pendingMcpImports = /* @__PURE__ */ new Map();
15388
+ pendingCliAdditions = /* @__PURE__ */ new Map();
15389
+ pendingCliTimestamps = /* @__PURE__ */ new Map();
15390
+ }
15391
+ });
15392
+
14275
15393
  // src/ui/pagination.ts
14276
15394
  function buildPaginatedKeyboard(opts) {
14277
15395
  const { items, page, callbackPrefix, renderItem, headerText, footerButtons } = opts;
@@ -14897,6 +16015,13 @@ async function promptAccount(chatId, channel) {
14897
16015
  await promptTimeout(chatId, channel);
14898
16016
  return;
14899
16017
  }
16018
+ const adapter = getAdapter(pending.backend);
16019
+ if (adapter.type === "api") {
16020
+ pending.credentialSlotId = null;
16021
+ pending.step = "timeout";
16022
+ await promptTimeout(chatId, channel);
16023
+ return;
16024
+ }
14900
16025
  const isGemini = pending.backend === BACKEND.GEMINI;
14901
16026
  const slots = isGemini ? getGeminiSlots() : getBackendSlots(pending.backend);
14902
16027
  const enabledSlots = slots.filter((s) => s.enabled);
@@ -15158,6 +16283,7 @@ __export(stt_exports, {
15158
16283
  LOCAL_WHISPER_MODELS: () => LOCAL_WHISPER_MODELS,
15159
16284
  MACOS_VOICES: () => MACOS_VOICES,
15160
16285
  downloadWhisperModel: () => downloadWhisperModel,
16286
+ getSttEcho: () => getSttEcho,
15161
16287
  getSttModel: () => getSttModel,
15162
16288
  getSttProvider: () => getSttProvider,
15163
16289
  getVoiceConfig: () => getVoiceConfig,
@@ -15165,10 +16291,12 @@ __export(stt_exports, {
15165
16291
  isVoiceEnabled: () => isVoiceEnabled,
15166
16292
  isWhisperCliAvailable: () => isWhisperCliAvailable,
15167
16293
  isWhisperModelDownloaded: () => isWhisperModelDownloaded,
16294
+ setSttEcho: () => setSttEcho,
15168
16295
  setSttModel: () => setSttModel,
15169
16296
  setSttProvider: () => setSttProvider,
15170
16297
  setVoiceProvider: () => setVoiceProvider,
15171
16298
  synthesizeSpeech: () => synthesizeSpeech,
16299
+ toggleSttEcho: () => toggleSttEcho,
15172
16300
  toggleVoice: () => toggleVoice,
15173
16301
  transcribeAudio: () => transcribeAudio
15174
16302
  });
@@ -15247,6 +16375,23 @@ function setSttModel(chatId, model2) {
15247
16375
  ON CONFLICT(chat_id) DO UPDATE SET stt_model = ?
15248
16376
  `).run(chatId, model2, model2);
15249
16377
  }
16378
+ function getSttEcho(chatId) {
16379
+ const db3 = getDb();
16380
+ const row = db3.prepare("SELECT stt_echo FROM chat_voice WHERE chat_id = ?").get(chatId);
16381
+ return row?.stt_echo === 1;
16382
+ }
16383
+ function setSttEcho(chatId, enabled) {
16384
+ const db3 = getDb();
16385
+ db3.prepare(`
16386
+ INSERT INTO chat_voice (chat_id, enabled, stt_echo) VALUES (?, 0, ?)
16387
+ ON CONFLICT(chat_id) DO UPDATE SET stt_echo = ?
16388
+ `).run(chatId, enabled ? 1 : 0, enabled ? 1 : 0);
16389
+ }
16390
+ function toggleSttEcho(chatId) {
16391
+ const newState = !getSttEcho(chatId);
16392
+ setSttEcho(chatId, newState);
16393
+ return newState;
16394
+ }
15250
16395
  function isFfmpegAvailable() {
15251
16396
  if (ffmpegAvailable !== null) return ffmpegAvailable;
15252
16397
  try {
@@ -15624,251 +16769,6 @@ var init_health2 = __esm({
15624
16769
  }
15625
16770
  });
15626
16771
 
15627
- // src/channels/telegram-throttle.ts
15628
- var telegram_throttle_exports = {};
15629
- __export(telegram_throttle_exports, {
15630
- TelegramThrottle: () => TelegramThrottle,
15631
- getThrottleState: () => getThrottleState
15632
- });
15633
- import { GrammyError } from "grammy";
15634
- function isEditLabel(label2) {
15635
- return label2.startsWith("editText") || label2.startsWith("editKeyboard");
15636
- }
15637
- function perChatInterval(chatId) {
15638
- return parseInt(chatId) < 0 ? PER_GROUP_INTERVAL_MS : PER_DM_INTERVAL_MS;
15639
- }
15640
- function getThrottleState() {
15641
- if (!_activeThrottle) return null;
15642
- return _activeThrottle.getState();
15643
- }
15644
- function is429(err) {
15645
- return err instanceof GrammyError && err.error_code === 429;
15646
- }
15647
- function sleep(ms) {
15648
- return new Promise((r) => setTimeout(r, ms));
15649
- }
15650
- var PER_DM_INTERVAL_MS, PER_GROUP_INTERVAL_MS, GLOBAL_INTERVAL_MS, MAX_RETRIES2, RETRY_DELAY_MS, MAX_QUEUE_SIZE, EDIT_PRESSURE_THRESHOLD, MAX_TOTAL_PAUSE_MS, _activeThrottle, TelegramThrottle;
15651
- var init_telegram_throttle = __esm({
15652
- "src/channels/telegram-throttle.ts"() {
15653
- "use strict";
15654
- init_log();
15655
- PER_DM_INTERVAL_MS = 1e3;
15656
- PER_GROUP_INTERVAL_MS = 3500;
15657
- GLOBAL_INTERVAL_MS = 100;
15658
- MAX_RETRIES2 = 2;
15659
- RETRY_DELAY_MS = 1e3;
15660
- MAX_QUEUE_SIZE = 100;
15661
- EDIT_PRESSURE_THRESHOLD = MAX_QUEUE_SIZE / 2;
15662
- MAX_TOTAL_PAUSE_MS = 30 * 60 * 1e3;
15663
- _activeThrottle = null;
15664
- TelegramThrottle = class {
15665
- queue = [];
15666
- processing = false;
15667
- lastSendPerChat = /* @__PURE__ */ new Map();
15668
- lastGlobalSend = 0;
15669
- // Pause state
15670
- pausedUntil = 0;
15671
- pauseStartedAt = 0;
15672
- chatsPendingNotification = /* @__PURE__ */ new Set();
15673
- resumeNotifier;
15674
- constructor() {
15675
- _activeThrottle = this;
15676
- }
15677
- /**
15678
- * Register a callback that fires when the throttle resumes after a pause.
15679
- * The callback should send a message directly via bot.api (NOT through the throttle).
15680
- */
15681
- setResumeNotifier(fn) {
15682
- this.resumeNotifier = fn;
15683
- }
15684
- /** Enqueue a Telegram API call with automatic pacing and 429 handling.
15685
- * When `priority` is true the item jumps to the front of the queue —
15686
- * used by fast-path commands (/status, /stop, etc.) so their responses
15687
- * aren't delayed behind tool-notification or error-message floods. */
15688
- async send(chatId, label2, fn, priority) {
15689
- if (isEditLabel(label2)) {
15690
- if (this.isPaused()) {
15691
- throw new Error("Throttle paused (rate limit active) \u2014 edit skipped");
15692
- }
15693
- if (this.queue.length >= EDIT_PRESSURE_THRESHOLD) {
15694
- throw new Error("Throttle queue pressured \u2014 edit skipped");
15695
- }
15696
- }
15697
- return new Promise((resolve3, reject) => {
15698
- if (this.queue.length >= MAX_QUEUE_SIZE) {
15699
- const editIdx = this.queue.findIndex((q) => isEditLabel(q.label));
15700
- const dropIdx = editIdx >= 0 ? editIdx : 0;
15701
- const dropped = this.queue.splice(dropIdx, 1)[0];
15702
- if (dropped) {
15703
- warn(`[throttle] Queue full (${MAX_QUEUE_SIZE}), dropping: ${dropped.label}`);
15704
- dropped.reject(new Error("Dropped from send queue (overflow)"));
15705
- }
15706
- }
15707
- const item = { chatId, label: label2, fn, resolve: resolve3, reject };
15708
- if (priority) {
15709
- this.queue.unshift(item);
15710
- } else {
15711
- this.queue.push(item);
15712
- }
15713
- this.drain();
15714
- });
15715
- }
15716
- /**
15717
- * Best-effort send — drops silently if throttle is paused or queue is pressured.
15718
- * Used for cosmetic calls (typing indicators, reactions) that should count toward
15719
- * rate limits but must never queue up or amplify 429 spirals.
15720
- */
15721
- async tryBestEffort(chatId, label2, fn, opts) {
15722
- if (this.isPaused()) return void 0;
15723
- if (this.queue.length > 10) return void 0;
15724
- if (!opts?.skipRecord) {
15725
- const lastChat = this.lastSendPerChat.get(chatId) ?? 0;
15726
- if (Date.now() - lastChat < perChatInterval(chatId)) return void 0;
15727
- if (Date.now() - this.lastGlobalSend < GLOBAL_INTERVAL_MS) return void 0;
15728
- }
15729
- try {
15730
- const result = await fn();
15731
- if (!opts?.skipRecord) this.recordSend(chatId);
15732
- return result;
15733
- } catch (err) {
15734
- if (is429(err)) {
15735
- const retrySec = err.parameters?.retry_after ?? 10;
15736
- this.pausedUntil = Date.now() + retrySec * 1e3;
15737
- if (this.pauseStartedAt === 0) this.pauseStartedAt = Date.now();
15738
- warn(`[throttle] Best-effort ${label2} hit 429, pausing for ${retrySec}s`);
15739
- }
15740
- return void 0;
15741
- }
15742
- }
15743
- /** Check whether the throttle is currently paused (rate-limited). */
15744
- isPaused() {
15745
- return Date.now() < this.pausedUntil;
15746
- }
15747
- /** Get structured state for diagnostics / health checks. */
15748
- getState() {
15749
- const now = Date.now();
15750
- const paused = now < this.pausedUntil;
15751
- return {
15752
- isPaused: paused,
15753
- queueDepth: this.queue.length,
15754
- pausedUntilMs: this.pausedUntil,
15755
- pauseRemainingSec: paused ? Math.ceil((this.pausedUntil - now) / 1e3) : 0
15756
- };
15757
- }
15758
- // ── Queue processor ─────────────────────────────────────────────────
15759
- async drain() {
15760
- if (this.processing) return;
15761
- this.processing = true;
15762
- try {
15763
- while (this.queue.length > 0) {
15764
- while (this.isPaused()) {
15765
- if (this.pauseStartedAt > 0 && Date.now() - this.pauseStartedAt > MAX_TOTAL_PAUSE_MS) {
15766
- warn(`[throttle] Max pause duration exceeded (${MAX_TOTAL_PAUSE_MS / 6e4}min), dropping ${this.queue.length} items`);
15767
- this.flushQueueWithError("Telegram rate limit exceeded max wait time");
15768
- this.pausedUntil = 0;
15769
- this.pauseStartedAt = 0;
15770
- this.chatsPendingNotification.clear();
15771
- break;
15772
- }
15773
- const waitMs = Math.min(this.pausedUntil - Date.now(), 5e3);
15774
- if (waitMs > 0) await sleep(waitMs);
15775
- }
15776
- if (this.queue.length === 0) break;
15777
- if (this.chatsPendingNotification.size > 0) {
15778
- await this.sendResumeNotifications();
15779
- }
15780
- const item = this.queue[0];
15781
- const lastChat = this.lastSendPerChat.get(item.chatId) ?? 0;
15782
- const chatWait = perChatInterval(item.chatId) - (Date.now() - lastChat);
15783
- if (chatWait > 0) await sleep(chatWait);
15784
- const globalWait = GLOBAL_INTERVAL_MS - (Date.now() - this.lastGlobalSend);
15785
- if (globalWait > 0) await sleep(globalWait);
15786
- this.queue.shift();
15787
- try {
15788
- const result = await this.execWithRetry(item.label, item.fn);
15789
- this.recordSend(item.chatId);
15790
- this.pauseStartedAt = 0;
15791
- item.resolve(result);
15792
- } catch (err) {
15793
- if (is429(err)) {
15794
- const retrySec = err.parameters?.retry_after ?? 10;
15795
- this.enterPause(retrySec, item);
15796
- continue;
15797
- }
15798
- item.reject(err);
15799
- }
15800
- }
15801
- } finally {
15802
- this.processing = false;
15803
- }
15804
- }
15805
- // ── Retry logic (non-429 errors only) ───────────────────────────────
15806
- async execWithRetry(label2, fn) {
15807
- for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
15808
- try {
15809
- return await fn();
15810
- } catch (err) {
15811
- if (is429(err)) throw err;
15812
- if (attempt < MAX_RETRIES2 && err instanceof GrammyError) {
15813
- warn(`[throttle] ${label2} attempt ${attempt + 1}/${MAX_RETRIES2} failed (${err.error_code}), retrying`);
15814
- await sleep(RETRY_DELAY_MS);
15815
- continue;
15816
- }
15817
- throw err;
15818
- }
15819
- }
15820
- throw new Error("unreachable");
15821
- }
15822
- // ── Pause management ────────────────────────────────────────────────
15823
- enterPause(retrySec, failedItem) {
15824
- this.queue.unshift(failedItem);
15825
- const bufferedSec = Math.ceil(retrySec * 1.5);
15826
- this.pausedUntil = Date.now() + bufferedSec * 1e3;
15827
- if (this.pauseStartedAt === 0) this.pauseStartedAt = Date.now();
15828
- for (const qi of this.queue) {
15829
- this.chatsPendingNotification.add(qi.chatId);
15830
- }
15831
- warn(`[throttle] 429 \u2014 pausing ALL sends for ${bufferedSec}s (retry_after=${retrySec}s + 50% buffer, ${this.queue.length} items queued)`);
15832
- }
15833
- async sendResumeNotifications() {
15834
- const chats2 = new Set(this.chatsPendingNotification);
15835
- this.chatsPendingNotification.clear();
15836
- if (!this.resumeNotifier) return;
15837
- const pausedSec = this.pauseStartedAt > 0 ? Math.round((Date.now() - this.pauseStartedAt) / 1e3) : 0;
15838
- for (const chatId of chats2) {
15839
- const queuedForChat = this.queue.filter((q) => q.chatId === chatId).length;
15840
- if (queuedForChat === 0) continue;
15841
- try {
15842
- await this.resumeNotifier(chatId, pausedSec, queuedForChat);
15843
- this.recordSend(chatId);
15844
- } catch (err) {
15845
- if (is429(err)) {
15846
- const retrySec = err.parameters?.retry_after ?? 10;
15847
- this.pausedUntil = Date.now() + retrySec * 1e3;
15848
- warn(`[throttle] Resume notification hit 429, re-pausing for ${retrySec}s (skipping further notifications)`);
15849
- return;
15850
- }
15851
- warn(`[throttle] Resume notification failed for chat ${chatId}: ${err}`);
15852
- }
15853
- }
15854
- this.pauseStartedAt = 0;
15855
- }
15856
- // ── Helpers ─────────────────────────────────────────────────────────
15857
- recordSend(chatId) {
15858
- const now = Date.now();
15859
- this.lastSendPerChat.set(chatId, now);
15860
- this.lastGlobalSend = now;
15861
- }
15862
- flushQueueWithError(message) {
15863
- while (this.queue.length > 0) {
15864
- const item = this.queue.shift();
15865
- item.reject(new Error(message));
15866
- }
15867
- }
15868
- };
15869
- }
15870
- });
15871
-
15872
16772
  // src/health/checks.ts
15873
16773
  import { existsSync as existsSync16, statSync as statSync5, readFileSync as readFileSync11 } from "fs";
15874
16774
  import { execFileSync as execFileSync2, execSync as execSync2 } from "child_process";
@@ -16135,8 +17035,8 @@ function enableHeartbeat(chatId, opts) {
16135
17035
  const { insertJob: insertJob2 } = (init_jobs(), __toCommonJS(jobs_exports));
16136
17036
  const { startSingleJob: startSingleJob2 } = (init_cron(), __toCommonJS(cron_exports));
16137
17037
  const intervalMs = opts?.intervalMs ?? DEFAULT_INTERVAL_MS;
16138
- const backend2 = opts?.backend ?? resolveDefaultBackend(chatId);
16139
- const model2 = opts?.model ?? resolveDefaultModel(backend2);
17038
+ const backend2 = opts?.backend ?? resolveDefaultBackend2(chatId);
17039
+ const model2 = opts?.model ?? resolveDefaultModel2(backend2);
16140
17040
  const job = insertJob2({
16141
17041
  scheduleType: "every",
16142
17042
  everyMs: intervalMs,
@@ -16156,7 +17056,7 @@ function enableHeartbeat(chatId, opts) {
16156
17056
  log(`[heartbeat] Created job #${job.id} (every ${intervalMs / 6e4}min, backend=${backend2}, model=${model2})`);
16157
17057
  return job.id;
16158
17058
  }
16159
- function resolveDefaultBackend(chatId) {
17059
+ function resolveDefaultBackend2(chatId) {
16160
17060
  try {
16161
17061
  const { getAdapterForChat: getAdapterForChat2 } = (init_backends(), __toCommonJS(backends_exports));
16162
17062
  return getAdapterForChat2(chatId).id;
@@ -16164,7 +17064,7 @@ function resolveDefaultBackend(chatId) {
16164
17064
  return "claude";
16165
17065
  }
16166
17066
  }
16167
- function resolveDefaultModel(backend2) {
17067
+ function resolveDefaultModel2(backend2) {
16168
17068
  try {
16169
17069
  const { getAdapter: getAdapter3 } = (init_backends(), __toCommonJS(backends_exports));
16170
17070
  return getAdapter3(backend2).defaultModel;
@@ -16173,8 +17073,8 @@ function resolveDefaultModel(backend2) {
16173
17073
  }
16174
17074
  }
16175
17075
  function migrateHeartbeatDefaults(job) {
16176
- const backend2 = resolveDefaultBackend(job.chatId);
16177
- const model2 = resolveDefaultModel(backend2);
17076
+ const backend2 = resolveDefaultBackend2(job.chatId);
17077
+ const model2 = resolveDefaultModel2(backend2);
16178
17078
  try {
16179
17079
  const { getDb: getDb2 } = (init_store5(), __toCommonJS(store_exports5));
16180
17080
  getDb2().prepare("UPDATE jobs SET backend = ?, model = ? WHERE id = ?").run(backend2, model2, job.id);
@@ -16418,10 +17318,10 @@ function formatNightlySummary(insights, totalPending) {
16418
17318
  } else {
16419
17319
  header2 = `Nightly Reflection \u2014 ${newCount} proposal${newCount === 1 ? "" : "s"} ready`;
16420
17320
  }
16421
- const list = insights.map((ins, i) => `\u2022 [${ins.category}] ${ins.insight}`).join("\n");
17321
+ const list2 = insights.map((ins, i) => `\u2022 [${ins.category}] ${ins.insight}`).join("\n");
16422
17322
  return `${header2}
16423
17323
 
16424
- ${list}
17324
+ ${list2}
16425
17325
 
16426
17326
  Review with /evolve`;
16427
17327
  }
@@ -17038,6 +17938,12 @@ async function sendVoiceConfigKeyboard(chatId, channel, messageId) {
17038
17938
  buttons.push(row);
17039
17939
  }
17040
17940
  }
17941
+ const echoOn = getSttEcho(chatId);
17942
+ buttons.push([{
17943
+ label: `${echoOn ? "\u2713 " : ""}\u{1F399} Show Transcription`,
17944
+ data: "vcfg:echo",
17945
+ ...echoOn ? { style: "success" } : {}
17946
+ }]);
17041
17947
  buttons.push([
17042
17948
  {
17043
17949
  label: `${!ttsConfig.enabled ? "\u2713 " : ""}\u{1F507} Replies Off`,
@@ -17204,38 +18110,32 @@ async function sendThinkingKeyboard(chatId, channel, messageId, forModelId) {
17204
18110
  const currentModel = forModelId ?? getModel(chatId) ?? adapter.defaultModel;
17205
18111
  const modelInfo = adapter.availableModels[currentModel];
17206
18112
  const currentLevel = getThinkingLevel(chatId) || "auto";
17207
- if (!modelInfo || modelInfo.thinking !== "adjustable" || !modelInfo.thinkingLevels) {
17208
- await sendOrEditKeyboard(
17209
- chatId,
17210
- channel,
17211
- messageId,
17212
- `Model ${shortModelName(currentModel)} uses fixed thinking \u2014 no adjustment needed.`,
17213
- [[{ label: "\u2190 Back to Model", data: "menu:model" }]]
17214
- );
17215
- return;
17216
- }
17217
18113
  const showThinkingUi = getShowThinkingUi(chatId);
17218
- const buttons = modelInfo.thinkingLevels.map((level) => [{
17219
- label: `${level === currentLevel ? "\u2713 " : ""}${level === "auto" ? "Auto" : capitalize(level)}`,
17220
- data: `thinking:${level}`,
17221
- ...level === currentLevel ? { style: "primary" } : {}
17222
- }]);
18114
+ const canAdjust = modelInfo?.thinking === "adjustable" && modelInfo.thinkingLevels;
18115
+ const buttons = [];
18116
+ if (canAdjust) {
18117
+ for (const level of modelInfo.thinkingLevels) {
18118
+ buttons.push([{
18119
+ label: `${level === currentLevel ? "\u2713 " : ""}${level === "auto" ? "Auto" : capitalize(level)}`,
18120
+ data: `thinking:${level}`,
18121
+ ...level === currentLevel ? { style: "primary" } : {}
18122
+ }]);
18123
+ }
18124
+ }
17223
18125
  buttons.push([{
17224
18126
  label: `${showThinkingUi ? "\u2713 " : ""}\u{1F4AD} Show Thinking`,
17225
18127
  data: "thinking_show_ui:toggle",
17226
18128
  ...showThinkingUi ? { style: "primary" } : {}
17227
18129
  }]);
17228
- await sendOrEditKeyboard(
17229
- chatId,
17230
- channel,
17231
- messageId,
17232
- `\u{1F4AD} Thinking Level \u2014 ${shortModelName(currentModel)}
18130
+ const header2 = canAdjust ? `\u{1F4AD} Thinking Level \u2014 ${shortModelName(currentModel)}
17233
18131
  Current: ${capitalize(currentLevel)}
17234
- Show thinking tokens: ${showThinkingUi ? "On" : "Off"}${adapter.id === "cursor" ? `
18132
+ Show thinking tokens: ${showThinkingUi ? "On" : "Off"}` : `\u{1F4AD} Thinking \u2014 ${shortModelName(currentModel)}
18133
+ Level: Fixed
18134
+ Show thinking tokens: ${showThinkingUi ? "On" : "Off"}`;
18135
+ const note = adapter.id === "cursor" ? `
17235
18136
 
17236
- \u26A0\uFE0F ${adapter.displayName} doesn't expose thinking tokens` : ""}`,
17237
- buttons
17238
- );
18137
+ \u26A0\uFE0F ${adapter.displayName} doesn't expose thinking tokens` : "";
18138
+ await sendOrEditKeyboard(chatId, channel, messageId, `${header2}${note}`, buttons);
17239
18139
  }
17240
18140
  async function sendSkillsPage(chatId, channel, skills2, page, messageId) {
17241
18141
  const approved = skills2.filter((s) => s.status === "approved");
@@ -17330,9 +18230,11 @@ async function sendMemoryPage(chatId, channel, page, messageId) {
17330
18230
  const buttons = [];
17331
18231
  for (const [i, m] of pageItems.entries()) {
17332
18232
  const num = start + i + 1;
17333
- const preview = `${m.trigger}: ${m.content}`.slice(0, 32);
18233
+ const cat = m.category ?? "uncategorized";
18234
+ const full = `[${cat}] ${m.trigger}: ${m.content}`;
18235
+ const preview = full.length > 36 ? `${full.slice(0, 36)}\u2026` : full;
17334
18236
  buttons.push([{
17335
- label: `${num}. ${preview}${m.content.length > 32 ? "\u2026" : ""}`,
18237
+ label: `${num}. ${preview}`,
17336
18238
  data: `mem:view:${m.id}`
17337
18239
  }]);
17338
18240
  }
@@ -17351,6 +18253,12 @@ async function sendMemoryPage(chatId, channel, page, messageId) {
17351
18253
  }
17352
18254
  buttons.push(footerRow);
17353
18255
  buttons.push([{ label: "\u2728 Optimize", data: "mem:opt", style: "success" }]);
18256
+ const sweepEnabled = getMetaValue(SWEEP_ENABLED_KEY) === "1";
18257
+ buttons.push([{
18258
+ label: sweepEnabled ? "\u2713 Weekly Sweep: On" : "Weekly Sweep: Off",
18259
+ data: "mem:sweep:toggle",
18260
+ style: sweepEnabled ? "success" : void 0
18261
+ }]);
17354
18262
  await sendOrEditKeyboard(chatId, channel, messageId, header2, buttons);
17355
18263
  }
17356
18264
  async function sendMemoryDetail(chatId, memoryId, channel, messageId) {
@@ -18028,7 +18936,8 @@ async function sendBackendModelPicker(chatId, backendId, channel, messageId) {
18028
18936
  const summary = backendConfigSummary(chatId, backendId, false);
18029
18937
  if (adapter.type === "api") {
18030
18938
  const apiModels = getApiModels(backendId);
18031
- if (apiModels.length === 0) {
18939
+ const adapterModelCount = Object.keys(adapter.availableModels).length;
18940
+ if (apiModels.length === 0 && adapterModelCount === 0) {
18032
18941
  await sendOrEditKeyboard(
18033
18942
  chatId,
18034
18943
  channel,
@@ -18043,23 +18952,25 @@ No models configured. Add one with \u2795`,
18043
18952
  );
18044
18953
  return;
18045
18954
  }
18046
- const modelButtons2 = [];
18047
- for (const m of apiModels) {
18048
- const isActive = m.modelId === currentModel;
18049
- const freeTag = m.isFree && !m.displayName.toLowerCase().includes("free") ? " (free)" : "";
18050
- modelButtons2.push([
18051
- {
18052
- label: `${isActive ? "\u2713 " : ""}${m.displayName}${freeTag}`,
18053
- data: `apimodel:sel:${m.id}`,
18054
- ...isActive ? { style: "primary" } : {}
18055
- },
18056
- { label: "\u{1F5D1}", data: `apimodel:del:${m.id}` }
18057
- ]);
18955
+ if (apiModels.length > 0) {
18956
+ const modelButtons2 = [];
18957
+ for (const m of apiModels) {
18958
+ const isActive = m.modelId === currentModel;
18959
+ const freeTag = m.isFree && !m.displayName.toLowerCase().includes("free") ? " (free)" : "";
18960
+ modelButtons2.push([
18961
+ {
18962
+ label: `${isActive ? "\u2713 " : ""}${m.displayName}${freeTag}`,
18963
+ data: `apimodel:sel:${m.id}`,
18964
+ ...isActive ? { style: "primary" } : {}
18965
+ },
18966
+ { label: "\u{1F5D1}", data: `apimodel:del:${m.id}` }
18967
+ ]);
18968
+ }
18969
+ modelButtons2.push([{ label: "\u2795 Add Model", data: `apimodel:add:${backendId}` }]);
18970
+ modelButtons2.push([{ label: "\u2190 Back", data: `bconf:panel:${backendId}` }]);
18971
+ await sendOrEditKeyboard(chatId, channel, messageId, summary, modelButtons2);
18972
+ return;
18058
18973
  }
18059
- modelButtons2.push([{ label: "\u2795 Add Model", data: `apimodel:add:${backendId}` }]);
18060
- modelButtons2.push([{ label: "\u2190 Back", data: `bconf:panel:${backendId}` }]);
18061
- await sendOrEditKeyboard(chatId, channel, messageId, summary, modelButtons2);
18062
- return;
18063
18974
  }
18064
18975
  const modelButtons = Object.entries(adapter.availableModels).map(([id, info]) => [{
18065
18976
  label: `${id === currentModel ? "\u2713 " : ""}${info.label}`,
@@ -18155,7 +19066,8 @@ async function sendBackendSwitchConfirmation(chatId, target, channel, messageId)
18155
19066
  }
18156
19067
  async function doBackendSwitch(chatId, backendId, channel, opts) {
18157
19068
  if (!opts?.skipContext) {
18158
- const bridge = buildContextBridge(chatId);
19069
+ const interrupted = consumeForceStoppedChat(chatId);
19070
+ const bridge = buildContextBridge(chatId, 15, interrupted);
18159
19071
  if (bridge) setPendingContextBridge(chatId, bridge);
18160
19072
  }
18161
19073
  clearSession(chatId);
@@ -18235,10 +19147,12 @@ var init_ui = __esm({
18235
19147
  init_format();
18236
19148
  init_backends();
18237
19149
  init_store5();
19150
+ init_sweep();
18238
19151
  init_chat_settings();
18239
19152
  init_api_models();
18240
19153
  init_summarize();
18241
19154
  init_inject();
19155
+ init_state();
18242
19156
  init_session_log();
18243
19157
  init_store3();
18244
19158
  init_log();
@@ -18282,7 +19196,7 @@ function getMemOptSession(chatId) {
18282
19196
  function buildOptimizationPrompt(memories) {
18283
19197
  const lines = memories.map((m, i) => {
18284
19198
  const accessed = m.access_count ?? 0;
18285
- return `[${i + 1}] ID=${m.id} | trigger="${m.trigger}" | content="${m.content}" | type=${m.type} | salience=${m.salience.toFixed(2)} | accessed=${accessed} | created=${m.created_at}`;
19199
+ return `[${i + 1}] ID=${m.id} | trigger="${m.trigger}" | content="${m.content}" | type=${m.type} | category=${m.category} | salience=${m.salience.toFixed(2)} | accessed=${accessed} | half_life=${(m.half_life_days ?? 14).toFixed(1)} | created=${m.created_at}`;
18286
19200
  });
18287
19201
  return ANALYSIS_PROMPT.replace("{MEMORIES}", lines.join("\n"));
18288
19202
  }
@@ -18291,7 +19205,7 @@ function parseOptimizationResponse(raw) {
18291
19205
  const suggestions = [];
18292
19206
  const blocks = raw.split(/^---$/m).filter((b) => b.trim());
18293
19207
  for (const block of blocks) {
18294
- const typeMatch = block.match(/TYPE:\s*(duplicate|merge|stale)/i);
19208
+ const typeMatch = block.match(/TYPE:\s*(duplicate|merge|stale|superseded|categorize|reclassify)/i);
18295
19209
  const idsMatch = block.match(/IDS:\s*([\d,\s]+)/);
18296
19210
  const reasonMatch = block.match(/REASON:\s*(.+?)(?=\n(?:ACTION|SUGGESTION|TYPE|IDS):|$)/s);
18297
19211
  const actionMatch = block.match(/ACTION:\s*(.+)/s);
@@ -18316,6 +19230,21 @@ function parseOptimizationResponse(raw) {
18316
19230
  suggestion.mergedContent = mergeMatch[2].trim();
18317
19231
  }
18318
19232
  }
19233
+ if (type === "superseded") {
19234
+ const supersededMatch = actionRaw.match(/supersede\s+(\d+)\s+by\s+(\d+)/i);
19235
+ if (supersededMatch) {
19236
+ suggestion.memoryIds = [
19237
+ parseInt(supersededMatch[1], 10),
19238
+ parseInt(supersededMatch[2], 10)
19239
+ ].filter((n) => !isNaN(n));
19240
+ }
19241
+ }
19242
+ if (type === "categorize" || type === "reclassify") {
19243
+ const catMatch = actionRaw.match(/(?:categorize|reclassify)\s+\d+\s+as\s+(fact|preference|event|decision)/i);
19244
+ if (catMatch) {
19245
+ suggestion.proposedCategory = catMatch[1].toLowerCase();
19246
+ }
19247
+ }
18319
19248
  suggestions.push(suggestion);
18320
19249
  }
18321
19250
  const seen = /* @__PURE__ */ new Set();
@@ -18365,14 +19294,18 @@ function applySuggestion(suggestion, session2) {
18365
19294
  case "merge": {
18366
19295
  if (!suggestion.mergedTrigger || !suggestion.mergedContent) return;
18367
19296
  const sources = suggestion.memoryIds.map((id) => getMemoryById(id)).filter((m) => m !== void 0);
18368
- const sourceType = sources.length > 0 ? sources.sort((a, b) => b.salience - a.salience)[0].type : "semantic";
19297
+ const topSource = sources.length > 0 ? sources.sort((a, b) => b.salience - a.salience)[0] : null;
19298
+ const sourceType = topSource?.type ?? "semantic";
19299
+ const sourceCategory = topSource?.category;
18369
19300
  for (const id of suggestion.memoryIds) {
18370
19301
  deleteMemoryById(id);
18371
19302
  }
18372
19303
  const newId = saveMemoryWithEmbedding(
18373
19304
  suggestion.mergedTrigger,
18374
19305
  suggestion.mergedContent,
18375
- sourceType
19306
+ sourceType,
19307
+ sourceCategory,
19308
+ 14
18376
19309
  );
18377
19310
  session2.createdIds.push(newId);
18378
19311
  break;
@@ -18383,6 +19316,19 @@ function applySuggestion(suggestion, session2) {
18383
19316
  }
18384
19317
  break;
18385
19318
  }
19319
+ case "superseded": {
19320
+ if (suggestion.memoryIds.length >= 2) {
19321
+ markSuperseded(suggestion.memoryIds[0], suggestion.memoryIds[1]);
19322
+ }
19323
+ break;
19324
+ }
19325
+ case "categorize":
19326
+ case "reclassify": {
19327
+ if (suggestion.proposedCategory && suggestion.memoryIds.length >= 1) {
19328
+ updateMemoryCategory(suggestion.memoryIds[0], suggestion.proposedCategory);
19329
+ }
19330
+ break;
19331
+ }
18386
19332
  }
18387
19333
  }
18388
19334
  function resolveAnalysisAdapter(chatId, backendId, model2) {
@@ -18798,12 +19744,19 @@ Analyze these memories and find optimization opportunities:
18798
19744
 
18799
19745
  1. DUPLICATES: Memories containing essentially the same information (different wording, same meaning). When found, recommend keeping the most complete version.
18800
19746
  2. MERGEABLE: Related memories that would be stronger and more token-efficient as a single combined entry.
18801
- 3. STALE: Memories with very low salience scores (below 0.3) AND zero or near-zero access count \u2014 content that has decayed and is never recalled.
19747
+ 3. STALE: Memories with very low half_life (below 3.0) AND zero access count \u2014 content that has never been recalled and has decayed past usefulness.
19748
+ 4. SUPERSEDED: Memories that contradict each other \u2014 a newer memory makes an older one obsolete. When found, recommend marking the older one as superseded by the newer one.
19749
+ 5. UNCATEGORIZED: Memories with category "uncategorized" that can be classified as fact, preference, event, or decision.
19750
+ 6. RECLASSIFY: Memories whose category appears wrong given their content.
19751
+
19752
+ For superseded: ACTION should be "supersede ID1 by ID2" (ID1=older memory, ID2=newer memory)
19753
+ For uncategorized: ACTION should be "categorize ID1 as <category>"
19754
+ For reclassify: ACTION should be "reclassify ID1 as <category>"
18802
19755
 
18803
19756
  For each suggestion, output EXACTLY this format:
18804
19757
  ---
18805
19758
  SUGGESTION: <short descriptive title>
18806
- TYPE: <duplicate|merge|stale>
19759
+ TYPE: <duplicate|merge|stale|superseded|categorize|reclassify>
18807
19760
  IDS: <comma-separated memory IDs to act on>
18808
19761
  REASON: <1-2 sentence explanation>
18809
19762
  ACTION: <"delete ID1" or "delete ID1, ID2" or "merge into: <new trigger> | <new content>">
@@ -18823,12 +19776,18 @@ Rules:
18823
19776
  TYPE_EMOJI = {
18824
19777
  duplicate: "\u{1F501}",
18825
19778
  merge: "\u{1F500}",
18826
- stale: "\u{1F5D1}"
19779
+ stale: "\u{1F5D1}",
19780
+ superseded: "\u{1F504}",
19781
+ categorize: "\u{1F3F7}",
19782
+ reclassify: "\u{1F3F7}"
18827
19783
  };
18828
19784
  TYPE_LABEL = {
18829
19785
  duplicate: "Duplicate",
18830
19786
  merge: "Merge",
18831
- stale: "Stale"
19787
+ stale: "Stale",
19788
+ superseded: "Superseded",
19789
+ categorize: "Categorize",
19790
+ reclassify: "Reclassify"
18832
19791
  };
18833
19792
  }
18834
19793
  });
@@ -18844,9 +19803,12 @@ var init_memory = __esm({
18844
19803
  try {
18845
19804
  const body = JSON.parse(await readBody(req));
18846
19805
  validateAgentIdentity(req, body);
18847
- const { saveMemoryWithEmbedding: saveMemoryWithEmbedding2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
18848
- const id = saveMemoryWithEmbedding2(body.trigger ?? body.tag, body.content, body.type ?? "semantic");
18849
- jsonResponse(res, { success: true, id });
19806
+ const { remember: remember2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
19807
+ const result = await remember2(body.trigger ?? body.tag, body.content, {
19808
+ category: body.category,
19809
+ type: body.type
19810
+ });
19811
+ jsonResponse(res, { success: true, id: result.id });
18850
19812
  } catch (err) {
18851
19813
  jsonResponse(res, { error: errorMessage(err) }, 400);
18852
19814
  }
@@ -18855,8 +19817,8 @@ var init_memory = __esm({
18855
19817
  try {
18856
19818
  const body = JSON.parse(await readBody(req));
18857
19819
  validateAgentIdentity(req, body);
18858
- const { searchMemories: searchMemories2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
18859
- const results = searchMemories2(body.query, body.limit ?? 5);
19820
+ const { recall: recall2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
19821
+ const results = recall2(body.query, { limit: body.limit ?? 5 });
18860
19822
  jsonResponse(res, { success: true, results });
18861
19823
  } catch (err) {
18862
19824
  jsonResponse(res, { error: errorMessage(err) }, 400);
@@ -18865,8 +19827,8 @@ var init_memory = __esm({
18865
19827
  handleMemoryList = async (_req, res, url) => {
18866
19828
  try {
18867
19829
  const limit = parseInt(url.searchParams.get("limit") ?? "10", 10);
18868
- const { getRecentMemories: getRecentMemories2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
18869
- const results = getRecentMemories2(limit);
19830
+ const { list: list2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
19831
+ const results = list2({ limit });
18870
19832
  jsonResponse(res, { success: true, results });
18871
19833
  } catch (err) {
18872
19834
  jsonResponse(res, { error: errorMessage(err) }, 400);
@@ -18876,14 +19838,13 @@ var init_memory = __esm({
18876
19838
  try {
18877
19839
  const body = JSON.parse(await readBody(req));
18878
19840
  validateAgentIdentity(req, body);
19841
+ const { forget: forget2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
18879
19842
  if (body.memoryId) {
18880
- const { deleteMemoryById: deleteMemoryById3 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
18881
- const deleted = deleteMemoryById3(body.memoryId);
18882
- return jsonResponse(res, { success: deleted, mode: "id" });
19843
+ const count = forget2(body.memoryId);
19844
+ return jsonResponse(res, { success: count > 0, mode: "id" });
18883
19845
  }
18884
19846
  if (body.keyword) {
18885
- const { forgetMemory: forgetMemory4 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
18886
- const count = forgetMemory4(body.keyword);
19847
+ const count = forget2(body.keyword);
18887
19848
  return jsonResponse(res, { success: true, count, mode: "keyword" });
18888
19849
  }
18889
19850
  jsonResponse(res, { error: "Either 'keyword' or 'memoryId' is required" }, 400);
@@ -21351,7 +22312,7 @@ var init_api_mcp = __esm({
21351
22312
  });
21352
22313
 
21353
22314
  // src/backends/api-common.ts
21354
- import { streamText, stepCountIs, NoOutputGeneratedError } from "ai";
22315
+ import { streamText, stepCountIs, NoOutputGeneratedError, APICallError } from "ai";
21355
22316
  function toModelMessage(msg) {
21356
22317
  switch (msg.role) {
21357
22318
  case "system":
@@ -21425,7 +22386,7 @@ var init_api_common = __esm({
21425
22386
  } catch (err) {
21426
22387
  log(`[api-common] MCP tools unavailable: ${err instanceof Error ? err.message : String(err)}`);
21427
22388
  }
21428
- const tools2 = buildApiTools(chatId, permMode, mcpTools, getApiWebSearchEnabled(chatId));
22389
+ const tools2 = buildApiTools(chatId, permMode, mcpTools, getApiWebSearchEnabled(chatId), timeoutMs > 0 ? timeoutMs : void 0);
21429
22390
  const hasTools = Object.keys(tools2).length > 0;
21430
22391
  let abortSignal = signal;
21431
22392
  let timeoutHandle;
@@ -21485,6 +22446,9 @@ var init_api_common = __esm({
21485
22446
  const usage2 = await result.usage;
21486
22447
  const providerMeta = await result.providerMetadata;
21487
22448
  const cost = this.extractCost(providerMeta);
22449
+ if (fullText === "" && !usage2.inputTokens) {
22450
+ throw new Error(`${this.backendId}: empty response with no tokens \u2014 possible rate limit or quota exhaustion on free tier`);
22451
+ }
21488
22452
  return {
21489
22453
  text: fullText,
21490
22454
  cost,
@@ -21494,6 +22458,9 @@ var init_api_common = __esm({
21494
22458
  } : void 0
21495
22459
  };
21496
22460
  } catch (err) {
22461
+ if (err instanceof APICallError && err.statusCode === 429) {
22462
+ throw new Error(`${this.backendId}: rate limit (429) \u2014 free tier quota may be exhausted. Retry later or switch to a paid model.`);
22463
+ }
21497
22464
  if (abortSignal?.aborted || signal?.aborted || err instanceof Error && (err.name === "AbortError" || err.message.toLowerCase().includes("abort") || err.message.toLowerCase().includes("cancel"))) {
21498
22465
  return { text: "", cost: null };
21499
22466
  }
@@ -21629,18 +22596,26 @@ var init_ollama2 = __esm({
21629
22596
  */
21630
22597
  async streamDirect(prompt, model2, opts) {
21631
22598
  const cleanPrompt = stripForLocalModel(prompt);
21632
- let forceThinkOff = false;
22599
+ let disableThinking = false;
21633
22600
  try {
21634
22601
  const { OllamaStore } = (init_ollama(), __toCommonJS(ollama_exports));
21635
22602
  const modelRecord = OllamaStore.getModelByName(model2);
21636
- forceThinkOff = modelRecord?.forceThinkOff ?? false;
22603
+ if (modelRecord?.forceThinkOff) {
22604
+ disableThinking = true;
22605
+ } else if (opts?.thinkingLevel === "off") {
22606
+ disableThinking = true;
22607
+ }
21637
22608
  } catch {
21638
22609
  }
21639
22610
  const apiOpts = {
21640
22611
  timeoutMs: opts?.timeoutMs,
21641
22612
  onStream: opts?.onStream,
21642
22613
  signal: opts?.signal,
21643
- ...forceThinkOff ? { providerOptions: { ollama: { think: false } } } : {}
22614
+ messageHistory: opts?.messageHistory,
22615
+ permMode: opts?.permMode,
22616
+ thinkingLevel: opts?.thinkingLevel,
22617
+ onThinking: opts?.onThinking,
22618
+ ...disableThinking ? { providerOptions: { ollama: { think: false } } } : {}
21644
22619
  };
21645
22620
  const result = await this.streamDirectWithHistory(
21646
22621
  cleanPrompt,
@@ -21665,9 +22640,10 @@ var init_ollama2 = __esm({
21665
22640
  const { OllamaStore } = (init_ollama(), __toCommonJS(ollama_exports));
21666
22641
  const models = OllamaStore.getAvailableModels();
21667
22642
  for (const m of models) {
22643
+ const isThinkingCapable = m.capability === "thinking" && !m.forceThinkOff;
21668
22644
  this.availableModels[m.name] = {
21669
22645
  label: `${m.name}${m.parameterSize ? ` (${m.parameterSize})` : ""}`,
21670
- thinking: "none"
22646
+ thinking: isThinkingCapable ? "adjustable" : "none"
21671
22647
  };
21672
22648
  this.pricing[m.name] = { in: 0, out: 0, cache: 0 };
21673
22649
  this.contextWindow[m.name] = m.contextWindow ?? 4096;
@@ -21962,7 +22938,7 @@ var init_backends = __esm({
21962
22938
  ollama: new OllamaAdapter(),
21963
22939
  openrouter: openRouterAdapter
21964
22940
  };
21965
- CHAT_BACKEND_IDS = ["claude", "gemini", "codex", "cursor", "openrouter"];
22941
+ CHAT_BACKEND_IDS = ["claude", "gemini", "codex", "cursor", "openrouter", "ollama"];
21966
22942
  availableSet = /* @__PURE__ */ new Set();
21967
22943
  }
21968
22944
  });
@@ -22158,14 +23134,14 @@ var init_delivery = __esm({
22158
23134
  });
22159
23135
 
22160
23136
  // src/scheduler/retry.ts
22161
- import { APICallError } from "ai";
23137
+ import { APICallError as APICallError2 } from "ai";
22162
23138
  function isExhaustedMessage(text) {
22163
23139
  return EXHAUSTED_PATTERNS.some((p) => p.test(text));
22164
23140
  }
22165
23141
  function classifyError(err) {
22166
23142
  const msg = err instanceof Error ? err.message : String(err);
22167
23143
  if (/spawn timeout/i.test(msg)) return "permanent";
22168
- if (err instanceof APICallError && err.isRetryable) {
23144
+ if (err instanceof APICallError2 && err.isRetryable) {
22169
23145
  return "transient";
22170
23146
  }
22171
23147
  for (const pattern of EXHAUSTED_PATTERNS) {
@@ -22577,7 +23553,7 @@ async function classifyIntentAsync(text, chatId) {
22577
23553
  return "agentic";
22578
23554
  }
22579
23555
  var intentCounts, CHAT_EXACT, MUTATION_PATTERNS, CHAT_QUESTION_PATTERNS, STRUCTURAL_PATTERNS, LLM_CLASSIFY_PROMPT, LLM_CLASSIFY_TIMEOUT_MS;
22580
- var init_classify = __esm({
23556
+ var init_classify2 = __esm({
22581
23557
  "src/intent/classify.ts"() {
22582
23558
  "use strict";
22583
23559
  init_store5();
@@ -23324,7 +24300,7 @@ function classifyAgentIntent(message) {
23324
24300
  return NOT_DETECTED;
23325
24301
  }
23326
24302
  var AGENT_SIGNAL_PATTERNS, CLAW_SIGNAL_PATTERNS, NEGATIVE_PATTERNS;
23327
- var init_classify2 = __esm({
24303
+ var init_classify3 = __esm({
23328
24304
  "src/agents/classify.ts"() {
23329
24305
  "use strict";
23330
24306
  AGENT_SIGNAL_PATTERNS = [
@@ -23559,13 +24535,152 @@ var init_session_log2 = __esm({
23559
24535
  }
23560
24536
  });
23561
24537
 
23562
- // src/router/live-status.ts
23563
- function canFlushGlobally() {
23564
- return Date.now() - globalLastFlushAt >= GLOBAL_MIN_GAP_MS;
24538
+ // src/channels/edit-coordinator.ts
24539
+ var edit_coordinator_exports = {};
24540
+ __export(edit_coordinator_exports, {
24541
+ getEditCoordinator: () => getEditCoordinator,
24542
+ resetEditCoordinator: () => resetEditCoordinator
24543
+ });
24544
+ function getEditCoordinator() {
24545
+ return EditCoordinator.getInstance();
23565
24546
  }
23566
- function markGlobalFlush() {
23567
- globalLastFlushAt = Date.now();
24547
+ function resetEditCoordinator() {
24548
+ EditCoordinator.resetInstance();
23568
24549
  }
24550
+ var TICK_INTERVAL_MS, MAX_EDITS_PER_WINDOW, EDIT_WINDOW_MS, EditCoordinator;
24551
+ var init_edit_coordinator = __esm({
24552
+ "src/channels/edit-coordinator.ts"() {
24553
+ "use strict";
24554
+ init_log();
24555
+ TICK_INTERVAL_MS = 1e3;
24556
+ MAX_EDITS_PER_WINDOW = 4;
24557
+ EDIT_WINDOW_MS = 6e4;
24558
+ EditCoordinator = class _EditCoordinator {
24559
+ static instance = null;
24560
+ /** Active streams indexed by messageId. */
24561
+ activeStreams = /* @__PURE__ */ new Map();
24562
+ /** Per-message edit tracking for the sliding window cap. */
24563
+ perMessageEditCount = /* @__PURE__ */ new Map();
24564
+ /** Single flush timer shared across all streams. */
24565
+ flushTimer = null;
24566
+ /** Round-robin index — cycles through stream keys. */
24567
+ roundRobinIndex = 0;
24568
+ /** Ordered list of stream keys for round-robin iteration.
24569
+ * Rebuilt on register/unregister to avoid iterator invalidation. */
24570
+ streamKeys = [];
24571
+ constructor() {
24572
+ }
24573
+ static getInstance() {
24574
+ if (!_EditCoordinator.instance) {
24575
+ _EditCoordinator.instance = new _EditCoordinator();
24576
+ }
24577
+ return _EditCoordinator.instance;
24578
+ }
24579
+ /** Reset the singleton (for testing only). */
24580
+ static resetInstance() {
24581
+ if (_EditCoordinator.instance) {
24582
+ _EditCoordinator.instance.shutdown();
24583
+ _EditCoordinator.instance = null;
24584
+ }
24585
+ }
24586
+ /** Register a stream to be managed by the coordinator.
24587
+ * Creates the flush timer if this is the first stream. */
24588
+ register(messageId, stream) {
24589
+ this.activeStreams.set(messageId, stream);
24590
+ this.rebuildKeys();
24591
+ log(`[edit-coordinator] registered stream ${messageId} (${this.activeStreams.size} active)`);
24592
+ if (!this.flushTimer) {
24593
+ this.flushTimer = setInterval(() => this.tick(), TICK_INTERVAL_MS);
24594
+ log(`[edit-coordinator] timer started (${TICK_INTERVAL_MS}ms tick)`);
24595
+ }
24596
+ }
24597
+ /** Unregister a stream (called on finalization).
24598
+ * Cleans up per-message edit tracking. Stops timer if no streams remain. */
24599
+ unregister(messageId) {
24600
+ this.activeStreams.delete(messageId);
24601
+ this.perMessageEditCount.delete(messageId);
24602
+ this.rebuildKeys();
24603
+ log(`[edit-coordinator] unregistered stream ${messageId} (${this.activeStreams.size} remaining)`);
24604
+ if (this.activeStreams.size === 0 && this.flushTimer) {
24605
+ clearInterval(this.flushTimer);
24606
+ this.flushTimer = null;
24607
+ this.roundRobinIndex = 0;
24608
+ log(`[edit-coordinator] timer stopped (no active streams)`);
24609
+ }
24610
+ }
24611
+ /** Shut down the coordinator — clear timer, remove all streams. */
24612
+ shutdown() {
24613
+ if (this.flushTimer) {
24614
+ clearInterval(this.flushTimer);
24615
+ this.flushTimer = null;
24616
+ }
24617
+ this.activeStreams.clear();
24618
+ this.perMessageEditCount.clear();
24619
+ this.streamKeys = [];
24620
+ this.roundRobinIndex = 0;
24621
+ }
24622
+ /** Get the number of active streams (for diagnostics). */
24623
+ get streamCount() {
24624
+ return this.activeStreams.size;
24625
+ }
24626
+ /** Check whether a message can be edited (under the per-message cap). */
24627
+ canEditMessage(messageId) {
24628
+ const record = this.perMessageEditCount.get(messageId);
24629
+ if (!record) return true;
24630
+ const now = Date.now();
24631
+ if (now - record.windowStart >= EDIT_WINDOW_MS) {
24632
+ return true;
24633
+ }
24634
+ return record.count < MAX_EDITS_PER_WINDOW;
24635
+ }
24636
+ /** Record that an edit was made to a message. */
24637
+ recordEdit(messageId) {
24638
+ const now = Date.now();
24639
+ const record = this.perMessageEditCount.get(messageId);
24640
+ if (!record || now - record.windowStart >= EDIT_WINDOW_MS) {
24641
+ this.perMessageEditCount.set(messageId, { count: 1, windowStart: now });
24642
+ } else {
24643
+ record.count++;
24644
+ }
24645
+ }
24646
+ // ── Internal ──────────────────────────────────────────────────────────
24647
+ /** Rebuild the ordered keys array after registration changes. */
24648
+ rebuildKeys() {
24649
+ this.streamKeys = Array.from(this.activeStreams.keys());
24650
+ if (this.streamKeys.length > 0) {
24651
+ this.roundRobinIndex = this.roundRobinIndex % this.streamKeys.length;
24652
+ } else {
24653
+ this.roundRobinIndex = 0;
24654
+ }
24655
+ }
24656
+ /** Timer tick — pick the next stream via round-robin and flush it.
24657
+ * If the selected stream is at its per-message edit cap, try the next one. */
24658
+ async tick() {
24659
+ if (this.streamKeys.length === 0) return;
24660
+ const startIdx = this.roundRobinIndex;
24661
+ let tried = 0;
24662
+ while (tried < this.streamKeys.length) {
24663
+ const idx = (startIdx + tried) % this.streamKeys.length;
24664
+ const messageId = this.streamKeys[idx];
24665
+ const stream = this.activeStreams.get(messageId);
24666
+ if (stream && this.canEditMessage(messageId)) {
24667
+ this.roundRobinIndex = (idx + 1) % this.streamKeys.length;
24668
+ try {
24669
+ await stream.flush();
24670
+ this.recordEdit(messageId);
24671
+ } catch {
24672
+ }
24673
+ return;
24674
+ }
24675
+ tried++;
24676
+ }
24677
+ this.roundRobinIndex = (startIdx + 1) % this.streamKeys.length;
24678
+ }
24679
+ };
24680
+ }
24681
+ });
24682
+
24683
+ // src/router/live-status.ts
23569
24684
  function dedupThinking(entries) {
23570
24685
  const out = [];
23571
24686
  for (const e of entries) {
@@ -23629,18 +24744,15 @@ function makeLiveStatus(chatId, channel, modelLabel, verboseLevel, showThinking)
23629
24744
  };
23630
24745
  return { liveStatus, toolCb };
23631
24746
  }
23632
- var FLUSH_INTERVAL_DM_MS, FLUSH_INTERVAL_GROUP_MS, MAX_THINKING_CHARS, GLOBAL_MIN_GAP_MS, globalLastFlushAt, TRIM_THRESHOLD, MAX_ENTRIES, SPINNER_FRAMES, HEARTBEAT_TEXTS, LiveStatusMessage;
24747
+ var MAX_THINKING_CHARS, TRIM_THRESHOLD, MAX_ENTRIES, SPINNER_FRAMES, HEARTBEAT_TEXTS, LiveStatusMessage;
23633
24748
  var init_live_status = __esm({
23634
24749
  "src/router/live-status.ts"() {
23635
24750
  "use strict";
23636
24751
  init_log();
23637
24752
  init_helpers();
23638
24753
  init_telegram_throttle();
23639
- FLUSH_INTERVAL_DM_MS = 2e3;
23640
- FLUSH_INTERVAL_GROUP_MS = 5e3;
24754
+ init_edit_coordinator();
23641
24755
  MAX_THINKING_CHARS = 800;
23642
- GLOBAL_MIN_GAP_MS = 1e3;
23643
- globalLastFlushAt = 0;
23644
24756
  TRIM_THRESHOLD = 3500;
23645
24757
  MAX_ENTRIES = 200;
23646
24758
  SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -23656,7 +24768,6 @@ var init_live_status = __esm({
23656
24768
  messageId = null;
23657
24769
  entries = [];
23658
24770
  startTime = Date.now();
23659
- flushTimer = null;
23660
24771
  lastRendered = "";
23661
24772
  finalized = false;
23662
24773
  /** Earliest time the next flush is allowed (set after 429 backoff) */
@@ -23671,18 +24782,12 @@ var init_live_status = __esm({
23671
24782
  /** Spinner frame counter — advances on each flush for animation. */
23672
24783
  spinnerFrame = 0;
23673
24784
  /** Timestamp of last successful edit — used for heartbeat force-through. */
23674
- lastSuccessfulFlushAt = Date.now();
23675
24785
  /** Callback to restart typing indicator as fallback. */
23676
24786
  onTypingFallback;
23677
24787
  /** Set a callback that restarts the typing indicator loop as a fallback. */
23678
24788
  setTypingFallback(cb) {
23679
24789
  this.onTypingFallback = cb;
23680
24790
  }
23681
- /** Resolve flush interval based on chat type (group chats are rate-limited more aggressively). */
23682
- get flushIntervalMs() {
23683
- const numericId = parseInt(this.chatId);
23684
- return numericId < 0 ? FLUSH_INTERVAL_GROUP_MS : FLUSH_INTERVAL_DM_MS;
23685
- }
23686
24791
  /** Send the initial status message. Must be called before adding entries. */
23687
24792
  async init() {
23688
24793
  if (!this.channel.sendTextReturningId) return;
@@ -23690,9 +24795,7 @@ var init_live_status = __esm({
23690
24795
  const initial = `\u23F3 ${this.modelLabel} \xB7 Processing\u2026`;
23691
24796
  this.messageId = await this.channel.sendTextReturningId(this.chatId, initial, "plain") ?? null;
23692
24797
  if (this.messageId) {
23693
- this.lastSuccessfulFlushAt = Date.now();
23694
- this.flushTimer = setInterval(() => this.flush().catch(() => {
23695
- }), this.flushIntervalMs);
24798
+ getEditCoordinator().register(this.messageId, this);
23696
24799
  }
23697
24800
  } catch (err) {
23698
24801
  log(`[live-status] init failed: ${err}`);
@@ -23744,21 +24847,22 @@ var init_live_status = __esm({
23744
24847
  }
23745
24848
  /**
23746
24849
  * Finalize the status message: replace the spinner with ✅ and elapsed time.
23747
- * Stops the flush loop. No-op if no message was created (channel doesn't support editing).
24850
+ * Unregisters from EditCoordinator and sends one final P0_CRITICAL edit that
24851
+ * bypasses the coordinator entirely (finalization must never be skipped).
24852
+ * No-op if no message was created (channel doesn't support editing).
23748
24853
  */
23749
24854
  async finalize(elapsedMs) {
23750
24855
  this.finalized = true;
23751
24856
  this.pendingTools.clear();
23752
- if (this.flushTimer) {
23753
- clearInterval(this.flushTimer);
23754
- this.flushTimer = null;
24857
+ if (this.messageId) {
24858
+ getEditCoordinator().unregister(this.messageId);
23755
24859
  }
23756
24860
  if (!this.messageId || !this.channel.editText) return;
23757
24861
  const elapsedSec = (elapsedMs / 1e3).toFixed(1);
23758
24862
  const deduped = dedupThinking(this.entries);
23759
24863
  const body = renderFinal(deduped, this.modelLabel, elapsedSec, this.hasTrimmed);
23760
24864
  try {
23761
- await this.channel.editText(this.chatId, this.messageId, body, "plain");
24865
+ await this.channel.editText(this.chatId, this.messageId, body, "plain", 0 /* P0_CRITICAL */);
23762
24866
  } catch (err) {
23763
24867
  log(`[live-status] finalize edit failed: ${err}`);
23764
24868
  }
@@ -23767,7 +24871,16 @@ var init_live_status = __esm({
23767
24871
  getMessageId() {
23768
24872
  return this.messageId;
23769
24873
  }
23770
- // ── Internal ──────────────────────────────────────────────────────────
24874
+ // ── FlushableStream interface ──────────────────────────────────────────
24875
+ /** Return the chatId this stream belongs to (FlushableStream interface). */
24876
+ getChatId() {
24877
+ return this.chatId;
24878
+ }
24879
+ /**
24880
+ * Flush the current status to Telegram via editMessageText.
24881
+ * Called by the EditCoordinator on each round-robin tick.
24882
+ * Public to satisfy FlushableStream interface — do not call directly.
24883
+ */
23771
24884
  async flush() {
23772
24885
  if (this.finalized || !this.messageId || !this.channel.editText) return;
23773
24886
  if (this.consecutiveEditFailures >= _LiveStatusMessage.MAX_EDIT_FAILURES) {
@@ -23780,10 +24893,9 @@ var init_live_status = __esm({
23780
24893
  if (Date.now() < this.nextFlushAllowedAt) return;
23781
24894
  const throttleState = getThrottleState();
23782
24895
  if (throttleState?.isPaused) return;
23783
- if (!canFlushGlobally()) return;
23784
24896
  this.spinnerFrame++;
23785
24897
  const deduped = dedupThinking(this.entries);
23786
- const body = renderEntries(
24898
+ let body = renderEntries(
23787
24899
  deduped,
23788
24900
  this.modelLabel,
23789
24901
  Date.now() - this.startTime,
@@ -23791,17 +24903,21 @@ var init_live_status = __esm({
23791
24903
  this.pendingTools,
23792
24904
  this.spinnerFrame
23793
24905
  );
24906
+ if (throttleState && throttleState.queueDepth > 30) {
24907
+ body += `
24908
+ (queue: ${throttleState.queueDepth})`;
24909
+ }
23794
24910
  if (body === this.lastRendered) return;
23795
24911
  this.lastRendered = body;
23796
- markGlobalFlush();
23797
24912
  try {
23798
- await this.channel.editText(this.chatId, this.messageId, body, "plain");
24913
+ await this.channel.editText(this.chatId, this.messageId, body, "plain", 2 /* P2_COSMETIC */);
23799
24914
  this.consecutiveEditFailures = 0;
23800
24915
  this.lastSuccessfulFlushAt = Date.now();
23801
24916
  } catch (err) {
23802
24917
  this.handleRateLimit(err);
23803
24918
  }
23804
24919
  }
24920
+ // ── Internal ──────────────────────────────────────────────────────────
23805
24921
  /**
23806
24922
  * Trim entries from the BEGINNING when the rendered body exceeds the threshold.
23807
24923
  * This is the core of the single-message pattern: always show the most recent
@@ -24232,6 +25348,88 @@ var init_response = __esm({
24232
25348
  }
24233
25349
  });
24234
25350
 
25351
+ // src/channels/typing-manager.ts
25352
+ var typing_manager_exports = {};
25353
+ __export(typing_manager_exports, {
25354
+ getTypingManager: () => getTypingManager,
25355
+ resetTypingManager: () => resetTypingManager
25356
+ });
25357
+ function getTypingManager() {
25358
+ return TypingManager.getInstance();
25359
+ }
25360
+ function resetTypingManager() {
25361
+ TypingManager.resetInstance();
25362
+ }
25363
+ var TypingManager;
25364
+ var init_typing_manager = __esm({
25365
+ "src/channels/typing-manager.ts"() {
25366
+ "use strict";
25367
+ TypingManager = class _TypingManager {
25368
+ static instance = null;
25369
+ activeChats = /* @__PURE__ */ new Map();
25370
+ static getInstance() {
25371
+ if (!_TypingManager.instance) {
25372
+ _TypingManager.instance = new _TypingManager();
25373
+ }
25374
+ return _TypingManager.instance;
25375
+ }
25376
+ /**
25377
+ * Start showing typing indicator for this chat.
25378
+ * If already showing (another agent acquired), just increments refCount.
25379
+ */
25380
+ acquire(chatId, channel) {
25381
+ const entry = this.activeChats.get(chatId);
25382
+ if (entry) {
25383
+ entry.refCount++;
25384
+ return;
25385
+ }
25386
+ channel.sendTyping?.(chatId).catch(() => {
25387
+ });
25388
+ const timer = setInterval(() => {
25389
+ channel.sendTyping?.(chatId).catch(() => {
25390
+ });
25391
+ }, 4e3);
25392
+ this.activeChats.set(chatId, { refCount: 1, timer });
25393
+ }
25394
+ /**
25395
+ * Stop showing typing for this agent's perspective.
25396
+ * Only stops the timer when refCount reaches 0.
25397
+ */
25398
+ release(chatId) {
25399
+ const entry = this.activeChats.get(chatId);
25400
+ if (!entry) return;
25401
+ entry.refCount--;
25402
+ if (entry.refCount <= 0) {
25403
+ clearInterval(entry.timer);
25404
+ this.activeChats.delete(chatId);
25405
+ }
25406
+ }
25407
+ /** Clean shutdown — clear all timers. */
25408
+ shutdown() {
25409
+ for (const [, entry] of this.activeChats) {
25410
+ clearInterval(entry.timer);
25411
+ }
25412
+ this.activeChats.clear();
25413
+ }
25414
+ /** Expose active chat count for testing/diagnostics. */
25415
+ get size() {
25416
+ return this.activeChats.size;
25417
+ }
25418
+ /** Get ref count for a chat (for testing). */
25419
+ getRefCount(chatId) {
25420
+ return this.activeChats.get(chatId)?.refCount ?? 0;
25421
+ }
25422
+ /** Reset singleton (for testing only). */
25423
+ static resetInstance() {
25424
+ if (_TypingManager.instance) {
25425
+ _TypingManager.instance.shutdown();
25426
+ _TypingManager.instance = null;
25427
+ }
25428
+ }
25429
+ };
25430
+ }
25431
+ });
25432
+
24235
25433
  // src/shell/exec.ts
24236
25434
  import { execFile as execFile4 } from "child_process";
24237
25435
  function resolveShell() {
@@ -24546,6 +25744,9 @@ async function handleVoice(msg, channel) {
24546
25744
  await channel.sendText(chatId, "Couldn't transcribe the voice message. Make sure a transcription provider is configured via /voice.", { parseMode: "plain" });
24547
25745
  return;
24548
25746
  }
25747
+ if (getSttEcho(chatId)) {
25748
+ await channel.sendText(chatId, `\u{1F399} ${transcript}`, { parseMode: "plain" });
25749
+ }
24549
25750
  const vBackendId = getBackend(chatId) ?? "claude";
24550
25751
  const vLimitMsg = checkBackendLimits(vBackendId);
24551
25752
  if (vLimitMsg) {
@@ -24829,168 +26030,6 @@ var init_media = __esm({
24829
26030
  }
24830
26031
  });
24831
26032
 
24832
- // src/router/state.ts
24833
- var state_exports = {};
24834
- __export(state_exports, {
24835
- activeSideQuests: () => activeSideQuests,
24836
- bypassBusyCheck: () => bypassBusyCheck,
24837
- clearHistoryFilter: () => clearHistoryFilter,
24838
- clearPendingCliAddition: () => clearPendingCliAddition,
24839
- clearPendingModelResults: () => clearPendingModelResults,
24840
- clearPendingModelSearch: () => clearPendingModelSearch,
24841
- councilResults: () => councilResults,
24842
- dashboardClawWarnings: () => dashboardClawWarnings,
24843
- getActiveSideQuestCount: () => getActiveSideQuestCount,
24844
- historyFilters: () => historyFilters,
24845
- parseSideQuestPrefix: () => parseSideQuestPrefix,
24846
- pendingCliAdditions: () => pendingCliAdditions,
24847
- pendingInterrupts: () => pendingInterrupts,
24848
- pendingMcpImports: () => pendingMcpImports,
24849
- pendingModelResults: () => pendingModelResults,
24850
- pendingModelSearch: () => pendingModelSearch,
24851
- pendingNewchatUndo: () => pendingNewchatUndo,
24852
- pendingSummaryUndo: () => pendingSummaryUndo,
24853
- setCouncilResult: () => setCouncilResult,
24854
- setHistoryFilter: () => setHistoryFilter,
24855
- setPendingCliAddition: () => setPendingCliAddition,
24856
- setPendingModelResults: () => setPendingModelResults,
24857
- setPendingModelSearch: () => setPendingModelSearch,
24858
- startStateSweep: () => startStateSweep,
24859
- stopAllSideQuests: () => stopAllSideQuests,
24860
- stopStateSweep: () => stopStateSweep
24861
- });
24862
- function setHistoryFilter(chatId, filter) {
24863
- historyFilters.set(chatId, filter);
24864
- historyFilterTimestamps.set(chatId, Date.now());
24865
- }
24866
- function clearHistoryFilter(chatId) {
24867
- historyFilters.delete(chatId);
24868
- historyFilterTimestamps.delete(chatId);
24869
- }
24870
- function setCouncilResult(chatId, result) {
24871
- councilResults.set(chatId, result);
24872
- councilResultTimestamps.set(chatId, Date.now());
24873
- }
24874
- function setPendingModelSearch(chatId, state) {
24875
- pendingModelSearch.set(chatId, state);
24876
- pendingModelSearchTimestamps.set(chatId, Date.now());
24877
- }
24878
- function clearPendingModelSearch(chatId) {
24879
- pendingModelSearch.delete(chatId);
24880
- pendingModelSearchTimestamps.delete(chatId);
24881
- }
24882
- function setPendingModelResults(chatId, results) {
24883
- pendingModelResults.set(chatId, results);
24884
- }
24885
- function clearPendingModelResults(chatId) {
24886
- pendingModelResults.delete(chatId);
24887
- }
24888
- function parseSideQuestPrefix(text) {
24889
- const match = text.match(/^(?:sq|btw):\s*/i);
24890
- if (match) return { isSideQuest: true, cleanText: text.slice(match[0].length) };
24891
- return { isSideQuest: false, cleanText: text };
24892
- }
24893
- function getActiveSideQuestCount(chatId) {
24894
- return activeSideQuests.get(chatId)?.size ?? 0;
24895
- }
24896
- function stopAllSideQuests(chatId) {
24897
- const active = activeSideQuests.get(chatId);
24898
- if (active) {
24899
- for (const sqId of active) {
24900
- stopAgent(sqId);
24901
- }
24902
- }
24903
- }
24904
- function startStateSweep() {
24905
- if (sweepTimer) return;
24906
- sweepTimer = setInterval(() => {
24907
- const now = Date.now();
24908
- for (const [chatId, ts2] of dashboardClawWarnings) {
24909
- if (now - ts2 > STALE_THRESHOLD_MS) dashboardClawWarnings.delete(chatId);
24910
- }
24911
- for (const [cid, ts2] of historyFilterTimestamps) {
24912
- if (now - ts2 > STALE_THRESHOLD_MS) {
24913
- historyFilters.delete(cid);
24914
- historyFilterTimestamps.delete(cid);
24915
- }
24916
- }
24917
- for (const [cid, ts2] of councilResultTimestamps) {
24918
- if (now - ts2 > STALE_THRESHOLD_MS) {
24919
- councilResults.delete(cid);
24920
- councilResultTimestamps.delete(cid);
24921
- }
24922
- }
24923
- for (const [cid, ts2] of pendingModelSearchTimestamps) {
24924
- if (now - ts2 > STALE_THRESHOLD_MS) {
24925
- pendingModelSearch.delete(cid);
24926
- pendingModelSearchTimestamps.delete(cid);
24927
- }
24928
- }
24929
- for (const chatId of pendingInterrupts.keys()) {
24930
- if (!_interruptSeen.has(chatId)) {
24931
- _interruptSeen.add(chatId);
24932
- } else {
24933
- pendingInterrupts.delete(chatId);
24934
- _interruptSeen.delete(chatId);
24935
- }
24936
- }
24937
- for (const chatId of _interruptSeen) {
24938
- if (!pendingInterrupts.has(chatId)) _interruptSeen.delete(chatId);
24939
- }
24940
- for (const [cid, ts2] of pendingCliTimestamps) {
24941
- if (now - ts2 > STALE_THRESHOLD_MS) {
24942
- pendingCliAdditions.delete(cid);
24943
- pendingCliTimestamps.delete(cid);
24944
- }
24945
- }
24946
- for (const [cid, state] of pendingMcpImports) {
24947
- if (now - state.startedAt > STALE_THRESHOLD_MS) pendingMcpImports.delete(cid);
24948
- }
24949
- }, SWEEP_INTERVAL_MS);
24950
- sweepTimer.unref();
24951
- }
24952
- function stopStateSweep() {
24953
- if (sweepTimer) {
24954
- clearInterval(sweepTimer);
24955
- sweepTimer = null;
24956
- }
24957
- }
24958
- function setPendingCliAddition(chatId, messageId) {
24959
- pendingCliAdditions.set(chatId, messageId);
24960
- pendingCliTimestamps.set(chatId, Date.now());
24961
- }
24962
- function clearPendingCliAddition(chatId) {
24963
- pendingCliAdditions.delete(chatId);
24964
- pendingCliTimestamps.delete(chatId);
24965
- }
24966
- var pendingInterrupts, bypassBusyCheck, activeSideQuests, dashboardClawWarnings, pendingSummaryUndo, pendingNewchatUndo, historyFilters, historyFilterTimestamps, councilResults, councilResultTimestamps, pendingModelSearch, pendingModelSearchTimestamps, pendingModelResults, SWEEP_INTERVAL_MS, STALE_THRESHOLD_MS, sweepTimer, _interruptSeen, pendingMcpImports, pendingCliAdditions, pendingCliTimestamps;
24967
- var init_state = __esm({
24968
- "src/router/state.ts"() {
24969
- "use strict";
24970
- init_agent();
24971
- pendingInterrupts = /* @__PURE__ */ new Map();
24972
- bypassBusyCheck = /* @__PURE__ */ new Set();
24973
- activeSideQuests = /* @__PURE__ */ new Map();
24974
- dashboardClawWarnings = /* @__PURE__ */ new Map();
24975
- pendingSummaryUndo = /* @__PURE__ */ new Map();
24976
- pendingNewchatUndo = /* @__PURE__ */ new Map();
24977
- historyFilters = /* @__PURE__ */ new Map();
24978
- historyFilterTimestamps = /* @__PURE__ */ new Map();
24979
- councilResults = /* @__PURE__ */ new Map();
24980
- councilResultTimestamps = /* @__PURE__ */ new Map();
24981
- pendingModelSearch = /* @__PURE__ */ new Map();
24982
- pendingModelSearchTimestamps = /* @__PURE__ */ new Map();
24983
- pendingModelResults = /* @__PURE__ */ new Map();
24984
- SWEEP_INTERVAL_MS = 30 * 60 * 1e3;
24985
- STALE_THRESHOLD_MS = 30 * 60 * 1e3;
24986
- sweepTimer = null;
24987
- _interruptSeen = /* @__PURE__ */ new Set();
24988
- pendingMcpImports = /* @__PURE__ */ new Map();
24989
- pendingCliAdditions = /* @__PURE__ */ new Map();
24990
- pendingCliTimestamps = /* @__PURE__ */ new Map();
24991
- }
24992
- });
24993
-
24994
26033
  // src/router/sidequest.ts
24995
26034
  import { randomUUID as randomUUID3 } from "crypto";
24996
26035
  async function handleSideQuest(parentChatId, msg, channel) {
@@ -25006,18 +26045,7 @@ async function handleSideQuest(parentChatId, msg, channel) {
25006
26045
  [{ label: "\u274C Cancel", data: `sq:cancel:${sqId}` }]
25007
26046
  ]);
25008
26047
  const startTime = Date.now();
25009
- let typingActive = true;
25010
- const typingLoop = async () => {
25011
- while (typingActive) {
25012
- try {
25013
- await channel.sendTyping?.(parentChatId);
25014
- } catch {
25015
- }
25016
- await new Promise((r) => setTimeout(r, 4e3));
25017
- }
25018
- };
25019
- typingLoop().catch(() => {
25020
- });
26048
+ getTypingManager().acquire(parentChatId, channel);
25021
26049
  try {
25022
26050
  const backend2 = getBackend(parentChatId);
25023
26051
  const model2 = getModel(parentChatId);
@@ -25033,7 +26061,6 @@ async function handleSideQuest(parentChatId, msg, channel) {
25033
26061
  entityType: "main",
25034
26062
  bootstrapProfile: "interactive"
25035
26063
  });
25036
- typingActive = false;
25037
26064
  const userText = msg.text ?? "";
25038
26065
  const truncated = userText.length > 60 ? userText.slice(0, 57) + "\u2026" : userText;
25039
26066
  const header2 = `\u{1F5FA} <b>Side quest: "${truncated}"</b>
@@ -25057,10 +26084,9 @@ async function handleSideQuest(parentChatId, msg, channel) {
25057
26084
  log(`[reflection] Side quest signal detection error: ${e}`);
25058
26085
  }
25059
26086
  } catch (err) {
25060
- typingActive = false;
25061
26087
  await channel.sendText(parentChatId, `\u{1F5FA} Side quest failed: ${err.message}`, { parseMode: "plain" });
25062
26088
  } finally {
25063
- typingActive = false;
26089
+ getTypingManager().release(parentChatId);
25064
26090
  const activeSet = activeSideQuests.get(parentChatId);
25065
26091
  if (activeSet) {
25066
26092
  activeSet.delete(sqId);
@@ -25072,6 +26098,7 @@ async function handleSideQuest(parentChatId, msg, channel) {
25072
26098
  var init_sidequest = __esm({
25073
26099
  "src/router/sidequest.ts"() {
25074
26100
  "use strict";
26101
+ init_typing_manager();
25075
26102
  init_agent();
25076
26103
  init_log();
25077
26104
  init_store5();
@@ -25513,19 +26540,27 @@ async function handleEvolveCallback(chatId, data, channel, messageId) {
25513
26540
  break;
25514
26541
  }
25515
26542
  case "apply": {
25516
- const { applyInsight: applyInsight2 } = await Promise.resolve().then(() => (init_apply(), apply_exports));
25517
- const { advanceReviewSession: advanceReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
25518
- const result = await applyInsight2(parseInt(idStr, 10));
25519
- advanceReviewSession2(getDb(), chatId, parseInt(idStr, 10), "applied");
25520
- await sendOrEditKeyboard(
25521
- chatId,
25522
- channel,
25523
- messageId,
25524
- `\u2705 ${result.message}`,
25525
- []
25526
- );
25527
- await new Promise((r) => setTimeout(r, 800));
25528
- await sendCurrentProposalInPlace(chatId, channel, messageId);
26543
+ const id = parseInt(idStr, 10);
26544
+ if (processingInsights.has(id)) break;
26545
+ processingInsights.add(id);
26546
+ await sendOrEditKeyboard(chatId, channel, messageId, "\u23F3 Applying...", []);
26547
+ try {
26548
+ const { applyInsight: applyInsight2 } = await Promise.resolve().then(() => (init_apply(), apply_exports));
26549
+ const { advanceReviewSession: advanceReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
26550
+ const result = await applyInsight2(id);
26551
+ advanceReviewSession2(getDb(), chatId, id, "applied");
26552
+ await sendOrEditKeyboard(
26553
+ chatId,
26554
+ channel,
26555
+ messageId,
26556
+ `\u2705 ${result.message}`,
26557
+ []
26558
+ );
26559
+ await new Promise((r) => setTimeout(r, 800));
26560
+ await sendCurrentProposalInPlace(chatId, channel, messageId);
26561
+ } finally {
26562
+ processingInsights.delete(id);
26563
+ }
25529
26564
  break;
25530
26565
  }
25531
26566
  case "skip": {
@@ -25535,10 +26570,17 @@ async function handleEvolveCallback(chatId, data, channel, messageId) {
25535
26570
  break;
25536
26571
  }
25537
26572
  case "reject": {
25538
- const { updateInsightStatus: updateInsightStatus2, advanceReviewSession: advanceReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
25539
- updateInsightStatus2(getDb(), parseInt(idStr, 10), "rejected");
25540
- advanceReviewSession2(getDb(), chatId, parseInt(idStr, 10), "rejected");
25541
- await sendCurrentProposalInPlace(chatId, channel, messageId);
26573
+ const rejId = parseInt(idStr, 10);
26574
+ if (processingInsights.has(rejId)) break;
26575
+ processingInsights.add(rejId);
26576
+ try {
26577
+ const { updateInsightStatus: updateInsightStatus2, advanceReviewSession: advanceReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
26578
+ updateInsightStatus2(getDb(), rejId, "rejected");
26579
+ advanceReviewSession2(getDb(), chatId, rejId, "rejected");
26580
+ await sendCurrentProposalInPlace(chatId, channel, messageId);
26581
+ } finally {
26582
+ processingInsights.delete(rejId);
26583
+ }
25542
26584
  break;
25543
26585
  }
25544
26586
  case "discuss": {
@@ -25785,11 +26827,13 @@ async function handleReflectCallback(chatId, data, channel, messageId) {
25785
26827
  );
25786
26828
  }
25787
26829
  }
26830
+ var processingInsights;
25788
26831
  var init_evolve2 = __esm({
25789
26832
  "src/router/evolve.ts"() {
25790
26833
  "use strict";
25791
26834
  init_store5();
25792
26835
  init_helpers();
26836
+ processingInsights = /* @__PURE__ */ new Set();
25793
26837
  }
25794
26838
  });
25795
26839
 
@@ -27954,15 +28998,16 @@ async function handleStopCommand(chatId, commandArgs, msg, channel) {
27954
28998
  const stopped = stopAgent(chatId);
27955
28999
  stopAllSideQuests(chatId);
27956
29000
  cancelAllAgents(chatId);
29001
+ if (stopped) addForceStoppedChat(chatId);
27957
29002
  await channel.sendText(
27958
29003
  chatId,
27959
29004
  stopped ? "Stopping current task..." : "Nothing is running.",
27960
- { parseMode: "plain", priority: true }
29005
+ { parseMode: "plain", priority: 0 /* P0_CRITICAL */ }
27961
29006
  );
27962
29007
  if (stopped && typeof channel.sendKeyboard === "function") {
27963
29008
  await channel.sendKeyboard(chatId, "", [
27964
29009
  [{ label: "\u{1F195} New Chat", data: "menu:newchat" }]
27965
- ], { priority: true });
29010
+ ], { priority: 0 /* P0_CRITICAL */ });
27966
29011
  }
27967
29012
  }
27968
29013
  async function handleVoiceCommand(chatId, commandArgs, msg, channel) {
@@ -27984,7 +29029,7 @@ async function handleRememberCommand(chatId, commandArgs, msg, channel) {
27984
29029
  }
27985
29030
  const content = commandArgs.replace(/^that\s+/i, "");
27986
29031
  const trigger = content.split(/\s+/).slice(0, 3).join(" ");
27987
- saveMemoryWithEmbedding(trigger, content, "semantic");
29032
+ await remember(trigger, content);
27988
29033
  await channel.sendText(chatId, "Got it, I'll remember that.", { parseMode: "plain" });
27989
29034
  if (typeof channel.sendKeyboard === "function") {
27990
29035
  await channel.sendKeyboard(chatId, "", [
@@ -28069,11 +29114,15 @@ async function handleStopallCommand(chatId, commandArgs, msg, channel) {
28069
29114
  }
28070
29115
  async function handleEditjobCommand(chatId, commandArgs, msg, channel) {
28071
29116
  if (!commandArgs) {
28072
- await channel.sendText(chatId, "Usage: /editjob <job-id>", { parseMode: "plain" });
29117
+ await sendJobsBoard(chatId, channel, 1);
28073
29118
  return;
28074
29119
  }
28075
29120
  const editId = parseInt(commandArgs, 10);
28076
- await startEditWizard(chatId, editId, channel);
29121
+ if (isNaN(editId)) {
29122
+ await sendJobsBoard(chatId, channel, 1);
29123
+ return;
29124
+ }
29125
+ await sendJobDetail(chatId, editId, channel);
28077
29126
  }
28078
29127
  async function handleJobsCommand(chatId, commandArgs, msg, channel) {
28079
29128
  await sendJobsBoard(chatId, channel, 1);
@@ -28325,7 +29374,7 @@ async function handleClearCommand(chatId, _commandArgs, _msg, channel) {
28325
29374
  clearChatPaidSlots(chatId);
28326
29375
  setSessionStartedAt(chatId);
28327
29376
  logActivity(getDb(), { chatId, source: "telegram", eventType: "config_changed", summary: "Session cleared (no summary)", detail: { field: "session", action: "clear" } });
28328
- await channel.sendText(chatId, "\u{1F9FD} Session cleared. No summary saved.", { parseMode: "plain", priority: true });
29377
+ await channel.sendText(chatId, "\u{1F9FD} Session cleared. No summary saved.", { parseMode: "plain", priority: 0 /* P0_CRITICAL */ });
28329
29378
  }
28330
29379
  async function handleSummarizeCommand(chatId, commandArgs, msg, channel) {
28331
29380
  if (commandArgs?.toLowerCase() === "all") {
@@ -28508,9 +29557,9 @@ async function handleStatusCommand(chatId, commandArgs, msg, channel) {
28508
29557
  { label: "Change Mode", data: "menu:permissions" },
28509
29558
  { label: "Change Style", data: "menu:style" }
28510
29559
  ]
28511
- ], { priority: true });
29560
+ ], { priority: 0 /* P0_CRITICAL */ });
28512
29561
  } else {
28513
- await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain", priority: true });
29562
+ await channel.sendText(chatId, lines.join("\n"), { parseMode: "plain", priority: 0 /* P0_CRITICAL */ });
28514
29563
  }
28515
29564
  }
28516
29565
  async function handleBackendCommand2(chatId, commandArgs, msg, channel) {
@@ -28629,7 +29678,7 @@ async function handleMemoryCommand(chatId, commandArgs, msg, channel) {
28629
29678
  return;
28630
29679
  }
28631
29680
  deleteMemoryById(editId);
28632
- saveMemoryWithEmbedding(mem.trigger, newContent, mem.type);
29681
+ await remember(mem.trigger, newContent, { category: mem.category });
28633
29682
  await channel.sendText(chatId, `Memory #${editId} updated.`, { parseMode: "plain" });
28634
29683
  return;
28635
29684
  }
@@ -28814,9 +29863,9 @@ Recent directories:` : "Recent directories:";
28814
29863
  const buttons = recents.map((r) => [{ label: r.alias, data: `cwdpick:${r.alias}` }]);
28815
29864
  await channel.sendKeyboard(chatId, text, buttons);
28816
29865
  } else {
28817
- const list = recents.map((r) => ` ${r.alias} \u2192 ${r.path}`).join("\n");
29866
+ const list2 = recents.map((r) => ` ${r.alias} \u2192 ${r.path}`).join("\n");
28818
29867
  await channel.sendText(chatId, `${text}
28819
- ${list}`, { parseMode: "plain" });
29868
+ ${list2}`, { parseMode: "plain" });
28820
29869
  }
28821
29870
  return;
28822
29871
  }
@@ -28971,12 +30020,12 @@ async function handleGeminiAccountsCommand(chatId, commandArgs, msg, channel) {
28971
30020
  await channel.sendKeyboard(chatId, "Gemini Accounts & Rotation:", rows);
28972
30021
  } else {
28973
30022
  const currentMode = getGeminiRotationMode();
28974
- const list = slots.filter((s) => s.enabled).map((s) => {
30023
+ const list2 = slots.filter((s) => s.enabled).map((s) => {
28975
30024
  const icon = s.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
28976
30025
  return `${icon} ${s.label || `slot-${s.id}`} (#${s.id})`;
28977
30026
  }).join("\n");
28978
30027
  await channel.sendText(chatId, `Slots:
28979
- ${list}
30028
+ ${list2}
28980
30029
 
28981
30030
  Rotation mode: ${currentMode}
28982
30031
  Use: /gemini_accounts <name> to pin`, { parseMode: "plain" });
@@ -29003,12 +30052,12 @@ Add with: <code>cc-claw ${slotBackend} add-key</code>`, { parseMode: "html" });
29003
30052
  await channel.sendKeyboard(chatId, `${slotDisplayName} Accounts & Rotation:`, rows);
29004
30053
  } else {
29005
30054
  const currentMode = getBackendRotationMode(slotBackend);
29006
- const list = slots.filter((s) => s.enabled).map((s) => {
30055
+ const list2 = slots.filter((s) => s.enabled).map((s) => {
29007
30056
  const icon = s.slotType === "oauth" ? "\u{1F468}\u{1F3FD}\u200D\u{1F4BB}" : "\u{1F511}";
29008
30057
  return `${icon} ${s.label || `slot-${s.id}`} (#${s.id})`;
29009
30058
  }).join("\n");
29010
30059
  await channel.sendText(chatId, `${slotDisplayName} Slots:
29011
- ${list}
30060
+ ${list2}
29012
30061
 
29013
30062
  Rotation mode: ${currentMode}
29014
30063
  Use: /${command} <name> to pin`, { parseMode: "plain" });
@@ -29339,10 +30388,10 @@ async function handleTasksCommand(chatId, commandArgs, msg, channel) {
29339
30388
  { label: "Abandoned", emoji: "\u{1F6AB}", status: "abandoned" }
29340
30389
  ];
29341
30390
  for (const { label: label2, emoji, status } of sections) {
29342
- const list = byStatus[status];
29343
- if (list.length === 0) continue;
30391
+ const list2 = byStatus[status];
30392
+ if (list2.length === 0) continue;
29344
30393
  lines.push(`${emoji} ${label2}:`);
29345
- for (const t of list) {
30394
+ for (const t of list2) {
29346
30395
  const assignee = t.assignee ? ` (\u2192 ${t.assignee.slice(0, 8)})` : "";
29347
30396
  lines.push(` #${t.id}: ${t.subject}${assignee}`);
29348
30397
  }
@@ -29518,6 +30567,7 @@ async function handleCouncilCommand(chatId, commandArgs, msg, channel) {
29518
30567
  var init_command_handlers = __esm({
29519
30568
  "src/router/command-handlers.ts"() {
29520
30569
  "use strict";
30570
+ init_telegram_throttle();
29521
30571
  init_format();
29522
30572
  init_log();
29523
30573
  init_format_time();
@@ -29525,11 +30575,12 @@ var init_command_handlers = __esm({
29525
30575
  init_image_gen();
29526
30576
  init_stt();
29527
30577
  init_agent();
29528
- init_classify();
30578
+ init_classify2();
29529
30579
  init_install();
29530
30580
  init_profile();
29531
30581
  init_heartbeat2();
29532
30582
  init_discover();
30583
+ init_engine();
29533
30584
  init_store5();
29534
30585
  init_summarize();
29535
30586
  init_session_log();
@@ -30662,6 +31713,9 @@ ${progressMsg}`,
30662
31713
  }
30663
31714
  setSttProvider(chatId, provider);
30664
31715
  await sendVoiceConfigKeyboard(chatId, channel, messageId);
31716
+ } else if (data === "vcfg:echo") {
31717
+ toggleSttEcho(chatId);
31718
+ await sendVoiceConfigKeyboard(chatId, channel, messageId);
30665
31719
  } else if (data.startsWith("vcfg:")) {
30666
31720
  const parts = data.slice(5).split(":");
30667
31721
  const action = parts[0];
@@ -30692,14 +31746,15 @@ ${progressMsg}`,
30692
31746
  await handleWizardCallback(chatId, data, channel);
30693
31747
  } else if (data.startsWith("job:")) {
30694
31748
  async function showJobAccountPicker(cid, jobId, backend2, model2, thinking2, ch) {
30695
- const isGemini = backend2 === "gemini";
30696
- const slots = isGemini ? getGeminiSlots() : getBackendSlots(backend2);
31749
+ const { getAdapter: getAdapter3 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
31750
+ const adapter = getAdapter3(backend2);
31751
+ const isApiBackend = adapter.type === "api";
31752
+ const isGemini = backend2 === BACKEND.GEMINI;
31753
+ const slots = isApiBackend ? [] : isGemini ? getGeminiSlots() : getBackendSlots(backend2);
30697
31754
  const enabledSlots = slots.filter((s) => s.enabled);
30698
31755
  if (enabledSlots.length === 0 || typeof ch.sendKeyboard !== "function") {
30699
31756
  const { updateJob: updateJobFields } = await Promise.resolve().then(() => (init_store5(), store_exports5));
30700
31757
  updateJobFields(jobId, { backend: backend2, model: model2, thinking: thinking2, credentialSlotId: null });
30701
- const { getAdapter: getAdapter3 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
30702
- const adapter = getAdapter3(backend2);
30703
31758
  await ch.sendText(cid, `Job #${jobId} updated: ${adapter.displayName} / ${model2} / ${thinking2}`, { parseMode: "plain" });
30704
31759
  await sendJobDetail(cid, jobId, ch);
30705
31760
  return;
@@ -31573,6 +32628,22 @@ Salience: ${memory2.salience.toFixed(2)} | Created: ${memory2.created_at.slice(0
31573
32628
  } else if (rest.startsWith("opt")) {
31574
32629
  const { handleMemOptCallback: handleMemOptCallback2 } = await Promise.resolve().then(() => (init_optimize(), optimize_exports));
31575
32630
  await handleMemOptCallback2(chatId, rest, channel, messageId);
32631
+ } else if (rest === "sweep:dismiss") {
32632
+ if (messageId && channel.editText) {
32633
+ await channel.editText(chatId, messageId, "\u2705 Memory sweep dismissed.", "plain").catch(() => {
32634
+ });
32635
+ }
32636
+ } else if (rest === "sweep:toggle") {
32637
+ const { getMetaValue: getMetaValue2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
32638
+ const { SWEEP_ENABLED_KEY: SWEEP_ENABLED_KEY2, enableSweep: enableSweep2, disableSweep: disableSweep2 } = await Promise.resolve().then(() => (init_sweep(), sweep_exports));
32639
+ const currentlyEnabled = getMetaValue2(SWEEP_ENABLED_KEY2) === "1";
32640
+ if (currentlyEnabled) {
32641
+ disableSweep2();
32642
+ } else {
32643
+ enableSweep2(chatId);
32644
+ }
32645
+ const { sendMemoryPage: sendMemoryPage3 } = await Promise.resolve().then(() => (init_ui(), ui_exports));
32646
+ await sendMemoryPage3(chatId, channel, 1, messageId);
31576
32647
  } else if (rest === "noop") {
31577
32648
  }
31578
32649
  return;
@@ -32489,7 +33560,9 @@ function withThread(channel, threadId) {
32489
33560
  // These operate on existing messages — no threadId needed
32490
33561
  editText: channel.editText?.bind(channel),
32491
33562
  editKeyboard: channel.editKeyboard?.bind(channel),
32492
- reactToMessage: channel.reactToMessage?.bind(channel)
33563
+ reactToMessage: channel.reactToMessage?.bind(channel),
33564
+ isDraftCapable: channel.isDraftCapable?.bind(channel),
33565
+ sendMessageDraft: channel.sendMessageDraft?.bind(channel)
32493
33566
  };
32494
33567
  }
32495
33568
  var init_thread_wrapper = __esm({
@@ -33169,7 +34242,8 @@ Try a different keyword.`,
33169
34242
  if (rememberMatch) {
33170
34243
  const content = rememberMatch[1];
33171
34244
  const trigger = content.split(/\s+/).slice(0, 3).join(" ");
33172
- saveMemoryWithEmbedding(trigger, content, "semantic");
34245
+ remember(trigger, content).catch(() => {
34246
+ });
33173
34247
  await channel.sendText(chatId, "Got it, I'll remember that.", { parseMode: "plain" });
33174
34248
  return;
33175
34249
  }
@@ -33410,18 +34484,7 @@ You're still in discussion mode \u2014 try again or click a button to exit.`, {
33410
34484
  isSideQuest: hasSqPrefix
33411
34485
  })) {
33412
34486
  const planDirective = buildPlanningDirective();
33413
- let typingActive2 = true;
33414
- const typingLoop2 = async () => {
33415
- while (typingActive2) {
33416
- try {
33417
- await channel.sendTyping?.(chatId);
33418
- } catch {
33419
- }
33420
- await new Promise((r) => setTimeout(r, 4e3));
33421
- }
33422
- };
33423
- typingLoop2().catch(() => {
33424
- });
34487
+ getTypingManager().acquire(chatId, channel);
33425
34488
  try {
33426
34489
  const planResponse = await askAgent(chatId, cleanText || text, {
33427
34490
  cwd: settings.getCwd(),
@@ -33431,7 +34494,7 @@ You're still in discussion mode \u2014 try again or click a button to exit.`, {
33431
34494
  agentMode: effectiveAgentMode,
33432
34495
  planningDirective: planDirective
33433
34496
  });
33434
- typingActive2 = false;
34497
+ getTypingManager().release(chatId);
33435
34498
  if (planResponse.text) {
33436
34499
  let planText = planResponse.text.replace(/\[REACT:.+?\]/g, "").replace(/\[SEND_FILE:.+?\]/g, "").replace(/\[GENERATE_IMAGE:.+?\]/g, "").replace(/\[HISTORY_SEARCH:[^\]]+\]/g, "").trim();
33437
34500
  const PLAN_DISPLAY_LIMIT = 3500;
@@ -33456,23 +34519,12 @@ You're still in discussion mode \u2014 try again or click a button to exit.`, {
33456
34519
  await channel.sendText(chatId, "(No plan generated \u2014 proceeding normally)", { parseMode: "plain" });
33457
34520
  }
33458
34521
  } catch (err) {
33459
- typingActive2 = false;
34522
+ getTypingManager().release(chatId);
33460
34523
  await channel.sendText(chatId, `\u26A0\uFE0F Planning error: ${err.message}`, { parseMode: "plain" });
33461
34524
  }
33462
34525
  return;
33463
34526
  }
33464
- let typingActive = true;
33465
- const typingLoop = async () => {
33466
- while (typingActive) {
33467
- try {
33468
- await channel.sendTyping?.(chatId);
33469
- } catch {
33470
- }
33471
- await new Promise((r) => setTimeout(r, 4e3));
33472
- }
33473
- };
33474
- typingLoop().catch(() => {
33475
- });
34527
+ getTypingManager().acquire(chatId, channel);
33476
34528
  try {
33477
34529
  const tMode = settings.getMode();
33478
34530
  const tVerbose = settings.getVerboseLevel();
@@ -33730,7 +34782,7 @@ Approve paid usage for this session?`,
33730
34782
  const userMsg = diagnoseAgentError(errMsg, chatId);
33731
34783
  await channel.sendText(chatId, userMsg, { parseMode: "plain" });
33732
34784
  } finally {
33733
- typingActive = false;
34785
+ getTypingManager().release(chatId);
33734
34786
  const pending = pendingInterrupts.get(chatId);
33735
34787
  if (pending) {
33736
34788
  pendingInterrupts.delete(chatId);
@@ -33748,12 +34800,13 @@ var init_router2 = __esm({
33748
34800
  init_agent();
33749
34801
  init_retry();
33750
34802
  init_quota();
33751
- init_classify();
34803
+ init_classify2();
34804
+ init_engine();
33752
34805
  init_store5();
33753
34806
  init_backends();
33754
34807
  init_wizard();
33755
34808
  init_ollama3();
33756
- init_classify2();
34809
+ init_classify3();
33757
34810
  init_session_log2();
33758
34811
  init_live_status();
33759
34812
  init_detect();
@@ -33764,6 +34817,7 @@ var init_router2 = __esm({
33764
34817
  init_gate();
33765
34818
  init_helpers();
33766
34819
  init_response();
34820
+ init_typing_manager();
33767
34821
  init_shell();
33768
34822
  init_ui();
33769
34823
  init_api_models();
@@ -34021,6 +35075,28 @@ async function executeJob(job) {
34021
35075
  async function runWithRetry(job, model2, runId, t0) {
34022
35076
  let lastError;
34023
35077
  const chatId = job.sessionType === "isolated" ? `cron:${job.id}:${runId}` : job.chatId;
35078
+ if (job.jobType === "memory_sweep") {
35079
+ const { runWeeklySweep: runWeeklySweep2 } = await Promise.resolve().then(() => (init_sweep(), sweep_exports));
35080
+ const { getChannelRegistry: getChannelRegistry2 } = await Promise.resolve().then(() => (init_delivery(), delivery_exports));
35081
+ const channelName = job.channel ?? "telegram";
35082
+ const channel = getChannelRegistry2()?.get(channelName);
35083
+ const deliveryTarget = job.target ?? job.chatId;
35084
+ if (!channel) {
35085
+ warn(`[sweep] Job #${job.id}: channel "${channelName}" not found \u2014 sweep ran silently`);
35086
+ }
35087
+ const result = await runWeeklySweep2(
35088
+ deliveryTarget,
35089
+ channel,
35090
+ resolveJobBackendId(job) ?? void 0,
35091
+ job.model ?? void 0
35092
+ );
35093
+ const parts = [];
35094
+ if (result.cleanedUp > 0) parts.push(`cleaned up ${result.cleanedUp} expired memories`);
35095
+ if (result.suggestionsCount > 0) parts.push(`found ${result.suggestionsCount} suggestions`);
35096
+ if (result.error) parts.push(`error: ${result.error}`);
35097
+ const summary = parts.length > 0 ? parts.join(", ") : "memory bank healthy, no action needed";
35098
+ return { text: summary };
35099
+ }
34024
35100
  if (job.jobType === "reflection") {
34025
35101
  const { runNightlyReflection: runNightlyReflection2 } = await Promise.resolve().then(() => (init_analyze(), analyze_exports));
34026
35102
  const { formatNightlySummary: formatNightlySummary2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
@@ -35036,6 +36112,11 @@ function isFastPathMessage(msg) {
35036
36112
  function sanitizeForTelegram(text) {
35037
36113
  return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFFFD\uFFFE\uFFFF]/g, "");
35038
36114
  }
36115
+ function isDraftCapable(chatId) {
36116
+ if (isSyntheticChatId(chatId)) return false;
36117
+ const numId = numericChatId(chatId);
36118
+ return !isNaN(numId) && numId > 0;
36119
+ }
35039
36120
  function numericChatId(chatId) {
35040
36121
  if (chatId.startsWith("sq:") || chatId.startsWith("cron:")) {
35041
36122
  throw new Error(`Synthetic chatId "${chatId}" passed to Telegram API`);
@@ -35051,6 +36132,7 @@ var init_telegram2 = __esm({
35051
36132
  init_log();
35052
36133
  init_health3();
35053
36134
  init_store5();
36135
+ init_agent();
35054
36136
  init_telegram_throttle();
35055
36137
  FAST_PATH_COMMANDS = /* @__PURE__ */ new Set(["stop", "status", "new", "newchat", "clear"]);
35056
36138
  TelegramChannel = class _TelegramChannel {
@@ -35069,18 +36151,23 @@ var init_telegram2 = __esm({
35069
36151
  mediaGroupBuffer = /* @__PURE__ */ new Map();
35070
36152
  static MEDIA_GROUP_DEBOUNCE_MS = 500;
35071
36153
  // ── Polling health tracking ─────────────────────────────────────────
35072
- /** Timestamp of last update received from Telegram (message, callback, reaction) */
35073
- lastUpdateAt = 0;
36154
+ /** Timestamp of last successful API keepalive ping (updated every 2 minutes) */
36155
+ lastPollingCheckAt = 0;
35074
36156
  /** True while polling is expected to be active (between start() and stop()) */
35075
36157
  pollingExpected = false;
35076
36158
  /** Watchdog interval that detects silent polling death */
35077
36159
  pollingWatchdog = null;
35078
- /** Max time without any update before we consider polling dead (ms) */
35079
- static POLLING_SILENCE_THRESHOLD_MS = 5 * 60 * 1e3;
35080
- // 5 minutes
36160
+ /** Keepalive interval: pings bot.api.getMe() to confirm API connection is alive */
36161
+ keepaliveInterval = null;
36162
+ /** Max time without a successful keepalive ping before we consider polling dead (ms) */
36163
+ static POLLING_SILENCE_THRESHOLD_MS = 10 * 60 * 1e3;
36164
+ // 10 minutes
35081
36165
  /** How often the watchdog checks for polling health (ms) */
35082
36166
  static POLLING_WATCHDOG_INTERVAL_MS = 60 * 1e3;
35083
36167
  // 60 seconds
36168
+ /** How often to ping bot.api.getMe() as a keepalive (ms) */
36169
+ static POLLING_KEEPALIVE_INTERVAL_MS = 2 * 60 * 1e3;
36170
+ // 2 minutes
35084
36171
  constructor() {
35085
36172
  const token = process.env.TELEGRAM_BOT_TOKEN;
35086
36173
  if (!token) {
@@ -35198,7 +36285,6 @@ var init_telegram2 = __esm({
35198
36285
  { command: "council", description: "Multi-model debate (select models, anonymous rounds)" }
35199
36286
  ]);
35200
36287
  this.bot.on("message", async (ctx) => {
35201
- this.lastUpdateAt = Date.now();
35202
36288
  const chatId = ctx.chat.id.toString();
35203
36289
  const senderId = ctx.from?.id?.toString() ?? "";
35204
36290
  const authorized = this.isAuthorized(chatId) || this.isAuthorized(senderId);
@@ -35239,7 +36325,6 @@ var init_telegram2 = __esm({
35239
36325
  });
35240
36326
  });
35241
36327
  this.bot.on("callback_query:data", (ctx) => {
35242
- this.lastUpdateAt = Date.now();
35243
36328
  const userId = ctx.from.id.toString();
35244
36329
  const chatId = ctx.callbackQuery.message?.chat?.id?.toString() ?? userId;
35245
36330
  log(`[telegram] Callback from user ${userId} in chat ${chatId}: ${ctx.callbackQuery.data}`);
@@ -35267,7 +36352,6 @@ var init_telegram2 = __esm({
35267
36352
  });
35268
36353
  });
35269
36354
  this.bot.on("message_reaction", async (ctx) => {
35270
- this.lastUpdateAt = Date.now();
35271
36355
  const chatId = String(ctx.chat.id);
35272
36356
  const messageId = ctx.messageReaction.message_id;
35273
36357
  if (!this.agentMessageIds.has(messageId)) return;
@@ -35284,7 +36368,6 @@ var init_telegram2 = __esm({
35284
36368
  }
35285
36369
  });
35286
36370
  this.bot.on("inline_query", (ctx) => {
35287
- this.lastUpdateAt = Date.now();
35288
36371
  if (!this.isAuthorized(ctx.from.id.toString())) return;
35289
36372
  this.handleInlineQuery(ctx).catch((err) => {
35290
36373
  error("[telegram] Inline query error:", err);
@@ -35299,7 +36382,16 @@ var init_telegram2 = __esm({
35299
36382
  }
35300
36383
  });
35301
36384
  this.pollingExpected = true;
35302
- this.lastUpdateAt = Date.now();
36385
+ this.lastPollingCheckAt = Date.now();
36386
+ this.keepaliveInterval = setInterval(async () => {
36387
+ if (!this.pollingExpected) return;
36388
+ try {
36389
+ await this.bot.api.getMe();
36390
+ this.lastPollingCheckAt = Date.now();
36391
+ } catch (err) {
36392
+ error("[telegram] Keepalive ping failed:", err);
36393
+ }
36394
+ }, _TelegramChannel.POLLING_KEEPALIVE_INTERVAL_MS);
35303
36395
  const pollingPromise = this.bot.start({
35304
36396
  allowed_updates: [...API_CONSTANTS.ALL_UPDATE_TYPES],
35305
36397
  onStart: () => log("[telegram] Polling for messages...")
@@ -35322,13 +36414,13 @@ var init_telegram2 = __esm({
35322
36414
  );
35323
36415
  this.pollingWatchdog = setInterval(() => {
35324
36416
  if (!this.pollingExpected) return;
35325
- const silenceMs = Date.now() - this.lastUpdateAt;
36417
+ const silenceMs = Date.now() - this.lastPollingCheckAt;
35326
36418
  if (silenceMs > _TelegramChannel.POLLING_SILENCE_THRESHOLD_MS) {
35327
36419
  log(
35328
- `[telegram] No updates received for ${Math.round(silenceMs / 1e3)}s \u2014 triggering reconnect`
36420
+ `[telegram] No polling confirmation for ${Math.round(silenceMs / 1e3)}s \u2014 triggering reconnect`
35329
36421
  );
35330
- markChannelDown("telegram", `No updates for ${Math.round(silenceMs / 1e3)}s`);
35331
- this.lastUpdateAt = Date.now();
36422
+ markChannelDown("telegram", `No polling confirmation for ${Math.round(silenceMs / 1e3)}s`);
36423
+ this.lastPollingCheckAt = Date.now();
35332
36424
  }
35333
36425
  }, _TelegramChannel.POLLING_WATCHDOG_INTERVAL_MS);
35334
36426
  }
@@ -35338,6 +36430,10 @@ var init_telegram2 = __esm({
35338
36430
  clearInterval(this.pollingWatchdog);
35339
36431
  this.pollingWatchdog = null;
35340
36432
  }
36433
+ if (this.keepaliveInterval) {
36434
+ clearInterval(this.keepaliveInterval);
36435
+ this.keepaliveInterval = null;
36436
+ }
35341
36437
  try {
35342
36438
  await this.bot.stop();
35343
36439
  } catch {
@@ -35451,15 +36547,18 @@ var init_telegram2 = __esm({
35451
36547
  return void 0;
35452
36548
  }
35453
36549
  }
35454
- async editText(chatId, messageId, text, parseMode) {
36550
+ async editText(chatId, messageId, text, parseMode, priority) {
35455
36551
  const formatted = sanitizeForTelegram(parseMode === "html" ? text : formatForTelegram(text));
36552
+ const isCritical = priority === true || priority === 0 /* P0_CRITICAL */;
36553
+ const label2 = isCritical ? "finalizeStatus" : "editText:html";
35456
36554
  try {
35457
36555
  await this.throttle.send(
35458
36556
  chatId,
35459
- "editText:html",
36557
+ label2,
35460
36558
  () => this.bot.api.editMessageText(numericChatId(chatId), parseInt(messageId), formatted, {
35461
36559
  parse_mode: "HTML"
35462
- })
36560
+ }),
36561
+ priority
35463
36562
  );
35464
36563
  return true;
35465
36564
  } catch (err) {
@@ -35472,12 +36571,13 @@ var init_telegram2 = __esm({
35472
36571
  try {
35473
36572
  await this.throttle.send(
35474
36573
  chatId,
35475
- "editText:fallback",
36574
+ priority ? "finalizeStatus:fallback" : "editText:fallback",
35476
36575
  () => this.bot.api.editMessageText(
35477
36576
  numericChatId(chatId),
35478
36577
  parseInt(messageId),
35479
36578
  formatted.replace(/<[^>]+>/g, "")
35480
- )
36579
+ ),
36580
+ priority
35481
36581
  );
35482
36582
  return true;
35483
36583
  } catch (err2) {
@@ -35503,7 +36603,7 @@ var init_telegram2 = __esm({
35503
36603
  }
35504
36604
  return keyboard;
35505
36605
  }
35506
- async editKeyboard(chatId, messageId, text, buttons) {
36606
+ async editKeyboard(chatId, messageId, text, buttons, opts) {
35507
36607
  const keyboard = this.buildInlineKeyboard(buttons);
35508
36608
  const formatted = sanitizeForTelegram(formatForTelegram(text));
35509
36609
  try {
@@ -35513,7 +36613,8 @@ var init_telegram2 = __esm({
35513
36613
  () => this.bot.api.editMessageText(numericChatId(chatId), parseInt(messageId), formatted, {
35514
36614
  parse_mode: "HTML",
35515
36615
  reply_markup: keyboard
35516
- })
36616
+ }),
36617
+ opts?.priority
35517
36618
  );
35518
36619
  return true;
35519
36620
  } catch (err) {
@@ -35527,7 +36628,8 @@ var init_telegram2 = __esm({
35527
36628
  parseInt(messageId),
35528
36629
  formatted.replace(/<[^>]+>/g, ""),
35529
36630
  { reply_markup: keyboard }
35530
- )
36631
+ ),
36632
+ opts?.priority
35531
36633
  );
35532
36634
  return true;
35533
36635
  } catch (err2) {
@@ -35621,6 +36723,44 @@ var init_telegram2 = __esm({
35621
36723
  log(`[telegram] reactToMessage failed (chat=${chatId} msg=${messageId}): ${err}`);
35622
36724
  }
35623
36725
  }
36726
+ /**
36727
+ * Check whether a chat supports native draft streaming.
36728
+ * sendMessageDraft only works in private/DM chats (positive chat IDs).
36729
+ */
36730
+ isDraftCapable(chatId) {
36731
+ return isDraftCapable(chatId);
36732
+ }
36733
+ /**
36734
+ * Send a streaming draft update to a DM chat using Telegram's native
36735
+ * sendMessageDraft API (Bot API 9.3+). The draft shows an animated
36736
+ * typing bubble with the text content, replacing editMessageText for
36737
+ * DM streaming. Subsequent calls with the same draft_id produce smooth
36738
+ * animated transitions.
36739
+ *
36740
+ * Draft updates are cosmetic — if one gets dropped, the next update
36741
+ * will contain the full accumulated text. Uses tryBestEffort so drafts
36742
+ * never block critical sends.
36743
+ *
36744
+ * @param chatId - Private chat ID (must be positive / draft-capable)
36745
+ * @param draftId - Non-zero integer, consistent per response for smooth animation
36746
+ * @param text - Plain text content (no parse_mode during streaming)
36747
+ */
36748
+ async sendMessageDraft(chatId, draftId, text) {
36749
+ if (!this.isDraftCapable(chatId)) {
36750
+ log(`[telegram] sendMessageDraft skipped \u2014 chat ${chatId} is not draft-capable`);
36751
+ return;
36752
+ }
36753
+ try {
36754
+ await this.throttle.tryBestEffort(
36755
+ chatId,
36756
+ "draft",
36757
+ () => this.bot.api.sendMessageDraft(numericChatId(chatId), draftId, sanitizeForTelegram(text))
36758
+ );
36759
+ log(`[telegram] sendMessageDraft sent (chat=${chatId}, draftId=${draftId}, len=${text.length})`);
36760
+ } catch (err) {
36761
+ log(`[telegram] sendMessageDraft failed (chat=${chatId}): ${err}`);
36762
+ }
36763
+ }
35624
36764
  /** Get the underlying Grammy Bot instance (for scheduler, etc.) */
35625
36765
  getBot() {
35626
36766
  return this.bot;
@@ -36606,7 +37746,7 @@ async function main() {
36606
37746
  pruneMessageLog(30, 2e3);
36607
37747
  bootstrapBuiltinMcps(getDb());
36608
37748
  try {
36609
- const { resetIntentStats: resetIntentStats2 } = await Promise.resolve().then(() => (init_classify(), classify_exports));
37749
+ const { resetIntentStats: resetIntentStats2 } = await Promise.resolve().then(() => (init_classify2(), classify_exports));
36610
37750
  resetIntentStats2();
36611
37751
  } catch {
36612
37752
  }
@@ -36782,6 +37922,22 @@ ${lines.join("\n")}`;
36782
37922
  migrateEmbeddings().catch((err) => error("[cc-claw] Embedding migration failed:", err));
36783
37923
  initHeartbeat(channelRegistry);
36784
37924
  startAllHeartbeats();
37925
+ try {
37926
+ const { getMetaValue: getMetaValue2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
37927
+ const { SWEEP_ENABLED_KEY: SWEEP_ENABLED_KEY2, findSweepJob: findSweepJob2, enableSweep: enableSweep2 } = await Promise.resolve().then(() => (init_sweep(), sweep_exports));
37928
+ const sweepEnabled = getMetaValue2(SWEEP_ENABLED_KEY2);
37929
+ if (sweepEnabled === "1") {
37930
+ if (!findSweepJob2()) {
37931
+ const primaryChatId = (process.env.ALLOWED_CHAT_ID ?? "").split(",")[0]?.trim();
37932
+ if (primaryChatId) {
37933
+ enableSweep2(primaryChatId);
37934
+ log("[sweep] Restored weekly memory sweep job from meta setting");
37935
+ }
37936
+ }
37937
+ }
37938
+ } catch (err) {
37939
+ warn(`[sweep] Failed to restore sweep job: ${err instanceof Error ? err.message : String(err)}`);
37940
+ }
36785
37941
  startHealthMonitor3(channelRegistry.list(), handleMessage);
36786
37942
  Promise.resolve().then(() => (init_health(), health_exports)).then(({ startHealthMonitor: startMcpHealthMonitor }) => {
36787
37943
  startMcpHealthMonitor(getDb());
@@ -36824,9 +37980,19 @@ ${lines.join("\n")}`;
36824
37980
  } catch {
36825
37981
  }
36826
37982
  ;
37983
+ try {
37984
+ const { getEditCoordinator: getEditCoordinator2 } = await Promise.resolve().then(() => (init_edit_coordinator(), edit_coordinator_exports));
37985
+ getEditCoordinator2().shutdown();
37986
+ } catch {
37987
+ }
37988
+ try {
37989
+ const { getTypingManager: getTypingManager2 } = await Promise.resolve().then(() => (init_typing_manager(), typing_manager_exports));
37990
+ getTypingManager2().shutdown();
37991
+ } catch {
37992
+ }
36827
37993
  shutdownOrchestrator();
36828
37994
  shutdownScheduler();
36829
- flushMemorySalienceUpdates();
37995
+ flushMemoryHalfLifeUpdates();
36830
37996
  flushSummarySalienceUpdates();
36831
37997
  await Promise.race([
36832
37998
  summarizeAllPending(),
@@ -37383,6 +38549,7 @@ async function statusCommand(globalOpts, localOpts) {
37383
38549
  const dbStat = existsSync35(DB_PATH) ? statSync10(DB_PATH) : null;
37384
38550
  let daemonRunning = false;
37385
38551
  let daemonInfo = {};
38552
+ let throttleData;
37386
38553
  try {
37387
38554
  const { apiGet: apiGet2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
37388
38555
  const healthRes = await apiGet2("/api/health");
@@ -37390,6 +38557,9 @@ async function statusCommand(globalOpts, localOpts) {
37390
38557
  if (healthRes.ok && healthRes.data?.uptime) {
37391
38558
  daemonInfo.uptime_seconds = Math.floor(healthRes.data.uptime);
37392
38559
  }
38560
+ if (healthRes.ok && healthRes.data?.throttle) {
38561
+ throttleData = healthRes.data.throttle;
38562
+ }
37393
38563
  } catch {
37394
38564
  }
37395
38565
  const contextUsed = (usageRow?.last_input_tokens ?? 0) + (usageRow?.last_cache_read_tokens ?? 0);
@@ -37411,7 +38581,8 @@ async function statusCommand(globalOpts, localOpts) {
37411
38581
  output_tokens: usageRow?.output_tokens ?? 0,
37412
38582
  cache_read_tokens: usageRow?.cache_read_tokens ?? 0
37413
38583
  },
37414
- db: { path: DB_PATH, sizeBytes: dbStat?.size ?? 0, exists: !!dbStat }
38584
+ db: { path: DB_PATH, sizeBytes: dbStat?.size ?? 0, exists: !!dbStat },
38585
+ throttle: throttleData
37415
38586
  };
37416
38587
  try {
37417
38588
  const { OllamaStore } = await Promise.resolve().then(() => (init_ollama(), ollama_exports));
@@ -37443,6 +38614,14 @@ async function statusCommand(globalOpts, localOpts) {
37443
38614
  if (localOpts.deep) {
37444
38615
  lines.push(kvLine("Daemon", s.daemon.running ? success(`running${s.daemon.uptime_seconds ? ` (uptime ${formatUptime2(s.daemon.uptime_seconds)})` : ""}`) : error2("offline")));
37445
38616
  }
38617
+ if (s.throttle) {
38618
+ const t = s.throttle;
38619
+ const queueStr = t.queueDepth > 0 ? warning(`${t.queueDepth} queued`) : success("0 queued");
38620
+ const pauseStr = t.isPaused ? error2(`PAUSED (${t.pauseRemainingSec}s remaining)`) : "";
38621
+ const circuitStr = t.circuitState !== "closed" ? warning(t.circuitState.toUpperCase()) : "";
38622
+ const parts = [queueStr, pauseStr, circuitStr].filter(Boolean).join(", ");
38623
+ lines.push(kvLine("Throttle", parts));
38624
+ }
37446
38625
  lines.push(
37447
38626
  "",
37448
38627
  divider("Usage (this session)"),
@@ -37779,6 +38958,7 @@ async function logsCommand(opts) {
37779
38958
  console.log(muted(` \u2500\u2500 ${logFile} (last ${tailLines.length} lines) \u2500\u2500`));
37780
38959
  console.log(tailLines.join("\n"));
37781
38960
  if (opts.follow) {
38961
+ globalThis._cliStayAlive = true;
37782
38962
  console.log(muted("\n Following... (Ctrl+C to stop)\n"));
37783
38963
  let lastLength = content.length;
37784
38964
  watchFile2(logFile, { interval: 500 }, () => {
@@ -37837,9 +39017,9 @@ async function sessionLogsList(opts) {
37837
39017
  `));
37838
39018
  console.log(` ${"Filename".padEnd(55)} ${"Size".padStart(8)} Chat ID`);
37839
39019
  console.log(` ${"\u2500".repeat(55)} ${"\u2500".repeat(8)} ${"\u2500".repeat(15)}`);
37840
- for (const log6 of logs) {
37841
- const size = log6.sizeBytes < 1024 ? `${log6.sizeBytes}B` : log6.sizeBytes < 1024 * 1024 ? `${(log6.sizeBytes / 1024).toFixed(1)}K` : `${(log6.sizeBytes / 1024 / 1024).toFixed(1)}M`;
37842
- console.log(` ${log6.filename.padEnd(55)} ${size.padStart(8)} ${log6.chatId}`);
39020
+ for (const log5 of logs) {
39021
+ 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`;
39022
+ console.log(` ${log5.filename.padEnd(55)} ${size.padStart(8)} ${log5.chatId}`);
37843
39023
  }
37844
39024
  console.log(muted(`
37845
39025
  Path: ${SESSION_LOGS_PATH}`));
@@ -37869,6 +39049,7 @@ async function sessionLogsTail(opts) {
37869
39049
  console.log(line);
37870
39050
  }
37871
39051
  if (opts.follow) {
39052
+ globalThis._cliStayAlive = true;
37872
39053
  console.log(muted("\n Following... (Ctrl+C to stop)\n"));
37873
39054
  let lastLength = 0;
37874
39055
  try {
@@ -37976,9 +39157,9 @@ async function geminiList(globalOpts) {
37976
39157
  email: s.slot_type === "oauth" ? resolveOAuthEmail(s.config_home) : null
37977
39158
  }));
37978
39159
  output(enriched, (data) => {
37979
- const list = data;
39160
+ const list2 = data;
37980
39161
  const lines = ["", divider("Gemini Credential Slots"), ""];
37981
- for (const s of list) {
39162
+ for (const s of list2) {
37982
39163
  const now = (/* @__PURE__ */ new Date()).toISOString();
37983
39164
  const inCooldown = s.cooldown_until && s.cooldown_until > now;
37984
39165
  const icon = !s.enabled ? error2("\u25CB disabled") : inCooldown ? warning("\u25D1 cooldown") : success("\u25CF active");
@@ -38301,7 +39482,7 @@ async function resolveSlotId2(backend2, idOrLabel) {
38301
39482
  return match?.id ?? null;
38302
39483
  }
38303
39484
  function makeList(backend2, displayName) {
38304
- return async function list(_globalOpts) {
39485
+ return async function list2(_globalOpts) {
38305
39486
  requireDb2();
38306
39487
  const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
38307
39488
  const readDb = openDatabaseReadOnly2();
@@ -38316,9 +39497,9 @@ Add one with: cc-claw ${backend2} add-account or cc-claw ${backend2} add-key`)
38316
39497
  return;
38317
39498
  }
38318
39499
  output(slots, (data) => {
38319
- const list2 = data;
39500
+ const list3 = data;
38320
39501
  const lines = ["", divider(`${displayName} Credential Slots`), ""];
38321
- for (const s of list2) {
39502
+ for (const s of list3) {
38322
39503
  const now = (/* @__PURE__ */ new Date()).toISOString();
38323
39504
  const inCooldown = s.cooldown_until && s.cooldown_until > now;
38324
39505
  const icon = !s.enabled ? error2("\u25CB disabled") : inCooldown ? warning("\u25D1 cooldown") : success("\u25CF active");
@@ -38782,9 +39963,9 @@ async function ollamaList(globalOpts) {
38782
39963
  modelCount: modelCounts.get(s.id) ?? 0
38783
39964
  }));
38784
39965
  output(data, (d) => {
38785
- const list = d;
39966
+ const list2 = d;
38786
39967
  const lines = ["", divider("Ollama Servers"), ""];
38787
- for (const s of list) {
39968
+ for (const s of list2) {
38788
39969
  const dot = statusDot(s.status === "online" ? "active" : "offline");
38789
39970
  lines.push(` ${dot} ${s.name} ${muted(`(${s.host}:${s.port})`)}`);
38790
39971
  lines.push(` Models: ${s.modelCount} \xB7 Status: ${s.status === "online" ? success("online") : error2("offline")}`);
@@ -38936,13 +40117,13 @@ async function ollamaDiscover(globalOpts, opts) {
38936
40117
  contextWindow: m.contextWindow,
38937
40118
  sizeBytes: m.sizeBytes
38938
40119
  })), (d) => {
38939
- const list = d;
38940
- if (list.length === 0) {
40120
+ const list2 = d;
40121
+ if (list2.length === 0) {
38941
40122
  return "\n No models found. Check server connectivity.\n";
38942
40123
  }
38943
40124
  const lines = [`
38944
- ${success(`\u2713 Discovered ${list.length} model(s):`)}`, ""];
38945
- for (const m of list) {
40125
+ ${success(`\u2713 Discovered ${list2.length} model(s):`)}`, ""];
40126
+ for (const m of list2) {
38946
40127
  const sizeGB = m.sizeBytes > 0 ? `${(m.sizeBytes / 1e9).toFixed(1)}GB` : "";
38947
40128
  const ctxK = m.contextWindow ? `${(m.contextWindow / 1e3).toFixed(0)}K ctx` : "";
38948
40129
  const meta = [m.parameterSize, sizeGB, ctxK].filter(Boolean).join(" \xB7 ");
@@ -39029,9 +40210,9 @@ async function backendList(globalOpts) {
39029
40210
  defaultModel: a.defaultModel
39030
40211
  }));
39031
40212
  output(data, (d) => {
39032
- const list = d;
40213
+ const list2 = d;
39033
40214
  const lines = ["", divider("Backends"), ""];
39034
- for (const b of list) {
40215
+ for (const b of list2) {
39035
40216
  const marker = b.active ? success("\u25CF ") : " ";
39036
40217
  lines.push(` ${marker}${b.displayName} (${b.id})${b.active ? success(" \u2190 active") : ""}`);
39037
40218
  lines.push(` Default model: ${muted(b.defaultModel)}`);
@@ -39349,12 +40530,12 @@ async function cronList(globalOpts) {
39349
40530
  const jobs = readDb.prepare("SELECT * FROM jobs ORDER BY id").all();
39350
40531
  readDb.close();
39351
40532
  output(jobs, (d) => {
39352
- const list = d;
39353
- if (list.length === 0) return `
40533
+ const list2 = d;
40534
+ if (list2.length === 0) return `
39354
40535
  ${muted("No scheduled jobs.")}
39355
40536
  `;
39356
- const lines = ["", divider(`Scheduled Jobs (${list.length})`), ""];
39357
- for (const j of list) {
40537
+ const lines = ["", divider(`Scheduled Jobs (${list2.length})`), ""];
40538
+ for (const j of list2) {
39358
40539
  const status = !j.active ? "cancelled" : !j.enabled ? "paused" : "active";
39359
40540
  const schedule2 = j.cron ?? (j.at_time ? `at ${j.at_time}` : j.every_ms ? `every ${j.every_ms / 1e3}s` : "?");
39360
40541
  const tz = j.timezone !== "UTC" ? ` (${j.timezone})` : "";
@@ -39574,12 +40755,12 @@ async function cronRuns(globalOpts, jobId, opts) {
39574
40755
  const runs = readDb.prepare(query).all(...params);
39575
40756
  readDb.close();
39576
40757
  output(runs, (d) => {
39577
- const list = d;
39578
- if (list.length === 0) return `
40758
+ const list2 = d;
40759
+ if (list2.length === 0) return `
39579
40760
  ${muted(jobId ? `No runs for job #${jobId}.` : "No run history yet.")}
39580
40761
  `;
39581
40762
  const lines = ["", divider("Run History"), ""];
39582
- for (const r of list) {
40763
+ for (const r of list2) {
39583
40764
  const duration = r.duration_ms ? ` (${(r.duration_ms / 1e3).toFixed(1)}s)` : "";
39584
40765
  lines.push(` #${r.job_id} [${r.status}] ${formatLocalDateTime(r.started_at)}${duration}`);
39585
40766
  if (r.error) lines.push(` Error: ${r.error.slice(0, 100)}`);
@@ -39622,12 +40803,12 @@ async function agentsList(globalOpts) {
39622
40803
  ).all();
39623
40804
  readDb.close();
39624
40805
  output(agents2, (d) => {
39625
- const list = d;
39626
- if (list.length === 0) return `
40806
+ const list2 = d;
40807
+ if (list2.length === 0) return `
39627
40808
  ${muted("No active agents.")}
39628
40809
  `;
39629
- const lines = ["", divider(`Active Agents (${list.length})`), ""];
39630
- for (const a of list) {
40810
+ const lines = ["", divider(`Active Agents (${list2.length})`), ""];
40811
+ for (const a of list2) {
39631
40812
  const shortId = a.id?.slice(0, 8) ?? "?";
39632
40813
  lines.push(` ${statusDot(a.status)} ${shortId} (${a.runnerId}) \u2014 ${a.status}`);
39633
40814
  if (a.task) lines.push(` Task: ${a.task.slice(0, 80)}${a.task.length > 80 ? "\u2026" : ""}`);
@@ -39653,12 +40834,12 @@ async function tasksList(globalOpts) {
39653
40834
  ).all();
39654
40835
  readDb.close();
39655
40836
  output(tasks, (d) => {
39656
- const list = d;
39657
- if (list.length === 0) return `
40837
+ const list2 = d;
40838
+ if (list2.length === 0) return `
39658
40839
  ${muted("No active tasks.")}
39659
40840
  `;
39660
- const lines = ["", divider(`Task Board (${list.length})`), ""];
39661
- for (const t of list) {
40841
+ const lines = ["", divider(`Task Board (${list2.length})`), ""];
40842
+ for (const t of list2) {
39662
40843
  const assignee = t.assignee ? ` (\u2192 ${t.assignee.slice(0, 8)})` : "";
39663
40844
  lines.push(` ${statusDot(t.status === "completed" ? "ok" : t.status === "in_progress" ? "running" : "paused")} #${t.id}: ${t.subject}${assignee}`);
39664
40845
  }
@@ -39737,12 +40918,12 @@ async function runnersList(globalOpts) {
39737
40918
  displayName: r.displayName,
39738
40919
  specialties: r.capabilities.specialties ?? []
39739
40920
  })), (d) => {
39740
- const list = d;
39741
- if (list.length === 0) return `
40921
+ const list2 = d;
40922
+ if (list2.length === 0) return `
39742
40923
  ${muted("No runners registered.")}
39743
40924
  `;
39744
- const lines = ["", divider(`Registered Runners (${list.length})`), ""];
39745
- for (const r of list) {
40925
+ const lines = ["", divider(`Registered Runners (${list2.length})`), ""];
40926
+ for (const r of list2) {
39746
40927
  const specs = r.specialties.length > 0 ? ` \u2014 ${r.specialties.join(", ")}` : "";
39747
40928
  lines.push(` \u2022 ${r.id} (${r.displayName})${specs}`);
39748
40929
  }
@@ -39965,9 +41146,9 @@ async function usageTokens(globalOpts) {
39965
41146
  });
39966
41147
  readDb.close();
39967
41148
  output(data, (d) => {
39968
- const list = d;
41149
+ const list2 = d;
39969
41150
  const lines = ["", divider("Backend usage (last 24h)"), ""];
39970
- for (const u of list) {
41151
+ for (const u of list2) {
39971
41152
  lines.push(` ${u.displayName}: ${(u.input_tokens / 1e3).toFixed(1)}K in / ${(u.output_tokens / 1e3).toFixed(1)}K out (${u.request_count} requests)`);
39972
41153
  }
39973
41154
  lines.push("");
@@ -39981,13 +41162,13 @@ async function limitsList(globalOpts) {
39981
41162
  const limits2 = readDb.prepare("SELECT * FROM backend_limits").all();
39982
41163
  readDb.close();
39983
41164
  output(limits2, (d) => {
39984
- const list = d;
39985
- if (list.length === 0) return `
41165
+ const list2 = d;
41166
+ if (list2.length === 0) return `
39986
41167
  ${muted("No usage limits set.")}
39987
41168
  ${muted("Set with: cc-claw usage limits set <backend> <window> <tokens>")}
39988
41169
  `;
39989
41170
  const lines = ["", divider("Usage limits"), ""];
39990
- for (const l of list) {
41171
+ for (const l of list2) {
39991
41172
  lines.push(` ${l.backend} (${l.window}): ${l.max_input_tokens ? `${(l.max_input_tokens / 1e3).toFixed(0)}K input tokens` : "no limit"}`);
39992
41173
  }
39993
41174
  lines.push("");
@@ -40295,9 +41476,9 @@ async function toolsList(globalOpts) {
40295
41476
  const toolMap = new Map(rows.map((r) => [r.tool, !!r.enabled]));
40296
41477
  const tools2 = ALL_TOOLS4.map((t) => ({ name: t, enabled: toolMap.get(t) ?? true }));
40297
41478
  output(tools2, (d) => {
40298
- const list = d;
41479
+ const list2 = d;
40299
41480
  const lines = ["", divider("Tools"), ""];
40300
- for (const t of list) {
41481
+ for (const t of list2) {
40301
41482
  lines.push(` ${checkMark(t.enabled)} ${t.name}`);
40302
41483
  }
40303
41484
  lines.push("");
@@ -40824,13 +42005,13 @@ async function chatsList(_globalOpts) {
40824
42005
  const aliases = readDb.prepare("SELECT alias, chat_id FROM chat_aliases ORDER BY alias").all();
40825
42006
  readDb.close();
40826
42007
  output(aliases, (d) => {
40827
- const list = d;
40828
- if (list.length === 0) return `
42008
+ const list2 = d;
42009
+ if (list2.length === 0) return `
40829
42010
  ${muted("No chat aliases configured yet.")}
40830
42011
  ${muted("Use: cc-claw chats alias <chat_id> <name>")}
40831
42012
  `;
40832
42013
  const lines = ["", divider("Chat Aliases"), ""];
40833
- for (const a of list) {
42014
+ for (const a of list2) {
40834
42015
  lines.push(` ${a.alias} \u2192 ${a.chat_id}`);
40835
42016
  }
40836
42017
  lines.push("");
@@ -40893,13 +42074,13 @@ async function skillsList(_globalOpts) {
40893
42074
  sources: s.sources,
40894
42075
  filePath: s.filePath
40895
42076
  })), (d) => {
40896
- const list = d;
40897
- if (list.length === 0) return `
42077
+ const list2 = d;
42078
+ if (list2.length === 0) return `
40898
42079
  ${muted("No skills found.")}
40899
42080
  ${muted("Install with: cc-claw skills install <github-url>")}
40900
42081
  `;
40901
- const lines = ["", divider(`Skills (${list.length})`), ""];
40902
- for (const s of list) {
42082
+ const lines = ["", divider(`Skills (${list2.length})`), ""];
42083
+ for (const s of list2) {
40903
42084
  const tags = s.sources.join(", ");
40904
42085
  const desc = s.description ? ` \u2014 ${s.description.slice(0, 60)}` : "";
40905
42086
  lines.push(` \u2022 ${s.name} [${muted(tags)}]${desc}`);
@@ -40964,14 +42145,14 @@ async function mcpList(globalOpts) {
40964
42145
  const mcps = listMcpServers2(db3);
40965
42146
  db3.close();
40966
42147
  output(mcps, (d) => {
40967
- const list = d;
40968
- if (list.length === 0) {
42148
+ const list2 = d;
42149
+ if (list2.length === 0) {
40969
42150
  return `
40970
42151
  ${muted("No MCP servers registered. Use `cc-claw mcp add` or `cc-claw mcp import`.")}
40971
42152
  `;
40972
42153
  }
40973
- const lines = ["", divider(`MCP Servers (${list.length})`), ""];
40974
- for (const m of list) {
42154
+ const lines = ["", divider(`MCP Servers (${list2.length})`), ""];
42155
+ for (const m of list2) {
40975
42156
  const lock = SYSTEM_MCP_NAMES.has(m.name) ? " \u{1F512}" : "";
40976
42157
  const pin = m.enabledByDefault ? " \u{1F4CC}" : "";
40977
42158
  const desc = m.description ? ` \u2014 ${muted(m.description)}` : "";
@@ -41378,6 +42559,7 @@ async function tuiCommand(globalOpts, cmdOpts) {
41378
42559
  outputError("DAEMON_OFFLINE", "CC-Claw daemon is not running.\n\n Start it with: cc-claw service start");
41379
42560
  process.exit(1);
41380
42561
  }
42562
+ globalThis._cliStayAlive = true;
41381
42563
  const chatId = resolveChatId2(globalOpts);
41382
42564
  let theme = getTheme();
41383
42565
  const rl2 = createInterface10({
@@ -43387,6 +44569,7 @@ program.command("council").alias("debate").description("Multi-model council deba
43387
44569
  console.log("Select 2+ models, pose a question, and they debate anonymously for up to 3 rounds.");
43388
44570
  });
43389
44571
  program.command("start", { hidden: true }).description("Run the bot in the foreground (use 'service start' for background daemon)").action(async () => {
44572
+ globalThis._cliStayAlive = true;
43390
44573
  await Promise.resolve().then(() => (init_index(), index_exports));
43391
44574
  });
43392
44575
  program.command("install", { hidden: true }).description("Install as background service \u2014 alias for service install").action(async () => {
@@ -43398,6 +44581,7 @@ program.command("uninstall", { hidden: true }).description("Remove background se
43398
44581
  uninstallService2();
43399
44582
  });
43400
44583
  program.command("setup").description("Interactive configuration wizard").option("--dry-run", "Run the wizard without saving anything (demo/test mode)").action(async (opts) => {
44584
+ globalThis._cliStayAlive = true;
43401
44585
  if (opts.dryRun) process.env.CC_CLAW_SETUP_DRY_RUN = "1";
43402
44586
  await Promise.resolve().then(() => (init_setup(), setup_exports));
43403
44587
  });
@@ -43433,10 +44617,12 @@ Update available: v${latest} (current: v${VERSION})`);
43433
44617
  return;
43434
44618
  }
43435
44619
  await program.parseAsync(argv);
43436
- if (process.stdout.writableLength > 0) {
43437
- process.stdout.once("drain", () => process.exit(0));
43438
- } else {
43439
- setImmediate(() => process.exit(0));
44620
+ if (!globalThis._cliStayAlive) {
44621
+ if (process.stdout.writableLength > 0) {
44622
+ process.stdout.once("drain", () => process.exit(0));
44623
+ } else {
44624
+ setTimeout(() => process.exit(0), 50);
44625
+ }
43440
44626
  }
43441
44627
  }
43442
44628