@cachly-dev/mcp-server 0.10.124 → 0.10.126

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 (88) 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 +536 -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 +272 -26
  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/versuch.d.ts +179 -0
  75. package/dist/src/versuch.js +250 -0
  76. package/dist/src/versuch.js.map +1 -0
  77. package/dist/src/vorspann.d.ts +73 -0
  78. package/dist/src/vorspann.js +146 -0
  79. package/dist/src/vorspann.js.map +1 -0
  80. package/dist/src/werkzeug-nutzung.d.ts +62 -0
  81. package/dist/src/werkzeug-nutzung.js +72 -0
  82. package/dist/src/werkzeug-nutzung.js.map +1 -0
  83. package/dist/src/wertbeitrag.d.ts +57 -0
  84. package/dist/src/wertbeitrag.js +73 -0
  85. package/dist/src/wertbeitrag.js.map +1 -0
  86. package/glama.json +1 -1
  87. package/package.json +12 -4
  88. package/server.json +2 -2
@@ -3,11 +3,69 @@ 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
+ import { versuchStart, neueKennung, wendeZuteilungAn, schliesseVersuchAb } from '../versuch.js';
10
+ /**
11
+ * Preview for one smart_recall hit.
12
+ *
13
+ * WHY THIS EXISTS: the previous version sliced `r.content` — the RAW JSON of the
14
+ * stored record — at 300/400 characters. So the budget was spent on syntax:
15
+ *
16
+ * > {"topic":"node4:einrichtung-contabo-und-fallen","outcome":"success","sev…
17
+ *
18
+ * Roughly the first 80 characters carry no information at all, the reader gets
19
+ * field names instead of facts, and `commands` / `what_failed` never appear
20
+ * because they sit far past the cut. smart_recall is the call this project
21
+ * mandates BEFORE EVERY TASK — it is the worst possible place for that.
22
+ *
23
+ * A lesson is now rendered as a lesson: trimmed prose, its warning when the
24
+ * lesson is critical, and its commands/paths. Anything that is not a lesson
25
+ * (context entries, indexes) keeps a raw preview — but trimmed at a word
26
+ * boundary and with an honest cut marker.
27
+ */
28
+ export function recallVorschau(key, content, maxChars) {
29
+ if (key.includes('lesson:best:')) {
30
+ const l = safeJsonParse(content, {});
31
+ if (l && typeof l === 'object' && (l.what_worked || l.what_failed || l.commands?.length)) {
32
+ return lessonPreviewLines(l, { maxChars, indent: ' ' });
33
+ }
34
+ }
35
+ const roh = trimTo(content, maxChars);
36
+ return roh ? [roh] : [];
37
+ }
6
38
  import { getRole, ROLE_BADGE, getScopes, lessonVisibleToScope, reviewModeEnabled, storeLessonProposal } from './team.js';
7
- import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
39
+ import { keywordSearch, wortindexEntwerten, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
8
40
  import { rerankByQuality, qualityMultiplier, extractLessonQuality } from '../rerank.js';
9
41
  import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
42
+ import { VEKTOR_PRAEFIX, NAME_VEKTOR_PRAEFIX, packe, textFuerVektor, textFuerNamensVektor, Vektorbestand, } from '../bedeutung.js';
43
+ import { schreibeEingaenge, Eingangsbestand, EINGANG_GEWICHT } from '../eingaenge.js';
44
+ import { Seltenheitsbestand } from '../seltenheitsbestand.js';
45
+ import { spurLegen } from '../spuren.js';
46
+ import { bewerteTopf, spreizeImTopf, reichereAn, inhaltsWoerter, grobStamm, GEWICHTE, } from '../rangfolge.js';
47
+ /**
48
+ * Ein Vektorbestand fuer den ganzen Prozess.
49
+ *
50
+ * Er haelt rund 2 MB je 500 Lektionen im Arbeitsspeicher. Die bei jeder Frage
51
+ * neu zu holen waere langsamer als der Wortabgleich, den wir gerade ergaenzen —
52
+ * der Gewinn waere weg, bevor er anfaengt.
53
+ */
54
+ const vektorbestand = new Vektorbestand();
55
+ /** Dieselbe Mechanik, andere Sicht: die Vektoren der Themennamen. */
56
+ const namensbestand = new Vektorbestand(60_000, NAME_VEKTOR_PRAEFIX);
57
+ /** Die Fehlertext-Eingaenge, je Lektion ihr bester. */
58
+ const eingangsbestand = new Eingangsbestand();
59
+ /** Die Wortstatistik des Bestands — stehend, nicht je Frage neu. */
60
+ const seltenheitsbestand = new Seltenheitsbestand();
10
61
  import { upgradeNudge } from '../upgrade-nudge.js';
62
+ import { recallTiefe, TIEFE_VOLL, TIEFE_VOLL_MEHRTHEMIG } from '../recall-tiefe.js';
63
+ import { nutzungInWorten, nutzungsSchluessel, verdichte } from '../werkzeug-nutzung.js';
64
+ import { inWorten as lieferungInWorten, lieferBild, lieferSchluessel, trefferSchluessel } from '../geliefert.js';
65
+ import { COAUTHOR_PREF_KEY, autorHinweis, coautorAus, echterGitLeser, ermittleAutor, mitCoautor, nurMensch } from '../autor.js';
66
+ import { TOOLS } from '../tools.js';
67
+ import { vorspannHinweis } from '../vorspann.js';
68
+ import { autorAbzeichen, fremdanteil } from '../autor-abzeichen.js';
11
69
  import { cachlyUrl } from '../cachly-url.js';
12
70
  // ── Changelog (shown once per version in session_start) ──────────────────────
13
71
  // Resolve the package version at runtime from package.json so the session briefing
@@ -407,11 +465,15 @@ export const BRAIN_TOOL_NAMES = new Set([
407
465
  ]);
408
466
  // ── Free-tier Teaser-Gate ──────────────────────────────────────────────────
409
467
  // 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
468
+ // the top hit — the magic moment is sacred — but withholds the long tail
469
+ // behind an upgrade teaser ("🔒 N further relevant lessons withheld"). This is a
412
470
  // DEPTH gate (hide the tail), never an access wall: the #1 result is always
413
471
  // returned. Usage/limit come from the same authoritative numbers as the Brain
414
472
  // Health bar (GET /instances/:id/memory), cached briefly to avoid per-call latency.
473
+ //
474
+ // Diese Zusage stand von 2026-07 bis 2026-08-18 NUR hier im Kommentar, waehrend
475
+ // der Zweig `visibleCount = gateActive ? 0 : 8` null Treffer zurueckgab. Seither
476
+ // ist sie eine Funktion mit Gegenprobe: ../recall-tiefe.ts (GROW-043).
415
477
  const UPGRADE_URL = cachlyUrl('/billing', 'upgrade');
416
478
  const _recallGateCache = new Map();
417
479
  /**
@@ -480,6 +542,29 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
480
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;
481
543
  const redis = await getConnection(instance_id);
482
544
  const ts = new Date().toISOString();
545
+ /*
546
+ * ── Der Autor wird HERGELEITET, nicht erwartet ─────────────────────
547
+ *
548
+ * Gemessen am 19.08.2026: 195 von 493 Lektionen ohne Autor. `author`
549
+ * war ein optionales Feld, das ein Agent bei jedem Aufruf mitschicken
550
+ * sollte — und natuerlich vergisst. Dabei steht der Name in
551
+ * `git config user.name`.
552
+ *
553
+ * Ein Pflichtfeld, das man vergessen kann, ist kein Pflichtfeld.
554
+ */
555
+ const herkunft = ermittleAutor({
556
+ uebergeben: author,
557
+ umgebung: process.env.CACHLY_AUTHOR,
558
+ git: echterGitLeser(process.env.CACHLY_WORKSPACE ?? process.cwd()),
559
+ });
560
+ // Mitautor nur, wenn ausdruecklich eingeschaltet — Standard ist aus.
561
+ let coautor = '';
562
+ try {
563
+ coautor = coautorAus(await redis.get(`cachly:prefs:${COAUTHOR_PREF_KEY}:${instance_id}`));
564
+ }
565
+ catch { /* eine Einstellung darf das Speichern nie stoppen */ }
566
+ const autorFeld = mitCoautor(herkunft.autor, coautor);
567
+ const autorTipp = autorHinweis(herkunft);
483
568
  // ── Lessons-Review workflow (P1-1) ─────────────────────────────────────
484
569
  // When the org enabled pending-mode on this (shared) brain, team-visible
485
570
  // lessons become proposals awaiting owner/admin approval instead of
@@ -489,7 +574,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
489
574
  return storeLessonProposal(redis, topic, {
490
575
  topic, outcome, what_worked, what_failed, context: ctx, severity,
491
576
  file_paths, commands, tags, depends_on,
492
- ...(author ? { author } : {}),
577
+ ...(autorFeld ? { author: autorFeld } : {}),
493
578
  ...(service ? { service } : {}),
494
579
  ...(group ? { group: String(group).toLowerCase().trim() } : {}),
495
580
  visibility, recall_count: 0, ts, confidence: 1.0, version: 3,
@@ -611,7 +696,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
611
696
  commands,
612
697
  tags,
613
698
  depends_on,
614
- ...(author ? { author } : {}),
699
+ // Auch hier der hergeleitete Name — sonst traegt die gespeicherte
700
+ // Lektion selbst keinen Autor, waehrend der Kausalgraph einen hat.
701
+ ...(autorFeld ? { author: autorFeld } : {}),
615
702
  ...(service ? { service } : {}),
616
703
  ...(group ? { group: String(group).toLowerCase().trim() } : {}),
617
704
  visibility,
@@ -633,11 +720,71 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
633
720
  await redis.ltrim(listKey, -100, -1);
634
721
  await redis.expire(listKey, 90 * 86400);
635
722
  // Update best key for success/partial; for failure only update if no success exists
723
+ let bestGeschrieben = false;
636
724
  if (outcome === 'success' || outcome === 'partial') {
637
725
  await redis.set(`cachly:lesson:best:${topic}`, lesson);
726
+ bestGeschrieben = true;
638
727
  }
639
728
  else if (!existingRaw) {
640
729
  await redis.set(`cachly:lesson:best:${topic}`, lesson);
730
+ bestGeschrieben = true;
731
+ }
732
+ // Der stehende Wortindex kennt die neue Lektion noch nicht.
733
+ //
734
+ // Ohne diese Zeile waere sie bis zu eine Minute lang nicht auffindbar —
735
+ // und genau der Fall "gerade gelernt, sofort gesucht" ist der, bei dem
736
+ // ein Gedaechtnis sich beweisen muss. Der Index wird beim naechsten
737
+ // Recall neu gebaut, nicht hier: Lernen soll nicht auf das Bauen warten.
738
+ if (bestGeschrieben)
739
+ wortindexEntwerten();
740
+ // ── Vektor fuer den Bedeutungsabgleich ─────────────────────────────────
741
+ //
742
+ // Wird beim Schreiben berechnet, nicht beim Suchen. Der Grund steht in
743
+ // Zahlen: die Einbettung kostet rund 280 ms Netz. Einmal je Lektion ist
744
+ // das nichts, bei jeder Frage waere es der halbe Gewinn.
745
+ //
746
+ // Bewusst NICHT abgewartet: wer eine Lektion speichert, soll nicht auf
747
+ // einen fremden Dienst warten. Faellt die Einbettung aus, fehlt der
748
+ // Vektor — und der Bedeutungsabgleich uebergeht diese eine Lektion still.
749
+ // Das ist der Preis dafuer, dass Lernen immer funktioniert, auch ohne
750
+ // Netz. Wie viele Lektionen einen Vektor haben, meldet brain_doctor.
751
+ if (bestGeschrieben && hasEmbedProvider()) {
752
+ void (async () => {
753
+ const gelesen = safeJsonParse(lesson, {});
754
+ try {
755
+ const v = await computeEmbedding(textFuerVektor(gelesen));
756
+ if (v?.length)
757
+ await redis.set(`${VEKTOR_PRAEFIX}${topic}`, packe(v));
758
+ }
759
+ catch { /* kein Vektor ist besser als keine Lektion */ }
760
+ // Der Themenname als eigene Sicht.
761
+ //
762
+ // Er ist kurz und frageaehnlich; eine 60-Zeichen-Frage gegen 60
763
+ // Zeichen zu halten ist symmetrisch, gegen 1376 Zeichen Volltext
764
+ // nicht. Der Sortierer bewertet beide getrennt (naeheText 1,0,
765
+ // naeheThema 0,6). Bis zum 20.08.2026 gab es diese Vektoren nur im
766
+ // Messstand — deshalb war rangfolge.ts auch nie verdrahtet.
767
+ try {
768
+ const nv = await computeEmbedding(textFuerNamensVektor(topic));
769
+ if (nv?.length)
770
+ await redis.set(`${NAME_VEKTOR_PRAEFIX}${topic}`, packe(nv));
771
+ }
772
+ catch { /* siehe oben */ }
773
+ // Die zweite Tuer: woertliche Fehlertexte als eigene Eingaenge.
774
+ //
775
+ // Gemessen am 20.08.2026 an 499 Lektionen mit zwei unabhaengigen
776
+ // 100-Fragen-Saetzen: von fuenf ausprobierten Tuerarten traegt genau
777
+ // DIESE eine. Erstsatz und Uebersetzung brachten null, der Themenname
778
+ // steckt schon im Sortierer. Volle Zahlen und Gegenproben stehen in
779
+ // .agent/cachly/tor0-tor1-ergebnis.md.
780
+ //
781
+ // Genau wie der Vektor: nicht abgewartet, Fehler verschluckt. Eine
782
+ // Lektion muss auch ohne Netz gespeichert werden koennen.
783
+ try {
784
+ await schreibeEingaenge(redis, topic, gelesen, computeEmbedding);
785
+ }
786
+ catch { /* keine Eingaenge ist besser als keine Lektion */ }
787
+ })();
641
788
  }
642
789
  // Track in decision log for session replay
643
790
  try {
@@ -711,7 +858,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
711
858
  fix_confidence: existEdge.confidence,
712
859
  fix_trials: existEdge.trials,
713
860
  failure_outcome: outcome,
714
- reported_by: author || 'unknown',
861
+ reported_by: autorFeld || 'unknown',
715
862
  what_failed: what_failed || '',
716
863
  resolved: false,
717
864
  }), 'EX', 60 * 60 * 24 * 90);
@@ -779,14 +926,19 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
779
926
  // ── Phase 3A: People + File nodes in knowledge graph ──────────────────────
780
927
  // Builds the "who knows what" map automatically from author + file_paths.
781
928
  // Person → authored → Concept edges power brain_who_knows queries.
782
- if (author) {
929
+ //
930
+ // Seit dem 19.08.2026 steht hier autorFeld statt author: der Name wird
931
+ // aus git hergeleitet, wenn der Aufrufer keinen mitschickt. Vorher blieb
932
+ // dieser ganze Block bei zwei Fuenfteln aller Lektionen ungenutzt — und
933
+ // damit brain_who_knows leer.
934
+ if (autorFeld) {
783
935
  // Agent activity registry (Move 4): track which agents are actively
784
936
  // writing to this Brain, keyed by author, with a 1h TTL. Powers
785
937
  // 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(() => { });
938
+ 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
939
  try {
788
940
  const domain = topic.split(':')[0] ?? 'unknown';
789
- const personId = await ckgUpsertPersonNode(redis, author, domain);
941
+ const personId = await ckgUpsertPersonNode(redis, nurMensch(autorFeld), domain);
790
942
  const conceptId = ckgSlug(topic);
791
943
  await ckgUpdateEdge(redis, personId, 'authored', conceptId, outcome === 'success', outcome === 'partial');
792
944
  for (const fp of file_paths.slice(0, 8)) {
@@ -811,8 +963,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
811
963
  const conceptId = ckgSlug(topic);
812
964
  // concept → affects → service (outcome decides edge confidence)
813
965
  await ckgUpdateEdge(redis, conceptId, 'affects', serviceId, outcome === 'success', outcome === 'partial');
814
- if (author) {
815
- const personId = `person:${ckgSlug(author)}`;
966
+ if (autorFeld) {
967
+ const personId = `person:${ckgSlug(nurMensch(autorFeld))}`;
816
968
  await ckgUpdateEdge(redis, personId, 'operates', serviceId, true);
817
969
  }
818
970
  for (const fp of file_paths.slice(0, 8)) {
@@ -826,12 +978,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
826
978
  // auto-store as a team lesson so knowledge is shared without a separate
827
979
  // team_learn call. Tags gain 'team'; the lesson is attributed to the author.
828
980
  // This makes team knowledge sharing the default, not the opt-in.
829
- if (author && (outcome === 'success' || outcome === 'partial')) {
981
+ if (autorFeld && (outcome === 'success' || outcome === 'partial')) {
830
982
  const teamLesson = {
831
983
  topic, outcome, what_worked,
832
984
  what_failed: what_failed ?? '',
833
985
  severity,
834
- author,
986
+ author: autorFeld,
835
987
  file_paths,
836
988
  commands,
837
989
  tags: [...new Set([...tags, 'team'])],
@@ -860,14 +1012,22 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
860
1012
  isUpdate
861
1013
  ? `♻️ Updated (recall count: ${recallCount} · audit entries: ${auditTrail.length})`
862
1014
  : `💡 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\``
1015
+ (autorFeld && (outcome === 'success' || outcome === 'partial'))
1016
+ ? `👥 Auto-shared with team (by _${autorFeld}_${herkunft.quelle !== 'uebergeben' ? ` · aus ${herkunft.quelle}` : ''}) — visible in \`team_recall\``
865
1017
  : '',
1018
+ autorTipp ?? '',
866
1019
  depends_on.length > 0
867
1020
  ? `🔗 Depends on: ${depends_on.map(d => `\`${d}\``).join(', ')} → trace with \`trace_dependency\``
868
1021
  : '',
869
1022
  ...templateWarnings,
870
1023
  ...iWasWrongWarning,
1024
+ // GROW-044: Die 100-Zeichen-Regel wird geprueft, nicht geglaubt.
1025
+ // Das Briefing der naechsten Sitzung zeigt je Lektion rund 100
1026
+ // Zeichen; am 16.08.2026 stand die gesuchte Adresse an Zeichen 323,
1027
+ // die Lektion war eingeblendet und der Fehler passierte trotzdem.
1028
+ // Hinweis statt Ablehnung: es gibt Lektionen ohne harte Zahl, und eine
1029
+ // abgelehnte Lektion ist immer schlechter als eine unscharfe.
1030
+ vorspannHinweis(what_worked) ?? '',
871
1031
  ].filter(l => l !== '').join('\n');
872
1032
  }
873
1033
  case 'recall_best_solution': {
@@ -898,9 +1058,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
898
1058
  // A targeted lookup is a recall too, and costs the same one quota unit
899
1059
  // as a broad smart_recall — the user asked the brain a question either way.
900
1060
  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(() => { });
1061
+ // Track estimated time saved (30m minor · 60m major · 240m critical).
1062
+ // Starter-Lektionen zaehlen 0 — siehe wertbeitrag.ts.
1063
+ const savedMins = ersparteMinuten(lesson);
1064
+ if (savedMins > 0) {
1065
+ redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => { });
1066
+ }
904
1067
  const sevEmoji = lesson.severity === 'critical' ? '🔴' : lesson.severity === 'major' ? '🟡' : lesson.severity ? '🟢' : '';
905
1068
  const auditSummary = (lesson.audit_trail ?? []).length > 1
906
1069
  ? `_Audit: ${(lesson.audit_trail ?? []).length} changes · stored ${new Date(lesson.ts).toLocaleDateString('de-DE')}_`
@@ -1006,11 +1169,179 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1006
1169
  // ── Layer 1.5: Quality-aware rerank — proven success lessons outrank
1007
1170
  // text-similar failed attempts (the moat; see src/rerank.ts + Cachly-Bench). ──
1008
1171
  const kwMatches = rerankByQuality(rawMatches);
1172
+ // ── Layer 1.7: Bedeutungsabgleich ──────────────────────────────────────
1173
+ //
1174
+ // Der Wortabgleich findet, was woertlich dasteht. Eine Frage beschreibt
1175
+ // aber ein SYMPTOM, die Lektion eine URSACHE — "Der Deploy haengt beim
1176
+ // Bauen" und "No space left on device" teilen kein Wort und meinen
1177
+ // dasselbe.
1178
+ //
1179
+ // Gemessen am 19.08.2026 an 499 echten Lektionen mit 100 Fragen:
1180
+ //
1181
+ // Verfahren Platz 1 Top 3 Top 10
1182
+ // nur Woerter 21 % 33 % 47 %
1183
+ // nur Bedeutung 40 % 60 % 68 %
1184
+ // gemischt 30/70 39 % 55 % 71 %
1185
+ //
1186
+ // Gemischt wird, nicht ersetzt: ohne Netz gibt es keine Einbettung, und
1187
+ // die ersten zehn Treffer sind gemischt besser als bei beiden einzeln.
1188
+ //
1189
+ // Faellt hier etwas aus, bleibt es beim Wortabgleich. Ein Recall, der
1190
+ // wegen eines fremden Dienstes gar nichts liefert, waere schlimmer als
1191
+ // einer, der schlechter sortiert.
1192
+ //
1193
+ // Jeder Ausstieg hier meldet sich EINMAL je Prozess (siehe
1194
+ // `aussetzer.ts`). Am 20.08.2026 wurde gemessen, dass dieser Pfad in
1195
+ // Produktion monatelang stumm ausstieg: 0 Vektoren bei 506 Lektionen.
1196
+ // Von aussen war das von "alles in Ordnung" nicht zu unterscheiden.
1197
+ let sinnAngewandt = false;
1198
+ const kwGemischt = await (async () => {
1199
+ if (!hasEmbedProvider()) {
1200
+ meldeEinmal(OHNE_DIENST, 'kein Einbettungsdienst eingerichtet — die Suche laeuft nur ueber Woerter');
1201
+ return kwMatches;
1202
+ }
1203
+ try {
1204
+ await vektorbestand.aktualisiere(redis);
1205
+ if (vektorbestand.groesse === 0) {
1206
+ meldeEinmal(OHNE_VEKTOREN, 'keine einzige Lektion hat eine Einbettung — die Suche laeuft nur ueber '
1207
+ + 'Woerter. Nachruesten mit eingaenge-nachruesten, pruefen mit brain_doctor');
1208
+ return kwMatches;
1209
+ }
1210
+ const frageVektor = await computeEmbedding(query);
1211
+ if (!frageVektor?.length) {
1212
+ meldeEinmal(OHNE_FRAGEVEKTOR, 'die Frage liess sich nicht einbetten — diese Suche laeuft nur ueber Woerter');
1213
+ return kwMatches;
1214
+ }
1215
+ await namensbestand.aktualisiere(redis);
1216
+ await eingangsbestand.aktualisiere(redis);
1217
+ await seltenheitsbestand.aktualisiere(redis);
1218
+ const wortThemen = kwMatches
1219
+ .filter((m) => m.key.startsWith('cachly:lesson:best:'))
1220
+ .map((m) => m.key.slice('cachly:lesson:best:'.length));
1221
+ // ── Die Vorauswahl ────────────────────────────────────────────────
1222
+ //
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.
1227
+ //
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.
1231
+ //
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));
1239
+ const alleThemen = [...seltenheitsbestand.themen()];
1240
+ const sinnThemen = (alleThemen.length > 0
1241
+ ? alleThemen
1242
+ : vektorbestand.aehnlichste(frageVektor, 500).map((x) => x.topic))
1243
+ .map((t) => ({ t, n: naeheBesteTuer(t) }))
1244
+ .sort((a, b) => b.n - a.n)
1245
+ .slice(0, 25)
1246
+ .map((x) => x.t);
1247
+ // ── Die Sortierung ───────────────────────────────────────────────
1248
+ //
1249
+ // Bis zum 20.08.2026 stand hier `mischeRangfolgen` (Reciprocal Rank
1250
+ // Fusion). Das war der Grund, warum die ganze Eingangs-Arbeit im
1251
+ // Produkt fast nichts brachte: RRF mischt ueber PLATZIERUNGEN und
1252
+ // kann die Naehe zum besten Eingang nicht als abgestufte Groesse
1253
+ // verwerten.
1254
+ //
1255
+ // frischer Pruefsatz, Findequote@3
1256
+ // RRF, wie ausgeliefert 50 %
1257
+ // RRF + Eingaenge als dritte Quelle 51 %
1258
+ // bewerteTopf 52 %
1259
+ // bewerteTopf + Eingaenge 58 %
1260
+ //
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.
1265
+ const topf = [...new Set([...wortThemen, ...sinnThemen])];
1266
+ const besteDrei = sinnThemen.slice(0, 3)
1267
+ .map((t) => vektorbestand.rohvektor(t)).filter(Boolean);
1268
+ const angereichert = besteDrei.length ? reichereAn(frageVektor, besteDrei) : frageVektor;
1269
+ const frageWoerter = inhaltsWoerter(query);
1270
+ const statistik = seltenheitsbestand.statistik;
1271
+ const bewertbar = topf.map((t) => ({
1272
+ naeheText: vektorbestand.naehe(frageVektor, t),
1273
+ naeheThema: namensbestand.naehe(frageVektor, t),
1274
+ naeheRueckkopplung: vektorbestand.naehe(angereichert, t),
1275
+ seltenheitsDeckung: statistik
1276
+ ? statistik.deckung(frageWoerter, new Set([...inhaltsWoerter(seltenheitsbestand.textVon(t))].map(grobStamm)))
1277
+ : 0,
1278
+ }));
1279
+ let punkte = bewerteTopf(bewertbar, GEWICHTE);
1280
+ if (eingangsbestand.groesse > 0) {
1281
+ const naehen = topf.map((t) => eingangsbestand.besteNaehe(frageVektor, t));
1282
+ const gespreizt = spreizeImTopf(naehen);
1283
+ punkte = punkte.map((pkt, i) => pkt + EINGANG_GEWICHT * gespreizt[i]);
1284
+ }
1285
+ const reihenfolge = topf
1286
+ .map((t, i) => ({ t, p: punkte[i] }))
1287
+ .sort((a, b) => b.p - a.p)
1288
+ .map((x) => x.t);
1289
+ const bekannt = new Map(kwMatches.map((m) => [m.key, m]));
1290
+ // Lektionen, die NUR der Bedeutungsabgleich gefunden hat, muessen
1291
+ // nachgeladen werden — der Wortabgleich hat sie nie angefasst.
1292
+ const sortiert = [];
1293
+ for (const thema of reihenfolge) {
1294
+ const key = `cachly:lesson:best:${thema}`;
1295
+ const da = bekannt.get(key);
1296
+ if (da) {
1297
+ sortiert.push(da);
1298
+ bekannt.delete(key);
1299
+ continue;
1300
+ }
1301
+ const inhalt = await redis.get(key).catch(() => null);
1302
+ if (inhalt) {
1303
+ sortiert.push({ key, content: inhalt, score: 0, matchedWords: [], finalScore: 0, qualityBoost: 1 });
1304
+ }
1305
+ }
1306
+ // Alles, was kein Thema war (Kontexteintraege, Index), bleibt hinten
1307
+ // erhalten statt still zu verschwinden.
1308
+ sortiert.push(...bekannt.values());
1309
+ sinnAngewandt = true;
1310
+ return sortiert;
1311
+ }
1312
+ catch {
1313
+ return kwMatches;
1314
+ }
1315
+ })();
1316
+ void sinnAngewandt;
1009
1317
  // One quota unit for this call, regardless of how many lessons it surfaced.
1010
1318
  void bumpRecallQuota(redis);
1319
+ // ── Gedaechtniszellen ────────────────────────────────────────────────
1320
+ //
1321
+ // Diese Frage hat diese Lektionen gefunden — das ist ein bewiesener Weg
1322
+ // zu ihnen. Fuehrt dieselbe Frage ein ZWEITES Mal hierher, wird sie ein
1323
+ // eigener Eingang, und beim naechsten Mal ist der Weg breiter.
1324
+ //
1325
+ // Warum erst beim zweiten Mal: das Signal "wurde benutzt" gibt es nicht.
1326
+ // Der Server sieht, was er ausgeliefert hat, nicht was jemand damit
1327
+ // gemacht hat. Wiederholung IST das, was einen Trampelpfad ausmacht.
1328
+ //
1329
+ // Nur die ersten DREI: das ist, was die Einblendung zeigt. Wer alle
1330
+ // Treffer aufzeichnet, macht aus jedem Streifschuss einen Weg.
1331
+ //
1332
+ // Nicht abgewartet: wer sucht, soll nicht warten, bis das Gedaechtnis
1333
+ // nachgefuehrt ist.
1334
+ if (hasEmbedProvider()) {
1335
+ const gefundeneThemen = kwGemischt
1336
+ .filter((m) => m.key.startsWith('cachly:lesson:best:'))
1337
+ .slice(0, 3)
1338
+ .map((m) => m.key.slice('cachly:lesson:best:'.length));
1339
+ void spurLegen(redis, query, gefundeneThemen, computeEmbedding)
1340
+ .catch(() => { });
1341
+ }
1011
1342
  const savedHere = [];
1012
1343
  // 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:'));
1344
+ const lessonMatches = kwGemischt.filter(m => m.key.startsWith('cachly:lesson:best:'));
1014
1345
  let crossAuthorThisCall = 0;
1015
1346
  let successRecallsThisCall = 0;
1016
1347
  for (const m of lessonMatches.slice(0, 5)) {
@@ -1024,9 +1355,11 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1024
1355
  const recalledAt = new Date().toISOString();
1025
1356
  const updated = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1, verified_at: recalledAt, last_recalled_at: recalledAt };
1026
1357
  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(() => { });
1358
+ // Starter-Lektionen zaehlen 0 — siehe wertbeitrag.ts.
1359
+ const savedMins = ersparteMinuten(lesson);
1360
+ if (savedMins > 0) {
1361
+ redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => { });
1362
+ }
1030
1363
  // Metric 3 (team-knowledge-reuse): a recall of a lesson authored by
1031
1364
  // someone OTHER than the requester is cross-author reuse — the value
1032
1365
  // only cachly delivers. Track total proven recalls + cross-author ones.
@@ -1044,11 +1377,16 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1044
1377
  }
1045
1378
  if (lesson.outcome === 'success')
1046
1379
  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) {
1380
+ // Surface banner for proven successes (recall_count >= 1 means it's been validated).
1381
+ //
1382
+ // Der Startvorrat kommt hier NICHT ins Banner. "Brain saved you ~8h
1383
+ // here — docker:layer-cache" ist keine Leistung des Gedaechtnisses,
1384
+ // sondern mitgeliefertes Allgemeinwissen. Genau dieses Banner hat
1385
+ // die Zahl im Panel jahrelang glaubwuerdig aussehen lassen.
1386
+ if (lesson.outcome === 'success' && (lesson.recall_count ?? 0) >= 1 && !istStarterLektion(lesson)) {
1049
1387
  savedHere.push({
1050
1388
  topic: m.key.replace('cachly:lesson:best:', ''),
1051
- severity: sev ?? 'major',
1389
+ severity: lesson.severity ?? 'major',
1052
1390
  recall_count: (lesson.recall_count ?? 0) + 1,
1053
1391
  savedMins,
1054
1392
  ts: lesson.ts,
@@ -1098,12 +1436,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1098
1436
  // Semantic search failed silently — keyword results are enough
1099
1437
  }
1100
1438
  }
1101
- const bm25Scores = kwMatches.map(m => m.score);
1439
+ const bm25Scores = kwGemischt.map(m => m.score);
1102
1440
  const bm25Min = bm25Scores.length ? Math.min(...bm25Scores) : 0;
1103
1441
  const bm25Range = bm25Scores.length ? (Math.max(...bm25Scores) - bm25Min) || 1 : 1;
1104
1442
  const bm25Norm = (s) => (s - bm25Min) / bm25Range;
1105
1443
  const hybridMap = new Map();
1106
- for (const m of kwMatches) {
1444
+ for (const m of kwGemischt) {
1107
1445
  const n = bm25Norm(m.score);
1108
1446
  hybridMap.set(m.key, {
1109
1447
  key: m.key, content: m.content, bm25Score: n,
@@ -1227,14 +1565,49 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1227
1565
  return true;
1228
1566
  })
1229
1567
  .sort((a, b) => b.hybridScore - a.hybridScore);
1568
+ // ── Versuch (Messtechnik, siehe versuch.ts) ───────────────────────────────
1569
+ //
1570
+ // Standardmaessig AUS: `versuchStart()` gibt dann `null` zurueck, und
1571
+ // dieser ganze Block tut nichts — `hybridResults` bleibt unangetastet,
1572
+ // nichts wird nach Redis geschrieben, kein Zeichen im Text aendert sich.
1573
+ // Erst mit `CACHLY_VERSUCH=an` UND gesetztem Salz greift die Zuteilung.
1574
+ const versuchSalz = versuchStart();
1575
+ let versuchKennung = '';
1576
+ let versuchTeil = null;
1577
+ if (versuchSalz !== null) {
1578
+ versuchKennung = neueKennung();
1579
+ const hoechstePunktzahl = hybridResults[0]?.hybridScore ?? 0;
1580
+ const ergebnis = wendeZuteilungAn(hybridResults.map((r) => ({ key: r.key, content: r.content, punktzahl: r.hybridScore })), versuchKennung, versuchSalz, hoechstePunktzahl);
1581
+ if (ergebnis.weggelassenesThema !== undefined) {
1582
+ // HOLD: der bisher beste Treffer wird aus der ausgelieferten Liste
1583
+ // entfernt. Die uebrigen Treffer und ihre Reihenfolge bleiben gleich.
1584
+ hybridResults.splice(0, 1);
1585
+ }
1586
+ else if (ergebnis.markierungGesetzt && hybridResults[0] && ergebnis.treffer[0]) {
1587
+ // DELIVER: derselbe Treffer, aber sein Inhalt traegt jetzt die
1588
+ // Kanarien-Markierung am Ende von `what_worked`.
1589
+ hybridResults[0] = { ...hybridResults[0], content: ergebnis.treffer[0].content };
1590
+ }
1591
+ versuchTeil = {
1592
+ frage: query,
1593
+ hoechstePunktzahl,
1594
+ themen: [], // unten befuellt, sobald feststeht, was wirklich sichtbar ist
1595
+ zulassungsfaehig: ergebnis.zulassungsfaehig,
1596
+ zulassungsGrund: ergebnis.zulassungsGrund,
1597
+ zuteilung: ergebnis.zuteilung,
1598
+ weggelassenesThema: ergebnis.weggelassenesThema,
1599
+ markierungGesetzt: ergebnis.markierungGesetzt,
1600
+ };
1601
+ }
1230
1602
  // ── Teaser-Gate: free tier over its recall limit hides the long tail ──────
1231
1603
  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.
1604
+ // Depth gate: once a free user is over their (monthly, goodwill-adjusted)
1605
+ // recall limit, the LONG TAIL is withheld — never the top hit. The rule
1606
+ // lives in recall-tiefe.ts with tests (GROW-043), because it used to live
1607
+ // only in a comment while the branch said `? 0 :` and returned nothing.
1236
1608
  const gateActive = recallGate.reached;
1237
- const visibleCount = gateActive ? 0 : 8;
1609
+ const tiefe = recallTiefe({ ueberLimit: gateActive, gesamt: hybridResults.length, volleTiefe: TIEFE_VOLL });
1610
+ const visibleCount = tiefe.sichtbar;
1238
1611
  // ── Build output ──────────────────────────────────────────────────────────
1239
1612
  const lines = [`🧠 **Smart Recall** for: _"${query}"_\n`];
1240
1613
  // Warm goodwill note — shown the month a free user is gifted +250 recalls.
@@ -1288,7 +1661,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1288
1661
  // Group by sub-query for multi-topic queries
1289
1662
  if (subQueries.length > 1) {
1290
1663
  const grouped = new Map();
1291
- for (const r of hybridResults.slice(0, gateActive ? 0 : 12)) {
1664
+ const tiefeMehrthemig = recallTiefe({ ueberLimit: gateActive, gesamt: hybridResults.length, volleTiefe: TIEFE_VOLL_MEHRTHEMIG });
1665
+ for (const r of hybridResults.slice(0, tiefeMehrthemig.sichtbar)) {
1292
1666
  const sq = r.subQuery ?? query;
1293
1667
  if (!grouped.has(sq))
1294
1668
  grouped.set(sq, []);
@@ -1307,7 +1681,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1307
1681
  : r.matchType === 'semantic'
1308
1682
  ? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
1309
1683
  : `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
1310
- const preview = r.content.slice(0, 300).replace(/\n/g, ' ');
1684
+ const vorschau = recallVorschau(r.key, r.content, 300);
1311
1685
  lines.push(` **${label}** _(${scorePart})_`);
1312
1686
  // Lesbares Brain (docs/produkt/team-puls.md P1): plain-language layer
1313
1687
  // stored at lesson:human:<topic> — display-only, ranking untouched.
@@ -1318,7 +1692,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1318
1692
  if (h.title)
1319
1693
  lines.push(` 📖 **${h.title}** — ${h.summary ?? ''}`);
1320
1694
  }
1321
- lines.push(` > ${preview}${r.content.length > 300 ? '…' : ''}\n`);
1695
+ for (const z of vorschau)
1696
+ lines.push(` > ${z}`);
1697
+ lines.push('');
1322
1698
  }
1323
1699
  }
1324
1700
  const matched = [...grouped.keys()];
@@ -1342,7 +1718,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1342
1718
  : r.matchType === 'semantic'
1343
1719
  ? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
1344
1720
  : `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
1345
- const preview = r.content.slice(0, 400).replace(/\n/g, ' ');
1721
+ const vorschau = recallVorschau(r.key, r.content, 400);
1346
1722
  lines.push(`**${label}**${authorBadge}${contextBadge} _(${scorePart})_`);
1347
1723
  // Lesbares Brain (docs/produkt/team-puls.md P1): plain-language layer.
1348
1724
  if (r.key.startsWith('cachly:lesson:best:')) {
@@ -1352,14 +1728,18 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1352
1728
  if (h.title)
1353
1729
  lines.push(`📖 **${h.title}** — ${h.summary ?? ''}`);
1354
1730
  }
1355
- lines.push(`> ${preview}${r.content.length > 400 ? '…' : ''}\n`);
1731
+ for (const z of vorschau)
1732
+ lines.push(`> ${z}`);
1733
+ lines.push('');
1356
1734
  }
1357
1735
  }
1358
- // ── Hard gate footer: over the recall limit, the tail is withheld ──────
1359
- if (gateActive) {
1360
- const withheld = Math.max(0, hybridResults.length - visibleCount);
1736
+ // ── Depth-gate footer: the top hit stayed, the tail is withheld ────────
1737
+ // Only speak when something was actually held back — announcing
1738
+ // "0 lessons withheld" would sell a wall that isn't there.
1739
+ if (gateActive && tiefe.zurueckgehalten > 0) {
1740
+ const withheld = tiefe.zurueckgehalten;
1361
1741
  lines.push(`🔒 **Free recall limit reached** (${recallGate.used}/${recallGate.limit} this month). ` +
1362
- `${withheld} relevant lesson${withheld !== 1 ? 's' : ''} withheld. ` +
1742
+ `Showing your top match; ${withheld} further relevant lesson${withheld !== 1 ? 's are' : ' is'} withheld. ` +
1363
1743
  `Your limit resets next month, or unlock your full Brain now → ${UPGRADE_URL}\n`);
1364
1744
  }
1365
1745
  }
@@ -1397,6 +1777,15 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1397
1777
  lines.push(` • Use \`recall_best_solution("topic")\` for exact topic lookup`);
1398
1778
  }
1399
1779
  }
1780
+ // Versuch, Abschluss: Kanarienvogel gegen die WIRKLICH zusammengebaute
1781
+ // Antwort pruefen, Markierung entfernen, Protokoll schreiben (schlaegt
1782
+ // das fehl, bleibt die Antwort trotzdem stehen — siehe schreibeTurnProtokoll).
1783
+ if (versuchSalz !== null && versuchTeil) {
1784
+ const themen = hybridResults.slice(0, visibleCount)
1785
+ .filter((r) => r.key.startsWith('cachly:lesson:best:'))
1786
+ .map((r) => r.key.slice('cachly:lesson:best:'.length));
1787
+ return schliesseVersuchAb(redis, versuchKennung, { ...versuchTeil, themen }, lines.join('\n'));
1788
+ }
1400
1789
  return lines.join('\n');
1401
1790
  }
1402
1791
  // ── get_api_status ────────────────────────────────────────────────────────
@@ -1741,7 +2130,13 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1741
2130
  // auto-promote a lesson without any manual step.
1742
2131
  {
1743
2132
  const CRYSTALLIZE_RECALLS = 5;
2133
+ // Der Startvorrat wird nie zum Gesetz. Eine mitgelieferte Lektion sammelt
2134
+ // Abrufe, weil sie zu allem ein bisschen passt — nicht, weil sie sich
2135
+ // bewaehrt hat. Gemessen am 17.08.2026: Die fuenf obersten "Gesetze"
2136
+ // eines Brains mit 475 Lektionen waren alle aus dem Startvorrat, und die
2137
+ // echten Lektionen standen darunter bei zwei bis fuenf Abrufen.
1744
2138
  const laws = lessons
2139
+ .filter(l => !istStarterLektion(l))
1745
2140
  .filter(l => l.outcome === 'success' && (l.pinned === true || (l.recall_count ?? 0) >= CRYSTALLIZE_RECALLS))
1746
2141
  .sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0))
1747
2142
  .slice(0, 5);
@@ -1751,7 +2146,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1751
2146
  const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
1752
2147
  const rc = l.recall_count ?? 0;
1753
2148
  const why = l.pinned === true ? 'pinned' : `recalled ${rc}×`;
1754
- lines.push(` 🏆${sev} \`${l.topic}\` _(${why})_ — ${l.what_worked.slice(0, 100)}`);
2149
+ const body = lessonPreviewLines(l);
2150
+ lines.push(` 🏆${sev} \`${l.topic}\` _(${why})_ — ${body[0] ?? ''}`, ...body.slice(1));
1755
2151
  }
1756
2152
  lines.push('');
1757
2153
  }
@@ -1841,14 +2237,18 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1841
2237
  for (const l of focusLessons.slice(0, 4)) {
1842
2238
  const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
1843
2239
  const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
1844
- lines.push(` ${emoji}${sev} \`${l.topic}\` — ${l.what_worked.slice(0, 100)}`);
2240
+ const body = lessonPreviewLines(l);
2241
+ lines.push(` ${emoji}${sev} \`${l.topic}\` — ${body[0] ?? ''}`, ...body.slice(1));
1845
2242
  }
1846
2243
  lines.push('');
1847
2244
  }
1848
2245
  // Top lessons — sorted by recall_count desc (most-used = most proven value).
1849
2246
  // If focus is set, focus-matched lessons already shown above; show remaining here.
1850
2247
  if (lessons.length > 0) {
1851
- const byRecall = [...lessons].sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0));
2248
+ // Auch hier ohne Startvorrat: "Most valuable" soll zeigen, was DIESES
2249
+ // Team gelernt hat, nicht was mitgeliefert wurde.
2250
+ const byRecall = [...lessons].filter(l => !istStarterLektion(l))
2251
+ .sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0));
1852
2252
  const topRecalled = byRecall.filter(l => (l.recall_count ?? 0) > 0).slice(0, 3);
1853
2253
  const remaining = (focusLessons.length > 0 ? lessons.filter(l => !focusLessons.includes(l)) : lessons)
1854
2254
  .filter(l => !topRecalled.includes(l))
@@ -1865,7 +2265,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1865
2265
  const trust = (nAuthors >= 2 && rc >= 5) || rc >= 10 ? ' 🏆'
1866
2266
  : nAuthors >= 2 || rc >= 5 ? ' ✅' : '';
1867
2267
  const rct = rc > 1 ? ` _(${rc}× recalled)_` : '';
1868
- lines.push(` ${emoji}${sev}${trust} \`${l.topic}\`${rct} — ${l.what_worked.slice(0, 100)}`);
2268
+ const body = lessonPreviewLines(l);
2269
+ lines.push(` ${emoji}${sev}${trust} \`${l.topic}\`${rct} — ${body[0] ?? ''}`, ...body.slice(1));
1869
2270
  }
1870
2271
  lines.push('');
1871
2272
  }
@@ -1875,7 +2276,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
1875
2276
  for (const l of remaining) {
1876
2277
  const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
1877
2278
  const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
1878
- lines.push(` ${emoji}${sev} \`${l.topic}\` — ${l.what_worked.slice(0, 100)}`);
2279
+ const body = lessonPreviewLines(l);
2280
+ lines.push(` ${emoji}${sev} \`${l.topic}\` — ${body[0] ?? ''}`, ...body.slice(1));
1879
2281
  }
1880
2282
  lines.push('');
1881
2283
  }
@@ -2323,9 +2725,19 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
2323
2725
  const rc = l.recall_count ?? 0;
2324
2726
  const rcStr = rc > 0 ? ` (✓ recalled ${rc}×)` : '';
2325
2727
  const snippet = l.what_worked.slice(0, 110);
2326
- summaryLines.push(`${sevIcon} ${sevLabel}\`${l.topic}\` — ${snippet}${rcStr}`);
2728
+ // GROW-045: `author` wurde bisher nur entgegengenommen, nie benutzt —
2729
+ // obwohl die Werkzeug-Spezifikation "used for team lesson filtering"
2730
+ // verspricht. Dieselbe Regel wie in smart_recall, jetzt aus EINEM Modul.
2731
+ const autor = autorAbzeichen(l.author, author);
2732
+ summaryLines.push(`${sevIcon} ${sevLabel}\`${l.topic}\` — ${snippet}${rcStr}${autor}`);
2327
2733
  }
2328
2734
  summaryLines.push('');
2735
+ // Die Zahl, die nur ein GETEILTES Gedaechtnis liefert. Ist sie null,
2736
+ // wird nichts gesagt: "0 von Teamkollegen" ist keine Nachricht.
2737
+ const fremde = fremdanteil(selected, author);
2738
+ if (fremde > 0) {
2739
+ summaryLines.push(`👥 ${fremde} of these ${fremde === 1 ? 'was' : 'were'} written by a teammate — the value only a shared brain delivers.`, '');
2740
+ }
2329
2741
  if (needsRanking) {
2330
2742
  summaryLines.push(`💡 Showing top ${shown} of ${total} lessons · \`session_start\` for full briefing`);
2331
2743
  }
@@ -3169,6 +3581,38 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
3169
3581
  redis.get(`cachly:stats:time_saved_mins:${instance_id}`).catch(() => null),
3170
3582
  redis.smembers(`cachly:stats:reuse_pairs:${instance_id}`).catch(() => []),
3171
3583
  ]);
3584
+ // ── GROW-046: welche Werkzeuge dieses Brain wirklich benutzt ────────────
3585
+ // Bewusst HIER und nicht in einem eigenen Werkzeug: ein 123. Werkzeug,
3586
+ // um zu messen, dass es zu viele Werkzeuge gibt, waere die Pointe, die
3587
+ // niemand will.
3588
+ // try/catch und nicht nur .catch(): faellt die Methode ganz aus (aelterer
3589
+ // Client, anderer Redis-Aufsatz), wirft schon der AUFRUF, und eine
3590
+ // Zusagen-Absicherung greift dann nicht. Eine Messzeile darf das
3591
+ // Werkzeug nie stoppen.
3592
+ let werkzeugRoh = {};
3593
+ try {
3594
+ werkzeugRoh = (await redis.hgetall(nutzungsSchluessel(instance_id))) ?? {};
3595
+ }
3596
+ catch {
3597
+ werkzeugRoh = {};
3598
+ }
3599
+ const nutzung = verdichte(werkzeugRoh, 10);
3600
+ // ── Die GEMESSENE Ersparnis, neben der gerechneten ─────────────────────
3601
+ // Heinrich am 19.08.2026: "Bei der Schaetzung der Tokenersparnis sollten
3602
+ // wir schauen, dass wir nicht nur schaetzen, denn das nervt Leute sehr
3603
+ // stark." Dies hier ist eine Summe, keine Rechnung: die Laenge dessen,
3604
+ // was das Brain wirklich zurueckgegeben hat. Dieselbe try/catch-Vorsicht
3605
+ // wie oben — eine Messzeile darf das Werkzeug nie stoppen.
3606
+ let lieferRoh = { token: 0, abrufe: 0, treffer: 0 };
3607
+ try {
3608
+ const token = await redis.get(lieferSchluessel(instance_id));
3609
+ const quoten = (await redis.hgetall(trefferSchluessel(instance_id))) ?? {};
3610
+ lieferRoh = { token: token ?? 0, abrufe: quoten.abrufe ?? 0, treffer: quoten.treffer ?? 0 };
3611
+ }
3612
+ catch {
3613
+ // still
3614
+ }
3615
+ const lieferung = lieferBild(lieferRoh);
3172
3616
  // ── Metric 1: Time-to-first-recall ──────────────────────────────────────
3173
3617
  let ttfrLine;
3174
3618
  if (bornAt && firstRecallAt) {
@@ -3192,6 +3636,36 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
3192
3636
  const crossAuthor = Number(crossAuthorRaw ?? 0);
3193
3637
  const reusePct = recallsTotal > 0 ? Math.round((crossAuthor / recallsTotal) * 100) : 0;
3194
3638
  const reuseTarget = reusePct >= 30 ? '🟢 above 30 % target' : recallsTotal === 0 ? '⚪ no recalls yet' : '🟡 below 30 % target';
3639
+ /*
3640
+ * ── Die geschaetzten Stunden stehen nicht mehr im Kundenblick ─────────
3641
+ *
3642
+ * "1.429,5 h total saved not re-researching known fixes" war die
3643
+ * groesste erfundene Zahl, die wir gezeigt haben. Sie ist gerechnet:
3644
+ * Minuten je Lektion nach Schwere, mal Abrufzahl. Der Faktor ist
3645
+ * geraten, und die Zahl waechst mit jedem Abruf mit — auch mit einem,
3646
+ * der nichts gefunden hat.
3647
+ *
3648
+ * Heinrich am 19.08.2026 zur alten Fassung: "Ich tue mich schwer mit den
3649
+ * 1200 h. Das haette vielleicht frueher zugetroffen, zu
3650
+ * Entwicklungszeiten ohne AI, aber heutzutage nicht mehr. Die erscheint
3651
+ * laecherlich hoch."
3652
+ *
3653
+ * Genau so ist es. Die Annahme dahinter — 30 bis 240 Minuten Recherche
3654
+ * je Lektion — ist der Preis von VOR den Assistenten. Heute findet ein
3655
+ * Modell dasselbe in Sekunden. Die Zahl misst eine Welt, die es nicht
3656
+ * mehr gibt.
3657
+ *
3658
+ * FREMDBELEG dazu (Artikel 4278052, METR-Studie): erfahrene Entwickler
3659
+ * FUEHLTEN sich mit KI schneller und waren gemessen 19 Prozent
3660
+ * langsamer. Selbstauskunft und Messung zeigen in entgegengesetzte
3661
+ * Richtungen — eine geschaetzte Zeitersparnis ist die schnellste Art,
3662
+ * Vertrauen fuer alle anderen Zahlen gleich mit zu verlieren.
3663
+ *
3664
+ * Sie wird nicht geloescht, sondern ZURUECKGESTUFT: sie steht weiterhin
3665
+ * da, aber klein, mit ihrer Annahme im selben Satz, und unter der
3666
+ * gemessenen Liefermenge. Eine erfundene Zahl ALLEIN nervt; eine
3667
+ * gemessene daneben traegt sie.
3668
+ */
3195
3669
  const timeSaved = Math.round(Number(timeSavedRaw ?? 0));
3196
3670
  const timeSavedHuman = timeSaved < 60 ? `${timeSaved} min` : `${(timeSaved / 60).toFixed(1)} h`;
3197
3671
  const lines = [
@@ -3209,10 +3683,24 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
3209
3683
  reusePairs.length > 0 ? `**${reusePairs.length}** distinct reuse relationship${reusePairs.length !== 1 ? 's' : ''} across the team` : '',
3210
3684
  ``,
3211
3685
  `---`,
3212
- `⏱️ **${timeSavedHuman}** total saved not re-researching known fixes.`,
3686
+ // GEMESSEN zuerst, GESCHAETZT darunter — in dieser Reihenfolge, immer.
3687
+ // Vorher stand die gerechnete Stundenzahl ganz oben und fett; die
3688
+ // einzige Zahl ohne Annahme kam gar nicht vor.
3689
+ `### 📦 Aus dem Brain geliefert _(gemessen, nicht gerechnet)_`,
3690
+ lieferungInWorten(lieferung),
3691
+ ``,
3692
+ lieferung.abrufe > 0
3693
+ ? `_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._`
3694
+ : `_Eine **Schätzung** von ${timeSavedHuman} „nicht neu recherchiert" wäre hier gerechnet, nicht gemessen — sie erscheint erst, wenn daneben eine gemessene Zahl steht._`,
3213
3695
  recallsTotal === 0
3214
3696
  ? `_Tip: pass \`author="your-handle"\` to \`smart_recall\` so cross-author reuse can be tracked._`
3215
3697
  : `_These numbers compound: every learned lesson and every teammate raises all three._`,
3698
+ ``,
3699
+ `### 🔧 Werkzeug-Nutzung _(erst messen, dann zusammenlegen)_`,
3700
+ nutzungInWorten(nutzung, TOOLS.length),
3701
+ ...(nutzung.spitze.length > 0
3702
+ ? nutzung.spitze.map((z, i) => ` ${String(i + 1).padStart(2)}. \`${z.name}\` — ${z.aufrufe}× (${Math.round(z.anteil * 100)} %)`)
3703
+ : []),
3216
3704
  ].filter(Boolean);
3217
3705
  return lines.join('\n');
3218
3706
  }