@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.
Files changed (85) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +7 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/src/aussetzer.d.ts +67 -0
  5. package/dist/src/aussetzer.js +82 -0
  6. package/dist/src/aussetzer.js.map +1 -0
  7. package/dist/src/autor-abzeichen.d.ts +41 -0
  8. package/dist/src/autor-abzeichen.js +56 -0
  9. package/dist/src/autor-abzeichen.js.map +1 -0
  10. package/dist/src/autor.d.ts +101 -0
  11. package/dist/src/autor.js +136 -0
  12. package/dist/src/autor.js.map +1 -0
  13. package/dist/src/bauabdruck-daten.generiert.json +5 -0
  14. package/dist/src/bauabdruck.d.ts +38 -0
  15. package/dist/src/bauabdruck.js +93 -0
  16. package/dist/src/bauabdruck.js.map +1 -0
  17. package/dist/src/bedeutung.d.ts +175 -0
  18. package/dist/src/bedeutung.js +254 -0
  19. package/dist/src/bedeutung.js.map +1 -0
  20. package/dist/src/brain-export.d.ts +24 -0
  21. package/dist/src/brain-export.js +31 -1
  22. package/dist/src/brain-export.js.map +1 -1
  23. package/dist/src/deutsch.d.ts +98 -0
  24. package/dist/src/deutsch.js +223 -0
  25. package/dist/src/deutsch.js.map +1 -0
  26. package/dist/src/eingaenge-nachruesten.d.ts +41 -0
  27. package/dist/src/eingaenge-nachruesten.js +290 -0
  28. package/dist/src/eingaenge-nachruesten.js.map +1 -0
  29. package/dist/src/eingaenge.d.ts +135 -0
  30. package/dist/src/eingaenge.js +228 -0
  31. package/dist/src/eingaenge.js.map +1 -0
  32. package/dist/src/geliefert.d.ts +63 -0
  33. package/dist/src/geliefert.js +114 -0
  34. package/dist/src/geliefert.js.map +1 -0
  35. package/dist/src/handlers/advanced.js +7 -3
  36. package/dist/src/handlers/advanced.js.map +1 -1
  37. package/dist/src/handlers/brain.d.ts +19 -0
  38. package/dist/src/handlers/brain.js +492 -48
  39. package/dist/src/handlers/brain.js.map +1 -1
  40. package/dist/src/handlers/instances.d.ts +24 -0
  41. package/dist/src/handlers/instances.js +49 -1
  42. package/dist/src/handlers/instances.js.map +1 -1
  43. package/dist/src/handlers/team.js +105 -0
  44. package/dist/src/handlers/team.js.map +1 -1
  45. package/dist/src/index.d.ts +20 -0
  46. package/dist/src/index.js +282 -36
  47. package/dist/src/index.js.map +1 -1
  48. package/dist/src/lesson-preview.d.ts +81 -0
  49. package/dist/src/lesson-preview.js +133 -0
  50. package/dist/src/lesson-preview.js.map +1 -0
  51. package/dist/src/rangfolge.d.ts +169 -0
  52. package/dist/src/rangfolge.js +214 -0
  53. package/dist/src/rangfolge.js.map +1 -0
  54. package/dist/src/raumkorrektur.d.ts +100 -0
  55. package/dist/src/raumkorrektur.js +197 -0
  56. package/dist/src/raumkorrektur.js.map +1 -0
  57. package/dist/src/recall-tiefe.d.ts +48 -0
  58. package/dist/src/recall-tiefe.js +42 -0
  59. package/dist/src/recall-tiefe.js.map +1 -0
  60. package/dist/src/rerank.d.ts +0 -14
  61. package/dist/src/rerank.js +39 -2
  62. package/dist/src/rerank.js.map +1 -1
  63. package/dist/src/search.d.ts +56 -17
  64. package/dist/src/search.js +335 -73
  65. package/dist/src/search.js.map +1 -1
  66. package/dist/src/seltenheitsbestand.d.ts +53 -0
  67. package/dist/src/seltenheitsbestand.js +92 -0
  68. package/dist/src/seltenheitsbestand.js.map +1 -0
  69. package/dist/src/spuren.d.ts +97 -0
  70. package/dist/src/spuren.js +150 -0
  71. package/dist/src/spuren.js.map +1 -0
  72. package/dist/src/tools.js +1 -1
  73. package/dist/src/tools.js.map +1 -1
  74. package/dist/src/vorspann.d.ts +73 -0
  75. package/dist/src/vorspann.js +146 -0
  76. package/dist/src/vorspann.js.map +1 -0
  77. package/dist/src/werkzeug-nutzung.d.ts +62 -0
  78. package/dist/src/werkzeug-nutzung.js +72 -0
  79. package/dist/src/werkzeug-nutzung.js.map +1 -0
  80. package/dist/src/wertbeitrag.d.ts +57 -0
  81. package/dist/src/wertbeitrag.js +73 -0
  82. package/dist/src/wertbeitrag.js.map +1 -0
  83. package/glama.json +1 -1
  84. package/package.json +12 -4
  85. 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 hits — the magic moment is sacred — but withholds the long tail
411
- // behind an upgrade teaser ("🔒 N more relevant lessons available"). This is a
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
- ...(author ? { author } : {}),
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
- ...(author ? { author } : {}),
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: author || 'unknown',
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
- if (author) {
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:${author}`, JSON.stringify({ author, last_topic: topic, last_outcome: outcome, ts: new Date().toISOString() }), 'EX', 60 * 60).catch(() => { });
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, author, domain);
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 (author) {
815
- const personId = `person:${ckgSlug(author)}`;
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 (author && (outcome === 'success' || outcome === 'partial')) {
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
- (author && (outcome === 'success' || outcome === 'partial'))
864
- ? `👥 Auto-shared with team (by _${author}_) — visible in \`team_recall\``
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
- const savedMins = lesson.severity === 'critical' ? 240 : lesson.severity === 'major' ? 60 : 30;
903
- redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => { });
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 = kwMatches.filter(m => m.key.startsWith('cachly:lesson:best:'));
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
- const sev = lesson.severity;
1028
- const savedMins = sev === 'critical' ? 240 : sev === 'major' ? 60 : 30;
1029
- redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => { });
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
- if (lesson.outcome === 'success' && (lesson.recall_count ?? 0) >= 1) {
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: sev ?? 'major',
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 = kwMatches.map(m => m.score);
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 kwMatches) {
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
- // Hard gate: once a free user is over their (monthly, goodwill-adjusted)
1233
- // recall limit, the long tail is fully withheld until they upgrade or the
1234
- // month resets. We still show the single highest-value proven lesson below
1235
- // as a teaser so the tool isn't dead and the value is obvious.
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 visibleCount = gateActive ? 0 : 8;
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
- for (const r of hybridResults.slice(0, gateActive ? 0 : 12)) {
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 preview = r.content.slice(0, 300).replace(/\n/g, ' ');
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
- lines.push(` > ${preview}${r.content.length > 300 ? '…' : ''}\n`);
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 preview = r.content.slice(0, 400).replace(/\n/g, ' ');
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
- lines.push(`> ${preview}${r.content.length > 400 ? '…' : ''}\n`);
1696
+ for (const z of vorschau)
1697
+ lines.push(`> ${z}`);
1698
+ lines.push('');
1356
1699
  }
1357
1700
  }
1358
- // ── Hard gate footer: over the recall limit, the tail is withheld ──────
1359
- if (gateActive) {
1360
- const withheld = Math.max(0, hybridResults.length - visibleCount);
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
- `${withheld} relevant lesson${withheld !== 1 ? 's' : ''} withheld. ` +
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
- lines.push(` 🏆${sev} \`${l.topic}\` _(${why})_ — ${l.what_worked.slice(0, 100)}`);
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
- lines.push(` ${emoji}${sev} \`${l.topic}\` — ${l.what_worked.slice(0, 100)}`);
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
- const byRecall = [...lessons].sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0));
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
- lines.push(` ${emoji}${sev}${trust} \`${l.topic}\`${rct} — ${l.what_worked.slice(0, 100)}`);
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
- lines.push(` ${emoji}${sev} \`${l.topic}\` — ${l.what_worked.slice(0, 100)}`);
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
- summaryLines.push(`${sevIcon} ${sevLabel}\`${l.topic}\` — ${snippet}${rcStr}`);
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
- `⏱️ **${timeSavedHuman}** total saved not re-researching known fixes.`,
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
  }