@cachly-dev/mcp-server 0.10.123 → 0.10.125
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/src/aussetzer.d.ts +67 -0
- package/dist/src/aussetzer.js +82 -0
- package/dist/src/aussetzer.js.map +1 -0
- package/dist/src/autor-abzeichen.d.ts +41 -0
- package/dist/src/autor-abzeichen.js +56 -0
- package/dist/src/autor-abzeichen.js.map +1 -0
- package/dist/src/autor.d.ts +101 -0
- package/dist/src/autor.js +136 -0
- package/dist/src/autor.js.map +1 -0
- package/dist/src/bauabdruck-daten.generiert.json +5 -0
- package/dist/src/bauabdruck.d.ts +38 -0
- package/dist/src/bauabdruck.js +93 -0
- package/dist/src/bauabdruck.js.map +1 -0
- package/dist/src/bedeutung.d.ts +175 -0
- package/dist/src/bedeutung.js +254 -0
- package/dist/src/bedeutung.js.map +1 -0
- package/dist/src/brain-export.d.ts +24 -0
- package/dist/src/brain-export.js +31 -1
- package/dist/src/brain-export.js.map +1 -1
- package/dist/src/deutsch.d.ts +98 -0
- package/dist/src/deutsch.js +223 -0
- package/dist/src/deutsch.js.map +1 -0
- package/dist/src/eingaenge-nachruesten.d.ts +41 -0
- package/dist/src/eingaenge-nachruesten.js +290 -0
- package/dist/src/eingaenge-nachruesten.js.map +1 -0
- package/dist/src/eingaenge.d.ts +135 -0
- package/dist/src/eingaenge.js +228 -0
- package/dist/src/eingaenge.js.map +1 -0
- package/dist/src/geliefert.d.ts +63 -0
- package/dist/src/geliefert.js +114 -0
- package/dist/src/geliefert.js.map +1 -0
- package/dist/src/handlers/advanced.js +7 -3
- package/dist/src/handlers/advanced.js.map +1 -1
- package/dist/src/handlers/brain.d.ts +19 -0
- package/dist/src/handlers/brain.js +492 -48
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/handlers/instances.d.ts +24 -0
- package/dist/src/handlers/instances.js +49 -1
- package/dist/src/handlers/instances.js.map +1 -1
- package/dist/src/handlers/team.js +105 -0
- package/dist/src/handlers/team.js.map +1 -1
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.js +282 -36
- package/dist/src/index.js.map +1 -1
- package/dist/src/lesson-preview.d.ts +81 -0
- package/dist/src/lesson-preview.js +133 -0
- package/dist/src/lesson-preview.js.map +1 -0
- package/dist/src/rangfolge.d.ts +169 -0
- package/dist/src/rangfolge.js +214 -0
- package/dist/src/rangfolge.js.map +1 -0
- package/dist/src/raumkorrektur.d.ts +100 -0
- package/dist/src/raumkorrektur.js +197 -0
- package/dist/src/raumkorrektur.js.map +1 -0
- package/dist/src/recall-tiefe.d.ts +48 -0
- package/dist/src/recall-tiefe.js +42 -0
- package/dist/src/recall-tiefe.js.map +1 -0
- package/dist/src/rerank.d.ts +0 -14
- package/dist/src/rerank.js +39 -2
- package/dist/src/rerank.js.map +1 -1
- package/dist/src/search.d.ts +56 -17
- package/dist/src/search.js +335 -73
- package/dist/src/search.js.map +1 -1
- package/dist/src/seltenheitsbestand.d.ts +53 -0
- package/dist/src/seltenheitsbestand.js +92 -0
- package/dist/src/seltenheitsbestand.js.map +1 -0
- package/dist/src/spuren.d.ts +97 -0
- package/dist/src/spuren.js +150 -0
- package/dist/src/spuren.js.map +1 -0
- package/dist/src/tools.js +1 -1
- package/dist/src/tools.js.map +1 -1
- package/dist/src/vorspann.d.ts +73 -0
- package/dist/src/vorspann.js +146 -0
- package/dist/src/vorspann.js.map +1 -0
- package/dist/src/werkzeug-nutzung.d.ts +62 -0
- package/dist/src/werkzeug-nutzung.js +72 -0
- package/dist/src/werkzeug-nutzung.js.map +1 -0
- package/dist/src/wertbeitrag.d.ts +57 -0
- package/dist/src/wertbeitrag.js +73 -0
- package/dist/src/wertbeitrag.js.map +1 -0
- package/glama.json +1 -1
- package/package.json +12 -4
- package/server.json +2 -2
|
@@ -3,11 +3,68 @@ import { createRequire } from 'node:module';
|
|
|
3
3
|
import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES, CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS } from '../confidence.js';
|
|
4
4
|
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge, ckgUpsertPersonNode, ckgUpsertFileNode, ckgRecordCollaboration, ckgUpsertServiceNode } from '../ckg.js';
|
|
5
5
|
import { safeJsonParse, scanKeys } from '../utils.js';
|
|
6
|
+
import { ersparteMinuten, istStarterLektion } from '../wertbeitrag.js';
|
|
7
|
+
import { lessonPreviewLines, trimTo } from '../lesson-preview.js';
|
|
8
|
+
import { meldeEinmal, OHNE_VEKTOREN, OHNE_DIENST, OHNE_FRAGEVEKTOR } from '../aussetzer.js';
|
|
9
|
+
/**
|
|
10
|
+
* Preview for one smart_recall hit.
|
|
11
|
+
*
|
|
12
|
+
* WHY THIS EXISTS: the previous version sliced `r.content` — the RAW JSON of the
|
|
13
|
+
* stored record — at 300/400 characters. So the budget was spent on syntax:
|
|
14
|
+
*
|
|
15
|
+
* > {"topic":"node4:einrichtung-contabo-und-fallen","outcome":"success","sev…
|
|
16
|
+
*
|
|
17
|
+
* Roughly the first 80 characters carry no information at all, the reader gets
|
|
18
|
+
* field names instead of facts, and `commands` / `what_failed` never appear
|
|
19
|
+
* because they sit far past the cut. smart_recall is the call this project
|
|
20
|
+
* mandates BEFORE EVERY TASK — it is the worst possible place for that.
|
|
21
|
+
*
|
|
22
|
+
* A lesson is now rendered as a lesson: trimmed prose, its warning when the
|
|
23
|
+
* lesson is critical, and its commands/paths. Anything that is not a lesson
|
|
24
|
+
* (context entries, indexes) keeps a raw preview — but trimmed at a word
|
|
25
|
+
* boundary and with an honest cut marker.
|
|
26
|
+
*/
|
|
27
|
+
export function recallVorschau(key, content, maxChars) {
|
|
28
|
+
if (key.includes('lesson:best:')) {
|
|
29
|
+
const l = safeJsonParse(content, {});
|
|
30
|
+
if (l && typeof l === 'object' && (l.what_worked || l.what_failed || l.commands?.length)) {
|
|
31
|
+
return lessonPreviewLines(l, { maxChars, indent: ' ' });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const roh = trimTo(content, maxChars);
|
|
35
|
+
return roh ? [roh] : [];
|
|
36
|
+
}
|
|
6
37
|
import { getRole, ROLE_BADGE, getScopes, lessonVisibleToScope, reviewModeEnabled, storeLessonProposal } from './team.js';
|
|
7
|
-
import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
|
|
38
|
+
import { keywordSearch, wortindexEntwerten, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
|
|
8
39
|
import { rerankByQuality, qualityMultiplier, extractLessonQuality } from '../rerank.js';
|
|
9
40
|
import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
|
|
41
|
+
import { VEKTOR_PRAEFIX, NAME_VEKTOR_PRAEFIX, packe, textFuerVektor, textFuerNamensVektor, Vektorbestand, } from '../bedeutung.js';
|
|
42
|
+
import { schreibeEingaenge, Eingangsbestand, EINGANG_GEWICHT } from '../eingaenge.js';
|
|
43
|
+
import { Seltenheitsbestand } from '../seltenheitsbestand.js';
|
|
44
|
+
import { spurLegen } from '../spuren.js';
|
|
45
|
+
import { bewerteTopf, spreizeImTopf, reichereAn, inhaltsWoerter, grobStamm, GEWICHTE, } from '../rangfolge.js';
|
|
46
|
+
/**
|
|
47
|
+
* Ein Vektorbestand fuer den ganzen Prozess.
|
|
48
|
+
*
|
|
49
|
+
* Er haelt rund 2 MB je 500 Lektionen im Arbeitsspeicher. Die bei jeder Frage
|
|
50
|
+
* neu zu holen waere langsamer als der Wortabgleich, den wir gerade ergaenzen —
|
|
51
|
+
* der Gewinn waere weg, bevor er anfaengt.
|
|
52
|
+
*/
|
|
53
|
+
const vektorbestand = new Vektorbestand();
|
|
54
|
+
/** Dieselbe Mechanik, andere Sicht: die Vektoren der Themennamen. */
|
|
55
|
+
const namensbestand = new Vektorbestand(60_000, NAME_VEKTOR_PRAEFIX);
|
|
56
|
+
/** Die Fehlertext-Eingaenge, je Lektion ihr bester. */
|
|
57
|
+
const eingangsbestand = new Eingangsbestand();
|
|
58
|
+
/** Die Wortstatistik des Bestands — stehend, nicht je Frage neu. */
|
|
59
|
+
const seltenheitsbestand = new Seltenheitsbestand();
|
|
10
60
|
import { upgradeNudge } from '../upgrade-nudge.js';
|
|
61
|
+
import { recallTiefe, TIEFE_VOLL, TIEFE_VOLL_MEHRTHEMIG } from '../recall-tiefe.js';
|
|
62
|
+
import { nutzungInWorten, nutzungsSchluessel, verdichte } from '../werkzeug-nutzung.js';
|
|
63
|
+
import { inWorten as lieferungInWorten, lieferBild, lieferSchluessel, trefferSchluessel } from '../geliefert.js';
|
|
64
|
+
import { COAUTHOR_PREF_KEY, autorHinweis, coautorAus, echterGitLeser, ermittleAutor, mitCoautor, nurMensch } from '../autor.js';
|
|
65
|
+
import { TOOLS } from '../tools.js';
|
|
66
|
+
import { vorspannHinweis } from '../vorspann.js';
|
|
67
|
+
import { autorAbzeichen, fremdanteil } from '../autor-abzeichen.js';
|
|
11
68
|
import { cachlyUrl } from '../cachly-url.js';
|
|
12
69
|
// ── Changelog (shown once per version in session_start) ──────────────────────
|
|
13
70
|
// Resolve the package version at runtime from package.json so the session briefing
|
|
@@ -407,11 +464,15 @@ export const BRAIN_TOOL_NAMES = new Set([
|
|
|
407
464
|
]);
|
|
408
465
|
// ── Free-tier Teaser-Gate ──────────────────────────────────────────────────
|
|
409
466
|
// When a Free-tier Brain crosses its recall limit, smart_recall still delivers
|
|
410
|
-
// the top
|
|
411
|
-
// behind an upgrade teaser ("🔒 N
|
|
467
|
+
// the top hit — the magic moment is sacred — but withholds the long tail
|
|
468
|
+
// behind an upgrade teaser ("🔒 N further relevant lessons withheld"). This is a
|
|
412
469
|
// DEPTH gate (hide the tail), never an access wall: the #1 result is always
|
|
413
470
|
// returned. Usage/limit come from the same authoritative numbers as the Brain
|
|
414
471
|
// Health bar (GET /instances/:id/memory), cached briefly to avoid per-call latency.
|
|
472
|
+
//
|
|
473
|
+
// Diese Zusage stand von 2026-07 bis 2026-08-18 NUR hier im Kommentar, waehrend
|
|
474
|
+
// der Zweig `visibleCount = gateActive ? 0 : 8` null Treffer zurueckgab. Seither
|
|
475
|
+
// ist sie eine Funktion mit Gegenprobe: ../recall-tiefe.ts (GROW-043).
|
|
415
476
|
const UPGRADE_URL = cachlyUrl('/billing', 'upgrade');
|
|
416
477
|
const _recallGateCache = new Map();
|
|
417
478
|
/**
|
|
@@ -480,6 +541,29 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
480
541
|
const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', visibility = 'team', service = '', service_kind = 'service', group = '', } = args;
|
|
481
542
|
const redis = await getConnection(instance_id);
|
|
482
543
|
const ts = new Date().toISOString();
|
|
544
|
+
/*
|
|
545
|
+
* ── Der Autor wird HERGELEITET, nicht erwartet ─────────────────────
|
|
546
|
+
*
|
|
547
|
+
* Gemessen am 19.08.2026: 195 von 493 Lektionen ohne Autor. `author`
|
|
548
|
+
* war ein optionales Feld, das ein Agent bei jedem Aufruf mitschicken
|
|
549
|
+
* sollte — und natuerlich vergisst. Dabei steht der Name in
|
|
550
|
+
* `git config user.name`.
|
|
551
|
+
*
|
|
552
|
+
* Ein Pflichtfeld, das man vergessen kann, ist kein Pflichtfeld.
|
|
553
|
+
*/
|
|
554
|
+
const herkunft = ermittleAutor({
|
|
555
|
+
uebergeben: author,
|
|
556
|
+
umgebung: process.env.CACHLY_AUTHOR,
|
|
557
|
+
git: echterGitLeser(process.env.CACHLY_WORKSPACE ?? process.cwd()),
|
|
558
|
+
});
|
|
559
|
+
// Mitautor nur, wenn ausdruecklich eingeschaltet — Standard ist aus.
|
|
560
|
+
let coautor = '';
|
|
561
|
+
try {
|
|
562
|
+
coautor = coautorAus(await redis.get(`cachly:prefs:${COAUTHOR_PREF_KEY}:${instance_id}`));
|
|
563
|
+
}
|
|
564
|
+
catch { /* eine Einstellung darf das Speichern nie stoppen */ }
|
|
565
|
+
const autorFeld = mitCoautor(herkunft.autor, coautor);
|
|
566
|
+
const autorTipp = autorHinweis(herkunft);
|
|
483
567
|
// ── Lessons-Review workflow (P1-1) ─────────────────────────────────────
|
|
484
568
|
// When the org enabled pending-mode on this (shared) brain, team-visible
|
|
485
569
|
// lessons become proposals awaiting owner/admin approval instead of
|
|
@@ -489,7 +573,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
489
573
|
return storeLessonProposal(redis, topic, {
|
|
490
574
|
topic, outcome, what_worked, what_failed, context: ctx, severity,
|
|
491
575
|
file_paths, commands, tags, depends_on,
|
|
492
|
-
...(
|
|
576
|
+
...(autorFeld ? { author: autorFeld } : {}),
|
|
493
577
|
...(service ? { service } : {}),
|
|
494
578
|
...(group ? { group: String(group).toLowerCase().trim() } : {}),
|
|
495
579
|
visibility, recall_count: 0, ts, confidence: 1.0, version: 3,
|
|
@@ -611,7 +695,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
611
695
|
commands,
|
|
612
696
|
tags,
|
|
613
697
|
depends_on,
|
|
614
|
-
|
|
698
|
+
// Auch hier der hergeleitete Name — sonst traegt die gespeicherte
|
|
699
|
+
// Lektion selbst keinen Autor, waehrend der Kausalgraph einen hat.
|
|
700
|
+
...(autorFeld ? { author: autorFeld } : {}),
|
|
615
701
|
...(service ? { service } : {}),
|
|
616
702
|
...(group ? { group: String(group).toLowerCase().trim() } : {}),
|
|
617
703
|
visibility,
|
|
@@ -633,11 +719,71 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
633
719
|
await redis.ltrim(listKey, -100, -1);
|
|
634
720
|
await redis.expire(listKey, 90 * 86400);
|
|
635
721
|
// Update best key for success/partial; for failure only update if no success exists
|
|
722
|
+
let bestGeschrieben = false;
|
|
636
723
|
if (outcome === 'success' || outcome === 'partial') {
|
|
637
724
|
await redis.set(`cachly:lesson:best:${topic}`, lesson);
|
|
725
|
+
bestGeschrieben = true;
|
|
638
726
|
}
|
|
639
727
|
else if (!existingRaw) {
|
|
640
728
|
await redis.set(`cachly:lesson:best:${topic}`, lesson);
|
|
729
|
+
bestGeschrieben = true;
|
|
730
|
+
}
|
|
731
|
+
// Der stehende Wortindex kennt die neue Lektion noch nicht.
|
|
732
|
+
//
|
|
733
|
+
// Ohne diese Zeile waere sie bis zu eine Minute lang nicht auffindbar —
|
|
734
|
+
// und genau der Fall "gerade gelernt, sofort gesucht" ist der, bei dem
|
|
735
|
+
// ein Gedaechtnis sich beweisen muss. Der Index wird beim naechsten
|
|
736
|
+
// Recall neu gebaut, nicht hier: Lernen soll nicht auf das Bauen warten.
|
|
737
|
+
if (bestGeschrieben)
|
|
738
|
+
wortindexEntwerten();
|
|
739
|
+
// ── Vektor fuer den Bedeutungsabgleich ─────────────────────────────────
|
|
740
|
+
//
|
|
741
|
+
// Wird beim Schreiben berechnet, nicht beim Suchen. Der Grund steht in
|
|
742
|
+
// Zahlen: die Einbettung kostet rund 280 ms Netz. Einmal je Lektion ist
|
|
743
|
+
// das nichts, bei jeder Frage waere es der halbe Gewinn.
|
|
744
|
+
//
|
|
745
|
+
// Bewusst NICHT abgewartet: wer eine Lektion speichert, soll nicht auf
|
|
746
|
+
// einen fremden Dienst warten. Faellt die Einbettung aus, fehlt der
|
|
747
|
+
// Vektor — und der Bedeutungsabgleich uebergeht diese eine Lektion still.
|
|
748
|
+
// Das ist der Preis dafuer, dass Lernen immer funktioniert, auch ohne
|
|
749
|
+
// Netz. Wie viele Lektionen einen Vektor haben, meldet brain_doctor.
|
|
750
|
+
if (bestGeschrieben && hasEmbedProvider()) {
|
|
751
|
+
void (async () => {
|
|
752
|
+
const gelesen = safeJsonParse(lesson, {});
|
|
753
|
+
try {
|
|
754
|
+
const v = await computeEmbedding(textFuerVektor(gelesen));
|
|
755
|
+
if (v?.length)
|
|
756
|
+
await redis.set(`${VEKTOR_PRAEFIX}${topic}`, packe(v));
|
|
757
|
+
}
|
|
758
|
+
catch { /* kein Vektor ist besser als keine Lektion */ }
|
|
759
|
+
// Der Themenname als eigene Sicht.
|
|
760
|
+
//
|
|
761
|
+
// Er ist kurz und frageaehnlich; eine 60-Zeichen-Frage gegen 60
|
|
762
|
+
// Zeichen zu halten ist symmetrisch, gegen 1376 Zeichen Volltext
|
|
763
|
+
// nicht. Der Sortierer bewertet beide getrennt (naeheText 1,0,
|
|
764
|
+
// naeheThema 0,6). Bis zum 20.08.2026 gab es diese Vektoren nur im
|
|
765
|
+
// Messstand — deshalb war rangfolge.ts auch nie verdrahtet.
|
|
766
|
+
try {
|
|
767
|
+
const nv = await computeEmbedding(textFuerNamensVektor(topic));
|
|
768
|
+
if (nv?.length)
|
|
769
|
+
await redis.set(`${NAME_VEKTOR_PRAEFIX}${topic}`, packe(nv));
|
|
770
|
+
}
|
|
771
|
+
catch { /* siehe oben */ }
|
|
772
|
+
// Die zweite Tuer: woertliche Fehlertexte als eigene Eingaenge.
|
|
773
|
+
//
|
|
774
|
+
// Gemessen am 20.08.2026 an 499 Lektionen mit zwei unabhaengigen
|
|
775
|
+
// 100-Fragen-Saetzen: von fuenf ausprobierten Tuerarten traegt genau
|
|
776
|
+
// DIESE eine. Erstsatz und Uebersetzung brachten null, der Themenname
|
|
777
|
+
// steckt schon im Sortierer. Volle Zahlen und Gegenproben stehen in
|
|
778
|
+
// .agent/cachly/tor0-tor1-ergebnis.md.
|
|
779
|
+
//
|
|
780
|
+
// Genau wie der Vektor: nicht abgewartet, Fehler verschluckt. Eine
|
|
781
|
+
// Lektion muss auch ohne Netz gespeichert werden koennen.
|
|
782
|
+
try {
|
|
783
|
+
await schreibeEingaenge(redis, topic, gelesen, computeEmbedding);
|
|
784
|
+
}
|
|
785
|
+
catch { /* keine Eingaenge ist besser als keine Lektion */ }
|
|
786
|
+
})();
|
|
641
787
|
}
|
|
642
788
|
// Track in decision log for session replay
|
|
643
789
|
try {
|
|
@@ -711,7 +857,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
711
857
|
fix_confidence: existEdge.confidence,
|
|
712
858
|
fix_trials: existEdge.trials,
|
|
713
859
|
failure_outcome: outcome,
|
|
714
|
-
reported_by:
|
|
860
|
+
reported_by: autorFeld || 'unknown',
|
|
715
861
|
what_failed: what_failed || '',
|
|
716
862
|
resolved: false,
|
|
717
863
|
}), 'EX', 60 * 60 * 24 * 90);
|
|
@@ -779,14 +925,19 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
779
925
|
// ── Phase 3A: People + File nodes in knowledge graph ──────────────────────
|
|
780
926
|
// Builds the "who knows what" map automatically from author + file_paths.
|
|
781
927
|
// Person → authored → Concept edges power brain_who_knows queries.
|
|
782
|
-
|
|
928
|
+
//
|
|
929
|
+
// Seit dem 19.08.2026 steht hier autorFeld statt author: der Name wird
|
|
930
|
+
// aus git hergeleitet, wenn der Aufrufer keinen mitschickt. Vorher blieb
|
|
931
|
+
// dieser ganze Block bei zwei Fuenfteln aller Lektionen ungenutzt — und
|
|
932
|
+
// damit brain_who_knows leer.
|
|
933
|
+
if (autorFeld) {
|
|
783
934
|
// Agent activity registry (Move 4): track which agents are actively
|
|
784
935
|
// writing to this Brain, keyed by author, with a 1h TTL. Powers
|
|
785
936
|
// brain_conflicts' "who is live" view and multi-agent arbitration.
|
|
786
|
-
redis.set(`cachly:agents:active:${
|
|
937
|
+
redis.set(`cachly:agents:active:${autorFeld}`, JSON.stringify({ author: autorFeld, last_topic: topic, last_outcome: outcome, ts: new Date().toISOString() }), 'EX', 60 * 60).catch(() => { });
|
|
787
938
|
try {
|
|
788
939
|
const domain = topic.split(':')[0] ?? 'unknown';
|
|
789
|
-
const personId = await ckgUpsertPersonNode(redis,
|
|
940
|
+
const personId = await ckgUpsertPersonNode(redis, nurMensch(autorFeld), domain);
|
|
790
941
|
const conceptId = ckgSlug(topic);
|
|
791
942
|
await ckgUpdateEdge(redis, personId, 'authored', conceptId, outcome === 'success', outcome === 'partial');
|
|
792
943
|
for (const fp of file_paths.slice(0, 8)) {
|
|
@@ -811,8 +962,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
811
962
|
const conceptId = ckgSlug(topic);
|
|
812
963
|
// concept → affects → service (outcome decides edge confidence)
|
|
813
964
|
await ckgUpdateEdge(redis, conceptId, 'affects', serviceId, outcome === 'success', outcome === 'partial');
|
|
814
|
-
if (
|
|
815
|
-
const personId = `person:${ckgSlug(
|
|
965
|
+
if (autorFeld) {
|
|
966
|
+
const personId = `person:${ckgSlug(nurMensch(autorFeld))}`;
|
|
816
967
|
await ckgUpdateEdge(redis, personId, 'operates', serviceId, true);
|
|
817
968
|
}
|
|
818
969
|
for (const fp of file_paths.slice(0, 8)) {
|
|
@@ -826,12 +977,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
826
977
|
// auto-store as a team lesson so knowledge is shared without a separate
|
|
827
978
|
// team_learn call. Tags gain 'team'; the lesson is attributed to the author.
|
|
828
979
|
// This makes team knowledge sharing the default, not the opt-in.
|
|
829
|
-
if (
|
|
980
|
+
if (autorFeld && (outcome === 'success' || outcome === 'partial')) {
|
|
830
981
|
const teamLesson = {
|
|
831
982
|
topic, outcome, what_worked,
|
|
832
983
|
what_failed: what_failed ?? '',
|
|
833
984
|
severity,
|
|
834
|
-
author,
|
|
985
|
+
author: autorFeld,
|
|
835
986
|
file_paths,
|
|
836
987
|
commands,
|
|
837
988
|
tags: [...new Set([...tags, 'team'])],
|
|
@@ -860,14 +1011,22 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
860
1011
|
isUpdate
|
|
861
1012
|
? `♻️ Updated (recall count: ${recallCount} · audit entries: ${auditTrail.length})`
|
|
862
1013
|
: `💡 Recall later with \`recall_best_solution(topic="${topic}")\``,
|
|
863
|
-
(
|
|
864
|
-
? `👥 Auto-shared with team (by _${
|
|
1014
|
+
(autorFeld && (outcome === 'success' || outcome === 'partial'))
|
|
1015
|
+
? `👥 Auto-shared with team (by _${autorFeld}_${herkunft.quelle !== 'uebergeben' ? ` · aus ${herkunft.quelle}` : ''}) — visible in \`team_recall\``
|
|
865
1016
|
: '',
|
|
1017
|
+
autorTipp ?? '',
|
|
866
1018
|
depends_on.length > 0
|
|
867
1019
|
? `🔗 Depends on: ${depends_on.map(d => `\`${d}\``).join(', ')} → trace with \`trace_dependency\``
|
|
868
1020
|
: '',
|
|
869
1021
|
...templateWarnings,
|
|
870
1022
|
...iWasWrongWarning,
|
|
1023
|
+
// GROW-044: Die 100-Zeichen-Regel wird geprueft, nicht geglaubt.
|
|
1024
|
+
// Das Briefing der naechsten Sitzung zeigt je Lektion rund 100
|
|
1025
|
+
// Zeichen; am 16.08.2026 stand die gesuchte Adresse an Zeichen 323,
|
|
1026
|
+
// die Lektion war eingeblendet und der Fehler passierte trotzdem.
|
|
1027
|
+
// Hinweis statt Ablehnung: es gibt Lektionen ohne harte Zahl, und eine
|
|
1028
|
+
// abgelehnte Lektion ist immer schlechter als eine unscharfe.
|
|
1029
|
+
vorspannHinweis(what_worked) ?? '',
|
|
871
1030
|
].filter(l => l !== '').join('\n');
|
|
872
1031
|
}
|
|
873
1032
|
case 'recall_best_solution': {
|
|
@@ -898,9 +1057,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
898
1057
|
// A targeted lookup is a recall too, and costs the same one quota unit
|
|
899
1058
|
// as a broad smart_recall — the user asked the brain a question either way.
|
|
900
1059
|
void bumpRecallQuota(redis);
|
|
901
|
-
// Track estimated time saved (30m minor · 60m major · 240m critical)
|
|
902
|
-
|
|
903
|
-
|
|
1060
|
+
// Track estimated time saved (30m minor · 60m major · 240m critical).
|
|
1061
|
+
// Starter-Lektionen zaehlen 0 — siehe wertbeitrag.ts.
|
|
1062
|
+
const savedMins = ersparteMinuten(lesson);
|
|
1063
|
+
if (savedMins > 0) {
|
|
1064
|
+
redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => { });
|
|
1065
|
+
}
|
|
904
1066
|
const sevEmoji = lesson.severity === 'critical' ? '🔴' : lesson.severity === 'major' ? '🟡' : lesson.severity ? '🟢' : '';
|
|
905
1067
|
const auditSummary = (lesson.audit_trail ?? []).length > 1
|
|
906
1068
|
? `_Audit: ${(lesson.audit_trail ?? []).length} changes · stored ${new Date(lesson.ts).toLocaleDateString('de-DE')}_`
|
|
@@ -1006,11 +1168,179 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1006
1168
|
// ── Layer 1.5: Quality-aware rerank — proven success lessons outrank
|
|
1007
1169
|
// text-similar failed attempts (the moat; see src/rerank.ts + Cachly-Bench). ──
|
|
1008
1170
|
const kwMatches = rerankByQuality(rawMatches);
|
|
1171
|
+
// ── Layer 1.7: Bedeutungsabgleich ──────────────────────────────────────
|
|
1172
|
+
//
|
|
1173
|
+
// Der Wortabgleich findet, was woertlich dasteht. Eine Frage beschreibt
|
|
1174
|
+
// aber ein SYMPTOM, die Lektion eine URSACHE — "Der Deploy haengt beim
|
|
1175
|
+
// Bauen" und "No space left on device" teilen kein Wort und meinen
|
|
1176
|
+
// dasselbe.
|
|
1177
|
+
//
|
|
1178
|
+
// Gemessen am 19.08.2026 an 499 echten Lektionen mit 100 Fragen:
|
|
1179
|
+
//
|
|
1180
|
+
// Verfahren Platz 1 Top 3 Top 10
|
|
1181
|
+
// nur Woerter 21 % 33 % 47 %
|
|
1182
|
+
// nur Bedeutung 40 % 60 % 68 %
|
|
1183
|
+
// gemischt 30/70 39 % 55 % 71 %
|
|
1184
|
+
//
|
|
1185
|
+
// Gemischt wird, nicht ersetzt: ohne Netz gibt es keine Einbettung, und
|
|
1186
|
+
// die ersten zehn Treffer sind gemischt besser als bei beiden einzeln.
|
|
1187
|
+
//
|
|
1188
|
+
// Faellt hier etwas aus, bleibt es beim Wortabgleich. Ein Recall, der
|
|
1189
|
+
// wegen eines fremden Dienstes gar nichts liefert, waere schlimmer als
|
|
1190
|
+
// einer, der schlechter sortiert.
|
|
1191
|
+
//
|
|
1192
|
+
// Jeder Ausstieg hier meldet sich EINMAL je Prozess (siehe
|
|
1193
|
+
// `aussetzer.ts`). Am 20.08.2026 wurde gemessen, dass dieser Pfad in
|
|
1194
|
+
// Produktion monatelang stumm ausstieg: 0 Vektoren bei 506 Lektionen.
|
|
1195
|
+
// Von aussen war das von "alles in Ordnung" nicht zu unterscheiden.
|
|
1196
|
+
let sinnAngewandt = false;
|
|
1197
|
+
const kwGemischt = await (async () => {
|
|
1198
|
+
if (!hasEmbedProvider()) {
|
|
1199
|
+
meldeEinmal(OHNE_DIENST, 'kein Einbettungsdienst eingerichtet — die Suche laeuft nur ueber Woerter');
|
|
1200
|
+
return kwMatches;
|
|
1201
|
+
}
|
|
1202
|
+
try {
|
|
1203
|
+
await vektorbestand.aktualisiere(redis);
|
|
1204
|
+
if (vektorbestand.groesse === 0) {
|
|
1205
|
+
meldeEinmal(OHNE_VEKTOREN, 'keine einzige Lektion hat eine Einbettung — die Suche laeuft nur ueber '
|
|
1206
|
+
+ 'Woerter. Nachruesten mit eingaenge-nachruesten, pruefen mit brain_doctor');
|
|
1207
|
+
return kwMatches;
|
|
1208
|
+
}
|
|
1209
|
+
const frageVektor = await computeEmbedding(query);
|
|
1210
|
+
if (!frageVektor?.length) {
|
|
1211
|
+
meldeEinmal(OHNE_FRAGEVEKTOR, 'die Frage liess sich nicht einbetten — diese Suche laeuft nur ueber Woerter');
|
|
1212
|
+
return kwMatches;
|
|
1213
|
+
}
|
|
1214
|
+
await namensbestand.aktualisiere(redis);
|
|
1215
|
+
await eingangsbestand.aktualisiere(redis);
|
|
1216
|
+
await seltenheitsbestand.aktualisiere(redis);
|
|
1217
|
+
const wortThemen = kwMatches
|
|
1218
|
+
.filter((m) => m.key.startsWith('cachly:lesson:best:'))
|
|
1219
|
+
.map((m) => m.key.slice('cachly:lesson:best:'.length));
|
|
1220
|
+
// ── Die Vorauswahl ────────────────────────────────────────────────
|
|
1221
|
+
//
|
|
1222
|
+
// Woerter (Top 25) vereinigt mit Bedeutung (Top 25). Bei vorhandenen
|
|
1223
|
+
// Eingaengen zaehlt je Lektion ihr BESTER Eingang statt des
|
|
1224
|
+
// Volltextvektors — gemessen am 20.08.2026 hebt das die Findequote@3
|
|
1225
|
+
// von 52 auf 56 Prozent, und mit dem Merkmal unten auf 58.
|
|
1226
|
+
//
|
|
1227
|
+
// Der Preis steht offen im Ergebnisdokument: dieselben Eingaenge
|
|
1228
|
+
// SENKEN die Decke von 89 auf 86 Prozent. Fuer den Nutzer zaehlt die
|
|
1229
|
+
// Findequote — er sieht drei Lektionen, nicht 25.
|
|
1230
|
+
//
|
|
1231
|
+
// WICHTIG: das Maximum ueber ALLE Tueren — Volltext, Themenname UND
|
|
1232
|
+
// Fehlertext. Der erste Versuch am 20.08. hat die Volltext-Auswahl
|
|
1233
|
+
// durch die Eingangs-Auswahl ERSETZT. Das war falsch: nur 399 von 507
|
|
1234
|
+
// Lektionen tragen einen Fehlertext, die uebrigen 108 fielen damit
|
|
1235
|
+
// aus der Vorauswahl. Am echten Bestand gemessen fiel die Trefferlage
|
|
1236
|
+
// von 90 auf 70 Prozent und die Findequote@3 von 50 auf 48.
|
|
1237
|
+
const naeheBesteTuer = (t) => Math.max(vektorbestand.naehe(frageVektor, t), namensbestand.naehe(frageVektor, t), eingangsbestand.besteNaehe(frageVektor, t));
|
|
1238
|
+
const alleThemen = [...seltenheitsbestand.themen()];
|
|
1239
|
+
const sinnThemen = (alleThemen.length > 0
|
|
1240
|
+
? alleThemen
|
|
1241
|
+
: vektorbestand.aehnlichste(frageVektor, 500).map((x) => x.topic))
|
|
1242
|
+
.map((t) => ({ t, n: naeheBesteTuer(t) }))
|
|
1243
|
+
.sort((a, b) => b.n - a.n)
|
|
1244
|
+
.slice(0, 25)
|
|
1245
|
+
.map((x) => x.t);
|
|
1246
|
+
// ── Die Sortierung ───────────────────────────────────────────────
|
|
1247
|
+
//
|
|
1248
|
+
// Bis zum 20.08.2026 stand hier `mischeRangfolgen` (Reciprocal Rank
|
|
1249
|
+
// Fusion). Das war der Grund, warum die ganze Eingangs-Arbeit im
|
|
1250
|
+
// Produkt fast nichts brachte: RRF mischt ueber PLATZIERUNGEN und
|
|
1251
|
+
// kann die Naehe zum besten Eingang nicht als abgestufte Groesse
|
|
1252
|
+
// verwerten.
|
|
1253
|
+
//
|
|
1254
|
+
// frischer Pruefsatz, Findequote@3
|
|
1255
|
+
// RRF, wie ausgeliefert 50 %
|
|
1256
|
+
// RRF + Eingaenge als dritte Quelle 51 %
|
|
1257
|
+
// bewerteTopf 52 %
|
|
1258
|
+
// bewerteTopf + Eingaenge 58 %
|
|
1259
|
+
//
|
|
1260
|
+
// Faellt ein Merkmal aus (kein Namensvektor, kein Eingang), meldet es
|
|
1261
|
+
// -2 und `spreizeImTopf` macht daraus fuer alle dieselbe Null — die
|
|
1262
|
+
// Sortierung faellt dann auf die uebrigen Merkmale zurueck, statt
|
|
1263
|
+
// falsch zu werden.
|
|
1264
|
+
const topf = [...new Set([...wortThemen, ...sinnThemen])];
|
|
1265
|
+
const besteDrei = sinnThemen.slice(0, 3)
|
|
1266
|
+
.map((t) => vektorbestand.rohvektor(t)).filter(Boolean);
|
|
1267
|
+
const angereichert = besteDrei.length ? reichereAn(frageVektor, besteDrei) : frageVektor;
|
|
1268
|
+
const frageWoerter = inhaltsWoerter(query);
|
|
1269
|
+
const statistik = seltenheitsbestand.statistik;
|
|
1270
|
+
const bewertbar = topf.map((t) => ({
|
|
1271
|
+
naeheText: vektorbestand.naehe(frageVektor, t),
|
|
1272
|
+
naeheThema: namensbestand.naehe(frageVektor, t),
|
|
1273
|
+
naeheRueckkopplung: vektorbestand.naehe(angereichert, t),
|
|
1274
|
+
seltenheitsDeckung: statistik
|
|
1275
|
+
? statistik.deckung(frageWoerter, new Set([...inhaltsWoerter(seltenheitsbestand.textVon(t))].map(grobStamm)))
|
|
1276
|
+
: 0,
|
|
1277
|
+
}));
|
|
1278
|
+
let punkte = bewerteTopf(bewertbar, GEWICHTE);
|
|
1279
|
+
if (eingangsbestand.groesse > 0) {
|
|
1280
|
+
const naehen = topf.map((t) => eingangsbestand.besteNaehe(frageVektor, t));
|
|
1281
|
+
const gespreizt = spreizeImTopf(naehen);
|
|
1282
|
+
punkte = punkte.map((pkt, i) => pkt + EINGANG_GEWICHT * gespreizt[i]);
|
|
1283
|
+
}
|
|
1284
|
+
const reihenfolge = topf
|
|
1285
|
+
.map((t, i) => ({ t, p: punkte[i] }))
|
|
1286
|
+
.sort((a, b) => b.p - a.p)
|
|
1287
|
+
.map((x) => x.t);
|
|
1288
|
+
const bekannt = new Map(kwMatches.map((m) => [m.key, m]));
|
|
1289
|
+
// Lektionen, die NUR der Bedeutungsabgleich gefunden hat, muessen
|
|
1290
|
+
// nachgeladen werden — der Wortabgleich hat sie nie angefasst.
|
|
1291
|
+
const sortiert = [];
|
|
1292
|
+
for (const thema of reihenfolge) {
|
|
1293
|
+
const key = `cachly:lesson:best:${thema}`;
|
|
1294
|
+
const da = bekannt.get(key);
|
|
1295
|
+
if (da) {
|
|
1296
|
+
sortiert.push(da);
|
|
1297
|
+
bekannt.delete(key);
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
const inhalt = await redis.get(key).catch(() => null);
|
|
1301
|
+
if (inhalt) {
|
|
1302
|
+
sortiert.push({ key, content: inhalt, score: 0, matchedWords: [], finalScore: 0, qualityBoost: 1 });
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
// Alles, was kein Thema war (Kontexteintraege, Index), bleibt hinten
|
|
1306
|
+
// erhalten statt still zu verschwinden.
|
|
1307
|
+
sortiert.push(...bekannt.values());
|
|
1308
|
+
sinnAngewandt = true;
|
|
1309
|
+
return sortiert;
|
|
1310
|
+
}
|
|
1311
|
+
catch {
|
|
1312
|
+
return kwMatches;
|
|
1313
|
+
}
|
|
1314
|
+
})();
|
|
1315
|
+
void sinnAngewandt;
|
|
1009
1316
|
// One quota unit for this call, regardless of how many lessons it surfaced.
|
|
1010
1317
|
void bumpRecallQuota(redis);
|
|
1318
|
+
// ── Gedaechtniszellen ────────────────────────────────────────────────
|
|
1319
|
+
//
|
|
1320
|
+
// Diese Frage hat diese Lektionen gefunden — das ist ein bewiesener Weg
|
|
1321
|
+
// zu ihnen. Fuehrt dieselbe Frage ein ZWEITES Mal hierher, wird sie ein
|
|
1322
|
+
// eigener Eingang, und beim naechsten Mal ist der Weg breiter.
|
|
1323
|
+
//
|
|
1324
|
+
// Warum erst beim zweiten Mal: das Signal "wurde benutzt" gibt es nicht.
|
|
1325
|
+
// Der Server sieht, was er ausgeliefert hat, nicht was jemand damit
|
|
1326
|
+
// gemacht hat. Wiederholung IST das, was einen Trampelpfad ausmacht.
|
|
1327
|
+
//
|
|
1328
|
+
// Nur die ersten DREI: das ist, was die Einblendung zeigt. Wer alle
|
|
1329
|
+
// Treffer aufzeichnet, macht aus jedem Streifschuss einen Weg.
|
|
1330
|
+
//
|
|
1331
|
+
// Nicht abgewartet: wer sucht, soll nicht warten, bis das Gedaechtnis
|
|
1332
|
+
// nachgefuehrt ist.
|
|
1333
|
+
if (hasEmbedProvider()) {
|
|
1334
|
+
const gefundeneThemen = kwGemischt
|
|
1335
|
+
.filter((m) => m.key.startsWith('cachly:lesson:best:'))
|
|
1336
|
+
.slice(0, 3)
|
|
1337
|
+
.map((m) => m.key.slice('cachly:lesson:best:'.length));
|
|
1338
|
+
void spurLegen(redis, query, gefundeneThemen, computeEmbedding)
|
|
1339
|
+
.catch(() => { });
|
|
1340
|
+
}
|
|
1011
1341
|
const savedHere = [];
|
|
1012
1342
|
// Exclude archived lessons — they are kept for audit but must not surface in recall.
|
|
1013
|
-
const lessonMatches =
|
|
1343
|
+
const lessonMatches = kwGemischt.filter(m => m.key.startsWith('cachly:lesson:best:'));
|
|
1014
1344
|
let crossAuthorThisCall = 0;
|
|
1015
1345
|
let successRecallsThisCall = 0;
|
|
1016
1346
|
for (const m of lessonMatches.slice(0, 5)) {
|
|
@@ -1024,9 +1354,11 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1024
1354
|
const recalledAt = new Date().toISOString();
|
|
1025
1355
|
const updated = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1, verified_at: recalledAt, last_recalled_at: recalledAt };
|
|
1026
1356
|
redis.set(m.key, JSON.stringify(updated)).catch(() => { });
|
|
1027
|
-
|
|
1028
|
-
const savedMins =
|
|
1029
|
-
|
|
1357
|
+
// Starter-Lektionen zaehlen 0 — siehe wertbeitrag.ts.
|
|
1358
|
+
const savedMins = ersparteMinuten(lesson);
|
|
1359
|
+
if (savedMins > 0) {
|
|
1360
|
+
redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => { });
|
|
1361
|
+
}
|
|
1030
1362
|
// Metric 3 (team-knowledge-reuse): a recall of a lesson authored by
|
|
1031
1363
|
// someone OTHER than the requester is cross-author reuse — the value
|
|
1032
1364
|
// only cachly delivers. Track total proven recalls + cross-author ones.
|
|
@@ -1044,11 +1376,16 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1044
1376
|
}
|
|
1045
1377
|
if (lesson.outcome === 'success')
|
|
1046
1378
|
successRecallsThisCall++;
|
|
1047
|
-
// Surface banner for proven successes (recall_count >= 1 means it's been validated)
|
|
1048
|
-
|
|
1379
|
+
// Surface banner for proven successes (recall_count >= 1 means it's been validated).
|
|
1380
|
+
//
|
|
1381
|
+
// Der Startvorrat kommt hier NICHT ins Banner. "Brain saved you ~8h
|
|
1382
|
+
// here — docker:layer-cache" ist keine Leistung des Gedaechtnisses,
|
|
1383
|
+
// sondern mitgeliefertes Allgemeinwissen. Genau dieses Banner hat
|
|
1384
|
+
// die Zahl im Panel jahrelang glaubwuerdig aussehen lassen.
|
|
1385
|
+
if (lesson.outcome === 'success' && (lesson.recall_count ?? 0) >= 1 && !istStarterLektion(lesson)) {
|
|
1049
1386
|
savedHere.push({
|
|
1050
1387
|
topic: m.key.replace('cachly:lesson:best:', ''),
|
|
1051
|
-
severity:
|
|
1388
|
+
severity: lesson.severity ?? 'major',
|
|
1052
1389
|
recall_count: (lesson.recall_count ?? 0) + 1,
|
|
1053
1390
|
savedMins,
|
|
1054
1391
|
ts: lesson.ts,
|
|
@@ -1098,12 +1435,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1098
1435
|
// Semantic search failed silently — keyword results are enough
|
|
1099
1436
|
}
|
|
1100
1437
|
}
|
|
1101
|
-
const bm25Scores =
|
|
1438
|
+
const bm25Scores = kwGemischt.map(m => m.score);
|
|
1102
1439
|
const bm25Min = bm25Scores.length ? Math.min(...bm25Scores) : 0;
|
|
1103
1440
|
const bm25Range = bm25Scores.length ? (Math.max(...bm25Scores) - bm25Min) || 1 : 1;
|
|
1104
1441
|
const bm25Norm = (s) => (s - bm25Min) / bm25Range;
|
|
1105
1442
|
const hybridMap = new Map();
|
|
1106
|
-
for (const m of
|
|
1443
|
+
for (const m of kwGemischt) {
|
|
1107
1444
|
const n = bm25Norm(m.score);
|
|
1108
1445
|
hybridMap.set(m.key, {
|
|
1109
1446
|
key: m.key, content: m.content, bm25Score: n,
|
|
@@ -1229,12 +1566,13 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1229
1566
|
.sort((a, b) => b.hybridScore - a.hybridScore);
|
|
1230
1567
|
// ── Teaser-Gate: free tier over its recall limit hides the long tail ──────
|
|
1231
1568
|
const recallGate = await getRecallGate(instance_id, apiFetch);
|
|
1232
|
-
//
|
|
1233
|
-
// recall limit, the
|
|
1234
|
-
//
|
|
1235
|
-
//
|
|
1569
|
+
// Depth gate: once a free user is over their (monthly, goodwill-adjusted)
|
|
1570
|
+
// recall limit, the LONG TAIL is withheld — never the top hit. The rule
|
|
1571
|
+
// lives in recall-tiefe.ts with tests (GROW-043), because it used to live
|
|
1572
|
+
// only in a comment while the branch said `? 0 :` and returned nothing.
|
|
1236
1573
|
const gateActive = recallGate.reached;
|
|
1237
|
-
const
|
|
1574
|
+
const tiefe = recallTiefe({ ueberLimit: gateActive, gesamt: hybridResults.length, volleTiefe: TIEFE_VOLL });
|
|
1575
|
+
const visibleCount = tiefe.sichtbar;
|
|
1238
1576
|
// ── Build output ──────────────────────────────────────────────────────────
|
|
1239
1577
|
const lines = [`🧠 **Smart Recall** for: _"${query}"_\n`];
|
|
1240
1578
|
// Warm goodwill note — shown the month a free user is gifted +250 recalls.
|
|
@@ -1288,7 +1626,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1288
1626
|
// Group by sub-query for multi-topic queries
|
|
1289
1627
|
if (subQueries.length > 1) {
|
|
1290
1628
|
const grouped = new Map();
|
|
1291
|
-
|
|
1629
|
+
const tiefeMehrthemig = recallTiefe({ ueberLimit: gateActive, gesamt: hybridResults.length, volleTiefe: TIEFE_VOLL_MEHRTHEMIG });
|
|
1630
|
+
for (const r of hybridResults.slice(0, tiefeMehrthemig.sichtbar)) {
|
|
1292
1631
|
const sq = r.subQuery ?? query;
|
|
1293
1632
|
if (!grouped.has(sq))
|
|
1294
1633
|
grouped.set(sq, []);
|
|
@@ -1307,7 +1646,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1307
1646
|
: r.matchType === 'semantic'
|
|
1308
1647
|
? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
|
|
1309
1648
|
: `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
|
|
1310
|
-
const
|
|
1649
|
+
const vorschau = recallVorschau(r.key, r.content, 300);
|
|
1311
1650
|
lines.push(` **${label}** _(${scorePart})_`);
|
|
1312
1651
|
// Lesbares Brain (docs/produkt/team-puls.md P1): plain-language layer
|
|
1313
1652
|
// stored at lesson:human:<topic> — display-only, ranking untouched.
|
|
@@ -1318,7 +1657,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1318
1657
|
if (h.title)
|
|
1319
1658
|
lines.push(` 📖 **${h.title}** — ${h.summary ?? ''}`);
|
|
1320
1659
|
}
|
|
1321
|
-
|
|
1660
|
+
for (const z of vorschau)
|
|
1661
|
+
lines.push(` > ${z}`);
|
|
1662
|
+
lines.push('');
|
|
1322
1663
|
}
|
|
1323
1664
|
}
|
|
1324
1665
|
const matched = [...grouped.keys()];
|
|
@@ -1342,7 +1683,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1342
1683
|
: r.matchType === 'semantic'
|
|
1343
1684
|
? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
|
|
1344
1685
|
: `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
|
|
1345
|
-
const
|
|
1686
|
+
const vorschau = recallVorschau(r.key, r.content, 400);
|
|
1346
1687
|
lines.push(`**${label}**${authorBadge}${contextBadge} _(${scorePart})_`);
|
|
1347
1688
|
// Lesbares Brain (docs/produkt/team-puls.md P1): plain-language layer.
|
|
1348
1689
|
if (r.key.startsWith('cachly:lesson:best:')) {
|
|
@@ -1352,14 +1693,18 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1352
1693
|
if (h.title)
|
|
1353
1694
|
lines.push(`📖 **${h.title}** — ${h.summary ?? ''}`);
|
|
1354
1695
|
}
|
|
1355
|
-
|
|
1696
|
+
for (const z of vorschau)
|
|
1697
|
+
lines.push(`> ${z}`);
|
|
1698
|
+
lines.push('');
|
|
1356
1699
|
}
|
|
1357
1700
|
}
|
|
1358
|
-
// ──
|
|
1359
|
-
|
|
1360
|
-
|
|
1701
|
+
// ── Depth-gate footer: the top hit stayed, the tail is withheld ────────
|
|
1702
|
+
// Only speak when something was actually held back — announcing
|
|
1703
|
+
// "0 lessons withheld" would sell a wall that isn't there.
|
|
1704
|
+
if (gateActive && tiefe.zurueckgehalten > 0) {
|
|
1705
|
+
const withheld = tiefe.zurueckgehalten;
|
|
1361
1706
|
lines.push(`🔒 **Free recall limit reached** (${recallGate.used}/${recallGate.limit} this month). ` +
|
|
1362
|
-
|
|
1707
|
+
`Showing your top match; ${withheld} further relevant lesson${withheld !== 1 ? 's are' : ' is'} withheld. ` +
|
|
1363
1708
|
`Your limit resets next month, or unlock your full Brain now → ${UPGRADE_URL}\n`);
|
|
1364
1709
|
}
|
|
1365
1710
|
}
|
|
@@ -1741,7 +2086,13 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1741
2086
|
// auto-promote a lesson without any manual step.
|
|
1742
2087
|
{
|
|
1743
2088
|
const CRYSTALLIZE_RECALLS = 5;
|
|
2089
|
+
// Der Startvorrat wird nie zum Gesetz. Eine mitgelieferte Lektion sammelt
|
|
2090
|
+
// Abrufe, weil sie zu allem ein bisschen passt — nicht, weil sie sich
|
|
2091
|
+
// bewaehrt hat. Gemessen am 17.08.2026: Die fuenf obersten "Gesetze"
|
|
2092
|
+
// eines Brains mit 475 Lektionen waren alle aus dem Startvorrat, und die
|
|
2093
|
+
// echten Lektionen standen darunter bei zwei bis fuenf Abrufen.
|
|
1744
2094
|
const laws = lessons
|
|
2095
|
+
.filter(l => !istStarterLektion(l))
|
|
1745
2096
|
.filter(l => l.outcome === 'success' && (l.pinned === true || (l.recall_count ?? 0) >= CRYSTALLIZE_RECALLS))
|
|
1746
2097
|
.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0))
|
|
1747
2098
|
.slice(0, 5);
|
|
@@ -1751,7 +2102,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1751
2102
|
const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
|
|
1752
2103
|
const rc = l.recall_count ?? 0;
|
|
1753
2104
|
const why = l.pinned === true ? 'pinned' : `recalled ${rc}×`;
|
|
1754
|
-
|
|
2105
|
+
const body = lessonPreviewLines(l);
|
|
2106
|
+
lines.push(` 🏆${sev} \`${l.topic}\` _(${why})_ — ${body[0] ?? ''}`, ...body.slice(1));
|
|
1755
2107
|
}
|
|
1756
2108
|
lines.push('');
|
|
1757
2109
|
}
|
|
@@ -1841,14 +2193,18 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1841
2193
|
for (const l of focusLessons.slice(0, 4)) {
|
|
1842
2194
|
const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
1843
2195
|
const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
|
|
1844
|
-
|
|
2196
|
+
const body = lessonPreviewLines(l);
|
|
2197
|
+
lines.push(` ${emoji}${sev} \`${l.topic}\` — ${body[0] ?? ''}`, ...body.slice(1));
|
|
1845
2198
|
}
|
|
1846
2199
|
lines.push('');
|
|
1847
2200
|
}
|
|
1848
2201
|
// Top lessons — sorted by recall_count desc (most-used = most proven value).
|
|
1849
2202
|
// If focus is set, focus-matched lessons already shown above; show remaining here.
|
|
1850
2203
|
if (lessons.length > 0) {
|
|
1851
|
-
|
|
2204
|
+
// Auch hier ohne Startvorrat: "Most valuable" soll zeigen, was DIESES
|
|
2205
|
+
// Team gelernt hat, nicht was mitgeliefert wurde.
|
|
2206
|
+
const byRecall = [...lessons].filter(l => !istStarterLektion(l))
|
|
2207
|
+
.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0));
|
|
1852
2208
|
const topRecalled = byRecall.filter(l => (l.recall_count ?? 0) > 0).slice(0, 3);
|
|
1853
2209
|
const remaining = (focusLessons.length > 0 ? lessons.filter(l => !focusLessons.includes(l)) : lessons)
|
|
1854
2210
|
.filter(l => !topRecalled.includes(l))
|
|
@@ -1865,7 +2221,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1865
2221
|
const trust = (nAuthors >= 2 && rc >= 5) || rc >= 10 ? ' 🏆'
|
|
1866
2222
|
: nAuthors >= 2 || rc >= 5 ? ' ✅' : '';
|
|
1867
2223
|
const rct = rc > 1 ? ` _(${rc}× recalled)_` : '';
|
|
1868
|
-
|
|
2224
|
+
const body = lessonPreviewLines(l);
|
|
2225
|
+
lines.push(` ${emoji}${sev}${trust} \`${l.topic}\`${rct} — ${body[0] ?? ''}`, ...body.slice(1));
|
|
1869
2226
|
}
|
|
1870
2227
|
lines.push('');
|
|
1871
2228
|
}
|
|
@@ -1875,7 +2232,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1875
2232
|
for (const l of remaining) {
|
|
1876
2233
|
const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
1877
2234
|
const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
|
|
1878
|
-
|
|
2235
|
+
const body = lessonPreviewLines(l);
|
|
2236
|
+
lines.push(` ${emoji}${sev} \`${l.topic}\` — ${body[0] ?? ''}`, ...body.slice(1));
|
|
1879
2237
|
}
|
|
1880
2238
|
lines.push('');
|
|
1881
2239
|
}
|
|
@@ -2323,9 +2681,19 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
2323
2681
|
const rc = l.recall_count ?? 0;
|
|
2324
2682
|
const rcStr = rc > 0 ? ` (✓ recalled ${rc}×)` : '';
|
|
2325
2683
|
const snippet = l.what_worked.slice(0, 110);
|
|
2326
|
-
|
|
2684
|
+
// GROW-045: `author` wurde bisher nur entgegengenommen, nie benutzt —
|
|
2685
|
+
// obwohl die Werkzeug-Spezifikation "used for team lesson filtering"
|
|
2686
|
+
// verspricht. Dieselbe Regel wie in smart_recall, jetzt aus EINEM Modul.
|
|
2687
|
+
const autor = autorAbzeichen(l.author, author);
|
|
2688
|
+
summaryLines.push(`${sevIcon} ${sevLabel}\`${l.topic}\` — ${snippet}${rcStr}${autor}`);
|
|
2327
2689
|
}
|
|
2328
2690
|
summaryLines.push('');
|
|
2691
|
+
// Die Zahl, die nur ein GETEILTES Gedaechtnis liefert. Ist sie null,
|
|
2692
|
+
// wird nichts gesagt: "0 von Teamkollegen" ist keine Nachricht.
|
|
2693
|
+
const fremde = fremdanteil(selected, author);
|
|
2694
|
+
if (fremde > 0) {
|
|
2695
|
+
summaryLines.push(`👥 ${fremde} of these ${fremde === 1 ? 'was' : 'were'} written by a teammate — the value only a shared brain delivers.`, '');
|
|
2696
|
+
}
|
|
2329
2697
|
if (needsRanking) {
|
|
2330
2698
|
summaryLines.push(`💡 Showing top ${shown} of ${total} lessons · \`session_start\` for full briefing`);
|
|
2331
2699
|
}
|
|
@@ -3169,6 +3537,38 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
3169
3537
|
redis.get(`cachly:stats:time_saved_mins:${instance_id}`).catch(() => null),
|
|
3170
3538
|
redis.smembers(`cachly:stats:reuse_pairs:${instance_id}`).catch(() => []),
|
|
3171
3539
|
]);
|
|
3540
|
+
// ── GROW-046: welche Werkzeuge dieses Brain wirklich benutzt ────────────
|
|
3541
|
+
// Bewusst HIER und nicht in einem eigenen Werkzeug: ein 123. Werkzeug,
|
|
3542
|
+
// um zu messen, dass es zu viele Werkzeuge gibt, waere die Pointe, die
|
|
3543
|
+
// niemand will.
|
|
3544
|
+
// try/catch und nicht nur .catch(): faellt die Methode ganz aus (aelterer
|
|
3545
|
+
// Client, anderer Redis-Aufsatz), wirft schon der AUFRUF, und eine
|
|
3546
|
+
// Zusagen-Absicherung greift dann nicht. Eine Messzeile darf das
|
|
3547
|
+
// Werkzeug nie stoppen.
|
|
3548
|
+
let werkzeugRoh = {};
|
|
3549
|
+
try {
|
|
3550
|
+
werkzeugRoh = (await redis.hgetall(nutzungsSchluessel(instance_id))) ?? {};
|
|
3551
|
+
}
|
|
3552
|
+
catch {
|
|
3553
|
+
werkzeugRoh = {};
|
|
3554
|
+
}
|
|
3555
|
+
const nutzung = verdichte(werkzeugRoh, 10);
|
|
3556
|
+
// ── Die GEMESSENE Ersparnis, neben der gerechneten ─────────────────────
|
|
3557
|
+
// Heinrich am 19.08.2026: "Bei der Schaetzung der Tokenersparnis sollten
|
|
3558
|
+
// wir schauen, dass wir nicht nur schaetzen, denn das nervt Leute sehr
|
|
3559
|
+
// stark." Dies hier ist eine Summe, keine Rechnung: die Laenge dessen,
|
|
3560
|
+
// was das Brain wirklich zurueckgegeben hat. Dieselbe try/catch-Vorsicht
|
|
3561
|
+
// wie oben — eine Messzeile darf das Werkzeug nie stoppen.
|
|
3562
|
+
let lieferRoh = { token: 0, abrufe: 0, treffer: 0 };
|
|
3563
|
+
try {
|
|
3564
|
+
const token = await redis.get(lieferSchluessel(instance_id));
|
|
3565
|
+
const quoten = (await redis.hgetall(trefferSchluessel(instance_id))) ?? {};
|
|
3566
|
+
lieferRoh = { token: token ?? 0, abrufe: quoten.abrufe ?? 0, treffer: quoten.treffer ?? 0 };
|
|
3567
|
+
}
|
|
3568
|
+
catch {
|
|
3569
|
+
// still
|
|
3570
|
+
}
|
|
3571
|
+
const lieferung = lieferBild(lieferRoh);
|
|
3172
3572
|
// ── Metric 1: Time-to-first-recall ──────────────────────────────────────
|
|
3173
3573
|
let ttfrLine;
|
|
3174
3574
|
if (bornAt && firstRecallAt) {
|
|
@@ -3192,6 +3592,36 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
3192
3592
|
const crossAuthor = Number(crossAuthorRaw ?? 0);
|
|
3193
3593
|
const reusePct = recallsTotal > 0 ? Math.round((crossAuthor / recallsTotal) * 100) : 0;
|
|
3194
3594
|
const reuseTarget = reusePct >= 30 ? '🟢 above 30 % target' : recallsTotal === 0 ? '⚪ no recalls yet' : '🟡 below 30 % target';
|
|
3595
|
+
/*
|
|
3596
|
+
* ── Die geschaetzten Stunden stehen nicht mehr im Kundenblick ─────────
|
|
3597
|
+
*
|
|
3598
|
+
* "1.429,5 h total saved not re-researching known fixes" war die
|
|
3599
|
+
* groesste erfundene Zahl, die wir gezeigt haben. Sie ist gerechnet:
|
|
3600
|
+
* Minuten je Lektion nach Schwere, mal Abrufzahl. Der Faktor ist
|
|
3601
|
+
* geraten, und die Zahl waechst mit jedem Abruf mit — auch mit einem,
|
|
3602
|
+
* der nichts gefunden hat.
|
|
3603
|
+
*
|
|
3604
|
+
* Heinrich am 19.08.2026 zur alten Fassung: "Ich tue mich schwer mit den
|
|
3605
|
+
* 1200 h. Das haette vielleicht frueher zugetroffen, zu
|
|
3606
|
+
* Entwicklungszeiten ohne AI, aber heutzutage nicht mehr. Die erscheint
|
|
3607
|
+
* laecherlich hoch."
|
|
3608
|
+
*
|
|
3609
|
+
* Genau so ist es. Die Annahme dahinter — 30 bis 240 Minuten Recherche
|
|
3610
|
+
* je Lektion — ist der Preis von VOR den Assistenten. Heute findet ein
|
|
3611
|
+
* Modell dasselbe in Sekunden. Die Zahl misst eine Welt, die es nicht
|
|
3612
|
+
* mehr gibt.
|
|
3613
|
+
*
|
|
3614
|
+
* FREMDBELEG dazu (Artikel 4278052, METR-Studie): erfahrene Entwickler
|
|
3615
|
+
* FUEHLTEN sich mit KI schneller und waren gemessen 19 Prozent
|
|
3616
|
+
* langsamer. Selbstauskunft und Messung zeigen in entgegengesetzte
|
|
3617
|
+
* Richtungen — eine geschaetzte Zeitersparnis ist die schnellste Art,
|
|
3618
|
+
* Vertrauen fuer alle anderen Zahlen gleich mit zu verlieren.
|
|
3619
|
+
*
|
|
3620
|
+
* Sie wird nicht geloescht, sondern ZURUECKGESTUFT: sie steht weiterhin
|
|
3621
|
+
* da, aber klein, mit ihrer Annahme im selben Satz, und unter der
|
|
3622
|
+
* gemessenen Liefermenge. Eine erfundene Zahl ALLEIN nervt; eine
|
|
3623
|
+
* gemessene daneben traegt sie.
|
|
3624
|
+
*/
|
|
3195
3625
|
const timeSaved = Math.round(Number(timeSavedRaw ?? 0));
|
|
3196
3626
|
const timeSavedHuman = timeSaved < 60 ? `${timeSaved} min` : `${(timeSaved / 60).toFixed(1)} h`;
|
|
3197
3627
|
const lines = [
|
|
@@ -3209,10 +3639,24 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
3209
3639
|
reusePairs.length > 0 ? `**${reusePairs.length}** distinct reuse relationship${reusePairs.length !== 1 ? 's' : ''} across the team` : '',
|
|
3210
3640
|
``,
|
|
3211
3641
|
`---`,
|
|
3212
|
-
|
|
3642
|
+
// GEMESSEN zuerst, GESCHAETZT darunter — in dieser Reihenfolge, immer.
|
|
3643
|
+
// Vorher stand die gerechnete Stundenzahl ganz oben und fett; die
|
|
3644
|
+
// einzige Zahl ohne Annahme kam gar nicht vor.
|
|
3645
|
+
`### 📦 Aus dem Brain geliefert _(gemessen, nicht gerechnet)_`,
|
|
3646
|
+
lieferungInWorten(lieferung),
|
|
3647
|
+
``,
|
|
3648
|
+
lieferung.abrufe > 0
|
|
3649
|
+
? `_Daneben eine **Schätzung**: ${timeSavedHuman} nicht neu recherchiert — gerechnet mit 30–240 Min. je Lektion nach Schwere, nicht gemessen. Die Zahl darüber braucht diese Annahme nicht._`
|
|
3650
|
+
: `_Eine **Schätzung** von ${timeSavedHuman} „nicht neu recherchiert" wäre hier gerechnet, nicht gemessen — sie erscheint erst, wenn daneben eine gemessene Zahl steht._`,
|
|
3213
3651
|
recallsTotal === 0
|
|
3214
3652
|
? `_Tip: pass \`author="your-handle"\` to \`smart_recall\` so cross-author reuse can be tracked._`
|
|
3215
3653
|
: `_These numbers compound: every learned lesson and every teammate raises all three._`,
|
|
3654
|
+
``,
|
|
3655
|
+
`### 🔧 Werkzeug-Nutzung _(erst messen, dann zusammenlegen)_`,
|
|
3656
|
+
nutzungInWorten(nutzung, TOOLS.length),
|
|
3657
|
+
...(nutzung.spitze.length > 0
|
|
3658
|
+
? nutzung.spitze.map((z, i) => ` ${String(i + 1).padStart(2)}. \`${z.name}\` — ${z.aufrufe}× (${Math.round(z.anteil * 100)} %)`)
|
|
3659
|
+
: []),
|
|
3216
3660
|
].filter(Boolean);
|
|
3217
3661
|
return lines.join('\n');
|
|
3218
3662
|
}
|