@cachly-dev/mcp-server 0.10.5 → 0.10.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/auth.d.ts +3 -0
- package/dist/auth.js +32 -0
- package/dist/auth.js.map +1 -0
- package/dist/handlers/advanced.js +38 -31
- package/dist/handlers/advanced.js.map +1 -1
- package/dist/handlers/brain.js +144 -50
- package/dist/handlers/brain.js.map +1 -1
- package/dist/index.js +252 -217
- package/dist/index.js.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/utils.js.map +1 -0
- package/package.json +1 -1
- package/src/auth.ts +46 -0
- package/src/handlers/advanced.ts +37 -23
- package/src/handlers/brain.ts +141 -47
- package/src/index.ts +251 -234
- package/src/utils.ts +10 -0
package/src/handlers/brain.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Redis } from 'ioredis';
|
|
|
3
3
|
import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES,
|
|
4
4
|
CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS } from '../confidence.js';
|
|
5
5
|
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from '../ckg.js';
|
|
6
|
+
import { safeJsonParse } from '../utils.js';
|
|
6
7
|
import type { CKGEdge, CKGNode } from '../ckg.js';
|
|
7
8
|
import { keywordSearch, tokenize, splitMultiQuery, levenshtein,
|
|
8
9
|
ZERO_RESULTS_LOG, zeroResultsTotal, indexVocab as _indexVocab } from '../search.js';
|
|
@@ -10,6 +11,19 @@ import type { KeywordMatch } from '../search.js';
|
|
|
10
11
|
import { computeEmbedding, hasEmbedProvider, embedProviderHint, EMBED_PROVIDER } from '../embeddings.js';
|
|
11
12
|
import { detectNamespace } from '../namespace.js';
|
|
12
13
|
|
|
14
|
+
// ── Changelog (shown once per version in session_start) ──────────────────────
|
|
15
|
+
const MCP_VERSION = '0.10.0';
|
|
16
|
+
const WHATS_NEW: Record<string, string[]> = {
|
|
17
|
+
'0.10.0': [
|
|
18
|
+
`🆕 **What's new in v${MCP_VERSION}:**`,
|
|
19
|
+
` ✅ Zero-friction setup — auto-provisions your Brain, no manual config needed`,
|
|
20
|
+
` ✅ 10× faster briefings — batch Redis fetches replace per-key loops`,
|
|
21
|
+
` ✅ Crash-proof data reads — corrupted cache entries no longer break tools`,
|
|
22
|
+
` ✅ \`session_end\` warns when called without an active session`,
|
|
23
|
+
` ✅ \`get_api_status\` now shows live Redis ping + all instance statuses`,
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
|
|
13
27
|
// Shared types used in brain handlers
|
|
14
28
|
export interface Instance {
|
|
15
29
|
id: string; name: string; tier: string; status: string; region: string;
|
|
@@ -167,7 +181,7 @@ export async function handleBrainTool(
|
|
|
167
181
|
for (const dep of depends_on) {
|
|
168
182
|
const depKey = `cachly:dep:${dep}`;
|
|
169
183
|
const existing = await redis.get(depKey);
|
|
170
|
-
const depTopics: string[] =
|
|
184
|
+
const depTopics: string[] = safeJsonParse<string[]>(existing, []);
|
|
171
185
|
if (!depTopics.includes(topic)) depTopics.push(topic);
|
|
172
186
|
await redis.set(depKey, JSON.stringify(depTopics));
|
|
173
187
|
}
|
|
@@ -266,7 +280,8 @@ export async function handleBrainTool(
|
|
|
266
280
|
for (const ek of existingEdgeKeys) {
|
|
267
281
|
const er = await redis.get(ek);
|
|
268
282
|
if (!er) continue;
|
|
269
|
-
const existEdge
|
|
283
|
+
const existEdge = safeJsonParse<CKGEdge | null>(er, null);
|
|
284
|
+
if (!existEdge) continue;
|
|
270
285
|
if (existEdge.edgeType === 'fixes' && existEdge.confidence > 0.7 && existEdge.trials >= 3) {
|
|
271
286
|
beliefConflict = `⚠️ **belief_conflict** on \`${topic}\`: previously confirmed fix (confidence ${existEdge.confidence.toFixed(2)}, n=${existEdge.trials}) now reports failure. Both beliefs retained as \`contested\`. Use \`ckg_inspect(concept="${conceptId}")\` to review.`;
|
|
272
287
|
// Store conflict marker
|
|
@@ -274,7 +289,7 @@ export async function handleBrainTool(
|
|
|
274
289
|
await redis.set(conflictKey, JSON.stringify({ topic, detected_at: new Date().toISOString(), fix_confidence: existEdge.confidence, fix_trials: existEdge.trials, failure_outcome: outcome }), 'EX', 60 * 60 * 24 * 90);
|
|
275
290
|
// Auto-trigger MADC deliberation in background — never blocks learn_from_attempts
|
|
276
291
|
// Note: fire-and-forget; madc_deliberate is handled elsewhere in the switch
|
|
277
|
-
|
|
292
|
+
// (conflict marker stored above — no warm-up call needed)
|
|
278
293
|
}
|
|
279
294
|
}
|
|
280
295
|
}
|
|
@@ -286,14 +301,16 @@ export async function handleBrainTool(
|
|
|
286
301
|
for (const ek of fromEdgeKeys.slice(0, 10)) {
|
|
287
302
|
const er = await redis.get(ek);
|
|
288
303
|
if (!er) continue;
|
|
289
|
-
const e2
|
|
304
|
+
const e2 = safeJsonParse<CKGEdge | null>(er, null);
|
|
305
|
+
if (!e2) continue;
|
|
290
306
|
if (e2.edgeType !== 'fixes') continue;
|
|
291
307
|
// Boost second-degree: edges from e2.to get a small fractional success
|
|
292
308
|
const secondKeys = await redis.smembers(`cachly:ckg:idx:from:${e2.to}`);
|
|
293
309
|
for (const sk of secondKeys.slice(0, 5)) {
|
|
294
310
|
const sr = await redis.get(sk);
|
|
295
311
|
if (!sr) continue;
|
|
296
|
-
const se
|
|
312
|
+
const se = safeJsonParse<CKGEdge | null>(sr, null);
|
|
313
|
+
if (!se) continue;
|
|
297
314
|
if (se.edgeType !== 'co-occurs') continue;
|
|
298
315
|
// Add 0.1 fractional success (second-degree signal)
|
|
299
316
|
se.successes = (se.successes || 0) + 0.1;
|
|
@@ -314,7 +331,8 @@ export async function handleBrainTool(
|
|
|
314
331
|
for (const ek of fromEdgeKeys) {
|
|
315
332
|
const er = await redis.get(ek);
|
|
316
333
|
if (!er) continue;
|
|
317
|
-
const de
|
|
334
|
+
const de = safeJsonParse<CKGEdge | null>(er, null);
|
|
335
|
+
if (!de) continue;
|
|
318
336
|
if (de.trials < 3 && de.last_updated && new Date(de.last_updated).getTime() < decayCutoff) {
|
|
319
337
|
de.confidence = de.confidence * 0.9;
|
|
320
338
|
de.last_updated = new Date().toISOString();
|
|
@@ -356,12 +374,13 @@ export async function handleBrainTool(
|
|
|
356
374
|
// Try exact best-solution key first
|
|
357
375
|
const best = await redis.get(`cachly:lesson:best:${topic}`);
|
|
358
376
|
if (best) {
|
|
359
|
-
const lesson =
|
|
377
|
+
const lesson = safeJsonParse(best, null as null | {
|
|
360
378
|
topic: string; outcome: string; what_worked: string; what_failed?: string;
|
|
361
379
|
context?: string; ts: string; verified_at?: string; severity?: string;
|
|
362
380
|
file_paths?: string[]; commands?: string[]; tags?: string[];
|
|
363
381
|
recall_count?: number; audit_trail?: unknown[];
|
|
364
|
-
};
|
|
382
|
+
});
|
|
383
|
+
if (!lesson) return `⚠️ Lesson data for \`${topic}\` is corrupted. Re-store it with \`learn_from_attempts\`.`;
|
|
365
384
|
|
|
366
385
|
// ── Confidence decay check ───────────────────────────────────────────
|
|
367
386
|
const confidence = calculateConfidence(lesson);
|
|
@@ -378,6 +397,10 @@ export async function handleBrainTool(
|
|
|
378
397
|
};
|
|
379
398
|
await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(updatedLesson));
|
|
380
399
|
|
|
400
|
+
// Track estimated time saved (30m minor · 60m major · 240m critical)
|
|
401
|
+
const savedMins = lesson.severity === 'critical' ? 240 : lesson.severity === 'major' ? 60 : 30;
|
|
402
|
+
redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => {});
|
|
403
|
+
|
|
381
404
|
const sevEmoji = lesson.severity === 'critical' ? '🔴' : lesson.severity === 'major' ? '🟡' : lesson.severity ? '🟢' : '';
|
|
382
405
|
const auditSummary = (lesson.audit_trail ?? []).length > 1
|
|
383
406
|
? `_Audit: ${(lesson.audit_trail ?? []).length} changes · stored ${new Date(lesson.ts).toLocaleDateString('de-DE')}_`
|
|
@@ -424,7 +447,7 @@ export async function handleBrainTool(
|
|
|
424
447
|
const histKey = `cachly:lessons:${topic}`;
|
|
425
448
|
const all = await redis.lrange(histKey, -3, -1);
|
|
426
449
|
if (all.length > 0) {
|
|
427
|
-
const parsed = all.map(e =>
|
|
450
|
+
const parsed = all.map(e => safeJsonParse(e, null as null | { outcome: string; what_worked: string; ts: string })).filter(Boolean) as Array<{ outcome: string; what_worked: string; ts: string }>;
|
|
428
451
|
const lines = parsed.map(p => `- ${p.outcome === 'success' ? '✅' : '❌'} ${p.what_worked.slice(0, 120)} (${new Date(p.ts).toLocaleDateString('de-DE')})`);
|
|
429
452
|
return `⚠️ No successful solution for \`${topic}\` yet. Last attempts:\n\n${lines.join('\n')}`;
|
|
430
453
|
}
|
|
@@ -436,7 +459,8 @@ export async function handleBrainTool(
|
|
|
436
459
|
for (const k of matching.slice(0, 5)) {
|
|
437
460
|
const raw = await redis.get(k);
|
|
438
461
|
if (!raw) continue;
|
|
439
|
-
const lesson =
|
|
462
|
+
const lesson = safeJsonParse(raw, null as null | { topic: string; what_worked: string; context?: string; ts: string });
|
|
463
|
+
if (!lesson) continue;
|
|
440
464
|
results.push(`**\`${lesson.topic}\`** — ${lesson.what_worked.slice(0, 200)}`);
|
|
441
465
|
}
|
|
442
466
|
return `🔍 **Partial matches for \`${topic}\`:**\n\n${results.join('\n\n')}`;
|
|
@@ -597,10 +621,12 @@ export async function handleBrainTool(
|
|
|
597
621
|
tags?: string[]; confidence?: number; audit_trail?: unknown[];
|
|
598
622
|
};
|
|
599
623
|
const lessons: Lesson[] = [];
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
624
|
+
if (lessonKeys.length > 0) {
|
|
625
|
+
const raws = await redis.mget(...lessonKeys);
|
|
626
|
+
for (const raw of raws) {
|
|
627
|
+
const l = safeJsonParse<Lesson | null>(raw ?? null, null);
|
|
628
|
+
if (l) lessons.push(l);
|
|
629
|
+
}
|
|
604
630
|
}
|
|
605
631
|
lessons.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime());
|
|
606
632
|
|
|
@@ -617,10 +643,8 @@ export async function handleBrainTool(
|
|
|
617
643
|
|
|
618
644
|
// 4. Last session
|
|
619
645
|
const lastSessionRaw = await redis.get('cachly:session:last');
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
try { lastSession = JSON.parse(lastSessionRaw); } catch { /* ignore */ }
|
|
623
|
-
}
|
|
646
|
+
type LastSession = { summary: string; ts: string; files_changed?: string[]; duration_min?: number };
|
|
647
|
+
const lastSession = safeJsonParse<LastSession | null>(lastSessionRaw, null);
|
|
624
648
|
|
|
625
649
|
// 5. Focus filtering
|
|
626
650
|
const focusTerms = focus.toLowerCase().split(/\s+/).filter(Boolean);
|
|
@@ -639,7 +663,7 @@ export async function handleBrainTool(
|
|
|
639
663
|
let streakMessage = '';
|
|
640
664
|
try {
|
|
641
665
|
const streakRaw = await redis.get('cachly:streak:current');
|
|
642
|
-
const streak =
|
|
666
|
+
const streak = safeJsonParse<{ days: number; last_date: string; record: number } | null>(streakRaw, null);
|
|
643
667
|
const today = new Date().toISOString().slice(0, 10);
|
|
644
668
|
if (streak) {
|
|
645
669
|
const lastDate = streak.last_date;
|
|
@@ -676,11 +700,54 @@ export async function handleBrainTool(
|
|
|
676
700
|
provider,
|
|
677
701
|
}), 'EX', 86400); // auto-expire after 24h if session_end never called
|
|
678
702
|
|
|
703
|
+
// 8. Time saved counter
|
|
704
|
+
let timeSavedMins = 0;
|
|
705
|
+
try {
|
|
706
|
+
const raw = await redis.get(`cachly:stats:time_saved_mins:${instance_id}`);
|
|
707
|
+
timeSavedMins = parseFloat(raw ?? '0');
|
|
708
|
+
} catch { /* non-critical */ }
|
|
709
|
+
|
|
679
710
|
// ── Build briefing ──────────────────────────────────────────────────────
|
|
680
711
|
const providerLabel = provider ? ` · ${provider}` : '';
|
|
681
712
|
const lines: string[] = [`🧠 **Session Briefing**${providerLabel}`, ''];
|
|
682
713
|
if (streakMessage) lines.push(streakMessage, '');
|
|
683
714
|
|
|
715
|
+
// Time saved (only show when meaningful — 30+ minutes)
|
|
716
|
+
if (timeSavedMins >= 30) {
|
|
717
|
+
const h = Math.floor(timeSavedMins / 60);
|
|
718
|
+
const m = Math.round(timeSavedMins % 60);
|
|
719
|
+
const timeStr = h > 0 ? `${h}h${m > 0 ? ` ${m}m` : ''}` : `${m}m`;
|
|
720
|
+
lines.push(`⏱️ **Brain saved you ~${timeStr} total** (time not re-researching known fixes)`, '');
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// ── What's New (shown once per version update) ──────────────────────────
|
|
724
|
+
try {
|
|
725
|
+
const seenVersion = await redis.get('cachly:mcp:version:last_seen');
|
|
726
|
+
if (seenVersion !== MCP_VERSION) {
|
|
727
|
+
await redis.set('cachly:mcp:version:last_seen', MCP_VERSION, 'EX', 365 * 86400);
|
|
728
|
+
const changelog = WHATS_NEW[MCP_VERSION];
|
|
729
|
+
if (changelog) { lines.push(...changelog, ''); }
|
|
730
|
+
}
|
|
731
|
+
} catch { /* non-critical */ }
|
|
732
|
+
|
|
733
|
+
// ── First-time welcome (empty brain) ───────────────────────────────────
|
|
734
|
+
const isFirstSession = !lastSession && lessons.length === 0 && ctxCount === 0;
|
|
735
|
+
if (isFirstSession) {
|
|
736
|
+
lines.push('🎉 **Welcome! Your AI Brain is live.**', '');
|
|
737
|
+
lines.push('It learns from your work automatically. After your first session it will look like this:', '');
|
|
738
|
+
lines.push(' ✅ `api:auth` — Bearer token in header, not cookie; 401 on missing scope');
|
|
739
|
+
lines.push(' ✅ `database:migrations` — always run migrations before deploy');
|
|
740
|
+
lines.push(' ⚠️ `docker:build` — ARG changes bust all subsequent cache layers');
|
|
741
|
+
lines.push('');
|
|
742
|
+
lines.push('**3 steps to grow your brain:**');
|
|
743
|
+
lines.push(' 1. Fix something → `learn_from_attempts(topic="bug:name", outcome="success", what_worked="...")`');
|
|
744
|
+
lines.push(' 2. Save context → `remember_context(key="arch", value="...")`');
|
|
745
|
+
lines.push(' 3. End session → `session_end(summary="What I did")`');
|
|
746
|
+
lines.push('');
|
|
747
|
+
lines.push('💡 Run `brain_doctor` for a setup health-check and personalised tips.');
|
|
748
|
+
lines.push('');
|
|
749
|
+
}
|
|
750
|
+
|
|
684
751
|
// Handoff from previous window (if any)
|
|
685
752
|
const handoffRaw = await redis.get('cachly:session:handoff');
|
|
686
753
|
if (handoffRaw) {
|
|
@@ -820,7 +887,8 @@ export async function handleBrainTool(
|
|
|
820
887
|
for (const ck of conflictKeys.slice(0, 3)) {
|
|
821
888
|
const cr = await redis.get(ck);
|
|
822
889
|
if (!cr) continue;
|
|
823
|
-
const cf =
|
|
890
|
+
const cf = safeJsonParse<{ topic: string; fix_confidence: number; fix_trials: number } | null>(cr, null);
|
|
891
|
+
if (!cf) continue;
|
|
824
892
|
lines.push(` ⚠️ \`${cf.topic}\` — previously confirmed fix (${(cf.fix_confidence * 100).toFixed(0)}%, n=${cf.fix_trials}) now contradicted. Use \`ckg_inspect(concept="${ckgSlug(cf.topic)}")\``);
|
|
825
893
|
}
|
|
826
894
|
lines.push('');
|
|
@@ -879,6 +947,15 @@ export async function handleBrainTool(
|
|
|
879
947
|
lines.push('📭 No lessons yet. Use `learn_from_attempts` after solving tasks.', '');
|
|
880
948
|
}
|
|
881
949
|
|
|
950
|
+
// Team invite prompt — fires once after 10th lesson if no team use yet
|
|
951
|
+
if (lessons.length >= 10) {
|
|
952
|
+
const hasTeamUse = lessons.some(l => (l as typeof l & { author?: string }).author);
|
|
953
|
+
if (!hasTeamUse) {
|
|
954
|
+
lines.push(`🤝 **Your Brain has ${lessons.length} lessons — your team could benefit instantly.**`);
|
|
955
|
+
lines.push(` Share: **cachly.dev/team** · or run \`team_learn\` after the next fix.`, '');
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
882
959
|
// Open failures (lessons whose best-key has outcome != success)
|
|
883
960
|
const openFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial');
|
|
884
961
|
if (openFailures.length > 0) {
|
|
@@ -975,17 +1052,17 @@ export async function handleBrainTool(
|
|
|
975
1052
|
try {
|
|
976
1053
|
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
977
1054
|
if (crystalRaw) {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1055
|
+
type CrystalData = { label: string; ts: string; session_count: number; top_patterns: Array<{ category: string; insight: string; count: number }> };
|
|
1056
|
+
const crystal = safeJsonParse<CrystalData | null>(crystalRaw, null);
|
|
1057
|
+
if (crystal) {
|
|
1058
|
+
const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
|
|
1059
|
+
if (crystalAge <= 90) {
|
|
1060
|
+
lines.push(`💎 **Memory Crystal** (${crystal.label} · ${crystal.session_count} sessions compressed):`);
|
|
1061
|
+
for (const p of crystal.top_patterns.slice(0, 3)) {
|
|
1062
|
+
lines.push(` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 90)}`);
|
|
1063
|
+
}
|
|
1064
|
+
lines.push('');
|
|
987
1065
|
}
|
|
988
|
-
lines.push('');
|
|
989
1066
|
}
|
|
990
1067
|
}
|
|
991
1068
|
} catch { /* non-critical */ }
|
|
@@ -997,7 +1074,10 @@ export async function handleBrainTool(
|
|
|
997
1074
|
const PRIORITY_ORDER: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
998
1075
|
const PRIORITY_ICON: Record<string, string> = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
|
|
999
1076
|
const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
|
|
1000
|
-
const allItems = Object.values(roadmapAll).
|
|
1077
|
+
const allItems = Object.values(roadmapAll).flatMap(v => {
|
|
1078
|
+
const item = safeJsonParse<Record<string, unknown> | null>(v as string, null);
|
|
1079
|
+
return item ? [item] : [];
|
|
1080
|
+
});
|
|
1001
1081
|
const openItems = allItems
|
|
1002
1082
|
.filter(i => openStatuses.has(i.status as string))
|
|
1003
1083
|
.sort((a, b) => {
|
|
@@ -1037,20 +1117,20 @@ export async function handleBrainTool(
|
|
|
1037
1117
|
for (const nk of nodeKeys.slice(0, 3)) {
|
|
1038
1118
|
const nodeRaw = await redis.get(nk);
|
|
1039
1119
|
if (!nodeRaw) continue;
|
|
1040
|
-
const node
|
|
1120
|
+
const node = safeJsonParse<CKGNode | null>(nodeRaw, null);
|
|
1121
|
+
if (!node) continue;
|
|
1041
1122
|
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
1042
1123
|
for (const ek of edgeKeys.slice(0, 15)) {
|
|
1043
1124
|
const edgeRaw = await redis.get(ek);
|
|
1044
1125
|
if (!edgeRaw) continue;
|
|
1045
|
-
const edge
|
|
1126
|
+
const edge = safeJsonParse<CKGEdge | null>(edgeRaw, null);
|
|
1127
|
+
if (!edge) continue;
|
|
1046
1128
|
if (edge.edgeType !== 'causes' && edge.edgeType !== 'co-occurs' && edge.edgeType !== 'fixes') continue;
|
|
1047
1129
|
if (edge.confidence < 0.35) continue;
|
|
1048
|
-
// For "fixes" edges, the lesson is on the "from" node (fix for "to")
|
|
1049
|
-
// For "causes" edges, target is the failure mode
|
|
1050
1130
|
const lessonKey = edge.edgeType === 'fixes' ? `cachly:lesson:best:${edge.from}` : `cachly:lesson:best:${edge.to}`;
|
|
1051
1131
|
const lessonRaw = await redis.get(lessonKey);
|
|
1052
|
-
const lesson =
|
|
1053
|
-
ppePredictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson });
|
|
1132
|
+
const lesson = safeJsonParse<{ what_worked?: string; topic: string } | null>(lessonRaw, null);
|
|
1133
|
+
ppePredictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson: lesson ?? undefined });
|
|
1054
1134
|
}
|
|
1055
1135
|
}
|
|
1056
1136
|
}
|
|
@@ -1081,7 +1161,7 @@ export async function handleBrainTool(
|
|
|
1081
1161
|
// Measures: of recalled lessons with confidence > 0.85, what % had outcome=success?
|
|
1082
1162
|
try {
|
|
1083
1163
|
const calRaw = await redis.get('cachly:mcm:calibration:last');
|
|
1084
|
-
const lastCalMs =
|
|
1164
|
+
const lastCalMs = safeJsonParse<{ ts?: number } | null>(calRaw, null)?.ts ?? 0;
|
|
1085
1165
|
const daysSinceCal = (Date.now() - lastCalMs) / 86_400_000;
|
|
1086
1166
|
if (daysSinceCal >= 30 && lessons.length >= 5) {
|
|
1087
1167
|
// Quick calibration pass on recalled lessons (recall_count > 0, outcome=success)
|
|
@@ -1125,8 +1205,6 @@ export async function handleBrainTool(
|
|
|
1125
1205
|
} catch { /* non-critical */ }
|
|
1126
1206
|
|
|
1127
1207
|
// ── 🌍 Knowledge Commons — community stats banner ───────────────────────
|
|
1128
|
-
// Fetch syndication stats and show a 1-liner: total lessons + confirms + weekly growth.
|
|
1129
|
-
// Non-fatal: never blocks session start if the API call fails.
|
|
1130
1208
|
try {
|
|
1131
1209
|
const commonsStats = await apiFetch<{
|
|
1132
1210
|
total_lessons: number;
|
|
@@ -1143,6 +1221,17 @@ export async function handleBrainTool(
|
|
|
1143
1221
|
}
|
|
1144
1222
|
} catch { /* non-critical — never block session start */ }
|
|
1145
1223
|
|
|
1224
|
+
// ── Brain Doctor hint (Punkt 6) — surface when brain needs attention ───
|
|
1225
|
+
const hasOpenFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial').length > 0;
|
|
1226
|
+
const hasStaleLessons = lessons.some(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
|
|
1227
|
+
if (lessons.length === 0 || hasOpenFailures || hasStaleLessons) {
|
|
1228
|
+
const reasons: string[] = [];
|
|
1229
|
+
if (lessons.length === 0) reasons.push('brain is empty');
|
|
1230
|
+
if (hasOpenFailures) reasons.push('open failures');
|
|
1231
|
+
if (hasStaleLessons) reasons.push('stale lessons');
|
|
1232
|
+
lines.push(`🩺 _Run \`brain_doctor\` to fix: ${reasons.join(', ')}._`);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1146
1235
|
return lines.join('\n');
|
|
1147
1236
|
}
|
|
1148
1237
|
|
|
@@ -1168,18 +1257,23 @@ export async function handleBrainTool(
|
|
|
1168
1257
|
// Calculate duration from session_start marker
|
|
1169
1258
|
let durationMin: number | undefined;
|
|
1170
1259
|
const currentRaw = await redis.get('cachly:session:current');
|
|
1171
|
-
if (currentRaw) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1260
|
+
if (!currentRaw) {
|
|
1261
|
+
return `⚠️ No active session found.\n\nRun \`session_start\` first to begin tracking a session, then call \`session_end\` when you're done.`;
|
|
1262
|
+
}
|
|
1263
|
+
const current = safeJsonParse<{ started: string } | null>(currentRaw, null);
|
|
1264
|
+
if (current?.started) {
|
|
1265
|
+
durationMin = Math.round((now.getTime() - new Date(current.started).getTime()) / 60000);
|
|
1176
1266
|
}
|
|
1177
1267
|
|
|
1178
1268
|
// ── Session Replay: capture decision log ─────────────────────────────
|
|
1179
|
-
|
|
1269
|
+
type DecisionEntry = { ts: string; topic: string; outcome: string; what_worked: string };
|
|
1270
|
+
let decisionLog: DecisionEntry[] = [];
|
|
1180
1271
|
try {
|
|
1181
1272
|
const dlEntries = await redis.lrange('cachly:session:decision-log', 0, -1);
|
|
1182
|
-
decisionLog = dlEntries.
|
|
1273
|
+
decisionLog = dlEntries.flatMap(e => {
|
|
1274
|
+
const entry = safeJsonParse<DecisionEntry | null>(e, null);
|
|
1275
|
+
return entry ? [entry] : [];
|
|
1276
|
+
});
|
|
1183
1277
|
await redis.del('cachly:session:decision-log');
|
|
1184
1278
|
} catch { /* non-critical */ }
|
|
1185
1279
|
|