brainllm 11.0.0 → 12.1.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.1.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,8 +25451,12 @@ var SingletonKinds = [
25449
25451
  "goals",
25450
25452
  "preferences",
25451
25453
  "responsibilities",
25452
- "protocols"
25454
+ "protocols",
25455
+ "selfcorrection"
25453
25456
  ];
25457
+ function isSingletonKind(k) {
25458
+ return SingletonKinds.includes(k);
25459
+ }
25454
25460
  var Statuses = ["active", "resolved", "superseded", "dormant", "eternal"];
25455
25461
  var RelationTypes = [
25456
25462
  "relatesTo",
@@ -25972,10 +25978,10 @@ class TriliumClient {
25972
25978
  import { existsSync, readFileSync, writeFileSync } from "fs";
25973
25979
  import { join, dirname } from "path";
25974
25980
  var EMPTY_BRAINLLM = {
25975
- version: 9,
25981
+ version: 10,
25976
25982
  root: "",
25977
25983
  master: { root: "", biography: "", goals: "", preferences: "" },
25978
- llm: { root: "", responsibilities: "", protocols: "", diary: "" },
25984
+ llm: { root: "", responsibilities: "", protocols: "", selfcorrection: "", diary: "" },
25979
25985
  memory: { root: "", sessions: "", threads: "", metaThread: "" },
25980
25986
  knowledge: { root: "", master: "", domains: "" },
25981
25987
  insights: { root: "", logs: "" },
@@ -26016,9 +26022,10 @@ function loadConfig() {
26016
26022
  return null;
26017
26023
  try {
26018
26024
  const parsed = JSON.parse(readFileSync(path, "utf-8"));
26019
- if (typeof parsed?.root === "string" && (parsed?.version === 9 || parsed?.version === 8 || parsed?.version === 5)) {
26025
+ if (typeof parsed?.root === "string" && [10, 9, 8, 5].includes(parsed?.version)) {
26020
26026
  return {
26021
26027
  ...parsed,
26028
+ llm: { selfcorrection: "", ...parsed.llm ?? {} },
26022
26029
  memory: { metaThread: "", ...parsed.memory ?? {} },
26023
26030
  policy: { ...DEFAULT_POLICY, ...parsed.policy ?? {} }
26024
26031
  };
@@ -26028,7 +26035,7 @@ function loadConfig() {
26028
26035
  }
26029
26036
  function saveConfig(config2) {
26030
26037
  const path = configFilePath();
26031
- writeFileSync(path, JSON.stringify({ ...config2, version: 9 }, null, 2) + `
26038
+ writeFileSync(path, JSON.stringify({ ...config2, version: 10 }, null, 2) + `
26032
26039
  `, "utf-8");
26033
26040
  return path;
26034
26041
  }
@@ -26062,7 +26069,7 @@ async function discoverBrainLLM(trilium) {
26062
26069
  config2.master = { root: id, biography: g("Biography"), goals: g("Goals"), preferences: g("Preferences") };
26063
26070
  break;
26064
26071
  case "LLM":
26065
- config2.llm = { root: id, responsibilities: g("Responsibilities"), protocols: g("Protocols"), diary: g("Diary") };
26072
+ config2.llm = { root: id, responsibilities: g("Responsibilities"), protocols: g("Protocols"), selfcorrection: g("Self-correction"), diary: g("Diary") };
26066
26073
  break;
26067
26074
  case "Memory":
26068
26075
  config2.memory = { root: id, sessions: g("Sessions"), threads: g("Threads"), metaThread: "" };
@@ -26756,8 +26763,9 @@ function setSection(html, heading, content, mode, occurrence = 1) {
26756
26763
  ${html.slice(headingStart)}`, matched: true, headingCount };
26757
26764
  }
26758
26765
  if (mode === "after") {
26759
- return { html: `${html.slice(0, contentStart)}
26760
- ${content}${html.slice(contentStart)}`, matched: true, headingCount };
26766
+ return { html: `${html.slice(0, end).replace(/\s*$/, "")}
26767
+ ${content}
26768
+ ${html.slice(end)}`, matched: true, headingCount };
26761
26769
  }
26762
26770
  const existing = html.slice(contentStart, end).trim();
26763
26771
  const inner = mode === "append" && existing ? `${existing}
@@ -27007,7 +27015,8 @@ var STRUCTURED_SINGLETONS = new Set([
27007
27015
  "goals",
27008
27016
  "preferences",
27009
27017
  "responsibilities",
27010
- "protocols"
27018
+ "protocols",
27019
+ "selfcorrection"
27011
27020
  ]);
27012
27021
  var hasHeading = (html, text) => new RegExp(`<h[2-4](?:\\s[^>]*)?>\\s*${text}\\s*</h[2-4]>`, "i").test(html);
27013
27022
  var REQUIRED_SECTIONS = {
@@ -27063,8 +27072,10 @@ ${o.goal ? `<p>${escapeHtml(o.goal)}</p>` : o.body || "<p>To be determined.</p>"
27063
27072
  <p></p>`].join(`
27064
27073
  `);
27065
27074
  case "protocols":
27066
- return ["<h2>Operating</h2>", o.body || "<p></p>", `<h2>Self-correction</h2>
27067
- <p></p>`].join(`
27075
+ return ["<h2>Operating</h2>", o.body || "<p></p>"].join(`
27076
+ `);
27077
+ case "selfcorrection":
27078
+ return ["<h2>Corrections</h2>", o.body || "<p></p>"].join(`
27068
27079
  `);
27069
27080
  case "domain":
27070
27081
  return domainContent(o.domain ?? "");
@@ -27235,7 +27246,7 @@ function structureRuleFor(kind) {
27235
27246
 
27236
27247
  // src/router.ts
27237
27248
  function isSingleton(kind) {
27238
- return SingletonKinds.includes(kind);
27249
+ return isSingletonKind(kind);
27239
27250
  }
27240
27251
  function kindHome(cfg, kind) {
27241
27252
  switch (kind) {
@@ -27249,6 +27260,8 @@ function kindHome(cfg, kind) {
27249
27260
  return cfg.llm.responsibilities;
27250
27261
  case "protocols":
27251
27262
  return cfg.llm.protocols;
27263
+ case "selfcorrection":
27264
+ return cfg.llm.selfcorrection;
27252
27265
  case "diary":
27253
27266
  return cfg.llm.diary;
27254
27267
  case "session":
@@ -27363,6 +27376,8 @@ function locationLabel(kind, domainTitle) {
27363
27376
  return "LLM \u2192 Responsibilities";
27364
27377
  case "protocols":
27365
27378
  return "LLM \u2192 Protocols";
27379
+ case "selfcorrection":
27380
+ return "LLM \u2192 Self-correction";
27366
27381
  case "diary":
27367
27382
  return "LLM \u2192 Diary";
27368
27383
  case "session":
@@ -27397,6 +27412,7 @@ function structuralIds(cfg) {
27397
27412
  cfg.llm.root,
27398
27413
  cfg.llm.responsibilities,
27399
27414
  cfg.llm.protocols,
27415
+ cfg.llm.selfcorrection,
27400
27416
  cfg.llm.diary,
27401
27417
  cfg.memory.root,
27402
27418
  cfg.memory.sessions,
@@ -27417,7 +27433,8 @@ function isContainer(cfg, noteId) {
27417
27433
  cfg.master.goals,
27418
27434
  cfg.master.preferences,
27419
27435
  cfg.llm.responsibilities,
27420
- cfg.llm.protocols
27436
+ cfg.llm.protocols,
27437
+ cfg.llm.selfcorrection
27421
27438
  ];
27422
27439
  return isStructural(cfg, noteId) && !singletons.includes(noteId);
27423
27440
  }
@@ -27760,6 +27777,8 @@ async function sweep(trilium, cfg, opts = {}) {
27760
27777
  if (reported.has(pairKey))
27761
27778
  continue;
27762
27779
  reported.add(pairKey);
27780
+ if (acknowledged(a) || acknowledged(bn))
27781
+ continue;
27763
27782
  const [ha, hb] = await Promise.all([headingsOf(a), headingsOf(bn)]);
27764
27783
  const overlap = ha.filter((h) => hb.some((x) => x.toLowerCase() === h.toLowerCase()));
27765
27784
  const show = (h) => h.length ? h.slice(0, 8).join(" \xB7 ") + (h.length > 8 ? " \xB7 \u2026" : "") : "(no headings)";
@@ -27932,7 +27951,8 @@ async function buildDigest(trilium, cfg, opts = {}) {
27932
27951
  ]),
27933
27952
  Promise.all([
27934
27953
  readSlot("responsibilities", cfg.llm.responsibilities),
27935
- readSlot("protocols", cfg.llm.protocols)
27954
+ readSlot("protocols", cfg.llm.protocols),
27955
+ readSlot("selfcorrection", cfg.llm.selfcorrection)
27936
27956
  ])
27937
27957
  ]);
27938
27958
  digest.master.push(...masterSlots.filter((s) => !!s));
@@ -28042,9 +28062,10 @@ async function createBrainLLMStructure(trilium) {
28042
28062
  leaf(masterRoot, "Preferences", "preferences", "A single maintained note of the master/user's preferences.")
28043
28063
  ]);
28044
28064
  const llmRoot = await book(rootId, "LLM", CONTAINER_PURPOSES.llm, "bx bx-bot");
28045
- const [responsibilities, protocols, diary] = await Promise.all([
28065
+ const [responsibilities, protocols, selfcorrection, diary] = await Promise.all([
28046
28066
  leaf(llmRoot, "Responsibilities", "responsibilities", "A single maintained note of the assistant's responsibilities to the master/user, derived from their goals and preferences."),
28047
28067
  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."),
28068
+ 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
28069
  book(llmRoot, "Diary", CONTAINER_PURPOSES.llmDiary, "bx bx-book-heart")
28049
28070
  ]);
28050
28071
  const memoryRoot = await book(rootId, "Memory", CONTAINER_PURPOSES.memory, "bx bx-been-here");
@@ -28060,10 +28081,10 @@ async function createBrainLLMStructure(trilium) {
28060
28081
  const insightsRoot = await book(rootId, "Insights", CONTAINER_PURPOSES.insights, "bx bx-bulb");
28061
28082
  const logs = await book(insightsRoot, "Logs", CONTAINER_PURPOSES.insightsLogs, "bx bx-history");
28062
28083
  return {
28063
- version: 9,
28084
+ version: 10,
28064
28085
  root: rootId,
28065
28086
  master: { root: masterRoot, biography, goals, preferences },
28066
- llm: { root: llmRoot, responsibilities, protocols, diary },
28087
+ llm: { root: llmRoot, responsibilities, protocols, selfcorrection, diary },
28067
28088
  memory: { root: memoryRoot, sessions, threads, metaThread: "" },
28068
28089
  knowledge: { root: knowledgeRoot, master: knowledgeMaster, domains },
28069
28090
  insights: { root: insightsRoot, logs },
@@ -28234,12 +28255,19 @@ day's blocks rather than the whole week. outline(id) lists the headings.`, {
28234
28255
  // src/tools-llm.ts
28235
28256
  function registerLlmTools(server, trilium, brainRef) {
28236
28257
  const b = () => brainRef.config;
28237
- server.tool("llm", `Read an LLM note: responsibilities or protocols (singletons), or a diary entry by id.
28258
+ server.tool("llm", `Read an LLM note: responsibilities, protocols or selfcorrection (singletons), or a diary
28259
+ entry by id.
28260
+
28261
+ selfcorrection holds the assistant's own corrections \u2014 what it got wrong, what generalises, and
28262
+ the rule that prevents a repeat. It is the one LLM singleton start() does NOT serve in full: it
28263
+ is the largest and the least load-bearing at a first message, so it arrives as section headings
28264
+ and is read here on demand. Reach for it when about to repeat a class of work that has gone
28265
+ wrong before \u2014 an audit, a large edit, a claim of completeness.
28238
28266
 
28239
28267
  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"]),
28268
+ protocols and selfcorrection, the two largest singletons and the ones most often needed in part
28269
+ rather than whole. outline(id) lists the headings.`, {
28270
+ which: exports_external.enum(["responsibilities", "protocols", "selfcorrection", "diary"]),
28243
28271
  id: exports_external.string().optional().describe("diary only: the entry id from llm_recall"),
28244
28272
  section: exports_external.string().optional().describe("Read only this heading's section (h2/h3/h4), instead of the whole note"),
28245
28273
  occurrence: exports_external.number().int().positive().optional().describe("section=: which same-text heading, 1-based (default: the first)")
@@ -28250,20 +28278,23 @@ whole. outline(id) lists the headings.`, {
28250
28278
  return txt(await readFull(trilium, id, { section, occurrence }));
28251
28279
  }
28252
28280
  const noteId = b().llm[which];
28253
- if (!noteId)
28254
- throw new Error("BrainLLM not bootstrapped \u2014 run bootstrap.");
28281
+ if (!noteId) {
28282
+ 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.");
28283
+ }
28255
28284
  return txt(await readFull(trilium, noteId, { section, occurrence }));
28256
28285
  });
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 }) => {
28286
+ 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
28287
  const cfg = b();
28259
- const [resp, prot, diary] = await Promise.all([
28288
+ const [resp, prot, selfcorr, diary] = await Promise.all([
28260
28289
  previewWithRelations(trilium, cfg.llm.responsibilities),
28261
28290
  previewWithRelations(trilium, cfg.llm.protocols),
28291
+ cfg.llm.selfcorrection ? previewWithRelations(trilium, cfg.llm.selfcorrection) : Promise.resolve(null),
28262
28292
  skim(trilium, cfg.llm.diary, { kind: "diary", limit: limit ?? 7 })
28263
28293
  ]);
28264
28294
  return txt({
28265
28295
  responsibilities: { id: cfg.llm.responsibilities, ...resp },
28266
28296
  protocols: { id: cfg.llm.protocols, ...prot },
28297
+ ...selfcorr ? { selfcorrection: { id: cfg.llm.selfcorrection, ...selfcorr } } : {},
28267
28298
  diary
28268
28299
  });
28269
28300
  });
@@ -28711,12 +28742,13 @@ rather than listing them unconditionally for every caller to work around.`, {
28711
28742
  const relations = relationSnippet(note);
28712
28743
  return { id, lastModified: note.dateModified.slice(0, 10), content, ...relations ? { relations } : {} };
28713
28744
  };
28714
- const [biography, goals, preferences, responsibilities, protocols] = await Promise.all([
28745
+ const [biography, goals, preferences, responsibilities, protocols, selfcorrection] = await Promise.all([
28715
28746
  fetchSingleton(cfg.master.biography),
28716
28747
  fetchSingleton(cfg.master.goals),
28717
28748
  fetchSingleton(cfg.master.preferences),
28718
28749
  fetchSingleton(cfg.llm.responsibilities),
28719
- fetchSingleton(cfg.llm.protocols)
28750
+ fetchSingleton(cfg.llm.protocols),
28751
+ cfg.llm.selfcorrection ? fetchSingleton(cfg.llm.selfcorrection) : Promise.resolve(null)
28720
28752
  ]);
28721
28753
  let diaryEntry = null;
28722
28754
  if (cfg.llm.diary) {
@@ -28736,14 +28768,15 @@ rather than listing them unconditionally for every caller to work around.`, {
28736
28768
  const contents = await Promise.all(candidates.map((n) => trilium.getNoteContent(n.noteId).catch(() => "")));
28737
28769
  return contents.filter(hasAddendumMarker).length;
28738
28770
  }).catch(() => null);
28739
- const singletonStubs = [
28740
- ["biography", biography],
28741
- ["goals", goals],
28742
- ["preferences", preferences],
28743
- ["responsibilities", responsibilities],
28744
- ["protocols", protocols]
28745
- ];
28746
- const touchedToday = singletonStubs.filter(([, s]) => s.lastModified === d).map(([name]) => name);
28771
+ const singletonStubs = {
28772
+ biography,
28773
+ goals,
28774
+ preferences,
28775
+ responsibilities,
28776
+ protocols,
28777
+ selfcorrection
28778
+ };
28779
+ const touchedToday = Object.keys(singletonStubs).filter((k) => singletonStubs[k]?.lastModified === d);
28747
28780
  const pending = {
28748
28781
  addendums: pendingAddendums === null ? "unknown" : pendingAddendums,
28749
28782
  maintenanceFlags: hygiene?.flagged.length ?? 0,
@@ -28756,19 +28789,19 @@ rather than listing them unconditionally for every caller to work around.`, {
28756
28789
  ...scoped ? { scope: "agent" } : {},
28757
28790
  ...!full ? { mode: "light", note: "Singleton content and the diary body are omitted (default) \u2014 fetch via master()/llm() only where lastModified indicates a revision is needed; start() already served all singletons in full, and diary() needs only the id." } : {},
28758
28791
  master: { biography, goals, preferences },
28759
- llm: { responsibilities, protocols },
28792
+ llm: { responsibilities, protocols, ...selfcorrection ? { selfcorrection } : {} },
28760
28793
  diary: diaryEntry,
28761
28794
  maintenance: hygiene ? { scanned: hygiene.scanned, fixed: hygiene.fixed.length, transitions: hygiene.transitions, flagged: hygiene.flagged, ...hygiene.suppressed ? { suppressed: hygiene.suppressed } : {} } : "skipped",
28762
28795
  pending,
28763
28796
  audit: scoped ? undefined : {
28764
- consistency: "Read all five singletons and check for ambiguity, internal contradiction, or claims that disagree across them. Fix what you find with revise() BEFORE close(), so the log records a brain that already agrees with itself.",
28797
+ consistency: "Read every singleton and check for ambiguity, internal contradiction, or claims that disagree across them. Fix what you find with revise() BEFORE close(), so the log records a brain that already agrees with itself.",
28765
28798
  alignment: "Then check correlation, not just agreement: do responsibilities and protocols actually serve what biography, goals and preferences describe? A protocol can be perfectly consistent and still be serving a goal that has moved.",
28766
- readThem: `master("biography"|"goals"|"preferences") and llm("responsibilities"|"protocols") \u2014 or one section at a time with section=. Written today: ${touchedToday.length ? touchedToday.join(", ") : "none"}.`
28799
+ readThem: `master("biography"|"goals"|"preferences") and llm("responsibilities"|"protocols"|"selfcorrection") \u2014 or one section at a time with section=. Written today: ${touchedToday.length ? touchedToday.join(", ") : "none"}.`
28767
28800
  },
28768
28801
  next: [
28769
28802
  ...scoped ? ["Scoped run \u2014 the user's master singletons and your LLM singletons are OUT of scope and deliberately not listed here."] : [
28770
28803
  "Update master singletons (biography / goals / preferences) via revise() with session observations about the user.",
28771
- "Update LLM singletons (responsibilities / protocols) via revise() with session observations about yourself."
28804
+ "Update LLM singletons (responsibilities / protocols / selfcorrection) via revise() with session observations about yourself \u2014 selfcorrection takes what you got WRONG and what generalises from it, not what you did."
28772
28805
  ],
28773
28806
  ...scoped ? [] : ["Audit the singletons against each other and against each other's PURPOSE \u2014 see audit= above. This is the step that catches an operating rule still serving a goal that has moved."],
28774
28807
  scoped ? `Call addendum() \u2014 fold only what is in your lane; leaving out-of-scope addendums for the next interactive session is correct, and the call itself satisfies the gate. (${pending.addendums} note(s) currently carry addendum markers.)` : pending.addendums === 0 ? "Call addendum() \u2014 0 notes currently carry addendum markers, so this should come back clean; the call still satisfies the gate." : `Call addendum() \u2014 ${pending.addendums} note(s) carry addendum markers; fold each into its section.`,
@@ -29576,14 +29609,17 @@ applies several surgeries in one call, in order, against one read and one write.
29576
29609
 
29577
29610
  Granularity warning: section + mode=replace swaps the ENTIRE section body \u2014 everything under
29578
29611
  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".`, {
29612
+ find=; to add a sibling block around a whole section without touching its body, use
29613
+ mode="before"/"after". Both are section-relative: "after" lands past the end of that section's
29614
+ body, so inserting a heading with it creates a NEW section rather than nesting one under the
29615
+ target \u2014 which is what it used to do, emptying the target while reporting matched: true.`, {
29580
29616
  noteId: exports_external.string().describe("Note to update"),
29581
29617
  body: exports_external.string().optional().describe("Content to add/replace: plain text, markdown, or HTML. With find=, the raw replacement string (no conversion)."),
29582
29618
  title: exports_external.string().optional().describe("New title (normalized server-side)"),
29583
29619
  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
29620
  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
29621
  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='),
29622
+ 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
29623
  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
29624
  nth: exports_external.number().int().positive().optional().describe("find=: replace only the Nth occurrence, 1-based (default: all of them)"),
29589
29625
  edits: exports_external.array(exports_external.object({
@@ -31035,7 +31071,12 @@ exception.`, {
31035
31071
  server.tool("brain", `Surface the entire BrainLLM content tree \u2014 every typed note across all five content areas
31036
31072
  (Master, LLM, Memory, Knowledge, Insights), grouped by area and sub-container, with
31037
31073
  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.`, {
31074
+ Structural containers are excluded; only content notes appear.
31075
+
31076
+ Insights returns { logs, claims }. Claims were missing entirely before V12 \u2014 they live in a
31077
+ container resolved by title rather than named in the config, and this read was scoped one level
31078
+ too deep to reach them, so the register was invisible to the tool that calls itself the full
31079
+ inventory.`, {
31039
31080
  includeArchived: exports_external.boolean().optional().describe("Include archived/resolved notes (default: false)")
31040
31081
  }, async ({ includeArchived }) => {
31041
31082
  const cfg = b();
@@ -31084,10 +31125,12 @@ Structural containers are excluded; only content notes appear.`, {
31084
31125
  fetchFrom(cfg.memory.threads),
31085
31126
  fetchFrom(cfg.knowledge.master),
31086
31127
  fetchFrom(cfg.knowledge.domains),
31087
- fetchFrom(cfg.insights.logs)
31128
+ fetchFrom(cfg.insights.root)
31088
31129
  ]);
31089
31130
  const diaryIds = new Set(llmDiary.map((n) => n.noteId));
31090
31131
  const llmSingletons = llmAll.filter((n) => !diaryIds.has(n.noteId));
31132
+ const insightLogs = insights.filter((n) => labelOf2(n, "noteType") === "log");
31133
+ const insightClaims = insights.filter((n) => labelOf2(n, "noteType") === "claim");
31091
31134
  const areas = {
31092
31135
  Master: masterAll.map(row),
31093
31136
  LLM: {
@@ -31102,7 +31145,10 @@ Structural containers are excluded; only content notes appear.`, {
31102
31145
  master: kMaster.map(row),
31103
31146
  domains: kDomains.map(row)
31104
31147
  },
31105
- Insights: insights.map(row)
31148
+ Insights: {
31149
+ logs: insightLogs.map(row),
31150
+ claims: insightClaims.map(row)
31151
+ }
31106
31152
  };
31107
31153
  const total = masterAll.length + llmSingletons.length + llmDiary.length + sessions.length + threads.length + kMaster.length + kDomains.length + insights.length;
31108
31154
  return txt3({ total, areas });
@@ -31229,9 +31275,10 @@ Pass area= to zoom into one surface and get its full detail.`, {
31229
31275
  ...includeArchived ? {} : { note: "Archived and resolved notes are excluded \u2014 pass includeArchived=true to see them." }
31230
31276
  });
31231
31277
  });
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),
31278
+ server.tool("bootstrap", `Initialize the BrainLLM structure in Trilium (idempotent \u2014 safe to re-run; refreshes config,
31279
+ heals singletons a newer version introduced, and re-engraves container purposes if the structure
31280
+ already exists). Creates the five areas \u2014 Master (Biography/Goals/Preferences),
31281
+ LLM (Responsibilities/Protocols/Self-correction/Diary), Memory (Sessions/Threads), Knowledge (Master/Domains),
31235
31282
  Insights (Logs) \u2014 each engraved with its purpose, and writes brainllm.json. Active
31236
31283
  immediately, no restart needed.`, {}, async () => {
31237
31284
  if (b().root) {
@@ -31242,6 +31289,26 @@ immediately, no restart needed.`, {}, async () => {
31242
31289
  return { id: child.noteId, title: child.title };
31243
31290
  }));
31244
31291
  const saved = saveConfig(brainRef.config);
31292
+ const healed = [];
31293
+ const ensureSingleton = async (slot, title, kind, purpose) => {
31294
+ if (brainRef.config.llm[slot])
31295
+ return;
31296
+ const hit = await trilium.searchNotes(`note.title = '${title}'`, { ancestorNoteId: brainRef.config.llm.root, fastSearch: true, limit: 1 }).catch(() => ({ results: [] }));
31297
+ let id = hit.results[0]?.noteId;
31298
+ if (!id) {
31299
+ const made = await trilium.createNote(brainRef.config.llm.root, title, purposeContent(purpose) + `
31300
+ ` + contentFor(kind, { date: localToday(), body: "" }), "text");
31301
+ id = made.note.noteId;
31302
+ await trilium.addLabel(id, "noteType", kind).catch(() => null);
31303
+ healed.push(`created ${title}`);
31304
+ } else {
31305
+ healed.push(`adopted existing ${title}`);
31306
+ }
31307
+ brainRef.config.llm[slot] = id;
31308
+ };
31309
+ 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.");
31310
+ if (healed.length)
31311
+ saveConfig(brainRef.config);
31245
31312
  const refreshed = [];
31246
31313
  for (const [id, purpose] of containerPurposes(brainRef.config)) {
31247
31314
  if (!id)
@@ -31260,6 +31327,7 @@ immediately, no restart needed.`, {}, async () => {
31260
31327
  status: "already_initialized",
31261
31328
  message: `BrainLLM structure exists. Config refreshed at: ${saved}`,
31262
31329
  ...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." } : {},
31330
+ ...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
31331
  root: { id: existing.noteId, title: existing.title },
31264
31332
  children
31265
31333
  });
@@ -32368,7 +32436,7 @@ function createServer(origin = null) {
32368
32436
  const s = new McpServer({
32369
32437
  name: "BrainLLM",
32370
32438
  title: "BrainLLM",
32371
- version: "11.0.0",
32439
+ version: "12.1.0",
32372
32440
  icons: brandingIcons(origin)
32373
32441
  });
32374
32442
  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.1.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.1.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.1.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.1.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). |