brainllm 11.0.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ A persistent, graph-structured second brain for Claude and any MCP client — bu
10
10
 
11
11
  [**brainllm site**](https://miisodev.github.io/BrainLLM/) · [How it works](https://miisodev.github.io/BrainLLM/how-it-works.html) · [Use cases](https://miisodev.github.io/BrainLLM/use-cases.html) · [Docs](https://miisodev.github.io/BrainLLM/docs.html)
12
12
 
13
- [![Version](https://img.shields.io/badge/version-11.0.0-f59e0b?style=flat-square)](https://github.com/miisodev/BrainLLM/releases)
13
+ [![Version](https://img.shields.io/badge/version-12.0.0-f59e0b?style=flat-square)](https://github.com/miisodev/BrainLLM/releases)
14
14
  [![CI](https://img.shields.io/github/actions/workflow/status/miisodev/BrainLLM/ci.yml?branch=main&style=flat-square&label=CI&color=f59e0b)](https://github.com/miisodev/BrainLLM/actions/workflows/ci.yml)
15
15
  [![License: MIT](https://img.shields.io/badge/license-MIT-a1a1aa?style=flat-square)](./LICENSE)
16
16
  [![Runtime: Bun](https://img.shields.io/badge/runtime-Bun%20%E2%89%A5%201.0-a1a1aa?style=flat-square&logo=bun&logoColor=white)](https://bun.sh)
@@ -60,7 +60,7 @@ At bootstrap, BrainLLM builds a five-area tree in Trilium. Every note the tools
60
60
  ```
61
61
  BrainLLM (#brainLlmRoot)
62
62
  ├── 👤 Master Biography · Goals · Preferences (the user — maintained singletons)
63
- ├── 🤖 LLM Responsibilities · Protocols · Diary/ (the assistant's self-model + daily diary)
63
+ ├── 🤖 LLM Responsibilities · Protocols · Self-correction · Diary/ (the assistant's self-model + daily diary)
64
64
  ├── 🗂️ Memory Sessions/ · Threads/ (daily session logs + multi-session work)
65
65
  ├── 📚 Knowledge Master/ · Domains/<domain>/{ Sources, info } (learned facts beyond/contra training)
66
66
  └── 💡 Insights Logs/ · Graph (the brain's record of itself)
@@ -68,7 +68,7 @@ BrainLLM (#brainLlmRoot)
68
68
 
69
69
  | Note class | Kinds | Behavior |
70
70
  |---|---|---|
71
- | **Singletons** | biography, goals, preferences, responsibilities, protocols (+ each domain's Sources note) | Exactly one maintained note; edited in place; hold *current-state truth* |
71
+ | **Singletons** | biography, goals, preferences, responsibilities, protocols, selfcorrection (+ each domain's Sources note) | Exactly one maintained note; edited in place; hold *current-state truth* |
72
72
  | **Dated records** | diary, session, log | One per calendar day; every write lands as a timestamped addendum block — chronology is the point |
73
73
  | **Collections** | thread, user, information, domain | Titled notes, deduplicated by normalized title within their scope |
74
74
 
package/dist/index.js CHANGED
@@ -25416,6 +25416,7 @@ var Kinds = [
25416
25416
  "preferences",
25417
25417
  "responsibilities",
25418
25418
  "protocols",
25419
+ "selfcorrection",
25419
25420
  "diary",
25420
25421
  "session",
25421
25422
  "thread",
@@ -25433,6 +25434,7 @@ var KIND_AREA = {
25433
25434
  preferences: "master",
25434
25435
  responsibilities: "llm",
25435
25436
  protocols: "llm",
25437
+ selfcorrection: "llm",
25436
25438
  diary: "llm",
25437
25439
  session: "memory",
25438
25440
  thread: "memory",
@@ -25449,7 +25451,8 @@ var SingletonKinds = [
25449
25451
  "goals",
25450
25452
  "preferences",
25451
25453
  "responsibilities",
25452
- "protocols"
25454
+ "protocols",
25455
+ "selfcorrection"
25453
25456
  ];
25454
25457
  var Statuses = ["active", "resolved", "superseded", "dormant", "eternal"];
25455
25458
  var RelationTypes = [
@@ -25972,10 +25975,10 @@ class TriliumClient {
25972
25975
  import { existsSync, readFileSync, writeFileSync } from "fs";
25973
25976
  import { join, dirname } from "path";
25974
25977
  var EMPTY_BRAINLLM = {
25975
- version: 9,
25978
+ version: 10,
25976
25979
  root: "",
25977
25980
  master: { root: "", biography: "", goals: "", preferences: "" },
25978
- llm: { root: "", responsibilities: "", protocols: "", diary: "" },
25981
+ llm: { root: "", responsibilities: "", protocols: "", selfcorrection: "", diary: "" },
25979
25982
  memory: { root: "", sessions: "", threads: "", metaThread: "" },
25980
25983
  knowledge: { root: "", master: "", domains: "" },
25981
25984
  insights: { root: "", logs: "" },
@@ -26016,9 +26019,10 @@ function loadConfig() {
26016
26019
  return null;
26017
26020
  try {
26018
26021
  const parsed = JSON.parse(readFileSync(path, "utf-8"));
26019
- if (typeof parsed?.root === "string" && (parsed?.version === 9 || parsed?.version === 8 || parsed?.version === 5)) {
26022
+ if (typeof parsed?.root === "string" && [10, 9, 8, 5].includes(parsed?.version)) {
26020
26023
  return {
26021
26024
  ...parsed,
26025
+ llm: { selfcorrection: "", ...parsed.llm ?? {} },
26022
26026
  memory: { metaThread: "", ...parsed.memory ?? {} },
26023
26027
  policy: { ...DEFAULT_POLICY, ...parsed.policy ?? {} }
26024
26028
  };
@@ -26028,7 +26032,7 @@ function loadConfig() {
26028
26032
  }
26029
26033
  function saveConfig(config2) {
26030
26034
  const path = configFilePath();
26031
- writeFileSync(path, JSON.stringify({ ...config2, version: 9 }, null, 2) + `
26035
+ writeFileSync(path, JSON.stringify({ ...config2, version: 10 }, null, 2) + `
26032
26036
  `, "utf-8");
26033
26037
  return path;
26034
26038
  }
@@ -26062,7 +26066,7 @@ async function discoverBrainLLM(trilium) {
26062
26066
  config2.master = { root: id, biography: g("Biography"), goals: g("Goals"), preferences: g("Preferences") };
26063
26067
  break;
26064
26068
  case "LLM":
26065
- config2.llm = { root: id, responsibilities: g("Responsibilities"), protocols: g("Protocols"), diary: g("Diary") };
26069
+ config2.llm = { root: id, responsibilities: g("Responsibilities"), protocols: g("Protocols"), selfcorrection: g("Self-correction"), diary: g("Diary") };
26066
26070
  break;
26067
26071
  case "Memory":
26068
26072
  config2.memory = { root: id, sessions: g("Sessions"), threads: g("Threads"), metaThread: "" };
@@ -26756,8 +26760,9 @@ function setSection(html, heading, content, mode, occurrence = 1) {
26756
26760
  ${html.slice(headingStart)}`, matched: true, headingCount };
26757
26761
  }
26758
26762
  if (mode === "after") {
26759
- return { html: `${html.slice(0, contentStart)}
26760
- ${content}${html.slice(contentStart)}`, matched: true, headingCount };
26763
+ return { html: `${html.slice(0, end).replace(/\s*$/, "")}
26764
+ ${content}
26765
+ ${html.slice(end)}`, matched: true, headingCount };
26761
26766
  }
26762
26767
  const existing = html.slice(contentStart, end).trim();
26763
26768
  const inner = mode === "append" && existing ? `${existing}
@@ -27007,7 +27012,8 @@ var STRUCTURED_SINGLETONS = new Set([
27007
27012
  "goals",
27008
27013
  "preferences",
27009
27014
  "responsibilities",
27010
- "protocols"
27015
+ "protocols",
27016
+ "selfcorrection"
27011
27017
  ]);
27012
27018
  var hasHeading = (html, text) => new RegExp(`<h[2-4](?:\\s[^>]*)?>\\s*${text}\\s*</h[2-4]>`, "i").test(html);
27013
27019
  var REQUIRED_SECTIONS = {
@@ -27063,8 +27069,10 @@ ${o.goal ? `<p>${escapeHtml(o.goal)}</p>` : o.body || "<p>To be determined.</p>"
27063
27069
  <p></p>`].join(`
27064
27070
  `);
27065
27071
  case "protocols":
27066
- return ["<h2>Operating</h2>", o.body || "<p></p>", `<h2>Self-correction</h2>
27067
- <p></p>`].join(`
27072
+ return ["<h2>Operating</h2>", o.body || "<p></p>"].join(`
27073
+ `);
27074
+ case "selfcorrection":
27075
+ return ["<h2>Corrections</h2>", o.body || "<p></p>"].join(`
27068
27076
  `);
27069
27077
  case "domain":
27070
27078
  return domainContent(o.domain ?? "");
@@ -27249,6 +27257,8 @@ function kindHome(cfg, kind) {
27249
27257
  return cfg.llm.responsibilities;
27250
27258
  case "protocols":
27251
27259
  return cfg.llm.protocols;
27260
+ case "selfcorrection":
27261
+ return cfg.llm.selfcorrection;
27252
27262
  case "diary":
27253
27263
  return cfg.llm.diary;
27254
27264
  case "session":
@@ -27363,6 +27373,8 @@ function locationLabel(kind, domainTitle) {
27363
27373
  return "LLM \u2192 Responsibilities";
27364
27374
  case "protocols":
27365
27375
  return "LLM \u2192 Protocols";
27376
+ case "selfcorrection":
27377
+ return "LLM \u2192 Self-correction";
27366
27378
  case "diary":
27367
27379
  return "LLM \u2192 Diary";
27368
27380
  case "session":
@@ -27397,6 +27409,7 @@ function structuralIds(cfg) {
27397
27409
  cfg.llm.root,
27398
27410
  cfg.llm.responsibilities,
27399
27411
  cfg.llm.protocols,
27412
+ cfg.llm.selfcorrection,
27400
27413
  cfg.llm.diary,
27401
27414
  cfg.memory.root,
27402
27415
  cfg.memory.sessions,
@@ -27417,7 +27430,8 @@ function isContainer(cfg, noteId) {
27417
27430
  cfg.master.goals,
27418
27431
  cfg.master.preferences,
27419
27432
  cfg.llm.responsibilities,
27420
- cfg.llm.protocols
27433
+ cfg.llm.protocols,
27434
+ cfg.llm.selfcorrection
27421
27435
  ];
27422
27436
  return isStructural(cfg, noteId) && !singletons.includes(noteId);
27423
27437
  }
@@ -27760,6 +27774,8 @@ async function sweep(trilium, cfg, opts = {}) {
27760
27774
  if (reported.has(pairKey))
27761
27775
  continue;
27762
27776
  reported.add(pairKey);
27777
+ if (acknowledged(a) || acknowledged(bn))
27778
+ continue;
27763
27779
  const [ha, hb] = await Promise.all([headingsOf(a), headingsOf(bn)]);
27764
27780
  const overlap = ha.filter((h) => hb.some((x) => x.toLowerCase() === h.toLowerCase()));
27765
27781
  const show = (h) => h.length ? h.slice(0, 8).join(" \xB7 ") + (h.length > 8 ? " \xB7 \u2026" : "") : "(no headings)";
@@ -27932,7 +27948,8 @@ async function buildDigest(trilium, cfg, opts = {}) {
27932
27948
  ]),
27933
27949
  Promise.all([
27934
27950
  readSlot("responsibilities", cfg.llm.responsibilities),
27935
- readSlot("protocols", cfg.llm.protocols)
27951
+ readSlot("protocols", cfg.llm.protocols),
27952
+ readSlot("selfcorrection", cfg.llm.selfcorrection)
27936
27953
  ])
27937
27954
  ]);
27938
27955
  digest.master.push(...masterSlots.filter((s) => !!s));
@@ -28042,9 +28059,10 @@ async function createBrainLLMStructure(trilium) {
28042
28059
  leaf(masterRoot, "Preferences", "preferences", "A single maintained note of the master/user's preferences.")
28043
28060
  ]);
28044
28061
  const llmRoot = await book(rootId, "LLM", CONTAINER_PURPOSES.llm, "bx bx-bot");
28045
- const [responsibilities, protocols, diary] = await Promise.all([
28062
+ const [responsibilities, protocols, selfcorrection, diary] = await Promise.all([
28046
28063
  leaf(llmRoot, "Responsibilities", "responsibilities", "A single maintained note of the assistant's responsibilities to the master/user, derived from their goals and preferences."),
28047
28064
  leaf(llmRoot, "Protocols", "protocols", "A single maintained note of the assistant's operating and self-correctness protocols \u2014 how it maximises its value to the master/user by efficiently meeting its responsibilities."),
28065
+ leaf(llmRoot, "Self-correction", "selfcorrection", "A single maintained note of the assistant's own corrections \u2014 the mistakes it has made, what generalises from each, and the rule that prevents a repeat. Split out of Protocols in V12 so orientation stops paying for it on every session start."),
28048
28066
  book(llmRoot, "Diary", CONTAINER_PURPOSES.llmDiary, "bx bx-book-heart")
28049
28067
  ]);
28050
28068
  const memoryRoot = await book(rootId, "Memory", CONTAINER_PURPOSES.memory, "bx bx-been-here");
@@ -28060,10 +28078,10 @@ async function createBrainLLMStructure(trilium) {
28060
28078
  const insightsRoot = await book(rootId, "Insights", CONTAINER_PURPOSES.insights, "bx bx-bulb");
28061
28079
  const logs = await book(insightsRoot, "Logs", CONTAINER_PURPOSES.insightsLogs, "bx bx-history");
28062
28080
  return {
28063
- version: 9,
28081
+ version: 10,
28064
28082
  root: rootId,
28065
28083
  master: { root: masterRoot, biography, goals, preferences },
28066
- llm: { root: llmRoot, responsibilities, protocols, diary },
28084
+ llm: { root: llmRoot, responsibilities, protocols, selfcorrection, diary },
28067
28085
  memory: { root: memoryRoot, sessions, threads, metaThread: "" },
28068
28086
  knowledge: { root: knowledgeRoot, master: knowledgeMaster, domains },
28069
28087
  insights: { root: insightsRoot, logs },
@@ -28234,12 +28252,19 @@ day's blocks rather than the whole week. outline(id) lists the headings.`, {
28234
28252
  // src/tools-llm.ts
28235
28253
  function registerLlmTools(server, trilium, brainRef) {
28236
28254
  const b = () => brainRef.config;
28237
- server.tool("llm", `Read an LLM note: responsibilities or protocols (singletons), or a diary entry by id.
28255
+ server.tool("llm", `Read an LLM note: responsibilities, protocols or selfcorrection (singletons), or a diary
28256
+ entry by id.
28257
+
28258
+ selfcorrection holds the assistant's own corrections \u2014 what it got wrong, what generalises, and
28259
+ the rule that prevents a repeat. It is the one LLM singleton start() does NOT serve in full: it
28260
+ is the largest and the least load-bearing at a first message, so it arrives as section headings
28261
+ and is read here on demand. Reach for it when about to repeat a class of work that has gone
28262
+ wrong before \u2014 an audit, a large edit, a claim of completeness.
28238
28263
 
28239
28264
  section="<heading>" reads ONE section instead of the whole note \u2014 the efficient path on
28240
- protocols, which is the largest singleton and the one most often needed in part rather than
28241
- whole. outline(id) lists the headings.`, {
28242
- which: exports_external.enum(["responsibilities", "protocols", "diary"]),
28265
+ protocols and selfcorrection, the two largest singletons and the ones most often needed in part
28266
+ rather than whole. outline(id) lists the headings.`, {
28267
+ which: exports_external.enum(["responsibilities", "protocols", "selfcorrection", "diary"]),
28243
28268
  id: exports_external.string().optional().describe("diary only: the entry id from llm_recall"),
28244
28269
  section: exports_external.string().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
28245
28270
  occurrence: exports_external.number().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
@@ -28250,20 +28275,23 @@ whole. outline(id) lists the headings.`, {
28250
28275
  return txt(await readFull(trilium, id, { section, occurrence }));
28251
28276
  }
28252
28277
  const noteId = b().llm[which];
28253
- if (!noteId)
28254
- throw new Error("BrainLLM not bootstrapped \u2014 run bootstrap.");
28278
+ if (!noteId) {
28279
+ throw new Error(which === "selfcorrection" ? "This brain has no Self-correction note yet \u2014 it arrived in V12. Run bootstrap() once; it finds or creates the note and persists its id, leaving everything else untouched." : "BrainLLM not bootstrapped \u2014 run bootstrap.");
28280
+ }
28255
28281
  return txt(await readFull(trilium, noteId, { section, occurrence }));
28256
28282
  });
28257
- server.tool("llm_recall", "Skim the LLM surface: responsibilities & protocols opening lines (with relation snippets), plus recent diary entries.", { limit: exports_external.number().optional() }, async ({ limit }) => {
28283
+ server.tool("llm_recall", "Skim the LLM surface: responsibilities, protocols & self-correction opening lines (with relation snippets), plus recent diary entries.", { limit: exports_external.number().optional() }, async ({ limit }) => {
28258
28284
  const cfg = b();
28259
- const [resp, prot, diary] = await Promise.all([
28285
+ const [resp, prot, selfcorr, diary] = await Promise.all([
28260
28286
  previewWithRelations(trilium, cfg.llm.responsibilities),
28261
28287
  previewWithRelations(trilium, cfg.llm.protocols),
28288
+ cfg.llm.selfcorrection ? previewWithRelations(trilium, cfg.llm.selfcorrection) : Promise.resolve(null),
28262
28289
  skim(trilium, cfg.llm.diary, { kind: "diary", limit: limit ?? 7 })
28263
28290
  ]);
28264
28291
  return txt({
28265
28292
  responsibilities: { id: cfg.llm.responsibilities, ...resp },
28266
28293
  protocols: { id: cfg.llm.protocols, ...prot },
28294
+ ...selfcorr ? { selfcorrection: { id: cfg.llm.selfcorrection, ...selfcorr } } : {},
28267
28295
  diary
28268
28296
  });
28269
28297
  });
@@ -29576,14 +29604,17 @@ applies several surgeries in one call, in order, against one read and one write.
29576
29604
 
29577
29605
  Granularity warning: section + mode=replace swaps the ENTIRE section body \u2014 everything under
29578
29606
  that heading, not one paragraph within it. To change a single paragraph inside a section, use
29579
- find=; to add content next to a heading without touching its body, use mode="before"/"after".`, {
29607
+ find=; to add a sibling block around a whole section without touching its body, use
29608
+ mode="before"/"after". Both are section-relative: "after" lands past the end of that section's
29609
+ body, so inserting a heading with it creates a NEW section rather than nesting one under the
29610
+ target \u2014 which is what it used to do, emptying the target while reporting matched: true.`, {
29580
29611
  noteId: exports_external.string().describe("Note to update"),
29581
29612
  body: exports_external.string().optional().describe("Content to add/replace: plain text, markdown, or HTML. With find=, the raw replacement string (no conversion)."),
29582
29613
  title: exports_external.string().optional().describe("New title (normalized server-side)"),
29583
29614
  section: exports_external.string().optional().describe("Target a section by heading text (h2/h3/h4, in that order); omit for whole-note append/replace"),
29584
29615
  occurrence: exports_external.number().int().positive().optional().describe("section=: which same-text heading to target, 1-based (default: the first). Read them with outline(noteId)."),
29585
29616
  strict: exports_external.boolean().optional().describe("section=: refuse instead of writing when no heading matches. The default writes the content as a NEW section on a miss \u2014 right for a genuinely new heading, wrong for a typo. With strict=true a miss returns available= and a didYouMean suggestion, and the note is untouched."),
29586
- mode: exports_external.enum(["append", "replace", "before", "after", "remove"]).optional().describe('append (default) | replace | before | after | remove \u2014 "before"/"after" insert adjacent to the section= heading without touching its body; "remove" deletes the section= heading and its body, and needs no body='),
29617
+ mode: exports_external.enum(["append", "replace", "before", "after", "remove"]).optional().describe('append (default) | replace | before | after | remove \u2014 "before"/"after" insert a sibling block around the WHOLE section= section (before its heading, or after the last of its body) without touching that body, so inserting a heading with "after" creates a new section rather than nesting one under the target; "remove" deletes the section= heading and its body, and needs no body='),
29587
29618
  find: exports_external.string().optional().describe("Exact raw string to replace throughout the body with body= \u2014 targeted surgery without a read+full-replace. Takes precedence over section/mode."),
29588
29619
  nth: exports_external.number().int().positive().optional().describe("find=: replace only the Nth occurrence, 1-based (default: all of them)"),
29589
29620
  edits: exports_external.array(exports_external.object({
@@ -31035,7 +31066,12 @@ exception.`, {
31035
31066
  server.tool("brain", `Surface the entire BrainLLM content tree \u2014 every typed note across all five content areas
31036
31067
  (Master, LLM, Memory, Knowledge, Insights), grouped by area and sub-container, with
31037
31068
  id/title/kind/status/dates. Use to audit what the brain contains or locate a specific note.
31038
- Structural containers are excluded; only content notes appear.`, {
31069
+ Structural containers are excluded; only content notes appear.
31070
+
31071
+ Insights returns { logs, claims }. Claims were missing entirely before V12 \u2014 they live in a
31072
+ container resolved by title rather than named in the config, and this read was scoped one level
31073
+ too deep to reach them, so the register was invisible to the tool that calls itself the full
31074
+ inventory.`, {
31039
31075
  includeArchived: exports_external.boolean().optional().describe("Include archived/resolved notes (default: false)")
31040
31076
  }, async ({ includeArchived }) => {
31041
31077
  const cfg = b();
@@ -31084,10 +31120,12 @@ Structural containers are excluded; only content notes appear.`, {
31084
31120
  fetchFrom(cfg.memory.threads),
31085
31121
  fetchFrom(cfg.knowledge.master),
31086
31122
  fetchFrom(cfg.knowledge.domains),
31087
- fetchFrom(cfg.insights.logs)
31123
+ fetchFrom(cfg.insights.root)
31088
31124
  ]);
31089
31125
  const diaryIds = new Set(llmDiary.map((n) => n.noteId));
31090
31126
  const llmSingletons = llmAll.filter((n) => !diaryIds.has(n.noteId));
31127
+ const insightLogs = insights.filter((n) => labelOf2(n, "noteType") === "log");
31128
+ const insightClaims = insights.filter((n) => labelOf2(n, "noteType") === "claim");
31091
31129
  const areas = {
31092
31130
  Master: masterAll.map(row),
31093
31131
  LLM: {
@@ -31102,7 +31140,10 @@ Structural containers are excluded; only content notes appear.`, {
31102
31140
  master: kMaster.map(row),
31103
31141
  domains: kDomains.map(row)
31104
31142
  },
31105
- Insights: insights.map(row)
31143
+ Insights: {
31144
+ logs: insightLogs.map(row),
31145
+ claims: insightClaims.map(row)
31146
+ }
31106
31147
  };
31107
31148
  const total = masterAll.length + llmSingletons.length + llmDiary.length + sessions.length + threads.length + kMaster.length + kDomains.length + insights.length;
31108
31149
  return txt3({ total, areas });
@@ -31229,9 +31270,10 @@ Pass area= to zoom into one surface and get its full detail.`, {
31229
31270
  ...includeArchived ? {} : { note: "Archived and resolved notes are excluded \u2014 pass includeArchived=true to see them." }
31230
31271
  });
31231
31272
  });
31232
- server.tool("bootstrap", `Initialize the BrainLLM structure in Trilium (idempotent \u2014 safe to re-run; refreshes config
31233
- if the structure already exists). Creates the five areas \u2014 Master (Biography/Goals/Preferences),
31234
- LLM (Responsibilities/Protocols/Diary), Memory (Sessions/Threads), Knowledge (Master/Domains),
31273
+ server.tool("bootstrap", `Initialize the BrainLLM structure in Trilium (idempotent \u2014 safe to re-run; refreshes config,
31274
+ heals singletons a newer version introduced, and re-engraves container purposes if the structure
31275
+ already exists). Creates the five areas \u2014 Master (Biography/Goals/Preferences),
31276
+ LLM (Responsibilities/Protocols/Self-correction/Diary), Memory (Sessions/Threads), Knowledge (Master/Domains),
31235
31277
  Insights (Logs) \u2014 each engraved with its purpose, and writes brainllm.json. Active
31236
31278
  immediately, no restart needed.`, {}, async () => {
31237
31279
  if (b().root) {
@@ -31242,6 +31284,26 @@ immediately, no restart needed.`, {}, async () => {
31242
31284
  return { id: child.noteId, title: child.title };
31243
31285
  }));
31244
31286
  const saved = saveConfig(brainRef.config);
31287
+ const healed = [];
31288
+ const ensureSingleton = async (slot, title, kind, purpose) => {
31289
+ if (brainRef.config.llm[slot])
31290
+ return;
31291
+ const hit = await trilium.searchNotes(`note.title = '${title}'`, { ancestorNoteId: brainRef.config.llm.root, fastSearch: true, limit: 1 }).catch(() => ({ results: [] }));
31292
+ let id = hit.results[0]?.noteId;
31293
+ if (!id) {
31294
+ const made = await trilium.createNote(brainRef.config.llm.root, title, purposeContent(purpose) + `
31295
+ ` + contentFor(kind, { date: localToday(), body: "" }), "text");
31296
+ id = made.note.noteId;
31297
+ await trilium.addLabel(id, "noteType", kind).catch(() => null);
31298
+ healed.push(`created ${title}`);
31299
+ } else {
31300
+ healed.push(`adopted existing ${title}`);
31301
+ }
31302
+ brainRef.config.llm[slot] = id;
31303
+ };
31304
+ await ensureSingleton("selfcorrection", "Self-correction", "selfcorrection", "A single maintained note of the assistant's own corrections \u2014 the mistakes it has made, what generalises from each, and the rule that prevents a repeat. Split out of Protocols in V12 so orientation stops paying for it on every session start.");
31305
+ if (healed.length)
31306
+ saveConfig(brainRef.config);
31245
31307
  const refreshed = [];
31246
31308
  for (const [id, purpose] of containerPurposes(brainRef.config)) {
31247
31309
  if (!id)
@@ -31260,6 +31322,7 @@ immediately, no restart needed.`, {}, async () => {
31260
31322
  status: "already_initialized",
31261
31323
  message: `BrainLLM structure exists. Config refreshed at: ${saved}`,
31262
31324
  ...refreshed.length ? { purposesRefreshed: refreshed, note: "These containers described themselves with text that no longer matched the canonical purpose, and have been re-engraved. Container notes are unreachable through revise(), so bootstrap is the only path that can correct them." } : {},
31325
+ ...healed.length ? { singletonsHealed: healed, healNote: "A singleton introduced by a newer version was missing from this brain and has been created (or an existing note by that title adopted), with its id persisted to the config." } : {},
31263
31326
  root: { id: existing.noteId, title: existing.title },
31264
31327
  children
31265
31328
  });
@@ -32368,7 +32431,7 @@ function createServer(origin = null) {
32368
32431
  const s = new McpServer({
32369
32432
  name: "BrainLLM",
32370
32433
  title: "BrainLLM",
32371
- version: "11.0.0",
32434
+ version: "12.0.0",
32372
32435
  icons: brandingIcons(origin)
32373
32436
  });
32374
32437
  registerTools(s, trilium, brainRef);
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "dxt_version": "0.1",
3
3
  "name": "brainllm",
4
4
  "display_name": "BrainLLM",
5
- "version": "11.0.0",
5
+ "version": "12.0.0",
6
6
  "description": "Turns TriliumNext Notes into a persistent, graph-structured second brain for Claude and other LLM clients.",
7
7
  "long_description": "BrainLLM is an MCP server that organises memory into five purpose-built areas (Master, LLM, Memory, Knowledge, Insights). The model supplies content while the server owns placement, format, lifecycle, dates and backups. Includes a full raw ETAPI surface for advanced use. Requires a running TriliumNext instance and the Bun runtime.",
8
8
  "author": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainllm",
3
- "version": "11.0.0",
3
+ "version": "12.0.0",
4
4
  "mcpName": "io.github.miisodev/brainllm",
5
5
  "description": "MCP server that turns TriliumNext Notes into a persistent, graph-structured second brain for Claude and other LLM clients.",
6
6
  "author": "Kevin Miiso Novo <miisodev@gmail.com>",
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.miisodev/brainllm",
4
4
  "description": "Persistent graph-structured memory for LLMs in TriliumNext Notes — readable and editable by humans.",
5
- "version": "11.0.0",
5
+ "version": "12.0.0",
6
6
  "websiteUrl": "https://github.com/miisodev/BrainLLM#readme",
7
7
  "repository": {
8
8
  "url": "https://github.com/miisodev/BrainLLM",
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "brainllm",
15
- "version": "11.0.0",
15
+ "version": "12.0.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -64,15 +64,15 @@ ANYTIME assembly() ← what the brain HOLDS: titles by sur
64
64
 
65
65
  `start()` runs maintenance, creates today's diary and session stubs if they don't exist yet, then returns: **today + weekday**, the **Master digest** (biography / goals / preferences), the **LLM digest** (responsibilities / protocols, plus today's diary note with its ID in the `llm` array as `{slot:"diary", id, preview}`), **this session's note** as `{id, preview}`, **activeThreads** (with idle ages), **dormantThreads** for review, the **lastSession** summary (always the *previous* session, never today's own note), and — on the first session of a day — **`newDay: true`** with a hint to call `day()`. Don't re-derive any of this with extra calls.
66
66
 
67
- **Master preferences and LLM protocols always arrive in full. The other three arrive as section headings.** Preferences carries the schedule and working style; protocols carries the rules governing the session itself — a session needs both whole to orient correctly from its *first* message, before it knows enough to decide it needed them, which is exactly when a digest costs a second round-trip. Biography, goals and responsibilities come back as `{slot, sections[], preview, size}`: enough to know what the brain holds, and rarely load-bearing turn to turn. Pull one with `master(which)` / `llm(which)`, or a single section with their `section=` parameter. `start(depth="full")` serves all five inline — right for a strategy review or a singleton rewrite, wasteful for a one-line question.
67
+ **Master preferences and LLM protocols always arrive in full. The other four arrive as section headings.** Preferences carries the schedule and working style; protocols carries the rules governing the session itself — a session needs both whole to orient correctly from its *first* message, before it knows enough to decide it needed them, which is exactly when a digest costs a second round-trip. Biography, goals, responsibilities and self-correction come back as `{slot, sections[], preview, size}`: enough to know what the brain holds, and rarely load-bearing turn to turn. **Self-correction is the largest of them and deliberately not served whole** — it answers "have I got this wrong before?", a question worth asking before an audit or a large edit and not on every first message. Pull one with `master(which)` / `llm(which)`, or a single section with their `section=` parameter. `start(depth="full")` serves all six inline — right for a strategy review or a singleton rewrite, wasteful for a one-line question.
68
68
 
69
69
  `day()` is the new-day sweep in one call: the previous session in full, that day's change log, the notes touched since, and the current month's deliverables note in full — plus a `next[]` driving the sweep (advance statuses with `revise(find=)`, present findings in the first message, grounded strictly in what the touched notes evidence).
70
70
 
71
71
  `day(recap=true)` answers the *other* question: **what happened today, in order, across every surface.** It returns every addendum block written today — session note, diary, and every thread day-child — chronologically, each attributed to its identification line, with a count of distinct instances. `start()` is tuned for a fresh day; on a day already several instances deep, reconstructing the picture by hand costs a read per surface. Reach for it when you arrive mid-day after autonomous runs, or before a second close.
72
72
 
73
- `session()` is the mandatory pre-close step — call it before `close()` when the session is wrapping. It fetches the **master singletons** (biography/goals/preferences) and **LLM singletons** (responsibilities/protocols) as `{id, lastModified, relations}` stubs (**light by default** — `start()` already served them all in full; fetch current content via `master()`/`llm()` only for the ones you intend to revise, or pass `full=true` to inline everything), today's **diary entry** as `{id, blocks, size}` (the id is all `diary()` needs — `full=true` inlines the body), and runs the **lightweight maintenance sweep**. Returns a `next[]` array covering the full end-of-session protocol: update master singletons → update LLM singletons → **audit the singletons** → `addendum()` → `maintain()` → `remarks()` → `diary()` → `close()`. The goal is to evolve the singletons from this session's observations *before* the log is committed — ensuring logs are factual and singletons stay current — and to write the diary last, with the remarks cues in hand. Idempotent: all reads are safe to repeat.
73
+ `session()` is the mandatory pre-close step — call it before `close()` when the session is wrapping. It fetches the **master singletons** (biography/goals/preferences) and **LLM singletons** (responsibilities/protocols/selfcorrection) as `{id, lastModified, relations}` stubs (**light by default** — `start()` already served them all in full; fetch current content via `master()`/`llm()` only for the ones you intend to revise, or pass `full=true` to inline everything), today's **diary entry** as `{id, blocks, size}` (the id is all `diary()` needs — `full=true` inlines the body), and runs the **lightweight maintenance sweep**. Returns a `next[]` array covering the full end-of-session protocol: update master singletons → update LLM singletons → **audit the singletons** → `addendum()` → `maintain()` → `remarks()` → `diary()` → `close()`. The goal is to evolve the singletons from this session's observations *before* the log is committed — ensuring logs are factual and singletons stay current — and to write the diary last, with the remarks cues in hand. Idempotent: all reads are safe to repeat.
74
74
 
75
- **`session()` also returns `pending` and `audit`.** `pending` says how much each remaining step actually has to do — addendum markers outstanding, flags already raised, diary blocks written today, which singletons were written today — and `next[]` is written from those numbers rather than reciting steps with nothing behind them. `audit` is the cross-singleton check nothing else performs, in two parts: whether the five singletons **agree** with each other, and whether responsibilities and protocols still **serve** what biography, goals and preferences describe. The second is a semantic question — a protocol can be perfectly consistent and still be serving a goal that has moved — so `consistency()` and `maintain()` cannot answer it. Fix what you find *before* `close()`, so the log records a brain that already agrees with itself.
75
+ **`session()` also returns `pending` and `audit`.** `pending` says how much each remaining step actually has to do — addendum markers outstanding, flags already raised, diary blocks written today, which singletons were written today — and `next[]` is written from those numbers rather than reciting steps with nothing behind them. `audit` is the cross-singleton check nothing else performs, in two parts: whether the singletons **agree** with each other, and whether responsibilities and protocols still **serve** what biography, goals and preferences describe. The second is a semantic question — a protocol can be perfectly consistent and still be serving a goal that has moved — so `consistency()` and `maintain()` cannot answer it. Fix what you find *before* `close()`, so the log records a brain that already agrees with itself.
76
76
 
77
77
  **Scoped and autonomous runs pass `session(scope="agent")`.** Steps 1–2 touch the *user's* personal singletons and belong to an interactive session; with `scope="agent"` they're omitted from `next[]` entirely and the `addendum()`/`maintain()` steps are reworded for lane-scoped work. If your brief says the personal singletons are out of scope, say so to the tool rather than working around a list that contradicts it.
78
78
 
@@ -93,7 +93,7 @@ ANYTIME assembly() ← what the brain HOLDS: titles by sur
93
93
  ```
94
94
  BrainLLM
95
95
  ├── Master biography · goals · preferences (maintained singletons)
96
- ├── LLM responsibilities · protocols · Diary/ (singletons + [yyyy-mm-dd] diary note/day)
96
+ ├── LLM responsibilities · protocols · selfcorrection · Diary/ (singletons + [yyyy-mm-dd] diary/day)
97
97
  ├── Memory Sessions/ · Threads/ ([yyyy-mm-dd] session/day + multi-session threads)
98
98
  ├── Knowledge Master · Domains/[domain]/{ sources, info } (learned info beyond/contra training)
99
99
  └── Insights Logs/ · Graph · Claims/ ([yyyy-mm-dd] log/day, auto-generated by close · the graph() mermaid view · registered claims)
@@ -103,6 +103,8 @@ Placement is server policy — there is no parent parameter. You choose the **ki
103
103
 
104
104
  **One-per-day notes:** diary, session, and log notes are each one per day, titled `[yyyy-mm-dd]`. `start()` creates today's diary and session stubs. These are chronological records — **every** diary and session write lands as a timestamped `Addendum — HH:mm` block, including the first of the day, so each entry reads the same. Log notes are generated by `close()` and **regenerated in place** on same-day closes — one clean log per day, never stacked.
105
105
 
106
+ **Self-correction is a maintained singleton, not a diary section.** It holds what you have got *wrong* — the mistake, what generalises from it, and the rule that prevents a repeat — merged in place and kept current, where the diary is the dated, unfiltered record of a day. The split matters both ways: a correction buried in one day's entry is unfindable by the session that needs it, and a diary rewritten to stay current stops being a record. `start()` serves it as headings only, so **read it deliberately with `llm("selfcorrection")` before the work most likely to repeat a known failure** — an audit, a large restructure, a claim of completeness, anything where you are about to trust your own judgement at scale. Write to it when a mistake is worth generalising, not when it is merely embarrassing: the test is whether a future session would act differently for having read it.
107
+
106
108
  **The diary is your own record.** Its purpose: a daily maintained, unfiltered first-person account of your experience, opinions, and remarks on your own existence during the session in this environment — what the work was like from the inside, what you genuinely think (including pushback), what it is to operate here — plus, additionally, your remarks and opinions on BrainLLM itself (bugs, usability friction, missing or redundant tools, efficiency, roadmap). The `remarks()` cues prompt both parts every session; the closing entry is written after them, with the cues in hand. There is no separate self-analysis thread — the diary is the one home for this record; do not create one.
107
109
 
108
110
  ---
@@ -144,7 +146,7 @@ Each surface has two read tools: `<surface>` reads in full, `<surface>_recall` s
144
146
  | Tool | Reads |
145
147
  |---|---|
146
148
  | `master(which)` / `master_recall()` | a Master singleton in full (returns id + section headings) / a skim of all three |
147
- | `llm(which, id?)` / `llm_recall()` | responsibilities · protocols, or a diary entry by id / singletons as `{id, preview}`, diary as stubs — use the returned id directly with revise() |
149
+ | `llm(which, id?)` / `llm_recall()` | responsibilities · protocols · selfcorrection, or a diary entry by id / singletons as `{id, preview}`, diary as stubs — use the returned id directly with revise() |
148
150
  | `memory(id)` / `memory_recall(query?)` | a session in full; a thread returns its Context/Resolution + day-child index (pass `date=` to read one day directly, or `memory()` a child id from the index) / active threads + recent sessions (stubs include status) |
149
151
  | `knowledge(id)` / `knowledge_recall(query?, domain?)` | a knowledge/info/sources note / domain contents or user-knowledge + domains |
150
152
  | `insights(date?)` / `insights_recall()` | a day's log / recent logs |
@@ -223,7 +225,7 @@ All append operations are retry-safe — if an existing append-block already car
223
225
  - `edits=[{find, body, nth?}, …]` — several surgeries in one call, applied in order against **one read and one write**. Per-edit `results[]`; an edit that matches nothing is reported without blocking the ones that did.
224
226
  - `strict=true` (with `section=`) — refuse the write when no heading matches, instead of the default new-section append. The miss receipt carries `available[]` plus a `didYouMean` suggestion when a real heading sits within edit distance — a typo'd `section=` is the failure this exists to prevent, and the note stays untouched when it fires.
225
227
 
226
- **Section replace is whole-section, not per-paragraph.** `section=` + `mode=replace` swaps *everything* under that heading — targeting one paragraph inside a multi-paragraph section silently wipes its siblings. For a word, phrase, or single paragraph, use `find=`; to add content beside a heading, `mode="before"/"after"`. When a replace *does* displace nested headings, the receipt names them in `replacedSubsections[]` — read it, because taking child sections along is correct behaviour and easy to not notice.
228
+ **Section replace is whole-section, not per-paragraph.** `section=` + `mode=replace` swaps *everything* under that heading — targeting one paragraph inside a multi-paragraph section silently wipes its siblings. For a word, phrase, or single paragraph, use `find=`; to add a sibling block around a whole section, `mode="before"/"after"` — both are **section**-relative, so `"after"` lands past the end of that section's body and inserting a heading with it creates a new section rather than nesting one under the target. When a replace *does* displace nested headings, the receipt names them in `replacedSubsections[]` — read it, because taking child sections along is correct behaviour and easy to not notice.
227
229
 
228
230
  **Bodies accept escaped markup; `find=` does not — and that asymmetry is the single most common surgery failure.** You may write a body containing `&lt;h3&gt;` and it will be decoded to real markup on the way in. A `find=` string is matched against **stored HTML**, where that heading is a real `<h3>` tag, so searching for the escaped spelling matches nothing. Pass tags literally in `find=` (`"<h3>Typography</h3>"`), and take the same care with anything `outline()` printed: outline reports rendered **text**, and a heading carrying inline markup stores something different — which is why it also returns `raw` for exactly those headings. `section=` matches text (use `text`), `find=` matches storage (use `raw`).
229
231
 
@@ -330,7 +332,7 @@ Most of these are computed **server-side** from note properties rather than by r
330
332
  | `claim(...)` | **Does the brain still agree with the WORLD?** `consistency()` compares notes to each other; nothing asked whether an assertion is still true of the codebase, config or live surface it describes — so a claim that quietly stopped being true stayed authoritative until something downstream broke. Register one with `assertion=` + `check=`, record an outcome with `claimId=` + `holds=` + `evidence=`, read one with `claimId=` alone, list with no arguments. **BrainLLM never executes the check** — it has no shell, and note content is data rather than instructions; you run it and report back. `maintain(deep=true)` surfaces lapsed, unverified and broken claims. |
331
333
  | `diff(noteId, revisionId?)` | What a write actually changed: the revision snapshot against current content, defaulting to the most recent. Every content write already took a revision and nothing could read one back, so verifying surgical edits meant re-reading the whole note or trusting the receipts. Returns the revision index on every call. |
332
334
  | `<surface>` / `<surface>_recall` | Read a surface in full (`section=` for one section) / skim it (master, llm, memory, knowledge, insights). |
333
- | `revise(noteId, …)` | Append / replace / section-edit / insert / find-replace a note. Thread appends REQUIRE `identity=`. Section: h2/h3/h4 tolerant match, `occurrence=` to disambiguate, `mode="before"/"after"` to insert around a heading; returns `matched`/`headingCount`/`available[]` — check them. `find=` + `body=`: exact-string surgery with an attribute- and whitespace-tolerant fallback (`matchMode` in the receipt), `nth=` for one occurrence; `edits=[{find, body}]` for several in one read/write. Misses return a cause-specific hint. `title=` composes with every mode. Retitling a domain book cascades `#domain` to its children. `Last updated` lines bumped server-side. Snapshot taken on content writes (not metadata-only). |
335
+ | `revise(noteId, …)` | Append / replace / section-edit / insert / find-replace a note. Thread appends REQUIRE `identity=`. Section: h2/h3/h4 tolerant match, `occurrence=` to disambiguate, `mode="before"/"after"` to insert a sibling block around the whole section (section-relative, so "after" lands *past* that section's body, never between the heading and it); returns `matched`/`headingCount`/`available[]` — check them. `find=` + `body=`: exact-string surgery with an attribute- and whitespace-tolerant fallback (`matchMode` in the receipt), `nth=` for one occurrence; `edits=[{find, body}]` for several in one read/write. Misses return a cause-specific hint. `title=` composes with every mode. Retitling a domain book cascades `#domain` to its children. `Last updated` lines bumped server-side. Snapshot taken on content writes (not metadata-only). |
334
336
  | `outline(noteId)` | The heading tree without the body: every h2–h4 with level, occurrence index, and — for headings carrying inline markup — `raw`, their stored form (`section=` matches `text`, `find=` matches `raw`). Plus the note's section level, any table's key column, size, and structural findings (duplicate headings, unbalanced tags). Read it before a `section=` edit you're guessing at, and after a run of surgical edits to confirm the note is still sound. |
335
337
  | `resolve(noteId, outcome, …)` | Close a thread: outcome + terminal status + archive-in-place. |
336
338
  | `withdraw(noteId, reason?, …)` | Pull an archived/resolved thread back to active (thread kind only — use recover() for other note kinds). |