@cachly-dev/mcp-server 0.10.127 → 0.10.129

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.
Files changed (46) hide show
  1. package/dist/src/aussetzer.d.ts +89 -0
  2. package/dist/src/aussetzer.js +143 -0
  3. package/dist/src/aussetzer.js.map +1 -1
  4. package/dist/src/bauabdruck-daten.generiert.json +3 -3
  5. package/dist/src/bedeutung.d.ts +0 -7
  6. package/dist/src/bedeutung.js +72 -3
  7. package/dist/src/bedeutung.js.map +1 -1
  8. package/dist/src/bench/cachly-bench.d.ts +15 -0
  9. package/dist/src/bench/cachly-bench.js +15 -0
  10. package/dist/src/bench/cachly-bench.js.map +1 -1
  11. package/dist/src/eingaenge.d.ts +24 -0
  12. package/dist/src/eingaenge.js +32 -0
  13. package/dist/src/eingaenge.js.map +1 -1
  14. package/dist/src/feldreparatur.d.ts +58 -0
  15. package/dist/src/feldreparatur.js +113 -0
  16. package/dist/src/feldreparatur.js.map +1 -0
  17. package/dist/src/handlers/advanced.js +3 -3
  18. package/dist/src/handlers/advanced.js.map +1 -1
  19. package/dist/src/handlers/brain.js +252 -36
  20. package/dist/src/handlers/brain.js.map +1 -1
  21. package/dist/src/handlers/team.js +60 -4
  22. package/dist/src/handlers/team.js.map +1 -1
  23. package/dist/src/interferenz.d.ts +111 -0
  24. package/dist/src/interferenz.js +141 -0
  25. package/dist/src/interferenz.js.map +1 -0
  26. package/dist/src/rangfolge.d.ts +36 -0
  27. package/dist/src/rangfolge.js +35 -0
  28. package/dist/src/rangfolge.js.map +1 -1
  29. package/dist/src/resonanz.d.ts +85 -0
  30. package/dist/src/resonanz.js +140 -0
  31. package/dist/src/resonanz.js.map +1 -0
  32. package/dist/src/speicherlage.d.ts +51 -0
  33. package/dist/src/speicherlage.js +84 -0
  34. package/dist/src/speicherlage.js.map +1 -0
  35. package/dist/src/versuch.d.ts +9 -1
  36. package/dist/src/versuch.js +17 -3
  37. package/dist/src/versuch.js.map +1 -1
  38. package/dist/src/wartung/vektoren-verdichten.d.ts +50 -0
  39. package/dist/src/wartung/vektoren-verdichten.js +200 -0
  40. package/dist/src/wartung/vektoren-verdichten.js.map +1 -0
  41. package/dist/src/wertbeitrag.d.ts +95 -4
  42. package/dist/src/wertbeitrag.js +135 -5
  43. package/dist/src/wertbeitrag.js.map +1 -1
  44. package/glama.json +1 -1
  45. package/package.json +3 -2
  46. package/server.json +2 -2
@@ -3,9 +3,9 @@ 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';
6
+ import { ersparteMinuten, istStarterLektion, zaehltAlsErsparnis } from '../wertbeitrag.js';
7
7
  import { lessonPreviewLines, trimTo } from '../lesson-preview.js';
8
- import { meldeEinmal, OHNE_VEKTOREN, OHNE_DIENST, OHNE_FRAGEVEKTOR } from '../aussetzer.js';
8
+ import { meldeEinmal, meldeUndVermerke, fehlerText, OHNE_VEKTOREN, OHNE_DIENST, OHNE_FRAGEVEKTOR, SINNPFAD_ABBRUCH, OHNE_SCHREIBVEKTOR } from '../aussetzer.js';
9
9
  import { versuchStart, neueKennung, wendeZuteilungAn, schliesseVersuchAb } from '../versuch.js';
10
10
  /**
11
11
  * Preview for one smart_recall hit.
@@ -38,9 +38,10 @@ export function recallVorschau(key, content, maxChars) {
38
38
  import { getRole, ROLE_BADGE, getScopes, lessonVisibleToScope, reviewModeEnabled, storeLessonProposal } from './team.js';
39
39
  import { keywordSearch, wortindexEntwerten, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
40
40
  import { rerankByQuality, qualityMultiplier, extractLessonQuality } from '../rerank.js';
41
- import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
41
+ import { computeEmbedding, hasEmbedProvider, EMBED_PROVIDER } from '../embeddings.js';
42
+ import { repariereFelder } from '../feldreparatur.js';
42
43
  import { VEKTOR_PRAEFIX, NAME_VEKTOR_PRAEFIX, packe, textFuerVektor, textFuerNamensVektor, Vektorbestand, } from '../bedeutung.js';
43
- import { schreibeEingaenge, Eingangsbestand, EINGANG_GEWICHT } from '../eingaenge.js';
44
+ import { schreibeEingaenge, Eingangsbestand } from '../eingaenge.js';
44
45
  import { Seltenheitsbestand } from '../seltenheitsbestand.js';
45
46
  import { spurLegen } from '../spuren.js';
46
47
  import { bewerteTopf, spreizeImTopf, reichereAn, inhaltsWoerter, grobStamm, GEWICHTE, } from '../rangfolge.js';
@@ -54,10 +55,127 @@ import { bewerteTopf, spreizeImTopf, reichereAn, inhaltsWoerter, grobStamm, GEWI
54
55
  const vektorbestand = new Vektorbestand();
55
56
  /** Dieselbe Mechanik, andere Sicht: die Vektoren der Themennamen. */
56
57
  const namensbestand = new Vektorbestand(60_000, NAME_VEKTOR_PRAEFIX);
57
- /** Die Fehlertext-Eingaenge, je Lektion ihr bester. */
58
+ /** Die Fehlertext-Eingaenge, je Lektion ihr bester. Sortiert mit, nominiert nicht. */
58
59
  const eingangsbestand = new Eingangsbestand();
59
60
  /** Die Wortstatistik des Bestands — stehend, nicht je Frage neu. */
60
61
  const seltenheitsbestand = new Seltenheitsbestand();
62
+ /**
63
+ * Wie viele Lektionen die Bedeutungs-Tuer in den Topf nominiert.
64
+ *
65
+ * Stand bis zum 21.08.2026 auf 25, ohne dass die Zahl je gemessen worden
66
+ * waere. Sie war der Grund fuer die "Decke", die am 20.08. fuer eine
67
+ * Naturkonstante gehalten wurde ("kein Sortierer kommt ueber 84/90 Prozent").
68
+ * Gemessen am eingefrorenen Korpus (src/bench/topfgroesse.ts):
69
+ *
70
+ * POOL Platz 1 @3 Top 10 Topf ms/Frage
71
+ * 25 39,0 51,0 71,0 90,0 57
72
+ * 50 38,0 52,0 71,0 95,0 50
73
+ * 75 38,0 53,0 71,0 97,0 52
74
+ * 100 38,0 52,0 70,0 99,0 54
75
+ * 150 37,0 53,0 70,0 99,0 57
76
+ *
77
+ * 75 ist der Punkt, an dem die Findequote@3 ihr Maximum hat (53 statt 51) und
78
+ * die Decke von 90 auf 97 Prozent steigt. Der Preis ist EIN Punkt auf Platz 1.
79
+ * Fuer den Nutzer zaehlt @3: er sieht drei Lektionen, nicht eine.
80
+ *
81
+ * Warum die Decke wichtiger ist als die Kopfzahl: was nicht im Topf ist, kann
82
+ * keine spaetere Stufe zurueckholen. Jede kuenftige Verbesserung der
83
+ * Sortierung hat damit 59 Punkte Spielraum (97 minus 38) statt 51.
84
+ *
85
+ * Die Rechenzeit bleibt im Rauschen: die Vorauswahl rechnet ohnehin ueber ALLE
86
+ * Lektionen: nur die Sortierung bekommt mehr Kandidaten, und die kostet je
87
+ * Kandidat vier Zahlen.
88
+ */
89
+ const SINN_TOPF = 75;
90
+ /**
91
+ * Ab welcher Naehe eine Fehlertext-Tuer als Signal zaehlt.
92
+ *
93
+ * ── Die Geschichte, weil sie die Zahl erklaert ──────────────────────────────
94
+ *
95
+ * Am 21.08.2026 wurden die Tueren ausgebaut: sie kosteten zwei Punkte auf
96
+ * Platz 1 und vier auf die Topfdeckung. Ein paar Stunden spaeter fiel bei der
97
+ * Resonanz-Messung der wahre Grund auf, und er lag nicht an der Idee.
98
+ *
99
+ * `spreizeImTopf` gibt einem fehlenden Wert (-2) eine NULL — den schlechtesten
100
+ * Wert im Bereich [0,1]. Wer das Merkmal hat, kann also nur gewinnen; wer es
101
+ * nicht hat, nur verlieren. Bei den Tueren betraf das 108 von 507 Lektionen
102
+ * ohne Fehlertext: sie wurden systematisch nach unten gedrueckt, unabhaengig
103
+ * davon, ob die Tueren der anderen ueberhaupt passten.
104
+ *
105
+ * Mit Schwelle zaehlt eine Tuer nur, wenn sie WIRKLICH nahe ist. Darunter
106
+ * meldet sie -2 und ist damit "kein Wert" statt "schlechter Wert" — und der
107
+ * Vergleich laeuft nur noch zwischen Lektionen, bei denen das Merkmal etwas
108
+ * aussagt.
109
+ *
110
+ * ── Warum 0,5 und nicht der beste Wert ──────────────────────────────────────
111
+ *
112
+ * Gemessen ueber alle 100 Fragen (src/bench/schwelle-abtasten.ts):
113
+ *
114
+ * Schwelle 0,35 0,40 0,45 0,50 0,55 0,60 0,70
115
+ * Platz 1 39,0 38,0 40,0 40,0 41,0 39,0 38,0
116
+ * @3 53,0 54,0 53,0 55,0 55,0 55,0 53,0
117
+ * (ohne Merkmal: 38,0 / 53,0)
118
+ *
119
+ * 0,55 waere der hoechste Punkt. Genommen wird 0,50, weil der Gewinn dort
120
+ * MITTIG in einem breiten Band liegt (0,45 bis 0,60 gewinnen alle). Ein Wert
121
+ * am Rand des Bandes ist eine Eigenschaft des Pruefsatzes — genau daran ist am
122
+ * 19.08. die alte Rangfolge-Formel gescheitert.
123
+ *
124
+ * Kreuzweise geprueft (auf einer Haelfte gesucht, auf der anderen gemessen):
125
+ * Platz 1 41,0 gegen 38,0 Prozent. Der Gewinn haelt, wenn er nie dieselben
126
+ * Fragen sieht, an denen er gefunden wurde.
127
+ *
128
+ * Die Tueren nominieren weiter NICHT — dort haben sie die richtige Antwort
129
+ * fuenfmal ganz aus dem Topf gedraengt und einmal hineingeholt.
130
+ */
131
+ const EINGANG_SCHWELLE = 0.5;
132
+ /**
133
+ * Wie stark die Tuer mitsortiert.
134
+ *
135
+ * NICHT EINGANG_GEWICHT (0,5) aus eingaenge.js: der Wert wurde fuer die alte
136
+ * Bauform OHNE Schwelle bemessen. Mit Schwelle ist er zu hoch — gemessen ueber
137
+ * alle 100 Fragen (src/bench/schwelle-abtasten.ts, Schwelle 0,5):
138
+ *
139
+ * Gewicht 0,10 0,15 0,20 0,30 0,40 0,50
140
+ * Platz 1 39,0 41,0 40,0 40,0 41,0 41,0
141
+ * @3 55,0 55,0 55,0 55,0 54,0 52,0
142
+ *
143
+ * Bei 0,5 bricht die Findequote@3 auf 52 ein — das Merkmal draengt dann
144
+ * Treffer von Platz 2 und 3 weg, um einen auf Platz 1 zu heben. Fuer den
145
+ * Nutzer zaehlt @3: er sieht drei Lektionen.
146
+ *
147
+ * 0,2 liegt mittig im Band, in dem BEIDE Kennzahlen ihr Maximum halten.
148
+ */
149
+ const EINGANG_SORTIER_GEWICHT = 0.2;
150
+ /*
151
+ * ── Ein ZWEITES Tuer-Merkmal wurde gemessen und NICHT eingebaut ─────────────
152
+ *
153
+ * Damit es niemand in vier Wochen neu baut. Der Kandidat: der MITTELWERT ueber
154
+ * alle Tueren einer Lektion, als sechstes Merkmal NEBEN dem Maximum oben
155
+ * (Eingangsbestand.mittelNaehe, samt Begruendung, warum die beiden
156
+ * entgegengesetzt ziehen). Abgetastet in src/bench/mittel-gewicht-abtasten.ts.
157
+ *
158
+ * Er haengt vollstaendig davon ab, WELCHE Tueren im Speicher liegen:
159
+ *
160
+ * Bestand Tueren Abdeckung @3 ohne -> mit
161
+ * echter Speicher (Fehlertexte) 965 78,4 % 55,0 -> 51,0
162
+ * volle Impfung (Fragewolken) 2462 100,0 % 58,0 -> 63,0
163
+ * dasselbe, unabhaengiger Fragensatz 2462 100,0 % 63,0 -> 67,0
164
+ *
165
+ * Auf dem heutigen Speicher SCHADET er also, auf dem geimpften nuetzt er. Die
166
+ * naheliegende Erklaerung — Lektionen ohne Tuer bekommen ueber spreizeImTopf
167
+ * eine Null, also den schlechtesten Platz — wurde geprueft und ist NICHT der
168
+ * Grund: mit mittiger statt schlechtester Einstufung faellt @3 genauso
169
+ * (55,0 -> 52,0, dabei 14 Fragen besser und 30 schlechter).
170
+ *
171
+ * Ehrlich bleibt eine Unschaerfe: die beiden Bestaende unterscheiden sich in
172
+ * ZWEI Dingen zugleich — Abdeckung (78 gegen 100 Prozent) UND Art der Tuer
173
+ * (Fehlertext gegen handgeschriebene Fragewolke). Welches davon den Ausschlag
174
+ * gibt, ist mit diesen Daten nicht zu trennen.
175
+ *
176
+ * Bedingung fuer einen zweiten Anlauf: die Fragewolken liegen im echten
177
+ * Speicher. Dann neu abtasten — nicht die 0,3 von damals uebernehmen.
178
+ */
61
179
  import { upgradeNudge } from '../upgrade-nudge.js';
62
180
  import { recallTiefe, TIEFE_VOLL, TIEFE_VOLL_MEHRTHEMIG } from '../recall-tiefe.js';
63
181
  import { nutzungInWorten, nutzungsSchluessel, verdichte } from '../werkzeug-nutzung.js';
@@ -539,7 +657,29 @@ async function getRecallGate(instanceId, apiFetch) {
539
657
  export async function handleBrainTool(name, args, getConnection, apiFetch) {
540
658
  switch (name) {
541
659
  case 'learn_from_attempts': {
542
- 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;
660
+ const { instance_id, topic, outcome, what_worked: rohWorked, what_failed: rohFailed = '', context: rohCtx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', visibility = 'team', service = '', service_kind = 'service', group = '', } = args;
661
+ /*
662
+ * Verklebte Felder trennen, BEVOR irgendetwas geschrieben wird.
663
+ *
664
+ * Wer ein Feld mit dem falschen Tag schliesst, uebergibt den ganzen
665
+ * Rest — samt der Marke des naechsten Feldes — als what_worked. Das
666
+ * Ergebnis sieht aus wie eine volle Lektion, hat aber ein leeres
667
+ * what_failed und Steuerzeichen im Fliesstext.
668
+ *
669
+ * Belegt am 22.08.2026 an derselben Lektion zweimal hintereinander,
670
+ * beide Male unbemerkt: die Rueckmeldung zeigte den kaputten Text an
671
+ * und sah trotzdem aus wie ein Erfolg.
672
+ *
673
+ * ohneMarkierungen() in eingaenge.ts raeumt dasselbe beim EINBETTEN
674
+ * weg. Das rettet den Vektor, aber nicht den Bestand — im Speicher
675
+ * bleibt der Text falsch und what_failed leer. Deshalb hier, an der
676
+ * einzigen Stelle, an der der Fehler entsteht.
677
+ */
678
+ const { what_worked, what_failed = '', context: ctx = '' } = repariereFelder({
679
+ what_worked: rohWorked,
680
+ what_failed: rohFailed,
681
+ context: rohCtx,
682
+ });
543
683
  const redis = await getConnection(instance_id);
544
684
  const ts = new Date().toISOString();
545
685
  /*
@@ -756,7 +896,14 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
756
896
  if (v?.length)
757
897
  await redis.set(`${VEKTOR_PRAEFIX}${topic}`, packe(v));
758
898
  }
759
- catch { /* kein Vektor ist besser als keine Lektion */ }
899
+ catch (e) {
900
+ // Kein Vektor ist besser als keine Lektion — aber der GRUND darf
901
+ // nicht verlorengehen. Vor dem 22.08.2026 stand hier ein leeres
902
+ // catch, und drei Kundeninstanzen hatten Lektionen ohne Vektoren,
903
+ // ohne dass irgendwo stand, warum (siehe OHNE_SCHREIBVEKTOR).
904
+ meldeEinmal(OHNE_SCHREIBVEKTOR, `Volltext-Vektor nicht geschrieben (${EMBED_PROVIDER}): ${fehlerText(e)}`
905
+ + ' — die Lektion ist gespeichert, der Bedeutungsabgleich uebergeht sie.');
906
+ }
760
907
  // Der Themenname als eigene Sicht.
761
908
  //
762
909
  // Er ist kurz und frageaehnlich; eine 60-Zeichen-Frage gegen 60
@@ -769,7 +916,10 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
769
916
  if (nv?.length)
770
917
  await redis.set(`${NAME_VEKTOR_PRAEFIX}${topic}`, packe(nv));
771
918
  }
772
- catch { /* siehe oben */ }
919
+ catch (e) {
920
+ meldeEinmal(OHNE_SCHREIBVEKTOR, `Namens-Vektor nicht geschrieben (${EMBED_PROVIDER}): ${fehlerText(e)}`
921
+ + ' — siehe oben, die Lektion selbst ist gespeichert.');
922
+ }
773
923
  // Die zweite Tuer: woertliche Fehlertexte als eigene Eingaenge.
774
924
  //
775
925
  // Gemessen am 20.08.2026 an 499 Lektionen mit zwei unabhaengigen
@@ -783,7 +933,10 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
783
933
  try {
784
934
  await schreibeEingaenge(redis, topic, gelesen, computeEmbedding);
785
935
  }
786
- catch { /* keine Eingaenge ist besser als keine Lektion */ }
936
+ catch (e) {
937
+ meldeEinmal(OHNE_SCHREIBVEKTOR, `Eingaenge nicht geschrieben (${EMBED_PROVIDER}): ${fehlerText(e)}`
938
+ + ' — siehe oben, die Lektion selbst ist gespeichert.');
939
+ }
787
940
  })();
788
941
  }
789
942
  // Track in decision log for session replay
@@ -1059,8 +1212,10 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1059
1212
  // as a broad smart_recall — the user asked the brain a question either way.
1060
1213
  void bumpRecallQuota(redis);
1061
1214
  // Track estimated time saved (30m minor · 60m major · 240m critical).
1062
- // Starter-Lektionen zaehlen 0 — siehe wertbeitrag.ts.
1063
- const savedMins = ersparteMinuten(lesson);
1215
+ // Nur der ERSTE Abruf zaehlt, und der Startvorrat gar nicht — die
1216
+ // Begruendung fuer beides steht in wertbeitrag.ts. `lesson` ist hier
1217
+ // noch der Stand VOR dieser Erhoehung, das ist Absicht.
1218
+ const savedMins = zaehltAlsErsparnis(lesson) ? ersparteMinuten(lesson) : 0;
1064
1219
  if (savedMins > 0) {
1065
1220
  redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => { });
1066
1221
  }
@@ -1220,29 +1375,41 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1220
1375
  .map((m) => m.key.slice('cachly:lesson:best:'.length));
1221
1376
  // ── Die Vorauswahl ────────────────────────────────────────────────
1222
1377
  //
1223
- // Woerter (Top 25) vereinigt mit Bedeutung (Top 25). Bei vorhandenen
1224
- // Eingaengen zaehlt je Lektion ihr BESTER Eingang statt des
1225
- // Volltextvektors — gemessen am 20.08.2026 hebt das die Findequote@3
1226
- // von 52 auf 56 Prozent, und mit dem Merkmal unten auf 58.
1378
+ // Woerter (Top 25) vereinigt mit Bedeutung (Top 25). Zwei Tueren je
1379
+ // Lektion: Volltext und Themenname.
1227
1380
  //
1228
- // Der Preis steht offen im Ergebnisdokument: dieselben Eingaenge
1229
- // SENKEN die Decke von 89 auf 86 Prozent. Fuer den Nutzer zaehlt die
1230
- // Findequote — er sieht drei Lektionen, nicht 25.
1381
+ // Die DRITTE Tuer — woertliche Fehlertexte als eigene Eingaenge —
1382
+ // stand hier vom 20.08. bis zum 21.08.2026 und ist AUSGEBAUT.
1383
+ // Die Begruendung, mit der sie kam ("hebt die Findequote@3 von 52
1384
+ // auf 56/58"), reproduzierte auf dem eingefrorenen 100-Fragen-Satz
1385
+ // nicht mehr. Dort, je Frage nachgemessen (tueren-vergleich.ts):
1231
1386
  //
1232
- // WICHTIG: das Maximum ueber ALLE Tueren — Volltext, Themenname UND
1233
- // Fehlertext. Der erste Versuch am 20.08. hat die Volltext-Auswahl
1234
- // durch die Eingangs-Auswahl ERSETZT. Das war falsch: nur 399 von 507
1235
- // Lektionen tragen einen Fehlertext, die uebrigen 108 fielen damit
1236
- // aus der Vorauswahl. Am echten Bestand gemessen fiel die Trefferlage
1237
- // von 90 auf 70 Prozent und die Findequote@3 von 50 auf 48.
1238
- const naeheBesteTuer = (t) => Math.max(vektorbestand.naehe(frageVektor, t), namensbestand.naehe(frageVektor, t), eingangsbestand.besteNaehe(frageVektor, t));
1387
+ // Bauform Platz 1 @3 Top 10 im Topf
1388
+ // mit Tueren 37,0 52,0 70,0 86,0
1389
+ // ohne Tueren 39,0 51,0 71,0 90,0
1390
+ // nur sortieren 35,0 50,0 71,0 90,0
1391
+ //
1392
+ // Der eigentliche Schaden sass in der Vorauswahl: die Tueren
1393
+ // draengten die richtige Antwort FUENFMAL ganz aus dem Topf und
1394
+ // retteten sie EINMAL. Ein schlechter Platz ist ein
1395
+ // Schoenheitsfehler — ein Rauswurf aus dem Topf ist ein
1396
+ // Totalausfall, den keine spaetere Stufe heilen kann. Und die
1397
+ // Decke des Topfes ist die harte Grenze jeder kuenftigen besseren
1398
+ // Sortierung: 90 statt 86 Prozent ist der wertvollere Rohstoff.
1399
+ //
1400
+ // Die Eingaenge werden weiter GESCHRIEBEN (siehe schreibeEingaenge
1401
+ // weiter oben): die Daten kosten einen Einbettungsaufruf je Lektion
1402
+ // und sind der natuerliche Speicherplatz fuer den naechsten Anlauf —
1403
+ // gemessene Frage-Vektoren echter Treffer statt geratener
1404
+ // Fehlertexte.
1405
+ const naeheBesteTuer = (t) => Math.max(vektorbestand.naehe(frageVektor, t), namensbestand.naehe(frageVektor, t));
1239
1406
  const alleThemen = [...seltenheitsbestand.themen()];
1240
1407
  const sinnThemen = (alleThemen.length > 0
1241
1408
  ? alleThemen
1242
1409
  : vektorbestand.aehnlichste(frageVektor, 500).map((x) => x.topic))
1243
1410
  .map((t) => ({ t, n: naeheBesteTuer(t) }))
1244
1411
  .sort((a, b) => b.n - a.n)
1245
- .slice(0, 25)
1412
+ .slice(0, SINN_TOPF)
1246
1413
  .map((x) => x.t);
1247
1414
  // ── Die Sortierung ───────────────────────────────────────────────
1248
1415
  //
@@ -1258,10 +1425,19 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1258
1425
  // bewerteTopf 52 %
1259
1426
  // bewerteTopf + Eingaenge 58 %
1260
1427
  //
1261
- // Faellt ein Merkmal aus (kein Namensvektor, kein Eingang), meldet es
1262
- // -2 und `spreizeImTopf` macht daraus fuer alle dieselbe Null — die
1263
- // Sortierung faellt dann auf die uebrigen Merkmale zurueck, statt
1264
- // falsch zu werden.
1428
+ // KORREKTUR 21.08.2026: die 58 % reproduzieren auf dem EINGEFRORENEN
1429
+ // 100-Fragen-Satz nicht (dieselben Fragen!): dort bringt die
1430
+ // Eingangs-Sortierung 52 statt 51 — ein Punkt, nicht sechs — und
1431
+ // kostet zwei Punkte auf Platz 1. Die Eingaenge sind deshalb aus
1432
+ // Vorauswahl UND Sortierung ausgebaut; Messung und alle drei
1433
+ // Bauformen in src/bench/tueren-vergleich.ts. Die Tabelle oben
1434
+ // bleibt als Beleg, wie eine Zahl aussieht, die eine Pipeline
1435
+ // beschreibt, die es nicht mehr gibt.
1436
+ //
1437
+ // Faellt ein Merkmal aus (kein Namensvektor), meldet es -2 und
1438
+ // `spreizeImTopf` (in bewerteTopf) macht daraus fuer alle dieselbe
1439
+ // Null — die Sortierung faellt dann auf die uebrigen Merkmale
1440
+ // zurueck, statt falsch zu werden.
1265
1441
  const topf = [...new Set([...wortThemen, ...sinnThemen])];
1266
1442
  const besteDrei = sinnThemen.slice(0, 3)
1267
1443
  .map((t) => vektorbestand.rohvektor(t)).filter(Boolean);
@@ -1277,10 +1453,21 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1277
1453
  : 0,
1278
1454
  }));
1279
1455
  let punkte = bewerteTopf(bewertbar, GEWICHTE);
1456
+ // Die Fehlertext-Tuer als fuenftes Merkmal — mit Schwelle.
1457
+ //
1458
+ // Sie NOMINIERT nicht (siehe Vorauswahl oben), sie sortiert nur mit.
1459
+ // Und sie zaehlt nur, wo sie wirklich nahe ist: unterhalb von
1460
+ // EINGANG_SCHWELLE meldet sie -2, und spreizeImTopf behandelt das
1461
+ // als "kein Wert" statt als "schlechter Wert". Ohne diese Schwelle
1462
+ // bekamen die 108 Lektionen ohne Fehlertext systematisch Abzug —
1463
+ // das war der ganze gemessene Schaden, nicht die Idee.
1280
1464
  if (eingangsbestand.groesse > 0) {
1281
- const naehen = topf.map((t) => eingangsbestand.besteNaehe(frageVektor, t));
1465
+ const naehen = topf.map((t) => {
1466
+ const n = eingangsbestand.besteNaehe(frageVektor, t);
1467
+ return n >= EINGANG_SCHWELLE ? n : -2;
1468
+ });
1282
1469
  const gespreizt = spreizeImTopf(naehen);
1283
- punkte = punkte.map((pkt, i) => pkt + EINGANG_GEWICHT * gespreizt[i]);
1470
+ punkte = punkte.map((pkt, i) => pkt + EINGANG_SORTIER_GEWICHT * gespreizt[i]);
1284
1471
  }
1285
1472
  const reihenfolge = topf
1286
1473
  .map((t, i) => ({ t, p: punkte[i] }))
@@ -1309,7 +1496,23 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1309
1496
  sinnAngewandt = true;
1310
1497
  return sortiert;
1311
1498
  }
1312
- catch {
1499
+ catch (e) {
1500
+ // Der Auffang. Bis zum 21.08.2026 stand hier `catch { return
1501
+ // kwMatches; }` — ohne Meldung, ohne Zaehler.
1502
+ //
1503
+ // Gefunden vom ersten Lauf der Gegenrede (Rolle: Der Wettbewerber),
1504
+ // aus einer fremden Narbe: ein Abruf hinter einem Zeitlimit gegen
1505
+ // einen Einbettungsdienst, der bei Inaktivitaet kalt startete. Der
1506
+ // erste Aufruf nach einer Pause riss das Limit, wurde hier gefangen
1507
+ // und lieferte dieselbe Antwort wie eine Suche ohne Treffer. Rund
1508
+ // 40 Prozent der Rueckrufe in der ersten Stunde nach jedem
1509
+ // Kaltstart waren stille Zeitueberlaeufe — bemerkt Wochen spaeter.
1510
+ //
1511
+ // Die drei Ausstiege darueber melden sich seit PR #424. Dieser ist der
1512
+ // gefaehrlichste von allen, weil er das UNERWARTETE faengt: die drei
1513
+ // anderen sind Luecken, das hier ist ein Fehler.
1514
+ await meldeUndVermerke(redis, SINNPFAD_ABBRUCH, `der Bedeutungspfad brach ab (${e instanceof Error ? e.message : String(e)}) `
1515
+ + '— diese Suche lief nur ueber Woerter');
1313
1516
  return kwMatches;
1314
1517
  }
1315
1518
  })();
@@ -1355,8 +1558,10 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1355
1558
  const recalledAt = new Date().toISOString();
1356
1559
  const updated = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1, verified_at: recalledAt, last_recalled_at: recalledAt };
1357
1560
  redis.set(m.key, JSON.stringify(updated)).catch(() => { });
1358
- // Starter-Lektionen zaehlen 0 — siehe wertbeitrag.ts.
1359
- const savedMins = ersparteMinuten(lesson);
1561
+ // Nur der ERSTE Abruf zaehlt — siehe wertbeitrag.ts. Diese
1562
+ // Schleife lief ueber bis zu FUENF Lektionen je Aufruf und schrieb
1563
+ // jeder die volle Recherchezeit gut, bei jedem Aufruf erneut.
1564
+ const savedMins = zaehltAlsErsparnis(lesson) ? ersparteMinuten(lesson) : 0;
1360
1565
  if (savedMins > 0) {
1361
1566
  redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => { });
1362
1567
  }
@@ -1889,11 +2094,22 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1889
2094
  if (streakMessage)
1890
2095
  lines.push(streakMessage, '');
1891
2096
  // Time saved (only show when meaningful — 30+ minutes)
2097
+ //
2098
+ // Die Zahl nennt ihre Grundlage. Bis zum 22.08.2026 stand hier nur
2099
+ // "~1664h total" — eine Zahl, die niemand nachrechnen konnte und die auf
2100
+ // diesem 82 Tage alten Brain mehr Stunden behauptete, als eine Person in
2101
+ // dieser Zeit ueberhaupt haette arbeiten koennen. Jetzt steht daneben,
2102
+ // WORAUS sie entsteht: so viele eigene Lektionen, jede einmal gezaehlt.
2103
+ // Wer will, kann sie abzaehlen.
1892
2104
  if (timeSavedMins >= 30) {
1893
2105
  const h = Math.floor(timeSavedMins / 60);
1894
2106
  const m = Math.round(timeSavedMins % 60);
1895
2107
  const timeStr = h > 0 ? `${h}h${m > 0 ? ` ${m}m` : ''}` : `${m}m`;
1896
- lines.push(`⏱️ **Brain saved you ~${timeStr} total** (time not re-researching known fixes)`, '');
2108
+ const zaehlende = lessons.filter((l) => !istStarterLektion(l) && (l.recall_count ?? 0) > 0).length;
2109
+ const grundlage = zaehlende > 0
2110
+ ? ` — ${zaehlende} of your own lessons that already helped, counted once each`
2111
+ : ' (time not re-researching known fixes)';
2112
+ lines.push(`⏱️ **Brain saved you ~${timeStr} total**${grundlage}`, '');
1897
2113
  }
1898
2114
  // Upgrade nudge — surfaces the free-tier quota BEFORE the wall, once
1899
2115
  // per session and right next to the value it just proved.