@cachly-dev/mcp-server 0.9.1 → 0.9.2
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/CHANGELOG.md +0 -28
- package/Dockerfile +2 -4
- package/README.md +173 -310
- package/dist/index.js +419 -329
- package/dist/index.js.map +1 -1
- package/package.json +4 -10
- package/server.json +3 -3
- package/src/index.ts +448 -356
- package/test-features.cjs +23 -0
- package/.github/workflows/publish.yml +0 -42
- package/LICENSE +0 -202
- package/glama.json +0 -6
package/src/index.ts
CHANGED
|
@@ -55,7 +55,7 @@ import { Redis } from 'ioredis';
|
|
|
55
55
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
56
56
|
const JWT = process.env.CACHLY_JWT ?? '';
|
|
57
57
|
const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
58
|
-
const CURRENT_VERSION = '0.
|
|
58
|
+
const CURRENT_VERSION = '0.9.2';
|
|
59
59
|
/**
|
|
60
60
|
* ┌──────────────────────────────────────────────────────────────────────┐
|
|
61
61
|
* │ EMBEDDING PROVIDER — pluggable, client-side first │
|
|
@@ -2194,6 +2194,58 @@ async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T>
|
|
|
2194
2194
|
|
|
2195
2195
|
import { detectNamespace } from './namespace.js';
|
|
2196
2196
|
|
|
2197
|
+
// ── Layer 1: Causal Knowledge Graph (CKG) helpers ────────────────────────────
|
|
2198
|
+
// Implements the CKG from the 10x Vision Document.
|
|
2199
|
+
// Nodes: cachly:ckg:node:{id} → { id, domain, type, count, ts }
|
|
2200
|
+
// Edges: cachly:ckg:edge:{from}:{edgeType}:{to} → { from, to, edgeType, successes, trials, confidence, last_updated }
|
|
2201
|
+
|
|
2202
|
+
type CKGEdge = {
|
|
2203
|
+
from: string; to: string; edgeType: string;
|
|
2204
|
+
successes: number; trials: number; confidence: number; last_updated: string;
|
|
2205
|
+
};
|
|
2206
|
+
type CKGNode = { id: string; domain: string; type: string; count: number; ts: string };
|
|
2207
|
+
|
|
2208
|
+
const STOPWORDS_CKG = new Set(['that','this','with','from','when','then','also','have','been','will','were','they','them','than','more','some','into','over','only','just','where','while','which','there','their','would','could','should','after','before','about']);
|
|
2209
|
+
|
|
2210
|
+
function ckgSlug(text: string): string {
|
|
2211
|
+
return text.toLowerCase().replace(/[^a-z0-9\-:]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 80);
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
/** Extract 1-3 significant keywords from free text for a problem concept */
|
|
2215
|
+
function extractProblemConcept(text: string): string | null {
|
|
2216
|
+
const words = text.toLowerCase()
|
|
2217
|
+
.replace(/[^a-z0-9\s\-_]/g, ' ').split(/\s+/)
|
|
2218
|
+
.filter(w => w.length > 3 && !STOPWORDS_CKG.has(w))
|
|
2219
|
+
.slice(0, 5);
|
|
2220
|
+
if (words.length === 0) return null;
|
|
2221
|
+
return words.slice(0, 2).join('-');
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
async function ckgUpsertNode(redis: Redis, id: string, domain: string, type: string): Promise<void> {
|
|
2225
|
+
const key = `cachly:ckg:node:${id}`;
|
|
2226
|
+
const raw = await redis.get(key);
|
|
2227
|
+
const node: CKGNode = raw ? JSON.parse(raw) : { id, domain, type, count: 0, ts: new Date().toISOString() };
|
|
2228
|
+
node.count = (node.count || 0) + 1;
|
|
2229
|
+
node.ts = new Date().toISOString();
|
|
2230
|
+
await redis.set(key, JSON.stringify(node));
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
async function ckgUpdateEdge(redis: Redis, from: string, edgeType: string, to: string, success: boolean, partial = false): Promise<void> {
|
|
2234
|
+
const key = `cachly:ckg:edge:${from}:${edgeType}:${to}`;
|
|
2235
|
+
const raw = await redis.get(key);
|
|
2236
|
+
const edge: CKGEdge = raw ? JSON.parse(raw) : { from, to, edgeType, successes: 0, trials: 0, confidence: 0, last_updated: '' };
|
|
2237
|
+
edge.trials = (edge.trials || 0) + 1;
|
|
2238
|
+
if (success) edge.successes = (edge.successes || 0) + 1;
|
|
2239
|
+
else if (partial) edge.successes = (edge.successes || 0) + 0.5;
|
|
2240
|
+
// Beta distribution smoothed confidence: (successes+1) / (trials+2)
|
|
2241
|
+
edge.confidence = (edge.successes + 1) / (edge.trials + 2);
|
|
2242
|
+
edge.last_updated = new Date().toISOString();
|
|
2243
|
+
await redis.set(key, JSON.stringify(edge));
|
|
2244
|
+
// Index: set of edge keys per source node (for fast traversal)
|
|
2245
|
+
await redis.sadd(`cachly:ckg:idx:from:${from}`, key);
|
|
2246
|
+
await redis.sadd(`cachly:ckg:idx:to:${to}`, key);
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2197
2249
|
// ── Tools ─────────────────────────────────────────────────────────────────────
|
|
2198
2250
|
|
|
2199
2251
|
const TOOLS = [
|
|
@@ -3037,22 +3089,6 @@ const TOOLS = [
|
|
|
3037
3089
|
required: ['instance_id'],
|
|
3038
3090
|
},
|
|
3039
3091
|
},
|
|
3040
|
-
// ── crystal_view — inspect current Memory Crystal ──────────────────────
|
|
3041
|
-
{
|
|
3042
|
-
name: 'crystal_view',
|
|
3043
|
-
description:
|
|
3044
|
-
'View the current Memory Crystal — the distilled snapshot of everything your AI brain has learned. ' +
|
|
3045
|
-
'Shows all top patterns grouped by category, lesson count, crystal age, and when to refresh. ' +
|
|
3046
|
-
'A crystal is created by memory_crystalize and injected into every session_start automatically. ' +
|
|
3047
|
-
'Call this to review what institutional knowledge is preserved, or to decide if a refresh is due.',
|
|
3048
|
-
inputSchema: {
|
|
3049
|
-
type: 'object',
|
|
3050
|
-
properties: {
|
|
3051
|
-
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
3052
|
-
},
|
|
3053
|
-
required: ['instance_id'],
|
|
3054
|
-
},
|
|
3055
|
-
},
|
|
3056
3092
|
// ── Roadmap — Persistent project plan tracker ───────────────────────────
|
|
3057
3093
|
{
|
|
3058
3094
|
name: 'roadmap_add',
|
|
@@ -3572,60 +3608,53 @@ const TOOLS = [
|
|
|
3572
3608
|
required: [],
|
|
3573
3609
|
},
|
|
3574
3610
|
},
|
|
3575
|
-
// ──
|
|
3611
|
+
// ── Layer 1: Causal Knowledge Graph ────────────────────────────────────────
|
|
3576
3612
|
{
|
|
3577
|
-
name: '
|
|
3613
|
+
name: 'brain_search',
|
|
3578
3614
|
description:
|
|
3579
|
-
'
|
|
3580
|
-
'
|
|
3581
|
-
'
|
|
3582
|
-
'Unlike session_start, this is instant — no full brain scan, no streak logic, just the critical recovery data. ' +
|
|
3583
|
-
'Typical use in CLAUDE.md: "If context is compacted, call compact_recover immediately."',
|
|
3615
|
+
'BM25+ full-text search over ALL brain data: lessons, context entries, session history, CKG nodes, roadmap items. ' +
|
|
3616
|
+
'Unlike smart_recall (which focuses on lessons + context), brain_search casts a wider net. ' +
|
|
3617
|
+
'Use when smart_recall returns nothing or when you want to find anything the brain knows about a topic.',
|
|
3584
3618
|
inputSchema: {
|
|
3585
3619
|
type: 'object',
|
|
3586
3620
|
properties: {
|
|
3587
|
-
instance_id: { type: 'string', description: '
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
description: 'What you are trying to recover context for — used to surface relevant lessons (optional)',
|
|
3591
|
-
},
|
|
3621
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3622
|
+
query: { type: 'string', description: 'What to search for' },
|
|
3623
|
+
limit: { type: 'number', description: 'Max results (default: 15)' },
|
|
3592
3624
|
},
|
|
3593
|
-
required: ['instance_id'],
|
|
3625
|
+
required: ['instance_id', 'query'],
|
|
3594
3626
|
},
|
|
3595
3627
|
},
|
|
3596
|
-
// ── v0.9 — brain_from_git ─────────────────────────────────────────────────
|
|
3597
3628
|
{
|
|
3598
|
-
name: '
|
|
3629
|
+
name: 'ckg_inspect',
|
|
3599
3630
|
description:
|
|
3600
|
-
'
|
|
3601
|
-
'
|
|
3602
|
-
'
|
|
3603
|
-
'Run once to pre-load months of team knowledge before the first session_start. ' +
|
|
3604
|
-
'Skips commits already in the Brain (safe to re-run). ' +
|
|
3605
|
-
'Example: brain_from_git(instance_id="...", workspace_path="/home/you/project", days=180) ' +
|
|
3606
|
-
'→ "📊 Scanned 342 commits · ✅ 28 lessons stored · ❌ 9 failure lessons · 0 duplicates skipped"',
|
|
3631
|
+
'Inspect the Causal Knowledge Graph (CKG) for a concept. Shows all typed edges (fixes, requires, co-occurs, causes) ' +
|
|
3632
|
+
'with Bayesian confidence scores. Use to understand what the brain knows about a topic and which fixes have the ' +
|
|
3633
|
+
'highest confidence. Also shows related concepts via graph traversal.',
|
|
3607
3634
|
inputSchema: {
|
|
3608
3635
|
type: 'object',
|
|
3609
3636
|
properties: {
|
|
3610
|
-
instance_id: { type: 'string', description: '
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
description: 'Absolute path to the git repository root (e.g. "/home/you/myproject")',
|
|
3614
|
-
},
|
|
3615
|
-
days: {
|
|
3616
|
-
type: 'number',
|
|
3617
|
-
description: 'How many days of history to scan (default: 180, max: 365)',
|
|
3618
|
-
},
|
|
3619
|
-
max_commits: {
|
|
3620
|
-
type: 'number',
|
|
3621
|
-
description: 'Maximum number of commits to process (default: 200, max: 500)',
|
|
3622
|
-
},
|
|
3623
|
-
author: {
|
|
3624
|
-
type: 'string',
|
|
3625
|
-
description: 'Filter to commits by this git author email or name (optional — omit to scan all authors)',
|
|
3626
|
-
},
|
|
3637
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3638
|
+
concept: { type: 'string', description: 'Concept to inspect, e.g. "fix:clickhouse-ipv6" or "docker"' },
|
|
3639
|
+
max_hops: { type: 'number', description: 'Traversal depth (default: 2)' },
|
|
3627
3640
|
},
|
|
3628
|
-
required: ['instance_id', '
|
|
3641
|
+
required: ['instance_id', 'concept'],
|
|
3642
|
+
},
|
|
3643
|
+
},
|
|
3644
|
+
{
|
|
3645
|
+
name: 'brain_predict',
|
|
3646
|
+
description:
|
|
3647
|
+
'Predictive Pre-fetch Engine (PPE): given your current context (what you\'re working on), traverses the CKG ' +
|
|
3648
|
+
'to predict likely failures and pre-load relevant fixes. Returns top predicted pitfalls + highest-confidence fixes. ' +
|
|
3649
|
+
'Call at session_start when working on a specific feature or debugging area.',
|
|
3650
|
+
inputSchema: {
|
|
3651
|
+
type: 'object',
|
|
3652
|
+
properties: {
|
|
3653
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3654
|
+
context: { type: 'string', description: 'What you\'re working on, e.g. "upgrading Keycloak from 21 to 24"' },
|
|
3655
|
+
top_k: { type: 'number', description: 'Max predictions to return (default: 5)' },
|
|
3656
|
+
},
|
|
3657
|
+
required: ['instance_id', 'context'],
|
|
3629
3658
|
},
|
|
3630
3659
|
},
|
|
3631
3660
|
] as const;
|
|
@@ -4720,6 +4749,51 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
4720
4749
|
await redis.ltrim(dlKey, -50, -1);
|
|
4721
4750
|
} catch { /* non-critical */ }
|
|
4722
4751
|
|
|
4752
|
+
// ── Layer 1+2: CKG update (Causal Knowledge Graph + Belief Update Engine) ──
|
|
4753
|
+
try {
|
|
4754
|
+
const conceptId = ckgSlug(topic);
|
|
4755
|
+
const domain = topic.split(':')[0] ?? 'unknown';
|
|
4756
|
+
const conceptType = domain; // fix, debug, deploy, infra, api, etc.
|
|
4757
|
+
|
|
4758
|
+
// Upsert concept node
|
|
4759
|
+
await ckgUpsertNode(redis, conceptId, domain, conceptType);
|
|
4760
|
+
|
|
4761
|
+
// Tag co-occurrence edges
|
|
4762
|
+
for (const tag of tags) {
|
|
4763
|
+
const tagId = ckgSlug(`tag:${tag}`);
|
|
4764
|
+
await ckgUpsertNode(redis, tagId, 'tag', 'tag');
|
|
4765
|
+
await ckgUpdateEdge(redis, conceptId, 'co-occurs', tagId, outcome === 'success', outcome === 'partial');
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
// depends_on → requires edges (structural, always confidence 1.0 direction)
|
|
4769
|
+
for (const dep of depends_on) {
|
|
4770
|
+
const depId = ckgSlug(dep);
|
|
4771
|
+
await ckgUpdateEdge(redis, conceptId, 'requires', depId, true);
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4774
|
+
// fixes edge: if category=fix and outcome=success, link to problem concept
|
|
4775
|
+
if ((domain === 'fix' || domain === 'debug') && (outcome === 'success' || outcome === 'partial')) {
|
|
4776
|
+
const problemText = what_failed || ctx || '';
|
|
4777
|
+
const problemConcept = problemText ? extractProblemConcept(problemText) : null;
|
|
4778
|
+
if (problemConcept) {
|
|
4779
|
+
const problemId = ckgSlug(`problem:${problemConcept}`);
|
|
4780
|
+
await ckgUpsertNode(redis, problemId, 'problem', 'problem');
|
|
4781
|
+
await ckgUpdateEdge(redis, conceptId, 'fixes', problemId, outcome === 'success', outcome === 'partial');
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
|
|
4785
|
+
// causes edge: if outcome=failure, link topic concept to the problem context
|
|
4786
|
+
if (outcome === 'failure' && (what_failed || what_worked)) {
|
|
4787
|
+
const causeText = what_failed || what_worked;
|
|
4788
|
+
const causeConcept = extractProblemConcept(causeText);
|
|
4789
|
+
if (causeConcept) {
|
|
4790
|
+
const causeId = ckgSlug(`cause:${causeConcept}`);
|
|
4791
|
+
await ckgUpsertNode(redis, causeId, 'cause', 'cause');
|
|
4792
|
+
await ckgUpdateEdge(redis, conceptId, 'causes', causeId, false);
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
} catch { /* CKG updates are non-critical */ }
|
|
4796
|
+
|
|
4723
4797
|
const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
|
|
4724
4798
|
const sevEmoji = severity === 'critical' ? '🔴' : severity === 'major' ? '🟡' : '🟢';
|
|
4725
4799
|
const action = isUpdate ? 'updated' : 'stored';
|
|
@@ -5240,7 +5314,36 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
5240
5314
|
// Brain health
|
|
5241
5315
|
lines.push(`📊 **Brain:** ${lessons.length} lessons · ${ctxCount} context entries`, '');
|
|
5242
5316
|
|
|
5243
|
-
//
|
|
5317
|
+
// ── Layer 7: MCM Domain Coverage Map ────────────────────────────────────
|
|
5318
|
+
if (lessons.length >= 3) {
|
|
5319
|
+
const domainMap = new Map<string, { total: number; success: number; critical: number }>();
|
|
5320
|
+
for (const l of lessons) {
|
|
5321
|
+
const dom = l.topic.split(':')[0] ?? 'other';
|
|
5322
|
+
if (!domainMap.has(dom)) domainMap.set(dom, { total: 0, success: 0, critical: 0 });
|
|
5323
|
+
const d = domainMap.get(dom)!;
|
|
5324
|
+
d.total++;
|
|
5325
|
+
if (l.outcome === 'success') d.success++;
|
|
5326
|
+
if (l.severity === 'critical') d.critical++;
|
|
5327
|
+
}
|
|
5328
|
+
const sorted = [...domainMap.entries()].sort((a, b) => b[1].total - a[1].total).slice(0, 6);
|
|
5329
|
+
const hasContestedDomains = sorted.some(([, d]) => d.success < d.total * 0.4 && d.total >= 2);
|
|
5330
|
+
if (sorted.length > 0) {
|
|
5331
|
+
lines.push(`🗺️ **Knowledge Coverage:**`);
|
|
5332
|
+
for (const [dom, d] of sorted) {
|
|
5333
|
+
const pct = Math.round((d.success / d.total) * 100);
|
|
5334
|
+
const filled = Math.round(pct / 10);
|
|
5335
|
+
const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
|
|
5336
|
+
const flag = d.critical > 0 ? ' 🔴' : pct < 40 && d.total >= 2 ? ' ⚠️' : '';
|
|
5337
|
+
lines.push(` ${bar} ${dom.padEnd(18)} ${String(pct).padStart(3)}% (${d.success}/${d.total} confirmed)${flag}`);
|
|
5338
|
+
}
|
|
5339
|
+
if (hasContestedDomains) {
|
|
5340
|
+
lines.push(` ⚠️ _Some domains have contested beliefs — use \`ckg_inspect\` to review_`);
|
|
5341
|
+
}
|
|
5342
|
+
lines.push('');
|
|
5343
|
+
}
|
|
5344
|
+
}
|
|
5345
|
+
|
|
5346
|
+
|
|
5244
5347
|
if (focusLessons.length > 0) {
|
|
5245
5348
|
lines.push(`🎯 **Relevant for "${focus}":**`);
|
|
5246
5349
|
for (const l of focusLessons.slice(0, 4)) {
|
|
@@ -5605,21 +5708,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
5605
5708
|
} catch { /* git not available or not a repo — silent skip */ }
|
|
5606
5709
|
}
|
|
5607
5710
|
|
|
5608
|
-
// ── Crystal age check — remind if > 30 days old or missing ──────────────
|
|
5609
|
-
let crystalReminder = '';
|
|
5610
|
-
try {
|
|
5611
|
-
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
5612
|
-
if (!crystalRaw) {
|
|
5613
|
-
crystalReminder = `💎 **Memory Crystal:** None exists yet. Run \`memory_crystalize(instance_id="${instance_id}")\` to distill your brain into a permanent snapshot — injected into every future session automatically.`;
|
|
5614
|
-
} else {
|
|
5615
|
-
const crystal = JSON.parse(crystalRaw) as { ts: string; label?: string; lesson_count?: number };
|
|
5616
|
-
const crystalAgeDays = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
|
|
5617
|
-
if (crystalAgeDays >= 30) {
|
|
5618
|
-
crystalReminder = `💎 **Memory Crystal is ${crystalAgeDays} days old** (${crystal.label ?? 'unnamed'}). Run \`memory_crystalize(instance_id="${instance_id}")\` to refresh it with your latest ${crystal.lesson_count ?? '?'} lessons.`;
|
|
5619
|
-
}
|
|
5620
|
-
}
|
|
5621
|
-
} catch { /* non-critical */ }
|
|
5622
|
-
|
|
5623
5711
|
const durationStr = durationMin !== undefined ? ` · ${durationMin} min` : '';
|
|
5624
5712
|
return [
|
|
5625
5713
|
`✅ **Session saved**${durationStr}`,
|
|
@@ -5629,7 +5717,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
5629
5717
|
lessons_learned !== undefined ? `🧠 **Lessons stored:** ${lessons_learned}` : '',
|
|
5630
5718
|
autoLearned.length > 0 ? `🤖 **Auto-learned:** ${autoLearned.length} lessons extracted from summary (${autoLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${autoLearned.length > 3 ? '…' : ''})` : '',
|
|
5631
5719
|
ambientLearned.length > 0 ? `🌿 **Ambient git learning:** ${ambientLearned.length} commit${ambientLearned.length > 1 ? 's' : ''} auto-learned (${ambientLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${ambientLearned.length > 3 ? '…' : ''})` : '',
|
|
5632
|
-
crystalReminder,
|
|
5633
5720
|
``,
|
|
5634
5721
|
`💡 Next session: \`session_start(focus="...")\` to see this summary.`,
|
|
5635
5722
|
].filter(l => l !== '').join('\n');
|
|
@@ -6109,63 +6196,6 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
6109
6196
|
return lines.join('\n');
|
|
6110
6197
|
}
|
|
6111
6198
|
|
|
6112
|
-
// ── crystal_view — show current Memory Crystal ───────────────────────────
|
|
6113
|
-
case 'crystal_view': {
|
|
6114
|
-
const { instance_id } = args as { instance_id: string };
|
|
6115
|
-
const redis = await getConnection(instance_id);
|
|
6116
|
-
|
|
6117
|
-
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
6118
|
-
if (!crystalRaw) {
|
|
6119
|
-
return [
|
|
6120
|
-
`💎 **No Memory Crystal yet**`,
|
|
6121
|
-
``,
|
|
6122
|
-
`Your brain has no crystal — all knowledge lives in raw lessons only.`,
|
|
6123
|
-
``,
|
|
6124
|
-
`Run \`memory_crystalize(instance_id="${instance_id}")\` to:`,
|
|
6125
|
-
` • Distill everything learned into a permanent, compact snapshot`,
|
|
6126
|
-
` • Inject it into every future \`session_start\` automatically`,
|
|
6127
|
-
` • Preserve team knowledge even after lesson TTLs expire`,
|
|
6128
|
-
``,
|
|
6129
|
-
`💡 Recommended: run monthly or after any major milestone.`,
|
|
6130
|
-
].join('\n');
|
|
6131
|
-
}
|
|
6132
|
-
|
|
6133
|
-
const crystal = JSON.parse(crystalRaw) as {
|
|
6134
|
-
label: string; ts: string; session_count: number; lesson_count: number;
|
|
6135
|
-
top_patterns: Array<{ category: string; insight: string; count: number }>;
|
|
6136
|
-
categories: string[]; created_from: string;
|
|
6137
|
-
};
|
|
6138
|
-
|
|
6139
|
-
const ageDays = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
|
|
6140
|
-
const ageStr = ageDays === 0 ? 'today' : ageDays === 1 ? '1 day ago' : `${ageDays} days ago`;
|
|
6141
|
-
const freshness = ageDays < 14 ? '🟢 Fresh' : ageDays < 30 ? '🟡 Aging' : '🔴 Stale — refresh recommended';
|
|
6142
|
-
|
|
6143
|
-
const lines: string[] = [
|
|
6144
|
-
`💎 **Memory Crystal: ${crystal.label}**`,
|
|
6145
|
-
``,
|
|
6146
|
-
`📅 Created: **${ageStr}** · Freshness: ${freshness}`,
|
|
6147
|
-
`📊 Compressed: **${crystal.session_count} sessions** + **${crystal.lesson_count} lessons** → ${crystal.top_patterns.length} patterns`,
|
|
6148
|
-
`🗂️ Categories: ${crystal.categories.slice(0, 10).join(', ')}${crystal.categories.length > 10 ? `… +${crystal.categories.length - 10}` : ''}`,
|
|
6149
|
-
``,
|
|
6150
|
-
`**Top patterns by category:**`,
|
|
6151
|
-
``,
|
|
6152
|
-
];
|
|
6153
|
-
|
|
6154
|
-
for (const p of crystal.top_patterns) {
|
|
6155
|
-
lines.push(`**${p.category}** (${p.count} lesson${p.count > 1 ? 's' : ''})`);
|
|
6156
|
-
lines.push(` → ${p.insight.slice(0, 150)}${p.insight.length > 150 ? '…' : ''}`);
|
|
6157
|
-
lines.push('');
|
|
6158
|
-
}
|
|
6159
|
-
|
|
6160
|
-
if (ageDays >= 30) {
|
|
6161
|
-
lines.push(`---`, `⚠️ Crystal is ${ageDays} days old. Run \`memory_crystalize(instance_id="${instance_id}")\` to refresh with your latest lessons.`);
|
|
6162
|
-
} else {
|
|
6163
|
-
lines.push(`---`, `💡 This crystal is injected automatically into every \`session_start\`. Refresh monthly with \`memory_crystalize\`.`);
|
|
6164
|
-
}
|
|
6165
|
-
|
|
6166
|
-
return lines.filter(l => l !== undefined).join('\n');
|
|
6167
|
-
}
|
|
6168
|
-
|
|
6169
6199
|
case 'brain_doctor': {
|
|
6170
6200
|
const { instance_id, workspace_path: drWorkspacePath = '' } = args as { instance_id: string; workspace_path?: string };
|
|
6171
6201
|
const redis = await getConnection(instance_id);
|
|
@@ -7485,7 +7515,58 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
|
|
|
7485
7515
|
.split(/\s+/)
|
|
7486
7516
|
.filter(t => t.length > 2);
|
|
7487
7517
|
|
|
7488
|
-
|
|
7518
|
+
const SEV_ICON: Record<string, string> = { critical: '🔴', major: '🟠', minor: '🟡' };
|
|
7519
|
+
|
|
7520
|
+
// ── Layer 1: CKG graph traversal (if graph data exists) ─────────────────
|
|
7521
|
+
type CKGResult = { conceptId: string; edge: CKGEdge; lesson?: { topic: string; what_worked?: string; ts: string; outcome: string; recall_count?: number; severity?: string } };
|
|
7522
|
+
const ckgResults: CKGResult[] = [];
|
|
7523
|
+
try {
|
|
7524
|
+
for (const token of tokens.slice(0, 4)) {
|
|
7525
|
+
// Search for CKG nodes matching this token
|
|
7526
|
+
const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${ckgSlug(token)}`);
|
|
7527
|
+
const toKeys = await redis.smembers(`cachly:ckg:idx:to:${ckgSlug(token)}`);
|
|
7528
|
+
// Also try pattern: scan nodes containing the token
|
|
7529
|
+
const nodeKeys: string[] = [];
|
|
7530
|
+
const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 50 });
|
|
7531
|
+
await new Promise<void>((res, rej) => { nStream.on('data', (b: string[]) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
7532
|
+
for (const nodeKey of nodeKeys.slice(0, 10)) {
|
|
7533
|
+
const nodeRaw = await redis.get(nodeKey);
|
|
7534
|
+
if (!nodeRaw) continue;
|
|
7535
|
+
const node: CKGNode = JSON.parse(nodeRaw);
|
|
7536
|
+
// Get edges from this node
|
|
7537
|
+
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
7538
|
+
for (const ek of edgeKeys.slice(0, 20)) {
|
|
7539
|
+
const edgeRaw = await redis.get(ek);
|
|
7540
|
+
if (!edgeRaw) continue;
|
|
7541
|
+
const edge: CKGEdge = JSON.parse(edgeRaw);
|
|
7542
|
+
if (edge.edgeType !== 'fixes' && edge.edgeType !== 'requires') continue;
|
|
7543
|
+
// Find lesson for this concept
|
|
7544
|
+
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from.replace(/-/g, ':').replace(/^fix:/, 'fix:')}`);
|
|
7545
|
+
const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
|
|
7546
|
+
ckgResults.push({ conceptId: node.id, edge, lesson });
|
|
7547
|
+
}
|
|
7548
|
+
}
|
|
7549
|
+
for (const ek of [...fromKeys, ...toKeys].slice(0, 20)) {
|
|
7550
|
+
const edgeRaw = await redis.get(ek);
|
|
7551
|
+
if (!edgeRaw) continue;
|
|
7552
|
+
const edge: CKGEdge = JSON.parse(edgeRaw);
|
|
7553
|
+
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
|
|
7554
|
+
const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
|
|
7555
|
+
ckgResults.push({ conceptId: edge.from, edge, lesson });
|
|
7556
|
+
}
|
|
7557
|
+
}
|
|
7558
|
+
} catch { /* CKG traversal non-critical */ }
|
|
7559
|
+
|
|
7560
|
+
// Deduplicate CKG results and sort by confidence
|
|
7561
|
+
const ckgSeen = new Set<string>();
|
|
7562
|
+
const ckgDeduped = ckgResults.filter(r => {
|
|
7563
|
+
const key = `${r.edge.from}:${r.edge.edgeType}:${r.edge.to}`;
|
|
7564
|
+
if (ckgSeen.has(key)) return false;
|
|
7565
|
+
ckgSeen.add(key);
|
|
7566
|
+
return true;
|
|
7567
|
+
}).sort((a, b) => b.edge.confidence - a.edge.confidence).slice(0, max_depth);
|
|
7568
|
+
|
|
7569
|
+
// ── Layer 2 (fallback): text similarity over all lessons ─────────────────
|
|
7489
7570
|
let cursor = 0;
|
|
7490
7571
|
const lessonKeys: string[] = [];
|
|
7491
7572
|
do {
|
|
@@ -7507,7 +7588,6 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
|
|
|
7507
7588
|
if (!raw) continue;
|
|
7508
7589
|
try {
|
|
7509
7590
|
const l = JSON.parse(raw) as Lesson;
|
|
7510
|
-
// Skip if filter tags given and lesson has none of them
|
|
7511
7591
|
if (filterTags.length > 0 && !(l.tags ?? []).some((t: string) => filterTags.includes(t))) continue;
|
|
7512
7592
|
const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? '', l.context ?? '']
|
|
7513
7593
|
.join(' ').toLowerCase();
|
|
@@ -7515,11 +7595,10 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
|
|
|
7515
7595
|
if (score > 0) scored.push({ score, lesson: l, key: k });
|
|
7516
7596
|
} catch { /* skip */ }
|
|
7517
7597
|
}
|
|
7518
|
-
|
|
7519
7598
|
scored.sort((a, b) => b.score - a.score);
|
|
7520
7599
|
const chain = scored.slice(0, max_depth);
|
|
7521
7600
|
|
|
7522
|
-
if (chain.length === 0) {
|
|
7601
|
+
if (chain.length === 0 && ckgDeduped.length === 0) {
|
|
7523
7602
|
return [
|
|
7524
7603
|
`🔍 **Causal Trace: "${problem}"**`,
|
|
7525
7604
|
``,
|
|
@@ -7538,55 +7617,68 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
|
|
|
7538
7617
|
].join('\n');
|
|
7539
7618
|
}
|
|
7540
7619
|
|
|
7541
|
-
// Build causal chain narrative: failures first → then successes
|
|
7542
|
-
const failures = chain.filter(c => c.lesson.outcome !== 'success');
|
|
7543
|
-
const solutions = chain.filter(c => c.lesson.outcome === 'success');
|
|
7544
|
-
|
|
7545
|
-
const SEV_ICON: Record<string, string> = { critical: '🔴', major: '🟠', minor: '🟡' };
|
|
7546
|
-
|
|
7547
7620
|
const lines: string[] = [
|
|
7548
7621
|
`🔍 **Causal Trace: "${problem}"**`,
|
|
7549
7622
|
``,
|
|
7550
|
-
`Found **${chain.length}** related lessons. Reconstructed causal chain:`,
|
|
7551
|
-
``,
|
|
7552
7623
|
];
|
|
7553
7624
|
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
const
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7625
|
+
// Show CKG graph results first if available
|
|
7626
|
+
if (ckgDeduped.length > 0) {
|
|
7627
|
+
lines.push(`### 🕸️ CKG Graph (confidence-ranked)`);
|
|
7628
|
+
for (const r of ckgDeduped) {
|
|
7629
|
+
const confPct = Math.round(r.edge.confidence * 100);
|
|
7630
|
+
const confBar = '▓'.repeat(Math.round(confPct / 10)) + '░'.repeat(10 - Math.round(confPct / 10));
|
|
7631
|
+
lines.push(` ${r.edge.from} **→[${r.edge.edgeType}]→** ${r.edge.to}`);
|
|
7632
|
+
lines.push(` ${confBar} ${confPct}% confidence (${r.edge.successes}/${r.edge.trials} confirmed)`);
|
|
7633
|
+
if (r.lesson?.what_worked) lines.push(` ✅ Fix: ${r.lesson.what_worked.slice(0, 150)}`);
|
|
7634
|
+
lines.push('');
|
|
7635
|
+
}
|
|
7563
7636
|
}
|
|
7564
7637
|
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
lines.push(` ${i + 1}. ✅ \`${l.topic}\` — ${date} · recalled ${l.recall_count ?? 0}× `);
|
|
7571
|
-
if (l.what_worked) lines.push(` ${l.what_worked.slice(0, 200)}`);
|
|
7572
|
-
});
|
|
7573
|
-
lines.push('');
|
|
7574
|
-
}
|
|
7638
|
+
// Build text-based causal chain narrative
|
|
7639
|
+
if (chain.length > 0) {
|
|
7640
|
+
lines.push(ckgDeduped.length > 0 ? `### 📚 Text Search (${chain.length} related lessons)` : `Found **${chain.length}** related lessons. Reconstructed causal chain:`, '');
|
|
7641
|
+
const failures = chain.filter(c => c.lesson.outcome !== 'success');
|
|
7642
|
+
const solutions = chain.filter(c => c.lesson.outcome === 'success');
|
|
7575
7643
|
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7644
|
+
if (failures.length > 0) {
|
|
7645
|
+
lines.push(`**Root causes & failure chain:**`);
|
|
7646
|
+
failures.forEach((c, i) => {
|
|
7647
|
+
const l = c.lesson;
|
|
7648
|
+
const sev = SEV_ICON[l.severity ?? 'minor'] ?? '🟡';
|
|
7649
|
+
lines.push(`${i === 0 ? ' Root:' : ' → :'} ${sev} \`${l.topic}\``);
|
|
7650
|
+
if (l.what_failed) lines.push(` ↳ ${l.what_failed.slice(0, 120)}`);
|
|
7651
|
+
});
|
|
7652
|
+
lines.push('');
|
|
7653
|
+
}
|
|
7654
|
+
|
|
7655
|
+
if (solutions.length > 0) {
|
|
7656
|
+
lines.push(`**Solutions that worked before:**`);
|
|
7657
|
+
solutions.forEach((c, i) => {
|
|
7658
|
+
const l = c.lesson;
|
|
7659
|
+
const date = new Date(l.ts).toLocaleDateString('de-DE');
|
|
7660
|
+
lines.push(` ${i + 1}. ✅ \`${l.topic}\` — ${date} · recalled ${l.recall_count ?? 0}×`);
|
|
7661
|
+
if (l.what_worked) lines.push(` ${l.what_worked.slice(0, 200)}`);
|
|
7662
|
+
});
|
|
7663
|
+
lines.push('');
|
|
7664
|
+
}
|
|
7665
|
+
|
|
7666
|
+
const topSolution = solutions[0]?.lesson;
|
|
7667
|
+
if (topSolution?.what_worked) {
|
|
7668
|
+
lines.push(`**⚡ Most likely fix:**`);
|
|
7669
|
+
lines.push(`\`\`\``);
|
|
7670
|
+
lines.push(topSolution.what_worked.slice(0, 500));
|
|
7671
|
+
lines.push(`\`\`\``);
|
|
7672
|
+
lines.push('');
|
|
7673
|
+
}
|
|
7583
7674
|
}
|
|
7584
7675
|
|
|
7585
7676
|
lines.push(`💡 After applying: \`learn_from_attempts(topic="fix:${tokens[0] ?? 'issue'}", outcome="success", ...)\``);
|
|
7677
|
+
if (ckgDeduped.length > 0) lines.push(`🕸️ Explore graph: \`ckg_inspect(concept="${tokens[0] ?? 'fix'}")\``);
|
|
7586
7678
|
return lines.join('\n');
|
|
7587
7679
|
}
|
|
7588
7680
|
|
|
7589
|
-
|
|
7681
|
+
|
|
7590
7682
|
case 'knowledge_decay': {
|
|
7591
7683
|
const { instance_id, min_age_days = 0, show_top = 20 } = args as {
|
|
7592
7684
|
instance_id: string; min_age_days?: number; show_top?: number;
|
|
@@ -7682,11 +7774,9 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
|
|
|
7682
7774
|
> Sessions are fully automatic. The Brain starts when you call any tool and saves a summary when you close.
|
|
7683
7775
|
|
|
7684
7776
|
### MANDATORY hooks (always fire, no exceptions):
|
|
7685
|
-
- **Context compacted** → call \`compact_recover(instance_id="${instance_id}", focus="<current task>")\` FIRST
|
|
7686
7777
|
- **Window opens** → call \`session_start(instance_id="${instance_id}", focus="<today's goal>")\`
|
|
7687
7778
|
- **Bug fixed / deployed / discovered** → call \`learn_from_attempts(instance_id="${instance_id}", topic="fix:<area>", outcome="success", what_worked="...")\`
|
|
7688
7779
|
- **Window closes** → call \`session_end(instance_id="${instance_id}", summary="...", files_changed=[...])\`
|
|
7689
|
-
- **Monthly** → call \`memory_crystalize(instance_id="${instance_id}")\` to distill knowledge into a permanent Crystal
|
|
7690
7780
|
`;
|
|
7691
7781
|
|
|
7692
7782
|
const full = `## Cachly AI Brain — Autopilot for ${project_name}
|
|
@@ -7747,40 +7837,18 @@ smart_recall(
|
|
|
7747
7837
|
|
|
7748
7838
|
---
|
|
7749
7839
|
|
|
7750
|
-
## ⚡
|
|
7751
|
-
|
|
7752
|
-
**If your context window was compacted**, call this FIRST — before reading any file:
|
|
7753
|
-
\`\`\`
|
|
7754
|
-
compact_recover(
|
|
7755
|
-
instance_id = "${instance_id}",
|
|
7756
|
-
focus = "<what you were working on>",
|
|
7757
|
-
)
|
|
7758
|
-
\`\`\`
|
|
7759
|
-
Returns: last checkpoint, pending tasks, top lessons. Instant — no full brain scan.
|
|
7760
|
-
|
|
7761
|
-
---
|
|
7762
|
-
|
|
7763
|
-
## 🔁 Scheduled Maintenance (run on schedule)
|
|
7764
|
-
|
|
7765
|
-
| When | Call |
|
|
7766
|
-
|---|---|
|
|
7767
|
-
| **Monthly** | \`memory_crystalize(instance_id="${instance_id}")\` — distill brain into a permanent snapshot |
|
|
7768
|
-
| Weekly review | \`brain_diff(instance_id="${instance_id}", since="7d")\` — see what your AI learned this week |
|
|
7769
|
-
| Before big refactor | \`knowledge_decay(instance_id="${instance_id}")\` — see which lessons to still trust |
|
|
7770
|
-
| After major milestone | \`memory_crystalize(instance_id="${instance_id}", label="v2 launch")\` — preserve milestone knowledge |
|
|
7771
|
-
|
|
7772
|
-
## 🧠 Cognitive Tools (use anytime)
|
|
7840
|
+
## ⚡ Cognitive Tools (v0.6)
|
|
7773
7841
|
|
|
7774
7842
|
| When | Call |
|
|
7775
7843
|
|---|---|
|
|
7776
7844
|
| Brain feeling cluttered | \`memory_consolidate(instance_id="${instance_id}")\` |
|
|
7845
|
+
| Weekly review | \`brain_diff(instance_id="${instance_id}", since="7d")\` |
|
|
7777
7846
|
| Weird bug, no idea why | \`causal_trace(instance_id="${instance_id}", problem="<symptom>")\` |
|
|
7778
|
-
|
|
|
7779
|
-
| Unknown error that you solved | \`syndicate(instance_id="${instance_id}", topic="fix:<area>", what_worked="...")\` |
|
|
7847
|
+
| Before big refactor | \`knowledge_decay(instance_id="${instance_id}")\` |
|
|
7780
7848
|
|
|
7781
7849
|
---
|
|
7782
7850
|
|
|
7783
|
-
*Cachly v0.
|
|
7851
|
+
*Cachly v0.6 · Generated ${new Date().toISOString().slice(0, 10)}*
|
|
7784
7852
|
`;
|
|
7785
7853
|
|
|
7786
7854
|
const content = style === 'minimal' ? minimal : full;
|
|
@@ -8021,194 +8089,218 @@ Returns: last checkpoint, pending tasks, top lessons. Instant — no full brain
|
|
|
8021
8089
|
return lines.filter(l => l !== '').join('\n');
|
|
8022
8090
|
}
|
|
8023
8091
|
|
|
8024
|
-
// ──
|
|
8025
|
-
case '
|
|
8026
|
-
const { instance_id,
|
|
8092
|
+
// ── Layer 1: brain_search ─────────────────────────────────────────────────
|
|
8093
|
+
case 'brain_search': {
|
|
8094
|
+
const { instance_id, query, limit = 15 } = args as { instance_id: string; query: string; limit?: number };
|
|
8027
8095
|
const redis = await getConnection(instance_id);
|
|
8028
8096
|
|
|
8029
|
-
|
|
8097
|
+
// BM25+ over ALL brain key namespaces
|
|
8098
|
+
const allMatches = await keywordSearch(
|
|
8099
|
+
redis,
|
|
8100
|
+
[
|
|
8101
|
+
'cachly:lesson:best:*',
|
|
8102
|
+
'cachly:ctx:*',
|
|
8103
|
+
'cachly:idx:*',
|
|
8104
|
+
'cachly:session:last',
|
|
8105
|
+
'cachly:session:handoff',
|
|
8106
|
+
'cachly:roadmap:*',
|
|
8107
|
+
'cachly:ckg:node:*',
|
|
8108
|
+
],
|
|
8109
|
+
query,
|
|
8110
|
+
limit,
|
|
8111
|
+
);
|
|
8030
8112
|
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
if (checkpointRaw) {
|
|
8034
|
-
try {
|
|
8035
|
-
const cp = JSON.parse(checkpointRaw) as {
|
|
8036
|
-
ts: string; task: string; files_touched?: string[]; next_step?: string; provider?: string;
|
|
8037
|
-
};
|
|
8038
|
-
const ago = Math.round((Date.now() - new Date(cp.ts).getTime()) / 60000);
|
|
8039
|
-
const agoStr = ago < 60 ? `${ago}m ago` : `${Math.round(ago / 60)}h ago`;
|
|
8040
|
-
lines.push(`📌 **Last checkpoint** (${agoStr}): ${cp.task}`);
|
|
8041
|
-
if (cp.files_touched && cp.files_touched.length > 0) {
|
|
8042
|
-
lines.push(` Files: ${cp.files_touched.slice(0, 5).join(', ')}${cp.files_touched.length > 5 ? '…' : ''}`);
|
|
8043
|
-
}
|
|
8044
|
-
if (cp.next_step) lines.push(`📍 **Resume with:** ${cp.next_step}`);
|
|
8045
|
-
lines.push('');
|
|
8046
|
-
} catch { /* ignore corrupt */ }
|
|
8113
|
+
if (allMatches.length === 0) {
|
|
8114
|
+
return [`🔎 **Brain Search: "${query}"**`, '', `No results found across all brain data.`, '', `💡 Try \`smart_recall\` or check \`list_remembered\`.`].join('\n');
|
|
8047
8115
|
}
|
|
8048
8116
|
|
|
8049
|
-
|
|
8050
|
-
const
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
if (hf.blocked_on) { lines.push(`🚫 **Blocked on:** ${hf.blocked_on}`, ''); }
|
|
8062
|
-
} catch { /* ignore */ }
|
|
8117
|
+
const lines = [`🔎 **Brain Search: "${query}"** — ${allMatches.length} result${allMatches.length !== 1 ? 's' : ''} across all brain data\n`];
|
|
8118
|
+
for (const m of allMatches.slice(0, limit)) {
|
|
8119
|
+
const ns = m.key.startsWith('cachly:lesson:') ? '💡 lesson'
|
|
8120
|
+
: m.key.startsWith('cachly:ctx:') ? '📝 context'
|
|
8121
|
+
: m.key.startsWith('cachly:idx:') ? '📂 index'
|
|
8122
|
+
: m.key.startsWith('cachly:session:') ? '🕐 session'
|
|
8123
|
+
: m.key.startsWith('cachly:roadmap:') ? '🗺️ roadmap'
|
|
8124
|
+
: m.key.startsWith('cachly:ckg:node:') ? '🕸️ ckg-node'
|
|
8125
|
+
: '🗄️ data';
|
|
8126
|
+
const preview = m.content.slice(0, 280).replace(/\n/g, ' ');
|
|
8127
|
+
lines.push(`**${ns}** \`${m.key.split(':').slice(2).join(':')}\` _(BM25: ${m.score.toFixed(2)})_`);
|
|
8128
|
+
lines.push(`> ${preview}${m.content.length > 280 ? '…' : ''}\n`);
|
|
8063
8129
|
}
|
|
8130
|
+
return lines.join('\n');
|
|
8131
|
+
}
|
|
8064
8132
|
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
const
|
|
8068
|
-
await
|
|
8069
|
-
lStream.on('data', (batch: string[]) => lessonKeys.push(...batch));
|
|
8070
|
-
lStream.on('end', resolve);
|
|
8071
|
-
lStream.on('error', reject);
|
|
8072
|
-
});
|
|
8133
|
+
// ── Layer 1: ckg_inspect ─────────────────────────────────────────────────
|
|
8134
|
+
case 'ckg_inspect': {
|
|
8135
|
+
const { instance_id, concept, max_hops = 2 } = args as { instance_id: string; concept: string; max_hops?: number };
|
|
8136
|
+
const redis = await getConnection(instance_id);
|
|
8073
8137
|
|
|
8074
|
-
|
|
8075
|
-
const
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8138
|
+
const conceptId = ckgSlug(concept);
|
|
8139
|
+
const visited = new Set<string>();
|
|
8140
|
+
const allEdges: CKGEdge[] = [];
|
|
8141
|
+
|
|
8142
|
+
// BFS traversal of CKG
|
|
8143
|
+
const queue: Array<{ id: string; hop: number }> = [{ id: conceptId, hop: 0 }];
|
|
8144
|
+
while (queue.length > 0) {
|
|
8145
|
+
const { id, hop } = queue.shift()!;
|
|
8146
|
+
if (visited.has(id) || hop > max_hops) continue;
|
|
8147
|
+
visited.add(id);
|
|
8148
|
+
|
|
8149
|
+
const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${id}`);
|
|
8150
|
+
const toKeys = await redis.smembers(`cachly:ckg:idx:to:${id}`);
|
|
8151
|
+
|
|
8152
|
+
for (const ek of [...fromKeys, ...toKeys].slice(0, 50)) {
|
|
8153
|
+
const raw = await redis.get(ek);
|
|
8154
|
+
if (!raw) continue;
|
|
8155
|
+
const edge: CKGEdge = JSON.parse(raw);
|
|
8156
|
+
allEdges.push(edge);
|
|
8157
|
+
if (hop < max_hops) {
|
|
8158
|
+
if (!visited.has(edge.from)) queue.push({ id: edge.from, hop: hop + 1 });
|
|
8159
|
+
if (!visited.has(edge.to)) queue.push({ id: edge.to, hop: hop + 1 });
|
|
8160
|
+
}
|
|
8161
|
+
}
|
|
8080
8162
|
}
|
|
8081
8163
|
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
for
|
|
8092
|
-
|
|
8093
|
-
|
|
8164
|
+
if (allEdges.length === 0) {
|
|
8165
|
+
// Try fuzzy: scan for nodes matching the concept as substring
|
|
8166
|
+
const nodeKeys: string[] = [];
|
|
8167
|
+
const nStream = redis.scanStream({ match: `cachly:ckg:node:*${conceptId}*`, count: 100 });
|
|
8168
|
+
await new Promise<void>((res, rej) => { nStream.on('data', (b: string[]) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
8169
|
+
if (nodeKeys.length === 0) {
|
|
8170
|
+
return [`🕸️ **CKG Inspect: "${concept}"**`, '', `No CKG nodes found. The graph builds automatically as you call \`learn_from_attempts\`.`, '', `💡 Once you have lessons stored, CKG edges will appear here.`].join('\n');
|
|
8171
|
+
}
|
|
8172
|
+
const nodeList = nodeKeys.slice(0, 10).map(k => ` • \`${k.replace('cachly:ckg:node:', '')}\``).join('\n');
|
|
8173
|
+
return [`🕸️ **CKG Inspect: "${concept}"**`, '', `No edges found for \`${conceptId}\`, but found similar nodes:`, nodeList, '', `Try: \`ckg_inspect(concept="<exact-node-id>")\``].join('\n');
|
|
8174
|
+
}
|
|
8175
|
+
|
|
8176
|
+
// Sort by confidence desc, deduplicate
|
|
8177
|
+
const edgeSeen = new Set<string>();
|
|
8178
|
+
const unique = allEdges.filter(e => {
|
|
8179
|
+
const k = `${e.from}:${e.edgeType}:${e.to}`;
|
|
8180
|
+
if (edgeSeen.has(k)) return false;
|
|
8181
|
+
edgeSeen.add(k);
|
|
8182
|
+
return true;
|
|
8183
|
+
}).sort((a, b) => b.confidence - a.confidence);
|
|
8184
|
+
|
|
8185
|
+
const EDGE_ICON: Record<string, string> = { fixes: '🔧', requires: '🔗', 'co-occurs': '🔄', causes: '⚡', contradicts: '⚠️', degrades_under: '📉' };
|
|
8186
|
+
|
|
8187
|
+
const lines = [`🕸️ **CKG Inspect: "${concept}"** (${unique.length} edge${unique.length !== 1 ? 's' : ''}, ${visited.size} node${visited.size !== 1 ? 's' : ''} traversed)\n`];
|
|
8188
|
+
|
|
8189
|
+
// Group by edge type
|
|
8190
|
+
const byType = new Map<string, CKGEdge[]>();
|
|
8191
|
+
for (const e of unique) {
|
|
8192
|
+
if (!byType.has(e.edgeType)) byType.set(e.edgeType, []);
|
|
8193
|
+
byType.get(e.edgeType)!.push(e);
|
|
8194
|
+
}
|
|
8195
|
+
for (const [eType, edges] of byType) {
|
|
8196
|
+
const icon = EDGE_ICON[eType] ?? '→';
|
|
8197
|
+
lines.push(`**${icon} ${eType}** (${edges.length})`);
|
|
8198
|
+
for (const e of edges.slice(0, 8)) {
|
|
8199
|
+
const confPct = Math.round(e.confidence * 100);
|
|
8200
|
+
const bar = '▓'.repeat(Math.round(confPct / 10)) + '░'.repeat(10 - Math.round(confPct / 10));
|
|
8201
|
+
lines.push(` \`${e.from}\` → \`${e.to}\` ${bar} ${confPct}% (${e.successes.toFixed(1)}/${e.trials} trials)`);
|
|
8094
8202
|
}
|
|
8095
8203
|
lines.push('');
|
|
8096
8204
|
}
|
|
8097
8205
|
|
|
8098
|
-
lines.push(`💡
|
|
8099
|
-
return lines.
|
|
8206
|
+
lines.push(`💡 Expand: \`ckg_inspect(concept="${concept}", max_hops=3)\` | Predict: \`brain_predict(context="${concept}")\``);
|
|
8207
|
+
return lines.join('\n');
|
|
8100
8208
|
}
|
|
8101
8209
|
|
|
8102
|
-
// ──
|
|
8103
|
-
case '
|
|
8104
|
-
const {
|
|
8105
|
-
instance_id,
|
|
8106
|
-
workspace_path,
|
|
8107
|
-
days = 180,
|
|
8108
|
-
max_commits = 200,
|
|
8109
|
-
author = '',
|
|
8110
|
-
} = args as { instance_id: string; workspace_path: string; days?: number; max_commits?: number; author?: string };
|
|
8111
|
-
|
|
8210
|
+
// ── Layer 4: brain_predict (PPE) ─────────────────────────────────────────
|
|
8211
|
+
case 'brain_predict': {
|
|
8212
|
+
const { instance_id, context: ctx, top_k = 5 } = args as { instance_id: string; context: string; top_k?: number };
|
|
8112
8213
|
const redis = await getConnection(instance_id);
|
|
8113
|
-
const { execSync } = await import('node:child_process');
|
|
8114
|
-
|
|
8115
|
-
const clampedDays = Math.min(Math.max(1, days), 365);
|
|
8116
|
-
const clampedMax = Math.min(Math.max(1, max_commits), 500);
|
|
8117
8214
|
|
|
8118
|
-
const
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8215
|
+
const ctxTokens = ctx.toLowerCase().replace(/[^a-z0-9\s\-_:]/g, ' ').split(/\s+/).filter(t => t.length > 2);
|
|
8216
|
+
|
|
8217
|
+
// Step 1: Find CKG nodes matching context tokens
|
|
8218
|
+
type Prediction = { concept: string; edgeType: string; target: string; confidence: number; lesson?: { what_worked?: string; topic: string } };
|
|
8219
|
+
const predictions: Prediction[] = [];
|
|
8220
|
+
|
|
8221
|
+
for (const token of ctxTokens.slice(0, 6)) {
|
|
8222
|
+
const nodeKeys: string[] = [];
|
|
8223
|
+
const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 50 });
|
|
8224
|
+
await new Promise<void>((res, rej) => { nStream.on('data', (b: string[]) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
8225
|
+
|
|
8226
|
+
for (const nk of nodeKeys.slice(0, 5)) {
|
|
8227
|
+
const nodeRaw = await redis.get(nk);
|
|
8228
|
+
if (!nodeRaw) continue;
|
|
8229
|
+
const node: CKGNode = JSON.parse(nodeRaw);
|
|
8230
|
+
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
8231
|
+
for (const ek of edgeKeys.slice(0, 20)) {
|
|
8232
|
+
const edgeRaw = await redis.get(ek);
|
|
8233
|
+
if (!edgeRaw) continue;
|
|
8234
|
+
const edge: CKGEdge = JSON.parse(edgeRaw);
|
|
8235
|
+
// Only interested in fixes and co-occurs for prediction
|
|
8236
|
+
if (edge.edgeType !== 'fixes' && edge.edgeType !== 'co-occurs' && edge.edgeType !== 'causes') continue;
|
|
8237
|
+
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
|
|
8238
|
+
const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
|
|
8239
|
+
predictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson });
|
|
8240
|
+
}
|
|
8241
|
+
}
|
|
8127
8242
|
}
|
|
8128
8243
|
|
|
8129
|
-
|
|
8130
|
-
|
|
8244
|
+
// Step 2: Text-based fallback — scan lessons for matching topics
|
|
8245
|
+
const textPredictions: Array<{ topic: string; what_worked?: string; what_failed?: string; outcome: string; severity?: string; confidence: number }> = [];
|
|
8246
|
+
const lessonKeys: string[] = [];
|
|
8247
|
+
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
8248
|
+
await new Promise<void>((res, rej) => { lStream.on('data', (b: string[]) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
|
|
8249
|
+
for (const k of lessonKeys) {
|
|
8250
|
+
const raw = await redis.get(k);
|
|
8251
|
+
if (!raw) continue;
|
|
8252
|
+
try {
|
|
8253
|
+
const l = JSON.parse(raw) as { topic: string; what_worked?: string; what_failed?: string; outcome: string; severity?: string; ts: string; verified_at?: string; recall_count?: number };
|
|
8254
|
+
const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? ''].join(' ').toLowerCase();
|
|
8255
|
+
const score = ctxTokens.reduce((s, t) => s + (haystack.includes(t) ? 1 : 0), 0);
|
|
8256
|
+
if (score >= 1 && l.outcome !== 'failure') {
|
|
8257
|
+
const conf = calculateConfidence(l);
|
|
8258
|
+
textPredictions.push({ ...l, confidence: conf });
|
|
8259
|
+
}
|
|
8260
|
+
} catch { /* skip */ }
|
|
8131
8261
|
}
|
|
8262
|
+
textPredictions.sort((a, b) => b.confidence - a.confidence);
|
|
8132
8263
|
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8264
|
+
if (predictions.length === 0 && textPredictions.length === 0) {
|
|
8265
|
+
return [
|
|
8266
|
+
`🔮 **Brain Predict: "${ctx}"**`,
|
|
8267
|
+
``,
|
|
8268
|
+
`No predictions yet — the brain hasn't seen this domain.`,
|
|
8269
|
+
``,
|
|
8270
|
+
`💡 As you solve problems in this area and call \`learn_from_attempts\`, the CKG builds up and predictions become available.`,
|
|
8271
|
+
].join('\n');
|
|
8272
|
+
}
|
|
8138
8273
|
|
|
8139
|
-
|
|
8140
|
-
let skipped = 0;
|
|
8141
|
-
let failures = 0;
|
|
8142
|
-
const topLessons: string[] = [];
|
|
8143
|
-
|
|
8144
|
-
for (const line of commitLines) {
|
|
8145
|
-
const parts = line.split('|||');
|
|
8146
|
-
if (parts.length < 2) continue;
|
|
8147
|
-
const [hash, msg, dateStr] = parts;
|
|
8148
|
-
if (!msg || !actionRe.test(msg)) continue;
|
|
8149
|
-
|
|
8150
|
-
const isFail = failRe.test(msg);
|
|
8151
|
-
const cleanMsg = msg
|
|
8152
|
-
.replace(/^(fix|feat|chore|docs|test|ci|perf|refactor|build|revert|hotfix|bump)[:(\s!]+/i, '')
|
|
8153
|
-
.trim();
|
|
8154
|
-
if (!cleanMsg || cleanMsg.length < 5) continue;
|
|
8155
|
-
|
|
8156
|
-
const slug = cleanMsg
|
|
8157
|
-
.toLowerCase()
|
|
8158
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
8159
|
-
.replace(/^-+|-+$/g, '')
|
|
8160
|
-
.slice(0, 40);
|
|
8161
|
-
if (!slug) continue;
|
|
8274
|
+
const lines = [`🔮 **Brain Predict: "${ctx}"**\n`];
|
|
8162
8275
|
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
const
|
|
8166
|
-
if (
|
|
8167
|
-
|
|
8168
|
-
const lesson = {
|
|
8169
|
-
topic,
|
|
8170
|
-
outcome: isFail ? ('failure' as const) : ('success' as const),
|
|
8171
|
-
what_worked: isFail ? '' : cleanMsg.slice(0, 300),
|
|
8172
|
-
what_failed: isFail ? cleanMsg.slice(0, 300) : undefined,
|
|
8173
|
-
context: `Auto-learned from git commit ${(hash ?? '').slice(0, 7)}${dateStr ? ` at ${dateStr}` : ''} in ${workspace_path}${author ? ` (author: ${author})` : ''}`,
|
|
8174
|
-
severity: 'minor' as const,
|
|
8175
|
-
ts: dateStr ? new Date(dateStr).toISOString() : now.toISOString(),
|
|
8176
|
-
recall_count: 0,
|
|
8177
|
-
auto_learned: true,
|
|
8178
|
-
source: 'brain_from_git',
|
|
8179
|
-
version: 3,
|
|
8180
|
-
};
|
|
8276
|
+
// CKG-based predictions
|
|
8277
|
+
if (predictions.length > 0) {
|
|
8278
|
+
const pSeen = new Set<string>();
|
|
8279
|
+
const pUniq = predictions.filter(p => { const k = `${p.concept}:${p.edgeType}:${p.target}`; if (pSeen.has(k)) return false; pSeen.add(k); return true; })
|
|
8280
|
+
.sort((a, b) => b.confidence - a.confidence).slice(0, top_k);
|
|
8181
8281
|
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8282
|
+
lines.push(`### 🕸️ CKG Predictions (based on ${pUniq.length} known edges)`);
|
|
8283
|
+
for (const p of pUniq) {
|
|
8284
|
+
const confPct = Math.round(p.confidence * 100);
|
|
8285
|
+
const icon = p.edgeType === 'fixes' ? '🔧' : p.edgeType === 'co-occurs' ? '🔄' : '⚡';
|
|
8286
|
+
lines.push(`${icon} **${confPct}%** \`${p.concept}\` _${p.edgeType}_ \`${p.target}\``);
|
|
8287
|
+
if (p.lesson?.what_worked) lines.push(` ✅ ${p.lesson.what_worked.slice(0, 120)}`);
|
|
8288
|
+
}
|
|
8289
|
+
lines.push('');
|
|
8187
8290
|
}
|
|
8188
8291
|
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
``,
|
|
8197
|
-
];
|
|
8198
|
-
|
|
8199
|
-
if (topLessons.length > 0) {
|
|
8200
|
-
lines.push(`**Top lessons extracted:**`);
|
|
8201
|
-
lines.push(...topLessons);
|
|
8202
|
-
if (stored > 5) lines.push(` …and ${stored - 5} more`);
|
|
8292
|
+
// Text-based lesson predictions
|
|
8293
|
+
if (textPredictions.length > 0) {
|
|
8294
|
+
lines.push(`### 📚 Relevant Lessons (${Math.min(textPredictions.length, top_k)} pre-loaded)`);
|
|
8295
|
+
for (const l of textPredictions.slice(0, top_k)) {
|
|
8296
|
+
const confPct = Math.round(l.confidence * 100);
|
|
8297
|
+
lines.push(` ✅ **${confPct}%** \`${l.topic}\` — ${(l.what_worked ?? '').slice(0, 120)}`);
|
|
8298
|
+
}
|
|
8203
8299
|
lines.push('');
|
|
8204
8300
|
}
|
|
8205
8301
|
|
|
8206
|
-
lines.push(
|
|
8207
|
-
|
|
8208
|
-
`🔄 **Re-run anytime** — duplicates are safely skipped.`,
|
|
8209
|
-
);
|
|
8210
|
-
|
|
8211
|
-
return lines.filter(l => l !== '').join('\n');
|
|
8302
|
+
lines.push(`💡 Outcome confirmed? \`learn_from_attempts(topic="fix:...", outcome="success", ...)\` → improves future predictions`);
|
|
8303
|
+
return lines.join('\n');
|
|
8212
8304
|
}
|
|
8213
8305
|
|
|
8214
8306
|
default:
|