@cachly-dev/mcp-server 0.5.4 → 0.5.6
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/README.md +2 -2
- package/dist/index.js +173 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +195 -30
package/package.json
CHANGED
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);
|
|
@@ -807,8 +840,8 @@ const TOOLS = [
|
|
|
807
840
|
description:
|
|
808
841
|
'Create a new managed Valkey/Redis cache instance on cachly.dev. ' +
|
|
809
842
|
'Free tier provisions in ~30 seconds. Paid tiers return a Stripe checkout URL. ' +
|
|
810
|
-
'Available tiers: free (
|
|
811
|
-
'speed (
|
|
843
|
+
'Available tiers: free (25 MB), dev (200 MB, €19/mo), pro (900 MB, €49/mo), ' +
|
|
844
|
+
'speed (900 MB Dragonfly + Semantic Cache, €79/mo), business (7 GB, €199/mo).',
|
|
812
845
|
inputSchema: {
|
|
813
846
|
type: 'object',
|
|
814
847
|
properties: {
|
|
@@ -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
|
|
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,
|
|
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
|
-
// ──
|
|
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
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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,32 @@ 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;
|
|
2605
|
-
|
|
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
|
+
? `\n_Audit: ${(lesson.audit_trail ?? []).length} changes · ${new Date(lesson.ts).toLocaleDateString('de-DE')}_`
|
|
2717
|
+
: '';
|
|
2612
2718
|
return [
|
|
2613
|
-
|
|
2719
|
+
`${badge} **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}×`,
|
|
2614
2720
|
``,
|
|
2615
2721
|
`**What worked:** ${lesson.what_worked}`,
|
|
2616
2722
|
lesson.what_failed ? `**What failed (avoid this):** ${lesson.what_failed}` : '',
|
|
@@ -2618,6 +2724,7 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
2618
2724
|
(lesson.file_paths ?? []).length > 0 ? `**Files:** ${(lesson.file_paths ?? []).map((f: string) => `\`${f}\``).join(', ')}` : '',
|
|
2619
2725
|
(lesson.commands ?? []).length > 0 ? `**Commands:** ${(lesson.commands ?? []).map((c: string) => `\`${c}\``).join(', ')}` : '',
|
|
2620
2726
|
(lesson.tags ?? []).length > 0 ? `**Tags:** ${(lesson.tags ?? []).map((t: string) => `#${t}`).join(' ')}` : '',
|
|
2727
|
+
auditSummary,
|
|
2621
2728
|
].filter(l => l !== '').join('\n');
|
|
2622
2729
|
}
|
|
2623
2730
|
|
|
@@ -2848,7 +2955,8 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
2848
2955
|
// 2. Fetch all lesson values for recency sorting + focus matching
|
|
2849
2956
|
type Lesson = {
|
|
2850
2957
|
topic: string; outcome: string; what_worked: string; what_failed?: string;
|
|
2851
|
-
ts: string;
|
|
2958
|
+
ts: string; verified_at?: string; severity?: string; recall_count?: number;
|
|
2959
|
+
tags?: string[]; confidence?: number; audit_trail?: unknown[];
|
|
2852
2960
|
};
|
|
2853
2961
|
const lessons: Lesson[] = [];
|
|
2854
2962
|
for (const k of lessonKeys) {
|
|
@@ -2977,6 +3085,35 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
2977
3085
|
lines.push('');
|
|
2978
3086
|
}
|
|
2979
3087
|
|
|
3088
|
+
// ── Stale / low-confidence lessons (confidence decay) ─────────────────
|
|
3089
|
+
const staleSuccessLessons = lessons.filter(l => {
|
|
3090
|
+
if (l.outcome !== 'success' && l.outcome !== 'partial') return false;
|
|
3091
|
+
return calculateConfidence(l) < CONFIDENCE_WARN_VALUE;
|
|
3092
|
+
});
|
|
3093
|
+
if (staleSuccessLessons.length > 0) {
|
|
3094
|
+
lines.push(`🔴 **Stale lessons** (not recalled in >${CONFIDENCE_WARN_DAYS}d — verify before applying):`);
|
|
3095
|
+
for (const l of staleSuccessLessons.slice(0, 4)) {
|
|
3096
|
+
const conf = calculateConfidence(l);
|
|
3097
|
+
const ageDays = Math.round((Date.now() - new Date(l.verified_at ?? l.ts).getTime()) / 86400000);
|
|
3098
|
+
const flag = conf < CONFIDENCE_STALE_VALUE ? '🔴' : '⚠️';
|
|
3099
|
+
lines.push(` ${flag} \`${l.topic}\` — ${ageDays}d stale, ${(conf * 100).toFixed(0)}% confidence`);
|
|
3100
|
+
}
|
|
3101
|
+
lines.push(` _Run \`recall_best_solution\` on these to reset their confidence clock._`);
|
|
3102
|
+
lines.push('');
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
// ── Session Replay: show last session's decision log ──────────────────
|
|
3106
|
+
const lastSessionAny = lastSession as unknown as { decision_log?: Array<{ topic: string; outcome: string; what_worked: string }> } | null;
|
|
3107
|
+
if (lastSessionAny?.decision_log?.length) {
|
|
3108
|
+
const dl = lastSessionAny.decision_log;
|
|
3109
|
+
const successes = dl.filter(d => d.outcome === 'success');
|
|
3110
|
+
const failures = dl.filter(d => d.outcome === 'failure');
|
|
3111
|
+
lines.push(`🎬 **Last session decisions** (${dl.length} lessons stored):`);
|
|
3112
|
+
if (successes.length > 0) lines.push(` ✅ Worked: ${successes.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
|
|
3113
|
+
if (failures.length > 0) lines.push(` ❌ Failed: ${failures.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
|
|
3114
|
+
lines.push('');
|
|
3115
|
+
}
|
|
3116
|
+
|
|
2980
3117
|
return lines.join('\n');
|
|
2981
3118
|
}
|
|
2982
3119
|
|
|
@@ -3007,12 +3144,21 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
3007
3144
|
} catch { /* ignore */ }
|
|
3008
3145
|
}
|
|
3009
3146
|
|
|
3147
|
+
// ── Session Replay: capture decision log ─────────────────────────────
|
|
3148
|
+
let decisionLog: Array<{ ts: string; topic: string; outcome: string; what_worked: string }> = [];
|
|
3149
|
+
try {
|
|
3150
|
+
const dlEntries = await redis.lrange('cachly:session:decision-log', 0, -1);
|
|
3151
|
+
decisionLog = dlEntries.map(e => JSON.parse(e) as { ts: string; topic: string; outcome: string; what_worked: string });
|
|
3152
|
+
await redis.del('cachly:session:decision-log');
|
|
3153
|
+
} catch { /* non-critical */ }
|
|
3154
|
+
|
|
3010
3155
|
const sessionRecord = {
|
|
3011
3156
|
ts: now.toISOString(),
|
|
3012
3157
|
summary,
|
|
3013
3158
|
files_changed,
|
|
3014
3159
|
...(lessons_learned !== undefined ? { lessons_learned } : {}),
|
|
3015
3160
|
...(durationMin !== undefined ? { duration_min: durationMin } : {}),
|
|
3161
|
+
...(decisionLog.length > 0 ? { decision_log: decisionLog } : {}),
|
|
3016
3162
|
};
|
|
3017
3163
|
|
|
3018
3164
|
// Save as "last session"
|
|
@@ -3425,12 +3571,15 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
3425
3571
|
});
|
|
3426
3572
|
|
|
3427
3573
|
// Load lessons for analysis
|
|
3428
|
-
type
|
|
3429
|
-
|
|
3574
|
+
type DrLesson = {
|
|
3575
|
+
topic: string; outcome: string; recall_count?: number; ts: string;
|
|
3576
|
+
verified_at?: string; severity?: string; audit_trail?: unknown[];
|
|
3577
|
+
};
|
|
3578
|
+
const lessons: DrLesson[] = [];
|
|
3430
3579
|
for (const k of lessonKeys) {
|
|
3431
3580
|
const raw = await redis.get(k);
|
|
3432
3581
|
if (!raw) continue;
|
|
3433
|
-
try { lessons.push(JSON.parse(raw) as
|
|
3582
|
+
try { lessons.push(JSON.parse(raw) as DrLesson); } catch { /* skip */ }
|
|
3434
3583
|
}
|
|
3435
3584
|
|
|
3436
3585
|
// Last session
|
|
@@ -3446,6 +3595,10 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
3446
3595
|
const unusedLessons = lessons.filter(l => (l.recall_count ?? 0) === 0);
|
|
3447
3596
|
// Critical lessons
|
|
3448
3597
|
const criticalLessons = lessons.filter(l => l.severity === 'critical');
|
|
3598
|
+
// Confidence decay analysis
|
|
3599
|
+
const staleLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_STALE_VALUE);
|
|
3600
|
+
const warnLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) >= CONFIDENCE_STALE_VALUE && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
|
|
3601
|
+
const withAudit = lessons.filter(l => (l.audit_trail ?? []).length > 1);
|
|
3449
3602
|
|
|
3450
3603
|
// Quality score (0-100)
|
|
3451
3604
|
let score = 50;
|
|
@@ -3457,12 +3610,15 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
3457
3610
|
if (openFailures.length === 0) score += 5;
|
|
3458
3611
|
const unusedRatio = lessons.length > 0 ? unusedLessons.length / lessons.length : 0;
|
|
3459
3612
|
if (unusedRatio < 0.5) score += 10;
|
|
3613
|
+
if (staleLessons.length === 0) score += 5;
|
|
3460
3614
|
|
|
3461
3615
|
const scoreEmoji = score >= 80 ? '🟢' : score >= 50 ? '🟡' : '🔴';
|
|
3462
3616
|
|
|
3463
3617
|
checks.push(`${scoreEmoji} **Brain Quality Score: ${score}/100**`);
|
|
3464
|
-
checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical)`);
|
|
3618
|
+
checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical · ${withAudit.length} with audit trail)`);
|
|
3465
3619
|
checks.push(`💾 **Context entries:** ${ctxCount}`);
|
|
3620
|
+
checks.push(`🎯 **Confidence:** ${lessons.length - staleLessons.length - warnLessons.length} fresh · ${warnLessons.length} warn · ${staleLessons.length} stale`);
|
|
3621
|
+
checks.push(`⏱️ **Decay config:** warn after ${CONFIDENCE_WARN_DAYS}d · stale after ${CONFIDENCE_STALE_DAYS}d`);
|
|
3466
3622
|
|
|
3467
3623
|
if (lastSession) {
|
|
3468
3624
|
const ageMin = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
|
|
@@ -3486,6 +3642,15 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
3486
3642
|
issues.push(`⚠️ ${openFailures.length} unresolved failure${openFailures.length > 1 ? 's' : ''}: ${openFailures.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
|
|
3487
3643
|
}
|
|
3488
3644
|
|
|
3645
|
+
if (staleLessons.length > 0) {
|
|
3646
|
+
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(', ')}`);
|
|
3647
|
+
issues.push(` → Re-verify with \`recall_best_solution\` to reset confidence clock`);
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
if (warnLessons.length > 0) {
|
|
3651
|
+
issues.push(`⚠️ ${warnLessons.length} lesson${warnLessons.length > 1 ? 's' : ''} aging (>${CONFIDENCE_WARN_DAYS}d): ${warnLessons.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3489
3654
|
if (unusedRatio > 0.7 && lessons.length > 5) {
|
|
3490
3655
|
issues.push(`💡 ${unusedLessons.length} lessons never recalled — verify topics match your workflow`);
|
|
3491
3656
|
}
|