@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/dist/index.js +226 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +245 -29
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);
|
|
@@ -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,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;
|
|
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
|
+
? `_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
|
-
|
|
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;
|
|
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.
|
|
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
|
|
3429
|
-
|
|
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
|
|
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
|
}
|