cc-claw 0.11.3 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +634 -276
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -72,7 +72,7 @@ var VERSION;
72
72
  var init_version = __esm({
73
73
  "src/version.ts"() {
74
74
  "use strict";
75
- VERSION = true ? "0.11.3" : (() => {
75
+ VERSION = true ? "0.12.2" : (() => {
76
76
  try {
77
77
  return JSON.parse(readFileSync(join2(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
78
78
  } catch {
@@ -678,22 +678,25 @@ var init_identity = __esm({
678
678
  // src/reflection/store.ts
679
679
  var store_exports4 = {};
680
680
  __export(store_exports4, {
681
+ advanceReviewSession: () => advanceReviewSession,
681
682
  cleanupBackupFiles: () => cleanupBackupFiles,
682
683
  cleanupReflectionData: () => cleanupReflectionData,
684
+ createReviewSession: () => createReviewSession,
685
+ deleteReviewSession: () => deleteReviewSession,
686
+ getAppliedCountTodayForFile: () => getAppliedCountTodayForFile,
683
687
  getAppliedInsightCountToday: () => getAppliedInsightCountToday,
684
688
  getAppliedInsights: () => getAppliedInsights,
685
- getConflictingInsights: () => getConflictingInsights,
686
689
  getGrowthMetrics: () => getGrowthMetrics,
687
690
  getInsightById: () => getInsightById,
688
691
  getLastAnalysisTime: () => getLastAnalysisTime,
689
692
  getPendingInsightCount: () => getPendingInsightCount,
690
693
  getPendingInsights: () => getPendingInsights,
691
- getRecommendations: () => getRecommendations,
692
694
  getReflectionModelConfig: () => getReflectionModelConfig,
695
+ getReflectionSettings: () => getReflectionSettings,
693
696
  getReflectionStatus: () => getReflectionStatus,
694
697
  getRejectedInsights: () => getRejectedInsights,
698
+ getReviewSession: () => getReviewSession,
695
699
  getSignalCountForChat: () => getSignalCountForChat,
696
- getSignalsForPeriod: () => getSignalsForPeriod,
697
700
  getUnprocessedSignalCount: () => getUnprocessedSignalCount,
698
701
  getUnprocessedSignals: () => getUnprocessedSignals,
699
702
  initReflectionTables: () => initReflectionTables,
@@ -701,6 +704,7 @@ __export(store_exports4, {
701
704
  logSignal: () => logSignal,
702
705
  markSignalsProcessed: () => markSignalsProcessed,
703
706
  setReflectionModelConfig: () => setReflectionModelConfig,
707
+ setReflectionSettings: () => setReflectionSettings,
704
708
  setReflectionStatus: () => setReflectionStatus,
705
709
  updateInsightEffectiveness: () => updateInsightEffectiveness,
706
710
  updateInsightProposal: () => updateInsightProposal,
@@ -710,16 +714,37 @@ __export(store_exports4, {
710
714
  });
711
715
  import { existsSync, readdirSync, statSync, unlinkSync } from "fs";
712
716
  import { join as join3 } from "path";
713
- function cleanupBackupFiles(ccClawHome) {
714
- const identityDir = join3(ccClawHome, "identity");
715
- if (!existsSync(identityDir)) return;
716
- const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1e3;
717
- for (const file of readdirSync(identityDir)) {
718
- if (!file.includes(".bak.")) continue;
719
- const fullPath = join3(identityDir, file);
717
+ function cleanupBackupFiles(ccClawHome, retentionDays = 7) {
718
+ const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
719
+ const dirs = [
720
+ join3(ccClawHome, "identity"),
721
+ join3(ccClawHome, "workspace", "context")
722
+ ];
723
+ const skillsRoot = join3(ccClawHome, "workspace", "skills");
724
+ try {
725
+ if (existsSync(skillsRoot)) {
726
+ for (const entry of readdirSync(skillsRoot)) {
727
+ const p = join3(skillsRoot, entry);
728
+ try {
729
+ if (statSync(p).isDirectory()) dirs.push(p);
730
+ } catch {
731
+ }
732
+ }
733
+ }
734
+ } catch {
735
+ }
736
+ for (const dir of dirs) {
737
+ if (!existsSync(dir)) continue;
720
738
  try {
721
- const stat2 = statSync(fullPath);
722
- if (stat2.mtimeMs < thirtyDaysAgo) unlinkSync(fullPath);
739
+ for (const file of readdirSync(dir)) {
740
+ if (!file.includes(".bak.")) continue;
741
+ const fullPath = join3(dir, file);
742
+ try {
743
+ const stat2 = statSync(fullPath);
744
+ if (stat2.mtimeMs < cutoffMs) unlinkSync(fullPath);
745
+ } catch {
746
+ }
747
+ }
723
748
  } catch {
724
749
  }
725
750
  }
@@ -795,9 +820,25 @@ function initReflectionTables(db3) {
795
820
  model TEXT,
796
821
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
797
822
  );
823
+
824
+ CREATE TABLE IF NOT EXISTS review_sessions (
825
+ chatId TEXT PRIMARY KEY,
826
+ insightIds TEXT NOT NULL,
827
+ currentIndex INTEGER NOT NULL DEFAULT 0,
828
+ results TEXT NOT NULL DEFAULT '{}',
829
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
830
+ );
831
+
832
+ CREATE TABLE IF NOT EXISTS reflection_settings (
833
+ chatId TEXT PRIMARY KEY,
834
+ perFileCap INTEGER NOT NULL DEFAULT 3,
835
+ backupRetentionDays INTEGER NOT NULL DEFAULT 7,
836
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
837
+ );
798
838
  `);
799
839
  cleanupReflectionData(db3);
800
- cleanupBackupFiles(CC_CLAW_HOME);
840
+ const settings = getReflectionSettings(db3, "global");
841
+ cleanupBackupFiles(CC_CLAW_HOME, settings.backupRetentionDays);
801
842
  }
802
843
  function logSignal(db3, params) {
803
844
  const result = db3.prepare(`
@@ -839,13 +880,6 @@ function getSignalCountForChat(db3, chatId, minutesBack) {
839
880
  `).get(chatId, minutesBack);
840
881
  return row.count;
841
882
  }
842
- function getSignalsForPeriod(db3, chatId, period) {
843
- return db3.prepare(`
844
- SELECT * FROM feedback_signals
845
- WHERE chatId = ? AND date(created_at) = ?
846
- ORDER BY created_at ASC
847
- `).all(chatId, period);
848
- }
849
883
  function getUnprocessedSignalCount(db3, chatId) {
850
884
  const row = db3.prepare(`
851
885
  SELECT COUNT(*) as count FROM feedback_signals
@@ -916,14 +950,6 @@ function getRejectedInsights(db3, chatId, limit = 20) {
916
950
  LIMIT ?
917
951
  `).all(chatId, limit);
918
952
  }
919
- function getRecommendations(db3, chatId, limit = 20) {
920
- return db3.prepare(`
921
- SELECT * FROM insights
922
- WHERE chatId = ? AND status = 'recommendation'
923
- ORDER BY created_at DESC
924
- LIMIT ?
925
- `).all(chatId, limit);
926
- }
927
953
  function updateInsightStatus(db3, id, status) {
928
954
  if (status === "applied") {
929
955
  db3.prepare(`
@@ -944,13 +970,6 @@ function updateInsightRollback(db3, id, rollbackData) {
944
970
  function updateInsightEffectiveness(db3, id, score) {
945
971
  db3.prepare("UPDATE insights SET effectiveness = ? WHERE id = ?").run(score, id);
946
972
  }
947
- function getConflictingInsights(db3, chatId, targetFile) {
948
- return db3.prepare(`
949
- SELECT * FROM insights
950
- WHERE chatId = ? AND targetFile = ? AND status = 'applied'
951
- ORDER BY appliedAt DESC
952
- `).all(chatId, targetFile);
953
- }
954
973
  function getPendingInsightCount(db3, chatId) {
955
974
  const row = db3.prepare(`
956
975
  SELECT COUNT(*) as count FROM insights WHERE chatId = ? AND status = 'pending'
@@ -960,10 +979,17 @@ function getPendingInsightCount(db3, chatId) {
960
979
  function getAppliedInsightCountToday(db3, chatId) {
961
980
  const row = db3.prepare(`
962
981
  SELECT COUNT(*) as count FROM insights
963
- WHERE chatId = ? AND status = 'applied' AND date(appliedAt) = date('now')
982
+ WHERE chatId = ? AND status = 'applied' AND appliedAt >= date('now') AND appliedAt < date('now', '+1 day')
964
983
  `).get(chatId);
965
984
  return row.count;
966
985
  }
986
+ function getAppliedCountTodayForFile(db3, chatId, targetFile) {
987
+ const row = db3.prepare(`
988
+ SELECT COUNT(*) as count FROM insights
989
+ WHERE chatId = ? AND status = 'applied' AND targetFile = ? AND appliedAt >= date('now') AND appliedAt < date('now', '+1 day')
990
+ `).get(chatId, targetFile);
991
+ return row.count;
992
+ }
967
993
  function upsertGrowthMetric(db3, chatId, period, data) {
968
994
  db3.prepare(`
969
995
  INSERT OR REPLACE INTO growth_metrics (chatId, period, corrections, praises, errors, insightsApplied)
@@ -991,6 +1017,58 @@ function setReflectionModelConfig(db3, chatId, mode, backend2, model2) {
991
1017
  VALUES (?, ?, ?, ?)
992
1018
  `).run(chatId, mode, backend2 ?? null, model2 ?? null);
993
1019
  }
1020
+ function createReviewSession(db3, chatId, insightIds) {
1021
+ db3.prepare(`
1022
+ INSERT OR REPLACE INTO review_sessions (chatId, insightIds, currentIndex, results)
1023
+ VALUES (?, ?, 0, '{}')
1024
+ `).run(chatId, JSON.stringify(insightIds));
1025
+ }
1026
+ function getReviewSession(db3, chatId) {
1027
+ const row = db3.prepare(
1028
+ "SELECT * FROM review_sessions WHERE chatId = ?"
1029
+ ).get(chatId);
1030
+ if (!row) return null;
1031
+ return {
1032
+ chatId: row.chatId,
1033
+ insightIds: JSON.parse(row.insightIds),
1034
+ currentIndex: row.currentIndex,
1035
+ results: JSON.parse(row.results),
1036
+ created_at: row.created_at
1037
+ };
1038
+ }
1039
+ function advanceReviewSession(db3, chatId, insightId, action) {
1040
+ const session2 = getReviewSession(db3, chatId);
1041
+ if (!session2) return null;
1042
+ session2.results[insightId] = action;
1043
+ session2.currentIndex = session2.currentIndex + 1;
1044
+ db3.prepare(`
1045
+ UPDATE review_sessions SET currentIndex = ?, results = ? WHERE chatId = ?
1046
+ `).run(session2.currentIndex, JSON.stringify(session2.results), chatId);
1047
+ return session2;
1048
+ }
1049
+ function deleteReviewSession(db3, chatId) {
1050
+ db3.prepare("DELETE FROM review_sessions WHERE chatId = ?").run(chatId);
1051
+ }
1052
+ function getReflectionSettings(db3, chatId) {
1053
+ const row = db3.prepare(
1054
+ "SELECT perFileCap, backupRetentionDays FROM reflection_settings WHERE chatId = ?"
1055
+ ).get(chatId);
1056
+ return {
1057
+ perFileCap: row?.perFileCap ?? 3,
1058
+ backupRetentionDays: row?.backupRetentionDays ?? 7
1059
+ };
1060
+ }
1061
+ function setReflectionSettings(db3, chatId, settings) {
1062
+ const current = getReflectionSettings(db3, chatId);
1063
+ db3.prepare(`
1064
+ INSERT OR REPLACE INTO reflection_settings (chatId, perFileCap, backupRetentionDays)
1065
+ VALUES (?, ?, ?)
1066
+ `).run(
1067
+ chatId,
1068
+ settings.perFileCap ?? current.perFileCap,
1069
+ settings.backupRetentionDays ?? current.backupRetentionDays
1070
+ );
1071
+ }
994
1072
  function cleanupReflectionData(db3) {
995
1073
  db3.prepare(`
996
1074
  DELETE FROM feedback_signals WHERE created_at < datetime('now', '-90 days')
@@ -1001,6 +1079,9 @@ function cleanupReflectionData(db3) {
1001
1079
  db3.prepare(`
1002
1080
  DELETE FROM growth_metrics WHERE created_at < datetime('now', '-365 days')
1003
1081
  `).run();
1082
+ db3.prepare(`
1083
+ DELETE FROM review_sessions WHERE created_at < datetime('now', '-1 day')
1084
+ `).run();
1004
1085
  }
1005
1086
  var init_store4 = __esm({
1006
1087
  "src/reflection/store.ts"() {
@@ -1558,6 +1639,7 @@ function initDatabase() {
1558
1639
  target TEXT,
1559
1640
  delivery_mode TEXT NOT NULL DEFAULT 'announce',
1560
1641
  timezone TEXT NOT NULL DEFAULT 'UTC',
1642
+ job_type TEXT DEFAULT 'normal',
1561
1643
  enabled INTEGER NOT NULL DEFAULT 1,
1562
1644
  active INTEGER NOT NULL DEFAULT 1,
1563
1645
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
@@ -1594,6 +1676,7 @@ function initDatabase() {
1594
1676
  target TEXT,
1595
1677
  delivery_mode TEXT NOT NULL DEFAULT 'announce',
1596
1678
  timezone TEXT NOT NULL DEFAULT 'UTC',
1679
+ job_type TEXT DEFAULT 'normal',
1597
1680
  enabled INTEGER NOT NULL DEFAULT 1,
1598
1681
  active INTEGER NOT NULL DEFAULT 1,
1599
1682
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
@@ -1681,6 +1764,14 @@ function initDatabase() {
1681
1764
  db.exec("ALTER TABLE jobs ADD COLUMN title TEXT");
1682
1765
  } catch {
1683
1766
  }
1767
+ try {
1768
+ db.exec("ALTER TABLE jobs ADD COLUMN job_type TEXT DEFAULT 'normal'");
1769
+ } catch {
1770
+ }
1771
+ try {
1772
+ db.exec("UPDATE jobs SET job_type = 'reflection' WHERE description LIKE '%reflection analysis%' AND job_type = 'normal'");
1773
+ } catch {
1774
+ }
1684
1775
  backfillJobTitles(db);
1685
1776
  }
1686
1777
  } catch {
@@ -2529,8 +2620,8 @@ function getBackendUsageInWindow(backend2, windowType) {
2529
2620
  function insertJob(params) {
2530
2621
  const result = db.prepare(`
2531
2622
  INSERT INTO jobs (schedule_type, cron, at_time, every_ms, title, description, chat_id,
2532
- backend, model, thinking, timeout, fallbacks, session_type, channel, target, delivery_mode, timezone)
2533
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2623
+ backend, model, thinking, timeout, fallbacks, session_type, channel, target, delivery_mode, timezone, job_type)
2624
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2534
2625
  `).run(
2535
2626
  params.scheduleType,
2536
2627
  params.cron ?? null,
@@ -2548,7 +2639,8 @@ function insertJob(params) {
2548
2639
  params.channel ?? null,
2549
2640
  params.target ?? null,
2550
2641
  params.deliveryMode ?? "announce",
2551
- params.timezone ?? "UTC"
2642
+ params.timezone ?? "UTC",
2643
+ params.jobType ?? "normal"
2552
2644
  );
2553
2645
  return getJobById(Number(result.lastInsertRowid));
2554
2646
  }
@@ -2629,7 +2721,8 @@ function updateJob(id, fields) {
2629
2721
  channel: "channel",
2630
2722
  target: "target",
2631
2723
  deliveryMode: "delivery_mode",
2632
- timezone: "timezone"
2724
+ timezone: "timezone",
2725
+ jobType: "job_type"
2633
2726
  };
2634
2727
  for (const [key, val] of Object.entries(fields)) {
2635
2728
  const col = fieldMap[key];
@@ -3066,7 +3159,7 @@ var init_store5 = __esm({
3066
3159
  SELECT id, schedule_type as scheduleType, cron, at_time as atTime, every_ms as everyMs,
3067
3160
  title, description, chat_id as chatId, backend, model, thinking, timeout, fallbacks,
3068
3161
  session_type as sessionType, channel, target, delivery_mode as deliveryMode,
3069
- timezone, enabled, active, created_at as createdAt, last_run_at as lastRunAt,
3162
+ timezone, job_type as jobType, enabled, active, created_at as createdAt, last_run_at as lastRunAt,
3070
3163
  next_run_at as nextRunAt, consecutive_failures as consecutiveFailures
3071
3164
  FROM jobs
3072
3165
  `;
@@ -4958,7 +5051,7 @@ ${transcript}`;
4958
5051
  if (!resultText) resultText = accumulatedText;
4959
5052
  if (!resultText) {
4960
5053
  warn(`[summarize] ${adapter.id}:${model2} returned empty result for chat ${chatId}`);
4961
- return false;
5054
+ return { success: false, rawText: "" };
4962
5055
  }
4963
5056
  const summaryMatch = resultText.match(/SUMMARY:\s*(.+?)(?=\nKEY_DETAILS:|\nTOPICS:|$)/s);
4964
5057
  const keyDetailsMatch = resultText.match(/KEY_DETAILS:\s*(.+?)(?=\nTOPICS:|$)/s);
@@ -4970,16 +5063,44 @@ Key details: ${keyDetails}`;
4970
5063
  const topics = topicsMatch?.[1]?.trim() ?? "";
4971
5064
  saveSessionSummaryWithEmbedding(chatId, summary, topics, Math.floor(entries.length / 2));
4972
5065
  log(`[summarize] Saved summary via ${adapter.id}:${model2} for chat ${chatId}`);
4973
- return true;
5066
+ return { success: true, rawText: resultText };
4974
5067
  } catch (err) {
4975
5068
  warn(`[summarize] ${adapter.id}:${model2} failed for ${chatId}: ${errorMessage(err)}`);
4976
- return false;
5069
+ return { success: false, rawText: "" };
4977
5070
  }
4978
5071
  }
4979
- async function summarizeWithFallbackChain(chatId, targetBackendId, excludeBackend) {
5072
+ async function extractAndLogSignals(rawText, chatId, adapterId, model2) {
5073
+ try {
5074
+ const { getReflectionStatus: getReflectionStatus2, logSignal: logSignal2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
5075
+ const { isSyntheticChatId: isSyntheticChatId3 } = await Promise.resolve().then(() => (init_agent(), agent_exports));
5076
+ const db3 = getDb();
5077
+ if (isSyntheticChatId3(chatId) || getReflectionStatus2(db3, chatId) === "frozen") return;
5078
+ const signalMatches = rawText.matchAll(/^-\s*\[(\w+)\]\s*(.+)/gm);
5079
+ for (const match of signalMatches) {
5080
+ const signalType = match[1].toLowerCase();
5081
+ const trigger = match[2].trim();
5082
+ if (VALID_SIGNAL_TYPES.includes(signalType)) {
5083
+ logSignal2(db3, {
5084
+ chatId,
5085
+ signalType,
5086
+ trigger,
5087
+ context: null,
5088
+ source: "llm_summary",
5089
+ confidence: 0.7,
5090
+ backend: adapterId,
5091
+ model: model2,
5092
+ thinkingLevel: null
5093
+ });
5094
+ }
5095
+ }
5096
+ } catch (e) {
5097
+ warn(`[summarize] Signal extraction failed for ${chatId}: ${e}`);
5098
+ }
5099
+ }
5100
+ async function summarizeWithFallbackChain(chatId, targetBackendId, excludeBackend, clearLogAfter = true) {
4980
5101
  const pairCount = getMessagePairCount(chatId);
4981
5102
  if (pairCount < MIN_PAIRS) {
4982
- clearLog(chatId);
5103
+ if (clearLogAfter) clearLog(chatId);
4983
5104
  return false;
4984
5105
  }
4985
5106
  const entries = getLog(chatId);
@@ -4996,8 +5117,10 @@ async function summarizeWithFallbackChain(chatId, targetBackendId, excludeBacken
4996
5117
  const model2 = config2.model ?? adapter.summarizerModel;
4997
5118
  const key = `${adapter.id}:${model2}`;
4998
5119
  tried.add(key);
4999
- if (await attemptSummarize(chatId, adapter, model2, entries)) {
5000
- clearLog(chatId);
5120
+ const result = await attemptSummarize(chatId, adapter, model2, entries);
5121
+ if (result.success) {
5122
+ await extractAndLogSignals(result.rawText, chatId, adapter.id, model2);
5123
+ if (clearLogAfter) clearLog(chatId);
5001
5124
  return true;
5002
5125
  }
5003
5126
  }
@@ -5010,8 +5133,10 @@ async function summarizeWithFallbackChain(chatId, targetBackendId, excludeBacken
5010
5133
  const key = `${targetAdapter.id}:${model2}`;
5011
5134
  if (!tried.has(key)) {
5012
5135
  tried.add(key);
5013
- if (await attemptSummarize(chatId, targetAdapter, model2, entries)) {
5014
- clearLog(chatId);
5136
+ const result = await attemptSummarize(chatId, targetAdapter, model2, entries);
5137
+ if (result.success) {
5138
+ await extractAndLogSignals(result.rawText, chatId, targetAdapter.id, model2);
5139
+ if (clearLogAfter) clearLog(chatId);
5015
5140
  return true;
5016
5141
  }
5017
5142
  }
@@ -5023,8 +5148,10 @@ async function summarizeWithFallbackChain(chatId, targetBackendId, excludeBacken
5023
5148
  const key = `${adapter.id}:${model2}`;
5024
5149
  if (!tried.has(key)) {
5025
5150
  tried.add(key);
5026
- if (await attemptSummarize(chatId, adapter, model2, entries)) {
5027
- clearLog(chatId);
5151
+ const result = await attemptSummarize(chatId, adapter, model2, entries);
5152
+ if (result.success) {
5153
+ await extractAndLogSignals(result.rawText, chatId, adapter.id, model2);
5154
+ if (clearLogAfter) clearLog(chatId);
5028
5155
  return true;
5029
5156
  }
5030
5157
  }
@@ -5032,8 +5159,8 @@ async function summarizeWithFallbackChain(chatId, targetBackendId, excludeBacken
5032
5159
  warn(`[summarize] All fallback attempts failed for chat ${chatId} \u2014 raw log preserved`);
5033
5160
  return false;
5034
5161
  }
5035
- async function summarizeSession(chatId) {
5036
- return summarizeWithFallbackChain(chatId);
5162
+ async function summarizeSession(chatId, clearLogAfter = true) {
5163
+ return summarizeWithFallbackChain(chatId, void 0, void 0, clearLogAfter);
5037
5164
  }
5038
5165
  async function summarizeAllPending() {
5039
5166
  const allIds = getLoggedChatIds();
@@ -5051,7 +5178,7 @@ async function summarizeAllPending() {
5051
5178
  await summarizeSession(chatId);
5052
5179
  }
5053
5180
  }
5054
- var MIN_PAIRS, USER_MSG_LIMIT, AGENT_MSG_LIMIT, TRANSCRIPT_CAP, SUMMARIZE_TIMEOUT_MS, SUMMARIZE_PROMPT, CONTEXT_BRIDGE_PROMPT;
5181
+ var MIN_PAIRS, USER_MSG_LIMIT, AGENT_MSG_LIMIT, TRANSCRIPT_CAP, SUMMARIZE_TIMEOUT_MS, SUMMARIZE_PROMPT, CONTEXT_BRIDGE_PROMPT, VALID_SIGNAL_TYPES;
5055
5182
  var init_summarize = __esm({
5056
5183
  "src/memory/summarize.ts"() {
5057
5184
  "use strict";
@@ -5086,7 +5213,15 @@ SUMMARY: <detailed summary>
5086
5213
  KEY_DETAILS:
5087
5214
  - <detail 1>
5088
5215
  - <detail 2>
5089
- TOPICS: <specific-keyword1, specific-keyword2, ...>`;
5216
+ TOPICS: <specific-keyword1, specific-keyword2, ...>
5217
+
5218
+ SIGNALS (0-5 items, only if the conversation contains user feedback about the assistant's performance):
5219
+ - [correction] brief description of what the user corrected
5220
+ - [preference] brief description of a preference the user expressed
5221
+ - [frustration] brief description of user frustration or dissatisfaction
5222
+ - [praise] brief description of user satisfaction or approval
5223
+ - [error_pattern] brief description of a recurring error the user pointed out
5224
+ If no feedback signals are present, omit this section entirely.`;
5090
5225
  CONTEXT_BRIDGE_PROMPT = `You are handing off an active work session to a new AI assistant. Write a concise handoff brief (3-8 sentences) covering:
5091
5226
  - What the user is actively working on RIGHT NOW
5092
5227
  - Exact current state: what's done, what's in progress, what's blocked
@@ -5095,6 +5230,7 @@ TOPICS: <specific-keyword1, specific-keyword2, ...>`;
5095
5230
  - Any critical constraints or preferences expressed by the user
5096
5231
 
5097
5232
  Be specific. The new assistant has no prior context. Cover any domain \u2014 personal, research, scheduling, content, technical \u2014 whatever applies to this session.`;
5233
+ VALID_SIGNAL_TYPES = ["correction", "preference", "praise", "frustration", "error_pattern"];
5098
5234
  }
5099
5235
  });
5100
5236
 
@@ -6446,9 +6582,9 @@ function aggregateDailyMetrics(db3, chatId, date) {
6446
6582
  const signalCounts = db3.prepare(`
6447
6583
  SELECT signalType, COUNT(*) as count
6448
6584
  FROM feedback_signals
6449
- WHERE chatId = ? AND date(created_at) = ?
6585
+ WHERE chatId = ? AND created_at >= ? AND created_at < date(?, '+1 day')
6450
6586
  GROUP BY signalType
6451
- `).all(chatId, period);
6587
+ `).all(chatId, period, period);
6452
6588
  let corrections = 0;
6453
6589
  let praises = 0;
6454
6590
  let errors = 0;
@@ -6467,8 +6603,8 @@ function aggregateDailyMetrics(db3, chatId, date) {
6467
6603
  }
6468
6604
  const insightsRow = db3.prepare(`
6469
6605
  SELECT COUNT(*) as count FROM insights
6470
- WHERE chatId = ? AND date(appliedAt) = ?
6471
- `).get(chatId, period);
6606
+ WHERE chatId = ? AND appliedAt >= ? AND appliedAt < date(?, '+1 day')
6607
+ `).get(chatId, period, period);
6472
6608
  upsertGrowthMetric(db3, chatId, period, {
6473
6609
  corrections,
6474
6610
  praises,
@@ -6659,6 +6795,7 @@ Rules:
6659
6795
  - WHY field max 2 sentences
6660
6796
  - Only propose changes you are confident about (confidence >= 0.6)
6661
6797
  - Never re-propose previously rejected insights
6798
+ - Do not propose more than 3 changes to the same target file in a single analysis (spread across files to avoid drift)
6662
6799
 
6663
6800
  Valid categories:
6664
6801
  ${categoryList}`);
@@ -6774,7 +6911,7 @@ function resolveReflectionAdapter(chatId) {
6774
6911
  if (config2.mode === "pinned" && config2.backend) {
6775
6912
  try {
6776
6913
  const adapter = getAdapter(config2.backend);
6777
- const model2 = config2.model ?? adapter.summarizerModel;
6914
+ const model2 = config2.model ?? adapter.defaultModel;
6778
6915
  return { adapter, model: model2 };
6779
6916
  } catch {
6780
6917
  return null;
@@ -6795,11 +6932,11 @@ function resolveReflectionAdapter(chatId) {
6795
6932
  }
6796
6933
  try {
6797
6934
  const adapter = getAdapterForChat(chatId);
6798
- return { adapter, model: adapter.summarizerModel };
6935
+ return { adapter, model: adapter.defaultModel };
6799
6936
  } catch {
6800
6937
  try {
6801
6938
  const adapter = getAdapter("claude");
6802
- return { adapter, model: adapter.summarizerModel };
6939
+ return { adapter, model: adapter.defaultModel };
6803
6940
  } catch {
6804
6941
  return null;
6805
6942
  }
@@ -6816,7 +6953,7 @@ function getTodayConversations(chatId) {
6816
6953
  const db3 = getDb();
6817
6954
  const rows = db3.prepare(`
6818
6955
  SELECT role, content FROM message_log
6819
- WHERE chat_id = ? AND date(created_at) = date('now')
6956
+ WHERE chat_id = ? AND created_at >= date('now') AND created_at < date('now', '+1 day')
6820
6957
  ORDER BY created_at ASC
6821
6958
  LIMIT 200
6822
6959
  `).all(chatId);
@@ -6835,7 +6972,7 @@ function getTodayConversations(chatId) {
6835
6972
  }
6836
6973
  return lines.join("\n\n");
6837
6974
  }
6838
- async function spawnAnalysis(adapter, model2, prompt) {
6975
+ async function spawnAnalysis(adapter, model2, prompt, timeoutMs = ANALYSIS_TIMEOUT_MS) {
6839
6976
  const config2 = adapter.buildSpawnConfig({
6840
6977
  prompt,
6841
6978
  model: model2,
@@ -6858,7 +6995,7 @@ async function spawnAnalysis(adapter, model2, prompt) {
6858
6995
  rl2.close();
6859
6996
  proc.kill("SIGTERM");
6860
6997
  setTimeout(() => proc.kill("SIGKILL"), 2e3);
6861
- }, ANALYSIS_TIMEOUT_MS);
6998
+ }, timeoutMs);
6862
6999
  rl2.on("line", (line) => {
6863
7000
  if (!line.trim()) return;
6864
7001
  let msg;
@@ -6950,7 +7087,7 @@ async function runAnalysis(chatId, opts = {}) {
6950
7087
  log(`[reflection] Running analysis via ${adapter.id}:${model2} for chat ${chatId} (${signals.length} signals, force=${force})`);
6951
7088
  let rawOutput;
6952
7089
  try {
6953
- rawOutput = await spawnAnalysis(adapter, model2, prompt);
7090
+ rawOutput = await spawnAnalysis(adapter, model2, prompt, opts.timeoutMs);
6954
7091
  } catch (err) {
6955
7092
  warn(`[reflection] Analysis spawn failed for ${chatId}: ${err instanceof Error ? err.message : String(err)}`);
6956
7093
  return [];
@@ -7043,7 +7180,7 @@ __export(apply_exports, {
7043
7180
  isTargetAllowed: () => isTargetAllowed,
7044
7181
  rollbackInsight: () => rollbackInsight
7045
7182
  });
7046
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync12, mkdirSync as mkdirSync3 } from "fs";
7183
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync12, mkdirSync as mkdirSync3, readdirSync as readdirSync6, unlinkSync as unlinkSync4 } from "fs";
7047
7184
  import { join as join11, dirname as dirname2 } from "path";
7048
7185
  function isTargetAllowed(relativePath) {
7049
7186
  if (relativePath.includes("..")) return false;
@@ -7094,6 +7231,21 @@ function applyDiff(original, diff, action) {
7094
7231
  warn(`[reflection/apply] Unknown diff action "${action}", returning original`);
7095
7232
  return original;
7096
7233
  }
7234
+ function pruneBackups(absolutePath) {
7235
+ const dir = dirname2(absolutePath);
7236
+ const baseName = absolutePath.split("/").pop() ?? "";
7237
+ try {
7238
+ const backups = readdirSync6(dir).filter((f) => f.startsWith(baseName + ".bak.")).sort().map((f) => join11(dir, f));
7239
+ while (backups.length > MAX_BACKUPS_PER_FILE) {
7240
+ const oldest = backups.shift();
7241
+ try {
7242
+ unlinkSync4(oldest);
7243
+ } catch {
7244
+ }
7245
+ }
7246
+ } catch {
7247
+ }
7248
+ }
7097
7249
  async function applyInsight(insightId) {
7098
7250
  const db3 = getDb();
7099
7251
  const insight = getInsightById(db3, insightId);
@@ -7123,11 +7275,13 @@ async function applyInsight(insightId) {
7123
7275
  }
7124
7276
  }
7125
7277
  const chatId = insight.chatId ?? "global";
7126
- const appliedToday = getAppliedInsightCountToday(db3, chatId);
7127
- if (appliedToday >= DAILY_APPLY_CAP) {
7278
+ const settings = getReflectionSettings(db3, chatId);
7279
+ const perFileCap = settings.perFileCap;
7280
+ const appliedTodayForFile = getAppliedCountTodayForFile(db3, chatId, insight.targetFile);
7281
+ if (appliedTodayForFile >= perFileCap) {
7128
7282
  return {
7129
7283
  success: false,
7130
- message: `Daily apply cap reached (${appliedToday}/${DAILY_APPLY_CAP}). Try again tomorrow.`
7284
+ message: `Reflection cap reached for ${insight.targetFile} (${appliedTodayForFile}/${perFileCap} today). Try again tomorrow.`
7131
7285
  };
7132
7286
  }
7133
7287
  let original = "";
@@ -7145,6 +7299,7 @@ async function applyInsight(insightId) {
7145
7299
  }
7146
7300
  if (original) {
7147
7301
  writeFileSync3(backupPath, original, "utf-8");
7302
+ pruneBackups(absolutePath);
7148
7303
  }
7149
7304
  const newContent = applyDiff(original, insight.proposedDiff, insight.proposedAction);
7150
7305
  writeFileSync3(absolutePath, newContent, "utf-8");
@@ -7265,7 +7420,7 @@ function computeLineDrift(baseline, absolutePath) {
7265
7420
  const changed = totalBaseline - unchanged;
7266
7421
  return changed / totalBaseline;
7267
7422
  }
7268
- var ALLOWED_TARGETS, ALLOWED_PREFIXES, SOUL_LINE_CAP, DAILY_APPLY_CAP;
7423
+ var ALLOWED_TARGETS, ALLOWED_PREFIXES, SOUL_LINE_CAP, MAX_BACKUPS_PER_FILE;
7269
7424
  var init_apply = __esm({
7270
7425
  "src/reflection/apply.ts"() {
7271
7426
  "use strict";
@@ -7284,7 +7439,7 @@ var init_apply = __esm({
7284
7439
  "workspace/skills/"
7285
7440
  ];
7286
7441
  SOUL_LINE_CAP = 100;
7287
- DAILY_APPLY_CAP = 3;
7442
+ MAX_BACKUPS_PER_FILE = 3;
7288
7443
  }
7289
7444
  });
7290
7445
 
@@ -7651,8 +7806,7 @@ function startDashboard() {
7651
7806
  const body = JSON.parse(await readBody(req));
7652
7807
  const { setBackend: setBackend2, clearSession: clearSession2, clearModel: clearModel2, clearThinkingLevel: clearThinkingLevel2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
7653
7808
  const { summarizeSession: summarizeSession2 } = await Promise.resolve().then(() => (init_summarize(), summarize_exports));
7654
- summarizeSession2(body.chatId).catch(() => {
7655
- });
7809
+ await summarizeSession2(body.chatId);
7656
7810
  clearSession2(body.chatId);
7657
7811
  clearModel2(body.chatId);
7658
7812
  clearThinkingLevel2(body.chatId);
@@ -8103,6 +8257,21 @@ data: ${JSON.stringify(data)}
8103
8257
  return jsonResponse(res, { error: errorMessage(err) }, 400);
8104
8258
  }
8105
8259
  }
8260
+ if (url.pathname === "/api/evolve/settings" && req.method === "POST") {
8261
+ try {
8262
+ const body = JSON.parse(await readBody(req));
8263
+ const { setReflectionSettings: setReflectionSettings2, getReflectionSettings: getReflectionSettings2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
8264
+ const chatId = body.chatId || (process.env.ALLOWED_CHAT_ID ?? "").split(",")[0]?.trim() || "default";
8265
+ const updates = {};
8266
+ if (body.perFileCap !== void 0) updates.perFileCap = body.perFileCap;
8267
+ if (body.backupRetentionDays !== void 0) updates.backupRetentionDays = body.backupRetentionDays;
8268
+ setReflectionSettings2(getDb(), chatId, updates);
8269
+ const current = getReflectionSettings2(getDb(), chatId);
8270
+ return jsonResponse(res, { success: true, ...current });
8271
+ } catch (err) {
8272
+ return jsonResponse(res, { error: errorMessage(err) }, 400);
8273
+ }
8274
+ }
8106
8275
  if (url.pathname === "/" || url.pathname === "/index.html") {
8107
8276
  return htmlResponse(res, DASHBOARD_HTML.replace("__TOKEN__", DASHBOARD_TOKEN));
8108
8277
  }
@@ -8577,7 +8746,8 @@ __export(agent_exports, {
8577
8746
  getInFlightMessage: () => getInFlightMessage,
8578
8747
  isChatBusy: () => isChatBusy,
8579
8748
  isSyntheticChatId: () => isSyntheticChatId,
8580
- stopAgent: () => stopAgent
8749
+ stopAgent: () => stopAgent,
8750
+ stopAllActiveAgents: () => stopAllActiveAgents
8581
8751
  });
8582
8752
  import { spawn as spawn5 } from "child_process";
8583
8753
  import { createInterface as createInterface4 } from "readline";
@@ -8605,13 +8775,22 @@ function withChatLock(chatId, fn) {
8605
8775
  }
8606
8776
  const next = prev.then(fn, fn);
8607
8777
  chatLocks.set(chatId, next.then(() => {
8778
+ chatLocks.delete(chatId);
8608
8779
  }, () => {
8780
+ chatLocks.delete(chatId);
8609
8781
  }));
8610
8782
  return next;
8611
8783
  }
8612
8784
  function isChatBusy(chatId) {
8613
8785
  return activeChats.has(chatId);
8614
8786
  }
8787
+ function stopAllActiveAgents() {
8788
+ for (const [, state] of activeChats) {
8789
+ if (state.process) {
8790
+ killProcessGroup(state.process);
8791
+ }
8792
+ }
8793
+ }
8615
8794
  function stopAgent(chatId) {
8616
8795
  const state = activeChats.get(chatId);
8617
8796
  if (!state) return false;
@@ -8867,7 +9046,7 @@ async function askAgentImpl(chatId, userMessage, opts) {
8867
9046
  const effectiveAgentMode = optsAgentMode ?? getAgentMode(settingsChat);
8868
9047
  const sideQuestCtx = settingsSourceChatId ? { parentChatId: settingsSourceChatId, actualChatId: chatId } : void 0;
8869
9048
  const fullPrompt = await assembleBootstrapPrompt(userMessage, tier, settingsChat, mode, responseStyle, effectiveAgentMode, sideQuestCtx);
8870
- const existingSessionId = getSessionId(settingsChat);
9049
+ const existingSessionId = settingsSourceChatId ? null : getSessionId(settingsChat);
8871
9050
  const allowedTools = getEnabledTools(settingsChat);
8872
9051
  const mcpConfigPath = tier !== "slim" && effectiveAgentMode !== "native" && MCP_CONFIG_FLAG[adapter.id] ? getMcpConfigPath(chatId) : null;
8873
9052
  const baseConfig = adapter.buildSpawnConfig({
@@ -8925,17 +9104,19 @@ async function askAgentImpl(chatId, userMessage, opts) {
8925
9104
  }
8926
9105
  }
8927
9106
  } catch (spawnErr) {
8928
- try {
8929
- const errMsg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
8930
- const { logErrorSignal: logErrorSignal2 } = await Promise.resolve().then(() => (init_detect(), detect_exports));
8931
- if (/timeout/i.test(errMsg)) {
8932
- logErrorSignal2(chatId, "timeout", `Backend timed out`, { backendId: adapter.id, model: resolvedModel });
8933
- } else if (/repetition.?loop/i.test(errMsg)) {
8934
- logErrorSignal2(chatId, "repetition_loop", `Repetition loop detected`, { backendId: adapter.id, model: resolvedModel });
8935
- } else {
8936
- logErrorSignal2(chatId, "spawn_error", errMsg.slice(0, 300), { backendId: adapter.id, model: resolvedModel });
9107
+ if (!isSyntheticChatId(chatId)) {
9108
+ try {
9109
+ const errMsg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
9110
+ const { logErrorSignal: logErrorSignal2 } = await Promise.resolve().then(() => (init_detect(), detect_exports));
9111
+ if (/timeout/i.test(errMsg)) {
9112
+ logErrorSignal2(chatId, "timeout", `Backend timed out`, { backendId: adapter.id, model: resolvedModel });
9113
+ } else if (/repetition.?loop/i.test(errMsg)) {
9114
+ logErrorSignal2(chatId, "repetition_loop", `Repetition loop detected`, { backendId: adapter.id, model: resolvedModel });
9115
+ } else {
9116
+ logErrorSignal2(chatId, "spawn_error", errMsg.slice(0, 300), { backendId: adapter.id, model: resolvedModel });
9117
+ }
9118
+ } catch {
8937
9119
  }
8938
- } catch {
8939
9120
  }
8940
9121
  throw spawnErr;
8941
9122
  } finally {
@@ -9070,9 +9251,10 @@ ${responseText.slice(0, 500)}`);
9070
9251
  return true;
9071
9252
  }
9072
9253
  try {
9254
+ const text = responseText.replace(/\s*\[REACT:[^\]]*\]\s*/g, " ").trim();
9073
9255
  if (job.deliveryMode === "webhook") {
9074
- await deliverWebhook(job, responseText);
9075
- appendToLog(job.chatId, `[cron:#${job.id}] ${job.description}`, responseText, job.backend ?? "claude", job.model, null);
9256
+ await deliverWebhook(job, text);
9257
+ appendToLog(job.chatId, `[cron:#${job.id}] ${job.description}`, text, job.backend ?? "claude", job.model, null);
9076
9258
  return true;
9077
9259
  }
9078
9260
  const channelName = job.channel ?? "telegram";
@@ -9082,7 +9264,7 @@ ${responseText.slice(0, 500)}`);
9082
9264
  return false;
9083
9265
  }
9084
9266
  const targetChatId = job.target ?? job.chatId;
9085
- const cleanText = await processFileSends(targetChatId, channel, responseText);
9267
+ const cleanText = await processFileSends(targetChatId, channel, text);
9086
9268
  if (!cleanText) return true;
9087
9269
  if (channelName === "telegram") {
9088
9270
  const parsed = parseTelegramTarget(targetChatId);
@@ -9313,6 +9495,173 @@ var init_health2 = __esm({
9313
9495
  }
9314
9496
  });
9315
9497
 
9498
+ // src/reflection/propose.ts
9499
+ var propose_exports = {};
9500
+ __export(propose_exports, {
9501
+ buildEvolveMenuKeyboard: () => buildEvolveMenuKeyboard,
9502
+ buildEvolveOnboardingKeyboard: () => buildEvolveOnboardingKeyboard,
9503
+ buildModelKeyboard: () => buildModelKeyboard,
9504
+ buildProposalKeyboard: () => buildProposalKeyboard,
9505
+ buildReviewCompleteMessage: () => buildReviewCompleteMessage,
9506
+ buildUndoKeyboard: () => buildUndoKeyboard,
9507
+ formatDiffCodeBlock: () => formatDiffCodeBlock,
9508
+ formatGrowthReport: () => formatGrowthReport,
9509
+ formatNightlySummary: () => formatNightlySummary,
9510
+ formatProposalCard: () => formatProposalCard,
9511
+ formatProposalCardWithProgress: () => formatProposalCardWithProgress
9512
+ });
9513
+ function pct(ratio) {
9514
+ return `${Math.round(ratio * 100)}%`;
9515
+ }
9516
+ function formatProposalCard(params) {
9517
+ const { category, insight, why, targetFile, confidence, proposedAction, proposedDiff } = params;
9518
+ const header2 = `[${category}] ${insight}`;
9519
+ const whyLine = why ? `Why: ${why}` : null;
9520
+ const confidencePct = pct(confidence);
9521
+ const showTarget = category !== "codebase" && targetFile && targetFile !== "codebase";
9522
+ const targetLine = showTarget ? `Target: ${targetFile} | Confidence: ${confidencePct}` : `Confidence: ${confidencePct}`;
9523
+ const lines = [header2, ""];
9524
+ if (whyLine) lines.push(whyLine);
9525
+ lines.push(targetLine);
9526
+ if (proposedDiff) {
9527
+ const actionLabel = proposedAction ? ` (${proposedAction})` : "";
9528
+ lines.push("", `Proposed change${actionLabel}:`, "```diff", proposedDiff, "```");
9529
+ }
9530
+ return lines.join("\n");
9531
+ }
9532
+ function formatProposalCardWithProgress(params, index, total) {
9533
+ const progress = `\u{1F4CB} Proposal ${index + 1} of ${total}`;
9534
+ const separator = "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
9535
+ const card = formatProposalCard(params);
9536
+ return `${progress}
9537
+ ${separator}
9538
+ ${card}`;
9539
+ }
9540
+ function buildReviewCompleteMessage(results) {
9541
+ const values = Object.values(results);
9542
+ const applied = values.filter((v) => v === "applied").length;
9543
+ const rejected = values.filter((v) => v === "rejected").length;
9544
+ const skipped = values.filter((v) => v === "skipped").length;
9545
+ const total = values.length;
9546
+ const parts = [];
9547
+ if (applied > 0) parts.push(`${applied} applied`);
9548
+ if (rejected > 0) parts.push(`${rejected} rejected`);
9549
+ if (skipped > 0) parts.push(`${skipped} skipped`);
9550
+ return `\u2705 Review complete \u2014 ${total} proposal${total === 1 ? "" : "s"}: ${parts.join(", ")}
9551
+
9552
+ Skipped proposals will appear in your next review.`;
9553
+ }
9554
+ function formatNightlySummary(insights) {
9555
+ const count = insights.length;
9556
+ const header2 = `Nightly Reflection \u2014 ${count} proposal${count === 1 ? "" : "s"} ready`;
9557
+ const list = insights.map((ins, i) => `${i + 1}. [${ins.category}] ${ins.insight}`).join("\n");
9558
+ return `${header2}
9559
+
9560
+ ${list}
9561
+
9562
+ Review with /evolve`;
9563
+ }
9564
+ function formatGrowthReport(metrics, modelPerformance) {
9565
+ const { correctionsBefore, correctionsAfter, praiseRatio, insightsApplied, pendingCount, topInsight } = metrics;
9566
+ const reduction = correctionsBefore > 0 ? Math.round((correctionsBefore - correctionsAfter) / correctionsBefore * 100) : 0;
9567
+ const lines = [];
9568
+ lines.push("Growth Report");
9569
+ lines.push("");
9570
+ lines.push(`Corrections: ${correctionsBefore.toFixed(1)} -> ${correctionsAfter.toFixed(1)} (${reduction}% reduction)`);
9571
+ lines.push(`Praise ratio: ${praiseRatio.toFixed(1)}x`);
9572
+ lines.push(`Insights applied: ${insightsApplied} | Pending: ${pendingCount}`);
9573
+ if (topInsight) {
9574
+ lines.push(`Top insight: "${topInsight.insight}" (${pct(topInsight.effectiveness)} effective)`);
9575
+ }
9576
+ if (modelPerformance.length > 0) {
9577
+ lines.push("");
9578
+ lines.push("Model Performance:");
9579
+ for (const row of modelPerformance) {
9580
+ const total = row.praises + row.corrections + row.errors;
9581
+ const scoreLabel = total > 0 ? ` (${row.praises}P / ${row.corrections}C / ${row.errors}E)` : "";
9582
+ lines.push(` ${row.backend} ${row.model} [${row.thinking}]${scoreLabel}`);
9583
+ }
9584
+ }
9585
+ return lines.join("\n");
9586
+ }
9587
+ function formatDiffCodeBlock(diff) {
9588
+ return `\`\`\`diff
9589
+ ${diff}
9590
+ \`\`\``;
9591
+ }
9592
+ function buildProposalKeyboard(insightId, category) {
9593
+ if (category === "codebase") {
9594
+ return [
9595
+ [
9596
+ { label: "Show Diff", data: `evolve:diff:${insightId}`, style: "primary" },
9597
+ { label: "Discuss", data: `evolve:discuss:${insightId}` }
9598
+ ],
9599
+ [
9600
+ { label: "Skip \u23ED", data: `evolve:skip:${insightId}` },
9601
+ { label: "Reject", data: `evolve:reject:${insightId}`, style: "danger" }
9602
+ ]
9603
+ ];
9604
+ }
9605
+ return [
9606
+ [
9607
+ { label: "Apply", data: `evolve:apply:${insightId}`, style: "success" },
9608
+ { label: "Discuss", data: `evolve:discuss:${insightId}` }
9609
+ ],
9610
+ [
9611
+ { label: "Skip \u23ED", data: `evolve:skip:${insightId}` },
9612
+ { label: "Reject", data: `evolve:reject:${insightId}`, style: "danger" }
9613
+ ]
9614
+ ];
9615
+ }
9616
+ function buildEvolveOnboardingKeyboard() {
9617
+ return [
9618
+ [{ label: "\u2705 Enable Self-Learning", data: "evolve:toggle", style: "success" }],
9619
+ [{ label: "One-Time Analysis", data: "evolve:analyze" }]
9620
+ ];
9621
+ }
9622
+ function buildEvolveMenuKeyboard(ctx) {
9623
+ const reviewLabel = ctx.pendingProposals > 0 ? `Review (${ctx.pendingProposals})` : "Review";
9624
+ return [
9625
+ [
9626
+ { label: "\u{1F50D} Analyze Now", data: "evolve:analyze", style: "success" },
9627
+ ctx.pendingProposals > 0 ? { label: reviewLabel, data: "evolve:review", style: "primary" } : { label: reviewLabel, data: "evolve:review" }
9628
+ ],
9629
+ [
9630
+ { label: "Stats", data: "evolve:stats" },
9631
+ { label: "History", data: "evolve:history" }
9632
+ ],
9633
+ [
9634
+ { label: "Model", data: "evolve:model" },
9635
+ { label: "Undo", data: "evolve:undo", style: "danger" },
9636
+ { label: "\u23F8 Disable", data: "evolve:toggle", style: "danger" }
9637
+ ]
9638
+ ];
9639
+ }
9640
+ function buildUndoKeyboard(insights) {
9641
+ return insights.map((ins) => [
9642
+ { label: `Undo #${ins.id}: ${ins.insight}`, data: `evolve:undo:${ins.id}`, style: "danger" }
9643
+ ]);
9644
+ }
9645
+ function buildModelKeyboard(currentMode) {
9646
+ const modes = [
9647
+ { label: "Auto", mode: "auto" },
9648
+ { label: "Pinned", mode: "pinned" },
9649
+ { label: "Cheap", mode: "cheap" }
9650
+ ];
9651
+ return [
9652
+ modes.map(({ label: label2, mode }) => ({
9653
+ label: mode === currentMode ? `[${label2}]` : label2,
9654
+ data: `evolve:model:${mode}`,
9655
+ ...mode === currentMode ? { style: "primary" } : {}
9656
+ }))
9657
+ ];
9658
+ }
9659
+ var init_propose = __esm({
9660
+ "src/reflection/propose.ts"() {
9661
+ "use strict";
9662
+ }
9663
+ });
9664
+
9316
9665
  // src/scheduler/cron.ts
9317
9666
  var cron_exports = {};
9318
9667
  __export(cron_exports, {
@@ -9533,6 +9882,17 @@ async function executeJob(job) {
9533
9882
  async function runWithRetry(job, model2, runId, t0) {
9534
9883
  let lastError;
9535
9884
  const chatId = job.sessionType === "isolated" ? `cron:${job.id}:${runId}` : job.chatId;
9885
+ if (job.jobType === "reflection") {
9886
+ const { runAnalysis: runAnalysis2 } = await Promise.resolve().then(() => (init_analyze(), analyze_exports));
9887
+ const { formatNightlySummary: formatNightlySummary2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
9888
+ const timeoutMs2 = job.timeout ? job.timeout * 1e3 : void 0;
9889
+ const insights = await runAnalysis2(job.chatId, { force: true, timeoutMs: timeoutMs2 });
9890
+ if (insights.length === 0) {
9891
+ return { text: "Nightly reflection: no new insights from recent interactions." };
9892
+ }
9893
+ const items = insights.map((ins, i) => ({ id: i + 1, ...ins }));
9894
+ return { text: formatNightlySummary2(items) };
9895
+ }
9536
9896
  if (job.thinking && job.thinking !== "auto") {
9537
9897
  setThinkingLevel(chatId, job.thinking);
9538
9898
  }
@@ -9769,7 +10129,7 @@ var init_wrap_backend = __esm({
9769
10129
  });
9770
10130
 
9771
10131
  // src/agents/runners/config-loader.ts
9772
- import { readFileSync as readFileSync8, readdirSync as readdirSync6, existsSync as existsSync14, mkdirSync as mkdirSync5, watchFile, unwatchFile } from "fs";
10132
+ import { readFileSync as readFileSync8, readdirSync as readdirSync7, existsSync as existsSync14, mkdirSync as mkdirSync5, watchFile, unwatchFile } from "fs";
9773
10133
  import { join as join13 } from "path";
9774
10134
  import { execFileSync } from "child_process";
9775
10135
  function resolveExecutable(config2) {
@@ -9923,7 +10283,7 @@ function loadAllRunnerConfigs() {
9923
10283
  mkdirSync5(RUNNERS_PATH, { recursive: true });
9924
10284
  return [];
9925
10285
  }
9926
- const files = readdirSync6(RUNNERS_PATH).filter((f) => f.endsWith(".json"));
10286
+ const files = readdirSync7(RUNNERS_PATH).filter((f) => f.endsWith(".json"));
9927
10287
  const configs = [];
9928
10288
  for (const file of files) {
9929
10289
  const config2 = loadRunnerConfig(join13(RUNNERS_PATH, file));
@@ -9954,7 +10314,7 @@ function watchRunnerConfigs(onChange) {
9954
10314
  watchedFiles.delete(prev);
9955
10315
  }
9956
10316
  }
9957
- const files = readdirSync6(RUNNERS_PATH).filter((f) => f.endsWith(".json"));
10317
+ const files = readdirSync7(RUNNERS_PATH).filter((f) => f.endsWith(".json"));
9958
10318
  for (const file of files) {
9959
10319
  const fullPath = join13(RUNNERS_PATH, file);
9960
10320
  if (watchedFiles.has(fullPath)) continue;
@@ -12786,143 +13146,6 @@ var init_pagination = __esm({
12786
13146
  }
12787
13147
  });
12788
13148
 
12789
- // src/reflection/propose.ts
12790
- var propose_exports = {};
12791
- __export(propose_exports, {
12792
- buildEvolveMenuKeyboard: () => buildEvolveMenuKeyboard,
12793
- buildEvolveOnboardingKeyboard: () => buildEvolveOnboardingKeyboard,
12794
- buildModelKeyboard: () => buildModelKeyboard,
12795
- buildProposalKeyboard: () => buildProposalKeyboard,
12796
- buildUndoKeyboard: () => buildUndoKeyboard,
12797
- formatDiffCodeBlock: () => formatDiffCodeBlock,
12798
- formatGrowthReport: () => formatGrowthReport,
12799
- formatNightlySummary: () => formatNightlySummary,
12800
- formatProposalCard: () => formatProposalCard
12801
- });
12802
- function pct(ratio) {
12803
- return `${Math.round(ratio * 100)}%`;
12804
- }
12805
- function formatProposalCard(params) {
12806
- const { category, insight, why, targetFile, confidence, proposedAction, proposedDiff } = params;
12807
- const header2 = `[${category}] ${insight}`;
12808
- const whyLine = why ? `Why: ${why}` : null;
12809
- const confidencePct = pct(confidence);
12810
- const showTarget = category !== "codebase" && targetFile && targetFile !== "codebase";
12811
- const targetLine = showTarget ? `Target: ${targetFile} | Confidence: ${confidencePct}` : `Confidence: ${confidencePct}`;
12812
- const lines = [header2, ""];
12813
- if (whyLine) lines.push(whyLine);
12814
- lines.push(targetLine);
12815
- if (proposedDiff) {
12816
- const actionLabel = proposedAction ? ` (${proposedAction})` : "";
12817
- lines.push("", `Proposed change${actionLabel}:`, "```diff", proposedDiff, "```");
12818
- }
12819
- return lines.join("\n");
12820
- }
12821
- function formatNightlySummary(insights) {
12822
- const count = insights.length;
12823
- const header2 = `Nightly Reflection \u2014 ${count} proposal${count === 1 ? "" : "s"} ready`;
12824
- const list = insights.map((ins, i) => `${i + 1}. [${ins.category}] ${ins.insight}`).join("\n");
12825
- return `${header2}
12826
-
12827
- ${list}
12828
-
12829
- Review with /evolve`;
12830
- }
12831
- function formatGrowthReport(metrics, modelPerformance) {
12832
- const { correctionsBefore, correctionsAfter, praiseRatio, insightsApplied, pendingCount, topInsight } = metrics;
12833
- const reduction = correctionsBefore > 0 ? Math.round((correctionsBefore - correctionsAfter) / correctionsBefore * 100) : 0;
12834
- const lines = [];
12835
- lines.push("Growth Report");
12836
- lines.push("");
12837
- lines.push(`Corrections: ${correctionsBefore.toFixed(1)} -> ${correctionsAfter.toFixed(1)} (${reduction}% reduction)`);
12838
- lines.push(`Praise ratio: ${praiseRatio.toFixed(1)}x`);
12839
- lines.push(`Insights applied: ${insightsApplied} | Pending: ${pendingCount}`);
12840
- if (topInsight) {
12841
- lines.push(`Top insight: "${topInsight.insight}" (${pct(topInsight.effectiveness)} effective)`);
12842
- }
12843
- if (modelPerformance.length > 0) {
12844
- lines.push("");
12845
- lines.push("Model Performance:");
12846
- for (const row of modelPerformance) {
12847
- const total = row.praises + row.corrections + row.errors;
12848
- const scoreLabel = total > 0 ? ` (${row.praises}P / ${row.corrections}C / ${row.errors}E)` : "";
12849
- lines.push(` ${row.backend} ${row.model} [${row.thinking}]${scoreLabel}`);
12850
- }
12851
- }
12852
- return lines.join("\n");
12853
- }
12854
- function formatDiffCodeBlock(diff) {
12855
- return `\`\`\`diff
12856
- ${diff}
12857
- \`\`\``;
12858
- }
12859
- function buildProposalKeyboard(insightId, category) {
12860
- if (category === "codebase") {
12861
- return [
12862
- [
12863
- { label: "Show Diff", data: `evolve:diff:${insightId}`, style: "primary" },
12864
- { label: "Discuss", data: `evolve:discuss:${insightId}` },
12865
- { label: "Reject", data: `evolve:reject:${insightId}`, style: "danger" }
12866
- ]
12867
- ];
12868
- }
12869
- return [
12870
- [
12871
- { label: "Apply", data: `evolve:apply:${insightId}`, style: "success" },
12872
- { label: "Discuss", data: `evolve:discuss:${insightId}` },
12873
- { label: "Reject", data: `evolve:reject:${insightId}`, style: "danger" }
12874
- ]
12875
- ];
12876
- }
12877
- function buildEvolveOnboardingKeyboard() {
12878
- return [
12879
- [{ label: "\u2705 Enable Self-Learning", data: "evolve:toggle", style: "success" }],
12880
- [{ label: "One-Time Analysis", data: "evolve:analyze" }]
12881
- ];
12882
- }
12883
- function buildEvolveMenuKeyboard(ctx) {
12884
- const reviewLabel = ctx.pendingProposals > 0 ? `Review (${ctx.pendingProposals})` : "Review";
12885
- return [
12886
- [
12887
- { label: "\u{1F50D} Analyze Now", data: "evolve:analyze", style: "success" },
12888
- ctx.pendingProposals > 0 ? { label: reviewLabel, data: "evolve:review", style: "primary" } : { label: reviewLabel, data: "evolve:review" }
12889
- ],
12890
- [
12891
- { label: "Stats", data: "evolve:stats" },
12892
- { label: "History", data: "evolve:history" }
12893
- ],
12894
- [
12895
- { label: "Model", data: "evolve:model" },
12896
- { label: "Undo", data: "evolve:undo", style: "danger" },
12897
- { label: "\u23F8 Disable", data: "evolve:toggle", style: "danger" }
12898
- ]
12899
- ];
12900
- }
12901
- function buildUndoKeyboard(insights) {
12902
- return insights.map((ins) => [
12903
- { label: `Undo #${ins.id}: ${ins.insight}`, data: `evolve:undo:${ins.id}`, style: "danger" }
12904
- ]);
12905
- }
12906
- function buildModelKeyboard(currentMode) {
12907
- const modes = [
12908
- { label: "Auto", mode: "auto" },
12909
- { label: "Pinned", mode: "pinned" },
12910
- { label: "Cheap", mode: "cheap" }
12911
- ];
12912
- return [
12913
- modes.map(({ label: label2, mode }) => ({
12914
- label: mode === currentMode ? `[${label2}]` : label2,
12915
- data: `evolve:model:${mode}`,
12916
- ...mode === currentMode ? { style: "primary" } : {}
12917
- }))
12918
- ];
12919
- }
12920
- var init_propose = __esm({
12921
- "src/reflection/propose.ts"() {
12922
- "use strict";
12923
- }
12924
- });
12925
-
12926
13149
  // src/router.ts
12927
13150
  import { readFile as readFile5, writeFile as writeFile3, unlink as unlink2, mkdir as mkdir2, readdir as readdir3, stat } from "fs/promises";
12928
13151
  import { existsSync as existsSync19 } from "fs";
@@ -13235,6 +13458,28 @@ function stopAllSideQuests(chatId) {
13235
13458
  }
13236
13459
  }
13237
13460
  }
13461
+ async function sendCurrentProposal(chatId, channel) {
13462
+ const { getReviewSession: getReviewSession2, getInsightById: getInsightById2, deleteReviewSession: deleteReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
13463
+ const { formatProposalCardWithProgress: formatProposalCardWithProgress2, buildProposalKeyboard: buildProposalKeyboard2, buildReviewCompleteMessage: buildReviewCompleteMessage2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
13464
+ const session2 = getReviewSession2(getDb(), chatId);
13465
+ if (!session2) return;
13466
+ if (session2.currentIndex >= session2.insightIds.length) {
13467
+ const summary = buildReviewCompleteMessage2(session2.results);
13468
+ deleteReviewSession2(getDb(), chatId);
13469
+ await channel.sendText(chatId, summary, { parseMode: "plain" });
13470
+ return;
13471
+ }
13472
+ const insightId = session2.insightIds[session2.currentIndex];
13473
+ const insight = getInsightById2(getDb(), insightId);
13474
+ if (!insight || insight.status !== "pending") {
13475
+ const { advanceReviewSession: advanceReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
13476
+ advanceReviewSession2(getDb(), chatId, insightId, "skipped");
13477
+ return sendCurrentProposal(chatId, channel);
13478
+ }
13479
+ const card = formatProposalCardWithProgress2(insight, session2.currentIndex, session2.insightIds.length);
13480
+ const kb = buildProposalKeyboard2(insight.id, insight.category);
13481
+ await channel.sendKeyboard(chatId, card, kb);
13482
+ }
13238
13483
  async function handleResponseExhaustion(responseText, chatId, msg, channel) {
13239
13484
  const raw = responseText.replace(/\n\n🧠 \[.+$/, "").trim();
13240
13485
  if (raw.length > 300 || !isExhaustedMessage(raw)) return false;
@@ -14838,32 +15083,43 @@ Message: "${testMsg}"`, { parseMode: "plain" });
14838
15083
  break;
14839
15084
  }
14840
15085
  case "reflect": {
14841
- const { runAnalysis: runAnalysis2 } = await Promise.resolve().then(() => (init_analyze(), analyze_exports));
14842
- const { formatNightlySummary: formatNightlySummary2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
14843
- await channel.sendText(chatId, "Analyzing recent interactions...", { parseMode: "plain" });
15086
+ const lastReflect = lastReflectTime.get(chatId);
15087
+ if (lastReflect && Date.now() - lastReflect < REFLECT_COOLDOWN_MS) {
15088
+ const remaining = Math.ceil((REFLECT_COOLDOWN_MS - (Date.now() - lastReflect)) / 1e3);
15089
+ await channel.sendText(chatId, `Please wait ${remaining}s before reflecting again.`, { parseMode: "plain" });
15090
+ break;
15091
+ }
15092
+ const { getMessagePairCount: getMessagePairCount2 } = await Promise.resolve().then(() => (init_session_log(), session_log_exports));
15093
+ const pairCount = getMessagePairCount2(chatId);
15094
+ if (pairCount < 2) {
15095
+ await channel.sendText(chatId, "Need more conversation to analyze \u2014 at least 2 message exchanges.", { parseMode: "plain" });
15096
+ break;
15097
+ }
15098
+ lastReflectTime.set(chatId, Date.now());
15099
+ await channel.sendText(chatId, "Step 1/3: Summarizing conversation...", { parseMode: "plain" });
15100
+ try {
15101
+ const { summarizeSession: summarizeSession2 } = await Promise.resolve().then(() => (init_summarize(), summarize_exports));
15102
+ await summarizeSession2(chatId, false);
15103
+ } catch (e) {
15104
+ await channel.sendText(chatId, `Summarization failed: ${e}`, { parseMode: "plain" });
15105
+ break;
15106
+ }
15107
+ await channel.sendText(chatId, "Step 2/3: Analyzing for insights...", { parseMode: "plain" });
14844
15108
  try {
15109
+ const { runAnalysis: runAnalysis2 } = await Promise.resolve().then(() => (init_analyze(), analyze_exports));
14845
15110
  const insights = await runAnalysis2(chatId, { force: true });
14846
15111
  if (insights.length === 0) {
14847
- const { getReflectionStatus: getRefStatus } = await Promise.resolve().then(() => (init_store4(), store_exports4));
14848
- const isFrozen = getRefStatus(getDb(), chatId) === "frozen";
14849
- if (isFrozen) {
14850
- const msg2 = "No insights found. Self-learning is disabled \u2014\nenable it with /evolve so feedback signals are\ncaptured automatically between analyses.";
14851
- if (typeof channel.sendKeyboard === "function") {
14852
- await channel.sendKeyboard(chatId, msg2, [[
14853
- { label: "Open /evolve", data: "evolve:menu", style: "primary" }
14854
- ]]);
14855
- } else {
14856
- await channel.sendText(chatId, msg2, { parseMode: "plain" });
14857
- }
14858
- } else {
14859
- await channel.sendText(chatId, "No new insights from recent interactions.", { parseMode: "plain" });
14860
- }
15112
+ await channel.sendText(chatId, "Step 3/3: No actionable improvements found in this session.", { parseMode: "plain" });
14861
15113
  } else {
14862
- const items = insights.map((ins, i) => ({ id: i + 1, category: ins.category, insight: ins.insight }));
14863
- await channel.sendText(chatId, formatNightlySummary2(items) + "\n\nUse /evolve to review and apply proposals.", { parseMode: "plain" });
15114
+ await channel.sendText(chatId, `Step 3/3: Found ${insights.length} proposal(s). Let's review them one by one.`, { parseMode: "plain" });
15115
+ const { getPendingInsights: getPendingInsights2, createReviewSession: createReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
15116
+ const pending = getPendingInsights2(getDb(), chatId);
15117
+ const insightIds = pending.slice(0, 5).map((p) => p.id);
15118
+ createReviewSession2(getDb(), chatId, insightIds);
15119
+ await sendCurrentProposal(chatId, channel);
14864
15120
  }
14865
15121
  } catch (e) {
14866
- await channel.sendText(chatId, `Analysis failed: ${e}`, { parseMode: "plain" });
15122
+ await channel.sendText(chatId, `Analysis failed: ${e}. You can review any pending proposals with /evolve.`, { parseMode: "plain" });
14867
15123
  }
14868
15124
  break;
14869
15125
  }
@@ -15317,7 +15573,8 @@ async function handleSideQuest(parentChatId, msg, channel) {
15317
15573
  backendId: adapter.id,
15318
15574
  model: model2 ?? adapter.defaultModel
15319
15575
  });
15320
- } catch {
15576
+ } catch (e) {
15577
+ log(`[reflection] Side quest signal detection error: ${e}`);
15321
15578
  }
15322
15579
  } catch (err) {
15323
15580
  typingActive = false;
@@ -16245,9 +16502,12 @@ ${PERM_MODES[chosen]}`,
16245
16502
  const pending2 = pendingInterrupts.get(targetChatId);
16246
16503
  if (pending2) {
16247
16504
  pendingInterrupts.delete(targetChatId);
16248
- handleSideQuest(targetChatId, pending2.msg, pending2.channel).catch(
16249
- (err) => error(`[router] Side quest error for ${targetChatId}:`, err)
16250
- );
16505
+ await channel.sendText(chatId, "\u{1F5FA} Launching side quest\u2026", { parseMode: "plain" });
16506
+ handleSideQuest(targetChatId, pending2.msg, pending2.channel).catch((err) => {
16507
+ error(`[router] Side quest error for ${targetChatId}:`, err);
16508
+ channel.sendText(targetChatId, `\u{1F5FA} Side quest failed: ${err.message}`, { parseMode: "plain" }).catch(() => {
16509
+ });
16510
+ });
16251
16511
  } else {
16252
16512
  await channel.sendText(chatId, "Main task finished \u2014 your message was already processed.", { parseMode: "plain" });
16253
16513
  }
@@ -16603,21 +16863,16 @@ Result: ${task.result.slice(0, 500)}` : ""
16603
16863
  break;
16604
16864
  }
16605
16865
  case "review": {
16606
- const { getPendingInsights: getPendingInsights2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
16607
- const { formatProposalCard: formatProposalCard2, buildProposalKeyboard: buildProposalKeyboard2 } = await Promise.resolve().then(() => (init_propose(), propose_exports));
16866
+ const { getPendingInsights: getPendingInsights2, createReviewSession: createReviewSession2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
16608
16867
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
16609
16868
  const pending = getPendingInsights2(getDb2(), chatId);
16610
16869
  if (pending.length === 0) {
16611
16870
  await channel.sendText(chatId, "No pending proposals.", { parseMode: "plain" });
16612
16871
  } else {
16613
- for (const insight of pending.slice(0, 5)) {
16614
- const card = formatProposalCard2(insight);
16615
- const kb = buildProposalKeyboard2(insight.id, insight.category);
16616
- await channel.sendKeyboard(chatId, card, kb);
16617
- }
16618
- if (pending.length > 5) {
16619
- await channel.sendText(chatId, `${pending.length - 5} more proposals. Run /evolve again to see next batch.`, { parseMode: "plain" });
16620
- }
16872
+ const insightIds = pending.slice(0, 5).map((p) => p.id);
16873
+ createReviewSession2(getDb2(), chatId, insightIds);
16874
+ await channel.sendText(chatId, `${pending.length} proposal(s) ready. Let's review them one by one.`, { parseMode: "plain" });
16875
+ await sendCurrentProposal(chatId, channel);
16621
16876
  }
16622
16877
  break;
16623
16878
  }
@@ -16635,10 +16890,16 @@ Result: ${task.result.slice(0, 500)}` : ""
16635
16890
  const { applyInsight: applyInsight2 } = await Promise.resolve().then(() => (init_apply(), apply_exports));
16636
16891
  const result = await applyInsight2(parseInt(idStr, 10));
16637
16892
  await channel.sendText(chatId, result.message, { parseMode: "plain" });
16893
+ const { advanceReviewSession: arAdvance } = await Promise.resolve().then(() => (init_store4(), store_exports4));
16894
+ arAdvance(getDb(), chatId, parseInt(idStr, 10), "applied");
16895
+ await sendCurrentProposal(chatId, channel);
16638
16896
  break;
16639
16897
  }
16640
16898
  case "skip": {
16641
16899
  await channel.sendText(chatId, "Skipped \u2014 will show again next review.", { parseMode: "plain" });
16900
+ const { advanceReviewSession: skAdvance } = await Promise.resolve().then(() => (init_store4(), store_exports4));
16901
+ skAdvance(getDb(), chatId, parseInt(idStr, 10), "skipped");
16902
+ await sendCurrentProposal(chatId, channel);
16642
16903
  break;
16643
16904
  }
16644
16905
  case "discuss": {
@@ -16710,10 +16971,10 @@ Result: ${task.result.slice(0, 500)}` : ""
16710
16971
  join19(homedir6(), ".cc-claw", "workspace", "skills")
16711
16972
  ];
16712
16973
  try {
16713
- const { readdirSync: readdirSync7, statSync: statSync9 } = await import("fs");
16974
+ const { readdirSync: readdirSync8, statSync: statSync9 } = await import("fs");
16714
16975
  for (const dir of skillDirs) {
16715
16976
  if (!existsSync19(dir)) continue;
16716
- for (const entry of readdirSync7(dir)) {
16977
+ for (const entry of readdirSync8(dir)) {
16717
16978
  if (statSync9(join19(dir, entry)).isDirectory()) {
16718
16979
  targets.push({ label: `skills/${entry}`, path: `workspace/skills/${entry}/SKILL.md` });
16719
16980
  }
@@ -16788,14 +17049,20 @@ Pick a different file for this change. Identity files (SOUL/USER) shape personal
16788
17049
  break;
16789
17050
  }
16790
17051
  case "discuss-back": {
16791
- const { getInsightById: bkIns } = await Promise.resolve().then(() => (init_store4(), store_exports4));
16792
- const { formatProposalCard: bkCard, buildProposalKeyboard: bkKb } = await Promise.resolve().then(() => (init_propose(), propose_exports));
17052
+ const { getInsightById: bkIns, getReviewSession: bkSession } = await Promise.resolve().then(() => (init_store4(), store_exports4));
17053
+ const { formatProposalCardWithProgress: formatProposalCardWithProgress2, formatProposalCard: bkCardFn, buildProposalKeyboard: bkKb } = await Promise.resolve().then(() => (init_propose(), propose_exports));
16793
17054
  const { getDb: bkDb } = await Promise.resolve().then(() => (init_store5(), store_exports5));
16794
17055
  const bkInsight = bkIns(bkDb(), parseInt(idStr, 10));
16795
17056
  if (bkInsight && bkInsight.status === "pending") {
16796
- const card = bkCard(bkInsight);
17057
+ const session2 = bkSession(bkDb(), chatId);
16797
17058
  const kb = bkKb(bkInsight.id, bkInsight.category);
16798
- await channel.sendKeyboard(chatId, card, kb);
17059
+ if (session2) {
17060
+ const card = formatProposalCardWithProgress2(bkInsight, session2.currentIndex, session2.insightIds.length);
17061
+ await channel.sendKeyboard(chatId, card, kb);
17062
+ } else {
17063
+ const card = bkCardFn(bkInsight);
17064
+ await channel.sendKeyboard(chatId, card, kb);
17065
+ }
16799
17066
  }
16800
17067
  break;
16801
17068
  }
@@ -16804,6 +17071,9 @@ Pick a different file for this change. Identity files (SOUL/USER) shape personal
16804
17071
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
16805
17072
  updateInsightStatus2(getDb2(), parseInt(idStr, 10), "rejected");
16806
17073
  await channel.sendText(chatId, "Rejected. Won't propose similar changes.", { parseMode: "plain" });
17074
+ const { advanceReviewSession: rjAdvance } = await Promise.resolve().then(() => (init_store4(), store_exports4));
17075
+ rjAdvance(getDb2(), chatId, parseInt(idStr, 10), "rejected");
17076
+ await sendCurrentProposal(chatId, channel);
16807
17077
  break;
16808
17078
  }
16809
17079
  case "stats": {
@@ -17457,7 +17727,7 @@ Use /skills <page> to navigate (e.g. /skills 2)` : "";
17457
17727
  const header2 = totalPages > 1 ? `${skills2.length} skills (page ${safePage}/${totalPages}). Select one to invoke:` : `${skills2.length} skills available. Select one to invoke:`;
17458
17728
  await channel.sendKeyboard(chatId, header2, buttons);
17459
17729
  }
17460
- var PERM_MODES, VERBOSE_LEVELS, HELP_CATEGORIES, USAGE_WINDOW_MAP, MEDIA_INCOMING_PATH, TONE_PATTERNS, pendingInterrupts, bypassBusyCheck, activeSideQuests, MAX_SIDE_QUESTS, pendingFallbackMessages, dashboardClawWarnings, pendingSummaryUndo, pendingNewchatUndo, CLI_INSTALL_HINTS, BLOCKED_PATH_PATTERNS2, ALLOWED_REACTION_EMOJIS, SKILLS_PER_PAGE;
17730
+ var PERM_MODES, VERBOSE_LEVELS, HELP_CATEGORIES, USAGE_WINDOW_MAP, MEDIA_INCOMING_PATH, TONE_PATTERNS, pendingInterrupts, bypassBusyCheck, activeSideQuests, MAX_SIDE_QUESTS, pendingFallbackMessages, dashboardClawWarnings, lastReflectTime, REFLECT_COOLDOWN_MS, pendingSummaryUndo, pendingNewchatUndo, CLI_INSTALL_HINTS, BLOCKED_PATH_PATTERNS2, ALLOWED_REACTION_EMOJIS, SKILLS_PER_PAGE;
17461
17731
  var init_router = __esm({
17462
17732
  "src/router.ts"() {
17463
17733
  "use strict";
@@ -17600,6 +17870,8 @@ var init_router = __esm({
17600
17870
  MAX_SIDE_QUESTS = 2;
17601
17871
  pendingFallbackMessages = /* @__PURE__ */ new Map();
17602
17872
  dashboardClawWarnings = /* @__PURE__ */ new Map();
17873
+ lastReflectTime = /* @__PURE__ */ new Map();
17874
+ REFLECT_COOLDOWN_MS = 12e4;
17603
17875
  pendingSummaryUndo = /* @__PURE__ */ new Map();
17604
17876
  pendingNewchatUndo = /* @__PURE__ */ new Map();
17605
17877
  CLI_INSTALL_HINTS = {
@@ -18614,6 +18886,8 @@ async function main() {
18614
18886
  const shutdown = async (signal) => {
18615
18887
  log(`[cc-claw] Received ${signal}, shutting down...`);
18616
18888
  try {
18889
+ const { stopAllActiveAgents: stopAllActiveAgents2 } = await Promise.resolve().then(() => (init_agent(), agent_exports));
18890
+ stopAllActiveAgents2();
18617
18891
  stopAllHeartbeats();
18618
18892
  stopHealthMonitor3();
18619
18893
  stopMonitor();
@@ -18784,7 +19058,7 @@ __export(service_exports, {
18784
19058
  serviceStatus: () => serviceStatus,
18785
19059
  uninstallService: () => uninstallService
18786
19060
  });
18787
- import { existsSync as existsSync24, mkdirSync as mkdirSync9, writeFileSync as writeFileSync7, unlinkSync as unlinkSync4 } from "fs";
19061
+ import { existsSync as existsSync24, mkdirSync as mkdirSync9, writeFileSync as writeFileSync7, unlinkSync as unlinkSync5 } from "fs";
18788
19062
  import { execFileSync as execFileSync2, execSync as execSync7 } from "child_process";
18789
19063
  import { homedir as homedir8, platform } from "os";
18790
19064
  import { join as join23, dirname as dirname4 } from "path";
@@ -18881,7 +19155,7 @@ function uninstallMacOS() {
18881
19155
  execFileSync2("launchctl", ["unload", PLIST_PATH]);
18882
19156
  } catch {
18883
19157
  }
18884
- unlinkSync4(PLIST_PATH);
19158
+ unlinkSync5(PLIST_PATH);
18885
19159
  console.log(" Service uninstalled.");
18886
19160
  }
18887
19161
  function formatUptime(seconds) {
@@ -18969,7 +19243,7 @@ function uninstallLinux() {
18969
19243
  execFileSync2("systemctl", ["--user", "disable", "cc-claw"]);
18970
19244
  } catch {
18971
19245
  }
18972
- unlinkSync4(UNIT_PATH);
19246
+ unlinkSync5(UNIT_PATH);
18973
19247
  execFileSync2("systemctl", ["--user", "daemon-reload"]);
18974
19248
  console.log(" Service uninstalled.");
18975
19249
  }
@@ -19381,14 +19655,18 @@ async function doctorCommand(globalOpts, localOpts) {
19381
19655
  checks.push({ name: "Environment", status: "error", message: "No .env found", fix: "cc-claw setup" });
19382
19656
  }
19383
19657
  const CLI_BINARIES = { claude: "claude", gemini: "gemini", codex: "codex", cursor: "agent" };
19658
+ let installedBackends = 0;
19384
19659
  for (const [label2, binary] of Object.entries(CLI_BINARIES)) {
19385
19660
  try {
19386
19661
  const path = execFileSync3("which", [binary], { encoding: "utf-8", timeout: 5e3 }).trim();
19387
19662
  checks.push({ name: `${label2} CLI`, status: "ok", message: path });
19663
+ installedBackends++;
19388
19664
  } catch {
19389
- checks.push({ name: `${label2} CLI`, status: "warning", message: "not found in PATH" });
19390
19665
  }
19391
19666
  }
19667
+ if (installedBackends === 0) {
19668
+ checks.push({ name: "Backend CLIs", status: "error", message: "No backend CLIs found. Install at least one (claude, gemini, codex, or cursor agent)." });
19669
+ }
19392
19670
  try {
19393
19671
  const { isDaemonRunning: isDaemonRunning2, apiGet: apiGet2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
19394
19672
  const running = await isDaemonRunning2();
@@ -19418,6 +19696,13 @@ async function doctorCommand(globalOpts, localOpts) {
19418
19696
  } catch {
19419
19697
  checks.push({ name: "Daemon", status: "warning", message: "could not probe" });
19420
19698
  }
19699
+ try {
19700
+ const latest = execFileSync3("npm", ["view", "cc-claw", "version"], { encoding: "utf-8", timeout: 1e4 }).trim();
19701
+ if (latest && latest !== VERSION) {
19702
+ checks.push({ name: "Update available", status: "warning", message: `v${latest} available (current: v${VERSION})`, fix: "npm install -g cc-claw@latest" });
19703
+ }
19704
+ } catch {
19705
+ }
19421
19706
  const tokenPath = `${DATA_PATH}/api-token`;
19422
19707
  if (existsSync26(tokenPath)) {
19423
19708
  try {
@@ -22047,6 +22332,7 @@ __export(evolve_exports, {
22047
22332
  evolveOff: () => evolveOff,
22048
22333
  evolveOn: () => evolveOn,
22049
22334
  evolveReject: () => evolveReject,
22335
+ evolveSettings: () => evolveSettings,
22050
22336
  evolveStats: () => evolveStats,
22051
22337
  evolveStatus: () => evolveStatus,
22052
22338
  evolveUndo: () => evolveUndo
@@ -22403,6 +22689,60 @@ async function evolveHistory(globalOpts, opts) {
22403
22689
  return lines.join("\n");
22404
22690
  });
22405
22691
  }
22692
+ async function evolveSettings(globalOpts, opts) {
22693
+ ensureDb3();
22694
+ const { openDatabaseReadOnly: openDatabaseReadOnly2 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
22695
+ const chatId = resolveChatId(globalOpts);
22696
+ const hasUpdates = opts.perFileCap !== void 0 || opts.backupRetentionDays !== void 0;
22697
+ if (hasUpdates) {
22698
+ await ensureDaemon();
22699
+ const { apiPost: apiPost2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
22700
+ const body = {};
22701
+ if (opts.perFileCap !== void 0) {
22702
+ const val = parseInt(opts.perFileCap, 10);
22703
+ if (isNaN(val) || val < 1) {
22704
+ outputError("INVALID_VALUE", "per-file-cap must be a positive integer.");
22705
+ process.exit(1);
22706
+ }
22707
+ body.perFileCap = val;
22708
+ }
22709
+ if (opts.backupRetentionDays !== void 0) {
22710
+ const val = parseInt(opts.backupRetentionDays, 10);
22711
+ if (isNaN(val) || val < 1) {
22712
+ outputError("INVALID_VALUE", "backup-retention-days must be a positive integer.");
22713
+ process.exit(1);
22714
+ }
22715
+ body.backupRetentionDays = val;
22716
+ }
22717
+ const res = await apiPost2("/api/evolve/settings", { chatId, ...body });
22718
+ if (res.ok) {
22719
+ output(res.data, () => `
22720
+ ${success("Reflection settings updated.")}
22721
+ `);
22722
+ } else {
22723
+ outputError("SETTINGS_FAILED", `Failed: ${JSON.stringify(res.data)}`);
22724
+ process.exit(1);
22725
+ }
22726
+ } else {
22727
+ const readDb = openDatabaseReadOnly2();
22728
+ const { getReflectionSettings: getReflectionSettings2 } = await Promise.resolve().then(() => (init_store4(), store_exports4));
22729
+ const settings = getReflectionSettings2(readDb, chatId);
22730
+ readDb.close();
22731
+ output(settings, () => {
22732
+ const lines = [
22733
+ "",
22734
+ divider("Reflection Settings"),
22735
+ "",
22736
+ kvLine("Per-file cap", `${settings.perFileCap} changes/day per file`),
22737
+ kvLine("Backup retention", `${settings.backupRetentionDays} days`),
22738
+ "",
22739
+ muted("Update with: cc-claw evolve settings --per-file-cap <n> --backup-retention-days <n>"),
22740
+ ""
22741
+ ];
22742
+ return lines.join("\n");
22743
+ });
22744
+ }
22745
+ }
22406
22746
  var init_evolve = __esm({
22407
22747
  "src/cli/commands/evolve.ts"() {
22408
22748
  "use strict";
@@ -23318,6 +23658,10 @@ evolve.command("history").description("Insight history").option("--status <statu
23318
23658
  const { evolveHistory: evolveHistory2 } = await Promise.resolve().then(() => (init_evolve(), evolve_exports));
23319
23659
  await evolveHistory2(program.opts(), opts);
23320
23660
  });
23661
+ evolve.command("settings").description("View or update reflection settings").option("--per-file-cap <n>", "Max applies per file per day").option("--backup-retention-days <n>", "Days to keep backup files").action(async (opts) => {
23662
+ const { evolveSettings: evolveSettings2 } = await Promise.resolve().then(() => (init_evolve(), evolve_exports));
23663
+ await evolveSettings2(program.opts(), opts);
23664
+ });
23321
23665
  program.command("start", { hidden: true }).description("Run the bot in the foreground (use 'service start' for background daemon)").action(async () => {
23322
23666
  await Promise.resolve().then(() => (init_index(), index_exports));
23323
23667
  });
@@ -23333,6 +23677,20 @@ program.command("setup").description("Interactive configuration wizard").action(
23333
23677
  await Promise.resolve().then(() => (init_setup(), setup_exports));
23334
23678
  });
23335
23679
  async function run(argv = process.argv) {
23680
+ if (argv.includes("--version") || argv.includes("-V")) {
23681
+ console.log(VERSION);
23682
+ try {
23683
+ const { execFileSync: execFileSync5 } = await import("child_process");
23684
+ const latest = execFileSync5("npm", ["view", "cc-claw", "version"], { encoding: "utf-8", timeout: 1e4 }).trim();
23685
+ if (latest && latest !== VERSION) {
23686
+ console.log(`
23687
+ Update available: v${latest} (current: v${VERSION})`);
23688
+ console.log(`Run: npm install -g cc-claw@latest`);
23689
+ }
23690
+ } catch {
23691
+ }
23692
+ return;
23693
+ }
23336
23694
  if (argv.includes("--ai")) {
23337
23695
  const { generateAiSkill: generateAiSkill2, installAiSkill: installAiSkill2 } = await Promise.resolve().then(() => (init_ai_skill(), ai_skill_exports));
23338
23696
  if (argv.includes("--install")) {