@cachly-dev/mcp-server 0.9.1 → 0.9.3
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 +42 -15
- package/Dockerfile +2 -4
- package/README.md +173 -310
- package/dist/index.js +1056 -312
- package/dist/index.js.map +1 -1
- package/package.json +4 -10
- package/server.json +3 -3
- package/src/index.ts +1136 -335
- package/test-features.cjs +23 -0
- package/.github/workflows/publish.yml +0 -42
- package/LICENSE +0 -202
- package/glama.json +0 -6
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createHash, createHmac } from 'node:crypto';
|
|
4
4
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
6
6
|
import { execSync } from 'node:child_process';
|
|
@@ -47,7 +47,7 @@ import { Redis } from 'ioredis';
|
|
|
47
47
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
48
48
|
const JWT = process.env.CACHLY_JWT ?? '';
|
|
49
49
|
const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
50
|
-
const CURRENT_VERSION = '0.
|
|
50
|
+
const CURRENT_VERSION = '0.9.3';
|
|
51
51
|
/**
|
|
52
52
|
* ┌──────────────────────────────────────────────────────────────────────┐
|
|
53
53
|
* │ EMBEDDING PROVIDER — pluggable, client-side first │
|
|
@@ -2171,6 +2171,45 @@ async function apiFetch(path, options = {}) {
|
|
|
2171
2171
|
return res.json();
|
|
2172
2172
|
}
|
|
2173
2173
|
import { detectNamespace } from './namespace.js';
|
|
2174
|
+
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']);
|
|
2175
|
+
function ckgSlug(text) {
|
|
2176
|
+
return text.toLowerCase().replace(/[^a-z0-9\-:]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 80);
|
|
2177
|
+
}
|
|
2178
|
+
/** Extract 1-3 significant keywords from free text for a problem concept */
|
|
2179
|
+
function extractProblemConcept(text) {
|
|
2180
|
+
const words = text.toLowerCase()
|
|
2181
|
+
.replace(/[^a-z0-9\s\-_]/g, ' ').split(/\s+/)
|
|
2182
|
+
.filter(w => w.length > 3 && !STOPWORDS_CKG.has(w))
|
|
2183
|
+
.slice(0, 5);
|
|
2184
|
+
if (words.length === 0)
|
|
2185
|
+
return null;
|
|
2186
|
+
return words.slice(0, 2).join('-');
|
|
2187
|
+
}
|
|
2188
|
+
async function ckgUpsertNode(redis, id, domain, type) {
|
|
2189
|
+
const key = `cachly:ckg:node:${id}`;
|
|
2190
|
+
const raw = await redis.get(key);
|
|
2191
|
+
const node = raw ? JSON.parse(raw) : { id, domain, type, count: 0, ts: new Date().toISOString() };
|
|
2192
|
+
node.count = (node.count || 0) + 1;
|
|
2193
|
+
node.ts = new Date().toISOString();
|
|
2194
|
+
await redis.set(key, JSON.stringify(node));
|
|
2195
|
+
}
|
|
2196
|
+
async function ckgUpdateEdge(redis, from, edgeType, to, success, partial = false) {
|
|
2197
|
+
const key = `cachly:ckg:edge:${from}:${edgeType}:${to}`;
|
|
2198
|
+
const raw = await redis.get(key);
|
|
2199
|
+
const edge = raw ? JSON.parse(raw) : { from, to, edgeType, successes: 0, trials: 0, confidence: 0, last_updated: '' };
|
|
2200
|
+
edge.trials = (edge.trials || 0) + 1;
|
|
2201
|
+
if (success)
|
|
2202
|
+
edge.successes = (edge.successes || 0) + 1;
|
|
2203
|
+
else if (partial)
|
|
2204
|
+
edge.successes = (edge.successes || 0) + 0.5;
|
|
2205
|
+
// Beta distribution smoothed confidence: (successes+1) / (trials+2)
|
|
2206
|
+
edge.confidence = (edge.successes + 1) / (edge.trials + 2);
|
|
2207
|
+
edge.last_updated = new Date().toISOString();
|
|
2208
|
+
await redis.set(key, JSON.stringify(edge));
|
|
2209
|
+
// Index: set of edge keys per source node (for fast traversal)
|
|
2210
|
+
await redis.sadd(`cachly:ckg:idx:from:${from}`, key);
|
|
2211
|
+
await redis.sadd(`cachly:ckg:idx:to:${to}`, key);
|
|
2212
|
+
}
|
|
2174
2213
|
// ── Tools ─────────────────────────────────────────────────────────────────────
|
|
2175
2214
|
const TOOLS = [
|
|
2176
2215
|
// ── Instance Management ──────────────────────────────────────────────────
|
|
@@ -2976,21 +3015,6 @@ const TOOLS = [
|
|
|
2976
3015
|
required: ['instance_id'],
|
|
2977
3016
|
},
|
|
2978
3017
|
},
|
|
2979
|
-
// ── crystal_view — inspect current Memory Crystal ──────────────────────
|
|
2980
|
-
{
|
|
2981
|
-
name: 'crystal_view',
|
|
2982
|
-
description: 'View the current Memory Crystal — the distilled snapshot of everything your AI brain has learned. ' +
|
|
2983
|
-
'Shows all top patterns grouped by category, lesson count, crystal age, and when to refresh. ' +
|
|
2984
|
-
'A crystal is created by memory_crystalize and injected into every session_start automatically. ' +
|
|
2985
|
-
'Call this to review what institutional knowledge is preserved, or to decide if a refresh is due.',
|
|
2986
|
-
inputSchema: {
|
|
2987
|
-
type: 'object',
|
|
2988
|
-
properties: {
|
|
2989
|
-
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
2990
|
-
},
|
|
2991
|
-
required: ['instance_id'],
|
|
2992
|
-
},
|
|
2993
|
-
},
|
|
2994
3018
|
// ── Roadmap — Persistent project plan tracker ───────────────────────────
|
|
2995
3019
|
{
|
|
2996
3020
|
name: 'roadmap_add',
|
|
@@ -3480,58 +3504,185 @@ const TOOLS = [
|
|
|
3480
3504
|
required: [],
|
|
3481
3505
|
},
|
|
3482
3506
|
},
|
|
3483
|
-
// ──
|
|
3507
|
+
// ── Layer 1: Causal Knowledge Graph ────────────────────────────────────────
|
|
3484
3508
|
{
|
|
3485
|
-
name: '
|
|
3486
|
-
description: '
|
|
3487
|
-
'
|
|
3488
|
-
'
|
|
3489
|
-
'Unlike session_start, this is instant — no full brain scan, no streak logic, just the critical recovery data. ' +
|
|
3490
|
-
'Typical use in CLAUDE.md: "If context is compacted, call compact_recover immediately."',
|
|
3509
|
+
name: 'brain_search',
|
|
3510
|
+
description: 'BM25+ full-text search over ALL brain data: lessons, context entries, session history, CKG nodes, roadmap items. ' +
|
|
3511
|
+
'Unlike smart_recall (which focuses on lessons + context), brain_search casts a wider net. ' +
|
|
3512
|
+
'Use when smart_recall returns nothing or when you want to find anything the brain knows about a topic.',
|
|
3491
3513
|
inputSchema: {
|
|
3492
3514
|
type: 'object',
|
|
3493
3515
|
properties: {
|
|
3494
|
-
instance_id: { type: 'string', description: '
|
|
3495
|
-
|
|
3516
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3517
|
+
query: { type: 'string', description: 'What to search for' },
|
|
3518
|
+
limit: { type: 'number', description: 'Max results (default: 15)' },
|
|
3519
|
+
},
|
|
3520
|
+
required: ['instance_id', 'query'],
|
|
3521
|
+
},
|
|
3522
|
+
},
|
|
3523
|
+
{
|
|
3524
|
+
name: 'ckg_inspect',
|
|
3525
|
+
description: 'Inspect the Causal Knowledge Graph (CKG) for a concept. Shows all typed edges (fixes, requires, co-occurs, causes) ' +
|
|
3526
|
+
'with Bayesian confidence scores. Use to understand what the brain knows about a topic and which fixes have the ' +
|
|
3527
|
+
'highest confidence. Also shows related concepts via graph traversal.',
|
|
3528
|
+
inputSchema: {
|
|
3529
|
+
type: 'object',
|
|
3530
|
+
properties: {
|
|
3531
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3532
|
+
concept: { type: 'string', description: 'Concept to inspect, e.g. "fix:clickhouse-ipv6" or "docker"' },
|
|
3533
|
+
max_hops: { type: 'number', description: 'Traversal depth (default: 2)' },
|
|
3534
|
+
},
|
|
3535
|
+
required: ['instance_id', 'concept'],
|
|
3536
|
+
},
|
|
3537
|
+
},
|
|
3538
|
+
{
|
|
3539
|
+
name: 'brain_predict',
|
|
3540
|
+
description: 'Predictive Pre-fetch Engine (PPE): given your current context (what you\'re working on), traverses the CKG ' +
|
|
3541
|
+
'to predict likely failures and pre-load relevant fixes. Returns top predicted pitfalls + highest-confidence fixes. ' +
|
|
3542
|
+
'Call at session_start when working on a specific feature or debugging area.',
|
|
3543
|
+
inputSchema: {
|
|
3544
|
+
type: 'object',
|
|
3545
|
+
properties: {
|
|
3546
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3547
|
+
context: { type: 'string', description: 'What you\'re working on, e.g. "upgrading Keycloak from 21 to 24"' },
|
|
3548
|
+
top_k: { type: 'number', description: 'Max predictions to return (default: 5)' },
|
|
3549
|
+
},
|
|
3550
|
+
required: ['instance_id', 'context'],
|
|
3551
|
+
},
|
|
3552
|
+
},
|
|
3553
|
+
// ── Layer 3: MADC ────────────────────────────────────────────────────────
|
|
3554
|
+
{
|
|
3555
|
+
name: 'madc_deliberate',
|
|
3556
|
+
description: 'Multi-Agent Deliberation Chamber (MADC — Layer 3): When conflicting lessons exist for a topic, ' +
|
|
3557
|
+
'run deliberation between 6 specialist expert agents (InfraAgent, AuthAgent, DeployAgent, DatabaseAgent, DebugAgent, APIAgent). ' +
|
|
3558
|
+
'Each agent votes based on its domain CKG coverage. Unanimous vote → loser superseded. ' +
|
|
3559
|
+
'Split vote → contested flag, causal_trace required before acting. ' +
|
|
3560
|
+
'Resolution stored as permanent CKG node. Called automatically when learn_from_attempts detects a contradiction.',
|
|
3561
|
+
inputSchema: {
|
|
3562
|
+
type: 'object',
|
|
3563
|
+
properties: {
|
|
3564
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3565
|
+
topic: { type: 'string', description: 'Topic to deliberate, e.g. "fix:jwks-rotation"' },
|
|
3566
|
+
context: { type: 'string', description: 'Optional context for the deliberation' },
|
|
3567
|
+
},
|
|
3568
|
+
required: ['instance_id', 'topic'],
|
|
3569
|
+
},
|
|
3570
|
+
},
|
|
3571
|
+
// ── Layer 5: CLS ─────────────────────────────────────────────────────────
|
|
3572
|
+
{
|
|
3573
|
+
name: 'cls_ingest',
|
|
3574
|
+
description: 'Continuous Learning Stream (CLS — Layer 5): Ingest learning signals WITHOUT explicit session_end calls. ' +
|
|
3575
|
+
'Sources: git_commit (commit message + files → CKG edges), ci_outcome (green/red build → confirms fix), ' +
|
|
3576
|
+
'ide_diagnostic (compiler error + fix pair → instant lesson). ' +
|
|
3577
|
+
'Install automatic ingestion with cls_install_hooks — brain learns from every commit and CI run.',
|
|
3578
|
+
inputSchema: {
|
|
3579
|
+
type: 'object',
|
|
3580
|
+
properties: {
|
|
3581
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3582
|
+
source: {
|
|
3496
3583
|
type: 'string',
|
|
3497
|
-
|
|
3584
|
+
enum: ['git_commit', 'ci_outcome', 'ide_diagnostic'],
|
|
3585
|
+
description: 'Event source type',
|
|
3586
|
+
},
|
|
3587
|
+
payload: {
|
|
3588
|
+
type: 'object',
|
|
3589
|
+
description: 'Event data. git_commit: {message, sha?, files?, diff?}. ' +
|
|
3590
|
+
'ci_outcome: {status, prev_status, job, context?}. ' +
|
|
3591
|
+
'ide_diagnostic: {error, fix, file?}',
|
|
3592
|
+
},
|
|
3593
|
+
},
|
|
3594
|
+
required: ['instance_id', 'source', 'payload'],
|
|
3595
|
+
},
|
|
3596
|
+
},
|
|
3597
|
+
{
|
|
3598
|
+
name: 'cls_install_hooks',
|
|
3599
|
+
description: 'Output a ready-to-install git post-commit hook + GitHub Actions step for Continuous Learning. ' +
|
|
3600
|
+
'Once installed, every git commit and CI build automatically feeds the brain — no session_end needed. ' +
|
|
3601
|
+
'Run once per repository.',
|
|
3602
|
+
inputSchema: {
|
|
3603
|
+
type: 'object',
|
|
3604
|
+
properties: {
|
|
3605
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3606
|
+
repo_path: { type: 'string', description: 'Path to repo root (default: current dir)' },
|
|
3607
|
+
hooks: {
|
|
3608
|
+
type: 'array',
|
|
3609
|
+
items: { type: 'string', enum: ['git', 'ci'] },
|
|
3610
|
+
description: 'Which hooks to output (default: ["git", "ci"])',
|
|
3498
3611
|
},
|
|
3499
3612
|
},
|
|
3500
3613
|
required: ['instance_id'],
|
|
3501
3614
|
},
|
|
3502
3615
|
},
|
|
3503
|
-
// ──
|
|
3616
|
+
// ── Layer 6: FedBrain ────────────────────────────────────────────────────
|
|
3504
3617
|
{
|
|
3505
|
-
name: '
|
|
3506
|
-
description: '
|
|
3507
|
-
'
|
|
3508
|
-
'
|
|
3509
|
-
'
|
|
3510
|
-
'Skips commits already in the Brain (safe to re-run). ' +
|
|
3511
|
-
'Example: brain_from_git(instance_id="...", workspace_path="/home/you/project", days=180) ' +
|
|
3512
|
-
'→ "📊 Scanned 342 commits · ✅ 28 lessons stored · ❌ 9 failure lessons · 0 duplicates skipped"',
|
|
3618
|
+
name: 'fedbrain_contribute',
|
|
3619
|
+
description: 'FedBrain (Layer 6): Contribute a lesson to the global Knowledge Commons with a cryptographic ' +
|
|
3620
|
+
'knowledge certificate. Certificate includes: domain fingerprint, confidence, outcome chain hash. ' +
|
|
3621
|
+
'Lessons with 10+ independent confirmations become Gold Standard. Context-weighted: ' +
|
|
3622
|
+
'other brains with similar tech stacks see your lesson ranked higher in fedbrain_search.',
|
|
3513
3623
|
inputSchema: {
|
|
3514
3624
|
type: 'object',
|
|
3515
3625
|
properties: {
|
|
3516
|
-
instance_id: { type: 'string', description: '
|
|
3517
|
-
|
|
3626
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3627
|
+
lesson_key: { type: 'string', description: 'Topic key to contribute, e.g. "fix:clickhouse-ipv6"' },
|
|
3628
|
+
visibility: {
|
|
3518
3629
|
type: 'string',
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
days: {
|
|
3522
|
-
type: 'number',
|
|
3523
|
-
description: 'How many days of history to scan (default: 180, max: 365)',
|
|
3630
|
+
enum: ['public', 'org_private'],
|
|
3631
|
+
description: 'Visibility (default: public)',
|
|
3524
3632
|
},
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3633
|
+
},
|
|
3634
|
+
required: ['instance_id', 'lesson_key'],
|
|
3635
|
+
},
|
|
3636
|
+
},
|
|
3637
|
+
{
|
|
3638
|
+
name: 'fedbrain_search',
|
|
3639
|
+
description: 'FedBrain context-weighted search: Search the global commons, weighting results by tech-stack similarity. ' +
|
|
3640
|
+
'Brains with matching domain context (Go/Kubernetes/Postgres) rank higher than unrelated stacks. ' +
|
|
3641
|
+
'Shows certificate provenance, confirm_count, and Gold Standard badges.',
|
|
3642
|
+
inputSchema: {
|
|
3643
|
+
type: 'object',
|
|
3644
|
+
properties: {
|
|
3645
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3646
|
+
query: { type: 'string', description: 'What to search for' },
|
|
3647
|
+
context_hints: {
|
|
3648
|
+
type: 'array',
|
|
3649
|
+
items: { type: 'string' },
|
|
3650
|
+
description: 'Your tech stack, e.g. ["go", "kubernetes", "postgres"]',
|
|
3528
3651
|
},
|
|
3529
|
-
|
|
3652
|
+
limit: { type: 'number', description: 'Max results (default: 10)' },
|
|
3653
|
+
},
|
|
3654
|
+
required: ['instance_id', 'query'],
|
|
3655
|
+
},
|
|
3656
|
+
},
|
|
3657
|
+
{
|
|
3658
|
+
name: 'fedbrain_confirm',
|
|
3659
|
+
description: 'Confirm that a syndicated lesson from the global commons worked for you. ' +
|
|
3660
|
+
'Propagates confirmation back — increments confirm_count on the knowledge certificate. ' +
|
|
3661
|
+
'Also updates your local CKG confidence. At 10 independent confirmations → Gold Standard.',
|
|
3662
|
+
inputSchema: {
|
|
3663
|
+
type: 'object',
|
|
3664
|
+
properties: {
|
|
3665
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3666
|
+
topic: { type: 'string', description: 'Topic of the lesson to confirm' },
|
|
3667
|
+
outcome: {
|
|
3530
3668
|
type: 'string',
|
|
3531
|
-
|
|
3669
|
+
enum: ['worked', 'partially_worked', 'did_not_work'],
|
|
3670
|
+
description: 'Did the lesson work for you?',
|
|
3532
3671
|
},
|
|
3533
3672
|
},
|
|
3534
|
-
required: ['instance_id', '
|
|
3673
|
+
required: ['instance_id', 'topic', 'outcome'],
|
|
3674
|
+
},
|
|
3675
|
+
},
|
|
3676
|
+
{
|
|
3677
|
+
name: 'fedbrain_status',
|
|
3678
|
+
description: 'Show your FedBrain federation status: lessons contributed to global commons, recent confirmations, ' +
|
|
3679
|
+
'Gold Standard lessons, pending propagations. Use to track your brain\'s global knowledge contribution.',
|
|
3680
|
+
inputSchema: {
|
|
3681
|
+
type: 'object',
|
|
3682
|
+
properties: {
|
|
3683
|
+
instance_id: { type: 'string', description: 'Brain instance ID' },
|
|
3684
|
+
},
|
|
3685
|
+
required: ['instance_id'],
|
|
3535
3686
|
},
|
|
3536
3687
|
},
|
|
3537
3688
|
];
|
|
@@ -4358,6 +4509,14 @@ async function handleTool(name, args) {
|
|
|
4358
4509
|
if (contradictionWarning.length > 0) {
|
|
4359
4510
|
// Store contradiction audit but don't block
|
|
4360
4511
|
auditTrail[auditTrail.length - 1].action = 'contradiction-resolved';
|
|
4512
|
+
// Layer 3: Write CKG contradicts edge for MADC to process
|
|
4513
|
+
try {
|
|
4514
|
+
const cId = ckgSlug(topic);
|
|
4515
|
+
const resId = ckgSlug(`resolution:${topic}`);
|
|
4516
|
+
await ckgUpdateEdge(redis, cId, 'contradicts', resId, false);
|
|
4517
|
+
}
|
|
4518
|
+
catch { /* non-critical */ }
|
|
4519
|
+
contradictionWarning.push(`🗳️ Run \`madc_deliberate(topic="${topic}")\` to resolve via expert agent voting.`);
|
|
4361
4520
|
}
|
|
4362
4521
|
}
|
|
4363
4522
|
catch { /* ignore parse error */ }
|
|
@@ -4442,6 +4601,46 @@ async function handleTool(name, args) {
|
|
|
4442
4601
|
await redis.ltrim(dlKey, -50, -1);
|
|
4443
4602
|
}
|
|
4444
4603
|
catch { /* non-critical */ }
|
|
4604
|
+
// ── Layer 1+2: CKG update (Causal Knowledge Graph + Belief Update Engine) ──
|
|
4605
|
+
try {
|
|
4606
|
+
const conceptId = ckgSlug(topic);
|
|
4607
|
+
const domain = topic.split(':')[0] ?? 'unknown';
|
|
4608
|
+
const conceptType = domain; // fix, debug, deploy, infra, api, etc.
|
|
4609
|
+
// Upsert concept node
|
|
4610
|
+
await ckgUpsertNode(redis, conceptId, domain, conceptType);
|
|
4611
|
+
// Tag co-occurrence edges
|
|
4612
|
+
for (const tag of tags) {
|
|
4613
|
+
const tagId = ckgSlug(`tag:${tag}`);
|
|
4614
|
+
await ckgUpsertNode(redis, tagId, 'tag', 'tag');
|
|
4615
|
+
await ckgUpdateEdge(redis, conceptId, 'co-occurs', tagId, outcome === 'success', outcome === 'partial');
|
|
4616
|
+
}
|
|
4617
|
+
// depends_on → requires edges (structural, always confidence 1.0 direction)
|
|
4618
|
+
for (const dep of depends_on) {
|
|
4619
|
+
const depId = ckgSlug(dep);
|
|
4620
|
+
await ckgUpdateEdge(redis, conceptId, 'requires', depId, true);
|
|
4621
|
+
}
|
|
4622
|
+
// fixes edge: if category=fix and outcome=success, link to problem concept
|
|
4623
|
+
if ((domain === 'fix' || domain === 'debug') && (outcome === 'success' || outcome === 'partial')) {
|
|
4624
|
+
const problemText = what_failed || ctx || '';
|
|
4625
|
+
const problemConcept = problemText ? extractProblemConcept(problemText) : null;
|
|
4626
|
+
if (problemConcept) {
|
|
4627
|
+
const problemId = ckgSlug(`problem:${problemConcept}`);
|
|
4628
|
+
await ckgUpsertNode(redis, problemId, 'problem', 'problem');
|
|
4629
|
+
await ckgUpdateEdge(redis, conceptId, 'fixes', problemId, outcome === 'success', outcome === 'partial');
|
|
4630
|
+
}
|
|
4631
|
+
}
|
|
4632
|
+
// causes edge: if outcome=failure, link topic concept to the problem context
|
|
4633
|
+
if (outcome === 'failure' && (what_failed || what_worked)) {
|
|
4634
|
+
const causeText = what_failed || what_worked;
|
|
4635
|
+
const causeConcept = extractProblemConcept(causeText);
|
|
4636
|
+
if (causeConcept) {
|
|
4637
|
+
const causeId = ckgSlug(`cause:${causeConcept}`);
|
|
4638
|
+
await ckgUpsertNode(redis, causeId, 'cause', 'cause');
|
|
4639
|
+
await ckgUpdateEdge(redis, conceptId, 'causes', causeId, false);
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
catch { /* CKG updates are non-critical */ }
|
|
4445
4644
|
const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
|
|
4446
4645
|
const sevEmoji = severity === 'critical' ? '🔴' : severity === 'major' ? '🟡' : '🟢';
|
|
4447
4646
|
const action = isUpdate ? 'updated' : 'stored';
|
|
@@ -4917,7 +5116,37 @@ async function handleTool(name, args) {
|
|
|
4917
5116
|
}
|
|
4918
5117
|
// Brain health
|
|
4919
5118
|
lines.push(`📊 **Brain:** ${lessons.length} lessons · ${ctxCount} context entries`, '');
|
|
4920
|
-
//
|
|
5119
|
+
// ── Layer 7: MCM Domain Coverage Map ────────────────────────────────────
|
|
5120
|
+
if (lessons.length >= 3) {
|
|
5121
|
+
const domainMap = new Map();
|
|
5122
|
+
for (const l of lessons) {
|
|
5123
|
+
const dom = l.topic.split(':')[0] ?? 'other';
|
|
5124
|
+
if (!domainMap.has(dom))
|
|
5125
|
+
domainMap.set(dom, { total: 0, success: 0, critical: 0 });
|
|
5126
|
+
const d = domainMap.get(dom);
|
|
5127
|
+
d.total++;
|
|
5128
|
+
if (l.outcome === 'success')
|
|
5129
|
+
d.success++;
|
|
5130
|
+
if (l.severity === 'critical')
|
|
5131
|
+
d.critical++;
|
|
5132
|
+
}
|
|
5133
|
+
const sorted = [...domainMap.entries()].sort((a, b) => b[1].total - a[1].total).slice(0, 6);
|
|
5134
|
+
const hasContestedDomains = sorted.some(([, d]) => d.success < d.total * 0.4 && d.total >= 2);
|
|
5135
|
+
if (sorted.length > 0) {
|
|
5136
|
+
lines.push(`🗺️ **Knowledge Coverage:**`);
|
|
5137
|
+
for (const [dom, d] of sorted) {
|
|
5138
|
+
const pct = Math.round((d.success / d.total) * 100);
|
|
5139
|
+
const filled = Math.round(pct / 10);
|
|
5140
|
+
const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
|
|
5141
|
+
const flag = d.critical > 0 ? ' 🔴' : pct < 40 && d.total >= 2 ? ' ⚠️' : '';
|
|
5142
|
+
lines.push(` ${bar} ${dom.padEnd(18)} ${String(pct).padStart(3)}% (${d.success}/${d.total} confirmed)${flag}`);
|
|
5143
|
+
}
|
|
5144
|
+
if (hasContestedDomains) {
|
|
5145
|
+
lines.push(` ⚠️ _Some domains have contested beliefs — use \`ckg_inspect\` to review_`);
|
|
5146
|
+
}
|
|
5147
|
+
lines.push('');
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
4921
5150
|
if (focusLessons.length > 0) {
|
|
4922
5151
|
lines.push(`🎯 **Relevant for "${focus}":**`);
|
|
4923
5152
|
for (const l of focusLessons.slice(0, 4)) {
|
|
@@ -5255,22 +5484,6 @@ async function handleTool(name, args) {
|
|
|
5255
5484
|
}
|
|
5256
5485
|
catch { /* git not available or not a repo — silent skip */ }
|
|
5257
5486
|
}
|
|
5258
|
-
// ── Crystal age check — remind if > 30 days old or missing ──────────────
|
|
5259
|
-
let crystalReminder = '';
|
|
5260
|
-
try {
|
|
5261
|
-
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
5262
|
-
if (!crystalRaw) {
|
|
5263
|
-
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.`;
|
|
5264
|
-
}
|
|
5265
|
-
else {
|
|
5266
|
-
const crystal = JSON.parse(crystalRaw);
|
|
5267
|
-
const crystalAgeDays = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
|
|
5268
|
-
if (crystalAgeDays >= 30) {
|
|
5269
|
-
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.`;
|
|
5270
|
-
}
|
|
5271
|
-
}
|
|
5272
|
-
}
|
|
5273
|
-
catch { /* non-critical */ }
|
|
5274
5487
|
const durationStr = durationMin !== undefined ? ` · ${durationMin} min` : '';
|
|
5275
5488
|
return [
|
|
5276
5489
|
`✅ **Session saved**${durationStr}`,
|
|
@@ -5280,7 +5493,6 @@ async function handleTool(name, args) {
|
|
|
5280
5493
|
lessons_learned !== undefined ? `🧠 **Lessons stored:** ${lessons_learned}` : '',
|
|
5281
5494
|
autoLearned.length > 0 ? `🤖 **Auto-learned:** ${autoLearned.length} lessons extracted from summary (${autoLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${autoLearned.length > 3 ? '…' : ''})` : '',
|
|
5282
5495
|
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 ? '…' : ''})` : '',
|
|
5283
|
-
crystalReminder,
|
|
5284
5496
|
``,
|
|
5285
5497
|
`💡 Next session: \`session_start(focus="...")\` to see this summary.`,
|
|
5286
5498
|
].filter(l => l !== '').join('\n');
|
|
@@ -5669,52 +5881,6 @@ async function handleTool(name, args) {
|
|
|
5669
5881
|
];
|
|
5670
5882
|
return lines.join('\n');
|
|
5671
5883
|
}
|
|
5672
|
-
// ── crystal_view — show current Memory Crystal ───────────────────────────
|
|
5673
|
-
case 'crystal_view': {
|
|
5674
|
-
const { instance_id } = args;
|
|
5675
|
-
const redis = await getConnection(instance_id);
|
|
5676
|
-
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
5677
|
-
if (!crystalRaw) {
|
|
5678
|
-
return [
|
|
5679
|
-
`💎 **No Memory Crystal yet**`,
|
|
5680
|
-
``,
|
|
5681
|
-
`Your brain has no crystal — all knowledge lives in raw lessons only.`,
|
|
5682
|
-
``,
|
|
5683
|
-
`Run \`memory_crystalize(instance_id="${instance_id}")\` to:`,
|
|
5684
|
-
` • Distill everything learned into a permanent, compact snapshot`,
|
|
5685
|
-
` • Inject it into every future \`session_start\` automatically`,
|
|
5686
|
-
` • Preserve team knowledge even after lesson TTLs expire`,
|
|
5687
|
-
``,
|
|
5688
|
-
`💡 Recommended: run monthly or after any major milestone.`,
|
|
5689
|
-
].join('\n');
|
|
5690
|
-
}
|
|
5691
|
-
const crystal = JSON.parse(crystalRaw);
|
|
5692
|
-
const ageDays = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
|
|
5693
|
-
const ageStr = ageDays === 0 ? 'today' : ageDays === 1 ? '1 day ago' : `${ageDays} days ago`;
|
|
5694
|
-
const freshness = ageDays < 14 ? '🟢 Fresh' : ageDays < 30 ? '🟡 Aging' : '🔴 Stale — refresh recommended';
|
|
5695
|
-
const lines = [
|
|
5696
|
-
`💎 **Memory Crystal: ${crystal.label}**`,
|
|
5697
|
-
``,
|
|
5698
|
-
`📅 Created: **${ageStr}** · Freshness: ${freshness}`,
|
|
5699
|
-
`📊 Compressed: **${crystal.session_count} sessions** + **${crystal.lesson_count} lessons** → ${crystal.top_patterns.length} patterns`,
|
|
5700
|
-
`🗂️ Categories: ${crystal.categories.slice(0, 10).join(', ')}${crystal.categories.length > 10 ? `… +${crystal.categories.length - 10}` : ''}`,
|
|
5701
|
-
``,
|
|
5702
|
-
`**Top patterns by category:**`,
|
|
5703
|
-
``,
|
|
5704
|
-
];
|
|
5705
|
-
for (const p of crystal.top_patterns) {
|
|
5706
|
-
lines.push(`**${p.category}** (${p.count} lesson${p.count > 1 ? 's' : ''})`);
|
|
5707
|
-
lines.push(` → ${p.insight.slice(0, 150)}${p.insight.length > 150 ? '…' : ''}`);
|
|
5708
|
-
lines.push('');
|
|
5709
|
-
}
|
|
5710
|
-
if (ageDays >= 30) {
|
|
5711
|
-
lines.push(`---`, `⚠️ Crystal is ${ageDays} days old. Run \`memory_crystalize(instance_id="${instance_id}")\` to refresh with your latest lessons.`);
|
|
5712
|
-
}
|
|
5713
|
-
else {
|
|
5714
|
-
lines.push(`---`, `💡 This crystal is injected automatically into every \`session_start\`. Refresh monthly with \`memory_crystalize\`.`);
|
|
5715
|
-
}
|
|
5716
|
-
return lines.filter(l => l !== undefined).join('\n');
|
|
5717
|
-
}
|
|
5718
5884
|
case 'brain_doctor': {
|
|
5719
5885
|
const { instance_id, workspace_path: drWorkspacePath = '' } = args;
|
|
5720
5886
|
const redis = await getConnection(instance_id);
|
|
@@ -6944,7 +7110,59 @@ async function handleBulkLockStream(name, args) {
|
|
|
6944
7110
|
.replace(/[^a-z0-9\s\-_:]/g, ' ')
|
|
6945
7111
|
.split(/\s+/)
|
|
6946
7112
|
.filter(t => t.length > 2);
|
|
6947
|
-
|
|
7113
|
+
const SEV_ICON = { critical: '🔴', major: '🟠', minor: '🟡' };
|
|
7114
|
+
const ckgResults = [];
|
|
7115
|
+
try {
|
|
7116
|
+
for (const token of tokens.slice(0, 4)) {
|
|
7117
|
+
// Search for CKG nodes matching this token
|
|
7118
|
+
const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${ckgSlug(token)}`);
|
|
7119
|
+
const toKeys = await redis.smembers(`cachly:ckg:idx:to:${ckgSlug(token)}`);
|
|
7120
|
+
// Also try pattern: scan nodes containing the token
|
|
7121
|
+
const nodeKeys = [];
|
|
7122
|
+
const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 50 });
|
|
7123
|
+
await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
7124
|
+
for (const nodeKey of nodeKeys.slice(0, 10)) {
|
|
7125
|
+
const nodeRaw = await redis.get(nodeKey);
|
|
7126
|
+
if (!nodeRaw)
|
|
7127
|
+
continue;
|
|
7128
|
+
const node = JSON.parse(nodeRaw);
|
|
7129
|
+
// Get edges from this node
|
|
7130
|
+
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
7131
|
+
for (const ek of edgeKeys.slice(0, 20)) {
|
|
7132
|
+
const edgeRaw = await redis.get(ek);
|
|
7133
|
+
if (!edgeRaw)
|
|
7134
|
+
continue;
|
|
7135
|
+
const edge = JSON.parse(edgeRaw);
|
|
7136
|
+
if (edge.edgeType !== 'fixes' && edge.edgeType !== 'requires')
|
|
7137
|
+
continue;
|
|
7138
|
+
// Find lesson for this concept
|
|
7139
|
+
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from.replace(/-/g, ':').replace(/^fix:/, 'fix:')}`);
|
|
7140
|
+
const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
|
|
7141
|
+
ckgResults.push({ conceptId: node.id, edge, lesson });
|
|
7142
|
+
}
|
|
7143
|
+
}
|
|
7144
|
+
for (const ek of [...fromKeys, ...toKeys].slice(0, 20)) {
|
|
7145
|
+
const edgeRaw = await redis.get(ek);
|
|
7146
|
+
if (!edgeRaw)
|
|
7147
|
+
continue;
|
|
7148
|
+
const edge = JSON.parse(edgeRaw);
|
|
7149
|
+
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
|
|
7150
|
+
const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
|
|
7151
|
+
ckgResults.push({ conceptId: edge.from, edge, lesson });
|
|
7152
|
+
}
|
|
7153
|
+
}
|
|
7154
|
+
}
|
|
7155
|
+
catch { /* CKG traversal non-critical */ }
|
|
7156
|
+
// Deduplicate CKG results and sort by confidence
|
|
7157
|
+
const ckgSeen = new Set();
|
|
7158
|
+
const ckgDeduped = ckgResults.filter(r => {
|
|
7159
|
+
const key = `${r.edge.from}:${r.edge.edgeType}:${r.edge.to}`;
|
|
7160
|
+
if (ckgSeen.has(key))
|
|
7161
|
+
return false;
|
|
7162
|
+
ckgSeen.add(key);
|
|
7163
|
+
return true;
|
|
7164
|
+
}).sort((a, b) => b.edge.confidence - a.edge.confidence).slice(0, max_depth);
|
|
7165
|
+
// ── Layer 2 (fallback): text similarity over all lessons ─────────────────
|
|
6948
7166
|
let cursor = 0;
|
|
6949
7167
|
const lessonKeys = [];
|
|
6950
7168
|
do {
|
|
@@ -6960,7 +7178,6 @@ async function handleBulkLockStream(name, args) {
|
|
|
6960
7178
|
continue;
|
|
6961
7179
|
try {
|
|
6962
7180
|
const l = JSON.parse(raw);
|
|
6963
|
-
// Skip if filter tags given and lesson has none of them
|
|
6964
7181
|
if (filterTags.length > 0 && !(l.tags ?? []).some((t) => filterTags.includes(t)))
|
|
6965
7182
|
continue;
|
|
6966
7183
|
const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? '', l.context ?? '']
|
|
@@ -6973,7 +7190,7 @@ async function handleBulkLockStream(name, args) {
|
|
|
6973
7190
|
}
|
|
6974
7191
|
scored.sort((a, b) => b.score - a.score);
|
|
6975
7192
|
const chain = scored.slice(0, max_depth);
|
|
6976
|
-
if (chain.length === 0) {
|
|
7193
|
+
if (chain.length === 0 && ckgDeduped.length === 0) {
|
|
6977
7194
|
return [
|
|
6978
7195
|
`🔍 **Causal Trace: "${problem}"**`,
|
|
6979
7196
|
``,
|
|
@@ -6991,50 +7208,64 @@ async function handleBulkLockStream(name, args) {
|
|
|
6991
7208
|
`\`\`\``,
|
|
6992
7209
|
].join('\n');
|
|
6993
7210
|
}
|
|
6994
|
-
// Build causal chain narrative: failures first → then successes
|
|
6995
|
-
const failures = chain.filter(c => c.lesson.outcome !== 'success');
|
|
6996
|
-
const solutions = chain.filter(c => c.lesson.outcome === 'success');
|
|
6997
|
-
const SEV_ICON = { critical: '🔴', major: '🟠', minor: '🟡' };
|
|
6998
7211
|
const lines = [
|
|
6999
7212
|
`🔍 **Causal Trace: "${problem}"**`,
|
|
7000
7213
|
``,
|
|
7001
|
-
`Found **${chain.length}** related lessons. Reconstructed causal chain:`,
|
|
7002
|
-
``,
|
|
7003
7214
|
];
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
const
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
lines.push(`**Solutions that worked before:**`);
|
|
7017
|
-
solutions.forEach((c, i) => {
|
|
7018
|
-
const l = c.lesson;
|
|
7019
|
-
const date = new Date(l.ts).toLocaleDateString('de-DE');
|
|
7020
|
-
lines.push(` ${i + 1}. ✅ \`${l.topic}\` — ${date} · recalled ${l.recall_count ?? 0}× `);
|
|
7021
|
-
if (l.what_worked)
|
|
7022
|
-
lines.push(` ${l.what_worked.slice(0, 200)}`);
|
|
7023
|
-
});
|
|
7024
|
-
lines.push('');
|
|
7215
|
+
// Show CKG graph results first if available
|
|
7216
|
+
if (ckgDeduped.length > 0) {
|
|
7217
|
+
lines.push(`### 🕸️ CKG Graph (confidence-ranked)`);
|
|
7218
|
+
for (const r of ckgDeduped) {
|
|
7219
|
+
const confPct = Math.round(r.edge.confidence * 100);
|
|
7220
|
+
const confBar = '▓'.repeat(Math.round(confPct / 10)) + '░'.repeat(10 - Math.round(confPct / 10));
|
|
7221
|
+
lines.push(` ${r.edge.from} **→[${r.edge.edgeType}]→** ${r.edge.to}`);
|
|
7222
|
+
lines.push(` ${confBar} ${confPct}% confidence (${r.edge.successes}/${r.edge.trials} confirmed)`);
|
|
7223
|
+
if (r.lesson?.what_worked)
|
|
7224
|
+
lines.push(` ✅ Fix: ${r.lesson.what_worked.slice(0, 150)}`);
|
|
7225
|
+
lines.push('');
|
|
7226
|
+
}
|
|
7025
7227
|
}
|
|
7026
|
-
|
|
7027
|
-
if (
|
|
7028
|
-
lines.push(
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7228
|
+
// Build text-based causal chain narrative
|
|
7229
|
+
if (chain.length > 0) {
|
|
7230
|
+
lines.push(ckgDeduped.length > 0 ? `### 📚 Text Search (${chain.length} related lessons)` : `Found **${chain.length}** related lessons. Reconstructed causal chain:`, '');
|
|
7231
|
+
const failures = chain.filter(c => c.lesson.outcome !== 'success');
|
|
7232
|
+
const solutions = chain.filter(c => c.lesson.outcome === 'success');
|
|
7233
|
+
if (failures.length > 0) {
|
|
7234
|
+
lines.push(`**Root causes & failure chain:**`);
|
|
7235
|
+
failures.forEach((c, i) => {
|
|
7236
|
+
const l = c.lesson;
|
|
7237
|
+
const sev = SEV_ICON[l.severity ?? 'minor'] ?? '🟡';
|
|
7238
|
+
lines.push(`${i === 0 ? ' Root:' : ' → :'} ${sev} \`${l.topic}\``);
|
|
7239
|
+
if (l.what_failed)
|
|
7240
|
+
lines.push(` ↳ ${l.what_failed.slice(0, 120)}`);
|
|
7241
|
+
});
|
|
7242
|
+
lines.push('');
|
|
7243
|
+
}
|
|
7244
|
+
if (solutions.length > 0) {
|
|
7245
|
+
lines.push(`**Solutions that worked before:**`);
|
|
7246
|
+
solutions.forEach((c, i) => {
|
|
7247
|
+
const l = c.lesson;
|
|
7248
|
+
const date = new Date(l.ts).toLocaleDateString('de-DE');
|
|
7249
|
+
lines.push(` ${i + 1}. ✅ \`${l.topic}\` — ${date} · recalled ${l.recall_count ?? 0}×`);
|
|
7250
|
+
if (l.what_worked)
|
|
7251
|
+
lines.push(` ${l.what_worked.slice(0, 200)}`);
|
|
7252
|
+
});
|
|
7253
|
+
lines.push('');
|
|
7254
|
+
}
|
|
7255
|
+
const topSolution = solutions[0]?.lesson;
|
|
7256
|
+
if (topSolution?.what_worked) {
|
|
7257
|
+
lines.push(`**⚡ Most likely fix:**`);
|
|
7258
|
+
lines.push(`\`\`\``);
|
|
7259
|
+
lines.push(topSolution.what_worked.slice(0, 500));
|
|
7260
|
+
lines.push(`\`\`\``);
|
|
7261
|
+
lines.push('');
|
|
7262
|
+
}
|
|
7033
7263
|
}
|
|
7034
7264
|
lines.push(`💡 After applying: \`learn_from_attempts(topic="fix:${tokens[0] ?? 'issue'}", outcome="success", ...)\``);
|
|
7265
|
+
if (ckgDeduped.length > 0)
|
|
7266
|
+
lines.push(`🕸️ Explore graph: \`ckg_inspect(concept="${tokens[0] ?? 'fix'}")\``);
|
|
7035
7267
|
return lines.join('\n');
|
|
7036
7268
|
}
|
|
7037
|
-
// ── v0.6 Cognitive Cache: knowledge_decay ────────────────────────────────
|
|
7038
7269
|
case 'knowledge_decay': {
|
|
7039
7270
|
const { instance_id, min_age_days = 0, show_top = 20 } = args;
|
|
7040
7271
|
const redis = await getConnection(instance_id);
|
|
@@ -7114,11 +7345,9 @@ async function handleBulkLockStream(name, args) {
|
|
|
7114
7345
|
> Sessions are fully automatic. The Brain starts when you call any tool and saves a summary when you close.
|
|
7115
7346
|
|
|
7116
7347
|
### MANDATORY hooks (always fire, no exceptions):
|
|
7117
|
-
- **Context compacted** → call \`compact_recover(instance_id="${instance_id}", focus="<current task>")\` FIRST
|
|
7118
7348
|
- **Window opens** → call \`session_start(instance_id="${instance_id}", focus="<today's goal>")\`
|
|
7119
7349
|
- **Bug fixed / deployed / discovered** → call \`learn_from_attempts(instance_id="${instance_id}", topic="fix:<area>", outcome="success", what_worked="...")\`
|
|
7120
7350
|
- **Window closes** → call \`session_end(instance_id="${instance_id}", summary="...", files_changed=[...])\`
|
|
7121
|
-
- **Monthly** → call \`memory_crystalize(instance_id="${instance_id}")\` to distill knowledge into a permanent Crystal
|
|
7122
7351
|
`;
|
|
7123
7352
|
const full = `## Cachly AI Brain — Autopilot for ${project_name}
|
|
7124
7353
|
|
|
@@ -7178,40 +7407,18 @@ smart_recall(
|
|
|
7178
7407
|
|
|
7179
7408
|
---
|
|
7180
7409
|
|
|
7181
|
-
## ⚡
|
|
7182
|
-
|
|
7183
|
-
**If your context window was compacted**, call this FIRST — before reading any file:
|
|
7184
|
-
\`\`\`
|
|
7185
|
-
compact_recover(
|
|
7186
|
-
instance_id = "${instance_id}",
|
|
7187
|
-
focus = "<what you were working on>",
|
|
7188
|
-
)
|
|
7189
|
-
\`\`\`
|
|
7190
|
-
Returns: last checkpoint, pending tasks, top lessons. Instant — no full brain scan.
|
|
7191
|
-
|
|
7192
|
-
---
|
|
7193
|
-
|
|
7194
|
-
## 🔁 Scheduled Maintenance (run on schedule)
|
|
7195
|
-
|
|
7196
|
-
| When | Call |
|
|
7197
|
-
|---|---|
|
|
7198
|
-
| **Monthly** | \`memory_crystalize(instance_id="${instance_id}")\` — distill brain into a permanent snapshot |
|
|
7199
|
-
| Weekly review | \`brain_diff(instance_id="${instance_id}", since="7d")\` — see what your AI learned this week |
|
|
7200
|
-
| Before big refactor | \`knowledge_decay(instance_id="${instance_id}")\` — see which lessons to still trust |
|
|
7201
|
-
| After major milestone | \`memory_crystalize(instance_id="${instance_id}", label="v2 launch")\` — preserve milestone knowledge |
|
|
7202
|
-
|
|
7203
|
-
## 🧠 Cognitive Tools (use anytime)
|
|
7410
|
+
## ⚡ Cognitive Tools (v0.6)
|
|
7204
7411
|
|
|
7205
7412
|
| When | Call |
|
|
7206
7413
|
|---|---|
|
|
7207
7414
|
| Brain feeling cluttered | \`memory_consolidate(instance_id="${instance_id}")\` |
|
|
7415
|
+
| Weekly review | \`brain_diff(instance_id="${instance_id}", since="7d")\` |
|
|
7208
7416
|
| Weird bug, no idea why | \`causal_trace(instance_id="${instance_id}", problem="<symptom>")\` |
|
|
7209
|
-
|
|
|
7210
|
-
| Unknown error that you solved | \`syndicate(instance_id="${instance_id}", topic="fix:<area>", what_worked="...")\` |
|
|
7417
|
+
| Before big refactor | \`knowledge_decay(instance_id="${instance_id}")\` |
|
|
7211
7418
|
|
|
7212
7419
|
---
|
|
7213
7420
|
|
|
7214
|
-
*Cachly v0.
|
|
7421
|
+
*Cachly v0.6 · Generated ${new Date().toISOString().slice(0, 10)}*
|
|
7215
7422
|
`;
|
|
7216
7423
|
const content = style === 'minimal' ? minimal : full;
|
|
7217
7424
|
const filename = editor === 'copilot'
|
|
@@ -7370,172 +7577,709 @@ Returns: last checkpoint, pending tasks, top lessons. Instant — no full brain
|
|
|
7370
7577
|
lines.push(`---`, `**Confirm** (if this helped you): \`syndicate(topic="${res.results[0]?.topic ?? '...'}", what_worked="...")\` → auto-deduped, trust +1`, `**All trending:** \`syndicate_trending(limit=50)\` | **Search:** \`syndicate_search(q="...")\``);
|
|
7371
7578
|
return lines.filter(l => l !== '').join('\n');
|
|
7372
7579
|
}
|
|
7373
|
-
// ──
|
|
7374
|
-
case '
|
|
7375
|
-
const { instance_id,
|
|
7580
|
+
// ── Layer 1: brain_search ─────────────────────────────────────────────────
|
|
7581
|
+
case 'brain_search': {
|
|
7582
|
+
const { instance_id, query, limit = 15 } = args;
|
|
7376
7583
|
const redis = await getConnection(instance_id);
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7584
|
+
// BM25+ over ALL brain key namespaces
|
|
7585
|
+
const allMatches = await keywordSearch(redis, [
|
|
7586
|
+
'cachly:lesson:best:*',
|
|
7587
|
+
'cachly:ctx:*',
|
|
7588
|
+
'cachly:idx:*',
|
|
7589
|
+
'cachly:session:last',
|
|
7590
|
+
'cachly:session:handoff',
|
|
7591
|
+
'cachly:roadmap:*',
|
|
7592
|
+
'cachly:ckg:node:*',
|
|
7593
|
+
], query, limit);
|
|
7594
|
+
if (allMatches.length === 0) {
|
|
7595
|
+
return [`🔎 **Brain Search: "${query}"**`, '', `No results found across all brain data.`, '', `💡 Try \`smart_recall\` or check \`list_remembered\`.`].join('\n');
|
|
7596
|
+
}
|
|
7597
|
+
const lines = [`🔎 **Brain Search: "${query}"** — ${allMatches.length} result${allMatches.length !== 1 ? 's' : ''} across all brain data\n`];
|
|
7598
|
+
for (const m of allMatches.slice(0, limit)) {
|
|
7599
|
+
const ns = m.key.startsWith('cachly:lesson:') ? '💡 lesson'
|
|
7600
|
+
: m.key.startsWith('cachly:ctx:') ? '📝 context'
|
|
7601
|
+
: m.key.startsWith('cachly:idx:') ? '📂 index'
|
|
7602
|
+
: m.key.startsWith('cachly:session:') ? '🕐 session'
|
|
7603
|
+
: m.key.startsWith('cachly:roadmap:') ? '🗺️ roadmap'
|
|
7604
|
+
: m.key.startsWith('cachly:ckg:node:') ? '🕸️ ckg-node'
|
|
7605
|
+
: '🗄️ data';
|
|
7606
|
+
const preview = m.content.slice(0, 280).replace(/\n/g, ' ');
|
|
7607
|
+
lines.push(`**${ns}** \`${m.key.split(':').slice(2).join(':')}\` _(BM25: ${m.score.toFixed(2)})_`);
|
|
7608
|
+
lines.push(`> ${preview}${m.content.length > 280 ? '…' : ''}\n`);
|
|
7609
|
+
}
|
|
7610
|
+
return lines.join('\n');
|
|
7611
|
+
}
|
|
7612
|
+
// ── Layer 1: ckg_inspect ─────────────────────────────────────────────────
|
|
7613
|
+
case 'ckg_inspect': {
|
|
7614
|
+
const { instance_id, concept, max_hops = 2 } = args;
|
|
7615
|
+
const redis = await getConnection(instance_id);
|
|
7616
|
+
const conceptId = ckgSlug(concept);
|
|
7617
|
+
const visited = new Set();
|
|
7618
|
+
const allEdges = [];
|
|
7619
|
+
// BFS traversal of CKG
|
|
7620
|
+
const queue = [{ id: conceptId, hop: 0 }];
|
|
7621
|
+
while (queue.length > 0) {
|
|
7622
|
+
const { id, hop } = queue.shift();
|
|
7623
|
+
if (visited.has(id) || hop > max_hops)
|
|
7624
|
+
continue;
|
|
7625
|
+
visited.add(id);
|
|
7626
|
+
const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${id}`);
|
|
7627
|
+
const toKeys = await redis.smembers(`cachly:ckg:idx:to:${id}`);
|
|
7628
|
+
for (const ek of [...fromKeys, ...toKeys].slice(0, 50)) {
|
|
7629
|
+
const raw = await redis.get(ek);
|
|
7630
|
+
if (!raw)
|
|
7631
|
+
continue;
|
|
7632
|
+
const edge = JSON.parse(raw);
|
|
7633
|
+
allEdges.push(edge);
|
|
7634
|
+
if (hop < max_hops) {
|
|
7635
|
+
if (!visited.has(edge.from))
|
|
7636
|
+
queue.push({ id: edge.from, hop: hop + 1 });
|
|
7637
|
+
if (!visited.has(edge.to))
|
|
7638
|
+
queue.push({ id: edge.to, hop: hop + 1 });
|
|
7388
7639
|
}
|
|
7389
|
-
if (cp.next_step)
|
|
7390
|
-
lines.push(`📍 **Resume with:** ${cp.next_step}`);
|
|
7391
|
-
lines.push('');
|
|
7392
7640
|
}
|
|
7393
|
-
catch { /* ignore corrupt */ }
|
|
7394
7641
|
}
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7642
|
+
if (allEdges.length === 0) {
|
|
7643
|
+
// Try fuzzy: scan for nodes matching the concept as substring
|
|
7644
|
+
const nodeKeys = [];
|
|
7645
|
+
const nStream = redis.scanStream({ match: `cachly:ckg:node:*${conceptId}*`, count: 100 });
|
|
7646
|
+
await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
7647
|
+
if (nodeKeys.length === 0) {
|
|
7648
|
+
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');
|
|
7649
|
+
}
|
|
7650
|
+
const nodeList = nodeKeys.slice(0, 10).map(k => ` • \`${k.replace('cachly:ckg:node:', '')}\``).join('\n');
|
|
7651
|
+
return [`🕸️ **CKG Inspect: "${concept}"**`, '', `No edges found for \`${conceptId}\`, but found similar nodes:`, nodeList, '', `Try: \`ckg_inspect(concept="<exact-node-id>")\``].join('\n');
|
|
7652
|
+
}
|
|
7653
|
+
// Sort by confidence desc, deduplicate
|
|
7654
|
+
const edgeSeen = new Set();
|
|
7655
|
+
const unique = allEdges.filter(e => {
|
|
7656
|
+
const k = `${e.from}:${e.edgeType}:${e.to}`;
|
|
7657
|
+
if (edgeSeen.has(k))
|
|
7658
|
+
return false;
|
|
7659
|
+
edgeSeen.add(k);
|
|
7660
|
+
return true;
|
|
7661
|
+
}).sort((a, b) => b.confidence - a.confidence);
|
|
7662
|
+
const EDGE_ICON = { fixes: '🔧', requires: '🔗', 'co-occurs': '🔄', causes: '⚡', contradicts: '⚠️', degrades_under: '📉' };
|
|
7663
|
+
const lines = [`🕸️ **CKG Inspect: "${concept}"** (${unique.length} edge${unique.length !== 1 ? 's' : ''}, ${visited.size} node${visited.size !== 1 ? 's' : ''} traversed)\n`];
|
|
7664
|
+
// Group by edge type
|
|
7665
|
+
const byType = new Map();
|
|
7666
|
+
for (const e of unique) {
|
|
7667
|
+
if (!byType.has(e.edgeType))
|
|
7668
|
+
byType.set(e.edgeType, []);
|
|
7669
|
+
byType.get(e.edgeType).push(e);
|
|
7670
|
+
}
|
|
7671
|
+
for (const [eType, edges] of byType) {
|
|
7672
|
+
const icon = EDGE_ICON[eType] ?? '→';
|
|
7673
|
+
lines.push(`**${icon} ${eType}** (${edges.length})`);
|
|
7674
|
+
for (const e of edges.slice(0, 8)) {
|
|
7675
|
+
const confPct = Math.round(e.confidence * 100);
|
|
7676
|
+
const bar = '▓'.repeat(Math.round(confPct / 10)) + '░'.repeat(10 - Math.round(confPct / 10));
|
|
7677
|
+
lines.push(` \`${e.from}\` → \`${e.to}\` ${bar} ${confPct}% (${e.successes.toFixed(1)}/${e.trials} trials)`);
|
|
7678
|
+
}
|
|
7679
|
+
lines.push('');
|
|
7680
|
+
}
|
|
7681
|
+
lines.push(`💡 Expand: \`ckg_inspect(concept="${concept}", max_hops=3)\` | Predict: \`brain_predict(context="${concept}")\``);
|
|
7682
|
+
return lines.join('\n');
|
|
7683
|
+
}
|
|
7684
|
+
// ── Layer 4: brain_predict (PPE) ─────────────────────────────────────────
|
|
7685
|
+
case 'brain_predict': {
|
|
7686
|
+
const { instance_id, context: ctx, top_k = 5 } = args;
|
|
7687
|
+
const redis = await getConnection(instance_id);
|
|
7688
|
+
const ctxTokens = ctx.toLowerCase().replace(/[^a-z0-9\s\-_:]/g, ' ').split(/\s+/).filter(t => t.length > 2);
|
|
7689
|
+
const predictions = [];
|
|
7690
|
+
for (const token of ctxTokens.slice(0, 6)) {
|
|
7691
|
+
const nodeKeys = [];
|
|
7692
|
+
const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 50 });
|
|
7693
|
+
await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
7694
|
+
for (const nk of nodeKeys.slice(0, 5)) {
|
|
7695
|
+
const nodeRaw = await redis.get(nk);
|
|
7696
|
+
if (!nodeRaw)
|
|
7697
|
+
continue;
|
|
7698
|
+
const node = JSON.parse(nodeRaw);
|
|
7699
|
+
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
7700
|
+
for (const ek of edgeKeys.slice(0, 20)) {
|
|
7701
|
+
const edgeRaw = await redis.get(ek);
|
|
7702
|
+
if (!edgeRaw)
|
|
7703
|
+
continue;
|
|
7704
|
+
const edge = JSON.parse(edgeRaw);
|
|
7705
|
+
// Only interested in fixes and co-occurs for prediction
|
|
7706
|
+
if (edge.edgeType !== 'fixes' && edge.edgeType !== 'co-occurs' && edge.edgeType !== 'causes')
|
|
7707
|
+
continue;
|
|
7708
|
+
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
|
|
7709
|
+
const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
|
|
7710
|
+
predictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson });
|
|
7410
7711
|
}
|
|
7411
7712
|
}
|
|
7412
|
-
catch { /* ignore */ }
|
|
7413
7713
|
}
|
|
7414
|
-
//
|
|
7714
|
+
// Step 2: Text-based fallback — scan lessons for matching topics
|
|
7715
|
+
const textPredictions = [];
|
|
7415
7716
|
const lessonKeys = [];
|
|
7416
7717
|
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
7417
|
-
await new Promise((
|
|
7418
|
-
lStream.on('data', (batch) => lessonKeys.push(...batch));
|
|
7419
|
-
lStream.on('end', resolve);
|
|
7420
|
-
lStream.on('error', reject);
|
|
7421
|
-
});
|
|
7422
|
-
const allLessons = [];
|
|
7718
|
+
await new Promise((res, rej) => { lStream.on('data', (b) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
|
|
7423
7719
|
for (const k of lessonKeys) {
|
|
7424
7720
|
const raw = await redis.get(k);
|
|
7425
7721
|
if (!raw)
|
|
7426
7722
|
continue;
|
|
7427
7723
|
try {
|
|
7428
|
-
|
|
7724
|
+
const l = JSON.parse(raw);
|
|
7725
|
+
const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? ''].join(' ').toLowerCase();
|
|
7726
|
+
const score = ctxTokens.reduce((s, t) => s + (haystack.includes(t) ? 1 : 0), 0);
|
|
7727
|
+
if (score >= 1 && l.outcome !== 'failure') {
|
|
7728
|
+
const conf = calculateConfidence(l);
|
|
7729
|
+
textPredictions.push({ ...l, confidence: conf });
|
|
7730
|
+
}
|
|
7429
7731
|
}
|
|
7430
7732
|
catch { /* skip */ }
|
|
7431
7733
|
}
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7734
|
+
textPredictions.sort((a, b) => b.confidence - a.confidence);
|
|
7735
|
+
if (predictions.length === 0 && textPredictions.length === 0) {
|
|
7736
|
+
return [
|
|
7737
|
+
`🔮 **Brain Predict: "${ctx}"**`,
|
|
7738
|
+
``,
|
|
7739
|
+
`No predictions yet — the brain hasn't seen this domain.`,
|
|
7740
|
+
``,
|
|
7741
|
+
`💡 As you solve problems in this area and call \`learn_from_attempts\`, the CKG builds up and predictions become available.`,
|
|
7742
|
+
].join('\n');
|
|
7743
|
+
}
|
|
7744
|
+
const lines = [`🔮 **Brain Predict: "${ctx}"**\n`];
|
|
7745
|
+
// CKG-based predictions
|
|
7746
|
+
if (predictions.length > 0) {
|
|
7747
|
+
const pSeen = new Set();
|
|
7748
|
+
const pUniq = predictions.filter(p => { const k = `${p.concept}:${p.edgeType}:${p.target}`; if (pSeen.has(k))
|
|
7749
|
+
return false; pSeen.add(k); return true; })
|
|
7750
|
+
.sort((a, b) => b.confidence - a.confidence).slice(0, top_k);
|
|
7751
|
+
lines.push(`### 🕸️ CKG Predictions (based on ${pUniq.length} known edges)`);
|
|
7752
|
+
for (const p of pUniq) {
|
|
7753
|
+
const confPct = Math.round(p.confidence * 100);
|
|
7754
|
+
const icon = p.edgeType === 'fixes' ? '🔧' : p.edgeType === 'co-occurs' ? '🔄' : '⚡';
|
|
7755
|
+
lines.push(`${icon} **${confPct}%** \`${p.concept}\` _${p.edgeType}_ \`${p.target}\``);
|
|
7756
|
+
if (p.lesson?.what_worked)
|
|
7757
|
+
lines.push(` ✅ ${p.lesson.what_worked.slice(0, 120)}`);
|
|
7441
7758
|
}
|
|
7442
7759
|
lines.push('');
|
|
7443
7760
|
}
|
|
7444
|
-
|
|
7445
|
-
|
|
7761
|
+
// Text-based lesson predictions
|
|
7762
|
+
if (textPredictions.length > 0) {
|
|
7763
|
+
lines.push(`### 📚 Relevant Lessons (${Math.min(textPredictions.length, top_k)} pre-loaded)`);
|
|
7764
|
+
for (const l of textPredictions.slice(0, top_k)) {
|
|
7765
|
+
const confPct = Math.round(l.confidence * 100);
|
|
7766
|
+
lines.push(` ✅ **${confPct}%** \`${l.topic}\` — ${(l.what_worked ?? '').slice(0, 120)}`);
|
|
7767
|
+
}
|
|
7768
|
+
lines.push('');
|
|
7769
|
+
}
|
|
7770
|
+
lines.push(`💡 Outcome confirmed? \`learn_from_attempts(topic="fix:...", outcome="success", ...)\` → improves future predictions`);
|
|
7771
|
+
return lines.join('\n');
|
|
7446
7772
|
}
|
|
7447
|
-
// ──
|
|
7448
|
-
case '
|
|
7449
|
-
const { instance_id,
|
|
7773
|
+
// ── Layer 3: MADC ─────────────────────────────────────────────────────────
|
|
7774
|
+
case 'madc_deliberate': {
|
|
7775
|
+
const { instance_id, topic } = args;
|
|
7450
7776
|
const redis = await getConnection(instance_id);
|
|
7451
|
-
const
|
|
7452
|
-
const
|
|
7453
|
-
|
|
7454
|
-
const authorArg = author ? `--author="${author}"` : '';
|
|
7455
|
-
let gitRaw = '';
|
|
7456
|
-
try {
|
|
7457
|
-
gitRaw = execSync(`git -C "${workspace_path}" log --since="${clampedDays} days ago" ${authorArg} --oneline --format="%H|||%s|||%ai|||%ae" -n ${clampedMax}`, { timeout: 15000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
7777
|
+
const historyRaw = await redis.lrange(`cachly:lessons:${topic}`, 0, -1);
|
|
7778
|
+
const history = historyRaw.map(r => { try {
|
|
7779
|
+
return JSON.parse(r);
|
|
7458
7780
|
}
|
|
7459
7781
|
catch {
|
|
7460
|
-
return
|
|
7782
|
+
return null;
|
|
7783
|
+
} }).filter(Boolean);
|
|
7784
|
+
if (history.length < 2) {
|
|
7785
|
+
return [
|
|
7786
|
+
`🗳️ **MADC: "${topic}"**`, '',
|
|
7787
|
+
`Not enough history for deliberation (need ≥ 2 entries, found ${history.length}).`,
|
|
7788
|
+
'', `Call \`learn_from_attempts\` with conflicting outcomes to trigger deliberation.`,
|
|
7789
|
+
].join('\n');
|
|
7461
7790
|
}
|
|
7462
|
-
|
|
7463
|
-
|
|
7791
|
+
// Specialist agents and their domain keywords
|
|
7792
|
+
const AGENTS = [
|
|
7793
|
+
{ name: 'InfraAgent', domains: ['infra', 'k8s', 'docker', 'server', 'wireguard', 'helm'] },
|
|
7794
|
+
{ name: 'AuthAgent', domains: ['auth', 'jwt', 'keycloak', 'oauth', 'oidc', 'token'] },
|
|
7795
|
+
{ name: 'DeployAgent', domains: ['deploy', 'ci', 'pipeline', 'rsync', 'release'] },
|
|
7796
|
+
{ name: 'DatabaseAgent', domains: ['db', 'gorm', 'migration', 'postgres', 'clickhouse', 'redis'] },
|
|
7797
|
+
{ name: 'DebugAgent', domains: ['debug', 'panic', 'race', 'nil', 'fix', 'error'] },
|
|
7798
|
+
{ name: 'APIAgent', domains: ['api', 'http', 'grpc', 'rest', 'fiber', 'web'] },
|
|
7799
|
+
];
|
|
7800
|
+
const topicDomain = topic.split(':')[0] ?? '';
|
|
7801
|
+
const relevantAgents = AGENTS.filter(a => a.domains.some(d => topicDomain === d || topic.includes(d)));
|
|
7802
|
+
const votingAgents = relevantAgents.length > 0 ? relevantAgents : AGENTS;
|
|
7803
|
+
// Measure each agent's CKG coverage in their domains
|
|
7804
|
+
const agentCoverage = new Map();
|
|
7805
|
+
for (const agent of votingAgents) {
|
|
7806
|
+
let edgeCount = 0;
|
|
7807
|
+
for (const domain of agent.domains) {
|
|
7808
|
+
const nodeKeys = [];
|
|
7809
|
+
const nStream = redis.scanStream({ match: `cachly:ckg:node:${domain}*`, count: 50 });
|
|
7810
|
+
await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
7811
|
+
edgeCount += nodeKeys.length;
|
|
7812
|
+
}
|
|
7813
|
+
agentCoverage.set(agent.name, edgeCount);
|
|
7464
7814
|
}
|
|
7465
|
-
const
|
|
7466
|
-
const
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
const
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
if (
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
.
|
|
7491
|
-
|
|
7815
|
+
const successLessons = history.filter(l => l.outcome === 'success' || l.outcome === 'partial');
|
|
7816
|
+
const failureLessons = history.filter(l => l.outcome === 'failure');
|
|
7817
|
+
if (failureLessons.length === 0) {
|
|
7818
|
+
return [
|
|
7819
|
+
`🗳️ **MADC: "${topic}"**`, '',
|
|
7820
|
+
`No contradictions found — all ${successLessons.length} entries have non-failure outcomes.`,
|
|
7821
|
+
'', `Use \`ckg_inspect(concept="${ckgSlug(topic)}")\` to explore the confidence graph.`,
|
|
7822
|
+
].join('\n');
|
|
7823
|
+
}
|
|
7824
|
+
// Agent voting logic
|
|
7825
|
+
const votes = [];
|
|
7826
|
+
for (const agent of votingAgents) {
|
|
7827
|
+
const coverage = agentCoverage.get(agent.name) ?? 0;
|
|
7828
|
+
let vote;
|
|
7829
|
+
let reason;
|
|
7830
|
+
if (coverage < 2) {
|
|
7831
|
+
vote = 'abstain';
|
|
7832
|
+
reason = 'insufficient domain coverage';
|
|
7833
|
+
}
|
|
7834
|
+
else if (successLessons.length >= failureLessons.length * 2) {
|
|
7835
|
+
vote = 'success';
|
|
7836
|
+
reason = `${successLessons.length}/${history.length} entries confirm success`;
|
|
7837
|
+
}
|
|
7838
|
+
else if (failureLessons.length >= successLessons.length * 2) {
|
|
7839
|
+
vote = 'failure';
|
|
7840
|
+
reason = `${failureLessons.length}/${history.length} entries confirm failure`;
|
|
7841
|
+
}
|
|
7842
|
+
else {
|
|
7843
|
+
vote = 'abstain';
|
|
7844
|
+
reason = `contested (${successLessons.length} success vs ${failureLessons.length} failure)`;
|
|
7845
|
+
}
|
|
7846
|
+
votes.push({ agent: agent.name, vote, coverage, reason });
|
|
7847
|
+
}
|
|
7848
|
+
const successVotes = votes.filter(v => v.vote === 'success').length;
|
|
7849
|
+
const failureVotes = votes.filter(v => v.vote === 'failure').length;
|
|
7850
|
+
const abstainVotes = votes.filter(v => v.vote === 'abstain').length;
|
|
7851
|
+
let resolution;
|
|
7852
|
+
let resolutionText;
|
|
7853
|
+
if (successVotes > 0 && failureVotes === 0) {
|
|
7854
|
+
resolution = 'unanimous_success';
|
|
7855
|
+
resolutionText = `✅ **Unanimous: SUCCESS** — ${successVotes} agent(s) confirm, ${abstainVotes} abstain`;
|
|
7856
|
+
}
|
|
7857
|
+
else if (failureVotes > 0 && successVotes === 0) {
|
|
7858
|
+
resolution = 'unanimous_failure';
|
|
7859
|
+
resolutionText = `❌ **Unanimous: FAILURE** — ${failureVotes} agent(s) confirm, ${abstainVotes} abstain`;
|
|
7860
|
+
}
|
|
7861
|
+
else {
|
|
7862
|
+
resolution = 'contested';
|
|
7863
|
+
resolutionText = `⚠️ **CONTESTED** — ${successVotes} success votes, ${failureVotes} failure votes, ${abstainVotes} abstain`;
|
|
7864
|
+
}
|
|
7865
|
+
// Store resolution as CKG node
|
|
7866
|
+
const resNodeId = ckgSlug(`resolution:${topic}`);
|
|
7867
|
+
const resNode = {
|
|
7868
|
+
id: resNodeId, domain: 'resolution', type: 'resolution', count: 1,
|
|
7869
|
+
ts: new Date().toISOString(), resolution, topic,
|
|
7870
|
+
votes: { success: successVotes, failure: failureVotes, abstain: abstainVotes },
|
|
7871
|
+
};
|
|
7872
|
+
await redis.set(`cachly:ckg:node:${resNodeId}`, JSON.stringify(resNode));
|
|
7873
|
+
// Write contradicts edge if contested
|
|
7874
|
+
if (resolution === 'contested') {
|
|
7875
|
+
await ckgUpdateEdge(redis, ckgSlug(topic), 'contradicts', resNodeId, false);
|
|
7876
|
+
}
|
|
7877
|
+
const lines = [
|
|
7878
|
+
`🗳️ **MADC Deliberation: "${topic}"**`, '',
|
|
7879
|
+
`📊 Evidence: ${successLessons.length} success/partial vs ${failureLessons.length} failure entries (${history.length} total)`,
|
|
7880
|
+
'', `**Voting agents (${votingAgents.length}):**`,
|
|
7881
|
+
...votes.map(v => {
|
|
7882
|
+
const icon = v.vote === 'success' ? '✅' : v.vote === 'failure' ? '❌' : '⬜';
|
|
7883
|
+
const covBar = '▓'.repeat(Math.min(v.coverage, 5)) + '░'.repeat(Math.max(0, 5 - v.coverage));
|
|
7884
|
+
return ` ${icon} **${v.agent}** [${covBar}] ${v.coverage} CKG edges — ${v.reason}`;
|
|
7885
|
+
}),
|
|
7886
|
+
'', resolutionText, '',
|
|
7887
|
+
];
|
|
7888
|
+
if (resolution === 'unanimous_success') {
|
|
7889
|
+
lines.push(`🔧 Failure entries superseded — store confirmed lesson: \`learn_from_attempts(topic="${topic}", outcome="success", ...)\``);
|
|
7890
|
+
}
|
|
7891
|
+
else if (resolution === 'unanimous_failure') {
|
|
7892
|
+
lines.push(`🚫 Success claims unconfirmed — re-verify: \`recall_best_solution(topic="${topic}")\``);
|
|
7893
|
+
}
|
|
7894
|
+
else {
|
|
7895
|
+
lines.push(`⚠️ Contested — run \`causal_trace\` before acting. Explore: \`ckg_inspect(concept="${ckgSlug(topic)}")\``);
|
|
7896
|
+
}
|
|
7897
|
+
lines.push('', `📝 Resolution node: \`cachly:ckg:node:${resNodeId}\``);
|
|
7898
|
+
return lines.join('\n');
|
|
7899
|
+
}
|
|
7900
|
+
// ── Layer 5: CLS — cls_ingest ─────────────────────────────────────────────
|
|
7901
|
+
case 'cls_ingest': {
|
|
7902
|
+
const { instance_id, source, payload } = args;
|
|
7903
|
+
const redis = await getConnection(instance_id);
|
|
7904
|
+
const ts = new Date().toISOString();
|
|
7905
|
+
const clsKey = 'cachly:cls:events';
|
|
7906
|
+
if (source === 'git_commit') {
|
|
7907
|
+
const message = String(payload.message ?? '');
|
|
7908
|
+
const sha = String(payload.sha ?? '');
|
|
7909
|
+
const files = (Array.isArray(payload.files) ? payload.files : []);
|
|
7910
|
+
const domain = /^fix/i.test(message) ? 'fix' : /^feat/i.test(message) ? 'feat' : /^refactor/i.test(message) ? 'refactor' : /^test/i.test(message) ? 'test' : 'commit';
|
|
7911
|
+
const slug = `${domain}:${ckgSlug(message.slice(0, 60))}`;
|
|
7912
|
+
const conceptId = ckgSlug(slug);
|
|
7913
|
+
await ckgUpsertNode(redis, conceptId, domain, 'commit');
|
|
7914
|
+
for (const f of files.slice(0, 10)) {
|
|
7915
|
+
const fd = f.includes('auth') ? 'auth' : f.includes('api') ? 'api' : f.includes('infra') ? 'infra' : f.includes('web') ? 'web' : 'code';
|
|
7916
|
+
const fileId = ckgSlug(`file:${fd}`);
|
|
7917
|
+
await ckgUpsertNode(redis, fileId, 'file', fd);
|
|
7918
|
+
await ckgUpdateEdge(redis, conceptId, 'co-occurs', fileId, true);
|
|
7919
|
+
}
|
|
7920
|
+
const lessonObj = {
|
|
7921
|
+
topic: slug, outcome: 'success', what_worked: message, what_failed: '',
|
|
7922
|
+
context: `CLS/git: sha=${sha}`, severity: 'minor',
|
|
7923
|
+
file_paths: files.slice(0, 10), commands: sha ? [`git show ${sha}`] : [],
|
|
7924
|
+
tags: ['cls', 'git'], depends_on: [], recall_count: 0, ts, verified_at: ts,
|
|
7925
|
+
confidence: 0.6, audit_trail: [{ ts, action: 'cls_git_commit' }], version: 3,
|
|
7926
|
+
};
|
|
7927
|
+
await redis.rpush(`cachly:lessons:${slug}`, JSON.stringify(lessonObj));
|
|
7928
|
+
const existing = await redis.get(`cachly:lesson:best:${slug}`);
|
|
7929
|
+
if (!existing)
|
|
7930
|
+
await redis.set(`cachly:lesson:best:${slug}`, JSON.stringify(lessonObj));
|
|
7931
|
+
await redis.rpush(clsKey, JSON.stringify({ source, payload: { message, sha }, ts }));
|
|
7932
|
+
await redis.ltrim(clsKey, -200, -1);
|
|
7933
|
+
return [
|
|
7934
|
+
`📨 **CLS Ingested: git_commit**`, '',
|
|
7935
|
+
`Commit \`${sha.slice(0, 8) || '?'}\`: ${message.slice(0, 80)}`,
|
|
7936
|
+
`Concept: \`${conceptId}\` (${domain}) · Files: ${files.length}`,
|
|
7937
|
+
'', `🕸️ CKG: \`${conceptId}\` + ${files.length} file edges · Lesson: \`${slug}\``,
|
|
7938
|
+
`💡 Inspect: \`ckg_inspect(concept="${domain}")\``,
|
|
7939
|
+
].join('\n');
|
|
7940
|
+
}
|
|
7941
|
+
if (source === 'ci_outcome') {
|
|
7942
|
+
const status = String(payload.status ?? '');
|
|
7943
|
+
const prev_status = String(payload.prev_status ?? '');
|
|
7944
|
+
const job = String(payload.job ?? 'unknown');
|
|
7945
|
+
const ciCtx = String(payload.context ?? '');
|
|
7946
|
+
const isFixed = ['failure', 'red', 'error'].includes(prev_status) && ['success', 'green', 'passed'].includes(status);
|
|
7947
|
+
const isBroken = ['success', 'green', 'passed'].includes(prev_status) && ['failure', 'red', 'error'].includes(status);
|
|
7948
|
+
const slug = `ci:${ckgSlug(job)}`;
|
|
7949
|
+
const conceptId = ckgSlug(slug);
|
|
7950
|
+
await ckgUpsertNode(redis, conceptId, 'ci', 'job');
|
|
7951
|
+
if (isFixed) {
|
|
7952
|
+
const problemId = ckgSlug(`problem:${ckgSlug(job)}`);
|
|
7953
|
+
await ckgUpsertNode(redis, problemId, 'problem', 'ci-failure');
|
|
7954
|
+
await ckgUpdateEdge(redis, conceptId, 'fixes', problemId, true);
|
|
7955
|
+
const lessonObj = {
|
|
7956
|
+
topic: slug, outcome: 'success',
|
|
7957
|
+
what_worked: `CI job "${job}" went ${prev_status} → ${status}`,
|
|
7958
|
+
what_failed: `Job "${job}" was failing`, context: `CLS/ci: ${ciCtx}`,
|
|
7959
|
+
severity: 'major', file_paths: [], commands: [], tags: ['cls', 'ci'],
|
|
7960
|
+
depends_on: [], recall_count: 0, ts, verified_at: ts, confidence: 0.75,
|
|
7961
|
+
audit_trail: [{ ts, action: 'cls_ci_fixed' }], version: 3,
|
|
7962
|
+
};
|
|
7963
|
+
await redis.rpush(`cachly:lessons:${slug}`, JSON.stringify(lessonObj));
|
|
7964
|
+
await redis.set(`cachly:lesson:best:${slug}`, JSON.stringify(lessonObj));
|
|
7965
|
+
}
|
|
7966
|
+
else if (isBroken) {
|
|
7967
|
+
const causeId = ckgSlug(`cause:${ckgSlug(job)}`);
|
|
7968
|
+
await ckgUpsertNode(redis, causeId, 'cause', 'ci-break');
|
|
7969
|
+
await ckgUpdateEdge(redis, conceptId, 'causes', causeId, false);
|
|
7970
|
+
}
|
|
7971
|
+
await redis.rpush(clsKey, JSON.stringify({ source, payload: { status, prev_status, job }, ts }));
|
|
7972
|
+
await redis.ltrim(clsKey, -200, -1);
|
|
7973
|
+
const statusIcon = isFixed ? '✅ Fixed' : isBroken ? '🔴 Broken' : '📊 Recorded';
|
|
7974
|
+
return [
|
|
7975
|
+
`📨 **CLS Ingested: ci_outcome**`, '',
|
|
7976
|
+
`${statusIcon}: \`${job}\` — ${prev_status || '?'} → ${status}`,
|
|
7977
|
+
isFixed ? `🔧 CKG \`fixes\` edge added (75% confidence)` : isBroken ? `⚡ CKG \`causes\` edge added` : `📊 State recorded`,
|
|
7978
|
+
`💡 Lesson: \`${slug}\` | Predict: \`brain_predict(context="${job}")\``,
|
|
7979
|
+
].join('\n');
|
|
7980
|
+
}
|
|
7981
|
+
if (source === 'ide_diagnostic') {
|
|
7982
|
+
const error = String(payload.error ?? '');
|
|
7983
|
+
const fix = String(payload.fix ?? '');
|
|
7984
|
+
const file = String(payload.file ?? '');
|
|
7985
|
+
const errorConcept = extractProblemConcept(error) ?? 'unknown-error';
|
|
7986
|
+
const slug = `debug:${ckgSlug(errorConcept)}`;
|
|
7987
|
+
const conceptId = ckgSlug(slug);
|
|
7988
|
+
const problemId = ckgSlug(`problem:${errorConcept}`);
|
|
7989
|
+
await ckgUpsertNode(redis, conceptId, 'debug', 'diagnostic');
|
|
7990
|
+
await ckgUpsertNode(redis, problemId, 'problem', 'compiler-error');
|
|
7991
|
+
await ckgUpdateEdge(redis, conceptId, 'fixes', problemId, true);
|
|
7992
|
+
const lessonObj = {
|
|
7993
|
+
topic: slug, outcome: 'success', what_worked: fix, what_failed: error,
|
|
7994
|
+
context: `CLS/ide: ${file}`, severity: 'minor',
|
|
7995
|
+
file_paths: file ? [file] : [], commands: [], tags: ['cls', 'ide-diagnostic'],
|
|
7996
|
+
depends_on: [], recall_count: 0, ts, verified_at: ts, confidence: 0.65,
|
|
7997
|
+
audit_trail: [{ ts, action: 'cls_ide_diagnostic' }], version: 3,
|
|
7998
|
+
};
|
|
7999
|
+
await redis.rpush(`cachly:lessons:${slug}`, JSON.stringify(lessonObj));
|
|
8000
|
+
const existingL = await redis.get(`cachly:lesson:best:${slug}`);
|
|
8001
|
+
if (!existingL)
|
|
8002
|
+
await redis.set(`cachly:lesson:best:${slug}`, JSON.stringify(lessonObj));
|
|
8003
|
+
await redis.rpush(clsKey, JSON.stringify({ source, payload: { error: error.slice(0, 60), fix: fix.slice(0, 60), file }, ts }));
|
|
8004
|
+
await redis.ltrim(clsKey, -200, -1);
|
|
8005
|
+
return [
|
|
8006
|
+
`📨 **CLS Ingested: ide_diagnostic**`, '',
|
|
8007
|
+
`Error: \`${error.slice(0, 80)}\``,
|
|
8008
|
+
`Fix: ${fix.slice(0, 100)}`,
|
|
8009
|
+
file ? `File: \`${file}\`` : '',
|
|
8010
|
+
'', `🕸️ CKG: \`${conceptId}\` → fixes → \`${problemId}\` | Lesson: \`${slug}\``,
|
|
8011
|
+
].filter(l => l !== '').join('\n');
|
|
8012
|
+
}
|
|
8013
|
+
return `❌ Unknown CLS source: "${source}". Valid: git_commit, ci_outcome, ide_diagnostic`;
|
|
8014
|
+
}
|
|
8015
|
+
// ── Layer 5: CLS — cls_install_hooks ─────────────────────────────────────
|
|
8016
|
+
case 'cls_install_hooks': {
|
|
8017
|
+
const { instance_id, repo_path = '.', hooks = ['git', 'ci'] } = args;
|
|
8018
|
+
const hooksArr = Array.isArray(hooks) ? hooks : ['git', 'ci'];
|
|
8019
|
+
const lines = [`🔌 **CLS Hook Installation Guide**\n`];
|
|
8020
|
+
if (hooksArr.includes('git')) {
|
|
8021
|
+
const hookScript = [
|
|
8022
|
+
`#!/bin/sh`,
|
|
8023
|
+
`# cachly CLS — Continuous Learning Stream git hook`,
|
|
8024
|
+
`# Installed by cls_install_hooks · runs silently on every commit`,
|
|
8025
|
+
`INSTANCE="${instance_id}"`,
|
|
8026
|
+
`SHA=$(git rev-parse HEAD 2>/dev/null || echo "")`,
|
|
8027
|
+
`MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1)`,
|
|
8028
|
+
`FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' ',' | sed 's/,$//')`,
|
|
8029
|
+
`node -e "`,
|
|
8030
|
+
`const p={instance_id:'$INSTANCE',source:'git_commit',payload:{message:$(echo "$MSG" | jq -R . 2>/dev/null || echo '"commit"'),sha:'$SHA',files:'$FILES'.split(',').filter(Boolean)}};`,
|
|
8031
|
+
`try{require('child_process').execSync('npx @cachly-dev/mcp-server@latest cls-ingest \\''+JSON.stringify(p)+'\\'',{stdio:'ignore',timeout:5000})}catch(e){}`,
|
|
8032
|
+
`" 2>/dev/null &`,
|
|
8033
|
+
`exit 0`,
|
|
8034
|
+
].join('\n');
|
|
8035
|
+
lines.push(`### Git post-commit hook`);
|
|
8036
|
+
lines.push(`**Quick install (run once per repo):**`);
|
|
8037
|
+
lines.push('```sh');
|
|
8038
|
+
lines.push(`cat > ${repo_path}/.git/hooks/post-commit << 'HOOK'`);
|
|
8039
|
+
lines.push(hookScript);
|
|
8040
|
+
lines.push(`HOOK`);
|
|
8041
|
+
lines.push(`chmod +x ${repo_path}/.git/hooks/post-commit`);
|
|
8042
|
+
lines.push('```');
|
|
8043
|
+
lines.push(`After install: every \`git commit\` automatically updates your brain's CKG.`);
|
|
8044
|
+
lines.push('');
|
|
8045
|
+
}
|
|
8046
|
+
if (hooksArr.includes('ci')) {
|
|
8047
|
+
lines.push(`### GitHub Actions CI outcome hook`);
|
|
8048
|
+
lines.push(`**Add at the end of each job** (after build/test steps):`);
|
|
8049
|
+
lines.push('```yaml');
|
|
8050
|
+
lines.push(`- name: cachly CLS — record CI outcome`);
|
|
8051
|
+
lines.push(` if: always()`);
|
|
8052
|
+
lines.push(` run: |`);
|
|
8053
|
+
lines.push(` node -e "`);
|
|
8054
|
+
lines.push(` const r=require('https');`);
|
|
8055
|
+
lines.push(` const d=JSON.stringify({instance_id:'${instance_id}',source:'ci_outcome',payload:{`);
|
|
8056
|
+
lines.push(` status:'\${{ job.status }}',prev_status:'unknown',job:'\${{ github.job }}',`);
|
|
8057
|
+
lines.push(` context:'github-actions run \${{ github.run_number }}'}});`);
|
|
8058
|
+
lines.push(` r.request({hostname:'api.cachly.dev',path:'/api/v1/cls/ingest',method:'POST',`);
|
|
8059
|
+
lines.push(` headers:{'Content-Type':'application/json','Authorization':'Bearer \$CACHLY_JWT',`);
|
|
8060
|
+
lines.push(` 'Content-Length':d.length}},()=>{}).end(d);`);
|
|
8061
|
+
lines.push(` " 2>/dev/null || true`);
|
|
8062
|
+
lines.push(` env:`);
|
|
8063
|
+
lines.push(` CACHLY_JWT: \${{ secrets.CACHLY_JWT }}`);
|
|
8064
|
+
lines.push('```');
|
|
8065
|
+
lines.push('');
|
|
8066
|
+
}
|
|
8067
|
+
lines.push(`💡 Once installed: \`brain_search(query="cls")\` to verify events are arriving.`);
|
|
8068
|
+
lines.push(`📊 Monitor CKG growth: \`ckg_inspect(concept="ci")\` or \`ckg_inspect(concept="fix")\``);
|
|
8069
|
+
return lines.join('\n');
|
|
8070
|
+
}
|
|
8071
|
+
// ── Layer 6: FedBrain — fedbrain_contribute ───────────────────────────────
|
|
8072
|
+
case 'fedbrain_contribute': {
|
|
8073
|
+
const { instance_id, lesson_key, visibility = 'public' } = args;
|
|
8074
|
+
const redis = await getConnection(instance_id);
|
|
8075
|
+
const raw = await redis.get(`cachly:lesson:best:${lesson_key}`);
|
|
8076
|
+
if (!raw)
|
|
8077
|
+
return `❌ Lesson \`${lesson_key}\` not found. Store it first with \`learn_from_attempts\`.`;
|
|
8078
|
+
const lesson = JSON.parse(raw);
|
|
8079
|
+
const domainTokens = [lesson.topic.split(':')[0], ...(lesson.tags ?? [])].filter(Boolean);
|
|
8080
|
+
const domainFingerprint = [...new Set(domainTokens)].sort().join(',');
|
|
8081
|
+
// HMAC certificate ID (non-reversible, privacy-safe)
|
|
8082
|
+
const certContent = `${lesson.topic}:${lesson.outcome}:${lesson.what_worked}`;
|
|
8083
|
+
const certId = createHmac('sha256', `cachly-fedbrain:${instance_id}`).update(certContent).digest('hex').slice(0, 16);
|
|
8084
|
+
const cert = {
|
|
8085
|
+
cert_id: certId, lesson_key, visibility,
|
|
8086
|
+
domain_fingerprint: domainFingerprint,
|
|
8087
|
+
contributed_at: new Date().toISOString(),
|
|
8088
|
+
confirm_count: 0,
|
|
8089
|
+
trust_score: lesson.outcome === 'success' ? 0.85 : 0.5,
|
|
8090
|
+
};
|
|
8091
|
+
await redis.set(`cachly:fedbrain:cert:${certId}`, JSON.stringify(cert));
|
|
8092
|
+
await redis.sadd('cachly:fedbrain:contributed', certId);
|
|
8093
|
+
// Try global commons via syndication API
|
|
8094
|
+
let syndicationResult;
|
|
8095
|
+
try {
|
|
8096
|
+
await apiFetch('/api/v1/syndication/contribute', {
|
|
8097
|
+
method: 'POST',
|
|
8098
|
+
body: JSON.stringify({
|
|
8099
|
+
topic: lesson.topic, outcome: lesson.outcome,
|
|
8100
|
+
what_worked: lesson.what_worked, what_failed: lesson.what_failed ?? '',
|
|
8101
|
+
severity: lesson.severity ?? 'major', cert_id: certId,
|
|
8102
|
+
domain_fingerprint: domainFingerprint, visibility,
|
|
8103
|
+
}),
|
|
8104
|
+
});
|
|
8105
|
+
syndicationResult = `✅ Contributed to global commons`;
|
|
8106
|
+
}
|
|
8107
|
+
catch {
|
|
8108
|
+
syndicationResult = `📦 Stored locally (commons API unavailable — will sync when online)`;
|
|
8109
|
+
}
|
|
8110
|
+
return [
|
|
8111
|
+
`🌐 **FedBrain Contribute: "${lesson_key}"**`, '',
|
|
8112
|
+
`📜 Certificate: \`${certId}\``,
|
|
8113
|
+
`🏷️ Domain fingerprint: ${domainTokens.slice(0, 6).map(t => `\`${t}\``).join(', ')}`,
|
|
8114
|
+
`🔒 Visibility: ${visibility}`,
|
|
8115
|
+
'', syndicationResult, '',
|
|
8116
|
+
`💡 At 10 independent confirms → 🏆 Gold Standard`,
|
|
8117
|
+
`🔍 Search: \`fedbrain_search(query="${lesson.topic.split(':').slice(-1)[0]}")\``,
|
|
8118
|
+
].join('\n');
|
|
8119
|
+
}
|
|
8120
|
+
// ── Layer 6: FedBrain — fedbrain_search ──────────────────────────────────
|
|
8121
|
+
case 'fedbrain_search': {
|
|
8122
|
+
const { instance_id, query, context_hints = [], limit = 10 } = args;
|
|
8123
|
+
const redis = await getConnection(instance_id);
|
|
8124
|
+
// Build local domain context from contributed certificates + explicit hints
|
|
8125
|
+
const contribIds = await redis.smembers('cachly:fedbrain:contributed');
|
|
8126
|
+
const localDomains = new Map();
|
|
8127
|
+
for (const certId of contribIds.slice(0, 30)) {
|
|
8128
|
+
const certRaw = await redis.get(`cachly:fedbrain:cert:${certId}`);
|
|
8129
|
+
if (!certRaw)
|
|
7492
8130
|
continue;
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
8131
|
+
try {
|
|
8132
|
+
const cert = JSON.parse(certRaw);
|
|
8133
|
+
for (const d of (cert.domain_fingerprint ?? '').split(',').filter(Boolean)) {
|
|
8134
|
+
localDomains.set(d, (localDomains.get(d) ?? 0) + 1);
|
|
8135
|
+
}
|
|
8136
|
+
}
|
|
8137
|
+
catch { /* skip */ }
|
|
8138
|
+
}
|
|
8139
|
+
for (const hint of (Array.isArray(context_hints) ? context_hints : [])) {
|
|
8140
|
+
localDomains.set(hint.toLowerCase(), (localDomains.get(hint.toLowerCase()) ?? 0) + 2);
|
|
8141
|
+
}
|
|
8142
|
+
let results = [];
|
|
8143
|
+
try {
|
|
8144
|
+
const params = new URLSearchParams({ q: query, limit: String(Math.min(limit * 2, 50)) });
|
|
8145
|
+
const res = await apiFetch(`/api/v1/syndication/search?${params}`);
|
|
8146
|
+
results = res.results ?? [];
|
|
8147
|
+
}
|
|
8148
|
+
catch {
|
|
8149
|
+
// Fallback: search local lessons
|
|
8150
|
+
const lessonKeys = [];
|
|
8151
|
+
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
8152
|
+
await new Promise((res, rej) => { lStream.on('data', (b) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
|
|
8153
|
+
for (const k of lessonKeys.slice(0, 60)) {
|
|
8154
|
+
const r = await redis.get(k);
|
|
8155
|
+
if (!r)
|
|
8156
|
+
continue;
|
|
8157
|
+
try {
|
|
8158
|
+
const l = JSON.parse(r);
|
|
8159
|
+
const haystack = `${l.topic} ${l.what_worked ?? ''} ${l.what_failed ?? ''}`.toLowerCase();
|
|
8160
|
+
if (query.toLowerCase().split(/\s+/).some(t => t.length > 2 && haystack.includes(t))) {
|
|
8161
|
+
results.push({ id: k.split(':').pop() ?? k, topic: l.topic, category: l.topic.split(':')[0], outcome: l.outcome, what_worked: l.what_worked ?? '', severity: l.severity ?? 'major', confirm_count: 0, created_at: l.ts ?? new Date().toISOString() });
|
|
8162
|
+
}
|
|
8163
|
+
}
|
|
8164
|
+
catch { /* skip */ }
|
|
8165
|
+
}
|
|
8166
|
+
}
|
|
8167
|
+
if (results.length === 0) {
|
|
8168
|
+
return [`🌐 **FedBrain Search: "${query}"**`, '', `No results. Contribute: \`fedbrain_contribute(lesson_key="fix:...")\``].join('\n');
|
|
8169
|
+
}
|
|
8170
|
+
// Context-weighted ranking
|
|
8171
|
+
const ranked = results.map(r => {
|
|
8172
|
+
const rDomains = (r.domain_fingerprint ?? r.category ?? '').split(',').filter(Boolean);
|
|
8173
|
+
const overlap = rDomains.reduce((s, d) => s + (localDomains.get(d) ?? 0), 0);
|
|
8174
|
+
const contextScore = localDomains.size > 0 ? overlap / Math.max(1, localDomains.size + rDomains.length) : 0;
|
|
8175
|
+
const confirmedScore = Math.min(1, r.confirm_count / 10);
|
|
8176
|
+
const weightedScore = (contextScore * 0.4) + (confirmedScore * 0.4) + (r.outcome === 'success' ? 0.2 : 0);
|
|
8177
|
+
return { ...r, weightedScore, isGoldStandard: r.confirm_count >= 10 };
|
|
8178
|
+
}).sort((a, b) => b.weightedScore - a.weightedScore).slice(0, limit);
|
|
8179
|
+
const lines = [`🌐 **FedBrain Search: "${query}"** — ${ranked.length} result${ranked.length !== 1 ? 's' : ''} (context-weighted)\n`];
|
|
8180
|
+
for (const r of ranked) {
|
|
8181
|
+
const icon = r.outcome === 'success' ? '✅' : r.outcome === 'failure' ? '❌' : '⚠️';
|
|
8182
|
+
const goldBadge = r.isGoldStandard ? ' 🏆 _Gold Standard_' : r.confirm_count >= 3 ? ` ✓${r.confirm_count}` : '';
|
|
8183
|
+
const ctxPct = Math.round(r.weightedScore * 100);
|
|
8184
|
+
lines.push(`${icon}${goldBadge} **\`${r.topic}\`** [ctx: ${ctxPct}%]`);
|
|
8185
|
+
if (r.what_worked)
|
|
8186
|
+
lines.push(` ✅ ${r.what_worked.slice(0, 150)}`);
|
|
8187
|
+
if (r.what_failed)
|
|
8188
|
+
lines.push(` ❌ ${r.what_failed.slice(0, 80)}`);
|
|
8189
|
+
lines.push(` _${r.confirm_count} confirm${r.confirm_count !== 1 ? 's' : ''} | \`${r.id.slice(0, 12)}\`_`, '');
|
|
8190
|
+
}
|
|
8191
|
+
if (localDomains.size > 0) {
|
|
8192
|
+
const topDomains = [...localDomains.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([d, n]) => `\`${d}\`(${n})`).join(', ');
|
|
8193
|
+
lines.push(`🎯 Your context: ${topDomains}`);
|
|
8194
|
+
}
|
|
8195
|
+
lines.push(`💡 Confirm: \`fedbrain_confirm(topic="<topic>", outcome="worked")\``);
|
|
8196
|
+
return lines.join('\n');
|
|
8197
|
+
}
|
|
8198
|
+
// ── Layer 6: FedBrain — fedbrain_confirm ─────────────────────────────────
|
|
8199
|
+
case 'fedbrain_confirm': {
|
|
8200
|
+
const { instance_id, topic, outcome } = args;
|
|
8201
|
+
const redis = await getConnection(instance_id);
|
|
8202
|
+
const ts = new Date().toISOString();
|
|
8203
|
+
const confirmEntry = JSON.stringify({ topic, outcome, ts });
|
|
8204
|
+
await redis.rpush('cachly:fedbrain:confirmations', confirmEntry);
|
|
8205
|
+
await redis.ltrim('cachly:fedbrain:confirmations', -200, -1);
|
|
8206
|
+
// Update local CKG confidence
|
|
8207
|
+
const worked = outcome === 'worked';
|
|
8208
|
+
const partial = outcome === 'partially_worked';
|
|
8209
|
+
await ckgUpdateEdge(redis, ckgSlug(topic), 'fixes', ckgSlug(`syndicated:${topic}`), worked, partial);
|
|
8210
|
+
// Propagate to global commons
|
|
8211
|
+
let propResult;
|
|
8212
|
+
try {
|
|
8213
|
+
await apiFetch('/api/v1/syndication/confirm', { method: 'POST', body: JSON.stringify({ topic, outcome }) });
|
|
8214
|
+
propResult = `✅ Confirmation propagated to global commons`;
|
|
8215
|
+
}
|
|
8216
|
+
catch {
|
|
8217
|
+
await redis.rpush('cachly:fedbrain:pending_confirms', confirmEntry);
|
|
8218
|
+
await redis.ltrim('cachly:fedbrain:pending_confirms', -50, -1);
|
|
8219
|
+
propResult = `📦 Queued locally (API unavailable — will propagate on next online session)`;
|
|
8220
|
+
}
|
|
8221
|
+
const icon = worked ? '✅' : partial ? '⚠️' : '❌';
|
|
8222
|
+
return [
|
|
8223
|
+
`${icon} **FedBrain Confirm: "${topic}"** → ${outcome}`, '',
|
|
8224
|
+
propResult, '',
|
|
8225
|
+
`🕸️ CKG confidence ${worked || partial ? 'boosted' : 'reduced'} for \`${ckgSlug(topic)}\``,
|
|
8226
|
+
`💡 Your confirmation helps other brains worldwide.`,
|
|
8227
|
+
`📊 Status: \`fedbrain_status(instance_id="...")\``,
|
|
8228
|
+
].join('\n');
|
|
8229
|
+
}
|
|
8230
|
+
// ── Layer 6: FedBrain — fedbrain_status ──────────────────────────────────
|
|
8231
|
+
case 'fedbrain_status': {
|
|
8232
|
+
const { instance_id } = args;
|
|
8233
|
+
const redis = await getConnection(instance_id);
|
|
8234
|
+
const contribIds = await redis.smembers('cachly:fedbrain:contributed');
|
|
8235
|
+
const confirmsRaw = await redis.lrange('cachly:fedbrain:confirmations', -10, -1);
|
|
8236
|
+
const pendingConfirms = await redis.llen('cachly:fedbrain:pending_confirms');
|
|
8237
|
+
const confirms = confirmsRaw.map(r => { try {
|
|
8238
|
+
return JSON.parse(r);
|
|
8239
|
+
}
|
|
8240
|
+
catch {
|
|
8241
|
+
return null;
|
|
8242
|
+
} }).filter(Boolean);
|
|
8243
|
+
const certDetails = [];
|
|
8244
|
+
for (const certId of contribIds.slice(0, 15)) {
|
|
8245
|
+
const raw = await redis.get(`cachly:fedbrain:cert:${certId}`);
|
|
8246
|
+
if (!raw)
|
|
7498
8247
|
continue;
|
|
8248
|
+
try {
|
|
8249
|
+
const cert = JSON.parse(raw);
|
|
8250
|
+
certDetails.push({ cert_id: cert.cert_id, lesson_key: cert.lesson_key, confirm_count: cert.confirm_count ?? 0, trust_score: cert.trust_score ?? 0.5, isGold: (cert.confirm_count ?? 0) >= 10 });
|
|
7499
8251
|
}
|
|
7500
|
-
|
|
7501
|
-
topic,
|
|
7502
|
-
outcome: isFail ? 'failure' : 'success',
|
|
7503
|
-
what_worked: isFail ? '' : cleanMsg.slice(0, 300),
|
|
7504
|
-
what_failed: isFail ? cleanMsg.slice(0, 300) : undefined,
|
|
7505
|
-
context: `Auto-learned from git commit ${(hash ?? '').slice(0, 7)}${dateStr ? ` at ${dateStr}` : ''} in ${workspace_path}${author ? ` (author: ${author})` : ''}`,
|
|
7506
|
-
severity: 'minor',
|
|
7507
|
-
ts: dateStr ? new Date(dateStr).toISOString() : now.toISOString(),
|
|
7508
|
-
recall_count: 0,
|
|
7509
|
-
auto_learned: true,
|
|
7510
|
-
source: 'brain_from_git',
|
|
7511
|
-
version: 3,
|
|
7512
|
-
};
|
|
7513
|
-
await redis.set(key, JSON.stringify(lesson));
|
|
7514
|
-
await redis.expire(key, 90 * 86400); // 90-day TTL for git-sourced lessons
|
|
7515
|
-
stored++;
|
|
7516
|
-
if (isFail)
|
|
7517
|
-
failures++;
|
|
7518
|
-
if (topLessons.length < 5)
|
|
7519
|
-
topLessons.push(` ${isFail ? '❌' : '✅'} \`${topic}\` — ${cleanMsg.slice(0, 80)}`);
|
|
8252
|
+
catch { /* skip */ }
|
|
7520
8253
|
}
|
|
7521
8254
|
const lines = [
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
`📊 **Scanned:** ${commitLines.filter(l => l.trim()).length} commits over last ${clampedDays} days`,
|
|
7525
|
-
`✅ **Success lessons stored:** ${stored - failures}`,
|
|
7526
|
-
`❌ **Failure/revert lessons:** ${failures}`,
|
|
7527
|
-
skipped > 0 ? `⏭️ **Already in brain (skipped):** ${skipped}` : '',
|
|
7528
|
-
``,
|
|
8255
|
+
`🌐 **FedBrain Status**\n`,
|
|
8256
|
+
`### 📤 Contributed Lessons: ${contribIds.length}`,
|
|
7529
8257
|
];
|
|
7530
|
-
if (
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
lines.push(`
|
|
7535
|
-
|
|
8258
|
+
if (certDetails.length > 0) {
|
|
8259
|
+
for (const c of certDetails) {
|
|
8260
|
+
const goldBadge = c.isGold ? ' 🏆 Gold Standard' : '';
|
|
8261
|
+
const confBar = '█'.repeat(Math.min(10, c.confirm_count)) + '░'.repeat(Math.max(0, 10 - c.confirm_count));
|
|
8262
|
+
lines.push(` \`${c.lesson_key}\` [${confBar}] ×${c.confirm_count}${goldBadge}`);
|
|
8263
|
+
}
|
|
7536
8264
|
}
|
|
7537
|
-
|
|
7538
|
-
|
|
8265
|
+
else {
|
|
8266
|
+
lines.push(` _None yet. Contribute with \`fedbrain_contribute(lesson_key="fix:...")\`_`);
|
|
8267
|
+
}
|
|
8268
|
+
lines.push('', `### 📥 Recent Confirmations: ${confirms.length}`);
|
|
8269
|
+
if (confirms.length > 0) {
|
|
8270
|
+
for (const c of confirms.slice(-5)) {
|
|
8271
|
+
const icon = c.outcome === 'worked' ? '✅' : c.outcome === 'partially_worked' ? '⚠️' : '❌';
|
|
8272
|
+
lines.push(` ${icon} \`${c.topic}\` — ${c.outcome} (${new Date(c.ts).toLocaleDateString('de-DE')})`);
|
|
8273
|
+
}
|
|
8274
|
+
}
|
|
8275
|
+
else {
|
|
8276
|
+
lines.push(` _None yet. Confirm syndicated lessons with \`fedbrain_confirm\`_`);
|
|
8277
|
+
}
|
|
8278
|
+
if (pendingConfirms > 0) {
|
|
8279
|
+
lines.push('', `⚠️ ${pendingConfirms} confirmation${pendingConfirms !== 1 ? 's' : ''} pending propagation`);
|
|
8280
|
+
}
|
|
8281
|
+
lines.push('', '---', `**Contribute:** \`fedbrain_contribute(lesson_key="fix:...")\``, `**Search:** \`fedbrain_search(query="...")\``, `**Confirm:** \`fedbrain_confirm(topic="...", outcome="worked")\``);
|
|
8282
|
+
return lines.join('\n');
|
|
7539
8283
|
}
|
|
7540
8284
|
default:
|
|
7541
8285
|
return null;
|