@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/dist/index.js
CHANGED
|
@@ -62,6 +62,39 @@ const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
|
62
62
|
* └──────────────────────────────────────────────────────────────────────┘
|
|
63
63
|
*/
|
|
64
64
|
const EMBED_PROVIDER = (process.env.CACHLY_EMBED_PROVIDER ?? detectEmbedProvider()).toLowerCase();
|
|
65
|
+
// ── Confidence Decay Config (all values configurable via env) ─────────────────
|
|
66
|
+
const CONFIDENCE_WARN_DAYS = Number(process.env.CACHLY_CONFIDENCE_WARN_DAYS ?? 5); // → 0.7
|
|
67
|
+
const CONFIDENCE_STALE_DAYS = Number(process.env.CACHLY_CONFIDENCE_STALE_DAYS ?? 10); // → 0.5
|
|
68
|
+
const CONFIDENCE_WARN_VALUE = 0.7;
|
|
69
|
+
const CONFIDENCE_STALE_VALUE = 0.5;
|
|
70
|
+
/** Calculate current confidence for a lesson based on how long since last verified. */
|
|
71
|
+
function calculateConfidence(lesson) {
|
|
72
|
+
const ref = lesson.verified_at ?? lesson.ts;
|
|
73
|
+
const ageMs = Date.now() - new Date(ref).getTime();
|
|
74
|
+
const ageDays = ageMs / 86400000;
|
|
75
|
+
if (ageDays >= CONFIDENCE_STALE_DAYS)
|
|
76
|
+
return CONFIDENCE_STALE_VALUE;
|
|
77
|
+
if (ageDays >= CONFIDENCE_WARN_DAYS)
|
|
78
|
+
return CONFIDENCE_WARN_VALUE;
|
|
79
|
+
// Linear interpolation between fresh (1.0) and warn threshold
|
|
80
|
+
return 1.0 - (ageDays / CONFIDENCE_WARN_DAYS) * (1.0 - CONFIDENCE_WARN_VALUE);
|
|
81
|
+
}
|
|
82
|
+
/** Render a confidence badge string. */
|
|
83
|
+
function confidenceBadge(confidence, ageDays) {
|
|
84
|
+
if (confidence >= 0.9)
|
|
85
|
+
return '✅';
|
|
86
|
+
if (confidence >= 0.7)
|
|
87
|
+
return `⚠️ (${Math.round(ageDays)}d old, confidence ${(confidence * 100).toFixed(0)}% — verify before applying)`;
|
|
88
|
+
return `🔴 STALE (${Math.round(ageDays)}d old, confidence ${(confidence * 100).toFixed(0)}% — likely outdated!)`;
|
|
89
|
+
}
|
|
90
|
+
/** Category-specific required fields for structured lessons. */
|
|
91
|
+
const STRUCTURED_TEMPLATES = {
|
|
92
|
+
'deploy': { required: ['commands'], hint: 'deploy:* needs commands[]' },
|
|
93
|
+
'bash': { required: ['commands'], hint: 'bash:* needs commands[]' },
|
|
94
|
+
'infra': { required: ['commands'], hint: 'infra:* needs commands[] + verified on real system' },
|
|
95
|
+
'pricing': { required: [], hint: 'pricing:* — add context with source (e.g. "Stripe Dashboard Apr 2026")' },
|
|
96
|
+
'stripe': { required: [], hint: 'stripe:* — add context with Stripe API version' },
|
|
97
|
+
};
|
|
65
98
|
/** Fast content hash for index invalidation (not cryptographic, just change detection) */
|
|
66
99
|
function simpleHash(text) {
|
|
67
100
|
return createHash('md5').update(text).digest('hex').slice(0, 12);
|
|
@@ -1142,7 +1175,9 @@ const TOOLS = [
|
|
|
1142
1175
|
'Fields: topic (short slug like "deploy:web"), outcome ("success"|"failure"), ' +
|
|
1143
1176
|
'what_worked (what solved it), what_failed (what did NOT work), context (extra details). ' +
|
|
1144
1177
|
'Supports structured metadata: severity, file_paths (files involved), commands (working commands), tags. ' +
|
|
1145
|
-
'Deduplication: if a lesson for this topic already exists, it is updated
|
|
1178
|
+
'Deduplication: if a lesson for this topic already exists, it is updated with full audit trail. ' +
|
|
1179
|
+
'Contradiction detection: warns if new outcome conflicts with existing lesson outcome. ' +
|
|
1180
|
+
'Confidence: lesson starts at 1.0, decays after 5d (→0.7) and 10d (→0.5) without recall. ' +
|
|
1146
1181
|
'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"])',
|
|
1147
1182
|
inputSchema: {
|
|
1148
1183
|
type: 'object',
|
|
@@ -1181,7 +1216,9 @@ const TOOLS = [
|
|
|
1181
1216
|
name: 'recall_best_solution',
|
|
1182
1217
|
description: 'Recall the best known solution for a topic from past lessons. ' +
|
|
1183
1218
|
'Call this BEFORE attempting any task that might have been done before. ' +
|
|
1184
|
-
'Returns the most recent successful lesson for the topic,
|
|
1219
|
+
'Returns the most recent successful lesson for the topic, with confidence indicator. ' +
|
|
1220
|
+
'⚠️ badge = lesson is >5d old (verify before applying). 🔴 = >10d old (likely stale!). ' +
|
|
1221
|
+
'Recalling a lesson resets its confidence clock to 1.0 (marks as recently verified). ' +
|
|
1185
1222
|
'Example: recall_best_solution(topic="deploy:web") → returns the working deploy command.',
|
|
1186
1223
|
inputSchema: {
|
|
1187
1224
|
type: 'object',
|
|
@@ -2219,21 +2256,50 @@ async function handleTool(name, args) {
|
|
|
2219
2256
|
const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], } = args;
|
|
2220
2257
|
const redis = await getConnection(instance_id);
|
|
2221
2258
|
const ts = new Date().toISOString();
|
|
2222
|
-
// ──
|
|
2259
|
+
// ── Structured template hints ──────────────────────────────────────────
|
|
2260
|
+
const category = topic.split(':')[0];
|
|
2261
|
+
const template = STRUCTURED_TEMPLATES[category];
|
|
2262
|
+
const templateWarnings = [];
|
|
2263
|
+
if (template) {
|
|
2264
|
+
for (const req of template.required) {
|
|
2265
|
+
if (req === 'commands' && commands.length === 0) {
|
|
2266
|
+
templateWarnings.push(`📋 ${template.hint}`);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
// ── Deduplication + audit trail ────────────────────────────────────────
|
|
2223
2271
|
let isUpdate = false;
|
|
2224
2272
|
let recallCount = 0;
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2273
|
+
let auditTrail = [];
|
|
2274
|
+
const existingRaw = await redis.get(`cachly:lesson:best:${topic}`);
|
|
2275
|
+
if (existingRaw) {
|
|
2276
|
+
try {
|
|
2277
|
+
const prev = JSON.parse(existingRaw);
|
|
2278
|
+
recallCount = prev.recall_count ?? 0;
|
|
2279
|
+
auditTrail = prev.audit_trail ?? [];
|
|
2280
|
+
auditTrail.push({ ts, action: 'updated', prev_outcome: prev.outcome });
|
|
2281
|
+
if (auditTrail.length > 20)
|
|
2282
|
+
auditTrail = auditTrail.slice(-20);
|
|
2283
|
+
isUpdate = true;
|
|
2284
|
+
// ── Contradiction detection ─────────────────────────────────────────
|
|
2285
|
+
const contradictionWarning = [];
|
|
2286
|
+
if (prev.outcome === 'success' && outcome === 'failure') {
|
|
2287
|
+
contradictionWarning.push(`⚠️ **Contradiction detected!** Existing lesson has outcome: \`success\`, but you're storing \`failure\`.`, `The existing "success" lesson will be preserved. Only the audit trail is updated.`, `If you meant to mark this as failed permanently, store a new lesson with a distinct topic slug.`);
|
|
2288
|
+
}
|
|
2289
|
+
else if (prev.outcome === 'failure' && outcome === 'success') {
|
|
2290
|
+
contradictionWarning.push(`✅ **Conflict resolved!** Previous lesson was \`failure\` — now overwriting with \`success\`.`);
|
|
2291
|
+
}
|
|
2292
|
+
if (contradictionWarning.length > 0) {
|
|
2293
|
+
// Store contradiction audit but don't block
|
|
2294
|
+
auditTrail[auditTrail.length - 1].action = 'contradiction-resolved';
|
|
2232
2295
|
}
|
|
2233
|
-
catch { /* ignore parse error */ }
|
|
2234
2296
|
}
|
|
2297
|
+
catch { /* ignore parse error */ }
|
|
2298
|
+
}
|
|
2299
|
+
else {
|
|
2300
|
+
auditTrail = [{ ts, action: 'created' }];
|
|
2235
2301
|
}
|
|
2236
|
-
const
|
|
2302
|
+
const lessonObj = {
|
|
2237
2303
|
topic,
|
|
2238
2304
|
outcome,
|
|
2239
2305
|
what_worked,
|
|
@@ -2245,15 +2311,30 @@ async function handleTool(name, args) {
|
|
|
2245
2311
|
tags,
|
|
2246
2312
|
recall_count: recallCount,
|
|
2247
2313
|
ts,
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2314
|
+
verified_at: outcome === 'success' || outcome === 'partial' ? ts : undefined,
|
|
2315
|
+
confidence: 1.0,
|
|
2316
|
+
audit_trail: auditTrail,
|
|
2317
|
+
version: 3,
|
|
2318
|
+
};
|
|
2319
|
+
const lesson = JSON.stringify(lessonObj);
|
|
2320
|
+
// Always append to the history list (audit log)
|
|
2251
2321
|
const listKey = `cachly:lessons:${topic}`;
|
|
2252
2322
|
await redis.rpush(listKey, lesson);
|
|
2253
|
-
// Update best key
|
|
2323
|
+
// Update best key for success/partial; for failure only update if no success exists
|
|
2254
2324
|
if (outcome === 'success' || outcome === 'partial') {
|
|
2255
2325
|
await redis.set(`cachly:lesson:best:${topic}`, lesson);
|
|
2256
2326
|
}
|
|
2327
|
+
else if (!existingRaw) {
|
|
2328
|
+
await redis.set(`cachly:lesson:best:${topic}`, lesson);
|
|
2329
|
+
}
|
|
2330
|
+
// Track in decision log for session replay
|
|
2331
|
+
try {
|
|
2332
|
+
const dlKey = 'cachly:session:decision-log';
|
|
2333
|
+
const dlEntry = JSON.stringify({ ts, topic, outcome, what_worked: what_worked.slice(0, 120) });
|
|
2334
|
+
await redis.rpush(dlKey, dlEntry);
|
|
2335
|
+
await redis.ltrim(dlKey, -50, -1);
|
|
2336
|
+
}
|
|
2337
|
+
catch { /* non-critical */ }
|
|
2257
2338
|
const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
|
|
2258
2339
|
const sevEmoji = severity === 'critical' ? '🔴' : severity === 'major' ? '🟡' : '🟢';
|
|
2259
2340
|
const action = isUpdate ? 'updated' : 'stored';
|
|
@@ -2268,8 +2349,9 @@ async function handleTool(name, args) {
|
|
|
2268
2349
|
tags.length > 0 ? `**Tags:** ${tags.map(t => `#${t}`).join(' ')}` : '',
|
|
2269
2350
|
``,
|
|
2270
2351
|
isUpdate
|
|
2271
|
-
? `♻️ Updated
|
|
2352
|
+
? `♻️ Updated (recall count: ${recallCount} · audit entries: ${auditTrail.length})`
|
|
2272
2353
|
: `💡 Recall later with \`recall_best_solution(topic="${topic}")\``,
|
|
2354
|
+
...templateWarnings,
|
|
2273
2355
|
].filter(l => l !== '').join('\n');
|
|
2274
2356
|
}
|
|
2275
2357
|
case 'recall_best_solution': {
|
|
@@ -2279,12 +2361,35 @@ async function handleTool(name, args) {
|
|
|
2279
2361
|
const best = await redis.get(`cachly:lesson:best:${topic}`);
|
|
2280
2362
|
if (best) {
|
|
2281
2363
|
const lesson = JSON.parse(best);
|
|
2282
|
-
//
|
|
2283
|
-
const
|
|
2364
|
+
// ── Confidence decay check ───────────────────────────────────────────
|
|
2365
|
+
const confidence = calculateConfidence(lesson);
|
|
2366
|
+
const ref = lesson.verified_at ?? lesson.ts;
|
|
2367
|
+
const ageDays = (Date.now() - new Date(ref).getTime()) / 86400000;
|
|
2368
|
+
const badge = confidenceBadge(confidence, ageDays);
|
|
2369
|
+
// Recall resets verified_at (confidence clock restart)
|
|
2370
|
+
const updatedLesson = {
|
|
2371
|
+
...lesson,
|
|
2372
|
+
recall_count: (lesson.recall_count ?? 0) + 1,
|
|
2373
|
+
verified_at: new Date().toISOString(),
|
|
2374
|
+
confidence: 1.0,
|
|
2375
|
+
};
|
|
2284
2376
|
await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(updatedLesson));
|
|
2285
2377
|
const sevEmoji = lesson.severity === 'critical' ? '🔴' : lesson.severity === 'major' ? '🟡' : lesson.severity ? '🟢' : '';
|
|
2378
|
+
const auditSummary = (lesson.audit_trail ?? []).length > 1
|
|
2379
|
+
? `_Audit: ${(lesson.audit_trail ?? []).length} changes · stored ${new Date(lesson.ts).toLocaleDateString('de-DE')}_`
|
|
2380
|
+
: '';
|
|
2381
|
+
// "Remember when..." — emotional header for lessons > 60 days old
|
|
2382
|
+
const ageFromStoreDays = (Date.now() - new Date(lesson.ts).getTime()) / 86400000;
|
|
2383
|
+
const rememberWhen = ageFromStoreDays > 60
|
|
2384
|
+
? `💭 _Remember when you solved this ${Math.round(ageFromStoreDays / 30)} months ago? Still works._`
|
|
2385
|
+
: '';
|
|
2386
|
+
// "Never Google This Again" — suggest pinning after 3rd recall
|
|
2387
|
+
const suggestPin = updatedLesson.recall_count === 3 && !lesson.pinned
|
|
2388
|
+
? `📌 **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.`
|
|
2389
|
+
: '';
|
|
2286
2390
|
return [
|
|
2287
|
-
|
|
2391
|
+
rememberWhen,
|
|
2392
|
+
`${badge} **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}×`,
|
|
2288
2393
|
``,
|
|
2289
2394
|
`**What worked:** ${lesson.what_worked}`,
|
|
2290
2395
|
lesson.what_failed ? `**What failed (avoid this):** ${lesson.what_failed}` : '',
|
|
@@ -2292,6 +2397,8 @@ async function handleTool(name, args) {
|
|
|
2292
2397
|
(lesson.file_paths ?? []).length > 0 ? `**Files:** ${(lesson.file_paths ?? []).map((f) => `\`${f}\``).join(', ')}` : '',
|
|
2293
2398
|
(lesson.commands ?? []).length > 0 ? `**Commands:** ${(lesson.commands ?? []).map((c) => `\`${c}\``).join(', ')}` : '',
|
|
2294
2399
|
(lesson.tags ?? []).length > 0 ? `**Tags:** ${(lesson.tags ?? []).map((t) => `#${t}`).join(' ')}` : '',
|
|
2400
|
+
auditSummary,
|
|
2401
|
+
suggestPin,
|
|
2295
2402
|
].filter(l => l !== '').join('\n');
|
|
2296
2403
|
}
|
|
2297
2404
|
// Partial match: scan all lesson keys for topic substring
|
|
@@ -2527,13 +2634,56 @@ async function handleTool(name, args) {
|
|
|
2527
2634
|
? lessons.filter(l => focusTerms.some(term => l.topic.toLowerCase().includes(term) ||
|
|
2528
2635
|
(l.tags ?? []).some((t) => t.toLowerCase().includes(term))))
|
|
2529
2636
|
: [];
|
|
2530
|
-
// 6.
|
|
2637
|
+
// 6. Streak tracking
|
|
2638
|
+
let streakDays = 0;
|
|
2639
|
+
let streakRecord = 0;
|
|
2640
|
+
let streakMessage = '';
|
|
2641
|
+
try {
|
|
2642
|
+
const streakRaw = await redis.get('cachly:streak:current');
|
|
2643
|
+
const streak = streakRaw ? JSON.parse(streakRaw) : null;
|
|
2644
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
2645
|
+
if (streak) {
|
|
2646
|
+
const lastDate = streak.last_date;
|
|
2647
|
+
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
|
2648
|
+
if (lastDate === today) {
|
|
2649
|
+
// Already counted today
|
|
2650
|
+
streakDays = streak.days;
|
|
2651
|
+
streakRecord = streak.record;
|
|
2652
|
+
}
|
|
2653
|
+
else if (lastDate === yesterday) {
|
|
2654
|
+
// Continuing streak
|
|
2655
|
+
streakDays = streak.days + 1;
|
|
2656
|
+
streakRecord = Math.max(streakDays, streak.record);
|
|
2657
|
+
await redis.set('cachly:streak:current', JSON.stringify({ days: streakDays, last_date: today, record: streakRecord }));
|
|
2658
|
+
}
|
|
2659
|
+
else {
|
|
2660
|
+
// Streak broken
|
|
2661
|
+
streakDays = 1;
|
|
2662
|
+
streakRecord = streak.record;
|
|
2663
|
+
await redis.set('cachly:streak:current', JSON.stringify({ days: 1, last_date: today, record: streakRecord }));
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
else {
|
|
2667
|
+
// First session ever
|
|
2668
|
+
streakDays = 1;
|
|
2669
|
+
streakRecord = 1;
|
|
2670
|
+
await redis.set('cachly:streak:current', JSON.stringify({ days: 1, last_date: today, record: 1 }));
|
|
2671
|
+
}
|
|
2672
|
+
if (streakDays >= 7)
|
|
2673
|
+
streakMessage = `🔥 **${streakDays}-day streak!** ${streakDays === streakRecord ? ' New record!' : `Best: ${streakRecord}d`}`;
|
|
2674
|
+
else if (streakDays > 1)
|
|
2675
|
+
streakMessage = `🔥 ${streakDays}-day streak`;
|
|
2676
|
+
}
|
|
2677
|
+
catch { /* non-critical */ }
|
|
2678
|
+
// 7. Save session start marker
|
|
2531
2679
|
await redis.set('cachly:session:current', JSON.stringify({
|
|
2532
2680
|
started: new Date().toISOString(),
|
|
2533
2681
|
focus,
|
|
2534
2682
|
}), 'EX', 86400); // auto-expire after 24h if session_end never called
|
|
2535
2683
|
// ── Build briefing ──────────────────────────────────────────────────────
|
|
2536
2684
|
const lines = ['🧠 **Session Briefing**', ''];
|
|
2685
|
+
if (streakMessage)
|
|
2686
|
+
lines.push(streakMessage, '');
|
|
2537
2687
|
// Handoff from previous window (if any)
|
|
2538
2688
|
const handoffRaw = await redis.get('cachly:session:handoff');
|
|
2539
2689
|
if (handoffRaw) {
|
|
@@ -2611,6 +2761,36 @@ async function handleTool(name, args) {
|
|
|
2611
2761
|
}
|
|
2612
2762
|
lines.push('');
|
|
2613
2763
|
}
|
|
2764
|
+
// ── Stale / low-confidence lessons (confidence decay) ─────────────────
|
|
2765
|
+
const staleSuccessLessons = lessons.filter(l => {
|
|
2766
|
+
if (l.outcome !== 'success' && l.outcome !== 'partial')
|
|
2767
|
+
return false;
|
|
2768
|
+
return calculateConfidence(l) < CONFIDENCE_WARN_VALUE;
|
|
2769
|
+
});
|
|
2770
|
+
if (staleSuccessLessons.length > 0) {
|
|
2771
|
+
lines.push(`🔴 **Stale lessons** (not recalled in >${CONFIDENCE_WARN_DAYS}d — verify before applying):`);
|
|
2772
|
+
for (const l of staleSuccessLessons.slice(0, 4)) {
|
|
2773
|
+
const conf = calculateConfidence(l);
|
|
2774
|
+
const ageDays = Math.round((Date.now() - new Date(l.verified_at ?? l.ts).getTime()) / 86400000);
|
|
2775
|
+
const flag = conf < CONFIDENCE_STALE_VALUE ? '🔴' : '⚠️';
|
|
2776
|
+
lines.push(` ${flag} \`${l.topic}\` — ${ageDays}d stale, ${(conf * 100).toFixed(0)}% confidence`);
|
|
2777
|
+
}
|
|
2778
|
+
lines.push(` _Run \`recall_best_solution\` on these to reset their confidence clock._`);
|
|
2779
|
+
lines.push('');
|
|
2780
|
+
}
|
|
2781
|
+
// ── Session Replay: show last session's decision log ──────────────────
|
|
2782
|
+
const lastSessionAny = lastSession;
|
|
2783
|
+
if (lastSessionAny?.decision_log?.length) {
|
|
2784
|
+
const dl = lastSessionAny.decision_log;
|
|
2785
|
+
const successes = dl.filter(d => d.outcome === 'success');
|
|
2786
|
+
const failures = dl.filter(d => d.outcome === 'failure');
|
|
2787
|
+
lines.push(`🎬 **Last session decisions** (${dl.length} lessons stored):`);
|
|
2788
|
+
if (successes.length > 0)
|
|
2789
|
+
lines.push(` ✅ Worked: ${successes.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
|
|
2790
|
+
if (failures.length > 0)
|
|
2791
|
+
lines.push(` ❌ Failed: ${failures.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
|
|
2792
|
+
lines.push('');
|
|
2793
|
+
}
|
|
2614
2794
|
return lines.join('\n');
|
|
2615
2795
|
}
|
|
2616
2796
|
// ── session_end ───────────────────────────────────────────────────────────
|
|
@@ -2628,12 +2808,21 @@ async function handleTool(name, args) {
|
|
|
2628
2808
|
}
|
|
2629
2809
|
catch { /* ignore */ }
|
|
2630
2810
|
}
|
|
2811
|
+
// ── Session Replay: capture decision log ─────────────────────────────
|
|
2812
|
+
let decisionLog = [];
|
|
2813
|
+
try {
|
|
2814
|
+
const dlEntries = await redis.lrange('cachly:session:decision-log', 0, -1);
|
|
2815
|
+
decisionLog = dlEntries.map(e => JSON.parse(e));
|
|
2816
|
+
await redis.del('cachly:session:decision-log');
|
|
2817
|
+
}
|
|
2818
|
+
catch { /* non-critical */ }
|
|
2631
2819
|
const sessionRecord = {
|
|
2632
2820
|
ts: now.toISOString(),
|
|
2633
2821
|
summary,
|
|
2634
2822
|
files_changed,
|
|
2635
2823
|
...(lessons_learned !== undefined ? { lessons_learned } : {}),
|
|
2636
2824
|
...(durationMin !== undefined ? { duration_min: durationMin } : {}),
|
|
2825
|
+
...(decisionLog.length > 0 ? { decision_log: decisionLog } : {}),
|
|
2637
2826
|
};
|
|
2638
2827
|
// Save as "last session"
|
|
2639
2828
|
await redis.set('cachly:session:last', JSON.stringify(sessionRecord));
|
|
@@ -3000,6 +3189,10 @@ async function handleTool(name, args) {
|
|
|
3000
3189
|
const unusedLessons = lessons.filter(l => (l.recall_count ?? 0) === 0);
|
|
3001
3190
|
// Critical lessons
|
|
3002
3191
|
const criticalLessons = lessons.filter(l => l.severity === 'critical');
|
|
3192
|
+
// Confidence decay analysis
|
|
3193
|
+
const staleLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_STALE_VALUE);
|
|
3194
|
+
const warnLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) >= CONFIDENCE_STALE_VALUE && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
|
|
3195
|
+
const withAudit = lessons.filter(l => (l.audit_trail ?? []).length > 1);
|
|
3003
3196
|
// Quality score (0-100)
|
|
3004
3197
|
let score = 50;
|
|
3005
3198
|
if (lessonKeys.length >= 5)
|
|
@@ -3017,10 +3210,14 @@ async function handleTool(name, args) {
|
|
|
3017
3210
|
const unusedRatio = lessons.length > 0 ? unusedLessons.length / lessons.length : 0;
|
|
3018
3211
|
if (unusedRatio < 0.5)
|
|
3019
3212
|
score += 10;
|
|
3213
|
+
if (staleLessons.length === 0)
|
|
3214
|
+
score += 5;
|
|
3020
3215
|
const scoreEmoji = score >= 80 ? '🟢' : score >= 50 ? '🟡' : '🔴';
|
|
3021
3216
|
checks.push(`${scoreEmoji} **Brain Quality Score: ${score}/100**`);
|
|
3022
|
-
checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical)`);
|
|
3217
|
+
checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical · ${withAudit.length} with audit trail)`);
|
|
3023
3218
|
checks.push(`💾 **Context entries:** ${ctxCount}`);
|
|
3219
|
+
checks.push(`🎯 **Confidence:** ${lessons.length - staleLessons.length - warnLessons.length} fresh · ${warnLessons.length} warn · ${staleLessons.length} stale`);
|
|
3220
|
+
checks.push(`⏱️ **Decay config:** warn after ${CONFIDENCE_WARN_DAYS}d · stale after ${CONFIDENCE_STALE_DAYS}d`);
|
|
3024
3221
|
if (lastSession) {
|
|
3025
3222
|
const ageMin = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
|
|
3026
3223
|
const ageStr = ageMin < 60 ? `${ageMin}m` : ageMin < 1440 ? `${Math.round(ageMin / 60)}h` : `${Math.round(ageMin / 1440)}d`;
|
|
@@ -3041,6 +3238,13 @@ async function handleTool(name, args) {
|
|
|
3041
3238
|
if (openFailures.length > 0) {
|
|
3042
3239
|
issues.push(`⚠️ ${openFailures.length} unresolved failure${openFailures.length > 1 ? 's' : ''}: ${openFailures.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
|
|
3043
3240
|
}
|
|
3241
|
+
if (staleLessons.length > 0) {
|
|
3242
|
+
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(', ')}`);
|
|
3243
|
+
issues.push(` → Re-verify with \`recall_best_solution\` to reset confidence clock`);
|
|
3244
|
+
}
|
|
3245
|
+
if (warnLessons.length > 0) {
|
|
3246
|
+
issues.push(`⚠️ ${warnLessons.length} lesson${warnLessons.length > 1 ? 's' : ''} aging (>${CONFIDENCE_WARN_DAYS}d): ${warnLessons.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
|
|
3247
|
+
}
|
|
3044
3248
|
if (unusedRatio > 0.7 && lessons.length > 5) {
|
|
3045
3249
|
issues.push(`💡 ${unusedLessons.length} lessons never recalled — verify topics match your workflow`);
|
|
3046
3250
|
}
|