@cachly-dev/mcp-server 0.5.5 → 0.5.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cachly-dev/mcp-server",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "MCP server – manage cachly.dev instances from any AI assistant (Copilot, Claude, Cursor, Windsurf)",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -71,6 +71,39 @@ const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
71
71
  */
72
72
  const EMBED_PROVIDER = (process.env.CACHLY_EMBED_PROVIDER ?? detectEmbedProvider()).toLowerCase();
73
73
 
74
+ // ── Confidence Decay Config (all values configurable via env) ─────────────────
75
+ const CONFIDENCE_WARN_DAYS = Number(process.env.CACHLY_CONFIDENCE_WARN_DAYS ?? 5); // → 0.7
76
+ const CONFIDENCE_STALE_DAYS = Number(process.env.CACHLY_CONFIDENCE_STALE_DAYS ?? 10); // → 0.5
77
+ const CONFIDENCE_WARN_VALUE = 0.7;
78
+ const CONFIDENCE_STALE_VALUE = 0.5;
79
+
80
+ /** Calculate current confidence for a lesson based on how long since last verified. */
81
+ function calculateConfidence(lesson: { verified_at?: string; ts: string; recall_count?: number }): number {
82
+ const ref = lesson.verified_at ?? lesson.ts;
83
+ const ageMs = Date.now() - new Date(ref).getTime();
84
+ const ageDays = ageMs / 86400000;
85
+ if (ageDays >= CONFIDENCE_STALE_DAYS) return CONFIDENCE_STALE_VALUE;
86
+ if (ageDays >= CONFIDENCE_WARN_DAYS) return CONFIDENCE_WARN_VALUE;
87
+ // Linear interpolation between fresh (1.0) and warn threshold
88
+ return 1.0 - (ageDays / CONFIDENCE_WARN_DAYS) * (1.0 - CONFIDENCE_WARN_VALUE);
89
+ }
90
+
91
+ /** Render a confidence badge string. */
92
+ function confidenceBadge(confidence: number, ageDays: number): string {
93
+ if (confidence >= 0.9) return '✅';
94
+ if (confidence >= 0.7) return `⚠️ (${Math.round(ageDays)}d old, confidence ${(confidence * 100).toFixed(0)}% — verify before applying)`;
95
+ return `🔴 STALE (${Math.round(ageDays)}d old, confidence ${(confidence * 100).toFixed(0)}% — likely outdated!)`;
96
+ }
97
+
98
+ /** Category-specific required fields for structured lessons. */
99
+ const STRUCTURED_TEMPLATES: Record<string, { required: string[]; hint: string }> = {
100
+ 'deploy': { required: ['commands'], hint: 'deploy:* needs commands[]' },
101
+ 'bash': { required: ['commands'], hint: 'bash:* needs commands[]' },
102
+ 'infra': { required: ['commands'], hint: 'infra:* needs commands[] + verified on real system' },
103
+ 'pricing': { required: [], hint: 'pricing:* — add context with source (e.g. "Stripe Dashboard Apr 2026")' },
104
+ 'stripe': { required: [], hint: 'stripe:* — add context with Stripe API version' },
105
+ };
106
+
74
107
  /** Fast content hash for index invalidation (not cryptographic, just change detection) */
75
108
  function simpleHash(text: string): string {
76
109
  return createHash('md5').update(text).digest('hex').slice(0, 12);
@@ -1277,7 +1310,9 @@ const TOOLS = [
1277
1310
  'Fields: topic (short slug like "deploy:web"), outcome ("success"|"failure"), ' +
1278
1311
  'what_worked (what solved it), what_failed (what did NOT work), context (extra details). ' +
1279
1312
  'Supports structured metadata: severity, file_paths (files involved), commands (working commands), tags. ' +
1280
- 'Deduplication: if a lesson for this topic already exists, it is updated instead of duplicated. ' +
1313
+ 'Deduplication: if a lesson for this topic already exists, it is updated with full audit trail. ' +
1314
+ 'Contradiction detection: warns if new outcome conflicts with existing lesson outcome. ' +
1315
+ 'Confidence: lesson starts at 1.0, decays after 5d (→0.7) and 10d (→0.5) without recall. ' +
1281
1316
  'Example: learn_from_attempts(topic="deploy:api", outcome="success", what_worked="nohup docker compose up -d --build", what_failed="docker compose up hangs on SSH timeout", severity="critical", commands=["nohup docker compose up -d --build"])',
1282
1317
  inputSchema: {
1283
1318
  type: 'object',
@@ -1317,7 +1352,9 @@ const TOOLS = [
1317
1352
  description:
1318
1353
  'Recall the best known solution for a topic from past lessons. ' +
1319
1354
  'Call this BEFORE attempting any task that might have been done before. ' +
1320
- 'Returns the most recent successful lesson for the topic, or a summary of attempts. ' +
1355
+ 'Returns the most recent successful lesson for the topic, with confidence indicator. ' +
1356
+ '⚠️ badge = lesson is >5d old (verify before applying). 🔴 = >10d old (likely stale!). ' +
1357
+ 'Recalling a lesson resets its confidence clock to 1.0 (marks as recently verified). ' +
1321
1358
  'Example: recall_best_solution(topic="deploy:web") → returns the working deploy command.',
1322
1359
  inputSchema: {
1323
1360
  type: 'object',
@@ -2535,21 +2572,59 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2535
2572
  const redis = await getConnection(instance_id);
2536
2573
  const ts = new Date().toISOString();
2537
2574
 
2538
- // ── Deduplication: check for existing lesson with same topic ─────────────
2575
+ // ── Structured template hints ──────────────────────────────────────────
2576
+ const category = topic.split(':')[0];
2577
+ const template = STRUCTURED_TEMPLATES[category];
2578
+ const templateWarnings: string[] = [];
2579
+ if (template) {
2580
+ for (const req of template.required) {
2581
+ if (req === 'commands' && commands.length === 0) {
2582
+ templateWarnings.push(`📋 ${template.hint}`);
2583
+ }
2584
+ }
2585
+ }
2586
+
2587
+ // ── Deduplication + audit trail ────────────────────────────────────────
2539
2588
  let isUpdate = false;
2540
2589
  let recallCount = 0;
2541
- if (outcome === 'success') {
2542
- const existing = await redis.get(`cachly:lesson:best:${topic}`);
2543
- if (existing) {
2544
- try {
2545
- const prev = JSON.parse(existing) as { recall_count?: number };
2546
- recallCount = (prev.recall_count ?? 0); // preserve existing recall count
2547
- isUpdate = true;
2548
- } catch { /* ignore parse error */ }
2549
- }
2590
+ let auditTrail: Array<{ ts: string; action: string; prev_outcome?: string }> = [];
2591
+ const existingRaw = await redis.get(`cachly:lesson:best:${topic}`);
2592
+ if (existingRaw) {
2593
+ try {
2594
+ const prev = JSON.parse(existingRaw) as {
2595
+ recall_count?: number;
2596
+ outcome?: string;
2597
+ audit_trail?: Array<{ ts: string; action: string; prev_outcome?: string }>;
2598
+ };
2599
+ recallCount = prev.recall_count ?? 0;
2600
+ auditTrail = prev.audit_trail ?? [];
2601
+ auditTrail.push({ ts, action: 'updated', prev_outcome: prev.outcome });
2602
+ if (auditTrail.length > 20) auditTrail = auditTrail.slice(-20);
2603
+ isUpdate = true;
2604
+
2605
+ // ── Contradiction detection ─────────────────────────────────────────
2606
+ const contradictionWarning: string[] = [];
2607
+ if (prev.outcome === 'success' && outcome === 'failure') {
2608
+ contradictionWarning.push(
2609
+ `⚠️ **Contradiction detected!** Existing lesson has outcome: \`success\`, but you're storing \`failure\`.`,
2610
+ `The existing "success" lesson will be preserved. Only the audit trail is updated.`,
2611
+ `If you meant to mark this as failed permanently, store a new lesson with a distinct topic slug.`,
2612
+ );
2613
+ } else if (prev.outcome === 'failure' && outcome === 'success') {
2614
+ contradictionWarning.push(
2615
+ `✅ **Conflict resolved!** Previous lesson was \`failure\` — now overwriting with \`success\`.`,
2616
+ );
2617
+ }
2618
+ if (contradictionWarning.length > 0) {
2619
+ // Store contradiction audit but don't block
2620
+ auditTrail[auditTrail.length - 1].action = 'contradiction-resolved';
2621
+ }
2622
+ } catch { /* ignore parse error */ }
2623
+ } else {
2624
+ auditTrail = [{ ts, action: 'created' }];
2550
2625
  }
2551
2626
 
2552
- const lesson = JSON.stringify({
2627
+ const lessonObj = {
2553
2628
  topic,
2554
2629
  outcome,
2555
2630
  what_worked,
@@ -2561,18 +2636,32 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2561
2636
  tags,
2562
2637
  recall_count: recallCount,
2563
2638
  ts,
2564
- version: 2,
2565
- });
2639
+ verified_at: outcome === 'success' || outcome === 'partial' ? ts : undefined,
2640
+ confidence: 1.0,
2641
+ audit_trail: auditTrail,
2642
+ version: 3,
2643
+ };
2644
+ const lesson = JSON.stringify(lessonObj);
2566
2645
 
2567
- // Always append to the history list
2646
+ // Always append to the history list (audit log)
2568
2647
  const listKey = `cachly:lessons:${topic}`;
2569
2648
  await redis.rpush(listKey, lesson);
2570
2649
 
2571
- // Update best key only for success/partial (not for failures)
2650
+ // Update best key for success/partial; for failure only update if no success exists
2572
2651
  if (outcome === 'success' || outcome === 'partial') {
2573
2652
  await redis.set(`cachly:lesson:best:${topic}`, lesson);
2653
+ } else if (!existingRaw) {
2654
+ await redis.set(`cachly:lesson:best:${topic}`, lesson);
2574
2655
  }
2575
2656
 
2657
+ // Track in decision log for session replay
2658
+ try {
2659
+ const dlKey = 'cachly:session:decision-log';
2660
+ const dlEntry = JSON.stringify({ ts, topic, outcome, what_worked: what_worked.slice(0, 120) });
2661
+ await redis.rpush(dlKey, dlEntry);
2662
+ await redis.ltrim(dlKey, -50, -1);
2663
+ } catch { /* non-critical */ }
2664
+
2576
2665
  const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
2577
2666
  const sevEmoji = severity === 'critical' ? '🔴' : severity === 'major' ? '🟡' : '🟢';
2578
2667
  const action = isUpdate ? 'updated' : 'stored';
@@ -2587,8 +2676,9 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2587
2676
  tags.length > 0 ? `**Tags:** ${tags.map(t => `#${t}`).join(' ')}` : '',
2588
2677
  ``,
2589
2678
  isUpdate
2590
- ? `♻️ Updated existing lesson (preserved recall count: ${recallCount})`
2679
+ ? `♻️ Updated (recall count: ${recallCount} · audit entries: ${auditTrail.length})`
2591
2680
  : `💡 Recall later with \`recall_best_solution(topic="${topic}")\``,
2681
+ ...templateWarnings,
2592
2682
  ].filter(l => l !== '').join('\n');
2593
2683
  }
2594
2684
 
@@ -2601,16 +2691,45 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2601
2691
  if (best) {
2602
2692
  const lesson = JSON.parse(best) as {
2603
2693
  topic: string; outcome: string; what_worked: string; what_failed?: string;
2604
- context?: string; ts: string; severity?: string; file_paths?: string[];
2605
- commands?: string[]; tags?: string[]; recall_count?: number;
2694
+ context?: string; ts: string; verified_at?: string; severity?: string;
2695
+ file_paths?: string[]; commands?: string[]; tags?: string[];
2696
+ recall_count?: number; audit_trail?: unknown[];
2697
+ };
2698
+
2699
+ // ── Confidence decay check ───────────────────────────────────────────
2700
+ const confidence = calculateConfidence(lesson);
2701
+ const ref = lesson.verified_at ?? lesson.ts;
2702
+ const ageDays = (Date.now() - new Date(ref).getTime()) / 86400000;
2703
+ const badge = confidenceBadge(confidence, ageDays);
2704
+
2705
+ // Recall resets verified_at (confidence clock restart)
2706
+ const updatedLesson = {
2707
+ ...lesson,
2708
+ recall_count: (lesson.recall_count ?? 0) + 1,
2709
+ verified_at: new Date().toISOString(),
2710
+ confidence: 1.0,
2606
2711
  };
2607
- // Increment recall_count
2608
- const updatedLesson = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1 };
2609
2712
  await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(updatedLesson));
2610
2713
 
2611
2714
  const sevEmoji = lesson.severity === 'critical' ? '🔴' : lesson.severity === 'major' ? '🟡' : lesson.severity ? '🟢' : '';
2715
+ const auditSummary = (lesson.audit_trail ?? []).length > 1
2716
+ ? `_Audit: ${(lesson.audit_trail ?? []).length} changes · stored ${new Date(lesson.ts).toLocaleDateString('de-DE')}_`
2717
+ : '';
2718
+
2719
+ // "Remember when..." — emotional header for lessons > 60 days old
2720
+ const ageFromStoreDays = (Date.now() - new Date(lesson.ts).getTime()) / 86400000;
2721
+ const rememberWhen = ageFromStoreDays > 60
2722
+ ? `💭 _Remember when you solved this ${Math.round(ageFromStoreDays / 30)} months ago? Still works._`
2723
+ : '';
2724
+
2725
+ // "Never Google This Again" — suggest pinning after 3rd recall
2726
+ const suggestPin = updatedLesson.recall_count === 3 && !(lesson as { pinned?: boolean }).pinned
2727
+ ? `📌 **You've looked this up 3 times.** Consider pinning it for instant access: add \`pinned: true\` via \`learn_from_attempts\` to always surface it first.`
2728
+ : '';
2729
+
2612
2730
  return [
2613
- `✅ **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}× · ${new Date(lesson.ts).toLocaleDateString('de-DE')}`,
2731
+ rememberWhen,
2732
+ `${badge} **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}×`,
2614
2733
  ``,
2615
2734
  `**What worked:** ${lesson.what_worked}`,
2616
2735
  lesson.what_failed ? `**What failed (avoid this):** ${lesson.what_failed}` : '',
@@ -2618,6 +2737,8 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2618
2737
  (lesson.file_paths ?? []).length > 0 ? `**Files:** ${(lesson.file_paths ?? []).map((f: string) => `\`${f}\``).join(', ')}` : '',
2619
2738
  (lesson.commands ?? []).length > 0 ? `**Commands:** ${(lesson.commands ?? []).map((c: string) => `\`${c}\``).join(', ')}` : '',
2620
2739
  (lesson.tags ?? []).length > 0 ? `**Tags:** ${(lesson.tags ?? []).map((t: string) => `#${t}`).join(' ')}` : '',
2740
+ auditSummary,
2741
+ suggestPin,
2621
2742
  ].filter(l => l !== '').join('\n');
2622
2743
  }
2623
2744
 
@@ -2848,7 +2969,8 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2848
2969
  // 2. Fetch all lesson values for recency sorting + focus matching
2849
2970
  type Lesson = {
2850
2971
  topic: string; outcome: string; what_worked: string; what_failed?: string;
2851
- ts: string; severity?: string; recall_count?: number; tags?: string[];
2972
+ ts: string; verified_at?: string; severity?: string; recall_count?: number;
2973
+ tags?: string[]; confidence?: number; audit_trail?: unknown[];
2852
2974
  };
2853
2975
  const lessons: Lesson[] = [];
2854
2976
  for (const k of lessonKeys) {
@@ -2887,7 +3009,43 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2887
3009
  )
2888
3010
  : [];
2889
3011
 
2890
- // 6. Save session start marker
3012
+ // 6. Streak tracking
3013
+ let streakDays = 0;
3014
+ let streakRecord = 0;
3015
+ let streakMessage = '';
3016
+ try {
3017
+ const streakRaw = await redis.get('cachly:streak:current');
3018
+ const streak = streakRaw ? JSON.parse(streakRaw) as { days: number; last_date: string; record: number } : null;
3019
+ const today = new Date().toISOString().slice(0, 10);
3020
+ if (streak) {
3021
+ const lastDate = streak.last_date;
3022
+ const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
3023
+ if (lastDate === today) {
3024
+ // Already counted today
3025
+ streakDays = streak.days;
3026
+ streakRecord = streak.record;
3027
+ } else if (lastDate === yesterday) {
3028
+ // Continuing streak
3029
+ streakDays = streak.days + 1;
3030
+ streakRecord = Math.max(streakDays, streak.record);
3031
+ await redis.set('cachly:streak:current', JSON.stringify({ days: streakDays, last_date: today, record: streakRecord }));
3032
+ } else {
3033
+ // Streak broken
3034
+ streakDays = 1;
3035
+ streakRecord = streak.record;
3036
+ await redis.set('cachly:streak:current', JSON.stringify({ days: 1, last_date: today, record: streakRecord }));
3037
+ }
3038
+ } else {
3039
+ // First session ever
3040
+ streakDays = 1;
3041
+ streakRecord = 1;
3042
+ await redis.set('cachly:streak:current', JSON.stringify({ days: 1, last_date: today, record: 1 }));
3043
+ }
3044
+ if (streakDays >= 7) streakMessage = `🔥 **${streakDays}-day streak!** ${streakDays === streakRecord ? ' New record!' : `Best: ${streakRecord}d`}`;
3045
+ else if (streakDays > 1) streakMessage = `🔥 ${streakDays}-day streak`;
3046
+ } catch { /* non-critical */ }
3047
+
3048
+ // 7. Save session start marker
2891
3049
  await redis.set('cachly:session:current', JSON.stringify({
2892
3050
  started: new Date().toISOString(),
2893
3051
  focus,
@@ -2895,6 +3053,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2895
3053
 
2896
3054
  // ── Build briefing ──────────────────────────────────────────────────────
2897
3055
  const lines: string[] = ['🧠 **Session Briefing**', ''];
3056
+ if (streakMessage) lines.push(streakMessage, '');
2898
3057
 
2899
3058
  // Handoff from previous window (if any)
2900
3059
  const handoffRaw = await redis.get('cachly:session:handoff');
@@ -2977,6 +3136,35 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
2977
3136
  lines.push('');
2978
3137
  }
2979
3138
 
3139
+ // ── Stale / low-confidence lessons (confidence decay) ─────────────────
3140
+ const staleSuccessLessons = lessons.filter(l => {
3141
+ if (l.outcome !== 'success' && l.outcome !== 'partial') return false;
3142
+ return calculateConfidence(l) < CONFIDENCE_WARN_VALUE;
3143
+ });
3144
+ if (staleSuccessLessons.length > 0) {
3145
+ lines.push(`🔴 **Stale lessons** (not recalled in >${CONFIDENCE_WARN_DAYS}d — verify before applying):`);
3146
+ for (const l of staleSuccessLessons.slice(0, 4)) {
3147
+ const conf = calculateConfidence(l);
3148
+ const ageDays = Math.round((Date.now() - new Date(l.verified_at ?? l.ts).getTime()) / 86400000);
3149
+ const flag = conf < CONFIDENCE_STALE_VALUE ? '🔴' : '⚠️';
3150
+ lines.push(` ${flag} \`${l.topic}\` — ${ageDays}d stale, ${(conf * 100).toFixed(0)}% confidence`);
3151
+ }
3152
+ lines.push(` _Run \`recall_best_solution\` on these to reset their confidence clock._`);
3153
+ lines.push('');
3154
+ }
3155
+
3156
+ // ── Session Replay: show last session's decision log ──────────────────
3157
+ const lastSessionAny = lastSession as unknown as { decision_log?: Array<{ topic: string; outcome: string; what_worked: string }> } | null;
3158
+ if (lastSessionAny?.decision_log?.length) {
3159
+ const dl = lastSessionAny.decision_log;
3160
+ const successes = dl.filter(d => d.outcome === 'success');
3161
+ const failures = dl.filter(d => d.outcome === 'failure');
3162
+ lines.push(`🎬 **Last session decisions** (${dl.length} lessons stored):`);
3163
+ if (successes.length > 0) lines.push(` ✅ Worked: ${successes.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
3164
+ if (failures.length > 0) lines.push(` ❌ Failed: ${failures.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
3165
+ lines.push('');
3166
+ }
3167
+
2980
3168
  return lines.join('\n');
2981
3169
  }
2982
3170
 
@@ -3007,12 +3195,21 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3007
3195
  } catch { /* ignore */ }
3008
3196
  }
3009
3197
 
3198
+ // ── Session Replay: capture decision log ─────────────────────────────
3199
+ let decisionLog: Array<{ ts: string; topic: string; outcome: string; what_worked: string }> = [];
3200
+ try {
3201
+ const dlEntries = await redis.lrange('cachly:session:decision-log', 0, -1);
3202
+ decisionLog = dlEntries.map(e => JSON.parse(e) as { ts: string; topic: string; outcome: string; what_worked: string });
3203
+ await redis.del('cachly:session:decision-log');
3204
+ } catch { /* non-critical */ }
3205
+
3010
3206
  const sessionRecord = {
3011
3207
  ts: now.toISOString(),
3012
3208
  summary,
3013
3209
  files_changed,
3014
3210
  ...(lessons_learned !== undefined ? { lessons_learned } : {}),
3015
3211
  ...(durationMin !== undefined ? { duration_min: durationMin } : {}),
3212
+ ...(decisionLog.length > 0 ? { decision_log: decisionLog } : {}),
3016
3213
  };
3017
3214
 
3018
3215
  // Save as "last session"
@@ -3425,12 +3622,15 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3425
3622
  });
3426
3623
 
3427
3624
  // Load lessons for analysis
3428
- type Lesson = { topic: string; outcome: string; recall_count?: number; ts: string; severity?: string };
3429
- const lessons: Lesson[] = [];
3625
+ type DrLesson = {
3626
+ topic: string; outcome: string; recall_count?: number; ts: string;
3627
+ verified_at?: string; severity?: string; audit_trail?: unknown[];
3628
+ };
3629
+ const lessons: DrLesson[] = [];
3430
3630
  for (const k of lessonKeys) {
3431
3631
  const raw = await redis.get(k);
3432
3632
  if (!raw) continue;
3433
- try { lessons.push(JSON.parse(raw) as Lesson); } catch { /* skip */ }
3633
+ try { lessons.push(JSON.parse(raw) as DrLesson); } catch { /* skip */ }
3434
3634
  }
3435
3635
 
3436
3636
  // Last session
@@ -3446,6 +3646,10 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3446
3646
  const unusedLessons = lessons.filter(l => (l.recall_count ?? 0) === 0);
3447
3647
  // Critical lessons
3448
3648
  const criticalLessons = lessons.filter(l => l.severity === 'critical');
3649
+ // Confidence decay analysis
3650
+ const staleLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_STALE_VALUE);
3651
+ const warnLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) >= CONFIDENCE_STALE_VALUE && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
3652
+ const withAudit = lessons.filter(l => (l.audit_trail ?? []).length > 1);
3449
3653
 
3450
3654
  // Quality score (0-100)
3451
3655
  let score = 50;
@@ -3457,12 +3661,15 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3457
3661
  if (openFailures.length === 0) score += 5;
3458
3662
  const unusedRatio = lessons.length > 0 ? unusedLessons.length / lessons.length : 0;
3459
3663
  if (unusedRatio < 0.5) score += 10;
3664
+ if (staleLessons.length === 0) score += 5;
3460
3665
 
3461
3666
  const scoreEmoji = score >= 80 ? '🟢' : score >= 50 ? '🟡' : '🔴';
3462
3667
 
3463
3668
  checks.push(`${scoreEmoji} **Brain Quality Score: ${score}/100**`);
3464
- checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical)`);
3669
+ checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical · ${withAudit.length} with audit trail)`);
3465
3670
  checks.push(`💾 **Context entries:** ${ctxCount}`);
3671
+ checks.push(`🎯 **Confidence:** ${lessons.length - staleLessons.length - warnLessons.length} fresh · ${warnLessons.length} warn · ${staleLessons.length} stale`);
3672
+ checks.push(`⏱️ **Decay config:** warn after ${CONFIDENCE_WARN_DAYS}d · stale after ${CONFIDENCE_STALE_DAYS}d`);
3466
3673
 
3467
3674
  if (lastSession) {
3468
3675
  const ageMin = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
@@ -3486,6 +3693,15 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
3486
3693
  issues.push(`⚠️ ${openFailures.length} unresolved failure${openFailures.length > 1 ? 's' : ''}: ${openFailures.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
3487
3694
  }
3488
3695
 
3696
+ if (staleLessons.length > 0) {
3697
+ issues.push(`🔴 ${staleLessons.length} STALE lesson${staleLessons.length > 1 ? 's' : ''} (>${CONFIDENCE_STALE_DAYS}d, confidence <${CONFIDENCE_STALE_VALUE * 100}%): ${staleLessons.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
3698
+ issues.push(` → Re-verify with \`recall_best_solution\` to reset confidence clock`);
3699
+ }
3700
+
3701
+ if (warnLessons.length > 0) {
3702
+ issues.push(`⚠️ ${warnLessons.length} lesson${warnLessons.length > 1 ? 's' : ''} aging (>${CONFIDENCE_WARN_DAYS}d): ${warnLessons.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
3703
+ }
3704
+
3489
3705
  if (unusedRatio > 0.7 && lessons.length > 5) {
3490
3706
  issues.push(`💡 ${unusedLessons.length} lessons never recalled — verify topics match your workflow`);
3491
3707
  }