@cachly-dev/mcp-server 0.10.128 → 0.10.130

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 (217) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +1 -7
  3. package/dist/packages/telegram-notify/client.js +2 -75
  4. package/dist/src/ambient-cli.js +7 -253
  5. package/dist/src/ambient-deps.js +1 -55
  6. package/dist/src/ambient-hooks.js +5 -278
  7. package/dist/src/ambient-ledger.js +6 -95
  8. package/dist/src/ambient-memory.js +1 -67
  9. package/dist/src/ambient-recall.js +1 -221
  10. package/dist/src/aussetzer.js +1 -82
  11. package/dist/src/auth.js +9 -140
  12. package/dist/src/autor-abzeichen.js +1 -56
  13. package/dist/src/autor.js +1 -136
  14. package/dist/src/bauabdruck.js +1 -93
  15. package/dist/src/bedeutung.js +2 -323
  16. package/dist/src/bench/cachly-bench.js +4 -249
  17. package/dist/src/bench/external-corpus.js +4 -135
  18. package/dist/src/bench/fixtures.js +1 -225
  19. package/dist/src/brain-export.js +16 -119
  20. package/dist/src/brain-watch-hook.js +8 -86
  21. package/dist/src/cachly-url.js +1 -21
  22. package/dist/src/ckg.js +1 -108
  23. package/dist/src/cls-hook.js +8 -94
  24. package/dist/src/confidence.js +1 -50
  25. package/dist/src/credentials.js +1 -85
  26. package/dist/src/deutsch.js +1 -223
  27. package/dist/src/doctor.js +2 -248
  28. package/dist/src/editor.js +1 -63
  29. package/dist/src/eingaenge-nachruesten.js +1 -290
  30. package/dist/src/eingaenge.js +2 -228
  31. package/dist/src/embeddings.js +13 -223
  32. package/dist/src/feldreparatur.js +3 -0
  33. package/dist/src/first-contact.js +4 -108
  34. package/dist/src/first-recall-proof.js +4 -41
  35. package/dist/src/funnel-milestones.js +2 -48
  36. package/dist/src/geliefert.js +1 -114
  37. package/dist/src/handlers/advanced.js +38 -537
  38. package/dist/src/handlers/brain.js +69 -4181
  39. package/dist/src/handlers/cache.js +59 -638
  40. package/dist/src/handlers/context.js +17 -182
  41. package/dist/src/handlers/fedbrain.js +40 -1436
  42. package/dist/src/handlers/instances.js +16 -201
  43. package/dist/src/handlers/roadmap.js +13 -175
  44. package/dist/src/handlers/share.js +19 -530
  45. package/dist/src/handlers/syndicate.js +38 -904
  46. package/dist/src/handlers/team.js +89 -1850
  47. package/dist/src/handlers/viz.js +3 -148
  48. package/dist/src/index.js +321 -4281
  49. package/dist/src/interferenz.js +1 -0
  50. package/dist/src/kostprobe.js +7 -125
  51. package/dist/src/lesson-preview.js +2 -133
  52. package/dist/src/namespace.js +1 -128
  53. package/dist/src/notifier.js +1 -6
  54. package/dist/src/rangfolge-stellschrauben.js +1 -0
  55. package/dist/src/rangfolge.js +1 -214
  56. package/dist/src/raumkorrektur.js +1 -197
  57. package/dist/src/recall-tiefe.js +1 -42
  58. package/dist/src/rerank.js +1 -154
  59. package/dist/src/resonanz.js +1 -0
  60. package/dist/src/search.js +1 -2104
  61. package/dist/src/seltenheitsbestand.js +1 -92
  62. package/dist/src/speicherlage.js +1 -84
  63. package/dist/src/spuren.js +1 -150
  64. package/dist/src/starter-corpus.js +1 -165
  65. package/dist/src/telemetry-types.js +0 -13
  66. package/dist/src/tools.js +10 -2465
  67. package/dist/src/toolspecs.js +1 -121
  68. package/dist/src/upgrade-nudge.js +1 -29
  69. package/dist/src/utils.js +1 -89
  70. package/dist/src/versuch.js +1 -250
  71. package/dist/src/vorspann.js +1 -146
  72. package/dist/src/wartung/vektoren-verdichten.js +1 -200
  73. package/dist/src/werkzeug-nutzung.js +1 -72
  74. package/dist/src/wertbeitrag.js +1 -73
  75. package/dist/src/wirkungsspur.js +1 -0
  76. package/glama.json +1 -1
  77. package/package.json +8 -7
  78. package/server.json +3 -3
  79. package/dist/index.d.ts +0 -1
  80. package/dist/index.js.map +0 -1
  81. package/dist/packages/telegram-notify/client.d.ts +0 -19
  82. package/dist/packages/telegram-notify/client.js.map +0 -1
  83. package/dist/src/ambient-cli.d.ts +0 -101
  84. package/dist/src/ambient-cli.js.map +0 -1
  85. package/dist/src/ambient-deps.d.ts +0 -26
  86. package/dist/src/ambient-deps.js.map +0 -1
  87. package/dist/src/ambient-hooks.d.ts +0 -137
  88. package/dist/src/ambient-hooks.js.map +0 -1
  89. package/dist/src/ambient-ledger.d.ts +0 -19
  90. package/dist/src/ambient-ledger.js.map +0 -1
  91. package/dist/src/ambient-memory.d.ts +0 -12
  92. package/dist/src/ambient-memory.js.map +0 -1
  93. package/dist/src/ambient-recall.d.ts +0 -127
  94. package/dist/src/ambient-recall.js.map +0 -1
  95. package/dist/src/aussetzer.d.ts +0 -67
  96. package/dist/src/aussetzer.js.map +0 -1
  97. package/dist/src/auth.d.ts +0 -43
  98. package/dist/src/auth.js.map +0 -1
  99. package/dist/src/autor-abzeichen.d.ts +0 -41
  100. package/dist/src/autor-abzeichen.js.map +0 -1
  101. package/dist/src/autor.d.ts +0 -101
  102. package/dist/src/autor.js.map +0 -1
  103. package/dist/src/bauabdruck-daten.generiert.json +0 -5
  104. package/dist/src/bauabdruck.d.ts +0 -38
  105. package/dist/src/bauabdruck.js.map +0 -1
  106. package/dist/src/bedeutung.d.ts +0 -168
  107. package/dist/src/bedeutung.js.map +0 -1
  108. package/dist/src/bench/cachly-bench.d.ts +0 -61
  109. package/dist/src/bench/cachly-bench.js.map +0 -1
  110. package/dist/src/bench/external-corpus.d.ts +0 -44
  111. package/dist/src/bench/external-corpus.js.map +0 -1
  112. package/dist/src/bench/fixtures.d.ts +0 -37
  113. package/dist/src/bench/fixtures.js.map +0 -1
  114. package/dist/src/brain-export.d.ts +0 -64
  115. package/dist/src/brain-export.js.map +0 -1
  116. package/dist/src/brain-watch-hook.d.ts +0 -21
  117. package/dist/src/brain-watch-hook.js.map +0 -1
  118. package/dist/src/cachly-url.d.ts +0 -68
  119. package/dist/src/cachly-url.js.map +0 -1
  120. package/dist/src/ckg.d.ts +0 -54
  121. package/dist/src/ckg.js.map +0 -1
  122. package/dist/src/cls-hook.d.ts +0 -15
  123. package/dist/src/cls-hook.js.map +0 -1
  124. package/dist/src/confidence.d.ts +0 -19
  125. package/dist/src/confidence.js.map +0 -1
  126. package/dist/src/credentials.d.ts +0 -37
  127. package/dist/src/credentials.js.map +0 -1
  128. package/dist/src/deutsch.d.ts +0 -98
  129. package/dist/src/deutsch.js.map +0 -1
  130. package/dist/src/doctor.d.ts +0 -47
  131. package/dist/src/doctor.js.map +0 -1
  132. package/dist/src/editor.d.ts +0 -9
  133. package/dist/src/editor.js.map +0 -1
  134. package/dist/src/eingaenge-nachruesten.d.ts +0 -41
  135. package/dist/src/eingaenge-nachruesten.js.map +0 -1
  136. package/dist/src/eingaenge.d.ts +0 -135
  137. package/dist/src/eingaenge.js.map +0 -1
  138. package/dist/src/embeddings.d.ts +0 -48
  139. package/dist/src/embeddings.js.map +0 -1
  140. package/dist/src/first-contact.d.ts +0 -56
  141. package/dist/src/first-contact.js.map +0 -1
  142. package/dist/src/first-recall-proof.d.ts +0 -19
  143. package/dist/src/first-recall-proof.js.map +0 -1
  144. package/dist/src/funnel-milestones.d.ts +0 -5
  145. package/dist/src/funnel-milestones.js.map +0 -1
  146. package/dist/src/geliefert.d.ts +0 -63
  147. package/dist/src/geliefert.js.map +0 -1
  148. package/dist/src/handlers/advanced.d.ts +0 -6
  149. package/dist/src/handlers/advanced.js.map +0 -1
  150. package/dist/src/handlers/brain.d.ts +0 -41
  151. package/dist/src/handlers/brain.js.map +0 -1
  152. package/dist/src/handlers/cache.d.ts +0 -6
  153. package/dist/src/handlers/cache.js.map +0 -1
  154. package/dist/src/handlers/context.d.ts +0 -6
  155. package/dist/src/handlers/context.js.map +0 -1
  156. package/dist/src/handlers/fedbrain.d.ts +0 -18
  157. package/dist/src/handlers/fedbrain.js.map +0 -1
  158. package/dist/src/handlers/instances.d.ts +0 -33
  159. package/dist/src/handlers/instances.js.map +0 -1
  160. package/dist/src/handlers/roadmap.d.ts +0 -6
  161. package/dist/src/handlers/roadmap.js.map +0 -1
  162. package/dist/src/handlers/share.d.ts +0 -8
  163. package/dist/src/handlers/share.js.map +0 -1
  164. package/dist/src/handlers/syndicate.d.ts +0 -6
  165. package/dist/src/handlers/syndicate.js.map +0 -1
  166. package/dist/src/handlers/team.d.ts +0 -45
  167. package/dist/src/handlers/team.js.map +0 -1
  168. package/dist/src/handlers/viz.d.ts +0 -6
  169. package/dist/src/handlers/viz.js.map +0 -1
  170. package/dist/src/index.d.ts +0 -93
  171. package/dist/src/index.js.map +0 -1
  172. package/dist/src/kostprobe.d.ts +0 -25
  173. package/dist/src/kostprobe.js.map +0 -1
  174. package/dist/src/lesson-preview.d.ts +0 -81
  175. package/dist/src/lesson-preview.js.map +0 -1
  176. package/dist/src/namespace.d.ts +0 -20
  177. package/dist/src/namespace.js.map +0 -1
  178. package/dist/src/notifier.d.ts +0 -5
  179. package/dist/src/notifier.js.map +0 -1
  180. package/dist/src/rangfolge.d.ts +0 -169
  181. package/dist/src/rangfolge.js.map +0 -1
  182. package/dist/src/raumkorrektur.d.ts +0 -100
  183. package/dist/src/raumkorrektur.js.map +0 -1
  184. package/dist/src/recall-tiefe.d.ts +0 -48
  185. package/dist/src/recall-tiefe.js.map +0 -1
  186. package/dist/src/rerank.d.ts +0 -48
  187. package/dist/src/rerank.js.map +0 -1
  188. package/dist/src/search.d.ts +0 -153
  189. package/dist/src/search.js.map +0 -1
  190. package/dist/src/seltenheitsbestand.d.ts +0 -53
  191. package/dist/src/seltenheitsbestand.js.map +0 -1
  192. package/dist/src/speicherlage.d.ts +0 -51
  193. package/dist/src/speicherlage.js.map +0 -1
  194. package/dist/src/spuren.d.ts +0 -97
  195. package/dist/src/spuren.js.map +0 -1
  196. package/dist/src/starter-corpus.d.ts +0 -28
  197. package/dist/src/starter-corpus.js.map +0 -1
  198. package/dist/src/telemetry-types.d.ts +0 -87
  199. package/dist/src/telemetry-types.js.map +0 -1
  200. package/dist/src/tools.d.ts +0 -2933
  201. package/dist/src/tools.js.map +0 -1
  202. package/dist/src/toolspecs.d.ts +0 -43
  203. package/dist/src/toolspecs.js.map +0 -1
  204. package/dist/src/upgrade-nudge.d.ts +0 -15
  205. package/dist/src/upgrade-nudge.js.map +0 -1
  206. package/dist/src/utils.d.ts +0 -34
  207. package/dist/src/utils.js.map +0 -1
  208. package/dist/src/versuch.d.ts +0 -179
  209. package/dist/src/versuch.js.map +0 -1
  210. package/dist/src/vorspann.d.ts +0 -73
  211. package/dist/src/vorspann.js.map +0 -1
  212. package/dist/src/wartung/vektoren-verdichten.d.ts +0 -50
  213. package/dist/src/wartung/vektoren-verdichten.js.map +0 -1
  214. package/dist/src/werkzeug-nutzung.d.ts +0 -62
  215. package/dist/src/werkzeug-nutzung.js.map +0 -1
  216. package/dist/src/wertbeitrag.d.ts +0 -57
  217. package/dist/src/wertbeitrag.js.map +0 -1
@@ -1,4181 +1,69 @@
1
- import { execSync } from 'node:child_process';
2
- import { createRequire } from 'node:module';
3
- import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES, CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS } from '../confidence.js';
4
- import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge, ckgUpsertPersonNode, ckgUpsertFileNode, ckgRecordCollaboration, ckgUpsertServiceNode } from '../ckg.js';
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
- }
38
- import { getRole, ROLE_BADGE, getScopes, lessonVisibleToScope, reviewModeEnabled, storeLessonProposal } from './team.js';
39
- import { keywordSearch, wortindexEntwerten, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
40
- import { rerankByQuality, qualityMultiplier, extractLessonQuality } from '../rerank.js';
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();
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';
69
- import { cachlyUrl } from '../cachly-url.js';
70
- // ── Changelog (shown once per version in session_start) ──────────────────────
71
- // Resolve the package version at runtime from package.json so the session briefing
72
- // `version` field always matches the published npm version. A hardcoded constant
73
- // silently drifted (0.10.119 vs published 0.10.122), making new-feature adoption
74
- // impossible to track. Paths cover both prod (dist/handlers/brain.js → ../../../) and
75
- // dev (src/handlers/brain.ts → ../../../). Falls back to a literal if resolution fails.
76
- const CURRENT_VERSION = (() => {
77
- try {
78
- const req = createRequire(import.meta.url);
79
- for (const rel of ['../../../package.json', '../../package.json']) {
80
- try {
81
- const pkg = req(rel);
82
- if (pkg.name === '@cachly-dev/mcp-server' && pkg.version)
83
- return pkg.version;
84
- }
85
- catch { /* try next candidate */ }
86
- }
87
- }
88
- catch { /* fall through to literal */ }
89
- return '0.10.100';
90
- })();
91
- // Resolve the total MCP tools count at runtime from generated capabilities.
92
- // This count is shown in session_start's What's-New briefing for the current version.
93
- // Falls back silently per CONTRACT: better to skip the count than show a wrong number.
94
- const getTotalMcpTools = () => {
95
- try {
96
- const req = createRequire(import.meta.url);
97
- const caps = req('../../../CACHLY_CAPABILITIES.json');
98
- return caps.mcp?.total_tools ?? null;
99
- }
100
- catch { /* fail silently */ }
101
- return null;
102
- };
103
- const WHATS_NEW = {
104
- '0.10.119': [
105
- `🔍 **Explainable briefings — provenance in every warning**`,
106
- ` 📎 \`/briefing\` warnings now carry \`outcome\`, \`author\`, \`learned_at\` and \`matched_on\` (the context tokens that triggered the warning) — editors can show WHY a fix hint fired, not just what it claims.`,
107
- ` 🧰 VS Code 0.12.0 uses this: severity + confidence in the toast, full lesson card with provenance, "Not helpful" per-file suppression, reviewable CLS auto-learns.`,
108
- ` 🧹 CI docs now describe exactly what ships: scan mode uses the REST \`/scan\` endpoint, the GitLab template covers learn/scan/confirm.`,
109
- ],
110
- '0.10.115': [
111
- `🔧 **\`brain_get_pref\` + \`brain_set_pref\` — persistent Brain preferences**`,
112
- ` 📖 \`brain_get_pref\` reads back any stored preference (omit key to list all).`,
113
- ` ✏️ \`brain_set_pref\` saves a preference to Redis — survives restarts, scoped per Brain instance.`,
114
- ` 💡 Known key: \`auto_changelog\` — set to "false" to silence the new-lessons digest at session_start.`,
115
- ` 📊 140 MCP tools`,
116
- ],
117
- '0.10.114': [
118
- `📋 **\`brain_changelog\` — weekly knowledge digest in one call**`,
119
- ` 📅 Generates a grouped Markdown changelog of lessons learned in the last N days.`,
120
- ` 👥 Grouped by topic category, annotated with author, recall count, and confidence — paste directly into standup or Slack.`,
121
- ` ⚙️ Params: \`days\` (default 7), \`max_lessons\` (default 30), \`include_failures\` (default true).`,
122
- ` 📊 138 MCP tools`,
123
- ],
124
- '0.10.113': [
125
- `📈 **Week-over-week savings trend in \`cache_stats\`**`,
126
- ` 📅 \`cache_stats\` now shows **7-day hits** + **WoW trend %** (e.g. \`+23.0% vs prev week\`) so ROI momentum is visible, not just the cumulative total.`,
127
- ` ⬇️ Goes red (📉) on drops, green (📈) on growth — first week always shows +100% (new).`,
128
- ` 🔧 Backed by new \`CountHitsInWindow\` repo method + \`hits_last_7d\`, \`hits_prev_7d\`, \`week_over_week_pct\` fields in \`SavingsEstimate\`.`,
129
- ` 📊 137 MCP tools`,
130
- ],
131
- '0.10.112': [
132
- `⚙️ **Configurable cost_per_call_usd — accurate ROI for any LLM**`,
133
- ` 💡 New \`set_cost_per_call\` tool: set your real per-call cost (claude-opus-4.8 → $0.02, gpt-5.5 → $0.015, claude-sonnet-4.6 → $0.009, claude-haiku-4.5 → $0.001) so savings estimates in \`cache_stats\` reflect your actual bill.`,
134
- ` 🔧 New API: \`PUT /api/v1/instances/:id/cost-per-call\` — persisted per instance, used by \`cache_stats\`, org savings, and SSE analytics stream.`,
135
- ` 📊 137 MCP tools`,
136
- ],
137
- '0.10.111': [
138
- `🏢 **Org-wide ROI + GitLab CI — the CFO number, aggregated**`,
139
- ` 💰 \`cache_org_stats\` now shows **org-wide Tokenmaxxing ROI**: total savings summed across every instance in your org, monthly projection, top-5 instances by savings. One number for the whole team.`,
140
- ` 🔌 Backed by new API endpoint \`GET /api/v1/orgs/:id/savings\` (org members only)`,
141
- ` 🦊 GitLab CI integration: \`brain-from-ci-gitlab.yml\` — two \`.post\` jobs (on_success/on_failure), same helper script, pipeline never fails because of Brain push`,
142
- ` 🛠️ GitHub Action template fixed: \`secrets.*\` in job-level \`if\` is invalid — guard removed (helper script skips cleanly on missing secrets)`,
143
- ` 📊 136 MCP tools`,
144
- ],
145
- '0.10.110': [
146
- `🏢 **Zero friction: ROI-Projektor, Org-Shared-Cache, CI ready-to-paste**`,
147
- ` 📈 \`cache_stats\` — zero-hit projection calculator: shows expected monthly savings for 5/10/20-dev teams before a single cache hit lands. ROI visible from day 1.`,
148
- ` 🏢 New \`cache_org_stats\` — shared org-level cache stats across multiple instances. Convention-based (\`org:{id}:sem\`), zero API changes needed.`,
149
- ` 🔗 \`cache_set\`/\`cache_get\` now accept optional \`org_id\` — write to personal + org namespace, fall back to org on miss. Fully backwards-compatible.`,
150
- ` 📋 Copy-paste CI integration: \`brain-from-ci-action.yml\` → Brain self-calibrates confidence on every GitHub Actions run. No config beyond two repo secrets.`,
151
- ` 📊 136 MCP tools`,
152
- ],
153
- '0.10.109': [
154
- `💰 **Tokenmaxxing ROI + Closed-loop CI learning**`,
155
- ` 📊 \`cache_stats\` now shows a **Tokenmaxxing ROI** section — total tokens saved, monthly cost projection, and top 3 most-cached prompts. Finally a number you can show the CFO.`,
156
- ` 🔄 New \`brain_from_ci\` — bulk-ingest CI history into the Brain (red→green = learned fix, green→red = CKG causal edge). The brain_from_git equivalent for CI. Bootstrap the Brain from your CI logs in one call.`,
157
- ` 📊 134 MCP tools`,
158
- ],
159
- '0.10.108': [
160
- `🧠 **Focused briefing + trustworthy autopilot**`,
161
- ` 🎯 New \`session_start_summary\` — top-N focused briefing by topic. Scores lessons by relevance × confidence + recall_count + severity + recency. Use it for a quick pre-brief without loading the full Brain.`,
162
- ` 🤝 Autopilot template now explains the data flow upfront ("your own Brain, EU-hosted, no third parties") — no more friction with cautious AI assistants during onboarding`,
163
- ` 📊 132 MCP tools`,
164
- ],
165
- '0.10.107': [
166
- `🎯 **Recall@3 gap closed — the moat proof got sharper (the head-to-head that matters)**`,
167
- ` 🔍 External-corpus Recall@3 **82.1% → 98.2%** · Precision@1 **80.4%** · MRR **89.0%** — now ahead of flat-file memory on every metric`,
168
- ` 🧬 Root-cause fix: documents are no longer indexed with cross-lingual synonyms (the symmetric-expansion bug inflated any doc containing a common word ~28×, burying topic-specific lessons). Queries still expand — a 日本語 query still finds English lessons.`,
169
- ` ⚖️ Reranker now compresses BM25 with score^0.3 before applying lesson quality, so a proven success can overtake a text-similar failed attempt even when the distractor scores 5–6× higher in raw BM25`,
170
- ` 🛡️ CI bench gate widened to both home + external corpora with committed floors (\`npm run bench:gate\`) — recall quality can never silently regress again`,
171
- ` 📊 121 MCP tools`,
172
- ],
173
- '0.10.98': [
174
- `🌐 **Model-Neutrality as Feature (W9) — "Bring your own model, keep your brain."**`,
175
- ` 🔌 New \`brain_portability()\` — shows your Brain ID, all compatible MCP clients, and ready-to-paste config snippets for each; proves the same Brain works in Claude, Cursor, Copilot, Windsurf, Cline, Zed and more`,
176
- ` 🧠 Your lessons, crystals, and predictions are model-agnostic — no vendor lock-in at the memory layer`,
177
- ` 📖 New docs: cachly.dev/docs/model-neutral — "Same Brain, Any Model" guide with live config examples`,
178
- ` 📊 121 MCP tools (new: \`brain_portability\`)`,
179
- ],
180
- '0.10.97': [
181
- `💠 **Team Crystallize (W8) — the team-wide, causal counter to per-user "Dreaming"**`,
182
- ` 🧬 New \`team_crystallize()\` — surfaces fixes that 2+ teammates *independently converged on*, the one thing a single-user memory structurally can't build`,
183
- ` 🧩 Cross-person clustering: groups structurally similar problems across people & namespaces (e.g. "pool exhaustion" solved the same way in payments, auth & db)`,
184
- ` 👥 Each pattern names who converged — instant "ask @alice and @bob, they both hit this"`,
185
- ` 💎 Shows up in \`crystal_view\` alongside your per-brain crystal`,
186
- ` 📊 120 MCP tools (new: \`team_crystallize\`)`,
187
- ],
188
- '0.10.96': [
189
- `🔐 **Roles & Team-Scopes now ENFORCED (W6) — the enterprise governance layer**`,
190
- ` 🛡️ \`team_confirm\` is now reviewer-gated, \`team_learn\` blocks viewers (recall-only) — roles stop being cosmetic the moment an admin is assigned`,
191
- ` 📜 New \`team_audit\` tool — immutable, admin-only trail of every role change & lesson confirmation, for compliance & security reviews`,
192
- ` 🔓 Zero impact on open teams: governance activates only once you bootstrap an admin; before that everything works exactly as before`,
193
- ` 📊 119 MCP tools (new: \`team_audit\`)`,
194
- ],
195
- '0.10.95': [
196
- `🧠 **Domain Brain Marketplace (W10) — bootstrap your Brain with curated community knowledge**`,
197
- ` 🛒 \`brain_marketplace()\` — browse installable packs of high-trust lessons by domain: ☸️ Kubernetes, 🔐 Auth, 🗄️ Database, ⚛️ React, 💳 Payments & more`,
198
- ` 📦 \`brain_install(slug="k8s")\` — merges a domain's verified lessons into your Brain; live in \`smart_recall\` instantly, even offline`,
199
- ` 🛡️ Idempotent + non-destructive: installed lessons are tagged \`source:"marketplace:<slug>"\` and NEVER override your own`,
200
- ` 🔌 New gRPC \`Subscribe\` RPC — agents get new lessons streamed live the moment they're learned (completes the M2M loop with \`Learn\`)`,
201
- ` 📊 118 MCP tools (new: \`brain_marketplace\`, \`brain_install\`)`,
202
- ],
203
- '0.10.94': [
204
- `🤝 **Person↔Person Collaboration Graph (W5) — "Frag X und Y, die haben das zusammen gelöst"**`,
205
- ` 🕸️ New \`brain_collab_pairs()\` tool: shows every contributor pair who've touched the same files or recalled each other's lessons — ideal for onboarding and bus-factor analysis`,
206
- ` 📡 \`smart_recall\` now wires CKG \`collaborates\` edges live when cross-author reuse fires — collaboration graph grows with every team recall`,
207
- ` ⚠️ Bus-factor section: flags solo contributors whose knowledge no teammate has yet recalled`,
208
- ` 📊 116 MCP tools (new: \`brain_collab_pairs\`)`,
209
- ],
210
- '0.10.93': [
211
- `🗺️ **\`brain_plan\` — your Brain goes from "what did we learn?" to "what should I do?"**`,
212
- ` 🧭 Give it a task you're about to start ("upgrade Postgres 14→16") and it returns an ordered, grounded action plan: ranked failure modes to avoid, proven fix steps (dependency-aware, with commands), and a pre-flight checklist — all from your own lessons`,
213
- ` 🔬 Generative layer on the CKG: where \`brain_predict\` answers "what might fail?", \`brain_plan\` answers "what, in what order?"`,
214
- ` 📊 115 MCP tools (new: \`brain_plan\`)`,
215
- ],
216
- '0.10.92': [
217
- `👋 **Welcome-back digest — re-entry after a week away now leads with what changed**`,
218
- ` 🌱 \`session_start\` detects a gap ≥ 7 days and surfaces how many lessons your Brain gained while you were gone (git, CI & teammates kept it learning), with the freshest topics`,
219
- ` 🧠 Zero extra latency — the digest is computed from data session_start already loads`,
220
- ` 🔌 New gRPC \`Learn\` RPC (M2M write path) — agents & CI can now teach the Brain directly, byte-compatible with the MCP lesson format`,
221
- ` 📊 115 MCP tools`,
222
- ],
223
- '0.10.91': [
224
- `🕐 **Lesson-anchored time attribution — every recall shows when it was learned and how much it saves**`,
225
- ` 💡 "Brain saved you" banner now includes: when the lesson was learned (e.g. "12 May"), estimated original debug cost by severity (critical=2h, major=1h, minor=20min), and team attribution when a teammate's lesson fires`,
226
- ` 📅 Origin date formatted as "12 May" (same year) or "12 May 2024" (different year) — no clutter`,
227
- ` 👥 Cross-author recalls now show "@author" on the same banner line — the "team knowledge reuse" value is front-and-center`,
228
- ` 📊 115 MCP tools`,
229
- ],
230
- '0.10.90': [
231
- `📊 **External benchmark corpus v2 — the proof is now statistically solid**`,
232
- ` 🧪 Expanded from 8 queries → 61 lessons / 56 queries across 10 real-world engineering domains (k8s, DB, auth, CI, frontend, API, payments, observability, network, Node.js, security, infra)`,
233
- ` ⚔️ 10 adversarial distractors added — failure lessons that share the exact same vocabulary as the query, forcing the quality reranker to earn its lift`,
234
- ` 📈 Results on the larger corpus: cachly +2.9% Precision@1 vs flat-file, +12.5% vs raw BM25 baseline — on a harder dataset, not an easy one`,
235
- ` 📊 115 MCP tools`,
236
- ],
237
- '0.10.89': [
238
- `⚡ **One-command onboarding — \`autopilot\` is now the single canonical setup**`,
239
- ` 🚀 Every surface (README, docs, emails, dashboard, blog) now leads with \`npx @cachly-dev/mcp-server@latest autopilot\` — one command signs you in, configures every editor, and bootstraps from git`,
240
- ` 🧹 Retired the split-brain instructions (legacy \`@cachly-dev/init\` wrapper + the old "three steps") so there's exactly one thing to copy-paste`,
241
- ` 🔧 Fixed a build-breaking duplicate \`export\` on buildClaudeMdBlock that failed CI on v0.10.88`,
242
- ` 🔢 Version hygiene — package.json, server.json, lockfile and MCP_VERSION resynced (server.json/lockfile were stuck at 0.10.85)`,
243
- ` 📊 115 MCP tools`,
244
- ],
245
- '0.10.88': [
246
- `✅ **Honest setup + tested config writer (v0.10.88)**`,
247
- ` 🔒 setup/autopilot now report the truth: if no editor config could be written it exits non-zero with the exact failures instead of falsely claiming "Brain is ready"`,
248
- ` 📊 New setup_config_write_failed telemetry — permission/path failures that block activation are now visible in reporting`,
249
- ` 🧪 24 new unit tests for the config writer (buildServerEnv, buildMcpConfig, mergeMcpConfig, buildClaudeMdBlock) — the merge logic that must never clobber your other MCP servers is now regression-guarded`,
250
- ` 📊 115 MCP tools`,
251
- ],
252
- '0.10.87': [
253
- `🌐 **Multi-editor auth persistence + full activation telemetry (v0.10.87)**`,
254
- ` ✅ JWT + instance_id now persisted to Cursor (\`~/.cursor/mcp.json\`) and Windsurf configs on auth — no more infinite re-auth for non-Claude-Code users`,
255
- ` 📡 \`device_flow_failed(reason="timeout")\` now fires when sign-in window expires — blind spot in the funnel is gone`,
256
- ` 📡 \`auto_provision_failed\` now fires on network errors too (was only HTTP non-2xx)`,
257
- ` 📡 \`brain_from_git_failed\` fires with error reason when auto-bootstrap throws — no more empty brain with no explanation`,
258
- ` ✨ Git bootstrap shows lesson count in session_start: "Brain bootstrapped — 23 lessons loaded" — the WOW moment`,
259
- ` 🔧 API: McpEvent stores brain quality metrics (fixes/features/refactors/total) — git learning quality now measurable`,
260
- ` 📊 115 MCP tools`,
261
- ],
262
- '0.10.86': [
263
- `🛡️ **Zero-friction activation — your Brain self-heals into existence**`,
264
- ` 🔧 Auto-provision now runs on *any* tool call, not just at sign-in — if instance creation ever fails once, the next call recovers it automatically`,
265
- ` 🤝 Parallel tool calls on startup are coalesced into one resolve (no more duplicate-instance races)`,
266
- ` 💬 Clearer "instance still starting" message — leads with "just retry", no manual config needed`,
267
- ` 📈 New activation-funnel telemetry: install → sign-in → provision, so silent churn is visible`,
268
- ` 📊 115 MCP tools`,
269
- ],
270
- '0.10.85': [
271
- `🚀 **One-command onboarding — \`npx @cachly-dev/mcp-server@latest autopilot\`**`,
272
- ` ⚡ Single command does it all: auth → instance → every editor config → CLAUDE.md → Brain bootstrap → health`,
273
- ` 🌱 Seeds the 16-lesson starter corpus when git history is empty — your first smart_recall hits instantly`,
274
- ` 🤖 Fully automatic (zero prompts); \`setup\` remains for interactive, pick-your-editors onboarding`,
275
- ],
276
- '0.10.84': [
277
- `🤖 **M2M & agent-ecosystem reach — cachly for every caller, human or machine**`,
278
- ` 🔑 OAuth2 \`client_credentials\` grant — set CACHLY_CLIENT_ID + CACHLY_CLIENT_SECRET for fully headless auth (CI, agents, AI-to-AI)`,
279
- ` 🗂️ \`npx ... tool-specs --format=openai|anthropic|langchain\` — export all 121 tools in any framework's dialect`,
280
- ` 🌐 \`npx ... openapi\` — OpenAPI 3.1 doc (1 POST path per tool) for Assistants / codegen / Postman`,
281
- ` 🧪 14 new tests: client_credentials helpers, all four spec dialects, OpenAPI required-body inference`,
282
- ],
283
- '0.10.83': [
284
- `🧠 **Brain Viz — 3D graph export**`,
285
- ` 🌐 \`brain_graph(instance_id)\` — exports the Causal Knowledge Graph as a render-ready {nodes, links} payload`,
286
- ` 🎨 Schema \`cachly.brain_graph/v1\`: node kinds (concept/person/file/service) with color groups + size, edges with confidence`,
287
- ` 🖥️ Consumed verbatim by the 3D frontend (react-force-graph-3d / three.js) — the visual brain map`,
288
- ` 🔎 Filters: \`domain\`, \`min_confidence\`, \`max_nodes\` (with truncation flag), \`format="summary"\``,
289
- ` 🧪 10 tests: kind detection, dangling-edge pruning, confidence/domain filters, schema contract`,
290
- ` 📊 115 MCP tools`,
291
- ],
292
- '0.10.82': [
293
- `📡 **Telemetry API contracts — dashboard ingest pipeline prepared**`,
294
- ` 🗂️ \`src/telemetry-types.ts\` — canonical event types + resolved event format (TelemetryEventRaw / TelemetryEventResolved)`,
295
- ` 🔐 \`user_fingerprint\` — non-reversible JWT sub hash, PII-free user clustering for dashboard`,
296
- ` 📊 \`metrics\` block on key events: smart_recall (hit/topic), brain_from_git (fixes/features/total), brain_seed_starter (seeded_count)`,
297
- ` 🚦 Internal API contract documented: GET /internal/telemetry/stream → TelemetryEventResolved[]`,
298
- ],
299
- '0.10.81': [
300
- `⏱️ **Onboarding-Bench — time-to-first-recall, now measured**`,
301
- ` 📐 \`npm run bench:onboarding\` — cold (empty) vs seeded (starter) first-query hit rate via the real search engine`,
302
- ` 📊 Result: first-query hit@1 **0% → 87.5%**, hit@3 **0% → 100%**, MRR **0% → 93.8%**`,
303
- ` 🛡️ CI-defended in \`onboarding-bench.test.ts\` (cold must answer 0%, seeded thresholds enforced)`,
304
- ` 📋 \`PLAN-DASHBOARD.md\` — full build spec for the external Team-Knowledge-Reuse dashboard (separate repo)`,
305
- ],
306
- '0.10.80': [
307
- `🌱 **Starter corpus — first recall in seconds, not sessions**`,
308
- ` 📚 \`brain_seed_starter(instance_id)\` — seed 16 curated universal lessons (Docker cache, JWT skew, K8s OOM, N+1, cache stampede…)`,
309
- ` 🚀 Auto-seeds on first session when git history is empty/shallow — your first \`smart_recall\` returns a real hit`,
310
- ` 🛡️ Tagged \`source:"starter"\`, idempotent, never overrides your own lessons`,
311
- ` ⏱️ Directly attacks time-to-first-recall (<2 min goal); stamps \`born_at\` on seed`,
312
- ` 🧪 18 new tests: seeding, idempotency, topic_filter, force, user-lesson protection`,
313
- ` 📊 113 MCP tools`,
314
- ],
315
- '0.10.79': [
316
- `🌐 **Brain Marketplace — full shareable Brain lifecycle**`,
317
- ` 📋 \`brain_share_list(instance_id)\` — see all your public shares with lesson counts + share IDs`,
318
- ` 🗑️ \`brain_unshare(share_id)\` — revoke a share; link goes dead immediately`,
319
- ` 🔍 \`brain_discover(query, topic)\` — search public Brains in the cachly marketplace`,
320
- ` 📤 \`publish\` CLI — fancy card + share URL; \`--public\` for discoverable; \`--title\` custom name`,
321
- ` 🧪 stress tests: team_expertise_map with 50 contributors, chain import/unshare, discover fallback`,
322
- ` 📊 112 MCP tools`,
323
- ],
324
- '0.10.78': [
325
- `🌐 **Phase 3: Shareable / Public Brains**`,
326
- ` 📤 \`brain_share(instance_id="...")\` — export a Brain snapshot as a shareable link (public or unlisted)`,
327
- ` 📥 \`brain_import(instance_id="...", share_id="...")\` — import any public Brain into yours (1-line)`,
328
- ` 🔍 Optional topic_filter, min_confidence, topic_prefix — precise control over what gets shared/imported`,
329
- ` 🔒 dry_run mode on both tools — preview before committing`,
330
- ` 🧪 Regression suite added: share handler, buildServerEnv, stdout cleanness`,
331
- ` 📊 109 MCP tools`,
332
- ],
333
- '0.10.77': [
334
- `🩹 **Frictionless onboarding + first-class self-hosting/BYOK**`,
335
- ` 🚀 Editor-launched server now serves all tools even before sign-in — zero-credential device flow finally triggers on first tool call`,
336
- ` 🔒 No more stdout pollution: stdio JSON-RPC stream stays clean when no JWT is set`,
337
- ` 🏠 \`setup --api-url\` / \`init --api-url\` point at a self-hosted backend; URL baked into config only when non-default`,
338
- ` 🧬 \`health\` now shows your BYOK embedding provider + accepts cky_ long-lived keys (was a false "invalid token" failure)`,
339
- ],
340
- '0.10.76': [
341
- `📊 **Agent-trace benchmark + editor matrix + setup timer**`,
342
- ` 📂 New \`agent-traces-corpus.json\` — 22 lessons, 15 queries, realistic AI-agent debug patterns`,
343
- ` 🏆 Benchmark result: **P@1 +66.7%, MRR +9.5% vs flat-file** (adversarial failure-distractors per category)`,
344
- ` 📋 README: first-class editor support matrix (Claude Code, Cursor, Windsurf, VSCode, Cline, Continue.dev, Zed)`,
345
- ` ⏱️ setup now shows elapsed time + "Brain is ready" banner at the end`,
346
- ],
347
- '0.10.75': [
348
- `🩹 **Setup hardened — works from the VSCode plugin & any non-TTY terminal**`,
349
- ` 🔌 \`setup\` no longer hangs on a non-interactive stdin (VSCode tasks, CI) — auto-detects + runs in automatic mode`,
350
- ` 🔐 Device-flow now uses the API proxy first (the old direct-Keycloak call returned 403) — sign-in actually completes`,
351
- ` 🌐 When auto sign-in is unavailable, the browser opens with a step-by-step API-key guide instead of a silent paste prompt`,
352
- ` 📊 Funnel fix: \`device_flow_completed\` / \`device_flow_failed(reason)\` now emitted from setup too`,
353
- ],
354
- '0.10.73': [
355
- `🔐 **Team visibility scopes + one-command init + external bench**`,
356
- ` 🆕 \`team_grant_scope\` / \`team_scopes\` — scope lessons to a sub-team (group="security"); only members + admins recall them`,
357
- ` 🏷️ \`learn_from_attempts(group="...")\` — orthogonal to private; smart_recall enforces it per requester`,
358
- ` ⚡ \`init\` is now zero-arg + idempotent (reuses saved creds, only writes what changed, <60s)`,
359
- ` 📦 \`npm run bench:external\` — prove recall-lift on a third-party-labeled corpus (portable JSON format)`,
360
- ` 📊 107 MCP tools`,
361
- ],
362
- '0.10.72': [
363
- `👑 **Role model — admin · reviewer · contributor · viewer (Phase 3)**`,
364
- ` 🆕 \`team_assign_role\` — establish governance; first call bootstraps, admins manage the rest`,
365
- ` 🆕 \`team_whoami\` — see your own role and capabilities`,
366
- ` 🆕 \`team_roster\` — full team role table (👑🛡️✏️👁️)`,
367
- ` 🛡️ \`team_confirm\` is now role-aware — admin/reviewer auto-get senior weight; contributor gets peer; no self-promotion`,
368
- ` 👑 Setup CLI now prompts for governance bootstrap (idempotent). 105 MCP tools`,
369
- ],
370
- '0.10.71': [
371
- `🛡️ **Self-healing auth** — no more silent "0 recalls because the token quietly died"`,
372
- ` ♻️ Near-expiry tokens auto-refresh into a long-lived key while still valid (zero interaction)`,
373
- ` 🔁 A rejected (401) call self-heals once and retries before surfacing an error`,
374
- ` 📣 If the credential is truly dead, session_start + get_api_status say so plainly + how to fix`,
375
- ],
376
- '0.10.70': [
377
- `🛰️ **Service/System nodes in the graph (Phase 3)** — the brain now models running systems`,
378
- ` 🆕 \`brain_service_map(service="prometheus")\` — who owns it + every failure/fix known`,
379
- ` 🏷️ Tag lessons with \`service="..."\` (\`service_kind="system"\` for infra) — person→operates, file→runs_in edges`,
380
- ` 🚨 Incident triage: a restarting pod → instant "who knows this and what's broken before"`,
381
- ` 📊 102 MCP tools`,
382
- ],
383
- '0.10.69': [
384
- `📁 **Personalized context-aware recall (Phase 3)** — pass \`context_files\` to \`smart_recall\``,
385
- ` 🎯 Lessons learned on your current files bubble up — even when the query doesn't name the file`,
386
- ` 🏷️ \`📁 context match\` badge on every file-boosted result so you know WHY it ranked up`,
387
- ` ✅ Works alongside keyword, semantic, CKG, and governance signals — a 6th ranking dimension`,
388
- ],
389
- '0.10.68': [
390
- `🤝 **Collaboration graph (Phase 3)** — person↔person edges from shared files`,
391
- ` 👥 \`brain_who_knows\` now shows who the top expert frequently works with`,
392
- ` 🚌 Bus-factor insight: "ask X and Y together — they solved this on shared files"`,
393
- ` 🛡️ Stability: every network call in the agent hot path is now timeout-bounded`,
394
- ],
395
- '0.10.64': [
396
- `📈 **The three decisive metrics, now measurable**`,
397
- ` ⏱️ \`brain_metrics()\` — time-to-first-recall, recall-lift, team-knowledge-reuse in one view`,
398
- ` 👥 Cross-author reuse tracked: recall a teammate's lesson → counted + surfaced inline`,
399
- ` 🛡️ Stability: scans now capped + timed out (a huge keyspace can't hang the agent)`,
400
- ],
401
- '0.10.61': [
402
- `🎯 **Phase 3C: 100 MCP tools milestone — zero-setup knowledge graph**`,
403
- ` 🔄 \`brain_from_git\` now auto-builds Person+File nodes from git history (zero setup!)`,
404
- ` 🔍 \`skill_gaps()\` — find domains with unresolved failures, missing attribution`,
405
- ` 📊 \`brain_coverage()\` — scored 0-100 health report: lessons · attribution · file coverage`,
406
- ],
407
- '0.10.60': [
408
- `🆕 **Phase 3B: File map + team overview + visibility**`,
409
- ` 📁 \`brain_file_map(file_paths=[...])\` — experts + lessons per file before you touch it`,
410
- ` 🗺️ \`team_expertise_map()\` — full team skills matrix in one table`,
411
- ` 🔒 \`visibility: "private"\` on \`learn_from_attempts\` — private notes never leak to smart_recall`,
412
- ` 📊 98 MCP tools`,
413
- ],
414
- '0.10.59': [
415
- `🆕 **Phase 3A: Org-wide knowledge graph**`,
416
- ` 👥 \`brain_who_knows(topic="...")\` — find your team's experts on any topic instantly`,
417
- ` 🕸️ Person + File nodes auto-built from \`learn_from_attempts(author="...", file_paths=[...])\``,
418
- ` 👤 Author attribution now shown inline in \`smart_recall\` results`,
419
- ` 📊 96 MCP tools · 386 tests green`,
420
- ],
421
- '0.10.58': [
422
- `🧹 Zero lint warnings — all unused imports cleaned across every handler`,
423
- `✅ Build: 0 errors, 0 warnings · 379 tests green`,
424
- ],
425
- '0.10.57': [
426
- `🆕 **What's new in v${CURRENT_VERSION}:**`,
427
- ` 🔧 **Critical fix** — corrected the npm bin/main entry path (\`dist/src/index.js\`); 0.10.50–0.10.52 shipped a broken entry point`,
428
- ` 🔧 Version hygiene — synced package.json, server.json, lockfile, MCP_VERSION; \`latest\` now points forward again`,
429
- ` ✅ Accurate tool count (95) across README, server.json, CLI banners`,
430
- ` ✅ Includes all Phase 2/3 work: quality reranking, Cachly-Bench, hybrid + CKG recall, team_confirm governance`,
431
- ],
432
- '0.10.52': [
433
- ` ✅ \`smart_recall\` — CKG traversal as Layer 3: finds lessons that FIXED causal-graph-similar problems (🕸️ causal graph badge)`,
434
- ` ✅ Bench corpus expanded: governance adversarial pair proves review-boost end-to-end (+22% P@1 headline)`,
435
- ` 🔧 Import cleanup: unused symbols removed from brain.ts (no functional change)`,
436
- ` 💡 \`smart_recall\` now shows keyword / semantic / CKG / hybrid match type for every result`,
437
- ],
438
- '0.10.51': [
439
- ` ✅ \`team_confirm\` — human review raises lesson recall ranking (🛡️ senior / ✔️ peer)`,
440
- ` ✅ Governance-aware recall — confirmed lessons outrank unreviewed auto-learned entries`,
441
- ],
442
- '0.10.50': [
443
- ` ✅ \`smart_recall\` — unified keyword + semantic hybrid list (one ranked result, not two sections)`,
444
- ` ✅ \`smart_recall\` — "Brain saved you here" banner surfaces time saved inline per proven lesson`,
445
- ` ✅ Quality reranking — proven successes outrank symptom-dense failures (+11% Precision@1)`,
446
- ` ✅ Cachly-Bench — IR benchmark with CI regression guard (see BENCH.md for the proof)`,
447
- ` 🔧 Contradiction resolution now persisted to Redis (90-day TTL)`,
448
- ` 💡 Run \`smart_recall\` to see your Brain's recall quality in action`,
449
- ],
450
- '0.10.49': [
451
- ` ✅ \`brain_from_git\` — auto-seed your Brain from git history in seconds`,
452
- ` ✅ \`brain_predict_failures\` — predict CI/build failures before they happen`,
453
- ` ✅ VS Code extension — Brain status bar, ambient learning, CodeLens hints`,
454
- ` ✅ Team Brain — share lessons across your whole engineering team`,
455
- ` 🔧 Stability — auto-expiring memory keys, longer provisioning wait, robust recall`,
456
- ],
457
- };
458
- export const BRAIN_TOOL_NAMES = new Set([
459
- 'learn_from_attempts', 'recall_best_solution', 'smart_recall',
460
- 'session_start', 'session_start_summary', 'session_end', 'session_ping', 'session_handoff', 'auto_learn_session',
461
- 'brain_who_knows', 'brain_file_map', 'team_expertise_map',
462
- 'skill_gaps', 'brain_coverage', 'brain_metrics', 'brain_service_map',
463
- 'brain_collab_pairs', 'brain_portability', 'brain_changelog',
464
- 'brain_set_pref', 'brain_get_pref',
465
- ]);
466
- // ── Free-tier Teaser-Gate ──────────────────────────────────────────────────
467
- // When a Free-tier Brain crosses its recall limit, smart_recall still delivers
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
470
- // DEPTH gate (hide the tail), never an access wall: the #1 result is always
471
- // returned. Usage/limit come from the same authoritative numbers as the Brain
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).
477
- const UPGRADE_URL = cachlyUrl('/billing', 'upgrade');
478
- const _recallGateCache = new Map();
479
- /**
480
- * Instances whose notice has already been shown in this process.
481
- *
482
- * Once per PROCESS, not once per call: the MCP server lives for the length of
483
- * the editor session, so this is exactly "once per session". Without it the
484
- * notice would repeat on every session_start — and with Ambient Recall that is
485
- * every prompt. That is the nagging the upgrade nudge was built to avoid, and
486
- * a line that appears every time stops being read within a day.
487
- */
488
- const _noticeGezeigt = new Set();
489
- // ── Recall quota: ONE unit per recall CALL ───────────────────────────────────
490
- // The gate above reads a monthly counter that this function writes. It counts
491
- // calls, not lessons.
492
- //
493
- // It used to be derived from the sum of every lesson's recall_count, and a
494
- // single smart_recall bumps up to five of those — so a free user burned their
495
- // 500 in about a hundred real calls and the number kept climbing past the cap.
496
- // The API's REST recall endpoint counts itself; this is the MCP side of the
497
- // same counter, and without it the cap would simply never be reached here.
498
- const RECALL_QUOTA_TTL_SECONDS = 70 * 24 * 60 * 60;
499
- function recallQuotaKey(now = new Date()) {
500
- // UTC month, matching the API's t.UTC().Format("2006-01"). Month-scoping in
501
- // the key is what makes the quota reset with no cron and no baseline.
502
- return `cachly:quota:recalls:${now.toISOString().slice(0, 7)}`;
503
- }
504
- async function bumpRecallQuota(redis) {
505
- try {
506
- const key = recallQuotaKey();
507
- const n = await redis.incr(key);
508
- if (n === 1)
509
- await redis.expire(key, RECALL_QUOTA_TTL_SECONDS);
510
- }
511
- catch {
512
- // Accounting must never fail a recall. Under-counting costs us a little
513
- // revenue; a thrown error costs the user the answer they asked for.
514
- }
515
- }
516
- async function getRecallGate(instanceId, apiFetch) {
517
- const cached = _recallGateCache.get(instanceId);
518
- if (cached && cached.expiresAt > Date.now())
519
- return cached.gate;
520
- try {
521
- const mem = await apiFetch(`/api/v1/instances/${instanceId}/memory`);
522
- const limit = mem?.recall_limit ?? -1;
523
- const used = mem?.total_recall_count ?? 0;
524
- const gate = {
525
- reached: limit > 0 && used >= limit,
526
- limit,
527
- used,
528
- goodwillMessage: mem?.goodwill_message,
529
- notice: mem?.notice,
530
- };
531
- _recallGateCache.set(instanceId, { gate, expiresAt: Date.now() + 60_000 });
532
- return gate;
533
- }
534
- catch {
535
- // Stats unavailable → never gate (fail open: value delivery beats monetization).
536
- return { reached: false, limit: -1, used: 0 };
537
- }
538
- }
539
- export async function handleBrainTool(name, args, getConnection, apiFetch) {
540
- switch (name) {
541
- case 'learn_from_attempts': {
542
- const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', visibility = 'team', service = '', service_kind = 'service', group = '', } = args;
543
- const redis = await getConnection(instance_id);
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);
568
- // ── Lessons-Review workflow (P1-1) ─────────────────────────────────────
569
- // When the org enabled pending-mode on this (shared) brain, team-visible
570
- // lessons become proposals awaiting owner/admin approval instead of
571
- // landing in the recall keyspace. Private notes stay direct — they never
572
- // surface in team recall anyway. Flag absent = direct write, as before.
573
- if (visibility !== 'private' && await reviewModeEnabled(redis, instance_id)) {
574
- return storeLessonProposal(redis, topic, {
575
- topic, outcome, what_worked, what_failed, context: ctx, severity,
576
- file_paths, commands, tags, depends_on,
577
- ...(autorFeld ? { author: autorFeld } : {}),
578
- ...(service ? { service } : {}),
579
- ...(group ? { group: String(group).toLowerCase().trim() } : {}),
580
- visibility, recall_count: 0, ts, confidence: 1.0, version: 3,
581
- });
582
- }
583
- // ── Structured template hints ──────────────────────────────────────────
584
- const category = topic.split(':')[0];
585
- const template = STRUCTURED_TEMPLATES[category];
586
- const templateWarnings = [];
587
- if (template) {
588
- for (const req of template.required) {
589
- if (req === 'commands' && commands.length === 0) {
590
- templateWarnings.push(`📋 ${template.hint}`);
591
- }
592
- }
593
- }
594
- // ── Deduplication + audit trail ────────────────────────────────────────
595
- let isUpdate = false;
596
- let recallCount = 0;
597
- let lastRecalledAt;
598
- let auditTrail = [];
599
- const existingRaw = await redis.get(`cachly:lesson:best:${topic}`);
600
- if (existingRaw) {
601
- try {
602
- const prev = JSON.parse(existingRaw);
603
- recallCount = prev.recall_count ?? 0;
604
- lastRecalledAt = prev.last_recalled_at; // learn never stamps it — only recall does
605
- auditTrail = prev.audit_trail ?? [];
606
- auditTrail.push({ ts, action: 'updated', prev_outcome: prev.outcome });
607
- if (auditTrail.length > 20)
608
- auditTrail = auditTrail.slice(-20);
609
- isUpdate = true;
610
- // ── Contradiction detection ─────────────────────────────────────────
611
- const contradictionWarning = [];
612
- if (prev.outcome === 'success' && outcome === 'failure') {
613
- contradictionWarning.push(`⚠️ **Contradiction detected!** Existing lesson has outcome: \`success\`, but you're storing \`failure\`.`, `The existing "success" lesson will be preserved. Only the audit trail is updated.`, `If you meant to mark this as failed permanently, store a new lesson with a distinct topic slug.`);
614
- }
615
- else if (prev.outcome === 'failure' && outcome === 'success') {
616
- contradictionWarning.push(`✅ **Conflict resolved!** Previous lesson was \`failure\` — now overwriting with \`success\`.`);
617
- }
618
- if (contradictionWarning.length > 0) {
619
- // Store contradiction audit but don't block
620
- auditTrail[auditTrail.length - 1].action = 'contradiction-resolved';
621
- // Layer 3: Write CKG contradicts edge for MADC to process
622
- try {
623
- const cId = ckgSlug(topic);
624
- const resId = ckgSlug(`resolution:${topic}`);
625
- await ckgUpdateEdge(redis, cId, 'contradicts', resId, false);
626
- }
627
- catch { /* non-critical */ }
628
- // Persist a queryable resolution record — so contradiction history survives
629
- // (CKG edges alone aren't easily auditable). Bounded + TTL'd like other history.
630
- try {
631
- const resolutionKey = `cachly:contradictions:${topic}`;
632
- const direction = prev.outcome === 'success' && outcome === 'failure'
633
- ? 'kept-success' : 'overwrote-failure';
634
- await redis.rpush(resolutionKey, JSON.stringify({
635
- ts, prev_outcome: prev.outcome, new_outcome: outcome, direction,
636
- }));
637
- await redis.ltrim(resolutionKey, -50, -1);
638
- await redis.expire(resolutionKey, 180 * 86400);
639
- }
640
- catch { /* non-critical */ }
641
- contradictionWarning.push(`🗳️ Run \`madc_deliberate(topic="${topic}")\` to resolve via expert agent voting.`);
642
- }
643
- }
644
- catch { /* ignore parse error */ }
645
- }
646
- else {
647
- auditTrail = [{ ts, action: 'created' }];
648
- }
649
- // ── "I Was Wrong" Protocol — failure attribution ───────────────────────
650
- const iWasWrongWarning = [];
651
- if (outcome === 'failure') {
652
- // Search for related success lessons that might have prevented this failure
653
- const scanKeys = [];
654
- const scanStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 100 });
655
- await new Promise((res, rej) => {
656
- scanStream.on('data', (b) => scanKeys.push(...b));
657
- scanStream.on('end', res);
658
- scanStream.on('error', rej);
659
- });
660
- const topicWords = topic.split(/[:\-_]/).filter(w => w.length > 2);
661
- for (const k of scanKeys.slice(0, 50)) {
662
- const raw = await redis.get(k);
663
- if (!raw)
664
- continue;
665
- try {
666
- const l = JSON.parse(raw);
667
- if (l.outcome !== 'success')
668
- continue;
669
- const lWords = (l.topic ?? '').split(/[:\-_]/).filter(w => w.length > 2);
670
- const overlap = topicWords.filter(w => lWords.includes(w)).length;
671
- if (overlap >= 1 && l.topic !== topic) {
672
- iWasWrongWarning.push(`⚠️ **"I Was Wrong"**: lesson \`${l.topic}\` (success, ${l.severity ?? 'major'}) might have prevented this failure.`, ` → Use \`recall_best_solution(topic="${l.topic}")\` before next attempt.`, ` → To mark it critical: \`learn_from_attempts(topic="${l.topic}", ..., severity="critical")\``);
673
- break; // only show the most relevant match
674
- }
675
- }
676
- catch { /* skip */ }
677
- }
678
- }
679
- // ── Register dependency index for causal chain ─────────────────────────
680
- for (const dep of depends_on) {
681
- const depKey = `cachly:dep:${dep}`;
682
- const existing = await redis.get(depKey);
683
- const depTopics = safeJsonParse(existing, []);
684
- if (!depTopics.includes(topic))
685
- depTopics.push(topic);
686
- await redis.set(depKey, JSON.stringify(depTopics), 'EX', 90 * 86400);
687
- }
688
- const lessonObj = {
689
- topic,
690
- outcome,
691
- what_worked,
692
- what_failed,
693
- context: ctx,
694
- severity,
695
- file_paths,
696
- commands,
697
- tags,
698
- depends_on,
699
- // Auch hier der hergeleitete Name — sonst traegt die gespeicherte
700
- // Lektion selbst keinen Autor, waehrend der Kausalgraph einen hat.
701
- ...(autorFeld ? { author: autorFeld } : {}),
702
- ...(service ? { service } : {}),
703
- ...(group ? { group: String(group).toLowerCase().trim() } : {}),
704
- visibility,
705
- recall_count: recallCount,
706
- ...(lastRecalledAt ? { last_recalled_at: lastRecalledAt } : {}),
707
- ts,
708
- verified_at: outcome === 'success' || outcome === 'partial' ? ts : undefined,
709
- confidence: 1.0,
710
- audit_trail: auditTrail,
711
- version: 3,
712
- };
713
- const lesson = JSON.stringify(lessonObj);
714
- // Metric 1 (time-to-first-recall): mark the moment the Brain first gained
715
- // knowledge. SET NX so only the very first learn wins. Fire-and-forget.
716
- redis.set(`cachly:stats:born_at:${instance_id}`, ts, 'EX', 365 * 86400, 'NX').catch(() => { });
717
- // Always append to the history list (audit log); keep last 100 entries, 90-day TTL
718
- const listKey = `cachly:lessons:${topic}`;
719
- await redis.rpush(listKey, lesson);
720
- await redis.ltrim(listKey, -100, -1);
721
- await redis.expire(listKey, 90 * 86400);
722
- // Update best key for success/partial; for failure only update if no success exists
723
- let bestGeschrieben = false;
724
- if (outcome === 'success' || outcome === 'partial') {
725
- await redis.set(`cachly:lesson:best:${topic}`, lesson);
726
- bestGeschrieben = true;
727
- }
728
- else if (!existingRaw) {
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
- })();
788
- }
789
- // Track in decision log for session replay
790
- try {
791
- const dlKey = 'cachly:session:decision-log';
792
- const dlEntry = JSON.stringify({ ts, topic, outcome, what_worked: what_worked.slice(0, 120) });
793
- await redis.rpush(dlKey, dlEntry);
794
- await redis.ltrim(dlKey, -50, -1);
795
- }
796
- catch { /* non-critical */ }
797
- // ── Layer 1+2: CKG update (Causal Knowledge Graph + Belief Update Engine) ──
798
- // BUE: Bayesian confidence, contradiction detection, second-degree propagation, decay
799
- let beliefConflict = null;
800
- try {
801
- const conceptId = ckgSlug(topic);
802
- const domain = topic.split(':')[0] ?? 'unknown';
803
- const conceptType = domain; // fix, debug, deploy, infra, api, etc.
804
- // Upsert concept node
805
- await ckgUpsertNode(redis, conceptId, domain, conceptType);
806
- // Tag co-occurrence edges
807
- for (const tag of tags) {
808
- const tagId = ckgSlug(`tag:${tag}`);
809
- await ckgUpsertNode(redis, tagId, 'tag', 'tag');
810
- await ckgUpdateEdge(redis, conceptId, 'co-occurs', tagId, outcome === 'success', outcome === 'partial');
811
- }
812
- // depends_on → requires edges (structural, always confidence 1.0 direction)
813
- for (const dep of depends_on) {
814
- const depId = ckgSlug(dep);
815
- await ckgUpdateEdge(redis, conceptId, 'requires', depId, true);
816
- }
817
- // fixes edge: if category=fix and outcome=success, link to problem concept
818
- if ((domain === 'fix' || domain === 'debug') && (outcome === 'success' || outcome === 'partial')) {
819
- const problemText = what_failed || ctx || '';
820
- const problemConcept = problemText ? extractProblemConcept(problemText) : null;
821
- if (problemConcept) {
822
- const problemId = ckgSlug(`problem:${problemConcept}`);
823
- await ckgUpsertNode(redis, problemId, 'problem', 'problem');
824
- await ckgUpdateEdge(redis, conceptId, 'fixes', problemId, outcome === 'success', outcome === 'partial');
825
- }
826
- }
827
- // causes edge: if outcome=failure, link topic concept to the problem context
828
- if (outcome === 'failure' && (what_failed || what_worked)) {
829
- const causeText = what_failed || what_worked;
830
- const causeConcept = extractProblemConcept(causeText);
831
- if (causeConcept) {
832
- const causeId = ckgSlug(`cause:${causeConcept}`);
833
- await ckgUpsertNode(redis, causeId, 'cause', 'cause');
834
- await ckgUpdateEdge(redis, conceptId, 'causes', causeId, false);
835
- }
836
- }
837
- // ── BUE: Contradiction detection ──────────────────────────────────────
838
- // If this topic previously had a confirmed 'fixes' edge (confidence > 0.7)
839
- // and now outcome=failure → flag belief_conflict
840
- if (outcome === 'failure') {
841
- const existingEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${conceptId}`);
842
- for (const ek of existingEdgeKeys) {
843
- const er = await redis.get(ek);
844
- if (!er)
845
- continue;
846
- const existEdge = safeJsonParse(er, null);
847
- if (!existEdge)
848
- continue;
849
- if (existEdge.edgeType === 'fixes' && existEdge.confidence > 0.7 && existEdge.trials >= 3) {
850
- beliefConflict = `⚠️ **belief_conflict** on \`${topic}\`: previously confirmed fix (confidence ${existEdge.confidence.toFixed(2)}, n=${existEdge.trials}) now reports failure. Both beliefs retained as \`contested\`. Use \`brain_resolve_conflict(instance_id="...", topic="${topic}", winner="success"|"failure")\` to arbitrate, or \`ckg_inspect(concept="${conceptId}")\` to review.`;
851
- // Store conflict marker — records WHICH agent reported the contradiction
852
- // so multi-agent arbitration can show both sides (Move 4).
853
- const conflictKey = `cachly:ckg:conflict:${conceptId}`;
854
- await redis.set(conflictKey, JSON.stringify({
855
- topic,
856
- concept_id: conceptId,
857
- detected_at: new Date().toISOString(),
858
- fix_confidence: existEdge.confidence,
859
- fix_trials: existEdge.trials,
860
- failure_outcome: outcome,
861
- reported_by: autorFeld || 'unknown',
862
- what_failed: what_failed || '',
863
- resolved: false,
864
- }), 'EX', 60 * 60 * 24 * 90);
865
- // Auto-trigger MADC deliberation in background — never blocks learn_from_attempts
866
- // Note: fire-and-forget; madc_deliberate is handled elsewhere in the switch
867
- // (conflict marker stored above — no warm-up call needed)
868
- }
869
- }
870
- }
871
- // ── BUE: Second-degree propagation ────────────────────────────────────
872
- // When a 'fixes' edge gets stronger, boost co-occurring second-degree edges slightly
873
- if (outcome === 'success' && (domain === 'fix' || domain === 'debug')) {
874
- const fromEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${conceptId}`);
875
- for (const ek of fromEdgeKeys.slice(0, 10)) {
876
- const er = await redis.get(ek);
877
- if (!er)
878
- continue;
879
- const e2 = safeJsonParse(er, null);
880
- if (!e2)
881
- continue;
882
- if (e2.edgeType !== 'fixes')
883
- continue;
884
- // Boost second-degree: edges from e2.to get a small fractional success
885
- const secondKeys = await redis.smembers(`cachly:ckg:idx:from:${e2.to}`);
886
- for (const sk of secondKeys.slice(0, 5)) {
887
- const sr = await redis.get(sk);
888
- if (!sr)
889
- continue;
890
- const se = safeJsonParse(sr, null);
891
- if (!se)
892
- continue;
893
- if (se.edgeType !== 'co-occurs')
894
- continue;
895
- // Add 0.1 fractional success (second-degree signal)
896
- se.successes = (se.successes || 0) + 0.1;
897
- se.trials = (se.trials || 0) + 0.1;
898
- se.confidence = (se.successes + 1) / (se.trials + 2);
899
- se.last_updated = new Date().toISOString();
900
- await redis.set(sk, JSON.stringify(se));
901
- }
902
- }
903
- }
904
- // ── BUE: Stale edge decay ─────────────────────────────────────────────
905
- // Edges older than 90 days with < 3 trials decay by 10% confidence.
906
- // Only run probabilistically (1% of calls) to avoid per-call overhead.
907
- if (Math.random() < 0.01) {
908
- const decayCutoff = Date.now() - 90 * 24 * 60 * 60 * 1000;
909
- const fromEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${conceptId}`);
910
- for (const ek of fromEdgeKeys) {
911
- const er = await redis.get(ek);
912
- if (!er)
913
- continue;
914
- const de = safeJsonParse(er, null);
915
- if (!de)
916
- continue;
917
- if (de.trials < 3 && de.last_updated && new Date(de.last_updated).getTime() < decayCutoff) {
918
- de.confidence = de.confidence * 0.9;
919
- de.last_updated = new Date().toISOString();
920
- await redis.set(ek, JSON.stringify(de));
921
- }
922
- }
923
- }
924
- }
925
- catch { /* CKG updates are non-critical */ }
926
- // ── Phase 3A: People + File nodes in knowledge graph ──────────────────────
927
- // Builds the "who knows what" map automatically from author + file_paths.
928
- // Person → authored → Concept edges power brain_who_knows queries.
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) {
935
- // Agent activity registry (Move 4): track which agents are actively
936
- // writing to this Brain, keyed by author, with a 1h TTL. Powers
937
- // brain_conflicts' "who is live" view and multi-agent arbitration.
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(() => { });
939
- try {
940
- const domain = topic.split(':')[0] ?? 'unknown';
941
- const personId = await ckgUpsertPersonNode(redis, nurMensch(autorFeld), domain);
942
- const conceptId = ckgSlug(topic);
943
- await ckgUpdateEdge(redis, personId, 'authored', conceptId, outcome === 'success', outcome === 'partial');
944
- for (const fp of file_paths.slice(0, 8)) {
945
- const fileId = await ckgUpsertFileNode(redis, fp);
946
- await ckgUpdateEdge(redis, personId, 'touched', fileId, true);
947
- // Phase 3: link this author to everyone else who touched the same file.
948
- await ckgRecordCollaboration(redis, fileId, personId);
949
- }
950
- }
951
- catch { /* non-critical */ }
952
- }
953
- // ── Phase 3: Service/System nodes ─────────────────────────────────────────
954
- // When a lesson names the service/system it concerns (e.g. "prometheus",
955
- // "cachly-web"), wire it into the graph: person→operates→service,
956
- // concept→affects→service, file→runs_in→service. This is what lets
957
- // brain_service_map answer "who owns X and what's gone wrong with X before".
958
- if (service && typeof service === 'string' && service.trim()) {
959
- try {
960
- const domain = topic.split(':')[0] ?? 'unknown';
961
- const kind = service_kind === 'system' ? 'system' : 'service';
962
- const serviceId = await ckgUpsertServiceNode(redis, service.trim(), domain, kind);
963
- const conceptId = ckgSlug(topic);
964
- // concept → affects → service (outcome decides edge confidence)
965
- await ckgUpdateEdge(redis, conceptId, 'affects', serviceId, outcome === 'success', outcome === 'partial');
966
- if (autorFeld) {
967
- const personId = `person:${ckgSlug(nurMensch(autorFeld))}`;
968
- await ckgUpdateEdge(redis, personId, 'operates', serviceId, true);
969
- }
970
- for (const fp of file_paths.slice(0, 8)) {
971
- const fileId = `file:${ckgSlug(fp)}`;
972
- await ckgUpdateEdge(redis, fileId, 'runs_in', serviceId, true);
973
- }
974
- }
975
- catch { /* non-critical */ }
976
- }
977
- // ── Ambient team propagation: if author is set + outcome is success,
978
- // auto-store as a team lesson so knowledge is shared without a separate
979
- // team_learn call. Tags gain 'team'; the lesson is attributed to the author.
980
- // This makes team knowledge sharing the default, not the opt-in.
981
- if (autorFeld && (outcome === 'success' || outcome === 'partial')) {
982
- const teamLesson = {
983
- topic, outcome, what_worked,
984
- what_failed: what_failed ?? '',
985
- severity,
986
- author: autorFeld,
987
- file_paths,
988
- commands,
989
- tags: [...new Set([...tags, 'team'])],
990
- timestamp: new Date().toISOString(),
991
- recall_count: 0,
992
- version: 2,
993
- auto_propagated: true,
994
- };
995
- const teamListKey = `cachly:lessons:${topic}`;
996
- redis.rpush(teamListKey, JSON.stringify(teamLesson)).catch(() => { });
997
- }
998
- const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
999
- const sevEmoji = severity === 'critical' ? '🔴' : severity === 'major' ? '🟡' : '🟢';
1000
- const action = isUpdate ? 'updated' : 'stored';
1001
- return [
1002
- `${emoji} **Lesson ${action}:** \`${topic}\` (${outcome}) ${sevEmoji} ${severity}`,
1003
- beliefConflict ?? '',
1004
- ``,
1005
- `**What worked:** ${what_worked}`,
1006
- what_failed ? `**What failed:** ${what_failed}` : '',
1007
- ctx ? `**Context:** ${ctx}` : '',
1008
- file_paths.length > 0 ? `**Files:** ${file_paths.map(f => `\`${f}\``).join(', ')}` : '',
1009
- commands.length > 0 ? `**Commands:** ${commands.map(c => `\`${c}\``).join(', ')}` : '',
1010
- tags.length > 0 ? `**Tags:** ${tags.map(t => `#${t}`).join(' ')}` : '',
1011
- ``,
1012
- isUpdate
1013
- ? `♻️ Updated (recall count: ${recallCount} · audit entries: ${auditTrail.length})`
1014
- : `💡 Recall later with \`recall_best_solution(topic="${topic}")\``,
1015
- (autorFeld && (outcome === 'success' || outcome === 'partial'))
1016
- ? `👥 Auto-shared with team (by _${autorFeld}_${herkunft.quelle !== 'uebergeben' ? ` · aus ${herkunft.quelle}` : ''}) — visible in \`team_recall\``
1017
- : '',
1018
- autorTipp ?? '',
1019
- depends_on.length > 0
1020
- ? `🔗 Depends on: ${depends_on.map(d => `\`${d}\``).join(', ')} → trace with \`trace_dependency\``
1021
- : '',
1022
- ...templateWarnings,
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) ?? '',
1031
- ].filter(l => l !== '').join('\n');
1032
- }
1033
- case 'recall_best_solution': {
1034
- const { instance_id, topic } = args;
1035
- const redis = await getConnection(instance_id);
1036
- // Try exact best-solution key first
1037
- const best = await redis.get(`cachly:lesson:best:${topic}`);
1038
- if (best) {
1039
- const lesson = safeJsonParse(best, null);
1040
- if (!lesson)
1041
- return `⚠️ Lesson data for \`${topic}\` is corrupted. Re-store it with \`learn_from_attempts\`.`;
1042
- // ── Confidence decay check ───────────────────────────────────────────
1043
- const confidence = calculateConfidence(lesson);
1044
- const ref = lesson.verified_at ?? lesson.ts;
1045
- const ageDays = (Date.now() - new Date(ref).getTime()) / 86400000;
1046
- const badge = confidenceBadge(confidence, ageDays);
1047
- // Recall resets verified_at (confidence clock restart) and stamps
1048
- // last_recalled_at so the recall-quality dashboard can measure recency.
1049
- const recalledAt = new Date().toISOString();
1050
- const updatedLesson = {
1051
- ...lesson,
1052
- recall_count: (lesson.recall_count ?? 0) + 1,
1053
- verified_at: recalledAt,
1054
- last_recalled_at: recalledAt,
1055
- confidence: 1.0,
1056
- };
1057
- await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(updatedLesson));
1058
- // A targeted lookup is a recall too, and costs the same one quota unit
1059
- // as a broad smart_recall — the user asked the brain a question either way.
1060
- void bumpRecallQuota(redis);
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
- }
1067
- const sevEmoji = lesson.severity === 'critical' ? '🔴' : lesson.severity === 'major' ? '🟡' : lesson.severity ? '🟢' : '';
1068
- const auditSummary = (lesson.audit_trail ?? []).length > 1
1069
- ? `_Audit: ${(lesson.audit_trail ?? []).length} changes · stored ${new Date(lesson.ts).toLocaleDateString('de-DE')}_`
1070
- : '';
1071
- // "Remember when..." — emotional header for lessons > 60 days old
1072
- const ageFromStoreDays = (Date.now() - new Date(lesson.ts).getTime()) / 86400000;
1073
- const rememberWhen = ageFromStoreDays > 60
1074
- ? `💭 _Remember when you solved this ${Math.round(ageFromStoreDays / 30)} months ago? Still works._`
1075
- : '';
1076
- // "Never Google This Again" — suggest pinning after 3rd recall
1077
- const suggestPin = updatedLesson.recall_count === 3 && !lesson.pinned
1078
- ? `📌 **You've looked this up 3 times.** Consider pinning it for instant access: add \`pinned: true\` via \`learn_from_attempts\` to always surface it first.`
1079
- : '';
1080
- // ── Trust signal (today-safe consensus layer) ───────────────────────
1081
- // A lesson recalled many times — or confirmed by multiple distinct
1082
- // authors — has proven its value. Surface that as social proof.
1083
- const lessonAuthors = lesson.authors
1084
- ?? (lesson.author ? [lesson.author] : []);
1085
- const distinctAuthors = [...new Set(lessonAuthors.filter(Boolean))];
1086
- const rc = updatedLesson.recall_count;
1087
- let trustBadge = '';
1088
- if (distinctAuthors.length >= 2 && rc >= 5) {
1089
- trustBadge = `🏆 **Battle-tested** — recalled ${rc}× · verified by ${distinctAuthors.length} developers. Trust this.`;
1090
- }
1091
- else if (rc >= 10) {
1092
- trustBadge = `🏆 **Battle-tested** — recalled ${rc}×. This is one of your most-proven solutions.`;
1093
- }
1094
- else if (distinctAuthors.length >= 2) {
1095
- trustBadge = `✅ **Team-verified** — confirmed by ${distinctAuthors.length} developers.`;
1096
- }
1097
- else if (rc >= 5) {
1098
- trustBadge = `✅ **Proven** — recalled ${rc}× without contradiction.`;
1099
- }
1100
- return [
1101
- rememberWhen,
1102
- trustBadge,
1103
- `${badge} **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}×`,
1104
- ``,
1105
- // what_worked may be empty for unresolved failure lessons (the API
1106
- // accepts empty what_worked when outcome=failure) — skip the line.
1107
- lesson.what_worked ? `**What worked:** ${lesson.what_worked}` : '',
1108
- lesson.what_failed ? `**What failed (avoid this):** ${lesson.what_failed}` : '',
1109
- lesson.context ? `**Context:** ${lesson.context}` : '',
1110
- (lesson.file_paths ?? []).length > 0 ? `**Files:** ${(lesson.file_paths ?? []).map((f) => `\`${f}\``).join(', ')}` : '',
1111
- (lesson.commands ?? []).length > 0 ? `**Commands:** ${(lesson.commands ?? []).map((c) => `\`${c}\``).join(', ')}` : '',
1112
- (lesson.tags ?? []).length > 0 ? `**Tags:** ${(lesson.tags ?? []).map((t) => `#${t}`).join(' ')}` : '',
1113
- auditSummary,
1114
- suggestPin,
1115
- ].filter(l => l !== '').join('\n');
1116
- }
1117
- // Partial match: scan all lesson keys for topic substring
1118
- const allKeys = [];
1119
- const scanStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 100 });
1120
- await new Promise((resolve, reject) => {
1121
- scanStream.on('data', (batch) => allKeys.push(...batch));
1122
- scanStream.on('end', resolve);
1123
- scanStream.on('error', reject);
1124
- });
1125
- const matching = allKeys.filter(k => k.toLowerCase().includes(topic.toLowerCase()));
1126
- if (matching.length === 0) {
1127
- // Check attempt history as fallback
1128
- const histKey = `cachly:lessons:${topic}`;
1129
- const all = await redis.lrange(histKey, -3, -1);
1130
- if (all.length > 0) {
1131
- const parsed = all.map(e => safeJsonParse(e, null)).filter(Boolean);
1132
- const lines = parsed.map(p => `- ${p.outcome === 'success' ? '✅' : '❌'} ${p.what_worked.slice(0, 120)} (${new Date(p.ts).toLocaleDateString('de-DE')})`);
1133
- return `⚠️ No successful solution for \`${topic}\` yet. Last attempts:\n\n${lines.join('\n')}`;
1134
- }
1135
- return `📭 No lessons found for \`${topic}\`. Use \`learn_from_attempts\` after solving it.`;
1136
- }
1137
- // Return all partial matches
1138
- const results = [];
1139
- for (const k of matching.slice(0, 5)) {
1140
- const raw = await redis.get(k);
1141
- if (!raw)
1142
- continue;
1143
- const lesson = safeJsonParse(raw, null);
1144
- if (!lesson)
1145
- continue;
1146
- results.push(`**\`${lesson.topic}\`** — ${lesson.what_worked.slice(0, 200)}`);
1147
- }
1148
- return `🔍 **Partial matches for \`${topic}\`:**\n\n${results.join('\n\n')}`;
1149
- }
1150
- case 'smart_recall': {
1151
- const { instance_id, query, threshold = 0.78, author: requester = '', context_files: rawContextFiles = [], } = args;
1152
- const contextFiles = Array.isArray(rawContextFiles)
1153
- ? rawContextFiles.filter((f) => typeof f === 'string')
1154
- : [];
1155
- const redis = await getConnection(instance_id);
1156
- // Team-level visibility scopes: resolve the requester's group memberships +
1157
- // admin status once, so group-scoped lessons only surface for members/admins.
1158
- let requesterScopes = new Set();
1159
- let requesterIsAdmin = false;
1160
- if (requester) {
1161
- requesterScopes = await getScopes(redis, instance_id, requester).catch(() => new Set());
1162
- const reqRole = await getRole(redis, instance_id, requester).catch(() => null);
1163
- requesterIsAdmin = reqRole === 'admin';
1164
- }
1165
- // ── Layer 1: Keyword search across ALL brain data (always works, no embedding) ──
1166
- // Wider candidate pool (25) lets the quality reranker rescue relevant lessons that
1167
- // BM25 alone would rank 11–25 due to vocabulary mismatch. Final slice to 5 happens below.
1168
- const rawMatches = await keywordSearch(redis, ['cachly:ctx:*', 'cachly:lesson:best:*', 'cachly:idx:*'], query, 25);
1169
- // ── Layer 1.5: Quality-aware rerank — proven success lessons outrank
1170
- // text-similar failed attempts (the moat; see src/rerank.ts + Cachly-Bench). ──
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;
1317
- // One quota unit for this call, regardless of how many lessons it surfaced.
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
- }
1342
- const savedHere = [];
1343
- // Exclude archived lessons — they are kept for audit but must not surface in recall.
1344
- const lessonMatches = kwGemischt.filter(m => m.key.startsWith('cachly:lesson:best:'));
1345
- let crossAuthorThisCall = 0;
1346
- let successRecallsThisCall = 0;
1347
- for (const m of lessonMatches.slice(0, 5)) {
1348
- const existing = await redis.get(m.key).catch(() => null);
1349
- if (existing) {
1350
- const lesson = safeJsonParse(existing, null);
1351
- // Skip archived lessons — brain_hygiene moves them here; they should not resurface.
1352
- if (lesson?.state === 'archived')
1353
- continue;
1354
- if (lesson) {
1355
- const recalledAt = new Date().toISOString();
1356
- const updated = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1, verified_at: recalledAt, last_recalled_at: recalledAt };
1357
- redis.set(m.key, JSON.stringify(updated)).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
- }
1363
- // Metric 3 (team-knowledge-reuse): a recall of a lesson authored by
1364
- // someone OTHER than the requester is cross-author reuse — the value
1365
- // only cachly delivers. Track total proven recalls + cross-author ones.
1366
- redis.incr(`cachly:stats:recalls_total:${instance_id}`).catch(() => { });
1367
- if (lesson.author && requester && lesson.author !== requester) {
1368
- crossAuthorThisCall++;
1369
- redis.incr(`cachly:stats:cross_author_recalls:${instance_id}`).catch(() => { });
1370
- redis.sadd(`cachly:stats:reuse_pairs:${instance_id}`, `${requester}<-${lesson.author}`).catch(() => { });
1371
- // W5: wire person↔person CKG collaborates edges from knowledge-reuse events.
1372
- // This feeds brain_collab_pairs and enriches brain_who_knows with reuse-based edges.
1373
- const reqPersonId = `person:${ckgSlug(requester)}`;
1374
- const authPersonId = `person:${ckgSlug(lesson.author)}`;
1375
- ckgUpdateEdge(redis, reqPersonId, 'collaborates', authPersonId, true).catch(() => { });
1376
- ckgUpdateEdge(redis, authPersonId, 'collaborates', reqPersonId, true).catch(() => { });
1377
- }
1378
- if (lesson.outcome === 'success')
1379
- successRecallsThisCall++;
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)) {
1387
- savedHere.push({
1388
- topic: m.key.replace('cachly:lesson:best:', ''),
1389
- severity: lesson.severity ?? 'major',
1390
- recall_count: (lesson.recall_count ?? 0) + 1,
1391
- savedMins,
1392
- ts: lesson.ts,
1393
- author: lesson.author,
1394
- });
1395
- }
1396
- }
1397
- }
1398
- }
1399
- // Metric 1 (time-to-first-recall): stamp the first time recall returned a
1400
- // successful lesson. SET NX so only the first successful recall wins.
1401
- // Deliberately NOT gated on savedHere: savedHere requires a pre-existing
1402
- // recall_count >= 1, which would delay the stamp until the SECOND recall
1403
- // and inflate every TTFR percentile derived from it.
1404
- if (successRecallsThisCall > 0) {
1405
- redis.set(`cachly:stats:first_recall_at:${instance_id}`, new Date().toISOString(), 'EX', 365 * 86400, 'NX').catch(() => { });
1406
- }
1407
- // ── Layer 2: Semantic search (parallel, optional) ────────────────────────
1408
- // Semantic recall is a Premium depth layer: free tier keeps full keyword +
1409
- // CKG recall (the magic moment), paid tiers add embedding-based retrieval.
1410
- const inst = await apiFetch(`/api/v1/instances/${instance_id}`).catch(() => null);
1411
- const tierIsFree = !inst?.tier || inst.tier.toLowerCase() === 'free';
1412
- const semHits = [];
1413
- if (!tierIsFree && inst?.vector_token && hasEmbedProvider()) {
1414
- try {
1415
- const embedding = await computeEmbedding(query);
1416
- const vectorUrl = `https://api.cachly.dev/v1/sem/${inst.vector_token}`;
1417
- const searchRes = await fetch(`${vectorUrl}/search`, {
1418
- method: 'POST',
1419
- headers: { 'Content-Type': 'application/json' },
1420
- body: JSON.stringify({ embedding, namespace: 'cachly:ctx', threshold, top_k: 5 }),
1421
- signal: AbortSignal.timeout(8000),
1422
- });
1423
- if (searchRes.ok) {
1424
- const results = (await searchRes.json());
1425
- for (const hit of results.filter(r => r.found && r.id)) {
1426
- const parts = hit.id.replace('ctx:', '').split(':');
1427
- const category = parts[0];
1428
- const key = parts.slice(1).join(':');
1429
- const redisKey = `cachly:ctx:${category}:${key}`;
1430
- const content = await redis.get(redisKey).catch(() => null);
1431
- semHits.push({ key: redisKey, similarity: hit.similarity ?? 0, content: content ?? '(evicted)' });
1432
- }
1433
- }
1434
- }
1435
- catch {
1436
- // Semantic search failed silently — keyword results are enough
1437
- }
1438
- }
1439
- const bm25Scores = kwGemischt.map(m => m.score);
1440
- const bm25Min = bm25Scores.length ? Math.min(...bm25Scores) : 0;
1441
- const bm25Range = bm25Scores.length ? (Math.max(...bm25Scores) - bm25Min) || 1 : 1;
1442
- const bm25Norm = (s) => (s - bm25Min) / bm25Range;
1443
- const hybridMap = new Map();
1444
- for (const m of kwGemischt) {
1445
- const n = bm25Norm(m.score);
1446
- hybridMap.set(m.key, {
1447
- key: m.key, content: m.content, bm25Score: n,
1448
- hybridScore: n * (semHits.length > 0 ? 0.7 : 1.0),
1449
- matchedWords: m.matchedWords, matchType: 'keyword', subQuery: m.subQuery,
1450
- });
1451
- }
1452
- for (const hit of semHits) {
1453
- const existing = hybridMap.get(hit.key);
1454
- if (existing) {
1455
- existing.semScore = hit.similarity;
1456
- existing.hybridScore = (existing.bm25Score ?? 0) * 0.6 + hit.similarity * 0.4;
1457
- existing.matchType = 'hybrid';
1458
- }
1459
- else {
1460
- hybridMap.set(hit.key, {
1461
- key: hit.key, content: hit.content, semScore: hit.similarity,
1462
- hybridScore: hit.similarity * 0.5, matchType: 'semantic',
1463
- });
1464
- }
1465
- }
1466
- // ── Layer 3: CKG traversal — follow "fixes" edges to find lessons that solved
1467
- // structurally similar problems, even when vocabulary differs ───────────────
1468
- try {
1469
- const qTokens = tokenize(query).filter(t => t.length >= 4).slice(0, 4);
1470
- const candidateNodeIds = new Set();
1471
- for (const token of qTokens) {
1472
- const nodeStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 20 });
1473
- await new Promise((res, rej) => {
1474
- nodeStream.on('data', (batch) => {
1475
- for (const k of batch.slice(0, 5))
1476
- candidateNodeIds.add(k.replace('cachly:ckg:node:', ''));
1477
- });
1478
- nodeStream.on('end', res);
1479
- nodeStream.on('error', rej);
1480
- });
1481
- }
1482
- const seenFroms = new Set();
1483
- for (const nodeId of [...candidateNodeIds].slice(0, 12)) {
1484
- const inboundEdgeKeys = await redis.smembers(`cachly:ckg:idx:to:${nodeId}`);
1485
- for (const ek of inboundEdgeKeys.slice(0, 6)) {
1486
- const er = await redis.get(ek);
1487
- if (!er)
1488
- continue;
1489
- const edge = safeJsonParse(er, null);
1490
- if (!edge || edge.edgeType !== 'fixes' || edge.confidence < 0.35)
1491
- continue;
1492
- if (seenFroms.has(edge.from))
1493
- continue;
1494
- seenFroms.add(edge.from);
1495
- const lessonKey = `cachly:lesson:best:${edge.from}`;
1496
- const existing = hybridMap.get(lessonKey);
1497
- if (existing) {
1498
- // Already found by BM25/semantic — boost with causal confirmation
1499
- existing.ckgScore = Math.max(existing.ckgScore ?? 0, edge.confidence);
1500
- existing.hybridScore = existing.hybridScore * 0.88 + edge.confidence * 0.12;
1501
- if (existing.matchType === 'keyword')
1502
- existing.matchType = 'hybrid';
1503
- }
1504
- else {
1505
- const content = await redis.get(lessonKey).catch(() => null);
1506
- if (!content)
1507
- continue;
1508
- hybridMap.set(lessonKey, {
1509
- key: lessonKey, content,
1510
- ckgScore: edge.confidence,
1511
- hybridScore: edge.confidence * 0.30, // CKG-only: lower than text hits
1512
- matchType: 'ckg',
1513
- });
1514
- }
1515
- }
1516
- }
1517
- }
1518
- catch { /* non-critical — keyword + semantic always available */ }
1519
- // ── Layer 4: File-context personalization ────────────────────────────────
1520
- // When the caller provides the files they are currently working on, lessons
1521
- // that were learned in the context of those files get a score boost. This
1522
- // surfaces file-specific institutional knowledge even when the query words
1523
- // don't mention the file name — the structural advantage of a graph brain.
1524
- if (contextFiles.length > 0) {
1525
- const ctxSet = new Set(contextFiles.map(f => f.replace(/\\/g, '/')));
1526
- for (const r of hybridMap.values()) {
1527
- if (!r.key.startsWith('cachly:lesson:best:'))
1528
- continue;
1529
- const ld = safeJsonParse(r.content, {});
1530
- const fps = Array.isArray(ld.file_paths)
1531
- ? ld.file_paths.filter((f) => typeof f === 'string').map(f => f.replace(/\\/g, '/'))
1532
- : [];
1533
- if (fps.some(f => ctxSet.has(f))) {
1534
- r.hybridScore *= 1.15;
1535
- r.contextBoost = true;
1536
- }
1537
- }
1538
- }
1539
- // Apply lesson quality to the NON-keyword-only paths. Keyword & hybrid
1540
- // entries already carry quality from the Layer-1.5 BM25 rerank
1541
- // (rerankByQuality above), so re-multiplying them would double-count. But a
1542
- // proven, senior-reviewed, high-severity success found ONLY via embeddings
1543
- // (matchType 'semantic') or the causal graph (matchType 'ckg') previously
1544
- // got zero quality lift — it ranked purely on similarity/edge-confidence.
1545
- // Weight just those two so a trustworthy lesson isn't buried behind a
1546
- // shallow-but-similar one.
1547
- for (const r of hybridMap.values()) {
1548
- if (r.matchType !== 'semantic' && r.matchType !== 'ckg')
1549
- continue;
1550
- const lq = extractLessonQuality({ key: r.key, content: r.content });
1551
- if (lq)
1552
- r.hybridScore *= qualityMultiplier(lq);
1553
- }
1554
- // Filter private lessons (recall_best_solution only) + enforce team scopes:
1555
- // group-scoped lessons surface only for members of that group (admins see all).
1556
- const hybridResults = [...hybridMap.values()]
1557
- .filter(r => {
1558
- if (!r.key.startsWith('cachly:lesson:best:'))
1559
- return true;
1560
- const ld = safeJsonParse(r.content, {});
1561
- if (ld.visibility === 'private')
1562
- return false;
1563
- if (!lessonVisibleToScope(ld.group, requesterScopes, requesterIsAdmin))
1564
- return false;
1565
- return true;
1566
- })
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
- }
1602
- // ── Teaser-Gate: free tier over its recall limit hides the long tail ──────
1603
- const recallGate = await getRecallGate(instance_id, apiFetch);
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.
1608
- const gateActive = recallGate.reached;
1609
- const tiefe = recallTiefe({ ueberLimit: gateActive, gesamt: hybridResults.length, volleTiefe: TIEFE_VOLL });
1610
- const visibleCount = tiefe.sichtbar;
1611
- // ── Build output ──────────────────────────────────────────────────────────
1612
- const lines = [`🧠 **Smart Recall** for: _"${query}"_\n`];
1613
- // Warm goodwill note — shown the month a free user is gifted +250 recalls.
1614
- if (recallGate.goodwillMessage) {
1615
- lines.push(`${recallGate.goodwillMessage}\n`);
1616
- }
1617
- // "Brain saved you here" banner — surfaces value inline for proven recalled lessons
1618
- const SORDER = { critical: 0, major: 1, minor: 2 };
1619
- const topLesson = savedHere.sort((a, b) => (SORDER[a.severity] ?? 1) - (SORDER[b.severity] ?? 1))[0];
1620
- if (topLesson) {
1621
- const fmtMins = (m) => m >= 60 ? `${m / 60}h` : `${m}min`;
1622
- const learnedStr = (() => {
1623
- if (!topLesson.ts)
1624
- return null;
1625
- const d = new Date(topLesson.ts);
1626
- if (isNaN(d.getTime()))
1627
- return null;
1628
- const now = new Date();
1629
- const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
1630
- const sameYear = d.getFullYear() === now.getFullYear();
1631
- return sameYear ? `${d.getDate()} ${months[d.getMonth()]}` : `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}`;
1632
- })();
1633
- const debugCostMins = topLesson.severity === 'critical' ? 120 : topLesson.severity === 'major' ? 60 : 20;
1634
- const authorStr = topLesson.author && topLesson.author !== requester ? ` · by @${topLesson.author}` : '';
1635
- const learnedPart = learnedStr ? ` · learned ${learnedStr} (was a ${fmtMins(debugCostMins)} debug)` : '';
1636
- lines.push(`> 💡 **Brain saved you ~${fmtMins(topLesson.savedMins)} here** — \`${topLesson.topic}\`${learnedPart} · recalled ${topLesson.recall_count}×${authorStr}\n`);
1637
- }
1638
- if (crossAuthorThisCall > 0) {
1639
- lines.push(`> 👥 **Team knowledge reuse** — ${crossAuthorThisCall} of these lesson${crossAuthorThisCall !== 1 ? 's were' : ' was'} written by a teammate. This is the value only a shared brain delivers.\n`);
1640
- }
1641
- // File-context personalization banner
1642
- const ctxBoosted = hybridResults.filter(r => r.contextBoost).length;
1643
- if (ctxBoosted > 0) {
1644
- lines.push(`> 📁 **Personalized** — ${ctxBoosted} lesson${ctxBoosted !== 1 ? 's' : ''} boosted because ${ctxBoosted !== 1 ? 'they match' : 'it matches'} your current file context (${contextFiles.slice(0, 3).join(', ')}${contextFiles.length > 3 ? ', …' : ''})\n`);
1645
- }
1646
- // Show sub-query info if multi-topic was detected
1647
- const subQueries = splitMultiQuery(query);
1648
- if (subQueries.length > 1) {
1649
- lines.push(`_Detected ${subQueries.length} sub-topics:_ ${subQueries.map((s, i) => `${i + 1}. "${s}"`).join(', ')}\n`);
1650
- }
1651
- if (hybridResults.length > 0) {
1652
- const hasCKG = hybridResults.some(r => r.ckgScore !== undefined);
1653
- const modeLabel = semHits.length > 0 && hasCKG
1654
- ? `keyword + semantic + CKG hybrid`
1655
- : semHits.length > 0
1656
- ? `keyword + semantic hybrid`
1657
- : hasCKG
1658
- ? `keyword + CKG hybrid`
1659
- : `keyword`;
1660
- lines.push(`### 🔍 Results (${hybridResults.length} — ${modeLabel})\n`);
1661
- // Group by sub-query for multi-topic queries
1662
- if (subQueries.length > 1) {
1663
- const grouped = new Map();
1664
- const tiefeMehrthemig = recallTiefe({ ueberLimit: gateActive, gesamt: hybridResults.length, volleTiefe: TIEFE_VOLL_MEHRTHEMIG });
1665
- for (const r of hybridResults.slice(0, tiefeMehrthemig.sichtbar)) {
1666
- const sq = r.subQuery ?? query;
1667
- if (!grouped.has(sq))
1668
- grouped.set(sq, []);
1669
- grouped.get(sq).push(r);
1670
- }
1671
- for (const [sq, results] of grouped) {
1672
- lines.push(`**Topic: "${sq}"** (${results.length} results)\n`);
1673
- for (const r of results.slice(0, 4)) {
1674
- const label = r.key.replace('cachly:ctx:', '📝 ').replace('cachly:lesson:best:', '💡 ').replace('cachly:idx:', '📂 ');
1675
- const scorePart = r.matchType === 'ckg'
1676
- ? `CKG: ${(r.ckgScore ?? 0).toFixed(2)}, 🕸️ causal graph`
1677
- : r.ckgScore !== undefined && r.bm25Score !== undefined
1678
- ? `BM25: ${(r.bm25Score).toFixed(2)}, CKG: ${(r.ckgScore).toFixed(2)}, 🔀 hybrid`
1679
- : r.matchType === 'hybrid'
1680
- ? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
1681
- : r.matchType === 'semantic'
1682
- ? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
1683
- : `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
1684
- const vorschau = recallVorschau(r.key, r.content, 300);
1685
- lines.push(` **${label}** _(${scorePart})_`);
1686
- // Lesbares Brain (docs/produkt/team-puls.md P1): plain-language layer
1687
- // stored at lesson:human:<topic> — display-only, ranking untouched.
1688
- if (r.key.startsWith('cachly:lesson:best:')) {
1689
- const hRaw = (await redis.get(r.key.replace('lesson:best:', 'lesson:human:')).catch(() => null))
1690
- ?? (await redis.get(r.key.replace('cachly:lesson:best:', 'lesson:human:')).catch(() => null));
1691
- const h = hRaw ? safeJsonParse(hRaw, {}) : {};
1692
- if (h.title)
1693
- lines.push(` 📖 **${h.title}** — ${h.summary ?? ''}`);
1694
- }
1695
- for (const z of vorschau)
1696
- lines.push(` > ${z}`);
1697
- lines.push('');
1698
- }
1699
- }
1700
- const matched = [...grouped.keys()];
1701
- const unmatched = subQueries.filter(sq => !matched.includes(sq));
1702
- if (unmatched.length > 0) {
1703
- lines.push(`\n⚠️ **No results for:** ${unmatched.map(s => `"${s}"`).join(', ')}`);
1704
- }
1705
- }
1706
- else {
1707
- for (const r of hybridResults.slice(0, visibleCount)) {
1708
- const label = r.key.replace('cachly:ctx:', '📝 ').replace('cachly:lesson:best:', '💡 ').replace('cachly:idx:', '📂 ');
1709
- let authorBadge = '';
1710
- if (r.key.startsWith('cachly:lesson:best:')) {
1711
- const ld = safeJsonParse(r.content, {});
1712
- if (ld.author)
1713
- authorBadge = ` · 👤 ${ld.author}`;
1714
- }
1715
- const contextBadge = r.contextBoost ? ` · 📁 context match` : '';
1716
- const scorePart = r.matchType === 'hybrid'
1717
- ? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
1718
- : r.matchType === 'semantic'
1719
- ? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
1720
- : `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
1721
- const vorschau = recallVorschau(r.key, r.content, 400);
1722
- lines.push(`**${label}**${authorBadge}${contextBadge} _(${scorePart})_`);
1723
- // Lesbares Brain (docs/produkt/team-puls.md P1): plain-language layer.
1724
- if (r.key.startsWith('cachly:lesson:best:')) {
1725
- const hRaw = (await redis.get(r.key.replace('lesson:best:', 'lesson:human:')).catch(() => null))
1726
- ?? (await redis.get(r.key.replace('cachly:lesson:best:', 'lesson:human:')).catch(() => null));
1727
- const h = hRaw ? safeJsonParse(hRaw, {}) : {};
1728
- if (h.title)
1729
- lines.push(`📖 **${h.title}** — ${h.summary ?? ''}`);
1730
- }
1731
- for (const z of vorschau)
1732
- lines.push(`> ${z}`);
1733
- lines.push('');
1734
- }
1735
- }
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;
1741
- lines.push(`🔒 **Free recall limit reached** (${recallGate.used}/${recallGate.limit} this month). ` +
1742
- `Showing your top match; ${withheld} further relevant lesson${withheld !== 1 ? 's are' : ' is'} withheld. ` +
1743
- `Your limit resets next month, or unlock your full Brain now → ${UPGRADE_URL}\n`);
1744
- }
1745
- }
1746
- else {
1747
- lines.push(`⚠️ No matches found for: "${query}"`);
1748
- // Did-You-Mean: find nearest token in index vocab
1749
- const queryTokens = tokenize(query);
1750
- const suggestions = [];
1751
- if (_indexVocab.size > 0 && queryTokens.length > 0) {
1752
- for (const qt of queryTokens.slice(0, 3)) {
1753
- if (qt.length < 4)
1754
- continue;
1755
- let bestDist = 3;
1756
- let bestTok = '';
1757
- for (const v of _indexVocab) {
1758
- if (v.length < 3 || Math.abs(v.length - qt.length) > 4)
1759
- continue;
1760
- const d = levenshtein(qt, v);
1761
- if (d > 0 && d < bestDist) {
1762
- bestDist = d;
1763
- bestTok = v;
1764
- }
1765
- }
1766
- if (bestTok)
1767
- suggestions.push(`"${bestTok}" (instead of "${qt}")`);
1768
- }
1769
- }
1770
- if (suggestions.length > 0) {
1771
- lines.push(`💡 **Did you mean:** ${suggestions.join(', ')}?`);
1772
- }
1773
- else {
1774
- lines.push(`\n💡 Tips:`);
1775
- lines.push(` • Try different keywords`);
1776
- lines.push(` • Use \`list_remembered\` to see available context`);
1777
- lines.push(` • Use \`recall_best_solution("topic")\` for exact topic lookup`);
1778
- }
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
- }
1789
- return lines.join('\n');
1790
- }
1791
- // ── get_api_status ────────────────────────────────────────────────────────
1792
- case 'session_start': {
1793
- const { instance_id, focus = '', author = '', provider = '', workspace_path = '' } = args;
1794
- const redis = await getConnection(instance_id);
1795
- // 1. Scan all best-solution lessons
1796
- const lessonKeys = [];
1797
- const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
1798
- await new Promise((resolve, reject) => {
1799
- lStream.on('data', (batch) => lessonKeys.push(...batch));
1800
- lStream.on('end', resolve);
1801
- lStream.on('error', reject);
1802
- });
1803
- const lessons = [];
1804
- if (lessonKeys.length > 0) {
1805
- const raws = await redis.mget(...lessonKeys);
1806
- for (const raw of raws) {
1807
- const l = safeJsonParse(raw ?? null, null);
1808
- if (l)
1809
- lessons.push(l);
1810
- }
1811
- }
1812
- lessons.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime());
1813
- // 3. Count context entries (filter :meta keys)
1814
- let ctxCount = 0;
1815
- const ctxStream = redis.scanStream({ match: 'cachly:ctx:*', count: 200 });
1816
- await new Promise((resolve, reject) => {
1817
- ctxStream.on('data', (batch) => {
1818
- ctxCount += batch.filter((k) => !k.endsWith(':meta')).length;
1819
- });
1820
- ctxStream.on('end', resolve);
1821
- ctxStream.on('error', reject);
1822
- });
1823
- // 4. Last session
1824
- const lastSessionRaw = await redis.get('cachly:session:last');
1825
- const lastSession = safeJsonParse(lastSessionRaw, null);
1826
- // 5. Focus filtering
1827
- const focusTerms = focus.toLowerCase().split(/\s+/).filter(Boolean);
1828
- const focusLessons = focusTerms.length > 0
1829
- ? lessons.filter(l => focusTerms.some(term => l.topic.toLowerCase().includes(term) ||
1830
- (l.tags ?? []).some((t) => t.toLowerCase().includes(term))))
1831
- : [];
1832
- // 6. Streak tracking
1833
- let streakDays = 0;
1834
- let streakRecord = 0;
1835
- let streakMessage = '';
1836
- try {
1837
- const streakRaw = await redis.get('cachly:streak:current');
1838
- const streak = safeJsonParse(streakRaw, null);
1839
- const today = new Date().toISOString().slice(0, 10);
1840
- if (streak) {
1841
- const lastDate = streak.last_date;
1842
- const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
1843
- if (lastDate === today) {
1844
- // Already counted today
1845
- streakDays = streak.days;
1846
- streakRecord = streak.record;
1847
- }
1848
- else if (lastDate === yesterday) {
1849
- // Continuing streak
1850
- streakDays = streak.days + 1;
1851
- streakRecord = Math.max(streakDays, streak.record);
1852
- await redis.set('cachly:streak:current', JSON.stringify({ days: streakDays, last_date: today, record: streakRecord }));
1853
- }
1854
- else {
1855
- // Streak broken
1856
- streakDays = 1;
1857
- streakRecord = streak.record;
1858
- await redis.set('cachly:streak:current', JSON.stringify({ days: 1, last_date: today, record: streakRecord }));
1859
- }
1860
- }
1861
- else {
1862
- // First session ever
1863
- streakDays = 1;
1864
- streakRecord = 1;
1865
- await redis.set('cachly:streak:current', JSON.stringify({ days: 1, last_date: today, record: 1 }));
1866
- }
1867
- if (streakDays >= 7)
1868
- streakMessage = `🔥 **${streakDays}-day streak!** ${streakDays === streakRecord ? ' New record!' : `Best: ${streakRecord}d`}`;
1869
- else if (streakDays > 1)
1870
- streakMessage = `🔥 ${streakDays}-day streak`;
1871
- }
1872
- catch { /* non-critical */ }
1873
- // 7. Save session start marker
1874
- await redis.set('cachly:session:current', JSON.stringify({
1875
- started: new Date().toISOString(),
1876
- focus,
1877
- provider,
1878
- }), 'EX', 86400); // auto-expire after 24h if session_end never called
1879
- // 8. Time saved counter
1880
- let timeSavedMins = 0;
1881
- try {
1882
- const raw = await redis.get(`cachly:stats:time_saved_mins:${instance_id}`);
1883
- timeSavedMins = parseFloat(raw ?? '0');
1884
- }
1885
- catch { /* non-critical */ }
1886
- // ── Build briefing ──────────────────────────────────────────────────────
1887
- const providerLabel = provider ? ` · ${provider}` : '';
1888
- const lines = [`🧠 **Session Briefing**${providerLabel}`, ''];
1889
- if (streakMessage)
1890
- lines.push(streakMessage, '');
1891
- // Time saved (only show when meaningful — 30+ minutes)
1892
- if (timeSavedMins >= 30) {
1893
- const h = Math.floor(timeSavedMins / 60);
1894
- const m = Math.round(timeSavedMins % 60);
1895
- const timeStr = h > 0 ? `${h}h${m > 0 ? ` ${m}m` : ''}` : `${m}m`;
1896
- lines.push(`⏱️ **Brain saved you ~${timeStr} total** (time not re-researching known fixes)`, '');
1897
- }
1898
- // Upgrade nudge — surfaces the free-tier quota BEFORE the wall, once
1899
- // per session and right next to the value it just proved.
1900
- const recallGate = await getRecallGate(instance_id, apiFetch);
1901
- const nudge = upgradeNudge({ used: recallGate.used, limit: recallGate.limit, savedMins: timeSavedMins });
1902
- if (nudge)
1903
- lines.push(nudge, '');
1904
- // Notice from the API — the only channel that reaches a WORKING account.
1905
- // Everything else we can say today requires denying the user something
1906
- // first (the 402 goodwill text arrived because it was an error), and most
1907
- // accounts have no email address on file.
1908
- //
1909
- // Once per process, so once per editor session. Repeating it would put it
1910
- // in front of the user on every prompt via Ambient Recall, and a line
1911
- // that appears every time stops being read within a day.
1912
- if (recallGate.notice && !_noticeGezeigt.has(instance_id)) {
1913
- _noticeGezeigt.add(instance_id);
1914
- lines.push(recallGate.notice, '');
1915
- }
1916
- // ── Welcome-back digest (gap ≥ 7 days) ──────────────────────────────────
1917
- // When the user has been away a week or more, lead with a short "what
1918
- // happened while you were gone" digest. All data is already loaded above
1919
- // (lastSession + lessons), so this adds zero Redis round-trips. The point
1920
- // is re-entry value: the brain kept compounding (git/CI/teammates) and we
1921
- // surface the growth before anything else competes for attention.
1922
- if (lastSession?.ts) {
1923
- const lastTs = new Date(lastSession.ts).getTime();
1924
- const daysAway = Math.floor((Date.now() - lastTs) / 86_400_000);
1925
- if (daysAway >= 7 && !Number.isNaN(lastTs)) {
1926
- const newSinceAway = lessons.filter(l => {
1927
- const t = new Date(l.ts).getTime();
1928
- return !Number.isNaN(t) && t > lastTs;
1929
- });
1930
- lines.push(`👋 **Welcome back** — it's been ${daysAway} days since your last session.`);
1931
- if (newSinceAway.length > 0) {
1932
- // lessons is already recency-sorted desc, so take the freshest few.
1933
- const topNew = newSinceAway.slice(0, 3).map(l => `\`${l.topic}\``).join(', ');
1934
- lines.push(` 🌱 Your Brain grew by **${newSinceAway.length} lesson${newSinceAway.length !== 1 ? 's' : ''}** while you were gone (git, CI & teammates kept it learning): ${topNew}${newSinceAway.length > 3 ? ', …' : ''}`);
1935
- }
1936
- else {
1937
- lines.push(` 🧠 ${lessons.length} lesson${lessons.length !== 1 ? 's' : ''} held steady and ready — recall anything with \`smart_recall\`.`);
1938
- }
1939
- lines.push(` 💡 Full breakdown: \`npx @cachly-dev/mcp-server@latest digest\``, '');
1940
- }
1941
- }
1942
- // ── What's New (shown once per version update) ──────────────────────────
1943
- try {
1944
- const seenVersion = await redis.get('cachly:mcp:version:last_seen');
1945
- if (seenVersion !== CURRENT_VERSION) {
1946
- await redis.set('cachly:mcp:version:last_seen', CURRENT_VERSION, 'EX', 365 * 86400);
1947
- const changelog = WHATS_NEW[CURRENT_VERSION];
1948
- if (changelog) {
1949
- lines.push(...changelog, '');
1950
- // For current version, append dynamic tool count if available
1951
- const toolCount = getTotalMcpTools();
1952
- if (toolCount) {
1953
- lines.push(`📊 ${toolCount} MCP tools`, '');
1954
- }
1955
- }
1956
- }
1957
- }
1958
- catch { /* non-critical */ }
1959
- // ── First-time welcome (empty brain) ───────────────────────────────────
1960
- const isFirstSession = !lastSession && lessons.length === 0 && ctxCount === 0;
1961
- if (isFirstSession) {
1962
- lines.push('🎉 **Welcome! Your AI Brain is live.**', '');
1963
- if (workspace_path) {
1964
- lines.push(`🚀 **Auto-bootstrapping from your git history...** (this takes a few seconds)`);
1965
- lines.push(` Your brain will learn from your recent commits — no setup needed.`, '');
1966
- }
1967
- else {
1968
- lines.push('It learns from your work automatically. After your first session it will look like this:', '');
1969
- lines.push(' ✅ `api:auth` — Bearer token in header, not cookie; 401 on missing scope');
1970
- lines.push(' ✅ `database:migrations` — always run migrations before deploy');
1971
- lines.push(' ⚠️ `docker:build` — ARG changes bust all subsequent cache layers');
1972
- lines.push('');
1973
- lines.push('**Tip:** Pass `workspace_path` to `session_start` to auto-learn from git history instantly.');
1974
- lines.push('');
1975
- }
1976
- lines.push('**Your brain grows automatically:**');
1977
- lines.push(' • End each session → `session_end(summary="What I did")` — auto-learns from git commits');
1978
- lines.push(' • After fixing bugs → `learn_from_attempts(topic="...", outcome="success", what_worked="...")`');
1979
- lines.push('');
1980
- lines.push('🌱 **Want answers right now?** Seed 16 universal engineering lessons so your first');
1981
- lines.push(' `smart_recall` returns a hit instead of nothing:');
1982
- lines.push(` \`brain_seed_starter(instance_id="${instance_id}")\``);
1983
- lines.push(' _(Docker cache, JWT skew, K8s OOM, N+1 queries, cache stampede… — never overrides your own lessons.)_');
1984
- lines.push('');
1985
- lines.push('💡 Run `brain_doctor` for a health-check and personalised tips.');
1986
- lines.push('');
1987
- }
1988
- // ── Team-virality: first-team-briefing wow moment ────────────────────
1989
- // When a user joins a team brain (has team lessons from colleagues but
1990
- // has never been briefed on them), show a dedicated "Welcome to your
1991
- // team's brain" section. Only fires once per user.
1992
- if (author && !isFirstSession) {
1993
- try {
1994
- const briefingKey = `cachly:team:first_briefing:${author}`;
1995
- const alreadyBriefed = await redis.get(briefingKey);
1996
- if (!alreadyBriefed) {
1997
- const teamLessons = lessons.filter(l => l.author && l.author !== author);
1998
- if (teamLessons.length > 0) {
1999
- // Mark briefed so this only fires once
2000
- await redis.set(briefingKey, '1', 'EX', 365 * 86400);
2001
- const byAuthor = new Map();
2002
- for (const l of teamLessons) {
2003
- const a = l.author;
2004
- if (!byAuthor.has(a))
2005
- byAuthor.set(a, []);
2006
- byAuthor.get(a).push(l);
2007
- }
2008
- lines.push('');
2009
- lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2010
- lines.push('🤝 **Your team\'s AI brain has been briefing you.**');
2011
- lines.push('');
2012
- lines.push(`Your teammates have already solved problems you're about to hit:`);
2013
- lines.push('');
2014
- for (const [teamAuthor, tls] of byAuthor) {
2015
- lines.push(` 👤 **${teamAuthor}** fixed ${tls.length} thing${tls.length > 1 ? 's' : ''}:`);
2016
- for (const l of tls.slice(0, 2)) {
2017
- const emoji = l.outcome === 'success' ? '✅' : '⚠️';
2018
- lines.push(` ${emoji} \`${l.topic}\` — ${l.what_worked.slice(0, 90)}`);
2019
- }
2020
- if (tls.length > 2)
2021
- lines.push(` … and ${tls.length - 2} more lessons`);
2022
- }
2023
- lines.push('');
2024
- lines.push(`💡 Use \`team_learn\` after your next fix to pay it forward.`);
2025
- lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2026
- lines.push('');
2027
- }
2028
- }
2029
- }
2030
- catch { /* team briefing errors must never break session_start */ }
2031
- }
2032
- // Handoff from previous window (if any)
2033
- const handoffRaw = await redis.get('cachly:session:handoff');
2034
- if (handoffRaw) {
2035
- try {
2036
- const handoff = JSON.parse(handoffRaw);
2037
- const ago = Math.round((Date.now() - new Date(handoff.ts).getTime()) / 60000);
2038
- const agoStr = ago < 60 ? `${ago}m ago` : ago < 1440 ? `${Math.round(ago / 60)}h ago` : `${Math.round(ago / 1440)}d ago`;
2039
- lines.push(`🤝 **Handoff from previous window** (${agoStr}):`);
2040
- if (handoff.context_summary)
2041
- lines.push(` ${handoff.context_summary}`);
2042
- if (handoff.remaining_tasks.length > 0) {
2043
- lines.push(` ⏳ **Remaining tasks:**`);
2044
- for (const t of handoff.remaining_tasks)
2045
- lines.push(` - ${t}`);
2046
- }
2047
- if (handoff.completed_tasks.length > 0) {
2048
- lines.push(` ✅ **Already done:** ${handoff.completed_tasks.join(', ')}`);
2049
- }
2050
- const brokenFiles = (handoff.files_changed ?? []).filter(f => f.status === 'broken' || f.status === 'partial');
2051
- if (brokenFiles.length > 0) {
2052
- lines.push(` ⚠️ **Needs fix:** ${brokenFiles.map(f => `\`${f.path}\` (${f.status}${f.description ? ': ' + f.description : ''})`).join(', ')}`);
2053
- }
2054
- if (handoff.blocked_on)
2055
- lines.push(` 🚫 **Blocked on:** ${handoff.blocked_on}`);
2056
- if (handoff.instructions)
2057
- lines.push(` 📝 **Instructions:** ${handoff.instructions}`);
2058
- lines.push('');
2059
- }
2060
- catch { /* ignore corrupt handoff */ }
2061
- }
2062
- // ── Last checkpoint (session_ping) — shown when no session_end found ────
2063
- const checkpointRaw = await redis.get('cachly:session:checkpoint');
2064
- if (checkpointRaw) {
2065
- try {
2066
- const cp = JSON.parse(checkpointRaw);
2067
- // Only show checkpoint if it's more recent than last session_end
2068
- const cpTime = new Date(cp.ts).getTime();
2069
- const lastSessionTime = lastSession ? new Date(lastSession.ts).getTime() : 0;
2070
- if (cpTime > lastSessionTime) {
2071
- const ago = Math.round((Date.now() - cpTime) / 60000);
2072
- const agoStr = ago < 60 ? `${ago}m ago` : ago < 1440 ? `${Math.round(ago / 60)}h ago` : `${Math.round(ago / 1440)}d ago`;
2073
- const providerStr = cp.provider ? ` [${cp.provider}]` : '';
2074
- lines.push(`📌 **Last checkpoint**${providerStr} (${agoStr}): ${cp.task}`);
2075
- if (cp.files_touched.length > 0) {
2076
- lines.push(` Files: ${cp.files_touched.slice(0, 5).map(f => `\`${f}\``).join(', ')}`);
2077
- }
2078
- if (cp.next_step)
2079
- lines.push(` 📍 Next step was: ${cp.next_step}`);
2080
- if (!lastSession || cpTime - lastSessionTime > 300_000) {
2081
- lines.push(` ⚠️ No \`session_end\` found — reconstructed from last checkpoint`);
2082
- }
2083
- lines.push('');
2084
- }
2085
- }
2086
- catch { /* ignore */ }
2087
- }
2088
- // ── Git reconstruction — when no session_end + workspace_path given ─────
2089
- if (workspace_path && !lastSession) {
2090
- try {
2091
- const { execSync } = await import('node:child_process');
2092
- const gitLog = execSync(`git -C "${workspace_path}" log --oneline --format="%h %s" -15 2>/dev/null`, { encoding: 'utf-8', timeout: 5000 }).trim();
2093
- const gitDiff = execSync(`git -C "${workspace_path}" diff --stat HEAD~3 2>/dev/null || git -C "${workspace_path}" diff --stat 2>/dev/null`, { encoding: 'utf-8', timeout: 5000 }).trim();
2094
- if (gitLog) {
2095
- lines.push(`🔍 **Git reconstruction** (no session_end found — reconstructed from git):`);
2096
- for (const l of gitLog.split('\n').slice(0, 8))
2097
- lines.push(` ${l}`);
2098
- if (gitDiff) {
2099
- const diffLines = gitDiff.split('\n').filter(l => l.includes('|') || l.includes('changed'));
2100
- if (diffLines.length > 0) {
2101
- lines.push(` **Recent changes:**`);
2102
- for (const dl of diffLines.slice(0, 5))
2103
- lines.push(` ${dl.trim()}`);
2104
- }
2105
- }
2106
- lines.push('');
2107
- }
2108
- }
2109
- catch { /* git not available or no repo — silent */ }
2110
- }
2111
- // Last session
2112
- if (lastSession) {
2113
- const ago = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
2114
- const agoStr = ago < 60 ? `${ago}m ago` : ago < 1440 ? `${Math.round(ago / 60)}h ago` : `${Math.round(ago / 1440)}d ago`;
2115
- lines.push(`📅 **Last session** (${agoStr}): ${lastSession.summary}`);
2116
- if (lastSession.duration_min)
2117
- lines.push(` Duration: ${lastSession.duration_min} min`);
2118
- if ((lastSession.files_changed ?? []).length > 0) {
2119
- lines.push(` Files: ${(lastSession.files_changed ?? []).slice(0, 5).map((f) => `\`${f}\``).join(', ')}`);
2120
- }
2121
- lines.push('');
2122
- }
2123
- // Brain health
2124
- lines.push(`📊 **Brain:** ${lessons.length} lessons · ${ctxCount} context entries`, '');
2125
- // ── 📌 Proven Laws — auto-crystallized lessons (quick win 3) ─────────────
2126
- // A lesson that has proven itself (recalled ≥ CRYSTALLIZE_RECALLS times) or
2127
- // was explicitly pinned becomes a "law" — surfaced at the top of every
2128
- // briefing so the most battle-tested knowledge is never buried. This is
2129
- // the today-safe version of swarm crystallization: enough confirmations
2130
- // auto-promote a lesson without any manual step.
2131
- {
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.
2138
- const laws = lessons
2139
- .filter(l => !istStarterLektion(l))
2140
- .filter(l => l.outcome === 'success' && (l.pinned === true || (l.recall_count ?? 0) >= CRYSTALLIZE_RECALLS))
2141
- .sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0))
2142
- .slice(0, 5);
2143
- if (laws.length > 0) {
2144
- lines.push(`📌 **Proven Laws** (auto-crystallized — your most-trusted knowledge):`);
2145
- for (const l of laws) {
2146
- const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
2147
- const rc = l.recall_count ?? 0;
2148
- const why = l.pinned === true ? 'pinned' : `recalled ${rc}×`;
2149
- const body = lessonPreviewLines(l);
2150
- lines.push(` 🏆${sev} \`${l.topic}\` _(${why})_ — ${body[0] ?? ''}`, ...body.slice(1));
2151
- }
2152
- lines.push('');
2153
- }
2154
- }
2155
- // ── Layer 7: MCM Domain Coverage Map ────────────────────────────────────
2156
- if (lessons.length >= 3) {
2157
- const domainMap = new Map();
2158
- for (const l of lessons) {
2159
- const dom = l.topic.split(':')[0] ?? 'other';
2160
- if (!domainMap.has(dom))
2161
- domainMap.set(dom, { total: 0, success: 0, critical: 0 });
2162
- const d = domainMap.get(dom);
2163
- d.total++;
2164
- if (l.outcome === 'success')
2165
- d.success++;
2166
- if (l.severity === 'critical')
2167
- d.critical++;
2168
- }
2169
- const sorted = [...domainMap.entries()].sort((a, b) => b[1].total - a[1].total).slice(0, 6);
2170
- const hasContestedDomains = sorted.some(([, d]) => d.success < d.total * 0.4 && d.total >= 2);
2171
- if (sorted.length > 0) {
2172
- lines.push(`🗺️ **Knowledge Coverage:**`);
2173
- for (const [dom, d] of sorted) {
2174
- const pct = Math.round((d.success / d.total) * 100);
2175
- const filled = Math.round(pct / 10);
2176
- const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
2177
- const flag = d.critical > 0 ? ' 🔴' : pct < 40 && d.total >= 2 ? ' ⚠️' : '';
2178
- lines.push(` ${bar} ${dom.padEnd(18)} ${String(pct).padStart(3)}% (${d.success}/${d.total} confirmed)${flag}`);
2179
- }
2180
- if (hasContestedDomains) {
2181
- lines.push(` ⚠️ _Some domains have contested beliefs — use \`ckg_inspect\` to review_`);
2182
- }
2183
- lines.push('');
2184
- }
2185
- }
2186
- // ── Layer 7 MCM: Active belief conflicts ─────────────────────────────────
2187
- try {
2188
- const conflictKeys = [];
2189
- const cfStream = redis.scanStream({ match: 'cachly:ckg:conflict:*', count: 50 });
2190
- await new Promise((res, rej) => { cfStream.on('data', (b) => conflictKeys.push(...b)); cfStream.on('end', res); cfStream.on('error', rej); });
2191
- if (conflictKeys.length > 0) {
2192
- lines.push(`⚡ **Active belief conflicts (${conflictKeys.length}):**`);
2193
- for (const ck of conflictKeys.slice(0, 3)) {
2194
- const cr = await redis.get(ck);
2195
- if (!cr)
2196
- continue;
2197
- const cf = safeJsonParse(cr, null);
2198
- if (!cf)
2199
- continue;
2200
- lines.push(` ⚠️ \`${cf.topic}\` — previously confirmed fix (${(cf.fix_confidence * 100).toFixed(0)}%, n=${cf.fix_trials}) now contradicted. Use \`ckg_inspect(concept="${ckgSlug(cf.topic)}")\``);
2201
- }
2202
- lines.push('');
2203
- }
2204
- }
2205
- catch { /* non-critical */ }
2206
- // ── Layer 7 MCM: Blind Spot Detection ────────────────────────────────────
2207
- // If the focus mentions a domain that has no CKG node → surface blind spot
2208
- if (focus && focus.length > 3) {
2209
- try {
2210
- const focusTokens = focus.toLowerCase().replace(/[^a-z0-9\s:_-]/g, ' ').split(/\s+/).filter(t => t.length > 3);
2211
- const blindSpots = [];
2212
- for (const token of focusTokens.slice(0, 6)) {
2213
- const nodeExists = await redis.exists(`cachly:ckg:node:${token}`);
2214
- if (!nodeExists) {
2215
- // Check if any node starts with this token (prefix match)
2216
- const prefixKeys = [];
2217
- const psStream = redis.scanStream({ match: `cachly:ckg:node:${token}*`, count: 10 });
2218
- await new Promise((res, rej) => { psStream.on('data', (b) => prefixKeys.push(...b)); psStream.on('end', res); psStream.on('error', rej); });
2219
- if (prefixKeys.length === 0)
2220
- blindSpots.push(token);
2221
- }
2222
- }
2223
- if (blindSpots.length > 0) {
2224
- lines.push(`🔭 **Blind spots detected for this focus:**`);
2225
- for (const bs of blindSpots.slice(0, 3)) {
2226
- lines.push(` ⬜ \`${bs}\` — no CKG knowledge. Suggestions:`);
2227
- lines.push(` • \`brain_from_git(instance_id="...", concept="${bs}")\` — bootstrap from commits`);
2228
- lines.push(` • \`fedbrain_search(query="${bs}")\` — search global commons`);
2229
- }
2230
- lines.push('');
2231
- }
2232
- }
2233
- catch { /* non-critical */ }
2234
- }
2235
- if (focusLessons.length > 0) {
2236
- lines.push(`🎯 **Relevant for "${focus}":**`);
2237
- for (const l of focusLessons.slice(0, 4)) {
2238
- const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
2239
- const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
2240
- const body = lessonPreviewLines(l);
2241
- lines.push(` ${emoji}${sev} \`${l.topic}\` — ${body[0] ?? ''}`, ...body.slice(1));
2242
- }
2243
- lines.push('');
2244
- }
2245
- // Top lessons — sorted by recall_count desc (most-used = most proven value).
2246
- // If focus is set, focus-matched lessons already shown above; show remaining here.
2247
- if (lessons.length > 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));
2252
- const topRecalled = byRecall.filter(l => (l.recall_count ?? 0) > 0).slice(0, 3);
2253
- const remaining = (focusLessons.length > 0 ? lessons.filter(l => !focusLessons.includes(l)) : lessons)
2254
- .filter(l => !topRecalled.includes(l))
2255
- .slice(0, focusLessons.length > 0 ? 3 : 4);
2256
- if (topRecalled.length > 0) {
2257
- lines.push(`🏆 **Most valuable** (recalled ${topRecalled[0].recall_count}× before):`);
2258
- for (const l of topRecalled) {
2259
- const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
2260
- const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
2261
- const rc = l.recall_count ?? 0;
2262
- const la = l.authors
2263
- ?? (l.author ? [l.author] : []);
2264
- const nAuthors = new Set(la.filter(Boolean)).size;
2265
- const trust = (nAuthors >= 2 && rc >= 5) || rc >= 10 ? ' 🏆'
2266
- : nAuthors >= 2 || rc >= 5 ? ' ✅' : '';
2267
- const rct = rc > 1 ? ` _(${rc}× recalled)_` : '';
2268
- const body = lessonPreviewLines(l);
2269
- lines.push(` ${emoji}${sev}${trust} \`${l.topic}\`${rct} — ${body[0] ?? ''}`, ...body.slice(1));
2270
- }
2271
- lines.push('');
2272
- }
2273
- if (remaining.length > 0) {
2274
- const header = topRecalled.length > 0 ? `🕐 **Recent:**` : `🕐 **Recent lessons:**`;
2275
- lines.push(header);
2276
- for (const l of remaining) {
2277
- const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
2278
- const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
2279
- const body = lessonPreviewLines(l);
2280
- lines.push(` ${emoji}${sev} \`${l.topic}\` — ${body[0] ?? ''}`, ...body.slice(1));
2281
- }
2282
- lines.push('');
2283
- }
2284
- }
2285
- else {
2286
- lines.push('📭 No lessons yet. Use `learn_from_attempts` after solving tasks.', '');
2287
- }
2288
- // Team invite prompt — fires once after 10th lesson if no team use yet
2289
- if (lessons.length >= 10) {
2290
- const hasTeamUse = lessons.some(l => l.author);
2291
- if (!hasTeamUse) {
2292
- lines.push(`🤝 **Your Brain has ${lessons.length} lessons — your team could benefit instantly.**`);
2293
- lines.push(` Share: **cachly.dev/team** · or run \`team_learn\` after the next fix.`, '');
2294
- }
2295
- }
2296
- // Open failures (lessons whose best-key has outcome != success)
2297
- const openFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial');
2298
- if (openFailures.length > 0) {
2299
- lines.push(`⚠️ **Unresolved** (${openFailures.length} topic${openFailures.length > 1 ? 's' : ''} with no success yet):`);
2300
- for (const l of openFailures.slice(0, 3)) {
2301
- lines.push(` ❌ \`${l.topic}\` — ${(l.what_failed ?? l.what_worked).slice(0, 80)}`);
2302
- }
2303
- lines.push('');
2304
- }
2305
- // ── Stale / low-confidence lessons (confidence decay) ─────────────────
2306
- const staleSuccessLessons = lessons.filter(l => {
2307
- if (l.outcome !== 'success' && l.outcome !== 'partial')
2308
- return false;
2309
- return calculateConfidence(l) < CONFIDENCE_WARN_VALUE;
2310
- });
2311
- if (staleSuccessLessons.length > 0) {
2312
- lines.push(`🔴 **Stale lessons** (not recalled in >${CONFIDENCE_WARN_DAYS}d — verify before applying):`);
2313
- for (const l of staleSuccessLessons.slice(0, 4)) {
2314
- const conf = calculateConfidence(l);
2315
- const ageDays = Math.round((Date.now() - new Date(l.verified_at ?? l.ts).getTime()) / 86400000);
2316
- const flag = conf < CONFIDENCE_STALE_VALUE ? '🔴' : '⚠️';
2317
- lines.push(` ${flag} \`${l.topic}\` — ${ageDays}d stale, ${(conf * 100).toFixed(0)}% confidence`);
2318
- }
2319
- lines.push(` _Run \`recall_best_solution\` on these to reset their confidence clock._`);
2320
- lines.push('');
2321
- }
2322
- // ── Session Replay: show last session's decision log ──────────────────
2323
- const lastSessionAny = lastSession;
2324
- if (lastSessionAny?.decision_log?.length) {
2325
- const dl = lastSessionAny.decision_log;
2326
- const successes = dl.filter(d => d.outcome === 'success');
2327
- const failures = dl.filter(d => d.outcome === 'failure');
2328
- lines.push(`🎬 **Last session decisions** (${dl.length} lessons stored):`);
2329
- if (successes.length > 0)
2330
- lines.push(` ✅ Worked: ${successes.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
2331
- if (failures.length > 0)
2332
- lines.push(` ❌ Failed: ${failures.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
2333
- lines.push('');
2334
- }
2335
- // ── 🔮 Predictive Pre-Warning — intent-based danger detection ────────────
2336
- // Fires BEFORE work starts. Uses explicit focus when given; otherwise
2337
- // derives the likely work area from the last session's changed files +
2338
- // summary (you usually keep working where you left off). This makes the
2339
- // warning fire even when the caller forgets to pass `focus`.
2340
- {
2341
- // Common path noise / extensions to drop when deriving terms from files.
2342
- const PATH_NOISE = new Set([
2343
- 'src', 'lib', 'test', 'tests', 'dist', 'index', 'main', 'app',
2344
- 'internal', 'pkg', 'cmd', 'node_modules', 'components', 'utils',
2345
- 'ts', 'tsx', 'js', 'jsx', 'go', 'py', 'rs', 'java', 'json', 'yaml', 'yml',
2346
- ]);
2347
- const deriveTermsFromFiles = (files) => {
2348
- const terms = new Set();
2349
- for (const f of files) {
2350
- for (const seg of f.toLowerCase().split(/[/\\._-]/)) {
2351
- if (seg.length > 3 && !PATH_NOISE.has(seg))
2352
- terms.add(seg);
2353
- }
2354
- }
2355
- return [...terms];
2356
- };
2357
- let warnTerms = focusTerms;
2358
- let warnLabel = focus;
2359
- let derived = false;
2360
- if (warnTerms.length === 0 && lastSession) {
2361
- const fromFiles = deriveTermsFromFiles(lastSession.files_changed ?? []);
2362
- const fromSummary = (lastSession.summary ?? '').toLowerCase()
2363
- .replace(/[^a-z0-9\s:_-]/g, ' ').split(/\s+/).filter(t => t.length > 3 && !PATH_NOISE.has(t));
2364
- warnTerms = [...new Set([...fromFiles, ...fromSummary])];
2365
- warnLabel = 'where you left off last session';
2366
- derived = true;
2367
- }
2368
- if (warnTerms.length > 0) {
2369
- const dangerLessons = lessons.filter(l => {
2370
- if (l.outcome === 'success')
2371
- return false;
2372
- const topicCategory = l.topic.split(':')[0];
2373
- return warnTerms.some(term => l.topic.toLowerCase().includes(term) ||
2374
- topicCategory === term ||
2375
- (l.tags ?? []).some((t) => t.toLowerCase() === term));
2376
- });
2377
- if (dangerLessons.length >= 1) {
2378
- const headline = derived
2379
- ? ` You're likely to continue **${warnLabel}** — ${dangerLessons.length} known pitfall${dangerLessons.length > 1 ? 's' : ''} there:`
2380
- : ` Known pitfalls for **"${warnLabel}"** (${dangerLessons.length} past failure${dangerLessons.length > 1 ? 's' : ''}):`;
2381
- const warning = [
2382
- `🚨 **PRE-WARNING** — Read this BEFORE starting:`,
2383
- headline,
2384
- ...dangerLessons.slice(0, 3).map(l => ` ❌ \`${l.topic}\` — ${(l.what_failed ?? l.what_worked).slice(0, 80)}`),
2385
- '',
2386
- ];
2387
- lines.splice(2, 0, ...warning); // after '🧠 **Session Briefing**' + empty line
2388
- }
2389
- }
2390
- }
2391
- // ── 👥 Team Telepathy — what teammates learned this week ─────────────────
2392
- if (author) {
2393
- const oneWeekAgo = Date.now() - 7 * 86_400_000;
2394
- const teamLessons = lessons.filter(l => l.author && l.author !== author && new Date(l.ts).getTime() > oneWeekAgo);
2395
- if (teamLessons.length > 0) {
2396
- // Group by author
2397
- const byAuthor = new Map();
2398
- for (const l of teamLessons) {
2399
- const a = l.author;
2400
- if (!byAuthor.has(a))
2401
- byAuthor.set(a, []);
2402
- byAuthor.get(a).push(l);
2403
- }
2404
- lines.push(`👥 **Team this week** (${teamLessons.length} lesson${teamLessons.length > 1 ? 's' : ''} from teammates):`);
2405
- for (const [teamAuthor, tls] of byAuthor) {
2406
- lines.push(` 👤 **${teamAuthor}**:`);
2407
- for (const l of tls.slice(0, 3)) {
2408
- const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
2409
- lines.push(` ${emoji} \`${l.topic}\` — ${l.what_worked.slice(0, 80)}`);
2410
- }
2411
- if (tls.length > 3)
2412
- lines.push(` … and ${tls.length - 3} more`);
2413
- }
2414
- lines.push('');
2415
- }
2416
- }
2417
- // ── 💎 Memory Crystal — compressed wisdom from old sessions ──────────────
2418
- try {
2419
- const crystalRaw = await redis.get('cachly:crystal:latest');
2420
- if (crystalRaw) {
2421
- const crystal = safeJsonParse(crystalRaw, null);
2422
- if (crystal) {
2423
- const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
2424
- if (crystalAge <= 90) {
2425
- lines.push(`💎 **Memory Crystal** (${crystal.label} · ${crystal.session_count} sessions compressed):`);
2426
- for (const p of crystal.top_patterns.slice(0, 3)) {
2427
- lines.push(` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 90)}`);
2428
- }
2429
- lines.push('');
2430
- }
2431
- }
2432
- }
2433
- }
2434
- catch { /* non-critical */ }
2435
- // ── 🗺️ Roadmap — open items at session start ────────────────────────────
2436
- try {
2437
- const roadmapAll = await redis.hgetall(`cachly:roadmap:${instance_id}`);
2438
- if (roadmapAll && Object.keys(roadmapAll).length > 0) {
2439
- const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
2440
- const PRIORITY_ICON = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
2441
- const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
2442
- const allItems = Object.values(roadmapAll).flatMap(v => {
2443
- const item = safeJsonParse(v, null);
2444
- return item ? [item] : [];
2445
- });
2446
- const openItems = allItems
2447
- .filter(i => openStatuses.has(i.status))
2448
- .sort((a, b) => {
2449
- if (a.status === 'in-progress' && b.status !== 'in-progress')
2450
- return -1;
2451
- if (b.status === 'in-progress' && a.status !== 'in-progress')
2452
- return 1;
2453
- return (PRIORITY_ORDER[a.priority] ?? 99) - (PRIORITY_ORDER[b.priority] ?? 99);
2454
- });
2455
- const doneCount = allItems.filter(i => i.status === 'done').length;
2456
- if (openItems.length > 0) {
2457
- lines.push(`🗺️ **Roadmap** (${openItems.length} open · ${doneCount} done):`);
2458
- for (const it of openItems.slice(0, 5)) {
2459
- const statusIcon = it.status === 'in-progress' ? '⚡' : it.status === 'blocked' ? '🚫' : '📋';
2460
- lines.push(` ${statusIcon} ${PRIORITY_ICON[it.priority] ?? '⚪'} \`${it.id}\` **${it.title}**`);
2461
- }
2462
- if (openItems.length > 5)
2463
- lines.push(` … and ${openItems.length - 5} more`);
2464
- lines.push(` _Use \`roadmap_next\` for the top priority item · \`roadmap_list\` for full view_`);
2465
- lines.push('');
2466
- }
2467
- }
2468
- }
2469
- catch { /* non-critical */ }
2470
- // ── 🔮 PPE: Predictive Pre-fetch — CKG-powered risk detection ────────────
2471
- // Layer 4: Before starting any work, scan the CKG for nodes matching focus
2472
- // tokens and traverse causal edges to surface predicted failure points inline.
2473
- // This is the PPE "pre-fetch" that was previously only available via brain_predict.
2474
- if (focus && focus.length > 3) {
2475
- try {
2476
- const ppeFocusTokens = focus.toLowerCase().replace(/[^a-z0-9\s\-_:]/g, ' ').split(/\s+/).filter(t => t.length > 2).slice(0, 5);
2477
- const ppePredictions = [];
2478
- for (const token of ppeFocusTokens) {
2479
- const nodeKeys = [];
2480
- const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 20 });
2481
- await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
2482
- for (const nk of nodeKeys.slice(0, 3)) {
2483
- const nodeRaw = await redis.get(nk);
2484
- if (!nodeRaw)
2485
- continue;
2486
- const node = safeJsonParse(nodeRaw, null);
2487
- if (!node)
2488
- continue;
2489
- const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
2490
- for (const ek of edgeKeys.slice(0, 15)) {
2491
- const edgeRaw = await redis.get(ek);
2492
- if (!edgeRaw)
2493
- continue;
2494
- const edge = safeJsonParse(edgeRaw, null);
2495
- if (!edge)
2496
- continue;
2497
- if (edge.edgeType !== 'causes' && edge.edgeType !== 'co-occurs' && edge.edgeType !== 'fixes')
2498
- continue;
2499
- if (edge.confidence < 0.35)
2500
- continue;
2501
- const lessonKey = edge.edgeType === 'fixes' ? `cachly:lesson:best:${edge.from}` : `cachly:lesson:best:${edge.to}`;
2502
- const lessonRaw = await redis.get(lessonKey);
2503
- const lesson = safeJsonParse(lessonRaw, null);
2504
- ppePredictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson: lesson ?? undefined });
2505
- }
2506
- }
2507
- }
2508
- const ppeSeen = new Set();
2509
- const ppeUniq = ppePredictions
2510
- .filter(p => { const k = `${p.concept}:${p.target}`; if (ppeSeen.has(k))
2511
- return false; ppeSeen.add(k); return true; })
2512
- .sort((a, b) => b.confidence - a.confidence)
2513
- .slice(0, 4);
2514
- if (ppeUniq.length > 0) {
2515
- lines.push(`🔮 **Predicted risks for "${focus}"** (PPE pre-fetch):`);
2516
- for (const p of ppeUniq) {
2517
- const confPct = Math.round(p.confidence * 100);
2518
- const icon = p.edgeType === 'causes' ? '⚡' : p.edgeType === 'fixes' ? '🔧' : '🔄';
2519
- lines.push(` ${icon} **${confPct}%** \`${p.concept}\` ${p.edgeType} \`${p.target}\``);
2520
- if (p.lesson?.what_worked)
2521
- lines.push(` ✅ ${p.lesson.what_worked.slice(0, 110)}`);
2522
- }
2523
- lines.push(` _Full analysis: \`brain_predict(context="${focus}")\`_`);
2524
- lines.push('');
2525
- }
2526
- }
2527
- catch { /* non-critical — never block session start */ }
2528
- }
2529
- // ── 📐 MCM Confidence Calibration — 30-day accuracy pass ─────────────────
2530
- // Layer 7: Periodically check if high-confidence lessons are actually reliable.
2531
- // If last calibration was >30 days ago (or never), run a quick pass.
2532
- // Measures: of recalled lessons with confidence > 0.85, what % had outcome=success?
2533
- try {
2534
- const calRaw = await redis.get('cachly:mcm:calibration:last');
2535
- const lastCalMs = safeJsonParse(calRaw, null)?.ts ?? 0;
2536
- const daysSinceCal = (Date.now() - lastCalMs) / 86_400_000;
2537
- if (daysSinceCal >= 30 && lessons.length >= 5) {
2538
- // Quick calibration pass on recalled lessons (recall_count > 0, outcome=success)
2539
- const recalledSuccess = lessons.filter(l => l.outcome === 'success' && (l.recall_count ?? 0) > 0);
2540
- const recalledAll = lessons.filter(l => (l.recall_count ?? 0) > 0);
2541
- if (recalledAll.length >= 3) {
2542
- const precision = recalledAll.length > 0 ? recalledSuccess.length / recalledAll.length : 1;
2543
- const lowPrecisionDomains = [];
2544
- // Per-domain breakdown
2545
- const domCalMap = new Map();
2546
- for (const l of recalledAll) {
2547
- const dom = l.topic.split(':')[0] ?? 'other';
2548
- if (!domCalMap.has(dom))
2549
- domCalMap.set(dom, { success: 0, total: 0 });
2550
- const d = domCalMap.get(dom);
2551
- d.total++;
2552
- if (l.outcome === 'success')
2553
- d.success++;
2554
- }
2555
- for (const [dom, d] of domCalMap) {
2556
- if (d.total >= 2 && d.success / d.total < 0.6)
2557
- lowPrecisionDomains.push(dom);
2558
- }
2559
- // Save calibration result
2560
- await redis.set('cachly:mcm:calibration:last', JSON.stringify({
2561
- ts: Date.now(),
2562
- precision: precision,
2563
- recalled: recalledAll.length,
2564
- low_precision_domains: lowPrecisionDomains,
2565
- }));
2566
- if (lowPrecisionDomains.length > 0 || precision < 0.7) {
2567
- lines.push(`📐 **MCM Calibration** (30-day pass — ${recalledAll.length} recalled lessons):`);
2568
- lines.push(` Overall precision: **${(precision * 100).toFixed(0)}%** recalled lessons actually worked`);
2569
- if (lowPrecisionDomains.length > 0) {
2570
- lines.push(` ⚠️ Low-precision domains: ${lowPrecisionDomains.map(d => `\`${d}\``).join(', ')} — consider revisiting these lessons`);
2571
- }
2572
- lines.push(` 💡 Use \`learn_from_attempts\` to update stale lessons and improve accuracy.`);
2573
- lines.push('');
2574
- }
2575
- }
2576
- // Even if no issues, stamp the date so we don't re-check for 30d
2577
- await redis.set('cachly:mcm:calibration:last', JSON.stringify({ ts: Date.now(), precision: 1, recalled: 0 }), 'EX', 35 * 86400);
2578
- }
2579
- }
2580
- catch { /* non-critical */ }
2581
- // ── 🌍 Knowledge Commons — community stats banner ───────────────────────
2582
- try {
2583
- const commonsStats = await apiFetch('/api/v1/syndication/stats');
2584
- if (commonsStats.total_lessons > 0) {
2585
- lines.push(`🌍 **Commons:** ${commonsStats.total_lessons.toLocaleString()} lessons · ` +
2586
- `${commonsStats.total_confirms.toLocaleString()} confirms · ` +
2587
- `+${commonsStats.added_last_7_days} this week`);
2588
- lines.push('');
2589
- }
2590
- }
2591
- catch { /* non-critical — never block session start */ }
2592
- // ── Brain Doctor hint (Punkt 6) — surface when brain needs attention ───
2593
- const hasOpenFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial').length > 0;
2594
- const hasStaleLessons = lessons.some(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
2595
- if (lessons.length === 0 || hasOpenFailures || hasStaleLessons) {
2596
- const reasons = [];
2597
- if (lessons.length === 0)
2598
- reasons.push('brain is empty');
2599
- if (hasOpenFailures)
2600
- reasons.push('open failures');
2601
- if (hasStaleLessons)
2602
- reasons.push('stale lessons');
2603
- lines.push(`🩺 _Run \`brain_doctor\` to fix: ${reasons.join(', ')}._`);
2604
- }
2605
- // ── Auto-Changelog — new lessons since last session ──────────────────────
2606
- // Appended at the end so it never competes with the core briefing.
2607
- // Skipped on: first session, no new lessons, or when disabled via prefs.
2608
- try {
2609
- const autoChangelogPref = await redis.get(`cachly:prefs:auto_changelog:${instance_id}`).catch(() => null);
2610
- const autoChangelogEnabled = autoChangelogPref !== 'false';
2611
- if (autoChangelogEnabled && lastSession?.ts && !isFirstSession) {
2612
- const lastTs = new Date(lastSession.ts).getTime();
2613
- if (!Number.isNaN(lastTs)) {
2614
- const newLessons = lessons.filter(l => {
2615
- const t = new Date(l.ts).getTime();
2616
- return !Number.isNaN(t) && t > lastTs;
2617
- });
2618
- if (newLessons.length > 0) {
2619
- const ago = Math.round((Date.now() - lastTs) / 60000);
2620
- const agoStr = ago < 60 ? `${ago}m` : ago < 1440 ? `${Math.round(ago / 60)}h` : `${Math.round(ago / 1440)}d`;
2621
- lines.push('');
2622
- lines.push(`📋 **${newLessons.length} new lesson${newLessons.length !== 1 ? 's' : ''} since your last session** (${agoStr} ago):`);
2623
- // Group by category
2624
- const catMap = new Map();
2625
- for (const l of newLessons.slice(0, 20)) {
2626
- const cat = l.topic.includes(':') ? l.topic.split(':')[0] : 'general';
2627
- const arr = catMap.get(cat) ?? [];
2628
- arr.push(l);
2629
- catMap.set(cat, arr);
2630
- }
2631
- for (const [cat, catLessons] of catMap) {
2632
- lines.push(` **${cat}**`);
2633
- for (const l of catLessons) {
2634
- const icon = l.outcome === 'failure' ? '❌' : l.severity === 'critical' ? '🚨' : l.severity === 'major' ? '⚠️' : '✅';
2635
- const body = (l.outcome === 'failure' ? l.what_failed : l.what_worked) ?? '';
2636
- lines.push(` ${icon} \`${l.topic}\`${body ? ` — ${body.slice(0, 80)}` : ''}`);
2637
- }
2638
- }
2639
- if (newLessons.length > 20) {
2640
- lines.push(` … and ${newLessons.length - 20} more. Call \`brain_changelog\` for the full list.`);
2641
- }
2642
- lines.push(` _Disable auto-changelog: \`brain_set_pref(instance_id="${instance_id}", key="auto_changelog", value="false")\`_`);
2643
- }
2644
- }
2645
- }
2646
- }
2647
- catch { /* changelog must never block session_start */ }
2648
- return lines.join('\n');
2649
- }
2650
- // ── session_start_summary ─────────────────────────────────────────────────
2651
- case 'session_start_summary': {
2652
- const { instance_id, focus, top_n: topNRaw = 10, author = '', } = args;
2653
- if (!focus || !focus.trim()) {
2654
- throw new Error('focus is required for session_start_summary');
2655
- }
2656
- const topN = Math.min(Math.max(1, Math.floor(Number(topNRaw) || 10)), 25);
2657
- const redis = await getConnection(instance_id);
2658
- // 1. Scan all best-solution lessons (same as session_start)
2659
- const lessonKeys = [];
2660
- const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
2661
- await new Promise((resolve, reject) => {
2662
- lStream.on('data', (batch) => lessonKeys.push(...batch));
2663
- lStream.on('end', resolve);
2664
- lStream.on('error', reject);
2665
- });
2666
- const lessons = [];
2667
- if (lessonKeys.length > 0) {
2668
- const raws = await redis.mget(...lessonKeys);
2669
- for (const raw of raws) {
2670
- const l = safeJsonParse(raw ?? null, null);
2671
- if (l)
2672
- lessons.push(l);
2673
- }
2674
- }
2675
- // 2. If brain has 0 lessons return graceful empty message
2676
- if (lessons.length === 0) {
2677
- return [
2678
- `🧠 Brain Summary — no lessons yet`,
2679
- ``,
2680
- `Your brain is empty. Use \`learn_from_attempts\` after solving tasks to build it up.`,
2681
- `💡 Tip: \`brain_seed_starter(instance_id="${instance_id}")\` to bootstrap with 16 universal lessons.`,
2682
- ].join('\n');
2683
- }
2684
- // 3. If lessons.length <= top_n, return all of them (no need to score)
2685
- const needsRanking = lessons.length > topN;
2686
- // 4. Score lessons for relevance to focus
2687
- const focusWords = focus.toLowerCase().split(/\s+/).filter(Boolean);
2688
- const nowMs = Date.now();
2689
- const scored = lessons.map(l => {
2690
- // Keyword relevance: topic or tags contain focus words
2691
- const inTopic = focusWords.filter(w => l.topic.toLowerCase().includes(w)).length;
2692
- const inTags = focusWords.filter(w => (l.tags ?? []).some(t => t.toLowerCase().includes(w))).length;
2693
- const relevance = (inTopic * 2) + inTags;
2694
- // Recall-count bonus (proven-ness, log-scaled, capped)
2695
- const rc = Math.max(0, l.recall_count ?? 0);
2696
- const recallBonus = Math.min(1.5, Math.log1p(rc) / 10);
2697
- // Severity bonus
2698
- const sevBonus = l.severity === 'critical' ? 1.0 : l.severity === 'major' ? 0.5 : 0;
2699
- // Recency bonus: lessons updated in the last 30 days get up to +0.5
2700
- const ageDays = (nowMs - new Date(l.verified_at ?? l.ts).getTime()) / 86_400_000;
2701
- const recencyBonus = ageDays < 30 ? 0.5 * (1 - ageDays / 30) : 0;
2702
- // Outcome bonus: success > partial > failure
2703
- const outcomeBonus = l.outcome === 'success' ? 0.5 : l.outcome === 'partial' ? 0.2 : 0;
2704
- const score = relevance + recallBonus + sevBonus + recencyBonus + outcomeBonus;
2705
- return { lesson: l, score };
2706
- });
2707
- // Sort descending by score, then by recency as tiebreak
2708
- scored.sort((a, b) => {
2709
- if (b.score !== a.score)
2710
- return b.score - a.score;
2711
- return new Date(b.lesson.ts).getTime() - new Date(a.lesson.ts).getTime();
2712
- });
2713
- const selected = (needsRanking ? scored.slice(0, topN) : scored).map(s => s.lesson);
2714
- const total = lessons.length;
2715
- const shown = selected.length;
2716
- // 5. Build concise briefing
2717
- const divider = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
2718
- const header = needsRanking
2719
- ? `🧠 Brain Summary — top ${shown} of ${total} lessons for "${focus}"`
2720
- : `🧠 Brain Summary — all ${shown} lesson${shown !== 1 ? 's' : ''} for "${focus}"`;
2721
- const summaryLines = [header, divider, ''];
2722
- for (const l of selected) {
2723
- const sevIcon = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '⚠️' : '✅';
2724
- const sevLabel = l.severity ? `[${l.severity}] ` : '[minor] ';
2725
- const rc = l.recall_count ?? 0;
2726
- const rcStr = rc > 0 ? ` (✓ recalled ${rc}×)` : '';
2727
- const snippet = l.what_worked.slice(0, 110);
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}`);
2733
- }
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
- }
2741
- if (needsRanking) {
2742
- summaryLines.push(`💡 Showing top ${shown} of ${total} lessons · \`session_start\` for full briefing`);
2743
- }
2744
- else {
2745
- summaryLines.push(`💡 All ${shown} lesson${shown !== 1 ? 's' : ''} shown · \`session_start\` for full briefing`);
2746
- }
2747
- return summaryLines.join('\n');
2748
- }
2749
- // ── session_end ───────────────────────────────────────────────────────────
2750
- case 'session_end': {
2751
- const { instance_id, summary, files_changed = [], lessons_learned, workspace_path = '', } = args;
2752
- const redis = await getConnection(instance_id);
2753
- const now = new Date();
2754
- // Calculate duration from session_start marker
2755
- let durationMin;
2756
- const currentRaw = await redis.get('cachly:session:current');
2757
- if (!currentRaw) {
2758
- return `⚠️ No active session found.\n\nRun \`session_start\` first to begin tracking a session, then call \`session_end\` when you're done.`;
2759
- }
2760
- const current = safeJsonParse(currentRaw, null);
2761
- if (current?.started) {
2762
- durationMin = Math.round((now.getTime() - new Date(current.started).getTime()) / 60000);
2763
- }
2764
- let decisionLog = [];
2765
- try {
2766
- const dlEntries = await redis.lrange('cachly:session:decision-log', 0, -1);
2767
- decisionLog = dlEntries.flatMap(e => {
2768
- const entry = safeJsonParse(e, null);
2769
- return entry ? [entry] : [];
2770
- });
2771
- await redis.del('cachly:session:decision-log');
2772
- }
2773
- catch { /* non-critical */ }
2774
- const sessionRecord = {
2775
- ts: now.toISOString(),
2776
- summary,
2777
- files_changed,
2778
- ...(lessons_learned !== undefined ? { lessons_learned } : {}),
2779
- ...(durationMin !== undefined ? { duration_min: durationMin } : {}),
2780
- ...(decisionLog.length > 0 ? { decision_log: decisionLog } : {}),
2781
- };
2782
- // Save as "last session"
2783
- await redis.set('cachly:session:last', JSON.stringify(sessionRecord));
2784
- // Append to history list (keep last 50 sessions, TTL 90 days)
2785
- await redis.lpush('cachly:session:history', JSON.stringify(sessionRecord));
2786
- await redis.ltrim('cachly:session:history', 0, 49);
2787
- await redis.expire('cachly:session:history', 90 * 86400);
2788
- // Clean up current session marker
2789
- await redis.del('cachly:session:current');
2790
- // ── AUTO-LEARN from session summary (no manual call needed) ─────────────
2791
- // Parse the summary for actionable lessons and store them automatically.
2792
- const autoLearned = [];
2793
- try {
2794
- // Extract key sentences from the summary that contain action verbs
2795
- const actionVerbs = /\b(fixed|deployed|added|removed|refactored|migrated|updated|resolved|implemented|improved|optimized|configured|created|deleted|disabled|enabled|discovered|found|learned|debugged|patched|upgraded|installed|tested|built|rewrote|moved|renamed|split|merged|extracted)\b/i;
2796
- const sentences = summary
2797
- .split(/[.!\n]+/)
2798
- .map(s => s.trim())
2799
- .filter(s => s.length > 20 && actionVerbs.test(s));
2800
- for (const sentence of sentences.slice(0, 6)) {
2801
- // Build a topic slug from the first meaningful words
2802
- const words = sentence.toLowerCase()
2803
- .replace(/[^a-z0-9\s]/g, ' ')
2804
- .split(/\s+/)
2805
- .filter(w => w.length > 3 && !['that', 'this', 'with', 'from', 'have', 'been', 'were', 'they', 'then', 'when', 'also', 'into', 'will', 'would', 'could', 'should'].includes(w));
2806
- const slug = words.slice(0, 4).join('-');
2807
- if (!slug)
2808
- continue;
2809
- const topic = `auto:${slug}`;
2810
- const key = `cachly:lesson:best:${topic}`;
2811
- // Don't overwrite existing successful lessons
2812
- const existing = await redis.get(key);
2813
- if (existing) {
2814
- try {
2815
- const ex = JSON.parse(existing);
2816
- if (ex.outcome === 'success')
2817
- continue;
2818
- }
2819
- catch { /* ignore */ }
2820
- }
2821
- const lesson = {
2822
- topic,
2823
- outcome: 'success',
2824
- what_worked: sentence,
2825
- context: `Auto-learned from session summary. Full summary: ${summary.slice(0, 300)}`,
2826
- severity: 'minor',
2827
- ts: now.toISOString(),
2828
- recall_count: 0,
2829
- auto_learned: true,
2830
- session_ts: now.toISOString(),
2831
- version: 2,
2832
- };
2833
- await redis.set(key, JSON.stringify(lesson));
2834
- // 90-day TTL for auto-learned lessons
2835
- await redis.expire(key, 90 * 86400);
2836
- autoLearned.push(topic);
2837
- }
2838
- }
2839
- catch { /* auto-learn errors must never break session_end */ }
2840
- // ── 🌿 Ambient Git Learning ────────────────────────────────────────────────
2841
- // Read git commits since session start → auto-learn each meaningful commit.
2842
- // The commit BODY (%b) carries the reasoning — what was measured, what the
2843
- // cause was, what trap it avoided. The subject (%s) alone is just a heading.
2844
- const ambientLearned = [];
2845
- if (workspace_path) {
2846
- try {
2847
- // Get the session start time (stored by session_start)
2848
- const sessionStartTs = currentRaw
2849
- ? (() => { try {
2850
- return JSON.parse(currentRaw).started ?? '';
2851
- }
2852
- catch {
2853
- return '';
2854
- } })()
2855
- : '';
2856
- const sinceArg = sessionStartTs ? `--since="${sessionStartTs}"` : '--since="1 hour ago"';
2857
- // '|||ENDE' terminates one commit's record. A commit body can contain
2858
- // blank lines and newlines of its own, so splitting the raw output on
2859
- // '\n' would tear a single multi-line body apart into several fake
2860
- // "commits" — splitting on the marker keeps each body intact.
2861
- const gitOut = execSync(`git -C "${workspace_path}" log ${sinceArg} --oneline --format="%H|||%s|||%b|||%ai|||ENDE"`, { timeout: 5000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
2862
- if (gitOut) {
2863
- const commitActionRe = /\b(fix|add|remove|refactor|migrate|update|resolve|implement|improve|optimize|configure|create|delete|disable|enable|debug|patch|upgrade|build|rewrite|deploy|feat|chore|docs|test|perf|ci)\b/i;
2864
- const commitBlocks = gitOut.split('|||ENDE').map(b => b.trim()).filter(Boolean);
2865
- for (const block of commitBlocks.slice(0, 10)) {
2866
- const [hash, msg, rawBody, dateStr] = block.split('|||');
2867
- // Files touched together in one commit are recorded as co-occurs
2868
- // edges between file nodes in the graph ("who touches A usually
2869
- // touches B") — that is independent of whether the commit message
2870
- // itself is lesson-worthy, so it runs for every recent commit.
2871
- if (hash) {
2872
- try {
2873
- // '--root' matters for a repo's very first commit: without it,
2874
- // diff-tree has no parent to compare against and silently
2875
- // reports zero changed files for that commit.
2876
- const nameOut = execSync(`git -C "${workspace_path}" diff-tree --no-commit-id --name-only -r --root "${hash}"`, { timeout: 5000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
2877
- // Pairing is quadratic, so the cap decides the cost — and this
2878
- // runs inside session_end, which must stay quick. At 12 files
2879
- // that is 66 pairs; at 25 it would be 300, times up to ten
2880
- // commits. Each file node is upserted ONCE up front instead of
2881
- // once per pair: with 12 files that is 12 upserts rather than
2882
- // 132, and it turns ~530 round-trips into ~144.
2883
- const CO_OCCUR_MAX_FILES = 12;
2884
- const changedFiles = nameOut
2885
- ? nameOut.split('\n').map(f => f.trim()).filter(Boolean).slice(0, CO_OCCUR_MAX_FILES)
2886
- : [];
2887
- const fileIds = [];
2888
- for (const f of changedFiles)
2889
- fileIds.push(await ckgUpsertFileNode(redis, f));
2890
- for (let i = 0; i < fileIds.length; i++) {
2891
- for (let j = i + 1; j < fileIds.length; j++) {
2892
- await ckgUpdateEdge(redis, fileIds[i], 'co-occurs', fileIds[j], true);
2893
- await ckgUpdateEdge(redis, fileIds[j], 'co-occurs', fileIds[i], true);
2894
- }
2895
- }
2896
- }
2897
- catch { /* file co-occurrence is best-effort */ }
2898
- }
2899
- if (!msg || !commitActionRe.test(msg))
2900
- continue;
2901
- // A heading alone is not a lesson. Without a substantial body —
2902
- // the why, the measurement, the trap — nothing gets stored: better
2903
- // no entry than one that fills space and says nothing.
2904
- const body = (rawBody ?? '').trim();
2905
- if (body.length < 120)
2906
- continue;
2907
- const slug = msg
2908
- .toLowerCase().replace(/^(fix|feat|chore|docs|test|ci|perf|refactor|build|revert)[:(\s]/i, '')
2909
- .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
2910
- if (!slug)
2911
- continue;
2912
- const topic = `git:${slug}`;
2913
- const key = `cachly:lesson:best:${topic}`;
2914
- const existing = await redis.get(key);
2915
- if (existing)
2916
- continue; // don't overwrite existing
2917
- const commitLesson = {
2918
- topic,
2919
- outcome: 'success',
2920
- what_worked: `${msg}\n\n${body.slice(0, 1200)}`,
2921
- context: `Auto-learned from git commit ${(hash ?? '').slice(0, 7)} at ${dateStr ?? ''} in ${workspace_path}`,
2922
- severity: 'minor',
2923
- ts: now.toISOString(),
2924
- recall_count: 0,
2925
- auto_learned: true,
2926
- source: 'ambient-git',
2927
- version: 3,
2928
- };
2929
- await redis.set(key, JSON.stringify(commitLesson));
2930
- await redis.expire(key, 60 * 86400); // 60 day TTL for git lessons
2931
- ambientLearned.push(topic);
2932
- }
2933
- }
2934
- }
2935
- catch { /* git not available or not a repo — silent skip */ }
2936
- }
2937
- const durationStr = durationMin !== undefined ? ` · ${durationMin} min` : '';
2938
- const totalAutoLearned = autoLearned.length + ambientLearned.length;
2939
- // ── Shareable Session Summary Card ────────────────────────────────────────
2940
- // Generated after each session so the user can share their progress.
2941
- const tweetLines = [];
2942
- if (durationMin !== undefined && durationMin > 0)
2943
- tweetLines.push(`⏱ ${durationMin} min session`);
2944
- if (totalAutoLearned > 0)
2945
- tweetLines.push(`🧠 ${totalAutoLearned} lessons saved to Brain`);
2946
- if (files_changed.length > 0)
2947
- tweetLines.push(`📁 ${files_changed.length} file${files_changed.length > 1 ? 's' : ''} changed`);
2948
- const tweetBody = tweetLines.length > 0
2949
- ? `${tweetLines.join(' · ')}\n\nMy AI Brain remembers this so I never repeat it. @cachlydev\ncachly.dev`
2950
- : `Session saved to my AI Brain. No more re-explaining this tomorrow. @cachlydev\ncachly.dev`;
2951
- const tweetURL = `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetBody)}`;
2952
- const sessionCard = [
2953
- ``,
2954
- `┌─────────────────────────────────────────────┐`,
2955
- `│ 🧠 Session Summary Card │`,
2956
- `│ Share your progress — cached forever │`,
2957
- `├─────────────────────────────────────────────┤`,
2958
- durationMin !== undefined ? `│ ⏱ Duration : ${String(durationMin + ' min').padEnd(26)}│` : '',
2959
- totalAutoLearned > 0 ? `│ 📚 Learned : ${String(totalAutoLearned + ' lessons').padEnd(26)}│` : '',
2960
- files_changed.length > 0 ? `│ 📁 Changed : ${String(files_changed.length + ' file' + (files_changed.length > 1 ? 's' : '')).padEnd(26)}│` : '',
2961
- `├─────────────────────────────────────────────┤`,
2962
- `│ 📣 Share: ${tweetURL.slice(0, 34).padEnd(34)}│`,
2963
- `└─────────────────────────────────────────────┘`,
2964
- ].filter(l => l !== '').join('\n');
2965
- return [
2966
- `✅ **Session saved**${durationStr}`,
2967
- ``,
2968
- `📋 **Summary:** ${summary}`,
2969
- files_changed.length > 0 ? `📁 **Files changed:** ${files_changed.map(f => `\`${f}\``).join(', ')}` : '',
2970
- lessons_learned !== undefined ? `🧠 **Lessons stored:** ${lessons_learned}` : '',
2971
- autoLearned.length > 0 ? `🤖 **Auto-learned:** ${autoLearned.length} lessons extracted from summary (${autoLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${autoLearned.length > 3 ? '…' : ''})` : '',
2972
- ambientLearned.length > 0 ? `🌿 **Ambient git learning:** ${ambientLearned.length} commit${ambientLearned.length > 1 ? 's' : ''} auto-learned (${ambientLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${ambientLearned.length > 3 ? '…' : ''})` : '',
2973
- sessionCard,
2974
- ``,
2975
- `💡 Next session: \`session_start(focus="...")\` to see this summary.`,
2976
- ].filter(l => l !== '').join('\n');
2977
- }
2978
- // ── session_ping — lightweight checkpoint ─────────────────────────────────
2979
- case 'session_ping': {
2980
- const { instance_id, task, files_touched = [], next_step = '', provider = '', } = args;
2981
- const redis = await getConnection(instance_id);
2982
- const checkpoint = {
2983
- ts: new Date().toISOString(),
2984
- task,
2985
- files_touched,
2986
- next_step,
2987
- provider,
2988
- };
2989
- // Store as the latest checkpoint — session_start reads this when no session_end found
2990
- await redis.set('cachly:session:checkpoint', JSON.stringify(checkpoint), 'EX', 86400 * 3); // 3-day TTL
2991
- // Also keep a short rolling log (last 20 checkpoints for history)
2992
- await redis.lpush('cachly:session:checkpoint:log', JSON.stringify(checkpoint));
2993
- await redis.ltrim('cachly:session:checkpoint:log', 0, 19);
2994
- const providerStr = provider ? ` [${provider}]` : '';
2995
- const filesStr = files_touched.length > 0 ? ` · ${files_touched.length} file${files_touched.length > 1 ? 's' : ''} touched` : '';
2996
- const nextStr = next_step ? `\n📍 **Next step:** ${next_step}` : '';
2997
- return [
2998
- `📌 **Checkpoint saved**${providerStr} — ${new Date().toLocaleTimeString()}`,
2999
- `🔨 **Working on:** ${task}${filesStr}`,
3000
- nextStr,
3001
- ``,
3002
- `💡 If you switch providers, \`session_start\` will show this checkpoint automatically.`,
3003
- ].filter(l => l !== '').join('\n');
3004
- }
3005
- // ── session_handoff — cross-window continuity ─────────────────────────────
3006
- case 'session_handoff': {
3007
- const { instance_id, completed_tasks = [], remaining_tasks = [], files_changed = [], instructions = '', context_summary = '', blocked_on = '', } = args;
3008
- const redis = await getConnection(instance_id);
3009
- const now = new Date();
3010
- const handoff = {
3011
- ts: now.toISOString(),
3012
- completed_tasks,
3013
- remaining_tasks,
3014
- files_changed,
3015
- instructions,
3016
- context_summary,
3017
- blocked_on,
3018
- };
3019
- // Store handoff — never expires until next handoff overwrites it
3020
- await redis.set('cachly:session:handoff', JSON.stringify(handoff));
3021
- // Also append to history
3022
- await redis.lpush('cachly:session:handoff:history', JSON.stringify(handoff));
3023
- await redis.ltrim('cachly:session:handoff:history', 0, 19);
3024
- const totalTasks = completed_tasks.length + remaining_tasks.length;
3025
- const pct = totalTasks > 0 ? Math.round((completed_tasks.length / totalTasks) * 100) : 0;
3026
- const brokenFiles = files_changed.filter(f => f.status === 'broken' || f.status === 'partial');
3027
- return [
3028
- `🤝 **Handoff saved** — ${completed_tasks.length}/${totalTasks} tasks done (${pct}%)`,
3029
- ``,
3030
- completed_tasks.length > 0 ? `✅ **Completed:**\n${completed_tasks.map(t => ` - ${t}`).join('\n')}` : '',
3031
- remaining_tasks.length > 0 ? `\n⏳ **Remaining for next window:**\n${remaining_tasks.map(t => ` - ${t}`).join('\n')}` : '',
3032
- brokenFiles.length > 0 ? `\n⚠️ **Needs attention:** ${brokenFiles.map(f => `\`${f.path}\` (${f.status})`).join(', ')}` : '',
3033
- blocked_on ? `\n🚫 **Blocked on:** ${blocked_on}` : '',
3034
- instructions ? `\n📝 **Instructions:** ${instructions}` : '',
3035
- ``,
3036
- `💡 The next \`session_start\` will include this handoff automatically.`,
3037
- ].filter(l => l !== '').join('\n');
3038
- }
3039
- // ── auto_learn_session ────────────────────────────────────────────────────
3040
- case 'auto_learn_session': {
3041
- const { instance_id, observations } = args;
3042
- const redis = await getConnection(instance_id);
3043
- const stored = [];
3044
- const skipped = [];
3045
- for (const obs of observations) {
3046
- // Auto-generate topic from action if not provided
3047
- const rawTopic = obs.topic ?? obs.action
3048
- .toLowerCase()
3049
- .replace(/[^a-z0-9:\-_\s]/g, '')
3050
- .trim()
3051
- .split(/\s+/)
3052
- .slice(0, 4)
3053
- .join('-');
3054
- const topic = rawTopic.includes(':') ? rawTopic : `auto:${rawTopic}`;
3055
- const key = `cachly:lesson:best:${topic}`;
3056
- // Only overwrite if this is a success and existing is failure, or topic is new
3057
- const existing = await redis.get(key);
3058
- if (existing) {
3059
- const existingLesson = JSON.parse(existing);
3060
- if (existingLesson.outcome === 'success' && obs.outcome !== 'success') {
3061
- skipped.push(topic);
3062
- continue;
3063
- }
3064
- }
3065
- const lesson = {
3066
- topic,
3067
- outcome: obs.outcome,
3068
- what_worked: obs.outcome === 'success' ? obs.action : (obs.details ?? obs.action),
3069
- what_failed: obs.outcome === 'failure' ? obs.action : undefined,
3070
- context: obs.details,
3071
- severity: obs.severity ?? 'minor',
3072
- ts: new Date().toISOString(),
3073
- // Give auto-learned lessons the same baseline confidence field the
3074
- // quality rerank / decay logic expect, so they are ranked like any
3075
- // other lesson instead of being treated as signal-less.
3076
- confidence: 1.0,
3077
- recall_count: 0,
3078
- auto_learned: true,
3079
- version: 2,
3080
- };
3081
- const lessonStr = JSON.stringify(lesson);
3082
- await redis.set(key, lessonStr);
3083
- // Make auto-learned lessons FIRST-CLASS, not just a bare best-key pointer:
3084
- // append to the topic history (audit + consolidation source) and register
3085
- // the concept in the Causal Knowledge Graph so causal_trace / brain_predict
3086
- // / memory_consolidate can see them. Mirrors the learn_from_attempts path;
3087
- // best-effort so a graph hiccup never fails the learn.
3088
- const listKey = `cachly:lessons:${topic}`;
3089
- await redis.rpush(listKey, lessonStr);
3090
- await redis.ltrim(listKey, -100, -1);
3091
- await redis.expire(listKey, 90 * 86400);
3092
- try {
3093
- const domain = topic.split(':')[0] ?? 'auto';
3094
- const conceptId = ckgSlug(topic);
3095
- await ckgUpsertNode(redis, conceptId, domain, domain);
3096
- // A successful action that resolved a described problem becomes
3097
- // causal-traceable via a `fixes` edge.
3098
- if (obs.outcome === 'success' || obs.outcome === 'partial') {
3099
- const problemConcept = obs.details ? extractProblemConcept(obs.details) : null;
3100
- if (problemConcept) {
3101
- const problemId = ckgSlug(`problem:${problemConcept}`);
3102
- await ckgUpsertNode(redis, problemId, 'problem', 'problem');
3103
- await ckgUpdateEdge(redis, conceptId, 'fixes', problemId, obs.outcome === 'success', obs.outcome === 'partial');
3104
- }
3105
- }
3106
- }
3107
- catch { /* CKG is best-effort */ }
3108
- stored.push(`${obs.outcome === 'success' ? '✅' : obs.outcome === 'partial' ? '⚠️' : '❌'} \`${topic}\``);
3109
- }
3110
- const lines = [
3111
- `🤖 **Auto-learn complete**: ${stored.length} stored, ${skipped.length} skipped`,
3112
- '',
3113
- ];
3114
- if (stored.length > 0)
3115
- lines.push('**Stored:**', ...stored.map(s => ' ' + s), '');
3116
- if (skipped.length > 0)
3117
- lines.push(`**Skipped** (better lesson already exists): ${skipped.map(t => `\`${t}\``).join(', ')}`);
3118
- return lines.join('\n');
3119
- }
3120
- // ── brain_who_knows ───────────────────────────────────────────────────────
3121
- // Phase 3A: Org-wide people intelligence. Returns ranked list of contributors
3122
- // whose authored edges in the CKG match the query topic.
3123
- case 'brain_who_knows': {
3124
- const { instance_id, topic, limit = 10, } = args;
3125
- if (!topic || typeof topic !== 'string' || !topic.trim()) {
3126
- return '⚠️ `brain_who_knows` requires a non-empty `topic`. Example: `brain_who_knows(topic="kubernetes deployment")`.';
3127
- }
3128
- const safeLimit = Math.max(1, Math.min(Number(limit) || 10, 50));
3129
- const redis = await getConnection(instance_id);
3130
- // Step 1: Find candidate concept node IDs matching the query
3131
- const querySlug = ckgSlug(topic);
3132
- const qTokens = tokenize(topic).filter((t) => t.length >= 3).slice(0, 5);
3133
- const candidateConceptIds = new Set([querySlug]);
3134
- for (const token of qTokens.slice(0, 3)) {
3135
- const nodeStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 30 });
3136
- await new Promise((res, rej) => {
3137
- nodeStream.on('data', (batch) => {
3138
- for (const k of batch.slice(0, 10)) {
3139
- const nodeId = k.replace('cachly:ckg:node:', '');
3140
- if (!nodeId.startsWith('person:') && !nodeId.startsWith('file:'))
3141
- candidateConceptIds.add(nodeId);
3142
- }
3143
- });
3144
- nodeStream.on('end', res);
3145
- nodeStream.on('error', rej);
3146
- });
3147
- }
3148
- const personScores = new Map();
3149
- for (const conceptId of [...candidateConceptIds].slice(0, 25)) {
3150
- const inboundEdgeKeys = await redis.smembers(`cachly:ckg:idx:to:${conceptId}`);
3151
- for (const ek of inboundEdgeKeys) {
3152
- const er = await redis.get(ek);
3153
- if (!er)
3154
- continue;
3155
- const edge = safeJsonParse(er, null);
3156
- if (!edge || edge.edgeType !== 'authored')
3157
- continue;
3158
- const personNodeRaw = await redis.get(`cachly:ckg:node:${edge.from}`);
3159
- if (!personNodeRaw)
3160
- continue;
3161
- const personNode = safeJsonParse(personNodeRaw, null);
3162
- if (!personNode || personNode.type !== 'person')
3163
- continue;
3164
- const existing = personScores.get(edge.from);
3165
- if (existing) {
3166
- existing.lessonCount++;
3167
- existing.totalConfidence += edge.confidence;
3168
- existing.domains.add(personNode.domain);
3169
- if (edge.last_updated > existing.lastActive)
3170
- existing.lastActive = edge.last_updated;
3171
- }
3172
- else {
3173
- personScores.set(edge.from, {
3174
- handle: personNode.handle,
3175
- lessonCount: 1,
3176
- totalConfidence: edge.confidence,
3177
- domains: new Set([personNode.domain]),
3178
- lastActive: personNode.last_active ?? edge.last_updated,
3179
- });
3180
- }
3181
- }
3182
- }
3183
- if (personScores.size === 0) {
3184
- return [
3185
- `## 👥 Who Knows About \`${topic}\`?`,
3186
- ``,
3187
- `No attributed lessons found yet.`,
3188
- ``,
3189
- `Knowledge attribution builds automatically — add \`author="name"\` to \`learn_from_attempts\` calls:`,
3190
- `\`learn_from_attempts(topic="${topic}", author="your-handle", ...)\``,
3191
- ].join('\n');
3192
- }
3193
- // Step 3: Sort by expertise score (lesson count × avg confidence)
3194
- const ranked = [...personScores.entries()]
3195
- .map(([id, s]) => ({
3196
- id,
3197
- handle: s.handle,
3198
- lessonCount: s.lessonCount,
3199
- avgConfidence: s.totalConfidence / s.lessonCount,
3200
- domains: [...s.domains].slice(0, 3).join(', '),
3201
- lastActive: s.lastActive,
3202
- }))
3203
- .sort((a, b) => (b.lessonCount * b.avgConfidence) - (a.lessonCount * a.avgConfidence))
3204
- .slice(0, safeLimit);
3205
- // Step 4: For the top expert, find frequent collaborators (person↔person edges).
3206
- const topCollaborators = [];
3207
- const topExpert = ranked[0];
3208
- if (topExpert) {
3209
- try {
3210
- const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${topExpert.id}`);
3211
- const collabs = [];
3212
- for (const ek of edgeKeys) {
3213
- const er = await redis.get(ek);
3214
- if (!er)
3215
- continue;
3216
- const edge = safeJsonParse(er, null);
3217
- if (!edge || edge.edgeType !== 'collaborates')
3218
- continue;
3219
- const pr = await redis.get(`cachly:ckg:node:${edge.to}`);
3220
- const pn = pr ? safeJsonParse(pr, null) : null;
3221
- if (pn?.handle)
3222
- collabs.push({ handle: pn.handle, trials: edge.trials });
3223
- }
3224
- collabs.sort((a, b) => b.trials - a.trials);
3225
- for (const c of collabs.slice(0, 3))
3226
- topCollaborators.push(c.handle);
3227
- }
3228
- catch { /* non-critical */ }
3229
- }
3230
- const lines = [
3231
- `## 👥 Who Knows About \`${topic}\`?`,
3232
- ``,
3233
- `**${ranked.length}** contributor${ranked.length !== 1 ? 's' : ''} with relevant lessons:`,
3234
- ``,
3235
- ];
3236
- for (const [i, p] of ranked.entries()) {
3237
- const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`;
3238
- const conf = (p.avgConfidence * 100).toFixed(0);
3239
- const daysSince = Math.floor((Date.now() - new Date(p.lastActive).getTime()) / 86400000);
3240
- const recency = daysSince < 1 ? 'today' : daysSince < 7 ? `${daysSince}d ago` : daysSince < 30 ? `${Math.floor(daysSince / 7)}w ago` : `${Math.floor(daysSince / 30)}mo ago`;
3241
- // Show role badge if assigned (non-blocking — role lookup is best-effort)
3242
- const roleVal = await getRole(redis, instance_id, p.handle).catch(() => null);
3243
- const roleBadge = roleVal ? ` ${ROLE_BADGE[roleVal]}` : '';
3244
- lines.push(`${medal} **${p.handle}**${roleBadge} — ${p.lessonCount} lesson${p.lessonCount !== 1 ? 's' : ''} · ${conf}% confidence · last active ${recency}`, ` _domains: ${p.domains}_`, ``);
3245
- }
3246
- if (topCollaborators.length > 0 && topExpert) {
3247
- lines.push(`🤝 **${topExpert.handle}** frequently works with: ${topCollaborators.map(h => `**${h}**`).join(', ')}`, ` _(ask them together — they've solved structurally similar things on shared files)_`, ``);
3248
- }
3249
- lines.push(`---`, `_Attribution grows with every \`learn_from_attempts(author="...", ...)\` call._`);
3250
- return lines.join('\n');
3251
- }
3252
- // ── brain_file_map ────────────────────────────────────────────────────────
3253
- // Phase 3B: "What do we know about these files?" — experts + lessons per file.
3254
- case 'brain_file_map': {
3255
- const { instance_id, file_paths = [], } = args;
3256
- // Defensive: accept only a non-empty array of non-empty strings.
3257
- const cleanPaths = (Array.isArray(file_paths) ? file_paths : [])
3258
- .filter((p) => typeof p === 'string' && p.trim().length > 0)
3259
- .map(p => p.trim());
3260
- if (!cleanPaths.length)
3261
- return '⚠️ Pass at least one file path. Example: `brain_file_map(file_paths=["src/auth/jwt.ts"])`.';
3262
- const redis = await getConnection(instance_id);
3263
- const lines = [
3264
- `## 📁 File Knowledge Map`,
3265
- ``,
3266
- `_What cachly knows about the files you're about to touch:_`,
3267
- ``,
3268
- ];
3269
- for (const fp of cleanPaths.slice(0, 10)) {
3270
- lines.push(`### \`${fp}\``);
3271
- // ── Experts via CKG file nodes ─────────────────────────────────────
3272
- const fileId = `file:${ckgSlug(fp)}`;
3273
- const inbound = await redis.smembers(`cachly:ckg:idx:to:${fileId}`);
3274
- const experts = [];
3275
- for (const ek of inbound) {
3276
- const er = await redis.get(ek);
3277
- if (!er)
3278
- continue;
3279
- const edge = safeJsonParse(er, null);
3280
- if (!edge || edge.edgeType !== 'touched')
3281
- continue;
3282
- const personRaw = await redis.get(`cachly:ckg:node:${edge.from}`);
3283
- if (!personRaw)
3284
- continue;
3285
- const pn = safeJsonParse(personRaw, null);
3286
- if (!pn || pn.type !== 'person')
3287
- continue;
3288
- experts.push({ handle: pn.handle, touches: edge.trials, lastSeen: edge.last_updated });
3289
- }
3290
- experts.sort((a, b) => b.touches - a.touches);
3291
- if (experts.length > 0) {
3292
- const expLine = experts.slice(0, 5).map((e, i) => {
3293
- const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`;
3294
- const days = Math.floor((Date.now() - new Date(e.lastSeen).getTime()) / 86400000);
3295
- const ago = days < 1 ? 'today' : days < 7 ? `${days}d ago` : `${Math.floor(days / 7)}w ago`;
3296
- return `${medal} **${e.handle}** (${e.touches}× · ${ago})`;
3297
- }).join(' · ');
3298
- lines.push(`**Experts:** ${expLine}`);
3299
- }
3300
- else {
3301
- lines.push(`**Experts:** _None yet — add \`author\` to \`learn_from_attempts\` calls_`);
3302
- }
3303
- // ── Related lessons via BM25 on filename tokens ────────────────────
3304
- const tokens = fp.replace(/[^a-z0-9]/gi, ' ').split(/\s+/).filter(t => t.length >= 4);
3305
- const lessonQuery = tokens.slice(0, 3).join(' ');
3306
- const relatedLessons = [];
3307
- if (lessonQuery) {
3308
- try {
3309
- const hits = await keywordSearch(redis, ['cachly:lesson:best:*'], lessonQuery, 4);
3310
- for (const h of hits) {
3311
- const ld = safeJsonParse(h.content, {});
3312
- if (ld.visibility === 'private')
3313
- continue;
3314
- const emoji = ld.outcome === 'success' ? '✅' : ld.outcome === 'partial' ? '⚠️' : '❌';
3315
- const sev = ld.severity === 'critical' ? ' 🔴' : ld.severity === 'major' ? ' 🟡' : '';
3316
- const by = ld.author ? ` · 👤 ${ld.author}` : '';
3317
- const topic = h.key.replace('cachly:lesson:best:', '');
3318
- relatedLessons.push(` - ${emoji} \`${topic}\`${sev}${by} — ${(ld.what_worked ?? '').slice(0, 100)}`);
3319
- }
3320
- }
3321
- catch { /* non-critical */ }
3322
- }
3323
- // Also: exact file_paths match via lesson scan
3324
- if (relatedLessons.length < 2) {
3325
- const allLessonKeys = await scanKeys(redis, 'cachly:lesson:best:*', { max: 1500 });
3326
- for (const k of allLessonKeys.slice(0, 300)) {
3327
- const raw = await redis.get(k);
3328
- if (!raw)
3329
- continue;
3330
- const ld = safeJsonParse(raw, {});
3331
- if (ld.visibility === 'private')
3332
- continue;
3333
- if (!(ld.file_paths ?? []).includes(fp))
3334
- continue;
3335
- const topic = k.replace('cachly:lesson:best:', '');
3336
- if (relatedLessons.some(l => l.includes(`\`${topic}\``)))
3337
- continue;
3338
- const emoji = ld.outcome === 'success' ? '✅' : ld.outcome === 'partial' ? '⚠️' : '❌';
3339
- const sev = ld.severity === 'critical' ? ' 🔴' : ld.severity === 'major' ? ' 🟡' : '';
3340
- const by = ld.author ? ` · 👤 ${ld.author}` : '';
3341
- relatedLessons.push(` - ${emoji} \`${topic}\`${sev}${by} — ${(ld.what_worked ?? '').slice(0, 100)}`);
3342
- }
3343
- }
3344
- if (relatedLessons.length > 0) {
3345
- lines.push(`**Related lessons:**`);
3346
- lines.push(...relatedLessons.slice(0, 5));
3347
- }
3348
- else {
3349
- lines.push(`**Related lessons:** _None yet_`);
3350
- }
3351
- lines.push(``);
3352
- }
3353
- lines.push(`---`, `_Attribution builds with \`learn_from_attempts(file_paths=[...], author="...")\`. Run this before committing to track what changed and why._`);
3354
- return lines.join('\n');
3355
- }
3356
- // ── team_expertise_map ────────────────────────────────────────────────────
3357
- // Phase 3B: Full team expertise overview — who knows what, at a glance.
3358
- case 'team_expertise_map': {
3359
- const { instance_id, top_n = 20, } = args;
3360
- const redis = await getConnection(instance_id);
3361
- // Scan all person nodes (capped + timed out so a huge keyspace can't hang the agent)
3362
- const personKeys = await scanKeys(redis, 'cachly:ckg:node:person:*', { max: 2000 });
3363
- if (personKeys.length === 0) {
3364
- return [
3365
- `## 🗺️ Team Expertise Map`,
3366
- ``,
3367
- `No contributors yet.`,
3368
- ``,
3369
- `Add \`author="handle"\` to any \`learn_from_attempts\` call to start building the map.`,
3370
- ].join('\n');
3371
- }
3372
- const people = [];
3373
- for (const pk of personKeys) {
3374
- const raw = await redis.get(pk);
3375
- if (!raw)
3376
- continue;
3377
- const pn = safeJsonParse(raw, null);
3378
- if (!pn || pn.type !== 'person')
3379
- continue;
3380
- // Count authored lessons + collect domains from authored edges
3381
- const outEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${pn.id}`);
3382
- const domainCounts = new Map();
3383
- let lessonCount = 0;
3384
- for (const ek of outEdgeKeys) {
3385
- const er = await redis.get(ek);
3386
- if (!er)
3387
- continue;
3388
- const edge = safeJsonParse(er, null);
3389
- if (!edge || edge.edgeType !== 'authored')
3390
- continue;
3391
- lessonCount++;
3392
- const domain = edge.to.split(':')[0] ?? 'unknown';
3393
- domainCounts.set(domain, (domainCounts.get(domain) ?? 0) + 1);
3394
- }
3395
- people.push({ handle: pn.handle, lessonCount, domains: domainCounts, lastActive: pn.last_active });
3396
- }
3397
- people.sort((a, b) => b.lessonCount - a.lessonCount);
3398
- const topPeople = people.slice(0, top_n);
3399
- const lines = [
3400
- `## 🗺️ Team Expertise Map`,
3401
- ``,
3402
- `**${topPeople.length}** contributor${topPeople.length !== 1 ? 's' : ''} tracked:`,
3403
- ``,
3404
- `| # | Handle | Lessons | Top domains | Last active |`,
3405
- `|---|--------|---------|-------------|-------------|`,
3406
- ];
3407
- for (const [i, p] of topPeople.entries()) {
3408
- const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}`;
3409
- const days = Math.floor((Date.now() - new Date(p.lastActive).getTime()) / 86400000);
3410
- const ago = days < 1 ? 'today' : days < 7 ? `${days}d ago` : days < 30 ? `${Math.floor(days / 7)}w ago` : `${Math.floor(days / 30)}mo ago`;
3411
- const domainStr = [...p.domains.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([d, c]) => `${d}(${c})`).join(', ') || '—';
3412
- lines.push(`| ${medal} | **${p.handle}** | ${p.lessonCount} | ${domainStr} | ${ago} |`);
3413
- }
3414
- lines.push(``, `---`, `_Use \`brain_who_knows(topic="...")\` to find the expert for a specific problem._`);
3415
- return lines.join('\n');
3416
- }
3417
- // ── skill_gaps ────────────────────────────────────────────────────────────
3418
- // Phase 3C: Show knowledge blind spots — domains/files with no success lessons,
3419
- // no attribution, or only failures.
3420
- case 'skill_gaps': {
3421
- const { instance_id, min_failures = 1, } = args;
3422
- const redis = await getConnection(instance_id);
3423
- // Scan all best lessons (capped + timed out)
3424
- const lessonKeys = await scanKeys(redis, 'cachly:lesson:best:*', { max: 3000 });
3425
- const domainMap = new Map();
3426
- let totalAttributed = 0;
3427
- let totalPrivate = 0;
3428
- for (const k of lessonKeys) {
3429
- const raw = await redis.get(k);
3430
- if (!raw)
3431
- continue;
3432
- const l = safeJsonParse(raw, {});
3433
- const domain = (l.topic ?? '').split(':')[0] ?? 'unknown';
3434
- const s = domainMap.get(domain) ?? { successes: 0, failures: 0, attributed: 0, topics: [] };
3435
- if (l.outcome === 'success' || l.outcome === 'partial')
3436
- s.successes++;
3437
- else
3438
- s.failures++;
3439
- if (l.author) {
3440
- s.attributed++;
3441
- totalAttributed++;
3442
- }
3443
- if (l.visibility === 'private')
3444
- totalPrivate++;
3445
- s.topics.push(l.topic ?? '');
3446
- domainMap.set(domain, s);
3447
- }
3448
- const gaps = [];
3449
- for (const [domain, s] of domainMap) {
3450
- if (s.failures >= min_failures && s.successes === 0) {
3451
- gaps.push({
3452
- domain, type: 'unresolved failures',
3453
- severity: s.failures >= 3 ? '🔴 critical' : '🟡 warn',
3454
- detail: `${s.failures} failure${s.failures !== 1 ? 's' : ''}, 0 solutions — use \`learn_from_attempts\` to store what fixed it`,
3455
- });
3456
- }
3457
- if (s.successes + s.failures >= 3 && s.attributed === 0) {
3458
- gaps.push({
3459
- domain, type: 'no attribution',
3460
- severity: '🔵 info',
3461
- detail: `${s.successes + s.failures} lessons with no \`author\` field — attribution lost, \`brain_who_knows\` can't help here`,
3462
- });
3463
- }
3464
- }
3465
- const lines = [
3466
- `## 🔍 Skill Gaps — Knowledge Blind Spots`,
3467
- ``,
3468
- `**${lessonKeys.length}** lessons scanned · **${totalAttributed}** attributed · **${gaps.length}** gap${gaps.length !== 1 ? 's' : ''} found`,
3469
- totalPrivate > 0 ? `_(${totalPrivate} private lessons excluded from gap analysis)_` : '',
3470
- ``,
3471
- ].filter(Boolean);
3472
- if (gaps.length === 0) {
3473
- lines.push(`✅ No significant knowledge gaps detected. All domains with failures have at least one success lesson.`);
3474
- }
3475
- else {
3476
- const bySev = [
3477
- ...gaps.filter(g => g.severity.includes('critical')),
3478
- ...gaps.filter(g => g.severity.includes('warn')),
3479
- ...gaps.filter(g => g.severity.includes('info')),
3480
- ];
3481
- for (const g of bySev.slice(0, 15)) {
3482
- lines.push(`${g.severity} **\`${g.domain}\`** — ${g.type}`);
3483
- lines.push(` ${g.detail}`);
3484
- lines.push(``);
3485
- }
3486
- if (gaps.length > 15)
3487
- lines.push(`_…and ${gaps.length - 15} more gaps. Fix critical ones first._`);
3488
- }
3489
- lines.push(`---`, `_Fix gaps with \`learn_from_attempts(topic="${gaps[0]?.domain ?? 'your:topic'}", outcome="success", what_worked="...")\`_`, `_Add \`author="name"\` to every call to enable \`brain_who_knows\` + \`team_expertise_map\`._`);
3490
- return lines.join('\n');
3491
- }
3492
- // ── brain_coverage ────────────────────────────────────────────────────────
3493
- // Phase 3C: Knowledge-coverage health score for the repository.
3494
- case 'brain_coverage': {
3495
- const { instance_id, repo_path = '.', } = args;
3496
- const redis = await getConnection(instance_id);
3497
- // Count lessons, person + file nodes (each capped + timed out, gathered in parallel)
3498
- const [lessonKeys, personKeys, fileKeys] = await Promise.all([
3499
- scanKeys(redis, 'cachly:lesson:best:*', { max: 5000 }),
3500
- scanKeys(redis, 'cachly:ckg:node:person:*', { max: 2000 }),
3501
- scanKeys(redis, 'cachly:ckg:node:file:*', { max: 5000 }),
3502
- ]);
3503
- // Get git file count for comparison (optional — non-critical if not a git repo)
3504
- let gitFileCount = 0;
3505
- let _gitFileCountLabel = 'n/a';
3506
- try {
3507
- const { execSync } = await import('node:child_process');
3508
- const { resolve } = await import('node:path');
3509
- const repoDir = resolve(repo_path);
3510
- const output = execSync('git ls-files', { cwd: repoDir, encoding: 'utf-8', stdio: 'pipe' });
3511
- gitFileCount = output.trim().split('\n').filter(Boolean).length;
3512
- _gitFileCountLabel = String(gitFileCount);
3513
- }
3514
- catch { /* not a git repo — skip */ }
3515
- // Lesson quality breakdown
3516
- let successCount = 0;
3517
- let failureCount = 0;
3518
- let attributedCount = 0;
3519
- const domainSet = new Set();
3520
- for (const k of lessonKeys) {
3521
- const raw = await redis.get(k);
3522
- if (!raw)
3523
- continue;
3524
- const l = safeJsonParse(raw, {});
3525
- if (l.outcome === 'success' || l.outcome === 'partial')
3526
- successCount++;
3527
- else
3528
- failureCount++;
3529
- if (l.author)
3530
- attributedCount++;
3531
- domainSet.add((l.topic ?? '').split(':')[0] ?? 'unknown');
3532
- }
3533
- const fileCoverage = gitFileCount > 0
3534
- ? `${Math.round((fileKeys.length / gitFileCount) * 100)}% (${fileKeys.length}/${gitFileCount} files)`
3535
- : `${fileKeys.length} files tracked`;
3536
- const attributionPct = lessonKeys.length > 0
3537
- ? Math.round((attributedCount / lessonKeys.length) * 100)
3538
- : 0;
3539
- const overallScore = Math.round((Math.min(1, lessonKeys.length / 50) * 25) + // lesson volume (25 pts)
3540
- (Math.min(1, successCount / Math.max(1, lessonKeys.length)) * 25) + // success ratio (25 pts)
3541
- (Math.min(1, attributionPct / 100) * 25) + // attribution completeness (25 pts)
3542
- (Math.min(1, personKeys.length / 5) * 25) // team engagement (25 pts)
3543
- );
3544
- const scoreEmoji = overallScore >= 80 ? '🟢' : overallScore >= 50 ? '🟡' : '🔴';
3545
- return [
3546
- `## 📊 Brain Coverage Report`,
3547
- ``,
3548
- `${scoreEmoji} **Overall score: ${overallScore}/100**`,
3549
- ``,
3550
- `| Metric | Value |`,
3551
- `|--------|-------|`,
3552
- `| Total lessons | **${lessonKeys.length}** (${successCount} success · ${failureCount} failure) |`,
3553
- `| Problem domains | **${domainSet.size}** |`,
3554
- `| Attribution | **${attributedCount}/${lessonKeys.length}** (${attributionPct}%) |`,
3555
- `| Contributors | **${personKeys.length}** tracked in knowledge graph |`,
3556
- `| File coverage | **${fileCoverage}** |`,
3557
- ``,
3558
- `**Score breakdown (each 0–25):**`,
3559
- `- Lesson volume: **${Math.round(Math.min(1, lessonKeys.length / 50) * 25)}/25** _(target: 50+ lessons)_`,
3560
- `- Success ratio: **${Math.round(Math.min(1, successCount / Math.max(1, lessonKeys.length)) * 25)}/25** _(target: all lessons resolved)_`,
3561
- `- Attribution: **${Math.round(Math.min(1, attributionPct / 100) * 25)}/25** _(target: 100% attributed)_`,
3562
- `- Team engagement: **${Math.round(Math.min(1, personKeys.length / 5) * 25)}/25** _(target: 5+ contributors)_`,
3563
- ``,
3564
- `---`,
3565
- `_Boost score: run \`brain_from_git\` · add \`author\` to \`learn_from_attempts\` · fix gaps with \`skill_gaps\`_`,
3566
- ].join('\n');
3567
- }
3568
- // ── brain_metrics ─────────────────────────────────────────────────────────
3569
- // The three decisive metrics (PROGRESS.md §3 / VISION_10X.md §4):
3570
- // 1. Time-to-first-recall — onboarding friction
3571
- // 2. Recall-lift — the moat proof (vs. raw BM25)
3572
- // 3. Team-knowledge-reuse — the value only a shared brain delivers
3573
- case 'brain_metrics': {
3574
- const { instance_id } = args;
3575
- const redis = await getConnection(instance_id);
3576
- const [bornAt, firstRecallAt, recallsTotalRaw, crossAuthorRaw, timeSavedRaw, reusePairs] = await Promise.all([
3577
- redis.get(`cachly:stats:born_at:${instance_id}`).catch(() => null),
3578
- redis.get(`cachly:stats:first_recall_at:${instance_id}`).catch(() => null),
3579
- redis.get(`cachly:stats:recalls_total:${instance_id}`).catch(() => null),
3580
- redis.get(`cachly:stats:cross_author_recalls:${instance_id}`).catch(() => null),
3581
- redis.get(`cachly:stats:time_saved_mins:${instance_id}`).catch(() => null),
3582
- redis.smembers(`cachly:stats:reuse_pairs:${instance_id}`).catch(() => []),
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);
3616
- // ── Metric 1: Time-to-first-recall ──────────────────────────────────────
3617
- let ttfrLine;
3618
- if (bornAt && firstRecallAt) {
3619
- const ms = new Date(firstRecallAt).getTime() - new Date(bornAt).getTime();
3620
- const mins = Math.max(0, Math.round(ms / 60000));
3621
- const human = mins < 1 ? '<1 min' : mins < 60 ? `${mins} min` : `${(mins / 60).toFixed(1)} h`;
3622
- const target = ms <= 2 * 60000 ? '🟢 under 2 min target' : ms <= 60 * 60000 ? '🟡 over 2 min' : '🔴 over 1 h';
3623
- ttfrLine = `**${human}** from first lesson → first proven recall · ${target}`;
3624
- }
3625
- else if (bornAt && !firstRecallAt) {
3626
- ttfrLine = `🟡 Brain has knowledge but no proven recall yet — try \`smart_recall\``;
3627
- }
3628
- else {
3629
- ttfrLine = `⚪ Not enough data yet — store a lesson, then recall it`;
3630
- }
3631
- // ── Metric 2: Recall-lift ────────────────────────────────────────────────
3632
- //
3633
- // HIER STAND EINE FEST EINGEBAUTE ZAHL.
3634
- //
3635
- // "+33.3 % Precision@1 vs. raw BM25" war eine Zeichenkette, keine Messung.
3636
- // Der Kommentar darueber behauptete "CI-defended" und liess sie wie einen
3637
- // Messwert aussehen. Gefunden am 20.08.2026 beim ersten Lauf der Gegenrede.
3638
- //
3639
- // Die Zahl stammte ausserdem aus einer Formel, die am 19.08.2026
3640
- // ABGELOEST wurde: auf 498 echten Lektionen erreichte sie 15 Prozent, die
3641
- // Nachfolgerin 30. Der kleine Pruefstand (17 Lektionen, 13 Fragen) belohnte
3642
- // dabei die schlechtere Fassung mit 92,3 gegen 69,2 Prozent — seine Zahl
3643
- // bewegt sich ENTGEGENGESETZT zur Wirklichkeit. Belegt in src/bench/gate.ts.
3644
- //
3645
- // Eine Kennzahl, die der Server nicht selbst erhebt, gehoert nicht in seine
3646
- // Ausgabe. Sie kann nicht fallen, also misst sie nichts. Ein leeres Feld ist
3647
- // ehrlicher als eine Zahl, die immer gut aussieht.
3648
- //
3649
- // Dieselbe Entscheidung wurde am 19.08.2026 eine Kennzahl weiter unten schon
3650
- // einmal getroffen (die erfundenen "1.429,5 h"). Sie galt hier genauso und
3651
- // wurde nur nicht angewandt.
3652
- const recallLiftLine = [
3653
- '⚪ **Nicht gemessen** — diese Zahl erhebt der Server nicht selbst.',
3654
- ' Selbst messen: `npm run bench` in `sdk/mcp`.',
3655
- ' Achtung, der Pruefstand hat 17 Lektionen und 13 Fragen — ein Beispielsatz, kein Bestand.',
3656
- ' Auf 498 ECHTEN Lektionen gemessen am 19.08.2026: Precision@1 **30 %** (vorher 15 %).',
3657
- ].join('\n');
3658
- // ── Metric 3: Team-knowledge-reuse ──────────────────────────────────────
3659
- const recallsTotal = Number(recallsTotalRaw ?? 0);
3660
- const crossAuthor = Number(crossAuthorRaw ?? 0);
3661
- const reusePct = recallsTotal > 0 ? Math.round((crossAuthor / recallsTotal) * 100) : 0;
3662
- const reuseTarget = reusePct >= 30 ? '🟢 above 30 % target' : recallsTotal === 0 ? '⚪ no recalls yet' : '🟡 below 30 % target';
3663
- /*
3664
- * ── Die geschaetzten Stunden stehen nicht mehr im Kundenblick ─────────
3665
- *
3666
- * "1.429,5 h total saved not re-researching known fixes" war die
3667
- * groesste erfundene Zahl, die wir gezeigt haben. Sie ist gerechnet:
3668
- * Minuten je Lektion nach Schwere, mal Abrufzahl. Der Faktor ist
3669
- * geraten, und die Zahl waechst mit jedem Abruf mit — auch mit einem,
3670
- * der nichts gefunden hat.
3671
- *
3672
- * Heinrich am 19.08.2026 zur alten Fassung: "Ich tue mich schwer mit den
3673
- * 1200 h. Das haette vielleicht frueher zugetroffen, zu
3674
- * Entwicklungszeiten ohne AI, aber heutzutage nicht mehr. Die erscheint
3675
- * laecherlich hoch."
3676
- *
3677
- * Genau so ist es. Die Annahme dahinter — 30 bis 240 Minuten Recherche
3678
- * je Lektion — ist der Preis von VOR den Assistenten. Heute findet ein
3679
- * Modell dasselbe in Sekunden. Die Zahl misst eine Welt, die es nicht
3680
- * mehr gibt.
3681
- *
3682
- * FREMDBELEG dazu (Artikel 4278052, METR-Studie): erfahrene Entwickler
3683
- * FUEHLTEN sich mit KI schneller und waren gemessen 19 Prozent
3684
- * langsamer. Selbstauskunft und Messung zeigen in entgegengesetzte
3685
- * Richtungen — eine geschaetzte Zeitersparnis ist die schnellste Art,
3686
- * Vertrauen fuer alle anderen Zahlen gleich mit zu verlieren.
3687
- *
3688
- * Sie wird nicht geloescht, sondern ZURUECKGESTUFT: sie steht weiterhin
3689
- * da, aber klein, mit ihrer Annahme im selben Satz, und unter der
3690
- * gemessenen Liefermenge. Eine erfundene Zahl ALLEIN nervt; eine
3691
- * gemessene daneben traegt sie.
3692
- */
3693
- const timeSaved = Math.round(Number(timeSavedRaw ?? 0));
3694
- const timeSavedHuman = timeSaved < 60 ? `${timeSaved} min` : `${(timeSaved / 60).toFixed(1)} h`;
3695
- const lines = [
3696
- `## 📈 Brain Metrics — the three that decide everything`,
3697
- ``,
3698
- `### 1. Time-to-first-recall _(onboarding friction)_`,
3699
- ttfrLine,
3700
- ``,
3701
- `### 2. Recall-lift _(the moat proof)_`,
3702
- recallLiftLine,
3703
- `_Reproduce: \`npm run bench\` · CI-defended in \`rerank.test.ts\` · details in BENCH.md_`,
3704
- ``,
3705
- `### 3. Team-knowledge-reuse _(the value only a shared brain delivers)_`,
3706
- `**${reusePct}%** of proven recalls used a teammate's lesson (${crossAuthor}/${recallsTotal}) · ${reuseTarget}`,
3707
- reusePairs.length > 0 ? `**${reusePairs.length}** distinct reuse relationship${reusePairs.length !== 1 ? 's' : ''} across the team` : '',
3708
- ``,
3709
- `---`,
3710
- // GEMESSEN zuerst, GESCHAETZT darunter — in dieser Reihenfolge, immer.
3711
- // Vorher stand die gerechnete Stundenzahl ganz oben und fett; die
3712
- // einzige Zahl ohne Annahme kam gar nicht vor.
3713
- `### 📦 Aus dem Brain geliefert _(gemessen, nicht gerechnet)_`,
3714
- lieferungInWorten(lieferung),
3715
- ``,
3716
- lieferung.abrufe > 0
3717
- ? `_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._`
3718
- : `_Eine **Schätzung** von ${timeSavedHuman} „nicht neu recherchiert" wäre hier gerechnet, nicht gemessen — sie erscheint erst, wenn daneben eine gemessene Zahl steht._`,
3719
- recallsTotal === 0
3720
- ? `_Tip: pass \`author="your-handle"\` to \`smart_recall\` so cross-author reuse can be tracked._`
3721
- : `_These numbers compound: every learned lesson and every teammate raises all three._`,
3722
- ``,
3723
- `### 🔧 Werkzeug-Nutzung _(erst messen, dann zusammenlegen)_`,
3724
- nutzungInWorten(nutzung, TOOLS.length),
3725
- ...(nutzung.spitze.length > 0
3726
- ? nutzung.spitze.map((z, i) => ` ${String(i + 1).padStart(2)}. \`${z.name}\` — ${z.aufrufe}× (${Math.round(z.anteil * 100)} %)`)
3727
- : []),
3728
- ].filter(Boolean);
3729
- return lines.join('\n');
3730
- }
3731
- // ── brain_service_map ───────────────────────────────────────────────────────
3732
- // Phase 3: "Who owns this service, and what's gone wrong with it before?"
3733
- // Bridges a running system (e.g. a restarting prometheus pod) to the people who
3734
- // operate it, the files that run in it, and the lessons learned operating it.
3735
- case 'brain_service_map': {
3736
- const { instance_id, service, } = args;
3737
- if (!service || typeof service !== 'string' || !service.trim()) {
3738
- return '⚠️ `brain_service_map` requires a non-empty `service`. Example: `brain_service_map(service="prometheus")`.';
3739
- }
3740
- const redis = await getConnection(instance_id);
3741
- const serviceId = `service:${ckgSlug(service.trim())}`;
3742
- const nodeRaw = await redis.get(`cachly:ckg:node:${serviceId}`).catch(() => null);
3743
- const node = nodeRaw ? safeJsonParse(nodeRaw, null) : null;
3744
- // Inbound edges: person→operates, concept→affects, file→runs_in
3745
- const inbound = await redis.smembers(`cachly:ckg:idx:to:${serviceId}`).catch(() => []);
3746
- const operators = [];
3747
- const files = [];
3748
- const concepts = [];
3749
- for (const ek of inbound) {
3750
- const er = await redis.get(ek).catch(() => null);
3751
- if (!er)
3752
- continue;
3753
- const edge = safeJsonParse(er, null);
3754
- if (!edge)
3755
- continue;
3756
- if (edge.edgeType === 'operates') {
3757
- const pr = await redis.get(`cachly:ckg:node:${edge.from}`).catch(() => null);
3758
- const pn = pr ? safeJsonParse(pr, null) : null;
3759
- if (pn?.handle)
3760
- operators.push({ handle: pn.handle, trials: edge.trials });
3761
- }
3762
- else if (edge.edgeType === 'runs_in') {
3763
- const fr = await redis.get(`cachly:ckg:node:${edge.from}`).catch(() => null);
3764
- const fn = fr ? safeJsonParse(fr, null) : null;
3765
- if (fn?.path)
3766
- files.push({ path: fn.path, trials: edge.trials });
3767
- }
3768
- else if (edge.edgeType === 'affects') {
3769
- concepts.push({ id: edge.from, confidence: edge.confidence });
3770
- }
3771
- }
3772
- operators.sort((a, b) => b.trials - a.trials);
3773
- files.sort((a, b) => b.trials - a.trials);
3774
- concepts.sort((a, b) => b.confidence - a.confidence);
3775
- const lessons = [];
3776
- const lessonKeys = await scanKeys(redis, 'cachly:lesson:best:*', { max: 1500 });
3777
- const svcLower = service.trim().toLowerCase();
3778
- for (const k of lessonKeys.slice(0, 400)) {
3779
- const raw = await redis.get(k).catch(() => null);
3780
- if (!raw)
3781
- continue;
3782
- const ld = safeJsonParse(raw, {});
3783
- if (ld.visibility === 'private')
3784
- continue;
3785
- if ((ld.service ?? '').toLowerCase() !== svcLower)
3786
- continue;
3787
- lessons.push({ ...ld, topic: k.replace('cachly:lesson:best:', '') });
3788
- }
3789
- if (!node && lessons.length === 0 && operators.length === 0) {
3790
- return [
3791
- `## 🛰️ Service Map — \`${service}\``,
3792
- ``,
3793
- `Nothing known about this service yet.`,
3794
- ``,
3795
- `Tag lessons with the service they concern so the brain can build this map:`,
3796
- `\`learn_from_attempts(topic="...", service="${service}", author="you", file_paths=[...])\``,
3797
- ].join('\n');
3798
- }
3799
- const kindLabel = node?.kind === 'system' ? '🖥️ system' : '🛰️ service';
3800
- const lines = [
3801
- `## ${node?.kind === 'system' ? '🖥️' : '🛰️'} Service Map — \`${service}\``,
3802
- ``,
3803
- node ? `_${kindLabel} · ${node.count} lesson${node.count !== 1 ? 's' : ''} referenced · domain: ${node.domain}_` : `_No graph node yet — built from tagged lessons below._`,
3804
- ``,
3805
- ];
3806
- // Operators (who owns it)
3807
- if (operators.length > 0) {
3808
- const owners = operators.slice(0, 5).map((o, i) => {
3809
- const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`;
3810
- return `${medal} **${o.handle}** (${o.trials}×)`;
3811
- }).join(' · ');
3812
- lines.push(`**Operators:** ${owners}`, ``);
3813
- }
3814
- else {
3815
- lines.push(`**Operators:** _None attributed — add \`author\` when learning about this service_`, ``);
3816
- }
3817
- // Recent failures first — the triage gold for an incident
3818
- const failures = lessons.filter(l => l.outcome === 'failure');
3819
- const successes = lessons.filter(l => l.outcome !== 'failure');
3820
- if (failures.length > 0) {
3821
- lines.push(`**⚠️ Known failures (${failures.length}):**`);
3822
- for (const l of failures.slice(0, 6)) {
3823
- const sev = l.severity === 'critical' ? ' 🔴' : l.severity === 'major' ? ' 🟡' : '';
3824
- const by = l.author ? ` · 👤 ${l.author}` : '';
3825
- lines.push(` - ❌ \`${l.topic}\`${sev}${by} — ${(l.what_worked ?? '').slice(0, 100)}`);
3826
- }
3827
- lines.push(``);
3828
- }
3829
- if (successes.length > 0) {
3830
- lines.push(`**✅ Proven fixes (${successes.length}):**`);
3831
- for (const l of successes.slice(0, 6)) {
3832
- const emoji = l.outcome === 'partial' ? '⚠️' : '✅';
3833
- const by = l.author ? ` · 👤 ${l.author}` : '';
3834
- lines.push(` - ${emoji} \`${l.topic}\`${by} — ${(l.what_worked ?? '').slice(0, 100)}`);
3835
- }
3836
- lines.push(``);
3837
- }
3838
- // Files that run in this service
3839
- if (files.length > 0) {
3840
- lines.push(`**Files in this service:** ${files.slice(0, 8).map(f => `\`${f.path}\``).join(', ')}`, ``);
3841
- }
3842
- lines.push(`---`, `_Tag lessons with \`service="${service}"\` to keep this map current. For infra, pass \`service_kind="system"\`._`);
3843
- return lines.join('\n');
3844
- }
3845
- // ── brain_collab_pairs ───────────────────────────────────────────────────
3846
- // W5: Person↔Person Collaboration Graph. Shows who works with whom on the
3847
- // team — built from (a) shared file-touches during learn_from_attempts and
3848
- // (b) cross-author knowledge-reuse events from smart_recall. Answers the
3849
- // onboarding question: "Frag X und Y — they solved that together."
3850
- case 'brain_collab_pairs': {
3851
- const { instance_id, min_weight = 1 } = args;
3852
- const redis = await getConnection(instance_id);
3853
- const minW = typeof min_weight === 'number' && min_weight >= 1 ? Math.floor(min_weight) : 1;
3854
- // Collect all person nodes.
3855
- const personKeys = await scanKeys(redis, 'cachly:ckg:node:person:*', { max: 500 });
3856
- const persons = new Map();
3857
- for (const k of personKeys) {
3858
- const raw = await redis.get(k);
3859
- if (!raw)
3860
- continue;
3861
- const pn = safeJsonParse(raw, null);
3862
- if (!pn || pn.type !== 'person')
3863
- continue;
3864
- persons.set(pn.id, { id: pn.id, handle: pn.handle, domain: pn.domain, count: pn.count });
3865
- }
3866
- if (persons.size === 0) {
3867
- return [
3868
- `## 🤝 Team Collaboration Graph`,
3869
- ``,
3870
- `No contributors found yet.`,
3871
- ``,
3872
- `The graph builds automatically as teammates store lessons with \`author="name"\`:`,
3873
- `\`learn_from_attempts(topic="...", author="alice", file_paths=["src/auth.ts"], ...)\``,
3874
- ].join('\n');
3875
- }
3876
- const pairMap = new Map();
3877
- for (const [personId, pInfo] of persons) {
3878
- const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${personId}`);
3879
- for (const ek of edgeKeys) {
3880
- const er = await redis.get(ek);
3881
- if (!er)
3882
- continue;
3883
- const edge = safeJsonParse(er, null);
3884
- if (!edge || edge.edgeType !== 'collaborates')
3885
- continue;
3886
- const otherInfo = persons.get(edge.to);
3887
- if (!otherInfo)
3888
- continue;
3889
- // Canonical pair key: alphabetically smaller id first.
3890
- const [idA, idB] = personId < edge.to ? [personId, edge.to] : [edge.to, personId];
3891
- const pairKey = `${idA}||${idB}`;
3892
- const existing = pairMap.get(pairKey);
3893
- if (existing) {
3894
- existing.trials += edge.trials;
3895
- }
3896
- else {
3897
- const [pA, pB] = personId < edge.to ? [pInfo, otherInfo] : [otherInfo, pInfo];
3898
- pairMap.set(pairKey, { a: pA, b: pB, trials: edge.trials, confidence: edge.confidence });
3899
- }
3900
- }
3901
- }
3902
- const pairs = [...pairMap.values()]
3903
- .filter(p => p.trials >= minW)
3904
- .sort((a, b) => b.trials - a.trials);
3905
- // Find solo contributors (no collaborators).
3906
- const connectedIds = new Set();
3907
- for (const p of pairs) {
3908
- connectedIds.add(p.a.id);
3909
- connectedIds.add(p.b.id);
3910
- }
3911
- const soloPersons = [...persons.values()].filter(p => !connectedIds.has(p.id));
3912
- const lines = [
3913
- `## 🤝 Team Collaboration Graph`,
3914
- ``,
3915
- ];
3916
- if (pairs.length === 0) {
3917
- lines.push(`No collaboration pairs yet (min weight: ${minW}).`, ``, `Pairs appear when two contributors touch the same file in \`learn_from_attempts\``, `or when someone recalls a teammate's lesson via \`smart_recall(requester="...")\`.`);
3918
- }
3919
- else {
3920
- lines.push(`**${pairs.length} collaboration pair${pairs.length !== 1 ? 's' : ''}** across ${connectedIds.size} contributor${connectedIds.size !== 1 ? 's' : ''}:`, ``);
3921
- for (const p of pairs.slice(0, 20)) {
3922
- const conf = (p.confidence * 100).toFixed(0);
3923
- const sharedDomains = [p.a.domain, p.b.domain]
3924
- .filter((d, i, arr) => d && arr.indexOf(d) === i)
3925
- .map(d => `\`${d}\``)
3926
- .join(', ');
3927
- lines.push(`### @${p.a.handle} ↔ @${p.b.handle}`, `- **${p.trials} collaboration event${p.trials !== 1 ? 's' : ''}** · ${conf}% confidence`, `- Domains: ${sharedDomains || '_unknown_'}`, `> 💬 _"Frag **@${p.a.handle}** und **@${p.b.handle}** — they've solved problems together in these areas."_`, ``);
3928
- }
3929
- if (pairs.length > 20) {
3930
- lines.push(`_...and ${pairs.length - 20} more pairs. Use \`min_weight\` to filter._`, ``);
3931
- }
3932
- }
3933
- if (soloPersons.length > 0) {
3934
- lines.push(`---`, `### ⚠️ Bus Factor Alert — No Collaborators Yet`, ``, `${soloPersons.length} contributor${soloPersons.length !== 1 ? 's have' : ' has'} knowledge that no teammate has recalled or co-touched:`, ``, ...soloPersons.slice(0, 10).map(p => `- **@${p.handle}** — ${p.count} lesson${p.count !== 1 ? 's' : ''} · domain: \`${p.domain}\``), ``, `_Tip: pair these contributors with teammates on shared files to reduce bus factor._`);
3935
- }
3936
- lines.push(``, `---`, `_Collaboration edges build from \`learn_from_attempts(author=..., file_paths=[...])\` and \`smart_recall(requester=...)\` cross-author reuse._`);
3937
- return lines.join('\n');
3938
- }
3939
- // ── brain_portability ─────────────────────────────────────────────────────
3940
- case 'brain_portability': {
3941
- const { instance_id } = args;
3942
- const clients = [
3943
- {
3944
- name: 'Claude Code',
3945
- slug: 'claude-code',
3946
- icon: '🤖',
3947
- configPath: '~/.claude/settings.json',
3948
- configBlock: JSON.stringify({
3949
- mcpServers: {
3950
- cachly: {
3951
- command: 'npx',
3952
- args: ['@cachly-dev/mcp-server@latest'],
3953
- env: { CACHLY_JWT: '<cky_live_...>', CACHLY_BRAIN_INSTANCE_ID: instance_id },
3954
- },
3955
- },
3956
- }, null, 2),
3957
- },
3958
- {
3959
- name: 'Cursor',
3960
- slug: 'cursor',
3961
- icon: '🖱️',
3962
- configPath: '~/.cursor/mcp.json',
3963
- configBlock: JSON.stringify({
3964
- mcpServers: {
3965
- cachly: {
3966
- command: 'npx',
3967
- args: ['@cachly-dev/mcp-server@latest'],
3968
- env: { CACHLY_JWT: '<cky_live_...>', CACHLY_BRAIN_INSTANCE_ID: instance_id },
3969
- },
3970
- },
3971
- }, null, 2),
3972
- },
3973
- {
3974
- name: 'Windsurf',
3975
- slug: 'windsurf',
3976
- icon: '🏄',
3977
- configPath: '~/.windsurf/mcp.json',
3978
- configBlock: JSON.stringify({
3979
- mcpServers: {
3980
- cachly: {
3981
- command: 'npx',
3982
- args: ['@cachly-dev/mcp-server@latest'],
3983
- env: { CACHLY_JWT: '<cky_live_...>', CACHLY_BRAIN_INSTANCE_ID: instance_id },
3984
- },
3985
- },
3986
- }, null, 2),
3987
- },
3988
- {
3989
- name: 'GitHub Copilot (VS Code)',
3990
- slug: 'copilot',
3991
- icon: '🐙',
3992
- configPath: '.vscode/mcp.json (workspace)',
3993
- configBlock: JSON.stringify({
3994
- servers: {
3995
- cachly: {
3996
- type: 'stdio',
3997
- command: 'npx',
3998
- args: ['@cachly-dev/mcp-server@latest'],
3999
- env: { CACHLY_JWT: '<cky_live_...>', CACHLY_BRAIN_INSTANCE_ID: instance_id },
4000
- },
4001
- },
4002
- }, null, 2),
4003
- },
4004
- {
4005
- name: 'Cline',
4006
- slug: 'cline',
4007
- icon: '⚡',
4008
- configPath: '~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json',
4009
- configBlock: JSON.stringify({
4010
- mcpServers: {
4011
- cachly: {
4012
- command: 'npx',
4013
- args: ['@cachly-dev/mcp-server@latest'],
4014
- env: { CACHLY_JWT: '<cky_live_...>', CACHLY_BRAIN_INSTANCE_ID: instance_id },
4015
- },
4016
- },
4017
- }, null, 2),
4018
- },
4019
- {
4020
- name: 'Zed',
4021
- slug: 'zed',
4022
- icon: '⚡',
4023
- configPath: '~/.config/zed/settings.json',
4024
- configBlock: JSON.stringify({
4025
- context_servers: {
4026
- cachly: {
4027
- command: { path: 'npx', args: ['@cachly-dev/mcp-server@latest'] },
4028
- env: { CACHLY_JWT: '<cky_live_...>', CACHLY_BRAIN_INSTANCE_ID: instance_id },
4029
- },
4030
- },
4031
- }, null, 2),
4032
- },
4033
- {
4034
- name: 'Continue',
4035
- slug: 'continue',
4036
- icon: '▶️',
4037
- configPath: '~/.continue/config.json',
4038
- configBlock: JSON.stringify({
4039
- mcpServers: [
4040
- {
4041
- name: 'cachly',
4042
- command: 'npx @cachly-dev/mcp-server@latest',
4043
- env: { CACHLY_JWT: '<cky_live_...>', CACHLY_BRAIN_INSTANCE_ID: instance_id },
4044
- },
4045
- ],
4046
- }, null, 2),
4047
- },
4048
- ];
4049
- const lines = [
4050
- `## 🌐 Brain Portability — Same Brain, Any Model`,
4051
- ``,
4052
- `> _"Bring your own model, keep your brain."_`,
4053
- ``,
4054
- `**Your Brain ID:** \`${instance_id}\``,
4055
- ``,
4056
- `Replace \`<cky_live_...>\` below with your own API key (get one at https://cachly.dev/setup-ai) — the Brain ID alone does not authenticate you.`,
4057
- ``,
4058
- `This Brain ID is model-agnostic. Every lesson you've learned, every Memory Crystal you've distilled,`,
4059
- `and every prediction from your Causal Knowledge Graph is accessible from **any MCP-compatible AI client**.`,
4060
- ``,
4061
- `---`,
4062
- ``,
4063
- `### Compatible Clients (${clients.length} supported)`,
4064
- ``,
4065
- ];
4066
- for (const c of clients) {
4067
- lines.push(`#### ${c.icon} ${c.name}`, `Config: \`${c.configPath}\``, ``, '```json', c.configBlock, '```', ``);
4068
- }
4069
- lines.push(`---`, ``, `### What travels with you`, ``, `| Data | Model-neutral? |`, `|------|---------------|`, `| Lessons (learn_from_attempts) | ✅ Full access in all clients |`, `| Memory Crystals (memory_crystalize) | ✅ Included in every session_start |`, `| Causal Knowledge Graph (brain_predict) | ✅ Same graph, same predictions |`, `| Team lessons (team_learn, team_confirm) | ✅ Team is per-Brain, not per-model |`, `| Domain Brain Marketplace installs | ✅ Installed once, available everywhere |`, `| Session handoffs (session_handoff) | ✅ Cross-client continuity |`, ``, `---`, ``, `### One-command setup for any new client`, ``, '```bash', `npx @cachly-dev/mcp-server@latest autopilot`, '```', ``, `Autopilot detects all installed editors and writes the config for each — with your \`${instance_id}\` Brain ID pre-filled.`, ``, `---`, `_Docs: cachly.dev/docs/model-neutral • Your Brain, Any Model_`);
4070
- return lines.join('\n');
4071
- }
4072
- // ── brain_changelog ───────────────────────────────────────────────────────
4073
- // Generates a human-readable Markdown changelog of recent lessons.
4074
- // Useful for weekly standups, sprint reviews, or async team updates.
4075
- case 'brain_changelog': {
4076
- const { instance_id, days = 7, max_lessons = 30, include_failures = true, } = args;
4077
- const redis = await getConnection(instance_id);
4078
- const cutoff = Date.now() - Number(days) * 86_400_000;
4079
- const keys = await scanKeys(redis, 'cachly:lesson:best:*', { max: 4000, timeoutMs: 4000 });
4080
- const recent = [];
4081
- for (const k of keys) {
4082
- const raw = await redis.get(k).catch(() => null);
4083
- if (!raw)
4084
- continue;
4085
- const lesson = safeJsonParse(raw, {});
4086
- const ts = lesson.ts ? new Date(lesson.ts).getTime() : 0;
4087
- if (ts < cutoff)
4088
- continue;
4089
- if (!include_failures && lesson.outcome === 'failure')
4090
- continue;
4091
- const topic = k.replace('cachly:lesson:best:', '');
4092
- recent.push({ ...lesson, topic });
4093
- }
4094
- recent.sort((a, b) => {
4095
- const ta = a.ts ? new Date(a.ts).getTime() : 0;
4096
- const tb = b.ts ? new Date(b.ts).getTime() : 0;
4097
- return tb - ta;
4098
- });
4099
- const lessons = recent.slice(0, Number(max_lessons));
4100
- if (lessons.length === 0) {
4101
- return [
4102
- `## 📋 Brain Changelog — last ${days} day${Number(days) !== 1 ? 's' : ''}`,
4103
- ``,
4104
- `_No new lessons learned in this window. Your Brain already knows everything it knows — add new lessons with \`learn_from_attempts\`._`,
4105
- ].join('\n');
4106
- }
4107
- const groups = new Map();
4108
- for (const l of lessons) {
4109
- const cat = l.topic.includes(':') ? l.topic.split(':')[0] : 'general';
4110
- const arr = groups.get(cat) ?? [];
4111
- arr.push(l);
4112
- groups.set(cat, arr);
4113
- }
4114
- const severityEmoji = (s) => s === 'critical' ? '🚨' : s === 'major' ? '⚠️' : s === 'minor' ? '💡' : '✨';
4115
- const outcomeEmoji = (o) => o === 'failure' ? '❌' : o === 'partial' ? '🔶' : '✅';
4116
- const dateStr = new Date().toISOString().slice(0, 10);
4117
- const lines = [
4118
- `## 📋 Brain Changelog — ${dateStr} (last ${days} day${Number(days) !== 1 ? 's' : ''})`,
4119
- ``,
4120
- `_${lessons.length} lesson${lessons.length !== 1 ? 's' : ''} learned across ${groups.size} topic area${groups.size !== 1 ? 's' : ''}_`,
4121
- ``,
4122
- ];
4123
- for (const [cat, catLessons] of Array.from(groups.entries()).sort((a, b) => b[1].length - a[1].length)) {
4124
- lines.push(`### ${cat}`);
4125
- lines.push('');
4126
- for (const l of catLessons) {
4127
- const icon = l.outcome === 'failure' ? outcomeEmoji(l.outcome) : severityEmoji(l.severity);
4128
- const body = l.outcome === 'failure' ? l.what_failed : l.what_worked;
4129
- const meta = [];
4130
- if (l.author)
4131
- meta.push(`by **${l.author}**`);
4132
- if (l.recall_count)
4133
- meta.push(`${l.recall_count} recall${l.recall_count !== 1 ? 's' : ''}`);
4134
- if (l.confidence !== undefined)
4135
- meta.push(`${Math.round(l.confidence * 100)}% confidence`);
4136
- lines.push(`- ${icon} **${l.topic}**${body ? ` — ${body}` : ''}`);
4137
- if (meta.length)
4138
- lines.push(` _${meta.join(' · ')}_`);
4139
- }
4140
- lines.push('');
4141
- }
4142
- lines.push('---', `_Generated from Brain \`${instance_id}\` · Share this in your standup or Slack_`);
4143
- return lines.join('\n');
4144
- }
4145
- // ── brain_set_pref ────────────────────────────────────────────────────────
4146
- case 'brain_set_pref': {
4147
- const { instance_id, key, value, } = args;
4148
- if (!key || !key.trim())
4149
- throw new Error('key is required');
4150
- const redis = await getConnection(instance_id);
4151
- const redisKey = `cachly:prefs:${key.trim()}:${instance_id}`;
4152
- await redis.set(redisKey, String(value));
4153
- return `✅ Preference \`${key}\` set to \`${value}\` for this Brain instance.`;
4154
- }
4155
- // ── brain_get_pref ────────────────────────────────────────────────────────
4156
- case 'brain_get_pref': {
4157
- const { instance_id, key } = args;
4158
- const redis = await getConnection(instance_id);
4159
- if (key && key.trim()) {
4160
- const val = await redis.get(`cachly:prefs:${key.trim()}:${instance_id}`).catch(() => null);
4161
- return val === null
4162
- ? `ℹ️ Preference \`${key}\` is not set (using default).`
4163
- : `🔧 Preference \`${key}\` = \`${val}\``;
4164
- }
4165
- const prefKeys = await scanKeys(redis, `cachly:prefs:*:${instance_id}`, { max: 200, timeoutMs: 2000 });
4166
- if (prefKeys.length === 0)
4167
- return 'ℹ️ No preferences set for this Brain instance.';
4168
- const vals = await redis.mget(...prefKeys);
4169
- const lines = ['🔧 **Brain preferences:**', ''];
4170
- for (let i = 0; i < prefKeys.length; i++) {
4171
- const k = prefKeys[i].replace('cachly:prefs:', '').replace(`:${instance_id}`, '');
4172
- lines.push(`- \`${k}\` = \`${vals[i] ?? '(null)'}\``);
4173
- }
4174
- return lines.join('\n');
4175
- }
4176
- // ── sync_file_changes ─────────────────────────────────────────────────────
4177
- default:
4178
- return null;
4179
- }
4180
- }
4181
- //# sourceMappingURL=brain.js.map
1
+ import{execSync as Me}from"node:child_process";import{createRequire as Ne}from"node:module";import{calculateConfidence as ge,confidenceBadge as Ye,STRUCTURED_TEMPLATES as Ue,CONFIDENCE_WARN_VALUE as Ae,CONFIDENCE_STALE_VALUE as Ve,CONFIDENCE_WARN_DAYS as Xe}from"../confidence.js";import{ckgSlug as Z,extractProblemConcept as $e,ckgUpsertNode as de,ckgUpdateEdge as ne,ckgUpsertPersonNode as Ze,ckgUpsertFileNode as Ie,ckgRecordCollaboration as Qe,ckgUpsertServiceNode as et}from"../ckg.js";import{safeJsonParse as P,scanKeys as ce}from"../utils.js";import{ersparteMinuten as Ee,istStarterLektion as ye,zaehltAlsErsparnis as Re}from"../wertbeitrag.js";import{lessonPreviewLines as me,trimTo as tt}from"../lesson-preview.js";import{meldeEinmal as fe,meldeUndVermerke as st,fehlerText as ve,OHNE_VEKTOREN as nt,OHNE_DIENST as ot,OHNE_FRAGEVEKTOR as at,SINNPFAD_ABBRUCH as it,OHNE_SCHREIBVEKTOR as ke}from"../aussetzer.js";import{versuchStart as rt,neueKennung as ct,wendeZuteilungAn as lt,schliesseVersuchAb as ut}from"../versuch.js";function Le(te,G,q){if(te.includes("lesson:best:")){const b=P(G,{});if(b&&typeof b=="object"&&(b.what_worked||b.what_failed||b.commands?.length))return me(b,{maxChars:q,indent:" "})}const ae=tt(G,q);return ae?[ae]:[]}import{getRole as Be,ROLE_BADGE as ht,getScopes as dt,lessonVisibleToScope as ft,reviewModeEnabled as pt,storeLessonProposal as mt}from"./team.js";import{keywordSearch as Pe,wortindexEntwerten as gt,tokenize as Se,splitMultiQuery as yt,levenshtein as wt,indexVocab as De}from"../search.js";import{rerankByQuality as _t,qualityMultiplier as bt,extractLessonQuality as $t}from"../rerank.js";import{computeEmbedding as pe,hasEmbedProvider as we,EMBED_PROVIDER as Ce}from"../embeddings.js";import{repariereFelder as vt}from"../feldreparatur.js";import{VEKTOR_PRAEFIX as kt,NAME_VEKTOR_PRAEFIX as Oe,packe as je,textFuerVektor as St,textFuerNamensVektor as Ct,Vektorbestand as We}from"../bedeutung.js";import{schreibeEingaenge as xt,Eingangsbestand as Tt}from"../eingaenge.js";import{Seltenheitsbestand as Mt}from"../seltenheitsbestand.js";import{spurLegen as Nt}from"../spuren.js";import{bewerteTopf as At,spreizeImTopf as It,reichereAn as Et,inhaltsWoerter as Fe,grobStamm as Rt,GEWICHTE as Lt}from"../rangfolge.js";import{pruefeWirkung as Bt,werteAus as Pt,lehrwert as Dt,leiteAb as Ot}from"../wirkungsspur.js";import{SINN_TOPF as jt,EINGANG_SCHWELLE as Wt,EINGANG_SORTIER_GEWICHT as Ft}from"../rangfolge-stellschrauben.js";const ue=new We,xe=new We(6e4,Oe),Te=new Tt,_e=new Mt,zt=jt,Kt=Wt,Gt=Ft;import{upgradeNudge as qt}from"../upgrade-nudge.js";import{recallTiefe as ze,TIEFE_VOLL as Ht,TIEFE_VOLL_MEHRTHEMIG as Jt}from"../recall-tiefe.js";import{nutzungInWorten as Yt,nutzungsSchluessel as Ut,verdichte as Vt}from"../werkzeug-nutzung.js";import{inWorten as Xt,lieferBild as Zt,lieferSchluessel as Qt,trefferSchluessel as es}from"../geliefert.js";import{COAUTHOR_PREF_KEY as ts,autorHinweis as ss,coautorAus as ns,echterGitLeser as os,ermittleAutor as as,mitCoautor as is,nurMensch as Ke}from"../autor.js";import{TOOLS as rs}from"../tools.js";import{vorspannHinweis as cs}from"../vorspann.js";import{autorAbzeichen as ls,fremdanteil as us}from"../autor-abzeichen.js";import{cachlyUrl as hs}from"../cachly-url.js";const be=(()=>{try{const te=Ne(import.meta.url);for(const G of["../../../package.json","../../package.json"])try{const q=te(G);if(q.name==="@cachly-dev/mcp-server"&&q.version)return q.version}catch{}}catch{}return"0.10.100"})(),ds=()=>{try{return Ne(import.meta.url)("../../../CACHLY_CAPABILITIES.json").mcp?.total_tools??null}catch{}return null},fs={"0.10.119":["\u{1F50D} **Explainable briefings \u2014 provenance in every warning**"," \u{1F4CE} `/briefing` warnings now carry `outcome`, `author`, `learned_at` and `matched_on` (the context tokens that triggered the warning) \u2014 editors can show WHY a fix hint fired, not just what it claims.",' \u{1F9F0} VS Code 0.12.0 uses this: severity + confidence in the toast, full lesson card with provenance, "Not helpful" per-file suppression, reviewable CLS auto-learns.'," \u{1F9F9} CI docs now describe exactly what ships: scan mode uses the REST `/scan` endpoint, the GitLab template covers learn/scan/confirm."],"0.10.115":["\u{1F527} **`brain_get_pref` + `brain_set_pref` \u2014 persistent Brain preferences**"," \u{1F4D6} `brain_get_pref` reads back any stored preference (omit key to list all)."," \u270F\uFE0F `brain_set_pref` saves a preference to Redis \u2014 survives restarts, scoped per Brain instance.",' \u{1F4A1} Known key: `auto_changelog` \u2014 set to "false" to silence the new-lessons digest at session_start.'," \u{1F4CA} 140 MCP tools"],"0.10.114":["\u{1F4CB} **`brain_changelog` \u2014 weekly knowledge digest in one call**"," \u{1F4C5} Generates a grouped Markdown changelog of lessons learned in the last N days."," \u{1F465} Grouped by topic category, annotated with author, recall count, and confidence \u2014 paste directly into standup or Slack."," \u2699\uFE0F Params: `days` (default 7), `max_lessons` (default 30), `include_failures` (default true)."," \u{1F4CA} 138 MCP tools"],"0.10.113":["\u{1F4C8} **Week-over-week savings trend in `cache_stats`**"," \u{1F4C5} `cache_stats` now shows **7-day hits** + **WoW trend %** (e.g. `+23.0% vs prev week`) so ROI momentum is visible, not just the cumulative total."," \u2B07\uFE0F Goes red (\u{1F4C9}) on drops, green (\u{1F4C8}) on growth \u2014 first week always shows +100% (new)."," \u{1F527} Backed by new `CountHitsInWindow` repo method + `hits_last_7d`, `hits_prev_7d`, `week_over_week_pct` fields in `SavingsEstimate`."," \u{1F4CA} 137 MCP tools"],"0.10.112":["\u2699\uFE0F **Configurable cost_per_call_usd \u2014 accurate ROI for any LLM**"," \u{1F4A1} New `set_cost_per_call` tool: set your real per-call cost (claude-opus-4.8 \u2192 $0.02, gpt-5.5 \u2192 $0.015, claude-sonnet-4.6 \u2192 $0.009, claude-haiku-4.5 \u2192 $0.001) so savings estimates in `cache_stats` reflect your actual bill."," \u{1F527} New API: `PUT /api/v1/instances/:id/cost-per-call` \u2014 persisted per instance, used by `cache_stats`, org savings, and SSE analytics stream."," \u{1F4CA} 137 MCP tools"],"0.10.111":["\u{1F3E2} **Org-wide ROI + GitLab CI \u2014 the CFO number, aggregated**"," \u{1F4B0} `cache_org_stats` now shows **org-wide Tokenmaxxing ROI**: total savings summed across every instance in your org, monthly projection, top-5 instances by savings. One number for the whole team."," \u{1F50C} Backed by new API endpoint `GET /api/v1/orgs/:id/savings` (org members only)"," \u{1F98A} GitLab CI integration: `brain-from-ci-gitlab.yml` \u2014 two `.post` jobs (on_success/on_failure), same helper script, pipeline never fails because of Brain push"," \u{1F6E0}\uFE0F GitHub Action template fixed: `secrets.*` in job-level `if` is invalid \u2014 guard removed (helper script skips cleanly on missing secrets)"," \u{1F4CA} 136 MCP tools"],"0.10.110":["\u{1F3E2} **Zero friction: ROI-Projektor, Org-Shared-Cache, CI ready-to-paste**"," \u{1F4C8} `cache_stats` \u2014 zero-hit projection calculator: shows expected monthly savings for 5/10/20-dev teams before a single cache hit lands. ROI visible from day 1."," \u{1F3E2} New `cache_org_stats` \u2014 shared org-level cache stats across multiple instances. Convention-based (`org:{id}:sem`), zero API changes needed."," \u{1F517} `cache_set`/`cache_get` now accept optional `org_id` \u2014 write to personal + org namespace, fall back to org on miss. Fully backwards-compatible."," \u{1F4CB} Copy-paste CI integration: `brain-from-ci-action.yml` \u2192 Brain self-calibrates confidence on every GitHub Actions run. No config beyond two repo secrets."," \u{1F4CA} 136 MCP tools"],"0.10.109":["\u{1F4B0} **Tokenmaxxing ROI + Closed-loop CI learning**"," \u{1F4CA} `cache_stats` now shows a **Tokenmaxxing ROI** section \u2014 total tokens saved, monthly cost projection, and top 3 most-cached prompts. Finally a number you can show the CFO."," \u{1F504} New `brain_from_ci` \u2014 bulk-ingest CI history into the Brain (red\u2192green = learned fix, green\u2192red = CKG causal edge). The brain_from_git equivalent for CI. Bootstrap the Brain from your CI logs in one call."," \u{1F4CA} 134 MCP tools"],"0.10.108":["\u{1F9E0} **Focused briefing + trustworthy autopilot**"," \u{1F3AF} New `session_start_summary` \u2014 top-N focused briefing by topic. Scores lessons by relevance \xD7 confidence + recall_count + severity + recency. Use it for a quick pre-brief without loading the full Brain.",' \u{1F91D} Autopilot template now explains the data flow upfront ("your own Brain, EU-hosted, no third parties") \u2014 no more friction with cautious AI assistants during onboarding'," \u{1F4CA} 132 MCP tools"],"0.10.107":["\u{1F3AF} **Recall@3 gap closed \u2014 the moat proof got sharper (the head-to-head that matters)**"," \u{1F50D} External-corpus Recall@3 **82.1% \u2192 98.2%** \xB7 Precision@1 **80.4%** \xB7 MRR **89.0%** \u2014 now ahead of flat-file memory on every metric"," \u{1F9EC} Root-cause fix: documents are no longer indexed with cross-lingual synonyms (the symmetric-expansion bug inflated any doc containing a common word ~28\xD7, burying topic-specific lessons). Queries still expand \u2014 a \u65E5\u672C\u8A9E query still finds English lessons."," \u2696\uFE0F Reranker now compresses BM25 with score^0.3 before applying lesson quality, so a proven success can overtake a text-similar failed attempt even when the distractor scores 5\u20136\xD7 higher in raw BM25"," \u{1F6E1}\uFE0F CI bench gate widened to both home + external corpora with committed floors (`npm run bench:gate`) \u2014 recall quality can never silently regress again"," \u{1F4CA} 121 MCP tools"],"0.10.98":['\u{1F310} **Model-Neutrality as Feature (W9) \u2014 "Bring your own model, keep your brain."**'," \u{1F50C} New `brain_portability()` \u2014 shows your Brain ID, all compatible MCP clients, and ready-to-paste config snippets for each; proves the same Brain works in Claude, Cursor, Copilot, Windsurf, Cline, Zed and more"," \u{1F9E0} Your lessons, crystals, and predictions are model-agnostic \u2014 no vendor lock-in at the memory layer",' \u{1F4D6} New docs: cachly.dev/docs/model-neutral \u2014 "Same Brain, Any Model" guide with live config examples'," \u{1F4CA} 121 MCP tools (new: `brain_portability`)"],"0.10.97":['\u{1F4A0} **Team Crystallize (W8) \u2014 the team-wide, causal counter to per-user "Dreaming"**'," \u{1F9EC} New `team_crystallize()` \u2014 surfaces fixes that 2+ teammates *independently converged on*, the one thing a single-user memory structurally can't build",' \u{1F9E9} Cross-person clustering: groups structurally similar problems across people & namespaces (e.g. "pool exhaustion" solved the same way in payments, auth & db)',' \u{1F465} Each pattern names who converged \u2014 instant "ask @alice and @bob, they both hit this"'," \u{1F48E} Shows up in `crystal_view` alongside your per-brain crystal"," \u{1F4CA} 120 MCP tools (new: `team_crystallize`)"],"0.10.96":["\u{1F510} **Roles & Team-Scopes now ENFORCED (W6) \u2014 the enterprise governance layer**"," \u{1F6E1}\uFE0F `team_confirm` is now reviewer-gated, `team_learn` blocks viewers (recall-only) \u2014 roles stop being cosmetic the moment an admin is assigned"," \u{1F4DC} New `team_audit` tool \u2014 immutable, admin-only trail of every role change & lesson confirmation, for compliance & security reviews"," \u{1F513} Zero impact on open teams: governance activates only once you bootstrap an admin; before that everything works exactly as before"," \u{1F4CA} 119 MCP tools (new: `team_audit`)"],"0.10.95":["\u{1F9E0} **Domain Brain Marketplace (W10) \u2014 bootstrap your Brain with curated community knowledge**"," \u{1F6D2} `brain_marketplace()` \u2014 browse installable packs of high-trust lessons by domain: \u2638\uFE0F Kubernetes, \u{1F510} Auth, \u{1F5C4}\uFE0F Database, \u269B\uFE0F React, \u{1F4B3} Payments & more",' \u{1F4E6} `brain_install(slug="k8s")` \u2014 merges a domain\'s verified lessons into your Brain; live in `smart_recall` instantly, even offline',' \u{1F6E1}\uFE0F Idempotent + non-destructive: installed lessons are tagged `source:"marketplace:<slug>"` and NEVER override your own'," \u{1F50C} New gRPC `Subscribe` RPC \u2014 agents get new lessons streamed live the moment they're learned (completes the M2M loop with `Learn`)"," \u{1F4CA} 118 MCP tools (new: `brain_marketplace`, `brain_install`)"],"0.10.94":['\u{1F91D} **Person\u2194Person Collaboration Graph (W5) \u2014 "Frag X und Y, die haben das zusammen gel\xF6st"**'," \u{1F578}\uFE0F New `brain_collab_pairs()` tool: shows every contributor pair who've touched the same files or recalled each other's lessons \u2014 ideal for onboarding and bus-factor analysis"," \u{1F4E1} `smart_recall` now wires CKG `collaborates` edges live when cross-author reuse fires \u2014 collaboration graph grows with every team recall"," \u26A0\uFE0F Bus-factor section: flags solo contributors whose knowledge no teammate has yet recalled"," \u{1F4CA} 116 MCP tools (new: `brain_collab_pairs`)"],"0.10.93":['\u{1F5FA}\uFE0F **`brain_plan` \u2014 your Brain goes from "what did we learn?" to "what should I do?"**',` \u{1F9ED} Give it a task you're about to start ("upgrade Postgres 14\u219216") and it returns an ordered, grounded action plan: ranked failure modes to avoid, proven fix steps (dependency-aware, with commands), and a pre-flight checklist \u2014 all from your own lessons`,' \u{1F52C} Generative layer on the CKG: where `brain_predict` answers "what might fail?", `brain_plan` answers "what, in what order?"'," \u{1F4CA} 115 MCP tools (new: `brain_plan`)"],"0.10.92":["\u{1F44B} **Welcome-back digest \u2014 re-entry after a week away now leads with what changed**"," \u{1F331} `session_start` detects a gap \u2265 7 days and surfaces how many lessons your Brain gained while you were gone (git, CI & teammates kept it learning), with the freshest topics"," \u{1F9E0} Zero extra latency \u2014 the digest is computed from data session_start already loads"," \u{1F50C} New gRPC `Learn` RPC (M2M write path) \u2014 agents & CI can now teach the Brain directly, byte-compatible with the MCP lesson format"," \u{1F4CA} 115 MCP tools"],"0.10.91":["\u{1F550} **Lesson-anchored time attribution \u2014 every recall shows when it was learned and how much it saves**",` \u{1F4A1} "Brain saved you" banner now includes: when the lesson was learned (e.g. "12 May"), estimated original debug cost by severity (critical=2h, major=1h, minor=20min), and team attribution when a teammate's lesson fires`,' \u{1F4C5} Origin date formatted as "12 May" (same year) or "12 May 2024" (different year) \u2014 no clutter',' \u{1F465} Cross-author recalls now show "@author" on the same banner line \u2014 the "team knowledge reuse" value is front-and-center'," \u{1F4CA} 115 MCP tools"],"0.10.90":["\u{1F4CA} **External benchmark corpus v2 \u2014 the proof is now statistically solid**"," \u{1F9EA} Expanded from 8 queries \u2192 61 lessons / 56 queries across 10 real-world engineering domains (k8s, DB, auth, CI, frontend, API, payments, observability, network, Node.js, security, infra)"," \u2694\uFE0F 10 adversarial distractors added \u2014 failure lessons that share the exact same vocabulary as the query, forcing the quality reranker to earn its lift"," \u{1F4C8} Results on the larger corpus: cachly +2.9% Precision@1 vs flat-file, +12.5% vs raw BM25 baseline \u2014 on a harder dataset, not an easy one"," \u{1F4CA} 115 MCP tools"],"0.10.89":["\u26A1 **One-command onboarding \u2014 `autopilot` is now the single canonical setup**"," \u{1F680} Every surface (README, docs, emails, dashboard, blog) now leads with `npx @cachly-dev/mcp-server@latest autopilot` \u2014 one command signs you in, configures every editor, and bootstraps from git",' \u{1F9F9} Retired the split-brain instructions (legacy `@cachly-dev/init` wrapper + the old "three steps") so there\'s exactly one thing to copy-paste'," \u{1F527} Fixed a build-breaking duplicate `export` on buildClaudeMdBlock that failed CI on v0.10.88"," \u{1F522} Version hygiene \u2014 package.json, server.json, lockfile and MCP_VERSION resynced (server.json/lockfile were stuck at 0.10.85)"," \u{1F4CA} 115 MCP tools"],"0.10.88":["\u2705 **Honest setup + tested config writer (v0.10.88)**",' \u{1F512} setup/autopilot now report the truth: if no editor config could be written it exits non-zero with the exact failures instead of falsely claiming "Brain is ready"'," \u{1F4CA} New setup_config_write_failed telemetry \u2014 permission/path failures that block activation are now visible in reporting"," \u{1F9EA} 24 new unit tests for the config writer (buildServerEnv, buildMcpConfig, mergeMcpConfig, buildClaudeMdBlock) \u2014 the merge logic that must never clobber your other MCP servers is now regression-guarded"," \u{1F4CA} 115 MCP tools"],"0.10.87":["\u{1F310} **Multi-editor auth persistence + full activation telemetry (v0.10.87)**"," \u2705 JWT + instance_id now persisted to Cursor (`~/.cursor/mcp.json`) and Windsurf configs on auth \u2014 no more infinite re-auth for non-Claude-Code users",' \u{1F4E1} `device_flow_failed(reason="timeout")` now fires when sign-in window expires \u2014 blind spot in the funnel is gone'," \u{1F4E1} `auto_provision_failed` now fires on network errors too (was only HTTP non-2xx)"," \u{1F4E1} `brain_from_git_failed` fires with error reason when auto-bootstrap throws \u2014 no more empty brain with no explanation",' \u2728 Git bootstrap shows lesson count in session_start: "Brain bootstrapped \u2014 23 lessons loaded" \u2014 the WOW moment'," \u{1F527} API: McpEvent stores brain quality metrics (fixes/features/refactors/total) \u2014 git learning quality now measurable"," \u{1F4CA} 115 MCP tools"],"0.10.86":["\u{1F6E1}\uFE0F **Zero-friction activation \u2014 your Brain self-heals into existence**"," \u{1F527} Auto-provision now runs on *any* tool call, not just at sign-in \u2014 if instance creation ever fails once, the next call recovers it automatically"," \u{1F91D} Parallel tool calls on startup are coalesced into one resolve (no more duplicate-instance races)",' \u{1F4AC} Clearer "instance still starting" message \u2014 leads with "just retry", no manual config needed'," \u{1F4C8} New activation-funnel telemetry: install \u2192 sign-in \u2192 provision, so silent churn is visible"," \u{1F4CA} 115 MCP tools"],"0.10.85":["\u{1F680} **One-command onboarding \u2014 `npx @cachly-dev/mcp-server@latest autopilot`**"," \u26A1 Single command does it all: auth \u2192 instance \u2192 every editor config \u2192 CLAUDE.md \u2192 Brain bootstrap \u2192 health"," \u{1F331} Seeds the 16-lesson starter corpus when git history is empty \u2014 your first smart_recall hits instantly"," \u{1F916} Fully automatic (zero prompts); `setup` remains for interactive, pick-your-editors onboarding"],"0.10.84":["\u{1F916} **M2M & agent-ecosystem reach \u2014 cachly for every caller, human or machine**"," \u{1F511} OAuth2 `client_credentials` grant \u2014 set CACHLY_CLIENT_ID + CACHLY_CLIENT_SECRET for fully headless auth (CI, agents, AI-to-AI)"," \u{1F5C2}\uFE0F `npx ... tool-specs --format=openai|anthropic|langchain` \u2014 export all 121 tools in any framework's dialect"," \u{1F310} `npx ... openapi` \u2014 OpenAPI 3.1 doc (1 POST path per tool) for Assistants / codegen / Postman"," \u{1F9EA} 14 new tests: client_credentials helpers, all four spec dialects, OpenAPI required-body inference"],"0.10.83":["\u{1F9E0} **Brain Viz \u2014 3D graph export**"," \u{1F310} `brain_graph(instance_id)` \u2014 exports the Causal Knowledge Graph as a render-ready {nodes, links} payload"," \u{1F3A8} Schema `cachly.brain_graph/v1`: node kinds (concept/person/file/service) with color groups + size, edges with confidence"," \u{1F5A5}\uFE0F Consumed verbatim by the 3D frontend (react-force-graph-3d / three.js) \u2014 the visual brain map",' \u{1F50E} Filters: `domain`, `min_confidence`, `max_nodes` (with truncation flag), `format="summary"`'," \u{1F9EA} 10 tests: kind detection, dangling-edge pruning, confidence/domain filters, schema contract"," \u{1F4CA} 115 MCP tools"],"0.10.82":["\u{1F4E1} **Telemetry API contracts \u2014 dashboard ingest pipeline prepared**"," \u{1F5C2}\uFE0F `src/telemetry-types.ts` \u2014 canonical event types + resolved event format (TelemetryEventRaw / TelemetryEventResolved)"," \u{1F510} `user_fingerprint` \u2014 non-reversible JWT sub hash, PII-free user clustering for dashboard"," \u{1F4CA} `metrics` block on key events: smart_recall (hit/topic), brain_from_git (fixes/features/total), brain_seed_starter (seeded_count)"," \u{1F6A6} Internal API contract documented: GET /internal/telemetry/stream \u2192 TelemetryEventResolved[]"],"0.10.81":["\u23F1\uFE0F **Onboarding-Bench \u2014 time-to-first-recall, now measured**"," \u{1F4D0} `npm run bench:onboarding` \u2014 cold (empty) vs seeded (starter) first-query hit rate via the real search engine"," \u{1F4CA} Result: first-query hit@1 **0% \u2192 87.5%**, hit@3 **0% \u2192 100%**, MRR **0% \u2192 93.8%**"," \u{1F6E1}\uFE0F CI-defended in `onboarding-bench.test.ts` (cold must answer 0%, seeded thresholds enforced)"," \u{1F4CB} `PLAN-DASHBOARD.md` \u2014 full build spec for the external Team-Knowledge-Reuse dashboard (separate repo)"],"0.10.80":["\u{1F331} **Starter corpus \u2014 first recall in seconds, not sessions**"," \u{1F4DA} `brain_seed_starter(instance_id)` \u2014 seed 16 curated universal lessons (Docker cache, JWT skew, K8s OOM, N+1, cache stampede\u2026)"," \u{1F680} Auto-seeds on first session when git history is empty/shallow \u2014 your first `smart_recall` returns a real hit",' \u{1F6E1}\uFE0F Tagged `source:"starter"`, idempotent, never overrides your own lessons'," \u23F1\uFE0F Directly attacks time-to-first-recall (<2 min goal); stamps `born_at` on seed"," \u{1F9EA} 18 new tests: seeding, idempotency, topic_filter, force, user-lesson protection"," \u{1F4CA} 113 MCP tools"],"0.10.79":["\u{1F310} **Brain Marketplace \u2014 full shareable Brain lifecycle**"," \u{1F4CB} `brain_share_list(instance_id)` \u2014 see all your public shares with lesson counts + share IDs"," \u{1F5D1}\uFE0F `brain_unshare(share_id)` \u2014 revoke a share; link goes dead immediately"," \u{1F50D} `brain_discover(query, topic)` \u2014 search public Brains in the cachly marketplace"," \u{1F4E4} `publish` CLI \u2014 fancy card + share URL; `--public` for discoverable; `--title` custom name"," \u{1F9EA} stress tests: team_expertise_map with 50 contributors, chain import/unshare, discover fallback"," \u{1F4CA} 112 MCP tools"],"0.10.78":["\u{1F310} **Phase 3: Shareable / Public Brains**",' \u{1F4E4} `brain_share(instance_id="...")` \u2014 export a Brain snapshot as a shareable link (public or unlisted)',' \u{1F4E5} `brain_import(instance_id="...", share_id="...")` \u2014 import any public Brain into yours (1-line)'," \u{1F50D} Optional topic_filter, min_confidence, topic_prefix \u2014 precise control over what gets shared/imported"," \u{1F512} dry_run mode on both tools \u2014 preview before committing"," \u{1F9EA} Regression suite added: share handler, buildServerEnv, stdout cleanness"," \u{1F4CA} 109 MCP tools"],"0.10.77":["\u{1FA79} **Frictionless onboarding + first-class self-hosting/BYOK**"," \u{1F680} Editor-launched server now serves all tools even before sign-in \u2014 zero-credential device flow finally triggers on first tool call"," \u{1F512} No more stdout pollution: stdio JSON-RPC stream stays clean when no JWT is set"," \u{1F3E0} `setup --api-url` / `init --api-url` point at a self-hosted backend; URL baked into config only when non-default",' \u{1F9EC} `health` now shows your BYOK embedding provider + accepts cky_ long-lived keys (was a false "invalid token" failure)'],"0.10.76":["\u{1F4CA} **Agent-trace benchmark + editor matrix + setup timer**"," \u{1F4C2} New `agent-traces-corpus.json` \u2014 22 lessons, 15 queries, realistic AI-agent debug patterns"," \u{1F3C6} Benchmark result: **P@1 +66.7%, MRR +9.5% vs flat-file** (adversarial failure-distractors per category)"," \u{1F4CB} README: first-class editor support matrix (Claude Code, Cursor, Windsurf, VSCode, Cline, Continue.dev, Zed)",' \u23F1\uFE0F setup now shows elapsed time + "Brain is ready" banner at the end'],"0.10.75":["\u{1FA79} **Setup hardened \u2014 works from the VSCode plugin & any non-TTY terminal**"," \u{1F50C} `setup` no longer hangs on a non-interactive stdin (VSCode tasks, CI) \u2014 auto-detects + runs in automatic mode"," \u{1F510} Device-flow now uses the API proxy first (the old direct-Keycloak call returned 403) \u2014 sign-in actually completes"," \u{1F310} When auto sign-in is unavailable, the browser opens with a step-by-step API-key guide instead of a silent paste prompt"," \u{1F4CA} Funnel fix: `device_flow_completed` / `device_flow_failed(reason)` now emitted from setup too"],"0.10.73":["\u{1F510} **Team visibility scopes + one-command init + external bench**",' \u{1F195} `team_grant_scope` / `team_scopes` \u2014 scope lessons to a sub-team (group="security"); only members + admins recall them',' \u{1F3F7}\uFE0F `learn_from_attempts(group="...")` \u2014 orthogonal to private; smart_recall enforces it per requester'," \u26A1 `init` is now zero-arg + idempotent (reuses saved creds, only writes what changed, <60s)"," \u{1F4E6} `npm run bench:external` \u2014 prove recall-lift on a third-party-labeled corpus (portable JSON format)"," \u{1F4CA} 107 MCP tools"],"0.10.72":["\u{1F451} **Role model \u2014 admin \xB7 reviewer \xB7 contributor \xB7 viewer (Phase 3)**"," \u{1F195} `team_assign_role` \u2014 establish governance; first call bootstraps, admins manage the rest"," \u{1F195} `team_whoami` \u2014 see your own role and capabilities"," \u{1F195} `team_roster` \u2014 full team role table (\u{1F451}\u{1F6E1}\uFE0F\u270F\uFE0F\u{1F441}\uFE0F)"," \u{1F6E1}\uFE0F `team_confirm` is now role-aware \u2014 admin/reviewer auto-get senior weight; contributor gets peer; no self-promotion"," \u{1F451} Setup CLI now prompts for governance bootstrap (idempotent). 105 MCP tools"],"0.10.71":['\u{1F6E1}\uFE0F **Self-healing auth** \u2014 no more silent "0 recalls because the token quietly died"'," \u267B\uFE0F Near-expiry tokens auto-refresh into a long-lived key while still valid (zero interaction)"," \u{1F501} A rejected (401) call self-heals once and retries before surfacing an error"," \u{1F4E3} If the credential is truly dead, session_start + get_api_status say so plainly + how to fix"],"0.10.70":["\u{1F6F0}\uFE0F **Service/System nodes in the graph (Phase 3)** \u2014 the brain now models running systems",' \u{1F195} `brain_service_map(service="prometheus")` \u2014 who owns it + every failure/fix known',' \u{1F3F7}\uFE0F Tag lessons with `service="..."` (`service_kind="system"` for infra) \u2014 person\u2192operates, file\u2192runs_in edges',` \u{1F6A8} Incident triage: a restarting pod \u2192 instant "who knows this and what's broken before"`," \u{1F4CA} 102 MCP tools"],"0.10.69":["\u{1F4C1} **Personalized context-aware recall (Phase 3)** \u2014 pass `context_files` to `smart_recall`"," \u{1F3AF} Lessons learned on your current files bubble up \u2014 even when the query doesn't name the file"," \u{1F3F7}\uFE0F `\u{1F4C1} context match` badge on every file-boosted result so you know WHY it ranked up"," \u2705 Works alongside keyword, semantic, CKG, and governance signals \u2014 a 6th ranking dimension"],"0.10.68":["\u{1F91D} **Collaboration graph (Phase 3)** \u2014 person\u2194person edges from shared files"," \u{1F465} `brain_who_knows` now shows who the top expert frequently works with",' \u{1F68C} Bus-factor insight: "ask X and Y together \u2014 they solved this on shared files"'," \u{1F6E1}\uFE0F Stability: every network call in the agent hot path is now timeout-bounded"],"0.10.64":["\u{1F4C8} **The three decisive metrics, now measurable**"," \u23F1\uFE0F `brain_metrics()` \u2014 time-to-first-recall, recall-lift, team-knowledge-reuse in one view"," \u{1F465} Cross-author reuse tracked: recall a teammate's lesson \u2192 counted + surfaced inline"," \u{1F6E1}\uFE0F Stability: scans now capped + timed out (a huge keyspace can't hang the agent)"],"0.10.61":["\u{1F3AF} **Phase 3C: 100 MCP tools milestone \u2014 zero-setup knowledge graph**"," \u{1F504} `brain_from_git` now auto-builds Person+File nodes from git history (zero setup!)"," \u{1F50D} `skill_gaps()` \u2014 find domains with unresolved failures, missing attribution"," \u{1F4CA} `brain_coverage()` \u2014 scored 0-100 health report: lessons \xB7 attribution \xB7 file coverage"],"0.10.60":["\u{1F195} **Phase 3B: File map + team overview + visibility**"," \u{1F4C1} `brain_file_map(file_paths=[...])` \u2014 experts + lessons per file before you touch it"," \u{1F5FA}\uFE0F `team_expertise_map()` \u2014 full team skills matrix in one table",' \u{1F512} `visibility: "private"` on `learn_from_attempts` \u2014 private notes never leak to smart_recall'," \u{1F4CA} 98 MCP tools"],"0.10.59":["\u{1F195} **Phase 3A: Org-wide knowledge graph**",' \u{1F465} `brain_who_knows(topic="...")` \u2014 find your team\'s experts on any topic instantly',' \u{1F578}\uFE0F Person + File nodes auto-built from `learn_from_attempts(author="...", file_paths=[...])`'," \u{1F464} Author attribution now shown inline in `smart_recall` results"," \u{1F4CA} 96 MCP tools \xB7 386 tests green"],"0.10.58":["\u{1F9F9} Zero lint warnings \u2014 all unused imports cleaned across every handler","\u2705 Build: 0 errors, 0 warnings \xB7 379 tests green"],"0.10.57":[`\u{1F195} **What's new in v${be}:**`," \u{1F527} **Critical fix** \u2014 corrected the npm bin/main entry path (`dist/src/index.js`); 0.10.50\u20130.10.52 shipped a broken entry point"," \u{1F527} Version hygiene \u2014 synced package.json, server.json, lockfile, MCP_VERSION; `latest` now points forward again"," \u2705 Accurate tool count (95) across README, server.json, CLI banners"," \u2705 Includes all Phase 2/3 work: quality reranking, Cachly-Bench, hybrid + CKG recall, team_confirm governance"],"0.10.52":[" \u2705 `smart_recall` \u2014 CKG traversal as Layer 3: finds lessons that FIXED causal-graph-similar problems (\u{1F578}\uFE0F causal graph badge)"," \u2705 Bench corpus expanded: governance adversarial pair proves review-boost end-to-end (+22% P@1 headline)"," \u{1F527} Import cleanup: unused symbols removed from brain.ts (no functional change)"," \u{1F4A1} `smart_recall` now shows keyword / semantic / CKG / hybrid match type for every result"],"0.10.51":[" \u2705 `team_confirm` \u2014 human review raises lesson recall ranking (\u{1F6E1}\uFE0F senior / \u2714\uFE0F peer)"," \u2705 Governance-aware recall \u2014 confirmed lessons outrank unreviewed auto-learned entries"],"0.10.50":[" \u2705 `smart_recall` \u2014 unified keyword + semantic hybrid list (one ranked result, not two sections)",' \u2705 `smart_recall` \u2014 "Brain saved you here" banner surfaces time saved inline per proven lesson'," \u2705 Quality reranking \u2014 proven successes outrank symptom-dense failures (+11% Precision@1)"," \u2705 Cachly-Bench \u2014 IR benchmark with CI regression guard (see BENCH.md for the proof)"," \u{1F527} Contradiction resolution now persisted to Redis (90-day TTL)"," \u{1F4A1} Run `smart_recall` to see your Brain's recall quality in action"],"0.10.49":[" \u2705 `brain_from_git` \u2014 auto-seed your Brain from git history in seconds"," \u2705 `brain_predict_failures` \u2014 predict CI/build failures before they happen"," \u2705 VS Code extension \u2014 Brain status bar, ambient learning, CodeLens hints"," \u2705 Team Brain \u2014 share lessons across your whole engineering team"," \u{1F527} Stability \u2014 auto-expiring memory keys, longer provisioning wait, robust recall"]},Js=new Set(["learn_from_attempts","recall_best_solution","smart_recall","recall_feedback","session_start","session_start_summary","session_end","session_ping","session_handoff","auto_learn_session","brain_who_knows","brain_file_map","team_expertise_map","skill_gaps","brain_coverage","brain_metrics","brain_service_map","brain_collab_pairs","brain_portability","brain_changelog","brain_set_pref","brain_get_pref"]),ps=hs("/billing","upgrade"),Ge=new Map,qe=new Set,ms=1680*60*60;function gs(te=new Date){return`cachly:quota:recalls:${te.toISOString().slice(0,7)}`}async function He(te){try{const G=gs();await te.incr(G)===1&&await te.expire(G,ms)}catch{}}async function Je(te,G){const q=Ge.get(te);if(q&&q.expiresAt>Date.now())return q.gate;try{const ae=await G(`/api/v1/instances/${te}/memory`),b=ae?.recall_limit??-1,t=ae?.total_recall_count??0,o={reached:b>0&&t>=b,limit:b,used:t,goodwillMessage:ae?.goodwill_message,notice:ae?.notice};return Ge.set(te,{gate:o,expiresAt:Date.now()+6e4}),o}catch{return{reached:!1,limit:-1,used:0}}}async function Ys(te,G,q,ae){switch(te){case"learn_from_attempts":{const{instance_id:b,topic:t,outcome:o,what_worked:S,what_failed:A="",context:p="",severity:l="major",file_paths:$=[],commands:s=[],tags:M=[],depends_on:g=[],author:x="",visibility:v="team",service:f="",service_kind:c="service",group:k=""}=G,{what_worked:m,what_failed:_="",context:I=""}=vt({what_worked:S,what_failed:A,context:p}),h=await q(b),e=new Date().toISOString(),D=as({uebergeben:x,umgebung:process.env.CACHLY_AUTHOR,git:os(process.env.CACHLY_WORKSPACE??process.cwd())});let O="";try{O=ns(await h.get(`cachly:prefs:${ts}:${b}`))}catch{}const F=is(D.autor,O),H=ss(D);if(v!=="private"&&await pt(h,b))return mt(h,t,{topic:t,outcome:o,what_worked:m,what_failed:_,context:I,severity:l,file_paths:$,commands:s,tags:M,depends_on:g,...F?{author:F}:{},...f?{service:f}:{},...k?{group:String(k).toLowerCase().trim()}:{},visibility:v,recall_count:0,ts:e,confidence:1,version:3});const W=t.split(":")[0],J=Ue[W],U=[];if(J)for(const T of J.required)T==="commands"&&s.length===0&&U.push(`\u{1F4CB} ${J.hint}`);let Q=!1,V=0,se,n=[];const y=await h.get(`cachly:lesson:best:${t}`);if(y)try{const T=JSON.parse(y);V=T.recall_count??0,se=T.last_recalled_at,n=T.audit_trail??[],n.push({ts:e,action:"updated",prev_outcome:T.outcome}),n.length>20&&(n=n.slice(-20)),Q=!0;const N=[];if(T.outcome==="success"&&o==="failure"?N.push("\u26A0\uFE0F **Contradiction detected!** Existing lesson has outcome: `success`, but you're storing `failure`.",'The existing "success" lesson will be preserved. Only the audit trail is updated.',"If you meant to mark this as failed permanently, store a new lesson with a distinct topic slug."):T.outcome==="failure"&&o==="success"&&N.push("\u2705 **Conflict resolved!** Previous lesson was `failure` \u2014 now overwriting with `success`."),N.length>0){n[n.length-1].action="contradiction-resolved";try{const B=Z(t),R=Z(`resolution:${t}`);await ne(h,B,"contradicts",R,!1)}catch{}try{const B=`cachly:contradictions:${t}`,R=T.outcome==="success"&&o==="failure"?"kept-success":"overwrote-failure";await h.rpush(B,JSON.stringify({ts:e,prev_outcome:T.outcome,new_outcome:o,direction:R})),await h.ltrim(B,-50,-1),await h.expire(B,180*86400)}catch{}N.push(`\u{1F5F3}\uFE0F Run \`madc_deliberate(topic="${t}")\` to resolve via expert agent voting.`)}}catch{}else n=[{ts:e,action:"created"}];const i=[];if(o==="failure"){const T=[],N=h.scanStream({match:"cachly:lesson:best:*",count:100});await new Promise((R,E)=>{N.on("data",j=>T.push(...j)),N.on("end",R),N.on("error",E)});const B=t.split(/[:\-_]/).filter(R=>R.length>2);for(const R of T.slice(0,50)){const E=await h.get(R);if(E)try{const j=JSON.parse(E);if(j.outcome!=="success")continue;const Y=(j.topic??"").split(/[:\-_]/).filter(oe=>oe.length>2);if(B.filter(oe=>Y.includes(oe)).length>=1&&j.topic!==t){i.push(`\u26A0\uFE0F **"I Was Wrong"**: lesson \`${j.topic}\` (success, ${j.severity??"major"}) might have prevented this failure.`,` \u2192 Use \`recall_best_solution(topic="${j.topic}")\` before next attempt.`,` \u2192 To mark it critical: \`learn_from_attempts(topic="${j.topic}", ..., severity="critical")\``);break}}catch{}}}for(const T of g){const N=`cachly:dep:${T}`,B=await h.get(N),R=P(B,[]);R.includes(t)||R.push(t),await h.set(N,JSON.stringify(R),"EX",90*86400)}const u={topic:t,outcome:o,what_worked:m,what_failed:_,context:I,severity:l,file_paths:$,commands:s,tags:M,depends_on:g,...F?{author:F}:{},...f?{service:f}:{},...k?{group:String(k).toLowerCase().trim()}:{},visibility:v,recall_count:V,...se?{last_recalled_at:se}:{},ts:e,verified_at:o==="success"||o==="partial"?e:void 0,confidence:1,audit_trail:n,version:3},r=JSON.stringify(u);h.set(`cachly:stats:born_at:${b}`,e,"EX",365*86400,"NX").catch(()=>{});const C=`cachly:lessons:${t}`;await h.rpush(C,r),await h.ltrim(C,-100,-1),await h.expire(C,90*86400);let L=!1;o==="success"||o==="partial"?(await h.set(`cachly:lesson:best:${t}`,r),L=!0):y||(await h.set(`cachly:lesson:best:${t}`,r),L=!0),L&&gt(),L&&we()&&(async()=>{const T=P(r,{});try{const N=await pe(St(T));N?.length&&await h.set(`${kt}${t}`,je(N))}catch(N){fe(ke,`Volltext-Vektor nicht geschrieben (${Ce}): ${ve(N)} \u2014 die Lektion ist gespeichert, der Bedeutungsabgleich uebergeht sie.`)}try{const N=await pe(Ct(t));N?.length&&await h.set(`${Oe}${t}`,je(N))}catch(N){fe(ke,`Namens-Vektor nicht geschrieben (${Ce}): ${ve(N)} \u2014 siehe oben, die Lektion selbst ist gespeichert.`)}try{await xt(h,t,T,pe)}catch(N){fe(ke,`Eingaenge nicht geschrieben (${Ce}): ${ve(N)} \u2014 siehe oben, die Lektion selbst ist gespeichert.`)}})();try{const T="cachly:session:decision-log",N=JSON.stringify({ts:e,topic:t,outcome:o,what_worked:m.slice(0,120)});await h.rpush(T,N),await h.ltrim(T,-50,-1)}catch{}let a=null;try{const T=Z(t),N=t.split(":")[0]??"unknown";await de(h,T,N,N);for(const R of M){const E=Z(`tag:${R}`);await de(h,E,"tag","tag"),await ne(h,T,"co-occurs",E,o==="success",o==="partial")}for(const R of g){const E=Z(R);await ne(h,T,"requires",E,!0)}if((N==="fix"||N==="debug")&&(o==="success"||o==="partial")){const R=_||I||"",E=R?$e(R):null;if(E){const j=Z(`problem:${E}`);await de(h,j,"problem","problem"),await ne(h,T,"fixes",j,o==="success",o==="partial")}}if(o==="failure"&&(_||m)){const E=$e(_||m);if(E){const j=Z(`cause:${E}`);await de(h,j,"cause","cause"),await ne(h,T,"causes",j,!1)}}if(o==="failure"){const R=await h.smembers(`cachly:ckg:idx:from:${T}`);for(const E of R){const j=await h.get(E);if(!j)continue;const Y=P(j,null);if(Y&&Y.edgeType==="fixes"&&Y.confidence>.7&&Y.trials>=3){a=`\u26A0\uFE0F **belief_conflict** on \`${t}\`: previously confirmed fix (confidence ${Y.confidence.toFixed(2)}, n=${Y.trials}) now reports failure. Both beliefs retained as \`contested\`. Use \`brain_resolve_conflict(instance_id="...", topic="${t}", winner="success"|"failure")\` to arbitrate, or \`ckg_inspect(concept="${T}")\` to review.`;const X=`cachly:ckg:conflict:${T}`;await h.set(X,JSON.stringify({topic:t,concept_id:T,detected_at:new Date().toISOString(),fix_confidence:Y.confidence,fix_trials:Y.trials,failure_outcome:o,reported_by:F||"unknown",what_failed:_||"",resolved:!1}),"EX",3600*24*90)}}}if(o==="success"&&(N==="fix"||N==="debug")){const R=await h.smembers(`cachly:ckg:idx:from:${T}`);for(const E of R.slice(0,10)){const j=await h.get(E);if(!j)continue;const Y=P(j,null);if(!Y||Y.edgeType!=="fixes")continue;const X=await h.smembers(`cachly:ckg:idx:from:${Y.to}`);for(const oe of X.slice(0,5)){const re=await h.get(oe);if(!re)continue;const ee=P(re,null);ee&&ee.edgeType==="co-occurs"&&(ee.successes=(ee.successes||0)+.1,ee.trials=(ee.trials||0)+.1,ee.confidence=(ee.successes+1)/(ee.trials+2),ee.last_updated=new Date().toISOString(),await h.set(oe,JSON.stringify(ee)))}}}if(Math.random()<.01){const R=Date.now()-7776e6,E=await h.smembers(`cachly:ckg:idx:from:${T}`);for(const j of E){const Y=await h.get(j);if(!Y)continue;const X=P(Y,null);X&&X.trials<3&&X.last_updated&&new Date(X.last_updated).getTime()<R&&(X.confidence=X.confidence*.9,X.last_updated=new Date().toISOString(),await h.set(j,JSON.stringify(X)))}}}catch{}if(F){h.set(`cachly:agents:active:${F}`,JSON.stringify({author:F,last_topic:t,last_outcome:o,ts:new Date().toISOString()}),"EX",3600).catch(()=>{});try{const T=t.split(":")[0]??"unknown",N=await Ze(h,Ke(F),T),B=Z(t);await ne(h,N,"authored",B,o==="success",o==="partial");for(const R of $.slice(0,8)){const E=await Ie(h,R);await ne(h,N,"touched",E,!0),await Qe(h,E,N)}}catch{}}if(f&&typeof f=="string"&&f.trim())try{const T=t.split(":")[0]??"unknown",N=c==="system"?"system":"service",B=await et(h,f.trim(),T,N),R=Z(t);if(await ne(h,R,"affects",B,o==="success",o==="partial"),F){const E=`person:${Z(Ke(F))}`;await ne(h,E,"operates",B,!0)}for(const E of $.slice(0,8)){const j=`file:${Z(E)}`;await ne(h,j,"runs_in",B,!0)}}catch{}if(F&&(o==="success"||o==="partial")){const T={topic:t,outcome:o,what_worked:m,what_failed:_??"",severity:l,author:F,file_paths:$,commands:s,tags:[...new Set([...M,"team"])],timestamp:new Date().toISOString(),recall_count:0,version:2,auto_propagated:!0},N=`cachly:lessons:${t}`;h.rpush(N,JSON.stringify(T)).catch(()=>{})}return[`${o==="success"?"\u2705":o==="partial"?"\u26A0\uFE0F":"\u274C"} **Lesson ${Q?"updated":"stored"}:** \`${t}\` (${o}) ${l==="critical"?"\u{1F534}":l==="major"?"\u{1F7E1}":"\u{1F7E2}"} ${l}`,a??"","",`**What worked:** ${m}`,_?`**What failed:** ${_}`:"",I?`**Context:** ${I}`:"",$.length>0?`**Files:** ${$.map(T=>`\`${T}\``).join(", ")}`:"",s.length>0?`**Commands:** ${s.map(T=>`\`${T}\``).join(", ")}`:"",M.length>0?`**Tags:** ${M.map(T=>`#${T}`).join(" ")}`:"","",Q?`\u267B\uFE0F Updated (recall count: ${V} \xB7 audit entries: ${n.length})`:`\u{1F4A1} Recall later with \`recall_best_solution(topic="${t}")\``,F&&(o==="success"||o==="partial")?`\u{1F465} Auto-shared with team (by _${F}_${D.quelle!=="uebergeben"?` \xB7 aus ${D.quelle}`:""}) \u2014 visible in \`team_recall\``:"",H??"",g.length>0?`\u{1F517} Depends on: ${g.map(T=>`\`${T}\``).join(", ")} \u2192 trace with \`trace_dependency\``:"",...U,...i,cs(m)??""].filter(T=>T!=="").join(`
2
+ `)}case"recall_feedback":{const{instance_id:b}=G,t=await q(b),o=Bt(G,new Date().toISOString());if(!o.ok)return[`\u26A0\uFE0F ${o.grund}`,"","Beispiel:","```",'recall_feedback(instance_id="\u2026", query="warum bricht der Deploy ab",',' topic="ci:zeitgrenze-meldet-cancelled", helped=true, rank=0)',"```","rank=0 heisst: die Lektion stand GAR NICHT in der Antwort.","Das ist die wertvollste Rueckmeldung von allen."].join(`
3
+ `);const S=o.eintrag,A=`cachly:wirkung:${b}`;await t.rpush(A,JSON.stringify(S));const l=(await t.lrange(A,0,-1).catch(()=>[])).map(x=>P(x,null)).filter(Boolean),$=Pt(l),s=Dt(S),M={"fehlender-treffer":"Sie half, stand aber gar nicht in der Antwort \u2014 der Topf hat sie nicht aufgenommen. Das ist der wertvollste Fall.","zu-weit-hinten":`Sie half, stand aber auf Platz ${S.platz} \u2014 die Sortierung war zu schwach.`,"falsch-und-sicher":`Sie stand auf Platz ${S.platz} und half nicht \u2014 die Sortierung war sich sicher und lag falsch.`,bestaetigung:"Sie stand vorn und half. Bestaetigung.",unauffaellig:"Half nicht und stand hinten. Sagt wenig."},g=$.trefferquote===null?"noch keine hilfreiche Rueckmeldung":`${($.trefferquote*100).toFixed(0)} % der hilfreichen Lektionen standen unter den ersten dreien`;return[`\u2705 Notiert: **${S.thema}** \u2014 ${S.geholfen?"hat geholfen":"hat nicht geholfen"}, Platz ${S.platz===0?"\u2014 (stand nicht drin)":S.platz}`,"",M[s],"",`Spur: ${$.gesamt} Rueckmeldungen, ${g}.`,`Davon: ${$.nachLehrwert["fehlender-treffer"]} nicht gefunden \xB7 ${$.nachLehrwert["zu-weit-hinten"]} zu weit hinten \xB7 ${$.nachLehrwert["falsch-und-sicher"]} falsch und sicher.`].join(`
4
+ `)}case"recall_best_solution":{const{instance_id:b,topic:t}=G,o=await q(b);try{const s=await o.get(`cachly:letzter-recall:${b}`),M=s?P(s,null):null,g=Ot(M,t,new Date().toISOString());g&&(await o.rpush(`cachly:wirkung:${b}`,JSON.stringify(g)),await o.del(`cachly:letzter-recall:${b}`))}catch{}const S=await o.get(`cachly:lesson:best:${t}`);if(S){const s=P(S,null);if(!s)return`\u26A0\uFE0F Lesson data for \`${t}\` is corrupted. Re-store it with \`learn_from_attempts\`.`;const M=ge(s),g=s.verified_at??s.ts,x=(Date.now()-new Date(g).getTime())/864e5,v=Ye(M,x),f=new Date().toISOString(),c={...s,recall_count:(s.recall_count??0)+1,verified_at:f,last_recalled_at:f,confidence:1};await o.set(`cachly:lesson:best:${t}`,JSON.stringify(c)),He(o);const k=Re(s)?Ee(s):0;k>0&&o.incrbyfloat(`cachly:stats:time_saved_mins:${b}`,k).catch(()=>{});const m=s.severity==="critical"?"\u{1F534}":s.severity==="major"?"\u{1F7E1}":s.severity?"\u{1F7E2}":"",_=(s.audit_trail??[]).length>1?`_Audit: ${(s.audit_trail??[]).length} changes \xB7 stored ${new Date(s.ts).toLocaleDateString("de-DE")}_`:"",I=(Date.now()-new Date(s.ts).getTime())/864e5,h=I>60?`\u{1F4AD} _Remember when you solved this ${Math.round(I/30)} months ago? Still works._`:"",e=c.recall_count===3&&!s.pinned?"\u{1F4CC} **You've looked this up 3 times.** Consider pinning it for instant access: add `pinned: true` via `learn_from_attempts` to always surface it first.":"",D=s.authors??(s.author?[s.author]:[]),O=[...new Set(D.filter(Boolean))],F=c.recall_count;let H="";return O.length>=2&&F>=5?H=`\u{1F3C6} **Battle-tested** \u2014 recalled ${F}\xD7 \xB7 verified by ${O.length} developers. Trust this.`:F>=10?H=`\u{1F3C6} **Battle-tested** \u2014 recalled ${F}\xD7. This is one of your most-proven solutions.`:O.length>=2?H=`\u2705 **Team-verified** \u2014 confirmed by ${O.length} developers.`:F>=5&&(H=`\u2705 **Proven** \u2014 recalled ${F}\xD7 without contradiction.`),[h,H,`${v} **Best solution for \`${t}\`** ${m}${s.severity?` (${s.severity})`:""} \xB7 recalled ${c.recall_count}\xD7`,"",s.what_worked?`**What worked:** ${s.what_worked}`:"",s.what_failed?`**What failed (avoid this):** ${s.what_failed}`:"",s.context?`**Context:** ${s.context}`:"",(s.file_paths??[]).length>0?`**Files:** ${(s.file_paths??[]).map(W=>`\`${W}\``).join(", ")}`:"",(s.commands??[]).length>0?`**Commands:** ${(s.commands??[]).map(W=>`\`${W}\``).join(", ")}`:"",(s.tags??[]).length>0?`**Tags:** ${(s.tags??[]).map(W=>`#${W}`).join(" ")}`:"",_,e].filter(W=>W!=="").join(`
5
+ `)}const A=[],p=o.scanStream({match:"cachly:lesson:best:*",count:100});await new Promise((s,M)=>{p.on("data",g=>A.push(...g)),p.on("end",s),p.on("error",M)});const l=A.filter(s=>s.toLowerCase().includes(t.toLowerCase()));if(l.length===0){const s=`cachly:lessons:${t}`,M=await o.lrange(s,-3,-1);if(M.length>0){const x=M.map(v=>P(v,null)).filter(Boolean).map(v=>`- ${v.outcome==="success"?"\u2705":"\u274C"} ${v.what_worked.slice(0,120)} (${new Date(v.ts).toLocaleDateString("de-DE")})`);return`\u26A0\uFE0F No successful solution for \`${t}\` yet. Last attempts:
6
+
7
+ ${x.join(`
8
+ `)}`}return`\u{1F4ED} No lessons found for \`${t}\`. Use \`learn_from_attempts\` after solving it.`}const $=[];for(const s of l.slice(0,5)){const M=await o.get(s);if(!M)continue;const g=P(M,null);g&&$.push(`**\`${g.topic}\`** \u2014 ${g.what_worked.slice(0,200)}`)}return`\u{1F50D} **Partial matches for \`${t}\`:**
9
+
10
+ ${$.join(`
11
+
12
+ `)}`}case"smart_recall":{const{instance_id:b,query:t,threshold:o=.78,author:S="",context_files:A=[]}=G,p=Array.isArray(A)?A.filter(a=>typeof a=="string"):[],l=await q(b);let $=new Set,s=!1;S&&($=await dt(l,b,S).catch(()=>new Set),s=await Be(l,b,S).catch(()=>null)==="admin");const M=await Pe(l,["cachly:ctx:*","cachly:lesson:best:*","cachly:idx:*"],t,25),g=_t(M);let x=!1;const v=await(async()=>{if(!we())return fe(ot,"kein Einbettungsdienst eingerichtet \u2014 die Suche laeuft nur ueber Woerter"),g;try{if(await ue.aktualisiere(l),ue.groesse===0)return fe(nt,"keine einzige Lektion hat eine Einbettung \u2014 die Suche laeuft nur ueber Woerter. Nachruesten mit eingaenge-nachruesten, pruefen mit brain_doctor"),g;const a=await pe(t);if(!a?.length)return fe(at,"die Frage liess sich nicht einbetten \u2014 diese Suche laeuft nur ueber Woerter"),g;await xe.aktualisiere(l),await Te.aktualisiere(l),await _e.aktualisiere(l);const d=g.filter(K=>K.key.startsWith("cachly:lesson:best:")).map(K=>K.key.slice(19)),w=K=>Math.max(ue.naehe(a,K),xe.naehe(a,K)),z=[..._e.themen()],T=(z.length>0?z:ue.aehnlichste(a,500).map(K=>K.topic)).map(K=>({t:K,n:w(K)})).sort((K,ie)=>ie.n-K.n).slice(0,zt).map(K=>K.t),N=[...new Set([...d,...T])],B=T.slice(0,3).map(K=>ue.rohvektor(K)).filter(Boolean),R=B.length?Et(a,B):a,E=Fe(t),j=_e.statistik,Y=N.map(K=>({naeheText:ue.naehe(a,K),naeheThema:xe.naehe(a,K),naeheRueckkopplung:ue.naehe(R,K),seltenheitsDeckung:j?j.deckung(E,new Set([...Fe(_e.textVon(K))].map(Rt))):0}));let X=At(Y,Lt);if(Te.groesse>0){const K=N.map(he=>{const le=Te.besteNaehe(a,he);return le>=Kt?le:-2}),ie=It(K);X=X.map((he,le)=>he+Gt*ie[le])}const oe=N.map((K,ie)=>({t:K,p:X[ie]})).sort((K,ie)=>ie.p-K.p).map(K=>K.t),re=new Map(g.map(K=>[K.key,K])),ee=[];for(const K of oe){const ie=`cachly:lesson:best:${K}`,he=re.get(ie);if(he){ee.push(he),re.delete(ie);continue}const le=await l.get(ie).catch(()=>null);le&&ee.push({key:ie,content:le,score:0,matchedWords:[],finalScore:0,qualityBoost:1})}return ee.push(...re.values()),x=!0,ee}catch(a){return await st(l,it,`der Bedeutungspfad brach ab (${a instanceof Error?a.message:String(a)}) \u2014 diese Suche lief nur ueber Woerter`),g}})();if(He(l),we()){const a=v.filter(d=>d.key.startsWith("cachly:lesson:best:")).slice(0,3).map(d=>d.key.slice(19));Nt(l,t,a,pe).catch(()=>{})}const f=[],c=v.filter(a=>a.key.startsWith("cachly:lesson:best:"));let k=0,m=0;for(const a of c.slice(0,5)){const d=await l.get(a.key).catch(()=>null);if(d){const w=P(d,null);if(w?.state==="archived")continue;if(w){const z=new Date().toISOString(),T={...w,recall_count:(w.recall_count??0)+1,verified_at:z,last_recalled_at:z};l.set(a.key,JSON.stringify(T)).catch(()=>{});const N=Re(w)?Ee(w):0;if(N>0&&l.incrbyfloat(`cachly:stats:time_saved_mins:${b}`,N).catch(()=>{}),l.incr(`cachly:stats:recalls_total:${b}`).catch(()=>{}),w.author&&S&&w.author!==S){k++,l.incr(`cachly:stats:cross_author_recalls:${b}`).catch(()=>{}),l.sadd(`cachly:stats:reuse_pairs:${b}`,`${S}<-${w.author}`).catch(()=>{});const B=`person:${Z(S)}`,R=`person:${Z(w.author)}`;ne(l,B,"collaborates",R,!0).catch(()=>{}),ne(l,R,"collaborates",B,!0).catch(()=>{})}w.outcome==="success"&&m++,w.outcome==="success"&&(w.recall_count??0)>=1&&!ye(w)&&f.push({topic:a.key.replace("cachly:lesson:best:",""),severity:w.severity??"major",recall_count:(w.recall_count??0)+1,savedMins:N,ts:w.ts,author:w.author})}}}m>0&&l.set(`cachly:stats:first_recall_at:${b}`,new Date().toISOString(),"EX",365*86400,"NX").catch(()=>{});const _=await ae(`/api/v1/instances/${b}`).catch(()=>null),I=!_?.tier||_.tier.toLowerCase()==="free",h=[];if(!I&&_?.vector_token&&we())try{const a=await pe(t),d=`https://api.cachly.dev/v1/sem/${_.vector_token}`,w=await fetch(`${d}/search`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({embedding:a,namespace:"cachly:ctx",threshold:o,top_k:5}),signal:AbortSignal.timeout(8e3)});if(w.ok){const z=await w.json();for(const T of z.filter(N=>N.found&&N.id)){const N=T.id.replace("ctx:","").split(":"),B=N[0],R=N.slice(1).join(":"),E=`cachly:ctx:${B}:${R}`,j=await l.get(E).catch(()=>null);h.push({key:E,similarity:T.similarity??0,content:j??"(evicted)"})}}}catch{}const e=v.map(a=>a.score),D=e.length?Math.min(...e):0,O=e.length&&Math.max(...e)-D||1,F=a=>(a-D)/O,H=new Map;for(const a of v){const d=F(a.score);H.set(a.key,{key:a.key,content:a.content,bm25Score:d,hybridScore:d*(h.length>0?.7:1),matchedWords:a.matchedWords,matchType:"keyword",subQuery:a.subQuery})}for(const a of h){const d=H.get(a.key);d?(d.semScore=a.similarity,d.hybridScore=(d.bm25Score??0)*.6+a.similarity*.4,d.matchType="hybrid"):H.set(a.key,{key:a.key,content:a.content,semScore:a.similarity,hybridScore:a.similarity*.5,matchType:"semantic"})}try{const a=Se(t).filter(z=>z.length>=4).slice(0,4),d=new Set;for(const z of a){const T=l.scanStream({match:`cachly:ckg:node:*${z}*`,count:20});await new Promise((N,B)=>{T.on("data",R=>{for(const E of R.slice(0,5))d.add(E.replace("cachly:ckg:node:",""))}),T.on("end",N),T.on("error",B)})}const w=new Set;for(const z of[...d].slice(0,12)){const T=await l.smembers(`cachly:ckg:idx:to:${z}`);for(const N of T.slice(0,6)){const B=await l.get(N);if(!B)continue;const R=P(B,null);if(!R||R.edgeType!=="fixes"||R.confidence<.35||w.has(R.from))continue;w.add(R.from);const E=`cachly:lesson:best:${R.from}`,j=H.get(E);if(j)j.ckgScore=Math.max(j.ckgScore??0,R.confidence),j.hybridScore=j.hybridScore*.88+R.confidence*.12,j.matchType==="keyword"&&(j.matchType="hybrid");else{const Y=await l.get(E).catch(()=>null);if(!Y)continue;H.set(E,{key:E,content:Y,ckgScore:R.confidence,hybridScore:R.confidence*.3,matchType:"ckg"})}}}}catch{}if(p.length>0){const a=new Set(p.map(d=>d.replace(/\\/g,"/")));for(const d of H.values()){if(!d.key.startsWith("cachly:lesson:best:"))continue;const w=P(d.content,{});(Array.isArray(w.file_paths)?w.file_paths.filter(T=>typeof T=="string").map(T=>T.replace(/\\/g,"/")):[]).some(T=>a.has(T))&&(d.hybridScore*=1.15,d.contextBoost=!0)}}for(const a of H.values()){if(a.matchType!=="semantic"&&a.matchType!=="ckg")continue;const d=$t({key:a.key,content:a.content});d&&(a.hybridScore*=bt(d))}const W=[...H.values()].filter(a=>{if(!a.key.startsWith("cachly:lesson:best:"))return!0;const d=P(a.content,{});return!(d.visibility==="private"||!ft(d.group,$,s))}).sort((a,d)=>d.hybridScore-a.hybridScore),J=rt();let U="",Q=null;if(J!==null){U=ct();const a=W[0]?.hybridScore??0,d=lt(W.map(w=>({key:w.key,content:w.content,punktzahl:w.hybridScore})),U,J,a);d.weggelassenesThema!==void 0?W.splice(0,1):d.markierungGesetzt&&W[0]&&d.treffer[0]&&(W[0]={...W[0],content:d.treffer[0].content}),Q={frage:t,hoechstePunktzahl:a,themen:[],zulassungsfaehig:d.zulassungsfaehig,zulassungsGrund:d.zulassungsGrund,zuteilung:d.zuteilung,weggelassenesThema:d.weggelassenesThema,markierungGesetzt:d.markierungGesetzt}}const V=await Je(b,ae),se=V.reached,n=ze({ueberLimit:se,gesamt:W.length,volleTiefe:Ht}),y=n.sichtbar,i=[`\u{1F9E0} **Smart Recall** for: _"${t}"_
13
+ `];V.goodwillMessage&&i.push(`${V.goodwillMessage}
14
+ `);const u={critical:0,major:1,minor:2},r=f.sort((a,d)=>(u[a.severity]??1)-(u[d.severity]??1))[0];if(r){const a=N=>N>=60?`${N/60}h`:`${N}min`,d=(()=>{if(!r.ts)return null;const N=new Date(r.ts);if(isNaN(N.getTime()))return null;const B=new Date,R=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];return N.getFullYear()===B.getFullYear()?`${N.getDate()} ${R[N.getMonth()]}`:`${N.getDate()} ${R[N.getMonth()]} ${N.getFullYear()}`})(),w=r.severity==="critical"?120:r.severity==="major"?60:20,z=r.author&&r.author!==S?` \xB7 by @${r.author}`:"",T=d?` \xB7 learned ${d} (was a ${a(w)} debug)`:"";i.push(`> \u{1F4A1} **Brain saved you ~${a(r.savedMins)} here** \u2014 \`${r.topic}\`${T} \xB7 recalled ${r.recall_count}\xD7${z}
15
+ `)}k>0&&i.push(`> \u{1F465} **Team knowledge reuse** \u2014 ${k} of these lesson${k!==1?"s were":" was"} written by a teammate. This is the value only a shared brain delivers.
16
+ `);const C=W.filter(a=>a.contextBoost).length;C>0&&i.push(`> \u{1F4C1} **Personalized** \u2014 ${C} lesson${C!==1?"s":""} boosted because ${C!==1?"they match":"it matches"} your current file context (${p.slice(0,3).join(", ")}${p.length>3?", \u2026":""})
17
+ `);const L=yt(t);if(L.length>1&&i.push(`_Detected ${L.length} sub-topics:_ ${L.map((a,d)=>`${d+1}. "${a}"`).join(", ")}
18
+ `),W.length>0){const a=W.some(w=>w.ckgScore!==void 0),d=h.length>0&&a?"keyword + semantic + CKG hybrid":h.length>0?"keyword + semantic hybrid":a?"keyword + CKG hybrid":"keyword";if(i.push(`### \u{1F50D} Results (${W.length} \u2014 ${d})
19
+ `),L.length>1){const w=new Map,z=ze({ueberLimit:se,gesamt:W.length,volleTiefe:Jt});for(const B of W.slice(0,z.sichtbar)){const R=B.subQuery??t;w.has(R)||w.set(R,[]),w.get(R).push(B)}for(const[B,R]of w){i.push(`**Topic: "${B}"** (${R.length} results)
20
+ `);for(const E of R.slice(0,4)){const j=E.key.replace("cachly:ctx:","\u{1F4DD} ").replace("cachly:lesson:best:","\u{1F4A1} ").replace("cachly:idx:","\u{1F4C2} "),Y=E.matchType==="ckg"?`CKG: ${(E.ckgScore??0).toFixed(2)}, \u{1F578}\uFE0F causal graph`:E.ckgScore!==void 0&&E.bm25Score!==void 0?`BM25: ${E.bm25Score.toFixed(2)}, CKG: ${E.ckgScore.toFixed(2)}, \u{1F500} hybrid`:E.matchType==="hybrid"?`BM25: ${(E.bm25Score??0).toFixed(2)}, sem: ${((E.semScore??0)*100).toFixed(0)}%, \u{1F500} hybrid`:E.matchType==="semantic"?`sem: ${((E.semScore??0)*100).toFixed(0)}%, \u{1F3AF} semantic`:`BM25: ${(E.bm25Score??0).toFixed(2)}, matched: ${E.matchedWords?.join(", ")}`,X=Le(E.key,E.content,300);if(i.push(` **${j}** _(${Y})_`),E.key.startsWith("cachly:lesson:best:")){const oe=await l.get(E.key.replace("lesson:best:","lesson:human:")).catch(()=>null)??await l.get(E.key.replace("cachly:lesson:best:","lesson:human:")).catch(()=>null),re=oe?P(oe,{}):{};re.title&&i.push(` \u{1F4D6} **${re.title}** \u2014 ${re.summary??""}`)}for(const oe of X)i.push(` > ${oe}`);i.push("")}}const T=[...w.keys()],N=L.filter(B=>!T.includes(B));N.length>0&&i.push(`
21
+ \u26A0\uFE0F **No results for:** ${N.map(B=>`"${B}"`).join(", ")}`)}else for(const w of W.slice(0,y)){const z=w.key.replace("cachly:ctx:","\u{1F4DD} ").replace("cachly:lesson:best:","\u{1F4A1} ").replace("cachly:idx:","\u{1F4C2} ");let T="";if(w.key.startsWith("cachly:lesson:best:")){const E=P(w.content,{});E.author&&(T=` \xB7 \u{1F464} ${E.author}`)}const N=w.contextBoost?" \xB7 \u{1F4C1} context match":"",B=w.matchType==="hybrid"?`BM25: ${(w.bm25Score??0).toFixed(2)}, sem: ${((w.semScore??0)*100).toFixed(0)}%, \u{1F500} hybrid`:w.matchType==="semantic"?`sem: ${((w.semScore??0)*100).toFixed(0)}%, \u{1F3AF} semantic`:`BM25: ${(w.bm25Score??0).toFixed(2)}, matched: ${w.matchedWords?.join(", ")}`,R=Le(w.key,w.content,400);if(i.push(`**${z}**${T}${N} _(${B})_`),w.key.startsWith("cachly:lesson:best:")){const E=await l.get(w.key.replace("lesson:best:","lesson:human:")).catch(()=>null)??await l.get(w.key.replace("cachly:lesson:best:","lesson:human:")).catch(()=>null),j=E?P(E,{}):{};j.title&&i.push(`\u{1F4D6} **${j.title}** \u2014 ${j.summary??""}`)}for(const E of R)i.push(`> ${E}`);i.push("")}if(se&&n.zurueckgehalten>0){const w=n.zurueckgehalten;i.push(`\u{1F512} **Free recall limit reached** (${V.used}/${V.limit} this month). Showing your top match; ${w} further relevant lesson${w!==1?"s are":" is"} withheld. Your limit resets next month, or unlock your full Brain now \u2192 ${ps}
22
+ `)}}else{i.push(`\u26A0\uFE0F No matches found for: "${t}"`);const a=Se(t),d=[];if(De.size>0&&a.length>0)for(const w of a.slice(0,3)){if(w.length<4)continue;let z=3,T="";for(const N of De){if(N.length<3||Math.abs(N.length-w.length)>4)continue;const B=wt(w,N);B>0&&B<z&&(z=B,T=N)}T&&d.push(`"${T}" (instead of "${w}")`)}d.length>0?i.push(`\u{1F4A1} **Did you mean:** ${d.join(", ")}?`):(i.push(`
23
+ \u{1F4A1} Tips:`),i.push(" \u2022 Try different keywords"),i.push(" \u2022 Use `list_remembered` to see available context"),i.push(' \u2022 Use `recall_best_solution("topic")` for exact topic lookup'))}if(W.length>0){i.push(""),i.push("\u21A9\uFE0E Hat eine davon geholfen? `recall_feedback(query=\u2026, topic=\u2026, helped=true, rank=N)` \u2014 und wenn die richtige Lektion **nicht** dabei war: dieselbe Meldung mit `rank=0`. Das ist die einzige Zahl, die die Sortierung besser macht.");const a=W.filter(d=>d.key.startsWith("cachly:lesson:best:")).map(d=>d.key.slice(19));if(a.length>0)try{await l.setex(`cachly:letzter-recall:${b}`,7200,JSON.stringify({frage:t,themen:a}))}catch{}}if(J!==null&&Q){const a=W.slice(0,y).filter(d=>d.key.startsWith("cachly:lesson:best:")).map(d=>d.key.slice(19));return ut(l,U,{...Q,themen:a},i.join(`
24
+ `))}return i.join(`
25
+ `)}case"session_start":{const{instance_id:b,focus:t="",author:o="",provider:S="",workspace_path:A=""}=G,p=await q(b),l=[],$=p.scanStream({match:"cachly:lesson:best:*",count:200});await new Promise((n,y)=>{$.on("data",i=>l.push(...i)),$.on("end",n),$.on("error",y)});const s=[];if(l.length>0){const n=await p.mget(...l);for(const y of n){const i=P(y??null,null);i&&s.push(i)}}s.sort((n,y)=>new Date(y.ts).getTime()-new Date(n.ts).getTime());let M=0;const g=p.scanStream({match:"cachly:ctx:*",count:200});await new Promise((n,y)=>{g.on("data",i=>{M+=i.filter(u=>!u.endsWith(":meta")).length}),g.on("end",n),g.on("error",y)});const x=await p.get("cachly:session:last"),v=P(x,null),f=t.toLowerCase().split(/\s+/).filter(Boolean),c=f.length>0?s.filter(n=>f.some(y=>n.topic.toLowerCase().includes(y)||(n.tags??[]).some(i=>i.toLowerCase().includes(y)))):[];let k=0,m=0,_="";try{const n=await p.get("cachly:streak:current"),y=P(n,null),i=new Date().toISOString().slice(0,10);if(y){const u=y.last_date,r=new Date(Date.now()-864e5).toISOString().slice(0,10);u===i?(k=y.days,m=y.record):u===r?(k=y.days+1,m=Math.max(k,y.record),await p.set("cachly:streak:current",JSON.stringify({days:k,last_date:i,record:m}))):(k=1,m=y.record,await p.set("cachly:streak:current",JSON.stringify({days:1,last_date:i,record:m})))}else k=1,m=1,await p.set("cachly:streak:current",JSON.stringify({days:1,last_date:i,record:1}));k>=7?_=`\u{1F525} **${k}-day streak!** ${k===m?" New record!":`Best: ${m}d`}`:k>1&&(_=`\u{1F525} ${k}-day streak`)}catch{}await p.set("cachly:session:current",JSON.stringify({started:new Date().toISOString(),focus:t,provider:S}),"EX",86400);let I=0;try{const n=await p.get(`cachly:stats:time_saved_mins:${b}`);I=parseFloat(n??"0")}catch{}const e=[`\u{1F9E0} **Session Briefing**${S?` \xB7 ${S}`:""}`,""];if(_&&e.push(_,""),I>=30){const n=Math.floor(I/60),y=Math.round(I%60),i=n>0?`${n}h${y>0?` ${y}m`:""}`:`${y}m`,u=s.filter(C=>!ye(C)&&(C.recall_count??0)>0).length,r=u>0?` \u2014 ${u} of your own lessons that already helped, counted once each`:" (time not re-researching known fixes)";e.push(`\u23F1\uFE0F **Brain saved you ~${i} total**${r}`,"")}const D=await Je(b,ae),O=qt({used:D.used,limit:D.limit,savedMins:I});if(O&&e.push(O,""),D.notice&&!qe.has(b)&&(qe.add(b),e.push(D.notice,"")),v?.ts){const n=new Date(v.ts).getTime(),y=Math.floor((Date.now()-n)/864e5);if(y>=7&&!Number.isNaN(n)){const i=s.filter(u=>{const r=new Date(u.ts).getTime();return!Number.isNaN(r)&&r>n});if(e.push(`\u{1F44B} **Welcome back** \u2014 it's been ${y} days since your last session.`),i.length>0){const u=i.slice(0,3).map(r=>`\`${r.topic}\``).join(", ");e.push(` \u{1F331} Your Brain grew by **${i.length} lesson${i.length!==1?"s":""}** while you were gone (git, CI & teammates kept it learning): ${u}${i.length>3?", \u2026":""}`)}else e.push(` \u{1F9E0} ${s.length} lesson${s.length!==1?"s":""} held steady and ready \u2014 recall anything with \`smart_recall\`.`);e.push(" \u{1F4A1} Full breakdown: `npx @cachly-dev/mcp-server@latest digest`","")}}try{if(await p.get("cachly:mcp:version:last_seen")!==be){await p.set("cachly:mcp:version:last_seen",be,"EX",365*86400);const y=fs[be];if(y){e.push(...y,"");const i=ds();i&&e.push(`\u{1F4CA} ${i} MCP tools`,"")}}}catch{}const F=!v&&s.length===0&&M===0;if(F&&(e.push("\u{1F389} **Welcome! Your AI Brain is live.**",""),A?(e.push("\u{1F680} **Auto-bootstrapping from your git history...** (this takes a few seconds)"),e.push(" Your brain will learn from your recent commits \u2014 no setup needed.","")):(e.push("It learns from your work automatically. After your first session it will look like this:",""),e.push(" \u2705 `api:auth` \u2014 Bearer token in header, not cookie; 401 on missing scope"),e.push(" \u2705 `database:migrations` \u2014 always run migrations before deploy"),e.push(" \u26A0\uFE0F `docker:build` \u2014 ARG changes bust all subsequent cache layers"),e.push(""),e.push("**Tip:** Pass `workspace_path` to `session_start` to auto-learn from git history instantly."),e.push("")),e.push("**Your brain grows automatically:**"),e.push(' \u2022 End each session \u2192 `session_end(summary="What I did")` \u2014 auto-learns from git commits'),e.push(' \u2022 After fixing bugs \u2192 `learn_from_attempts(topic="...", outcome="success", what_worked="...")`'),e.push(""),e.push("\u{1F331} **Want answers right now?** Seed 16 universal engineering lessons so your first"),e.push(" `smart_recall` returns a hit instead of nothing:"),e.push(` \`brain_seed_starter(instance_id="${b}")\``),e.push(" _(Docker cache, JWT skew, K8s OOM, N+1 queries, cache stampede\u2026 \u2014 never overrides your own lessons.)_"),e.push(""),e.push("\u{1F4A1} Run `brain_doctor` for a health-check and personalised tips."),e.push("")),o&&!F)try{const n=`cachly:team:first_briefing:${o}`;if(!await p.get(n)){const i=s.filter(u=>u.author&&u.author!==o);if(i.length>0){await p.set(n,"1","EX",365*86400);const u=new Map;for(const r of i){const C=r.author;u.has(C)||u.set(C,[]),u.get(C).push(r)}e.push(""),e.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),e.push("\u{1F91D} **Your team's AI brain has been briefing you.**"),e.push(""),e.push("Your teammates have already solved problems you're about to hit:"),e.push("");for(const[r,C]of u){e.push(` \u{1F464} **${r}** fixed ${C.length} thing${C.length>1?"s":""}:`);for(const L of C.slice(0,2)){const a=L.outcome==="success"?"\u2705":"\u26A0\uFE0F";e.push(` ${a} \`${L.topic}\` \u2014 ${L.what_worked.slice(0,90)}`)}C.length>2&&e.push(` \u2026 and ${C.length-2} more lessons`)}e.push(""),e.push("\u{1F4A1} Use `team_learn` after your next fix to pay it forward."),e.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),e.push("")}}}catch{}const H=await p.get("cachly:session:handoff");if(H)try{const n=JSON.parse(H),y=Math.round((Date.now()-new Date(n.ts).getTime())/6e4),i=y<60?`${y}m ago`:y<1440?`${Math.round(y/60)}h ago`:`${Math.round(y/1440)}d ago`;if(e.push(`\u{1F91D} **Handoff from previous window** (${i}):`),n.context_summary&&e.push(` ${n.context_summary}`),n.remaining_tasks.length>0){e.push(" \u23F3 **Remaining tasks:**");for(const r of n.remaining_tasks)e.push(` - ${r}`)}n.completed_tasks.length>0&&e.push(` \u2705 **Already done:** ${n.completed_tasks.join(", ")}`);const u=(n.files_changed??[]).filter(r=>r.status==="broken"||r.status==="partial");u.length>0&&e.push(` \u26A0\uFE0F **Needs fix:** ${u.map(r=>`\`${r.path}\` (${r.status}${r.description?": "+r.description:""})`).join(", ")}`),n.blocked_on&&e.push(` \u{1F6AB} **Blocked on:** ${n.blocked_on}`),n.instructions&&e.push(` \u{1F4DD} **Instructions:** ${n.instructions}`),e.push("")}catch{}const W=await p.get("cachly:session:checkpoint");if(W)try{const n=JSON.parse(W),y=new Date(n.ts).getTime(),i=v?new Date(v.ts).getTime():0;if(y>i){const u=Math.round((Date.now()-y)/6e4),r=u<60?`${u}m ago`:u<1440?`${Math.round(u/60)}h ago`:`${Math.round(u/1440)}d ago`,C=n.provider?` [${n.provider}]`:"";e.push(`\u{1F4CC} **Last checkpoint**${C} (${r}): ${n.task}`),n.files_touched.length>0&&e.push(` Files: ${n.files_touched.slice(0,5).map(L=>`\`${L}\``).join(", ")}`),n.next_step&&e.push(` \u{1F4CD} Next step was: ${n.next_step}`),(!v||y-i>3e5)&&e.push(" \u26A0\uFE0F No `session_end` found \u2014 reconstructed from last checkpoint"),e.push("")}}catch{}if(A&&!v)try{const{execSync:n}=await import("node:child_process"),y=n(`git -C "${A}" log --oneline --format="%h %s" -15 2>/dev/null`,{encoding:"utf-8",timeout:5e3}).trim(),i=n(`git -C "${A}" diff --stat HEAD~3 2>/dev/null || git -C "${A}" diff --stat 2>/dev/null`,{encoding:"utf-8",timeout:5e3}).trim();if(y){e.push("\u{1F50D} **Git reconstruction** (no session_end found \u2014 reconstructed from git):");for(const u of y.split(`
26
+ `).slice(0,8))e.push(` ${u}`);if(i){const u=i.split(`
27
+ `).filter(r=>r.includes("|")||r.includes("changed"));if(u.length>0){e.push(" **Recent changes:**");for(const r of u.slice(0,5))e.push(` ${r.trim()}`)}}e.push("")}}catch{}if(v){const n=Math.round((Date.now()-new Date(v.ts).getTime())/6e4),y=n<60?`${n}m ago`:n<1440?`${Math.round(n/60)}h ago`:`${Math.round(n/1440)}d ago`;e.push(`\u{1F4C5} **Last session** (${y}): ${v.summary}`),v.duration_min&&e.push(` Duration: ${v.duration_min} min`),(v.files_changed??[]).length>0&&e.push(` Files: ${(v.files_changed??[]).slice(0,5).map(i=>`\`${i}\``).join(", ")}`),e.push("")}e.push(`\u{1F4CA} **Brain:** ${s.length} lessons \xB7 ${M} context entries`,"");{const y=s.filter(i=>!ye(i)).filter(i=>i.outcome==="success"&&(i.pinned===!0||(i.recall_count??0)>=5)).sort((i,u)=>(u.recall_count??0)-(i.recall_count??0)).slice(0,5);if(y.length>0){e.push("\u{1F4CC} **Proven Laws** (auto-crystallized \u2014 your most-trusted knowledge):");for(const i of y){const u=i.severity==="critical"?"\u{1F534}":i.severity==="major"?"\u{1F7E1}":"",r=i.recall_count??0,C=i.pinned===!0?"pinned":`recalled ${r}\xD7`,L=me(i);e.push(` \u{1F3C6}${u} \`${i.topic}\` _(${C})_ \u2014 ${L[0]??""}`,...L.slice(1))}e.push("")}}if(s.length>=3){const n=new Map;for(const u of s){const r=u.topic.split(":")[0]??"other";n.has(r)||n.set(r,{total:0,success:0,critical:0});const C=n.get(r);C.total++,u.outcome==="success"&&C.success++,u.severity==="critical"&&C.critical++}const y=[...n.entries()].sort((u,r)=>r[1].total-u[1].total).slice(0,6),i=y.some(([,u])=>u.success<u.total*.4&&u.total>=2);if(y.length>0){e.push("\u{1F5FA}\uFE0F **Knowledge Coverage:**");for(const[u,r]of y){const C=Math.round(r.success/r.total*100),L=Math.round(C/10),a="\u2588".repeat(L)+"\u2591".repeat(10-L),d=r.critical>0?" \u{1F534}":C<40&&r.total>=2?" \u26A0\uFE0F":"";e.push(` ${a} ${u.padEnd(18)} ${String(C).padStart(3)}% (${r.success}/${r.total} confirmed)${d}`)}i&&e.push(" \u26A0\uFE0F _Some domains have contested beliefs \u2014 use `ckg_inspect` to review_"),e.push("")}}try{const n=[],y=p.scanStream({match:"cachly:ckg:conflict:*",count:50});if(await new Promise((i,u)=>{y.on("data",r=>n.push(...r)),y.on("end",i),y.on("error",u)}),n.length>0){e.push(`\u26A1 **Active belief conflicts (${n.length}):**`);for(const i of n.slice(0,3)){const u=await p.get(i);if(!u)continue;const r=P(u,null);r&&e.push(` \u26A0\uFE0F \`${r.topic}\` \u2014 previously confirmed fix (${(r.fix_confidence*100).toFixed(0)}%, n=${r.fix_trials}) now contradicted. Use \`ckg_inspect(concept="${Z(r.topic)}")\``)}e.push("")}}catch{}if(t&&t.length>3)try{const n=t.toLowerCase().replace(/[^a-z0-9\s:_-]/g," ").split(/\s+/).filter(i=>i.length>3),y=[];for(const i of n.slice(0,6))if(!await p.exists(`cachly:ckg:node:${i}`)){const r=[],C=p.scanStream({match:`cachly:ckg:node:${i}*`,count:10});await new Promise((L,a)=>{C.on("data",d=>r.push(...d)),C.on("end",L),C.on("error",a)}),r.length===0&&y.push(i)}if(y.length>0){e.push("\u{1F52D} **Blind spots detected for this focus:**");for(const i of y.slice(0,3))e.push(` \u2B1C \`${i}\` \u2014 no CKG knowledge. Suggestions:`),e.push(` \u2022 \`brain_from_git(instance_id="...", concept="${i}")\` \u2014 bootstrap from commits`),e.push(` \u2022 \`fedbrain_search(query="${i}")\` \u2014 search global commons`);e.push("")}}catch{}if(c.length>0){e.push(`\u{1F3AF} **Relevant for "${t}":**`);for(const n of c.slice(0,4)){const y=n.outcome==="success"?"\u2705":n.outcome==="partial"?"\u26A0\uFE0F":"\u274C",i=n.severity==="critical"?"\u{1F534}":n.severity==="major"?"\u{1F7E1}":"",u=me(n);e.push(` ${y}${i} \`${n.topic}\` \u2014 ${u[0]??""}`,...u.slice(1))}e.push("")}if(s.length>0){const y=[...s].filter(u=>!ye(u)).sort((u,r)=>(r.recall_count??0)-(u.recall_count??0)).filter(u=>(u.recall_count??0)>0).slice(0,3),i=(c.length>0?s.filter(u=>!c.includes(u)):s).filter(u=>!y.includes(u)).slice(0,c.length>0?3:4);if(y.length>0){e.push(`\u{1F3C6} **Most valuable** (recalled ${y[0].recall_count}\xD7 before):`);for(const u of y){const r=u.outcome==="success"?"\u2705":u.outcome==="partial"?"\u26A0\uFE0F":"\u274C",C=u.severity==="critical"?"\u{1F534}":u.severity==="major"?"\u{1F7E1}":"",L=u.recall_count??0,a=u.authors??(u.author?[u.author]:[]),d=new Set(a.filter(Boolean)).size,w=d>=2&&L>=5||L>=10?" \u{1F3C6}":d>=2||L>=5?" \u2705":"",z=L>1?` _(${L}\xD7 recalled)_`:"",T=me(u);e.push(` ${r}${C}${w} \`${u.topic}\`${z} \u2014 ${T[0]??""}`,...T.slice(1))}e.push("")}if(i.length>0){const u=y.length>0?"\u{1F550} **Recent:**":"\u{1F550} **Recent lessons:**";e.push(u);for(const r of i){const C=r.outcome==="success"?"\u2705":r.outcome==="partial"?"\u26A0\uFE0F":"\u274C",L=r.severity==="critical"?"\u{1F534}":r.severity==="major"?"\u{1F7E1}":"",a=me(r);e.push(` ${C}${L} \`${r.topic}\` \u2014 ${a[0]??""}`,...a.slice(1))}e.push("")}}else e.push("\u{1F4ED} No lessons yet. Use `learn_from_attempts` after solving tasks.","");s.length>=10&&(s.some(y=>y.author)||(e.push(`\u{1F91D} **Your Brain has ${s.length} lessons \u2014 your team could benefit instantly.**`),e.push(" Share: **cachly.dev/team** \xB7 or run `team_learn` after the next fix.","")));const J=s.filter(n=>n.outcome==="failure"||n.outcome==="partial");if(J.length>0){e.push(`\u26A0\uFE0F **Unresolved** (${J.length} topic${J.length>1?"s":""} with no success yet):`);for(const n of J.slice(0,3))e.push(` \u274C \`${n.topic}\` \u2014 ${(n.what_failed??n.what_worked).slice(0,80)}`);e.push("")}const U=s.filter(n=>n.outcome!=="success"&&n.outcome!=="partial"?!1:ge(n)<Ae);if(U.length>0){e.push(`\u{1F534} **Stale lessons** (not recalled in >${Xe}d \u2014 verify before applying):`);for(const n of U.slice(0,4)){const y=ge(n),i=Math.round((Date.now()-new Date(n.verified_at??n.ts).getTime())/864e5),u=y<Ve?"\u{1F534}":"\u26A0\uFE0F";e.push(` ${u} \`${n.topic}\` \u2014 ${i}d stale, ${(y*100).toFixed(0)}% confidence`)}e.push(" _Run `recall_best_solution` on these to reset their confidence clock._"),e.push("")}const Q=v;if(Q?.decision_log?.length){const n=Q.decision_log,y=n.filter(u=>u.outcome==="success"),i=n.filter(u=>u.outcome==="failure");e.push(`\u{1F3AC} **Last session decisions** (${n.length} lessons stored):`),y.length>0&&e.push(` \u2705 Worked: ${y.slice(0,3).map(u=>`\`${u.topic}\``).join(", ")}`),i.length>0&&e.push(` \u274C Failed: ${i.slice(0,3).map(u=>`\`${u.topic}\``).join(", ")}`),e.push("")}{const n=new Set(["src","lib","test","tests","dist","index","main","app","internal","pkg","cmd","node_modules","components","utils","ts","tsx","js","jsx","go","py","rs","java","json","yaml","yml"]),y=C=>{const L=new Set;for(const a of C)for(const d of a.toLowerCase().split(/[/\\._-]/))d.length>3&&!n.has(d)&&L.add(d);return[...L]};let i=f,u=t,r=!1;if(i.length===0&&v){const C=y(v.files_changed??[]),L=(v.summary??"").toLowerCase().replace(/[^a-z0-9\s:_-]/g," ").split(/\s+/).filter(a=>a.length>3&&!n.has(a));i=[...new Set([...C,...L])],u="where you left off last session",r=!0}if(i.length>0){const C=s.filter(L=>{if(L.outcome==="success")return!1;const a=L.topic.split(":")[0];return i.some(d=>L.topic.toLowerCase().includes(d)||a===d||(L.tags??[]).some(w=>w.toLowerCase()===d))});if(C.length>=1){const a=["\u{1F6A8} **PRE-WARNING** \u2014 Read this BEFORE starting:",r?` You're likely to continue **${u}** \u2014 ${C.length} known pitfall${C.length>1?"s":""} there:`:` Known pitfalls for **"${u}"** (${C.length} past failure${C.length>1?"s":""}):`,...C.slice(0,3).map(d=>` \u274C \`${d.topic}\` \u2014 ${(d.what_failed??d.what_worked).slice(0,80)}`),""];e.splice(2,0,...a)}}}if(o){const n=Date.now()-6048e5,y=s.filter(i=>i.author&&i.author!==o&&new Date(i.ts).getTime()>n);if(y.length>0){const i=new Map;for(const u of y){const r=u.author;i.has(r)||i.set(r,[]),i.get(r).push(u)}e.push(`\u{1F465} **Team this week** (${y.length} lesson${y.length>1?"s":""} from teammates):`);for(const[u,r]of i){e.push(` \u{1F464} **${u}**:`);for(const C of r.slice(0,3)){const L=C.outcome==="success"?"\u2705":C.outcome==="partial"?"\u26A0\uFE0F":"\u274C";e.push(` ${L} \`${C.topic}\` \u2014 ${C.what_worked.slice(0,80)}`)}r.length>3&&e.push(` \u2026 and ${r.length-3} more`)}e.push("")}}try{const n=await p.get("cachly:crystal:latest");if(n){const y=P(n,null);if(y&&Math.round((Date.now()-new Date(y.ts).getTime())/864e5)<=90){e.push(`\u{1F48E} **Memory Crystal** (${y.label} \xB7 ${y.session_count} sessions compressed):`);for(const u of y.top_patterns.slice(0,3))e.push(` \u2022 **${u.category}** (${u.count}\xD7): ${u.insight.slice(0,90)}`);e.push("")}}}catch{}try{const n=await p.hgetall(`cachly:roadmap:${b}`);if(n&&Object.keys(n).length>0){const y={critical:0,high:1,medium:2,low:3},i={critical:"\u{1F534}",high:"\u{1F7E0}",medium:"\u{1F7E1}",low:"\u{1F535}"},u=new Set(["planned","in-progress","blocked"]),r=Object.values(n).flatMap(a=>{const d=P(a,null);return d?[d]:[]}),C=r.filter(a=>u.has(a.status)).sort((a,d)=>a.status==="in-progress"&&d.status!=="in-progress"?-1:d.status==="in-progress"&&a.status!=="in-progress"?1:(y[a.priority]??99)-(y[d.priority]??99)),L=r.filter(a=>a.status==="done").length;if(C.length>0){e.push(`\u{1F5FA}\uFE0F **Roadmap** (${C.length} open \xB7 ${L} done):`);for(const a of C.slice(0,5)){const d=a.status==="in-progress"?"\u26A1":a.status==="blocked"?"\u{1F6AB}":"\u{1F4CB}";e.push(` ${d} ${i[a.priority]??"\u26AA"} \`${a.id}\` **${a.title}**`)}C.length>5&&e.push(` \u2026 and ${C.length-5} more`),e.push(" _Use `roadmap_next` for the top priority item \xB7 `roadmap_list` for full view_"),e.push("")}}}catch{}if(t&&t.length>3)try{const n=t.toLowerCase().replace(/[^a-z0-9\s\-_:]/g," ").split(/\s+/).filter(r=>r.length>2).slice(0,5),y=[];for(const r of n){const C=[],L=p.scanStream({match:`cachly:ckg:node:*${r}*`,count:20});await new Promise((a,d)=>{L.on("data",w=>C.push(...w)),L.on("end",a),L.on("error",d)});for(const a of C.slice(0,3)){const d=await p.get(a);if(!d)continue;const w=P(d,null);if(!w)continue;const z=await p.smembers(`cachly:ckg:idx:from:${w.id}`);for(const T of z.slice(0,15)){const N=await p.get(T);if(!N)continue;const B=P(N,null);if(!B||B.edgeType!=="causes"&&B.edgeType!=="co-occurs"&&B.edgeType!=="fixes"||B.confidence<.35)continue;const R=B.edgeType==="fixes"?`cachly:lesson:best:${B.from}`:`cachly:lesson:best:${B.to}`,E=await p.get(R),j=P(E,null);y.push({concept:w.id,edgeType:B.edgeType,target:B.to,confidence:B.confidence,lesson:j??void 0})}}}const i=new Set,u=y.filter(r=>{const C=`${r.concept}:${r.target}`;return i.has(C)?!1:(i.add(C),!0)}).sort((r,C)=>C.confidence-r.confidence).slice(0,4);if(u.length>0){e.push(`\u{1F52E} **Predicted risks for "${t}"** (PPE pre-fetch):`);for(const r of u){const C=Math.round(r.confidence*100),L=r.edgeType==="causes"?"\u26A1":r.edgeType==="fixes"?"\u{1F527}":"\u{1F504}";e.push(` ${L} **${C}%** \`${r.concept}\` ${r.edgeType} \`${r.target}\``),r.lesson?.what_worked&&e.push(` \u2705 ${r.lesson.what_worked.slice(0,110)}`)}e.push(` _Full analysis: \`brain_predict(context="${t}")\`_`),e.push("")}}catch{}try{const n=await p.get("cachly:mcm:calibration:last"),y=P(n,null)?.ts??0;if((Date.now()-y)/864e5>=30&&s.length>=5){const u=s.filter(C=>C.outcome==="success"&&(C.recall_count??0)>0),r=s.filter(C=>(C.recall_count??0)>0);if(r.length>=3){const C=r.length>0?u.length/r.length:1,L=[],a=new Map;for(const d of r){const w=d.topic.split(":")[0]??"other";a.has(w)||a.set(w,{success:0,total:0});const z=a.get(w);z.total++,d.outcome==="success"&&z.success++}for(const[d,w]of a)w.total>=2&&w.success/w.total<.6&&L.push(d);await p.set("cachly:mcm:calibration:last",JSON.stringify({ts:Date.now(),precision:C,recalled:r.length,low_precision_domains:L})),(L.length>0||C<.7)&&(e.push(`\u{1F4D0} **MCM Calibration** (30-day pass \u2014 ${r.length} recalled lessons):`),e.push(` Overall precision: **${(C*100).toFixed(0)}%** recalled lessons actually worked`),L.length>0&&e.push(` \u26A0\uFE0F Low-precision domains: ${L.map(d=>`\`${d}\``).join(", ")} \u2014 consider revisiting these lessons`),e.push(" \u{1F4A1} Use `learn_from_attempts` to update stale lessons and improve accuracy."),e.push(""))}await p.set("cachly:mcm:calibration:last",JSON.stringify({ts:Date.now(),precision:1,recalled:0}),"EX",35*86400)}}catch{}try{const n=await ae("/api/v1/syndication/stats");n.total_lessons>0&&(e.push(`\u{1F30D} **Commons:** ${n.total_lessons.toLocaleString()} lessons \xB7 ${n.total_confirms.toLocaleString()} confirms \xB7 +${n.added_last_7_days} this week`),e.push(""))}catch{}const V=s.filter(n=>n.outcome==="failure"||n.outcome==="partial").length>0,se=s.some(n=>n.outcome==="success"&&ge(n)<Ae);if(s.length===0||V||se){const n=[];s.length===0&&n.push("brain is empty"),V&&n.push("open failures"),se&&n.push("stale lessons"),e.push(`\u{1FA7A} _Run \`brain_doctor\` to fix: ${n.join(", ")}._`)}try{if(await p.get(`cachly:prefs:auto_changelog:${b}`).catch(()=>null)!=="false"&&v?.ts&&!F){const i=new Date(v.ts).getTime();if(!Number.isNaN(i)){const u=s.filter(r=>{const C=new Date(r.ts).getTime();return!Number.isNaN(C)&&C>i});if(u.length>0){const r=Math.round((Date.now()-i)/6e4),C=r<60?`${r}m`:r<1440?`${Math.round(r/60)}h`:`${Math.round(r/1440)}d`;e.push(""),e.push(`\u{1F4CB} **${u.length} new lesson${u.length!==1?"s":""} since your last session** (${C} ago):`);const L=new Map;for(const a of u.slice(0,20)){const d=a.topic.includes(":")?a.topic.split(":")[0]:"general",w=L.get(d)??[];w.push(a),L.set(d,w)}for(const[a,d]of L){e.push(` **${a}**`);for(const w of d){const z=w.outcome==="failure"?"\u274C":w.severity==="critical"?"\u{1F6A8}":w.severity==="major"?"\u26A0\uFE0F":"\u2705",T=(w.outcome==="failure"?w.what_failed:w.what_worked)??"";e.push(` ${z} \`${w.topic}\`${T?` \u2014 ${T.slice(0,80)}`:""}`)}}u.length>20&&e.push(` \u2026 and ${u.length-20} more. Call \`brain_changelog\` for the full list.`),e.push(` _Disable auto-changelog: \`brain_set_pref(instance_id="${b}", key="auto_changelog", value="false")\`_`)}}}}catch{}return e.join(`
28
+ `)}case"session_start_summary":{const{instance_id:b,focus:t,top_n:o=10,author:S=""}=G;if(!t||!t.trim())throw new Error("focus is required for session_start_summary");const A=Math.min(Math.max(1,Math.floor(Number(o)||10)),25),p=await q(b),l=[],$=p.scanStream({match:"cachly:lesson:best:*",count:200});await new Promise((e,D)=>{$.on("data",O=>l.push(...O)),$.on("end",e),$.on("error",D)});const s=[];if(l.length>0){const e=await p.mget(...l);for(const D of e){const O=P(D??null,null);O&&s.push(O)}}if(s.length===0)return["\u{1F9E0} Brain Summary \u2014 no lessons yet","","Your brain is empty. Use `learn_from_attempts` after solving tasks to build it up.",`\u{1F4A1} Tip: \`brain_seed_starter(instance_id="${b}")\` to bootstrap with 16 universal lessons.`].join(`
29
+ `);const M=s.length>A,g=t.toLowerCase().split(/\s+/).filter(Boolean),x=Date.now(),v=s.map(e=>{const D=g.filter(n=>e.topic.toLowerCase().includes(n)).length,O=g.filter(n=>(e.tags??[]).some(y=>y.toLowerCase().includes(n))).length,F=D*2+O,H=Math.max(0,e.recall_count??0),W=Math.min(1.5,Math.log1p(H)/10),J=e.severity==="critical"?1:e.severity==="major"?.5:0,U=(x-new Date(e.verified_at??e.ts).getTime())/864e5,Q=U<30?.5*(1-U/30):0,V=e.outcome==="success"?.5:e.outcome==="partial"?.2:0,se=F+W+J+Q+V;return{lesson:e,score:se}});v.sort((e,D)=>D.score!==e.score?D.score-e.score:new Date(D.lesson.ts).getTime()-new Date(e.lesson.ts).getTime());const f=(M?v.slice(0,A):v).map(e=>e.lesson),c=s.length,k=f.length,I=[M?`\u{1F9E0} Brain Summary \u2014 top ${k} of ${c} lessons for "${t}"`:`\u{1F9E0} Brain Summary \u2014 all ${k} lesson${k!==1?"s":""} for "${t}"`,"\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",""];for(const e of f){const D=e.severity==="critical"?"\u{1F534}":e.severity==="major"?"\u26A0\uFE0F":"\u2705",O=e.severity?`[${e.severity}] `:"[minor] ",F=e.recall_count??0,H=F>0?` (\u2713 recalled ${F}\xD7)`:"",W=e.what_worked.slice(0,110),J=ls(e.author,S);I.push(`${D} ${O}\`${e.topic}\` \u2014 ${W}${H}${J}`)}I.push("");const h=us(f,S);return h>0&&I.push(`\u{1F465} ${h} of these ${h===1?"was":"were"} written by a teammate \u2014 the value only a shared brain delivers.`,""),M?I.push(`\u{1F4A1} Showing top ${k} of ${c} lessons \xB7 \`session_start\` for full briefing`):I.push(`\u{1F4A1} All ${k} lesson${k!==1?"s":""} shown \xB7 \`session_start\` for full briefing`),I.join(`
30
+ `)}case"session_end":{const{instance_id:b,summary:t,files_changed:o=[],lessons_learned:S,workspace_path:A=""}=G,p=await q(b),l=new Date;let $;const s=await p.get("cachly:session:current");if(!s)return"\u26A0\uFE0F No active session found.\n\nRun `session_start` first to begin tracking a session, then call `session_end` when you're done.";const M=P(s,null);M?.started&&($=Math.round((l.getTime()-new Date(M.started).getTime())/6e4));let g=[];try{g=(await p.lrange("cachly:session:decision-log",0,-1)).flatMap(D=>{const O=P(D,null);return O?[O]:[]}),await p.del("cachly:session:decision-log")}catch{}const x={ts:l.toISOString(),summary:t,files_changed:o,...S!==void 0?{lessons_learned:S}:{},...$!==void 0?{duration_min:$}:{},...g.length>0?{decision_log:g}:{}};await p.set("cachly:session:last",JSON.stringify(x)),await p.lpush("cachly:session:history",JSON.stringify(x)),await p.ltrim("cachly:session:history",0,49),await p.expire("cachly:session:history",90*86400),await p.del("cachly:session:current");const v=[];try{const e=/\b(fixed|deployed|added|removed|refactored|migrated|updated|resolved|implemented|improved|optimized|configured|created|deleted|disabled|enabled|discovered|found|learned|debugged|patched|upgraded|installed|tested|built|rewrote|moved|renamed|split|merged|extracted)\b/i,D=t.split(/[.!\n]+/).map(O=>O.trim()).filter(O=>O.length>20&&e.test(O));for(const O of D.slice(0,6)){const H=O.toLowerCase().replace(/[^a-z0-9\s]/g," ").split(/\s+/).filter(V=>V.length>3&&!["that","this","with","from","have","been","were","they","then","when","also","into","will","would","could","should"].includes(V)).slice(0,4).join("-");if(!H)continue;const W=`auto:${H}`,J=`cachly:lesson:best:${W}`,U=await p.get(J);if(U)try{if(JSON.parse(U).outcome==="success")continue}catch{}const Q={topic:W,outcome:"success",what_worked:O,context:`Auto-learned from session summary. Full summary: ${t.slice(0,300)}`,severity:"minor",ts:l.toISOString(),recall_count:0,auto_learned:!0,session_ts:l.toISOString(),version:2};await p.set(J,JSON.stringify(Q)),await p.expire(J,90*86400),v.push(W)}}catch{}const f=[];if(A)try{const e=s?(()=>{try{return JSON.parse(s).started??""}catch{return""}})():"",D=e?`--since="${e}"`:'--since="1 hour ago"',O=Me(`git -C "${A}" log ${D} --oneline --format="%H|||%s|||%b|||%ai|||ENDE"`,{timeout:5e3,encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim();if(O){const F=/\b(fix|add|remove|refactor|migrate|update|resolve|implement|improve|optimize|configure|create|delete|disable|enable|debug|patch|upgrade|build|rewrite|deploy|feat|chore|docs|test|perf|ci)\b/i,H=O.split("|||ENDE").map(W=>W.trim()).filter(Boolean);for(const W of H.slice(0,10)){const[J,U,Q,V]=W.split("|||");if(J)try{const C=Me(`git -C "${A}" diff-tree --no-commit-id --name-only -r --root "${J}"`,{timeout:5e3,encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim(),a=C?C.split(`
31
+ `).map(w=>w.trim()).filter(Boolean).slice(0,12):[],d=[];for(const w of a)d.push(await Ie(p,w));for(let w=0;w<d.length;w++)for(let z=w+1;z<d.length;z++)await ne(p,d[w],"co-occurs",d[z],!0),await ne(p,d[z],"co-occurs",d[w],!0)}catch{}if(!U||!F.test(U))continue;const se=(Q??"").trim();if(se.length<120)continue;const n=U.toLowerCase().replace(/^(fix|feat|chore|docs|test|ci|perf|refactor|build|revert)[:(\s]/i,"").replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,40);if(!n)continue;const y=`git:${n}`,i=`cachly:lesson:best:${y}`;if(await p.get(i))continue;const r={topic:y,outcome:"success",what_worked:`${U}
32
+
33
+ ${se.slice(0,1200)}`,context:`Auto-learned from git commit ${(J??"").slice(0,7)} at ${V??""} in ${A}`,severity:"minor",ts:l.toISOString(),recall_count:0,auto_learned:!0,source:"ambient-git",version:3};await p.set(i,JSON.stringify(r)),await p.expire(i,60*86400),f.push(y)}}}catch{}const c=$!==void 0?` \xB7 ${$} min`:"",k=v.length+f.length,m=[];$!==void 0&&$>0&&m.push(`\u23F1 ${$} min session`),k>0&&m.push(`\u{1F9E0} ${k} lessons saved to Brain`),o.length>0&&m.push(`\u{1F4C1} ${o.length} file${o.length>1?"s":""} changed`);const _=m.length>0?`${m.join(" \xB7 ")}
34
+
35
+ My AI Brain remembers this so I never repeat it. @cachlydev
36
+ cachly.dev`:`Session saved to my AI Brain. No more re-explaining this tomorrow. @cachlydev
37
+ cachly.dev`,I=`https://twitter.com/intent/tweet?text=${encodeURIComponent(_)}`,h=["","\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510","\u2502 \u{1F9E0} Session Summary Card \u2502","\u2502 Share your progress \u2014 cached forever \u2502","\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524",$!==void 0?`\u2502 \u23F1 Duration : ${($+" min").padEnd(26)}\u2502`:"",k>0?`\u2502 \u{1F4DA} Learned : ${(k+" lessons").padEnd(26)}\u2502`:"",o.length>0?`\u2502 \u{1F4C1} Changed : ${(o.length+" file"+(o.length>1?"s":"")).padEnd(26)}\u2502`:"","\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524",`\u2502 \u{1F4E3} Share: ${I.slice(0,34).padEnd(34)}\u2502`,"\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"].filter(e=>e!=="").join(`
38
+ `);return[`\u2705 **Session saved**${c}`,"",`\u{1F4CB} **Summary:** ${t}`,o.length>0?`\u{1F4C1} **Files changed:** ${o.map(e=>`\`${e}\``).join(", ")}`:"",S!==void 0?`\u{1F9E0} **Lessons stored:** ${S}`:"",v.length>0?`\u{1F916} **Auto-learned:** ${v.length} lessons extracted from summary (${v.slice(0,3).map(e=>`\`${e}\``).join(", ")}${v.length>3?"\u2026":""})`:"",f.length>0?`\u{1F33F} **Ambient git learning:** ${f.length} commit${f.length>1?"s":""} auto-learned (${f.slice(0,3).map(e=>`\`${e}\``).join(", ")}${f.length>3?"\u2026":""})`:"",h,"",'\u{1F4A1} Next session: `session_start(focus="...")` to see this summary.'].filter(e=>e!=="").join(`
39
+ `)}case"session_ping":{const{instance_id:b,task:t,files_touched:o=[],next_step:S="",provider:A=""}=G,p=await q(b),l={ts:new Date().toISOString(),task:t,files_touched:o,next_step:S,provider:A};await p.set("cachly:session:checkpoint",JSON.stringify(l),"EX",86400*3),await p.lpush("cachly:session:checkpoint:log",JSON.stringify(l)),await p.ltrim("cachly:session:checkpoint:log",0,19);const $=A?` [${A}]`:"",s=o.length>0?` \xB7 ${o.length} file${o.length>1?"s":""} touched`:"",M=S?`
40
+ \u{1F4CD} **Next step:** ${S}`:"";return[`\u{1F4CC} **Checkpoint saved**${$} \u2014 ${new Date().toLocaleTimeString()}`,`\u{1F528} **Working on:** ${t}${s}`,M,"","\u{1F4A1} If you switch providers, `session_start` will show this checkpoint automatically."].filter(g=>g!=="").join(`
41
+ `)}case"session_handoff":{const{instance_id:b,completed_tasks:t=[],remaining_tasks:o=[],files_changed:S=[],instructions:A="",context_summary:p="",blocked_on:l=""}=G,$=await q(b),M={ts:new Date().toISOString(),completed_tasks:t,remaining_tasks:o,files_changed:S,instructions:A,context_summary:p,blocked_on:l};await $.set("cachly:session:handoff",JSON.stringify(M)),await $.lpush("cachly:session:handoff:history",JSON.stringify(M)),await $.ltrim("cachly:session:handoff:history",0,19);const g=t.length+o.length,x=g>0?Math.round(t.length/g*100):0,v=S.filter(f=>f.status==="broken"||f.status==="partial");return[`\u{1F91D} **Handoff saved** \u2014 ${t.length}/${g} tasks done (${x}%)`,"",t.length>0?`\u2705 **Completed:**
42
+ ${t.map(f=>` - ${f}`).join(`
43
+ `)}`:"",o.length>0?`
44
+ \u23F3 **Remaining for next window:**
45
+ ${o.map(f=>` - ${f}`).join(`
46
+ `)}`:"",v.length>0?`
47
+ \u26A0\uFE0F **Needs attention:** ${v.map(f=>`\`${f.path}\` (${f.status})`).join(", ")}`:"",l?`
48
+ \u{1F6AB} **Blocked on:** ${l}`:"",A?`
49
+ \u{1F4DD} **Instructions:** ${A}`:"","","\u{1F4A1} The next `session_start` will include this handoff automatically."].filter(f=>f!=="").join(`
50
+ `)}case"auto_learn_session":{const{instance_id:b,observations:t}=G,o=await q(b),S=[],A=[];for(const l of t){const $=l.topic??l.action.toLowerCase().replace(/[^a-z0-9:\-_\s]/g,"").trim().split(/\s+/).slice(0,4).join("-"),s=$.includes(":")?$:`auto:${$}`,M=`cachly:lesson:best:${s}`,g=await o.get(M);if(g&&JSON.parse(g).outcome==="success"&&l.outcome!=="success"){A.push(s);continue}const x={topic:s,outcome:l.outcome,what_worked:l.outcome==="success"?l.action:l.details??l.action,what_failed:l.outcome==="failure"?l.action:void 0,context:l.details,severity:l.severity??"minor",ts:new Date().toISOString(),confidence:1,recall_count:0,auto_learned:!0,version:2},v=JSON.stringify(x);await o.set(M,v);const f=`cachly:lessons:${s}`;await o.rpush(f,v),await o.ltrim(f,-100,-1),await o.expire(f,90*86400);try{const c=s.split(":")[0]??"auto",k=Z(s);if(await de(o,k,c,c),l.outcome==="success"||l.outcome==="partial"){const m=l.details?$e(l.details):null;if(m){const _=Z(`problem:${m}`);await de(o,_,"problem","problem"),await ne(o,k,"fixes",_,l.outcome==="success",l.outcome==="partial")}}}catch{}S.push(`${l.outcome==="success"?"\u2705":l.outcome==="partial"?"\u26A0\uFE0F":"\u274C"} \`${s}\``)}const p=[`\u{1F916} **Auto-learn complete**: ${S.length} stored, ${A.length} skipped`,""];return S.length>0&&p.push("**Stored:**",...S.map(l=>" "+l),""),A.length>0&&p.push(`**Skipped** (better lesson already exists): ${A.map(l=>`\`${l}\``).join(", ")}`),p.join(`
51
+ `)}case"brain_who_knows":{const{instance_id:b,topic:t,limit:o=10}=G;if(!t||typeof t!="string"||!t.trim())return'\u26A0\uFE0F `brain_who_knows` requires a non-empty `topic`. Example: `brain_who_knows(topic="kubernetes deployment")`.';const S=Math.max(1,Math.min(Number(o)||10,50)),A=await q(b),p=Z(t),l=Se(t).filter(f=>f.length>=3).slice(0,5),$=new Set([p]);for(const f of l.slice(0,3)){const c=A.scanStream({match:`cachly:ckg:node:*${f}*`,count:30});await new Promise((k,m)=>{c.on("data",_=>{for(const I of _.slice(0,10)){const h=I.replace("cachly:ckg:node:","");!h.startsWith("person:")&&!h.startsWith("file:")&&$.add(h)}}),c.on("end",k),c.on("error",m)})}const s=new Map;for(const f of[...$].slice(0,25)){const c=await A.smembers(`cachly:ckg:idx:to:${f}`);for(const k of c){const m=await A.get(k);if(!m)continue;const _=P(m,null);if(!_||_.edgeType!=="authored")continue;const I=await A.get(`cachly:ckg:node:${_.from}`);if(!I)continue;const h=P(I,null);if(!h||h.type!=="person")continue;const e=s.get(_.from);e?(e.lessonCount++,e.totalConfidence+=_.confidence,e.domains.add(h.domain),_.last_updated>e.lastActive&&(e.lastActive=_.last_updated)):s.set(_.from,{handle:h.handle,lessonCount:1,totalConfidence:_.confidence,domains:new Set([h.domain]),lastActive:h.last_active??_.last_updated})}}if(s.size===0)return[`## \u{1F465} Who Knows About \`${t}\`?`,"","No attributed lessons found yet.","",'Knowledge attribution builds automatically \u2014 add `author="name"` to `learn_from_attempts` calls:',`\`learn_from_attempts(topic="${t}", author="your-handle", ...)\``].join(`
52
+ `);const M=[...s.entries()].map(([f,c])=>({id:f,handle:c.handle,lessonCount:c.lessonCount,avgConfidence:c.totalConfidence/c.lessonCount,domains:[...c.domains].slice(0,3).join(", "),lastActive:c.lastActive})).sort((f,c)=>c.lessonCount*c.avgConfidence-f.lessonCount*f.avgConfidence).slice(0,S),g=[],x=M[0];if(x)try{const f=await A.smembers(`cachly:ckg:idx:from:${x.id}`),c=[];for(const k of f){const m=await A.get(k);if(!m)continue;const _=P(m,null);if(!_||_.edgeType!=="collaborates")continue;const I=await A.get(`cachly:ckg:node:${_.to}`),h=I?P(I,null):null;h?.handle&&c.push({handle:h.handle,trials:_.trials})}c.sort((k,m)=>m.trials-k.trials);for(const k of c.slice(0,3))g.push(k.handle)}catch{}const v=[`## \u{1F465} Who Knows About \`${t}\`?`,"",`**${M.length}** contributor${M.length!==1?"s":""} with relevant lessons:`,""];for(const[f,c]of M.entries()){const k=f===0?"\u{1F947}":f===1?"\u{1F948}":f===2?"\u{1F949}":`${f+1}.`,m=(c.avgConfidence*100).toFixed(0),_=Math.floor((Date.now()-new Date(c.lastActive).getTime())/864e5),I=_<1?"today":_<7?`${_}d ago`:_<30?`${Math.floor(_/7)}w ago`:`${Math.floor(_/30)}mo ago`,h=await Be(A,b,c.handle).catch(()=>null),e=h?` ${ht[h]}`:"";v.push(`${k} **${c.handle}**${e} \u2014 ${c.lessonCount} lesson${c.lessonCount!==1?"s":""} \xB7 ${m}% confidence \xB7 last active ${I}`,` _domains: ${c.domains}_`,"")}return g.length>0&&x&&v.push(`\u{1F91D} **${x.handle}** frequently works with: ${g.map(f=>`**${f}**`).join(", ")}`," _(ask them together \u2014 they've solved structurally similar things on shared files)_",""),v.push("---",'_Attribution grows with every `learn_from_attempts(author="...", ...)` call._'),v.join(`
53
+ `)}case"brain_file_map":{const{instance_id:b,file_paths:t=[]}=G,o=(Array.isArray(t)?t:[]).filter(p=>typeof p=="string"&&p.trim().length>0).map(p=>p.trim());if(!o.length)return'\u26A0\uFE0F Pass at least one file path. Example: `brain_file_map(file_paths=["src/auth/jwt.ts"])`.';const S=await q(b),A=["## \u{1F4C1} File Knowledge Map","","_What cachly knows about the files you're about to touch:_",""];for(const p of o.slice(0,10)){A.push(`### \`${p}\``);const l=`file:${Z(p)}`,$=await S.smembers(`cachly:ckg:idx:to:${l}`),s=[];for(const v of $){const f=await S.get(v);if(!f)continue;const c=P(f,null);if(!c||c.edgeType!=="touched")continue;const k=await S.get(`cachly:ckg:node:${c.from}`);if(!k)continue;const m=P(k,null);!m||m.type!=="person"||s.push({handle:m.handle,touches:c.trials,lastSeen:c.last_updated})}if(s.sort((v,f)=>f.touches-v.touches),s.length>0){const v=s.slice(0,5).map((f,c)=>{const k=c===0?"\u{1F947}":c===1?"\u{1F948}":c===2?"\u{1F949}":`${c+1}.`,m=Math.floor((Date.now()-new Date(f.lastSeen).getTime())/864e5),_=m<1?"today":m<7?`${m}d ago`:`${Math.floor(m/7)}w ago`;return`${k} **${f.handle}** (${f.touches}\xD7 \xB7 ${_})`}).join(" \xB7 ");A.push(`**Experts:** ${v}`)}else A.push("**Experts:** _None yet \u2014 add `author` to `learn_from_attempts` calls_");const g=p.replace(/[^a-z0-9]/gi," ").split(/\s+/).filter(v=>v.length>=4).slice(0,3).join(" "),x=[];if(g)try{const v=await Pe(S,["cachly:lesson:best:*"],g,4);for(const f of v){const c=P(f.content,{});if(c.visibility==="private")continue;const k=c.outcome==="success"?"\u2705":c.outcome==="partial"?"\u26A0\uFE0F":"\u274C",m=c.severity==="critical"?" \u{1F534}":c.severity==="major"?" \u{1F7E1}":"",_=c.author?` \xB7 \u{1F464} ${c.author}`:"",I=f.key.replace("cachly:lesson:best:","");x.push(` - ${k} \`${I}\`${m}${_} \u2014 ${(c.what_worked??"").slice(0,100)}`)}}catch{}if(x.length<2){const v=await ce(S,"cachly:lesson:best:*",{max:1500});for(const f of v.slice(0,300)){const c=await S.get(f);if(!c)continue;const k=P(c,{});if(k.visibility==="private"||!(k.file_paths??[]).includes(p))continue;const m=f.replace("cachly:lesson:best:","");if(x.some(e=>e.includes(`\`${m}\``)))continue;const _=k.outcome==="success"?"\u2705":k.outcome==="partial"?"\u26A0\uFE0F":"\u274C",I=k.severity==="critical"?" \u{1F534}":k.severity==="major"?" \u{1F7E1}":"",h=k.author?` \xB7 \u{1F464} ${k.author}`:"";x.push(` - ${_} \`${m}\`${I}${h} \u2014 ${(k.what_worked??"").slice(0,100)}`)}}x.length>0?(A.push("**Related lessons:**"),A.push(...x.slice(0,5))):A.push("**Related lessons:** _None yet_"),A.push("")}return A.push("---",'_Attribution builds with `learn_from_attempts(file_paths=[...], author="...")`. Run this before committing to track what changed and why._'),A.join(`
54
+ `)}case"team_expertise_map":{const{instance_id:b,top_n:t=20}=G,o=await q(b),S=await ce(o,"cachly:ckg:node:person:*",{max:2e3});if(S.length===0)return["## \u{1F5FA}\uFE0F Team Expertise Map","","No contributors yet.","",'Add `author="handle"` to any `learn_from_attempts` call to start building the map.'].join(`
55
+ `);const A=[];for(const $ of S){const s=await o.get($);if(!s)continue;const M=P(s,null);if(!M||M.type!=="person")continue;const g=await o.smembers(`cachly:ckg:idx:from:${M.id}`),x=new Map;let v=0;for(const f of g){const c=await o.get(f);if(!c)continue;const k=P(c,null);if(!k||k.edgeType!=="authored")continue;v++;const m=k.to.split(":")[0]??"unknown";x.set(m,(x.get(m)??0)+1)}A.push({handle:M.handle,lessonCount:v,domains:x,lastActive:M.last_active})}A.sort(($,s)=>s.lessonCount-$.lessonCount);const p=A.slice(0,t),l=["## \u{1F5FA}\uFE0F Team Expertise Map","",`**${p.length}** contributor${p.length!==1?"s":""} tracked:`,"","| # | Handle | Lessons | Top domains | Last active |","|---|--------|---------|-------------|-------------|"];for(const[$,s]of p.entries()){const M=$===0?"\u{1F947}":$===1?"\u{1F948}":$===2?"\u{1F949}":`${$+1}`,g=Math.floor((Date.now()-new Date(s.lastActive).getTime())/864e5),x=g<1?"today":g<7?`${g}d ago`:g<30?`${Math.floor(g/7)}w ago`:`${Math.floor(g/30)}mo ago`,v=[...s.domains.entries()].sort((f,c)=>c[1]-f[1]).slice(0,3).map(([f,c])=>`${f}(${c})`).join(", ")||"\u2014";l.push(`| ${M} | **${s.handle}** | ${s.lessonCount} | ${v} | ${x} |`)}return l.push("","---",'_Use `brain_who_knows(topic="...")` to find the expert for a specific problem._'),l.join(`
56
+ `)}case"skill_gaps":{const{instance_id:b,min_failures:t=1}=G,o=await q(b),S=await ce(o,"cachly:lesson:best:*",{max:3e3}),A=new Map;let p=0,l=0;for(const M of S){const g=await o.get(M);if(!g)continue;const x=P(g,{}),v=(x.topic??"").split(":")[0]??"unknown",f=A.get(v)??{successes:0,failures:0,attributed:0,topics:[]};x.outcome==="success"||x.outcome==="partial"?f.successes++:f.failures++,x.author&&(f.attributed++,p++),x.visibility==="private"&&l++,f.topics.push(x.topic??""),A.set(v,f)}const $=[];for(const[M,g]of A)g.failures>=t&&g.successes===0&&$.push({domain:M,type:"unresolved failures",severity:g.failures>=3?"\u{1F534} critical":"\u{1F7E1} warn",detail:`${g.failures} failure${g.failures!==1?"s":""}, 0 solutions \u2014 use \`learn_from_attempts\` to store what fixed it`}),g.successes+g.failures>=3&&g.attributed===0&&$.push({domain:M,type:"no attribution",severity:"\u{1F535} info",detail:`${g.successes+g.failures} lessons with no \`author\` field \u2014 attribution lost, \`brain_who_knows\` can't help here`});const s=["## \u{1F50D} Skill Gaps \u2014 Knowledge Blind Spots","",`**${S.length}** lessons scanned \xB7 **${p}** attributed \xB7 **${$.length}** gap${$.length!==1?"s":""} found`,l>0?`_(${l} private lessons excluded from gap analysis)_`:"",""].filter(Boolean);if($.length===0)s.push("\u2705 No significant knowledge gaps detected. All domains with failures have at least one success lesson.");else{const M=[...$.filter(g=>g.severity.includes("critical")),...$.filter(g=>g.severity.includes("warn")),...$.filter(g=>g.severity.includes("info"))];for(const g of M.slice(0,15))s.push(`${g.severity} **\`${g.domain}\`** \u2014 ${g.type}`),s.push(` ${g.detail}`),s.push("");$.length>15&&s.push(`_\u2026and ${$.length-15} more gaps. Fix critical ones first._`)}return s.push("---",`_Fix gaps with \`learn_from_attempts(topic="${$[0]?.domain??"your:topic"}", outcome="success", what_worked="...")\`_`,'_Add `author="name"` to every call to enable `brain_who_knows` + `team_expertise_map`._'),s.join(`
57
+ `)}case"brain_coverage":{const{instance_id:b,repo_path:t="."}=G,o=await q(b),[S,A,p]=await Promise.all([ce(o,"cachly:lesson:best:*",{max:5e3}),ce(o,"cachly:ckg:node:person:*",{max:2e3}),ce(o,"cachly:ckg:node:file:*",{max:5e3})]);let l=0,$="n/a";try{const{execSync:m}=await import("node:child_process"),{resolve:_}=await import("node:path"),I=_(t);l=m("git ls-files",{cwd:I,encoding:"utf-8",stdio:"pipe"}).trim().split(`
58
+ `).filter(Boolean).length,$=String(l)}catch{}let s=0,M=0,g=0;const x=new Set;for(const m of S){const _=await o.get(m);if(!_)continue;const I=P(_,{});I.outcome==="success"||I.outcome==="partial"?s++:M++,I.author&&g++,x.add((I.topic??"").split(":")[0]??"unknown")}const v=l>0?`${Math.round(p.length/l*100)}% (${p.length}/${l} files)`:`${p.length} files tracked`,f=S.length>0?Math.round(g/S.length*100):0,c=Math.round(Math.min(1,S.length/50)*25+Math.min(1,s/Math.max(1,S.length))*25+Math.min(1,f/100)*25+Math.min(1,A.length/5)*25);return["## \u{1F4CA} Brain Coverage Report","",`${c>=80?"\u{1F7E2}":c>=50?"\u{1F7E1}":"\u{1F534}"} **Overall score: ${c}/100**`,"","| Metric | Value |","|--------|-------|",`| Total lessons | **${S.length}** (${s} success \xB7 ${M} failure) |`,`| Problem domains | **${x.size}** |`,`| Attribution | **${g}/${S.length}** (${f}%) |`,`| Contributors | **${A.length}** tracked in knowledge graph |`,`| File coverage | **${v}** |`,"","**Score breakdown (each 0\u201325):**",`- Lesson volume: **${Math.round(Math.min(1,S.length/50)*25)}/25** _(target: 50+ lessons)_`,`- Success ratio: **${Math.round(Math.min(1,s/Math.max(1,S.length))*25)}/25** _(target: all lessons resolved)_`,`- Attribution: **${Math.round(Math.min(1,f/100)*25)}/25** _(target: 100% attributed)_`,`- Team engagement: **${Math.round(Math.min(1,A.length/5)*25)}/25** _(target: 5+ contributors)_`,"","---","_Boost score: run `brain_from_git` \xB7 add `author` to `learn_from_attempts` \xB7 fix gaps with `skill_gaps`_"].join(`
59
+ `)}case"brain_metrics":{const{instance_id:b}=G,t=await q(b),[o,S,A,p,l,$]=await Promise.all([t.get(`cachly:stats:born_at:${b}`).catch(()=>null),t.get(`cachly:stats:first_recall_at:${b}`).catch(()=>null),t.get(`cachly:stats:recalls_total:${b}`).catch(()=>null),t.get(`cachly:stats:cross_author_recalls:${b}`).catch(()=>null),t.get(`cachly:stats:time_saved_mins:${b}`).catch(()=>null),t.smembers(`cachly:stats:reuse_pairs:${b}`).catch(()=>[])]);let s={};try{s=await t.hgetall(Ut(b))??{}}catch{s={}}const M=Vt(s,10);let g={token:0,abrufe:0,treffer:0};try{const D=await t.get(Qt(b)),O=await t.hgetall(es(b))??{};g={token:D??0,abrufe:O.abrufe??0,treffer:O.treffer??0}}catch{}const x=Zt(g);let v;if(o&&S){const D=new Date(S).getTime()-new Date(o).getTime(),O=Math.max(0,Math.round(D/6e4)),F=O<1?"<1 min":O<60?`${O} min`:`${(O/60).toFixed(1)} h`,H=D<=2*6e4?"\u{1F7E2} under 2 min target":D<=60*6e4?"\u{1F7E1} over 2 min":"\u{1F534} over 1 h";v=`**${F}** from first lesson \u2192 first proven recall \xB7 ${H}`}else o&&!S?v="\u{1F7E1} Brain has knowledge but no proven recall yet \u2014 try `smart_recall`":v="\u26AA Not enough data yet \u2014 store a lesson, then recall it";const f=["\u26AA **Nicht gemessen** \u2014 diese Zahl erhebt der Server nicht selbst."," Selbst messen: `npm run bench` in `sdk/mcp`."," Achtung, der Pruefstand hat 17 Lektionen und 13 Fragen \u2014 ein Beispielsatz, kein Bestand."," Auf 498 ECHTEN Lektionen gemessen am 19.08.2026: Precision@1 **30 %** (vorher 15 %)."].join(`
60
+ `),c=Number(A??0),k=Number(p??0),m=c>0?Math.round(k/c*100):0,_=m>=30?"\u{1F7E2} above 30 % target":c===0?"\u26AA no recalls yet":"\u{1F7E1} below 30 % target",I=Math.round(Number(l??0)),h=I<60?`${I} min`:`${(I/60).toFixed(1)} h`;return["## \u{1F4C8} Brain Metrics \u2014 the three that decide everything","","### 1. Time-to-first-recall _(onboarding friction)_",v,"","### 2. Recall-lift _(the moat proof)_",f,"_Reproduce: `npm run bench` \xB7 CI-defended in `rerank.test.ts` \xB7 details in BENCH.md_","","### 3. Team-knowledge-reuse _(the value only a shared brain delivers)_",`**${m}%** of proven recalls used a teammate's lesson (${k}/${c}) \xB7 ${_}`,$.length>0?`**${$.length}** distinct reuse relationship${$.length!==1?"s":""} across the team`:"","","---","### \u{1F4E6} Aus dem Brain geliefert _(gemessen, nicht gerechnet)_",Xt(x),"",x.abrufe>0?`_Daneben eine **Sch\xE4tzung**: ${h} nicht neu recherchiert \u2014 gerechnet mit 30\u2013240 Min. je Lektion nach Schwere, nicht gemessen. Die Zahl dar\xFCber braucht diese Annahme nicht._`:`_Eine **Sch\xE4tzung** von ${h} \u201Enicht neu recherchiert" w\xE4re hier gerechnet, nicht gemessen \u2014 sie erscheint erst, wenn daneben eine gemessene Zahl steht._`,c===0?'_Tip: pass `author="your-handle"` to `smart_recall` so cross-author reuse can be tracked._':"_These numbers compound: every learned lesson and every teammate raises all three._","","### \u{1F527} Werkzeug-Nutzung _(erst messen, dann zusammenlegen)_",Yt(M,rs.length),...M.spitze.length>0?M.spitze.map((D,O)=>` ${String(O+1).padStart(2)}. \`${D.name}\` \u2014 ${D.aufrufe}\xD7 (${Math.round(D.anteil*100)} %)`):[]].filter(Boolean).join(`
61
+ `)}case"brain_service_map":{const{instance_id:b,service:t}=G;if(!t||typeof t!="string"||!t.trim())return'\u26A0\uFE0F `brain_service_map` requires a non-empty `service`. Example: `brain_service_map(service="prometheus")`.';const o=await q(b),S=`service:${Z(t.trim())}`,A=await o.get(`cachly:ckg:node:${S}`).catch(()=>null),p=A?P(A,null):null,l=await o.smembers(`cachly:ckg:idx:to:${S}`).catch(()=>[]),$=[],s=[],M=[];for(const _ of l){const I=await o.get(_).catch(()=>null);if(!I)continue;const h=P(I,null);if(h)if(h.edgeType==="operates"){const e=await o.get(`cachly:ckg:node:${h.from}`).catch(()=>null),D=e?P(e,null):null;D?.handle&&$.push({handle:D.handle,trials:h.trials})}else if(h.edgeType==="runs_in"){const e=await o.get(`cachly:ckg:node:${h.from}`).catch(()=>null),D=e?P(e,null):null;D?.path&&s.push({path:D.path,trials:h.trials})}else h.edgeType==="affects"&&M.push({id:h.from,confidence:h.confidence})}$.sort((_,I)=>I.trials-_.trials),s.sort((_,I)=>I.trials-_.trials),M.sort((_,I)=>I.confidence-_.confidence);const g=[],x=await ce(o,"cachly:lesson:best:*",{max:1500}),v=t.trim().toLowerCase();for(const _ of x.slice(0,400)){const I=await o.get(_).catch(()=>null);if(!I)continue;const h=P(I,{});h.visibility!=="private"&&(h.service??"").toLowerCase()===v&&g.push({...h,topic:_.replace("cachly:lesson:best:","")})}if(!p&&g.length===0&&$.length===0)return[`## \u{1F6F0}\uFE0F Service Map \u2014 \`${t}\``,"","Nothing known about this service yet.","","Tag lessons with the service they concern so the brain can build this map:",`\`learn_from_attempts(topic="...", service="${t}", author="you", file_paths=[...])\``].join(`
62
+ `);const f=p?.kind==="system"?"\u{1F5A5}\uFE0F system":"\u{1F6F0}\uFE0F service",c=[`## ${p?.kind==="system"?"\u{1F5A5}\uFE0F":"\u{1F6F0}\uFE0F"} Service Map \u2014 \`${t}\``,"",p?`_${f} \xB7 ${p.count} lesson${p.count!==1?"s":""} referenced \xB7 domain: ${p.domain}_`:"_No graph node yet \u2014 built from tagged lessons below._",""];if($.length>0){const _=$.slice(0,5).map((I,h)=>`${h===0?"\u{1F947}":h===1?"\u{1F948}":h===2?"\u{1F949}":`${h+1}.`} **${I.handle}** (${I.trials}\xD7)`).join(" \xB7 ");c.push(`**Operators:** ${_}`,"")}else c.push("**Operators:** _None attributed \u2014 add `author` when learning about this service_","");const k=g.filter(_=>_.outcome==="failure"),m=g.filter(_=>_.outcome!=="failure");if(k.length>0){c.push(`**\u26A0\uFE0F Known failures (${k.length}):**`);for(const _ of k.slice(0,6)){const I=_.severity==="critical"?" \u{1F534}":_.severity==="major"?" \u{1F7E1}":"",h=_.author?` \xB7 \u{1F464} ${_.author}`:"";c.push(` - \u274C \`${_.topic}\`${I}${h} \u2014 ${(_.what_worked??"").slice(0,100)}`)}c.push("")}if(m.length>0){c.push(`**\u2705 Proven fixes (${m.length}):**`);for(const _ of m.slice(0,6)){const I=_.outcome==="partial"?"\u26A0\uFE0F":"\u2705",h=_.author?` \xB7 \u{1F464} ${_.author}`:"";c.push(` - ${I} \`${_.topic}\`${h} \u2014 ${(_.what_worked??"").slice(0,100)}`)}c.push("")}return s.length>0&&c.push(`**Files in this service:** ${s.slice(0,8).map(_=>`\`${_.path}\``).join(", ")}`,""),c.push("---",`_Tag lessons with \`service="${t}"\` to keep this map current. For infra, pass \`service_kind="system"\`._`),c.join(`
63
+ `)}case"brain_collab_pairs":{const{instance_id:b,min_weight:t=1}=G,o=await q(b),S=typeof t=="number"&&t>=1?Math.floor(t):1,A=await ce(o,"cachly:ckg:node:person:*",{max:500}),p=new Map;for(const x of A){const v=await o.get(x);if(!v)continue;const f=P(v,null);!f||f.type!=="person"||p.set(f.id,{id:f.id,handle:f.handle,domain:f.domain,count:f.count})}if(p.size===0)return["## \u{1F91D} Team Collaboration Graph","","No contributors found yet.","",'The graph builds automatically as teammates store lessons with `author="name"`:','`learn_from_attempts(topic="...", author="alice", file_paths=["src/auth.ts"], ...)`'].join(`
64
+ `);const l=new Map;for(const[x,v]of p){const f=await o.smembers(`cachly:ckg:idx:from:${x}`);for(const c of f){const k=await o.get(c);if(!k)continue;const m=P(k,null);if(!m||m.edgeType!=="collaborates")continue;const _=p.get(m.to);if(!_)continue;const[I,h]=x<m.to?[x,m.to]:[m.to,x],e=`${I}||${h}`,D=l.get(e);if(D)D.trials+=m.trials;else{const[O,F]=x<m.to?[v,_]:[_,v];l.set(e,{a:O,b:F,trials:m.trials,confidence:m.confidence})}}}const $=[...l.values()].filter(x=>x.trials>=S).sort((x,v)=>v.trials-x.trials),s=new Set;for(const x of $)s.add(x.a.id),s.add(x.b.id);const M=[...p.values()].filter(x=>!s.has(x.id)),g=["## \u{1F91D} Team Collaboration Graph",""];if($.length===0)g.push(`No collaboration pairs yet (min weight: ${S}).`,"","Pairs appear when two contributors touch the same file in `learn_from_attempts`",'or when someone recalls a teammate\'s lesson via `smart_recall(requester="...")`.');else{g.push(`**${$.length} collaboration pair${$.length!==1?"s":""}** across ${s.size} contributor${s.size!==1?"s":""}:`,"");for(const x of $.slice(0,20)){const v=(x.confidence*100).toFixed(0),f=[x.a.domain,x.b.domain].filter((c,k,m)=>c&&m.indexOf(c)===k).map(c=>`\`${c}\``).join(", ");g.push(`### @${x.a.handle} \u2194 @${x.b.handle}`,`- **${x.trials} collaboration event${x.trials!==1?"s":""}** \xB7 ${v}% confidence`,`- Domains: ${f||"_unknown_"}`,`> \u{1F4AC} _"Frag **@${x.a.handle}** und **@${x.b.handle}** \u2014 they've solved problems together in these areas."_`,"")}$.length>20&&g.push(`_...and ${$.length-20} more pairs. Use \`min_weight\` to filter._`,"")}return M.length>0&&g.push("---","### \u26A0\uFE0F Bus Factor Alert \u2014 No Collaborators Yet","",`${M.length} contributor${M.length!==1?"s have":" has"} knowledge that no teammate has recalled or co-touched:`,"",...M.slice(0,10).map(x=>`- **@${x.handle}** \u2014 ${x.count} lesson${x.count!==1?"s":""} \xB7 domain: \`${x.domain}\``),"","_Tip: pair these contributors with teammates on shared files to reduce bus factor._"),g.push("","---","_Collaboration edges build from `learn_from_attempts(author=..., file_paths=[...])` and `smart_recall(requester=...)` cross-author reuse._"),g.join(`
65
+ `)}case"brain_portability":{const{instance_id:b}=G,t=[{name:"Claude Code",slug:"claude-code",icon:"\u{1F916}",configPath:"~/.claude/settings.json",configBlock:JSON.stringify({mcpServers:{cachly:{command:"npx",args:["@cachly-dev/mcp-server@latest"],env:{CACHLY_JWT:"<cky_live_...>",CACHLY_BRAIN_INSTANCE_ID:b}}}},null,2)},{name:"Cursor",slug:"cursor",icon:"\u{1F5B1}\uFE0F",configPath:"~/.cursor/mcp.json",configBlock:JSON.stringify({mcpServers:{cachly:{command:"npx",args:["@cachly-dev/mcp-server@latest"],env:{CACHLY_JWT:"<cky_live_...>",CACHLY_BRAIN_INSTANCE_ID:b}}}},null,2)},{name:"Windsurf",slug:"windsurf",icon:"\u{1F3C4}",configPath:"~/.windsurf/mcp.json",configBlock:JSON.stringify({mcpServers:{cachly:{command:"npx",args:["@cachly-dev/mcp-server@latest"],env:{CACHLY_JWT:"<cky_live_...>",CACHLY_BRAIN_INSTANCE_ID:b}}}},null,2)},{name:"GitHub Copilot (VS Code)",slug:"copilot",icon:"\u{1F419}",configPath:".vscode/mcp.json (workspace)",configBlock:JSON.stringify({servers:{cachly:{type:"stdio",command:"npx",args:["@cachly-dev/mcp-server@latest"],env:{CACHLY_JWT:"<cky_live_...>",CACHLY_BRAIN_INSTANCE_ID:b}}}},null,2)},{name:"Cline",slug:"cline",icon:"\u26A1",configPath:"~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",configBlock:JSON.stringify({mcpServers:{cachly:{command:"npx",args:["@cachly-dev/mcp-server@latest"],env:{CACHLY_JWT:"<cky_live_...>",CACHLY_BRAIN_INSTANCE_ID:b}}}},null,2)},{name:"Zed",slug:"zed",icon:"\u26A1",configPath:"~/.config/zed/settings.json",configBlock:JSON.stringify({context_servers:{cachly:{command:{path:"npx",args:["@cachly-dev/mcp-server@latest"]},env:{CACHLY_JWT:"<cky_live_...>",CACHLY_BRAIN_INSTANCE_ID:b}}}},null,2)},{name:"Continue",slug:"continue",icon:"\u25B6\uFE0F",configPath:"~/.continue/config.json",configBlock:JSON.stringify({mcpServers:[{name:"cachly",command:"npx @cachly-dev/mcp-server@latest",env:{CACHLY_JWT:"<cky_live_...>",CACHLY_BRAIN_INSTANCE_ID:b}}]},null,2)}],o=["## \u{1F310} Brain Portability \u2014 Same Brain, Any Model","",'> _"Bring your own model, keep your brain."_',"",`**Your Brain ID:** \`${b}\``,"","Replace `<cky_live_...>` below with your own API key (get one at https://cachly.dev/setup-ai) \u2014 the Brain ID alone does not authenticate you.","","This Brain ID is model-agnostic. Every lesson you've learned, every Memory Crystal you've distilled,","and every prediction from your Causal Knowledge Graph is accessible from **any MCP-compatible AI client**.","","---","",`### Compatible Clients (${t.length} supported)`,""];for(const S of t)o.push(`#### ${S.icon} ${S.name}`,`Config: \`${S.configPath}\``,"","```json",S.configBlock,"```","");return o.push("---","","### What travels with you","","| Data | Model-neutral? |","|------|---------------|","| Lessons (learn_from_attempts) | \u2705 Full access in all clients |","| Memory Crystals (memory_crystalize) | \u2705 Included in every session_start |","| Causal Knowledge Graph (brain_predict) | \u2705 Same graph, same predictions |","| Team lessons (team_learn, team_confirm) | \u2705 Team is per-Brain, not per-model |","| Domain Brain Marketplace installs | \u2705 Installed once, available everywhere |","| Session handoffs (session_handoff) | \u2705 Cross-client continuity |","","---","","### One-command setup for any new client","","```bash","npx @cachly-dev/mcp-server@latest autopilot","```","",`Autopilot detects all installed editors and writes the config for each \u2014 with your \`${b}\` Brain ID pre-filled.`,"","---","_Docs: cachly.dev/docs/model-neutral \u2022 Your Brain, Any Model_"),o.join(`
66
+ `)}case"brain_changelog":{const{instance_id:b,days:t=7,max_lessons:o=30,include_failures:S=!0}=G,A=await q(b),p=Date.now()-Number(t)*864e5,l=await ce(A,"cachly:lesson:best:*",{max:4e3,timeoutMs:4e3}),$=[];for(const c of l){const k=await A.get(c).catch(()=>null);if(!k)continue;const m=P(k,{});if((m.ts?new Date(m.ts).getTime():0)<p||!S&&m.outcome==="failure")continue;const I=c.replace("cachly:lesson:best:","");$.push({...m,topic:I})}$.sort((c,k)=>{const m=c.ts?new Date(c.ts).getTime():0;return(k.ts?new Date(k.ts).getTime():0)-m});const s=$.slice(0,Number(o));if(s.length===0)return[`## \u{1F4CB} Brain Changelog \u2014 last ${t} day${Number(t)!==1?"s":""}`,"","_No new lessons learned in this window. Your Brain already knows everything it knows \u2014 add new lessons with `learn_from_attempts`._"].join(`
67
+ `);const M=new Map;for(const c of s){const k=c.topic.includes(":")?c.topic.split(":")[0]:"general",m=M.get(k)??[];m.push(c),M.set(k,m)}const g=c=>c==="critical"?"\u{1F6A8}":c==="major"?"\u26A0\uFE0F":c==="minor"?"\u{1F4A1}":"\u2728",x=c=>c==="failure"?"\u274C":c==="partial"?"\u{1F536}":"\u2705",f=[`## \u{1F4CB} Brain Changelog \u2014 ${new Date().toISOString().slice(0,10)} (last ${t} day${Number(t)!==1?"s":""})`,"",`_${s.length} lesson${s.length!==1?"s":""} learned across ${M.size} topic area${M.size!==1?"s":""}_`,""];for(const[c,k]of Array.from(M.entries()).sort((m,_)=>_[1].length-m[1].length)){f.push(`### ${c}`),f.push("");for(const m of k){const _=m.outcome==="failure"?x(m.outcome):g(m.severity),I=m.outcome==="failure"?m.what_failed:m.what_worked,h=[];m.author&&h.push(`by **${m.author}**`),m.recall_count&&h.push(`${m.recall_count} recall${m.recall_count!==1?"s":""}`),m.confidence!==void 0&&h.push(`${Math.round(m.confidence*100)}% confidence`),f.push(`- ${_} **${m.topic}**${I?` \u2014 ${I}`:""}`),h.length&&f.push(` _${h.join(" \xB7 ")}_`)}f.push("")}return f.push("---",`_Generated from Brain \`${b}\` \xB7 Share this in your standup or Slack_`),f.join(`
68
+ `)}case"brain_set_pref":{const{instance_id:b,key:t,value:o}=G;if(!t||!t.trim())throw new Error("key is required");const S=await q(b),A=`cachly:prefs:${t.trim()}:${b}`;return await S.set(A,String(o)),`\u2705 Preference \`${t}\` set to \`${o}\` for this Brain instance.`}case"brain_get_pref":{const{instance_id:b,key:t}=G,o=await q(b);if(t&&t.trim()){const l=await o.get(`cachly:prefs:${t.trim()}:${b}`).catch(()=>null);return l===null?`\u2139\uFE0F Preference \`${t}\` is not set (using default).`:`\u{1F527} Preference \`${t}\` = \`${l}\``}const S=await ce(o,`cachly:prefs:*:${b}`,{max:200,timeoutMs:2e3});if(S.length===0)return"\u2139\uFE0F No preferences set for this Brain instance.";const A=await o.mget(...S),p=["\u{1F527} **Brain preferences:**",""];for(let l=0;l<S.length;l++){const $=S[l].replace("cachly:prefs:","").replace(`:${b}`,"");p.push(`- \`${$}\` = \`${A[l]??"(null)"}\``)}return p.join(`
69
+ `)}default:return null}}export{Js as BRAIN_TOOL_NAMES,Ys as handleBrainTool,Le as recallVorschau};