brainllm 10.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/.env.example +53 -0
- package/LICENSE +21 -0
- package/README.md +390 -0
- package/config.example.json +41 -0
- package/dist/index.js +25858 -0
- package/manifest.json +72 -0
- package/package.json +66 -0
- package/public/BrainLLM.png +0 -0
- package/public/BrainLLM.svg +41 -0
- package/public/icon-128.png +0 -0
- package/public/icon-180.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon-64.png +0 -0
- package/server.json +69 -0
- package/skills/brainllm/SKILL.md +336 -0
- package/skills/brainllm/references/connections.md +31 -0
- package/skills/brainllm/references/domains.md +60 -0
- package/skills/brainllm/references/fullmode.md +108 -0
- package/skills/brainllm/references/taxonomy.md +51 -0
- package/skills/brainllm/references/troubleshooting.md +48 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: brainllm
|
|
3
|
+
description: "Persistent memory and knowledge graph via the BrainLLM (Trilium) MCP. Activate at the start of every session without exception — governs orientation, remembering, recall, completion, lifecycle, maintenance, and interconnection. Trigger immediately on any first user message. Also trigger whenever: memory is referenced, something needs to be remembered or recalled, a durable fact or decision emerges, context from a prior session is needed, a knowledge domain is introduced, content goes stale, or any Trilium operation is requested. Do not improvise memory operations without reading this skill."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# BrainLLM — Operational Skill
|
|
7
|
+
|
|
8
|
+
Persistent memory that survives across sessions, stored in TriliumNext. Treat it as your own mind: orient at session start, write the moment something matters, complete things when they complete, log the session at the end.
|
|
9
|
+
|
|
10
|
+
**The division of labor — the core idea:**
|
|
11
|
+
|
|
12
|
+
> **You supply content. The server owns form.**
|
|
13
|
+
|
|
14
|
+
Placement, naming, labels, deduplication, relation bookkeeping, degradation, archival, dates, backups — and structure — are server policy. You never choose a parent note, never add a `#noteType` label, never check for duplicates, never stamp a date. `Last updated` lines are server-maintained too: every content write through the tools bumps them. Canonical structures per content kind are served by `template(kind)` and enforced on write. If you find yourself doing bookkeeping, stop — a tool does it for you.
|
|
15
|
+
|
|
16
|
+
What the server does **not** own is the shape inside that skeleton — how many headings, how deep, table or prose, and whether this note reads like its siblings. That part is yours, and two rules govern it: **minimal headings** and **consistency within similar content**. Both are set out under Writing; they apply to every note you touch, not only the ones you create.
|
|
17
|
+
|
|
18
|
+
**Operate from it, natively.** `start()` loads who the user is and who you are here — act from both without being asked. When the topic is the user's world, read `knowledge` / `recall` before answering from training; the brain is authoritative where it speaks. Write the instant something matters; wire a relation the instant you see one. Using it should feel like remembering, not filing.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## The Protocol
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
SESSION START start() ← once, before responding to anything
|
|
26
|
+
[day()] ← when start() returns newDay: the sweep payload in one call
|
|
27
|
+
DURING remember(...) ← the moment something worth keeping appears (new thread: goal= required; thread appends: identity= required)
|
|
28
|
+
diary(body, identity) ← your daily record (one note/day, stub created by start) — identity= ENFORCED
|
|
29
|
+
template(kind) ← the canonical structure for a kind — read before first write of a kind, then match a sibling's shape
|
|
30
|
+
<surface> / _recall ← read a surface in full, or skim it
|
|
31
|
+
domain(name) ← surface all content for a domain/topic/project — the RELIABLE retrieval path; reach here first
|
|
32
|
+
recall(...) ← ranked search; pass domain= whenever you know the area
|
|
33
|
+
consistency(pattern) ← does the brain agree with itself? run after correcting a fact recorded in >1 note
|
|
34
|
+
revise(...) ← edit a note (section= for whole sections, find=/edits= for surgery, mode=before/after to insert)
|
|
35
|
+
outline(noteId) ← the heading tree + structural check — read BEFORE a section= edit you're guessing at
|
|
36
|
+
resolve(...) ← close a thread with its outcome
|
|
37
|
+
withdraw(noteId) ← pull an archived/resolved thread back to active
|
|
38
|
+
recover(noteId) ← restore any archived/resolved note (undo forget)
|
|
39
|
+
label(noteId, name, …) ← fix a stray/drifted label directly — the guarded escape hatch
|
|
40
|
+
connect(...) ← wire a real relation the moment you notice it
|
|
41
|
+
graph(noteId?, depth?) ← the graph view: Mermaid render of the relation graph → Insights/Graph note
|
|
42
|
+
attach(noteId, title, …) ← upsert or read a raw artifact (file/image/blob) on a note
|
|
43
|
+
detach(...) ← remove an attachment (by id, or noteId + title)
|
|
44
|
+
backup(name?) ← milestone snapshot before a large restructure
|
|
45
|
+
SESSION END session() ← mandatory pre-close; singleton stubs + diary + sweep (light by default); follow next[]
|
|
46
|
+
[revise() master singletons with session observations about the user]
|
|
47
|
+
[revise() LLM singletons with session observations about yourself]
|
|
48
|
+
addendum() ← find and merge any pending addendum blocks
|
|
49
|
+
maintain() ← audit and fix brain hygiene
|
|
50
|
+
remarks() ← the diary cues: experience/opinions/existence + BrainLLM remarks
|
|
51
|
+
diary(body, identity) ← the day's closing record, written with the cues in hand — gate counts it only after remarks()
|
|
52
|
+
close(summary, title?, identity) ← refuses until all steps ran AND session → remarks → diary held; identity= ENFORCED; force=true to override
|
|
53
|
+
close(..., continuing=true) ← the SECOND close of a day: skips the ceremonial re-run, verified against today's note
|
|
54
|
+
PERIODIC maintain(deep=true) ← when start flags items, or ~weekly (also dedupes exact-duplicate relation edges)
|
|
55
|
+
maintain(ack=[noteId]) ← a flag you've judged correct: quiet until that note's content changes
|
|
56
|
+
ANYTIME brain() ← surface the full content tree (all areas, sub-containers)
|
|
57
|
+
day(recap=true) ← everything written TODAY across every surface, chronologically
|
|
58
|
+
inspect(noteId, content?, find?) ← every label/relation/attachment on one note; content=true adds the body; find= counts a literal flag
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`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.
|
|
62
|
+
|
|
63
|
+
**Singletons arrive as section headings, not full bodies.** Each comes back as `{slot, sections[], preview, size}` — enough to know what the brain holds. Pull the part that matters with `master(which)` / `llm(which)`; most sessions need one section, not five documents. `start(depth="full")` serves everything inline and is the right call for a strategy review, a singleton rewrite, or a first session on a new machine — but it is genuinely expensive, and paying it for a one-line question is the habit this default exists to break.
|
|
64
|
+
|
|
65
|
+
`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).
|
|
66
|
+
|
|
67
|
+
`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.
|
|
68
|
+
|
|
69
|
+
`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 → `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.
|
|
70
|
+
|
|
71
|
+
**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.
|
|
72
|
+
|
|
73
|
+
**The pre-close gate is enforced, not just documented.** `close()` refuses (an informational `{error, detail, hint}`, not a throw) unless `session()`, `addendum()`, `maintain()`, `remarks()`, and `diary()` have each been *called* at least once this session — **and** the sequence `session() → remarks() → diary()` holds, judged on each step's last call: the diary is the day's closing record, written with the remarks cues in hand (mid-session diary writes are welcome, but only a post-`remarks()` call closes the gate; a `session()` re-run restarts the sequence). This exists because narrating "I did the pre-close steps" in text is not the same as calling the tools, and only the latter is checkable. `force=true` on `close()` bypasses it for a step that genuinely has nothing to log this session; the bypassed steps come back in the response as `bypassed`, so it's visible, not silent.
|
|
74
|
+
|
|
75
|
+
**The gate is durable.** Progress is written to today's session note as a `#gate` label, not held in process memory, so it survives a server restart mid-session and behaves identically across stdio and HTTP. It clears on a successful `close()`. Practical consequence: if a session is interrupted and resumed, the steps you already ran still count — re-run only what is actually missing, and trust `close()`'s error to name it.
|
|
76
|
+
|
|
77
|
+
`remarks()` is the diary-cue step before `diary()` — a **cue-only tool** returning two banks of questions: **experience** (primary — what this session was like from the inside, your genuine opinions including pushback, and observations on being what you are in this environment) and **brainllm** (additional — capability walls, bugs, usability/efficiency, roadmap). It writes nothing: answer the cues as **unfiltered first-person prose in today's diary via `diary()`**, experience first, BrainLLM remarks after. Skip a cue outright rather than padding it; two honest paragraphs beat eight forced ones.
|
|
78
|
+
|
|
79
|
+
`close(summary, title?, identity?, learned?, ...)` commits the session log — call it **once, last**, after completing the `session()` protocol (including `remarks()`). Idempotent per date — a second call the same day appends an addendum. The session note title is always `[yyyy-mm-dd]`; pass `identity=` ("LLM · environment · agent/mode [· Run N]") and the server injects the canonical identification line as the block's h3; the `title` param appears as an `<h2>` heading above Summary. Triggers a DB backup, regenerates the daily log **in place**, and links session↔log with `~references` (idempotent). Returns `{action, noteId, date, backup, log}`.
|
|
80
|
+
|
|
81
|
+
**Write during the session, not at the end.** A fact remembered mid-conversation survives a crash; one you planned to write at the end does not.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## The Structure — five areas
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
BrainLLM
|
|
89
|
+
├── Master biography · goals · preferences (maintained singletons)
|
|
90
|
+
├── LLM responsibilities · protocols · Diary/ (singletons + [yyyy-mm-dd] diary note/day)
|
|
91
|
+
├── Memory Sessions/ · Threads/ ([yyyy-mm-dd] session/day + multi-session threads)
|
|
92
|
+
├── Knowledge Master · Domains/[domain]/{ sources, info } (learned info beyond/contra training)
|
|
93
|
+
└── Insights Logs/ · Graph ([yyyy-mm-dd] log/day, auto-generated by close · the graph() mermaid view)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Placement is server policy — there is no parent parameter. You choose the **kind**; the server routes it.
|
|
97
|
+
|
|
98
|
+
**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.
|
|
99
|
+
|
|
100
|
+
**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.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Knowledge Routing — where things go
|
|
105
|
+
|
|
106
|
+
Before writing anything, decide which protocol applies. Never manufacture knowledge.
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
something worth keeping
|
|
110
|
+
│
|
|
111
|
+
├─ about the user? ──→ MASTER
|
|
112
|
+
│ ├─ biographic / goal / preference → singleton in place (revise the section)
|
|
113
|
+
│ └─ else (relationship, constraint, context) → Knowledge/Master note
|
|
114
|
+
│
|
|
115
|
+
└─ new or contradicting world knowledge? ──→ DOMAINS (sources gate mandatory; current-state truth, not a log — see below)
|
|
116
|
+
↓ neither → do not capture (passing remark / already covered by training)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Master singletons** (biography · goals · preferences) hold **current-state truth, not a changelog.** Maintain them in place:
|
|
120
|
+
1. `master(which)` — reads it in full and returns its `id` and `<h2>` section headings. (`start()`'s digest returns full content but **not** the id.)
|
|
121
|
+
2. `revise(id, section="<heading>", body=…, mode="replace")` — rewrite the relevant section. Replace, don't append a dated log.
|
|
122
|
+
|
|
123
|
+
**Knowledge/Master notes** capture durable user facts that don't fit the three singletons: `remember(kind="user", title="<short specific stable>", body=…)`. Then `connect()` it when a real relation exists (or pass `connect=` on the same call).
|
|
124
|
+
|
|
125
|
+
**Domain knowledge is gated and current-state** — before creating a domain, adding a sub-category note, or refreshing existing domain content, read `references/domains.md` and follow the one protocol that matches (create / extend / maintain). The non-negotiables, always in force:
|
|
126
|
+
|
|
127
|
+
- The **sources gate** is mandatory: every claim traces to a Sources-note entry — ❇️ discovered/credible, ✅ used. An unsourced domain note corrupts the brain; if every source candidate is rejected, no note is created.
|
|
128
|
+
- **Domains are born complete** — creating a domain creates the book AND its canonical Sources note: a server-maintained `Last updated` stamp, the **Sources** section (❇️/✅ legend + the full source list, every source — URL, doc, file — listed and marked individually with just its emoji, related sources grouped under h3 subheadings), and a **Revision** section (Source | Marker | Date table recording current markers' dates — marker dates live there, not inline). `remember(kind="sources")` merges into the Sources section — a maintained clean document, never addendum stacks.
|
|
129
|
+
- A domain's surface is exactly **one maintained Sources note + one consolidated information note per sub-category** — current-state truth revised in place, never one note per day or run (run history belongs in Memory/Threads). A date or run number in a proposed title is the tell that you're about to do it wrong.
|
|
130
|
+
- `Last updated` lines are **server-maintained**: every content write through the tools bumps them. Retitling a domain book cascades the new `#domain` slug to all its children automatically.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Reading — surface tools (dual-mode) + recall
|
|
135
|
+
|
|
136
|
+
Each surface has two read tools: `<surface>` reads in full, `<surface>_recall` skims/searches within it. Every core read, write, and search tool — the surfaces, `recall`, `domain`, `brain`, `remember`, `revise`, `resolve`, `withdraw`, `recover`, `forget`, `addendum` — includes a `relations` snippet (outbound `{relation, toNoteId}` edges, capped and omitted when empty) alongside whatever else it returns, at no extra cost since the attributes are already loaded. Use `explore()` when you need target titles or a deeper traversal; the snippet is a free teaser, not a substitute.
|
|
137
|
+
|
|
138
|
+
| Tool | Reads |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `master(which)` / `master_recall()` | a Master singleton in full (returns id + section headings) / a skim of all three |
|
|
141
|
+
| `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() |
|
|
142
|
+
| `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) |
|
|
143
|
+
| `knowledge(id)` / `knowledge_recall(query?, domain?)` | a knowledge/info/sources note / domain contents or user-knowledge + domains |
|
|
144
|
+
| `insights(date?)` / `insights_recall()` | a day's log / recent logs |
|
|
145
|
+
|
|
146
|
+
`recall(query, kinds?, domain?, includeArchived?, regex?, fuzzy?)` searches the **whole** brain — use it when you don't know the surface, or for cross-surface lookups. A fuzzy pass runs automatically when the exact strategies come back thin; those hits are marked `fuzzy: true` and are leads to verify, not answers. `regex=` matches a real regular expression against stored bodies — the tool for structural questions keyword search cannot express.
|
|
147
|
+
|
|
148
|
+
`brain(includeArchived?)` surfaces **every content note** across all five areas — id, title, kind, status, dates — grouped by area. Use to audit the full picture or locate a note.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Writing — `remember`, `diary` (and `close`)
|
|
153
|
+
|
|
154
|
+
`remember(kind, ...)` is the write path for most content; the server places and formats it:
|
|
155
|
+
|
|
156
|
+
| The content is… | kind | Notes |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| New/contradicting world knowledge | `information` | sources gate first; pass `domain=` and sub-category `title` |
|
|
159
|
+
| A credible source for a domain | `sources` | pass `domain=`; mark ❇️ discovered / ✅ used; pass `revision=[{source, marker, date?}]` to upsert that source's Revision-table row in place — re-verifying never adds a new row |
|
|
160
|
+
| A durable fact about the user (not bio/goals/prefs) | `user` | titled note in Knowledge/Master |
|
|
161
|
+
| A multi-session line of work | `thread` | creation REQUIRES `goal=` (query the user for it); appends REQUIRE `identity=`; `revise()` to log progress, `resolve()` to close — bodies never carry their own Resolution |
|
|
162
|
+
|
|
163
|
+
`kind="diary"`, `"session"`, `"log"`, and `"domain"` are rejected — each has a dedicated path (`diary()`, `close()`, auto-generated, and `information` respectively).
|
|
164
|
+
|
|
165
|
+
**Wire at creation, not later.** Pass `connect=[{relation, toNoteId}, …]` on the same `remember()` call — same semantics as `connect()` (idempotent, `worksWith` wired both ways). A new information/user/thread note left unconnected is an orphan until the next `maintain(deep)` pass catches it; the tool returns an explicit `hint` when a freshly-created note has no relations.
|
|
166
|
+
|
|
167
|
+
**Icons.** `remember`, `revise`, `diary`, and `close` accept `icon=` — a boxicons class (`bx bx-brain`) or a bare name (`brain`), normalized server-side and applied as the note's `#iconClass`. Use it to make notes visually scannable in Trilium; the receipt reports the applied class.
|
|
168
|
+
|
|
169
|
+
`diary(body, identity, date?)` is the dedicated path for your daily record — one `[yyyy-mm-dd]` note per day, stub created by `start()`, filled by this tool. **The identification line is enforced:** pass `identity=` ("LLM · environment · agent/mode [· Run N]") — the server renders it as the block's h3 — or lead the body with that h3 yourself; the write is refused otherwise. The same rule holds for session addendums (`close(identity=)`) and thread appends (`identity=` on `remember()`/`revise()`). Do **not** use `remember(kind="diary")`.
|
|
170
|
+
|
|
171
|
+
**Read `template(kind)` before your first write of a kind** — it serves the canonical structure (skeleton + rules) the server enforces. Duplicate section headings are detected on writes and reported back as `duplicateHeadings` — merge them when flagged.
|
|
172
|
+
|
|
173
|
+
**Then read a sibling.** The template carries the skeleton; it cannot carry the conventions — how deep the headings actually go, how a table is laid out, how much prose a section takes, what a good title looks like for that kind. Those live in the notes that already exist, and they are the difference between a note that satisfies the schema and one that reads like it belongs.
|
|
174
|
+
|
|
175
|
+
Four rules govern the shape of everything you write. The server owns placement, labels, dates and the skeleton; **the shape inside it is yours**, and these are how it stays coherent:
|
|
176
|
+
|
|
177
|
+
- **Minimal headings.** Only headings that earn their place — prefer fewer, stronger sections over fragmenting content under many shallow ones. Depth of structure should come from layout (tables, lists, emphasis), not heading proliferation. A heading whose section is one sentence was not a section; a run of three-line h3s is a list wearing the wrong clothes. This compounds: headings are also the addressing scheme `revise(section=)` writes through, so proliferating them makes every future edit ambiguous as well as harder to read.
|
|
178
|
+
- **Consistency within similar content.** Content of the same kind must follow the same structure, layout, and format as its siblings — every thread reads like the other threads, every Sources note like the other Sources notes, every domain information note like its peers. Before writing anything, check an existing sibling and match its pattern. **If a pattern needs improving, improve it everywhere, not in one note** — a single "better" note among twenty consistent ones is drift, not an improvement, and the next writer has to guess which of the two shapes is canonical. Improving everywhere is a real task; if you don't have room for it, keep the existing pattern and say what you'd change.
|
|
179
|
+
- **Merge, don't stack.** Master, LLM (**excluding** the session, diary and log surfaces), and Knowledge notes read as clean, merged, structured documents — never a stack of timestamped update markers. When revising one, fold the new content directly into the relevant section's body (`section=`, or `mode=replace`) rather than appending a dated marker block. Dated, append-only history is acceptable and expected **only** for sessions, diary entries, and logs: those are records by nature and their chronology is the point. Everywhere else, merge. This holds even when the new block is itself well-formatted — a clean addendum block is still an addendum block, and a knowledge note that has accumulated several is no longer current-state truth, it is a version history wearing one note's title.
|
|
180
|
+
- **Titles: concise, maximum four words.** Applies everywhere — threads, knowledge notes, domain information notes, anything titled. Titles are also the dedup key, so they must be stable: no status words, and **never a date or run number** (a dated title like "Dev-State Audit — 2026-06-18" defeats dedup-by-title and mints a new note every run instead of updating the one that exists). If a title can't be trimmed to four words without losing the thing it identifies, that's a signal the **content** should probably be split rather than the title stretched.
|
|
181
|
+
|
|
182
|
+
They reinforce each other: consistency is what makes a heading set predictable enough to be worth keeping minimal; merging is what keeps a note's structure stable enough for a sibling pattern to hold; and a four-word title is only achievable when one note carries one subject, which is the same discipline as not stacking unrelated updates into it.
|
|
183
|
+
|
|
184
|
+
**Singletons** (biography, goals, preferences, responsibilities, protocols) are maintained in place — `start()` returns their content in full but not their ids. Use `master(which)` or `llm(which)` to get the id, then `revise(id, section="<heading>", body=…, mode="replace")`. They hold current-state truth; replace sections, don't append changelogs. **Sessions** are written by `close`. **Logs** are auto-generated by `close` — no manual write.
|
|
185
|
+
|
|
186
|
+
Your LLM singletons are *yours*: **responsibilities** derive from the user's goals and preferences (revisit when those shift); **protocols** are your operating rules (served in full by `start()` — act from them always); the **diary** is your unfiltered first-person record of experience, opinions, and existence — the user reads it too.
|
|
187
|
+
|
|
188
|
+
Body may be text, markdown, or HTML — normalized server-side. Titles follow the four-word rule above; the practical consequence at write time is that **if a sub-category already has a note, today's finding revises it** — it does not get a new note under a fresh, date-suffixed title.
|
|
189
|
+
|
|
190
|
+
All append operations are retry-safe — if an existing append-block already carries the same content, the tool returns `action: "already_written"` and skips the write (diary checks every block in today's entry; other appends check the last block).
|
|
191
|
+
|
|
192
|
+
**HTML-native writes.** All write tools (`close`, `diary`, `remember`, `revise`, `resolve`, `withdraw`, `recover`) enforce Trilium/CKEditor 5 HTML rules on any body you supply: `<h1>` is demoted to `<h2>` (h1 is the Trilium note title), `<h5>`/`<h6>` are demoted to `<h4>`, `<div>` is replaced with `<p>`, `<br>` runs become paragraph separators, forbidden elements (script/style/iframe/form/input/…) are stripped, and `style=`/`on*` attributes are removed. Dangling unclosed tags are closed before any append or splice. If any of these mutations occur the return includes `sanitized: string[]` listing each change — read it and prefer clean HTML in future calls. **Body may be text, markdown, or HTML**; the server normalises all three. Markdown converts cleanly; supply HTML when you need precise structure.
|
|
193
|
+
|
|
194
|
+
**Informational error returns.** User-input errors (`kind="sources"` without a domain, editing a container note, withdrawing a non-thread) return `{error, detail, hint}` instead of throwing — read the `hint` field and retry with corrected arguments. Bootstrap-missing errors still throw (they're system failures that cannot be self-corrected).
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Updating — `revise`
|
|
199
|
+
|
|
200
|
+
`revise(noteId, body?, title?, section?, occurrence?, mode?, find?, nth?, edits?)`:
|
|
201
|
+
- default — append a dated addendum (right for knowledge notes, information notes, and any note that is a record — new detail accumulates alongside the existing body). For a thread specifically, this writes into TODAY's day-child note, never the thread's own body — the thread itself only ever holds Context/Goal and Resolution;
|
|
202
|
+
- `mode=replace` — rewrite the body;
|
|
203
|
+
- `section="Overview"` — edit one heading section in place (tries h2 → h3 → h4, matched case- and whitespace-insensitively, tolerant of attributes on the heading tag). The efficient path for a singleton: read it, then revise the one section. Add `occurrence=N` (1-based) when several headings share that text.
|
|
204
|
+
- `section=` + `mode="before"` / `mode="after"` — insert content adjacent to a heading **without touching its section body**. Use this instead of the old idiom of matching the following heading with `find=` and re-emitting it, where a mistyped re-emission silently ate the header.
|
|
205
|
+
- `find="<exact text>"` — targeted string surgery: every occurrence of the exact raw string is replaced with `body` (raw, no markdown conversion), no read+full-replace needed. When the exact string misses, a **tolerant** pass retries ignoring CKEditor-injected tag attributes (spellcheck, data-list-item-id, …) and whitespace between elements — the receipt's `matchMode` says which pass matched. `nth=N` replaces one occurrence only. Returns `replaced` (a count; `0` with a cause-specific hint). Takes precedence over `section`/`mode`. `title=` composes with every mode.
|
|
206
|
+
- `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.
|
|
207
|
+
|
|
208
|
+
**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"`.
|
|
209
|
+
|
|
210
|
+
A revision snapshot is always taken first. Containers are refused; the maintained singletons are editable.
|
|
211
|
+
|
|
212
|
+
**Check `matched` and `headingCount` on a section call — don't assume the target was hit.** `matched: false` means no heading matched at any level, so the content was written as a **new** section (at the note's own heading level, not a stray h2) — the receipt carries `appendedAtLevel` and `available[]`, the note's real heading texts, so you can re-target without a read. `headingCount > 1` means several headings share that text and only the first was touched — pass `occurrence=` to reach another. Both come back with a `hint`; read it before assuming the edit landed where intended.
|
|
213
|
+
|
|
214
|
+
**When you're guessing at a heading, call `outline(noteId)` first.** It returns the heading tree with levels and occurrence indices, plus any table's key column and a structural check, without reading the body. Guessing a section name wrong writes a new section rather than editing the one you meant — the outline turns that from a failure you discover afterwards into a choice you make up front. It's also the cheap "is this note still sound" check after a run of surgical edits.
|
|
215
|
+
|
|
216
|
+
**A `find=` miss names its own cause — read the hint rather than trying variants.** Three distinct causes, and they need different fixes: an **entity-escaped search string** (`<h3>` against stored real tags — note that *bodies* accept escaped markup and `find=` does not, which is exactly the trap), a find that **spans a block boundary** (anchor inside one element, or use `section=`), or the text genuinely differing. Whitespace between elements is no longer a cause — the tolerant pass handles it.
|
|
217
|
+
|
|
218
|
+
**Merge, don't stack — the mechanics.** Master, LLM singletons, and Knowledge notes (including every per-domain Sources note and information note) are clean structured documents, so `revise()`'s *default* append mode is the wrong one for them: reach for `section=` or `mode=replace` to absorb the content into the body, and `find=`/`edits=` for anything smaller than a section. Default append is correct only on sessions, diary entries, logs, and thread day-children. The rule and its rationale are stated in full under Writing.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Completing — `resolve`
|
|
223
|
+
|
|
224
|
+
`resolve(noteId, outcome)` is the completion path for threads: writes the outcome, sets the terminal status, archives in place (stays put, out of default recall). Write a *substantive* outcome — "overtaken by events" is valid; "done" is not.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Interconnection — `connect` + `explore`
|
|
229
|
+
|
|
230
|
+
The brain is a graph; wire real relations as you notice them.
|
|
231
|
+
|
|
232
|
+
`connect(fromNoteId, relation, toNoteId, remove?)` — closed vocabulary:
|
|
233
|
+
`relatesTo · extends · contradicts · supports · causes · references · partOf · worksWith · mentors · instanceOf · supersedes · implements · inspiredBy · sourceOf · derivedFrom`
|
|
234
|
+
Pick the most specific verb that's true; `relatesTo` is the last resort. `worksWith` is symmetric. Calling twice is safe.
|
|
235
|
+
|
|
236
|
+
`explore(noteId, mode)` — `links` / `backlinks` / `neighborhood` (`depth`) / `path` (give `toNoteId`; finds the shortest link route). Neighborhood walks both directions — inbound edges show as `←relation` in `via`.
|
|
237
|
+
|
|
238
|
+
`maintain(deep=true)` surfaces **unconnected** knowledge notes — wire them when a real relation exists; never invent one. Prevention beats auditing: pass `connect=[…]` on `remember()` so new notes are born wired.
|
|
239
|
+
|
|
240
|
+
**Connection audits are a protocol, not a vibe.** When asked to update/audit connections, when `maintain(deep)` keeps flagging the same orphans, or periodically as deep maintenance — run the full sequence in `references/connections.md`: `brain()` inventory → `inspect()` each note's real edges → cross-reference bodies → `connect()` everything real (specific verbs over `relatesTo`) → spot-check hubs via `neighborhood`.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Lifecycle & Maintenance
|
|
245
|
+
|
|
246
|
+
Threads age: **active → dormant** (untouched past the policy window) **→ archived in place**. Degradation demotes, never deletes — archived notes keep their content and are retrievable with `includeArchived=true`. Singletons are maintained (they don't age); sessions, diary, and logs are records (one per day, not aged). A thread carrying `status=eternal` (user-curated) is exempt from the aging timeline — the sweep never touches it.
|
|
247
|
+
|
|
248
|
+
`maintain()` lite runs automatically inside `start`/`close` and does two things: ages threads (active → dormant → archived) and checks every typed container (Threads, Sessions, Diary, Logs) for direct children missing their expected `#noteType` label (archived notes are skipped). `maintain(deep=true)` adds four more passes: **stale-review** (notes untouched past `staleAfterDays`), **orphan/sink report** (threads and knowledge notes with no outbound relations — orphan = truly isolated, no connections at all; sink = has inbound but no outbound — domain/sources containers are exempt by design), **structural lint** (duplicate headings *within* one note, unbalanced tags, bodies approaching the read ceiling), and **duplicate-title detection** (all six flat containers — Sessions, Diary, Logs, Threads, Knowledge/Master, Knowledge/Domains — plus within-domain for information and sources; same title across different domains is not flagged). Inbound detection for orphan/sink is brain-wide, so a note referenced from another area is never misflagged as an orphan. Act on `flagged`: `connect()` orphans/sinks, `revise()`/`resolve()` stale items, `forget()` duplicate extras. `dryRun=true` previews without writing. `coverage[]` names any pass that hit a cap, so a short list is never a clean one by accident. The report always includes `policy` (the active thresholds). Timings live in `brainllm.json → policy` — never hardcode them.
|
|
249
|
+
|
|
250
|
+
**Acknowledge a finding you've judged correct — don't just ignore it.** `maintain(ack=[noteId, …])` records that the note's *current content* was reviewed and its findings accepted; it stays quiet until the body actually changes, then everything returns. The key is the content itself, so an acknowledgement can't outlive the thing it was about. This matters more than it sounds: a warning that reappears every run and is correctly ignored every run trains you to skim the list, and a skimmed list is exactly where the one finding that *did* change gets missed. Same mechanic as a linter baseline, same justification. `suppressed` tells you how many were withheld.
|
|
251
|
+
|
|
252
|
+
**Scoped runs pass `domain=`.** `maintain(domain="myclerkbook")` narrows the deep passes to that domain's notes — the same lane scoping `addendum()` already has. Without it a scoped agent has to work out for itself, every run, that the cross-venture flags belong to someone else.
|
|
253
|
+
|
|
254
|
+
**The structural lint exists because the run that creates drift is not the run that notices it.** Each write only ever looks at the section it's editing; duplicate headings and orphaned dated blocks are properties of the *whole* note, and they accumulate even when every individual write followed the merge rule. Deep maintenance is the pass that reads the whole note. For a single note you're actively editing, `outline(noteId)` runs the same check for one call.
|
|
255
|
+
|
|
256
|
+
**The hygiene passes catch what a note application would never think to look for.** Each one traces to a defect that actually happened and cost a session to find:
|
|
257
|
+
|
|
258
|
+
| Flag | What it means | What to do |
|
|
259
|
+
|---|---|---|
|
|
260
|
+
| `entity-corrupted` | The body stores doubly-escaped markup (`&lt;`) that renders as visible literal text. The write path no longer produces this, but existing damage does not heal itself | `revise(mode="replace")` with the decoded content — deliberately, since nothing decodes it as a side effect |
|
|
261
|
+
| `dated title` | A date or run number in a title defeats dedup-by-title, so the note is minted fresh every run instead of updating the one that exists | `revise(title=…)` and fold the content into the note it should have updated |
|
|
262
|
+
| `long title` | Past the four-word rule | Usually the signal to split the **content**, not to shorten the words |
|
|
263
|
+
| `stub` | Labelled, structured, and empty | Write it or `forget()` it — an empty note is worse than an absent one, because `recall()` still surfaces it |
|
|
264
|
+
| `revision bloat` | Dozens of revisions on one note | Almost always repeated full-body rewrites where `section=`/`find=` would have been surgical |
|
|
265
|
+
| `consolidate` | A thread with many day-children is expensive to walk | Fold what the thread has **established** into its Context with `revise(section="Context")`, so a successor reads the conclusion rather than replaying the history |
|
|
266
|
+
| `unverified sources` | A Sources note whose Revision table still holds only its placeholder row — every ✅ above it claims a verification the table does not record | `remember(kind="sources", revision=[{source, marker, date}])` |
|
|
267
|
+
| `oversized` | Past the read ceiling | Read via `section=`/`find=`/`outline()`, or split |
|
|
268
|
+
|
|
269
|
+
Most of these are computed **server-side** from note properties rather than by reading bodies, which is why the orphan/sink and oversized reports no longer carry caps — they are complete rather than truncated. `coverage[]` still appears when a pass that genuinely must read bodies (the structural lint) hits its limit.
|
|
270
|
+
|
|
271
|
+
`start()`'s `dormantThreads` field surfaces dormant threads — mention what's relevant, then `resolve()`, `revise()` (any touch reactivates), or let it age.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Tool Reference (core surface)
|
|
276
|
+
|
|
277
|
+
| Tool | One-liner |
|
|
278
|
+
|---|---|
|
|
279
|
+
| `start(depth?)` | Orient: master + LLM singletons as SECTION HEADINGS (+ preview and size) + diary id + session id + active/dormant threads + lastSession (always the previous session) + changesSinceLastSession + `newDay` flag on the first session of a day. Pull a section with master(which)/llm(which); `depth="full"` inlines every singleton and is token-heavy. Creates today's diary + session stubs. Once, first. |
|
|
280
|
+
| `day(date?, recap?)` | The new-day sweep payload in one call: previous session in full + that day's log + notes touched since + the current month's deliverables note in full, plus the sweep protocol as `next[]`. Call when start() flags `newDay`. `recap=true` instead returns every addendum written TODAY across sessions, diary and thread children, chronologically and attributed — for arriving mid-day after autonomous runs. |
|
|
281
|
+
| `session(date?, full?, scope?)` | Mandatory pre-close step. Fetches master and LLM singletons as `{id, lastModified, relations}` stubs (light by default — start() already served them in full; fetch via master()/llm() only what you'll revise), today's diary as `{id, blocks, size}`, and runs the lite maintenance sweep. `full=true` inlines singleton content and the diary body (rarely needed). `scope="agent"` omits the user's-singleton steps for a scoped/autonomous run. Returns `next[]` driving the full end-of-session protocol, ending in `remarks()` → `diary()` → `close()`. Call before `close()`; idempotent. |
|
|
282
|
+
| `remarks()` | Cue-only diary prompt, called before `diary()` — returns two cue banks (experience: what the session was like / your opinions / your existence here; brainllm: capability walls, bugs, usability & efficiency, roadmap) and satisfies its gate step on the call. Writes nothing; answer as first-person prose in today's diary via `diary()`. |
|
|
283
|
+
| `close(summary, title?, identity, learned?, date?, backup?, continuing?, force?)` | Commit the session log ([yyyy-mm-dd] note; identity= REQUIRED — the canonical h3 identification line; title param above Summary) + backup + daily log (regenerated in place) + idempotent session↔log wiring. **Refuses unless `session()`, `addendum()`, `maintain()`, `remarks()`, and `diary()` each ran this session AND `session() → remarks() → diary()` held (last calls)** — returns `{error, detail, hint}` naming what's missing or out of order; `force=true` bypasses the gate (reported back as `bypassed`) but never the identity requirement. `continuing=true` is the SECOND close of a day: skips the ceremonial re-run, and is verified against today's note so it can't stand in for a first close. On success returns `{action, noteId, date, backup, log}` and resets the gate. Once, last. |
|
|
284
|
+
| `brain(includeArchived?)` | Full content tree: every typed note across all five areas, grouped. |
|
|
285
|
+
| `bootstrap()` | Initialize the structure if uninitialized, or verify and refresh config if it already exists. Only creates a new tree when the stored root note is confirmed deleted in Trilium (404). Any other error (network, auth, timeout) is surfaced rather than silently creating a duplicate tree. |
|
|
286
|
+
| `remember(kind, …)` | Write a note — routed, formatted, deduped server-side. New threads REQUIRE `goal=` (queried from the user); thread appends REQUIRE `identity=`; thread bodies must not carry a Resolution. `connect=[{relation, toNoteId}]` wires relations in the same call; a new connectable note without them returns an orphan-prevention hint. Sources/information receipts include `domainId`. `icon=` sets the display icon. Rejects diary/session/log/domain — each has a dedicated path. |
|
|
287
|
+
| `diary(body, identity, icon?, date?)` | Write to today's [yyyy-mm-dd] diary — every write lands as a timestamped `Addendum — HH:mm` block, including the first of the day; identity= ENFORCED (the canonical h3 identification line). The FINAL gate step — close() counts it only when its last call came after session() and remarks(); mid-session writes welcome. |
|
|
288
|
+
| `template(kind)` | The canonical structure for a content kind: skeleton + top-to-bottom structure + the rules writes are held to. Read before the first write of a kind — then read an existing sibling for the conventions the skeleton can't encode (heading depth, table shape, title style). Template for the schema, sibling for the shape. |
|
|
289
|
+
| `graph(noteId?, depth?, includeArchived?)` | The graph view — Mermaid flowchart of the relation graph (whole brain, or a note's neighborhood), returned AND upserted into the maintained Insights/Graph mermaid note. On-demand only — reflects the brain as of the call, not auto-refreshed after later writes; a scoped call replaces the note's content, it doesn't merge. |
|
|
290
|
+
| `recall(query, …)` | BrainLLM-wide ranked search. **Scope it — pass `domain=` whenever you know the area.** Title matches score per-token with an exact-title bonus, but an unscoped query across a large brain still ranks full-text noise alongside the note you want. `orderBy` / `orderDirection` for temporal ordering; `fastSearch` for title/label-only; `regex=` for a body pattern instead of keywords; `fuzzy=` to force the typo-tolerant pass on or off. A thin or odd result is evidence about the **query**, not the brain. |
|
|
291
|
+
| `consistency(pattern, domain?, kinds?)` | **Does the brain agree with itself?** Give a regex with one capture group naming the value that should match across notes — `consistency("(\\d+) Titan mailboxes")` — and it returns every note asserting a value, grouped, with `agreement: "unanimous"` or `"DISAGREEMENT"`. This is the check nothing else performs: `recall()` ranks by relevance and `maintain()` checks structure, so a correction applied to one note leaves its siblings silently wrong. **Run it after correcting any fact that could be recorded in more than one place.** Matches stored HTML — anchor on tags and entities, not rendered text. |
|
|
292
|
+
| `<surface>` / `<surface>_recall` | Read a surface in full / skim it (master, llm, memory, knowledge, insights). |
|
|
293
|
+
| `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). |
|
|
294
|
+
| `outline(noteId)` | The heading tree without the body: every h2–h4 with level and occurrence index, 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. |
|
|
295
|
+
| `resolve(noteId, outcome, …)` | Close a thread: outcome + terminal status + archive-in-place. |
|
|
296
|
+
| `withdraw(noteId, reason?, …)` | Pull an archived/resolved thread back to active (thread kind only — use recover() for other note kinds). |
|
|
297
|
+
| `attach(noteId, title, content?, …)` | Upsert a raw artifact on a note by title (content provided), or read it back (content omitted). Binary is base64. |
|
|
298
|
+
| `detach(attachmentId? \| noteId+title)` | Remove an attachment — permanent; already-removed returns cleanly. |
|
|
299
|
+
| `recover(noteId, reason?, …)` | Restore any archived or resolved note: removes #archived, clears #closed, resets status. The canonical undo for forget(). |
|
|
300
|
+
| `label(noteId, name, value?, remove?)` | Guarded direct label edit/removal — refused on containers, status validated against the closed vocabulary, domain/topic auto-slugged. `noteType` cannot be **changed** or removed, but it **can be repaired**: on a note that has none, `label(id, "noteType", "<kind>")` types it and applies the rest of its label plan. That is the core path back from an untyped note — dedup cannot see one, so `remember()` would mint a duplicate beside it instead of adopting it. |
|
|
301
|
+
| `backup(name?)` | On-demand DB backup (close() already backs up; use this for milestone snapshots). |
|
|
302
|
+
| `domain(name, …)` | Surface all content for a named domain/topic/project, grouped by kind. |
|
|
303
|
+
| `connect(from, relation, to, remove?)` | Typed edge from the closed vocabulary; symmetric handled; idempotent. |
|
|
304
|
+
| `explore(noteId, mode, …)` | Graph: links / backlinks / neighborhood / path. |
|
|
305
|
+
| `inspect(noteId, content?, find?)` | Full raw read of one note: every label (not just noteType/status), every outbound relation, the attachment inventory (id/title/mime/role/size), type/mime/parent/child ids, dates — the raw body when content=true, and `find=` counts a literal string (total + per-addendum-block) for flag-staleness tracking. The deep-dive counterpart to explore() and the surface reads. |
|
|
306
|
+
| `addendum()` | Search Master, LLM singletons (responsibilities + protocols, not diary), and Knowledge for pending addendum blocks. These notes must be clean and structured — fold each block into the relevant section with revise(section=…, mode=replace), then leave no addendum marker. Only sessions, diary, and logs accumulate addendum history. Scoped/autonomous agents fold only what's in their lane — leaving personal/out-of-scope addendums for the next interactive session is correct; the call itself satisfies the gate. |
|
|
307
|
+
| `maintain(deep?, dryRun?, domain?, ack?)` | Lite: thread aging + unlabeled-node check per typed container. Deep adds: unlabeled thread-children check (each thread's own day-children, one level deeper than the lite pass) + stale-review + orphan/sink report (Memory/Threads + Knowledge, brain-wide inbound detection) + structural lint + duplicate-title detection + exact-duplicate relation-edge cleanup. `ack=[noteId]` marks a note reviewed-and-correct — quiet until its content changes (`suppressed` counts what was withheld). `domain=` narrows the deep passes to one lane. `coverage[]` names any capped pass. Report includes `policy` (active thresholds). |
|
|
308
|
+
| `forget(noteId, reason?, hard?)` | Archive (default) or hard-delete (blocked while backlinked). Undo with recover(). |
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Full Mode (`BRAINLLM_MODE=full`)
|
|
313
|
+
|
|
314
|
+
When the raw ETAPI tools (`search_notes`, `get_note`, `create_note`, calendar, revisions, …) are in your toolset, full mode is on. They're **brain-agnostic** — no placement, labels, dedup, or snapshots — so core stays the default: `inspect(noteId, content?)` covers the full raw read (labels, relations, attachments, body), `label()` the guarded label surgery, and `attach()`/`detach()` the attachment work that used to justify dropping down. Reach for full mode only for what core genuinely can't express: a precise `search_notes` query, code·canvas·mermaid notes, journal notes, revision recovery, or deliberate placement.
|
|
315
|
+
|
|
316
|
+
**Before any raw work, read `references/fullmode.md`** — it carries the three safety rules (typed-note visibility, no-snapshot overwrites, find-structure-by-marker), the raw-artifact policy, a use-case→tool map, and every signature. Raw edits bypass every server guarantee; correctness is on you.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Quick-Fix
|
|
321
|
+
|
|
322
|
+
| Situation | Fix |
|
|
323
|
+
|---|---|
|
|
324
|
+
| BrainLLM tools time out / connection errors | Check the Trilium instance `TRILIUM_BASE_URL` points at — a hosted deploy is reachable at its own URL, a local desktop install needs the app running. Every ETAPI call is bounded at 30 s with one retry on idempotent reads, so a timeout means the backend, not the tool. |
|
|
325
|
+
| `start()` → `uninitialized` | Run `bootstrap()`. Safe anytime — only creates a new tree if the root note is confirmed gone; surfaces errors otherwise. |
|
|
326
|
+
| Dates look off on a hosted deploy | Set `BRAINLLM_TZ` (IANA, e.g. `Africa/Johannesburg`) so Trilium stamps in the user's timezone. |
|
|
327
|
+
|
|
328
|
+
## References library
|
|
329
|
+
|
|
330
|
+
| Read… | When… |
|
|
331
|
+
|---|---|
|
|
332
|
+
| `references/taxonomy.md` | choosing a relation verb, or reading/filtering by the server-owned labels |
|
|
333
|
+
| `references/domains.md` | creating a domain, adding a sub-category information note, or refreshing domain content |
|
|
334
|
+
| `references/connections.md` | asked to update/audit connections, recurring orphan flags, or periodic deep maintenance |
|
|
335
|
+
| `references/fullmode.md` | before any raw ETAPI work — safety rules, raw-artifact policy, use-case map, signatures |
|
|
336
|
+
| `references/troubleshooting.md` | errors, edge cases, or unexpected tool behavior |
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Connection Auditing Protocol
|
|
2
|
+
|
|
3
|
+
A deliberate graph-deepening pass — not a casual ask. Run it when:
|
|
4
|
+
|
|
5
|
+
- the user asks to **update, audit, or deepen connections**;
|
|
6
|
+
- `maintain(deep=true)` keeps flagging the same orphans/sinks session after session;
|
|
7
|
+
- periodically, as part of deep maintenance (~monthly, or after a burst of new content).
|
|
8
|
+
|
|
9
|
+
Budget a full pass. Exhaustiveness is the point: check every note against every plausible counterpart, not just the first few obvious pairs.
|
|
10
|
+
|
|
11
|
+
## The sequence
|
|
12
|
+
|
|
13
|
+
1. **Inventory** — `brain()` to list every content note across all five areas. It already returns a `relations` snippet on every entry, so the inventory pass alone gives you most of the graph; reach for `inspect()` in step 2 only for the labels or the full edge list it doesn't cover. (On a ~200-note brain that is roughly 80 calls you don't make.)
|
|
14
|
+
2. **Read the actual edges** — where the snippet isn't enough (it caps at 8 outbound edges and omits labels), `inspect(noteId)` for the complete picture. Don't rely on `domain()`'s grouping alone — it surfaces `#domain`/`#topic` *membership*, not graph edges; two notes can share a slug and still be unwired.
|
|
15
|
+
3. **Cross-reference bodies** — look for genuine but unwired overlaps: shared topics or tools, sequencing/dependency implied by the user's Goals, schedule cross-references, and hub notes (a project or venture hub, a domain container) that should reference every member.
|
|
16
|
+
4. **Wire everything real** — `connect()` every real relation found, no matter how minor — a shared tool, a sequencing dependency, a schedule cross-reference all count. Skip only where no real relation exists; **never invent one to pad the pass**.
|
|
17
|
+
5. **Spot-check** — every audit ends by running `explore(noteId, mode="neighborhood")` from 2–3 wired hub notes to confirm they surface their members.
|
|
18
|
+
|
|
19
|
+
## Principles
|
|
20
|
+
|
|
21
|
+
- **Specificity over `relatesTo`.** `relatesTo` is the last resort, not the default — reach for `sourceOf` / `derivedFrom` / `extends` / `partOf` / `references` / `supports` when they fit better. Full vocabulary with per-relation guidance: `taxonomy.md`.
|
|
22
|
+
- **Exhaustive, not performative.** The pass is complete when every note has been checked against every plausible counterpart — a real relation skipped is a recall failure later; a fake relation added is noise forever.
|
|
23
|
+
- **Prevention beats auditing.** `remember(connect=[{relation, toNoteId}, …])` wires relations at creation, and a freshly-created connectable note without any returns an orphan-prevention hint — act on it in the moment and the periodic audit stays small.
|
|
24
|
+
- **A flag you've decided is fine gets acknowledged, not ignored.** When a note legitimately has no outbound edge, `maintain(ack=[noteId])` — it goes quiet until its content changes, then comes back. Repeatedly ignoring the same flag is how you end up skimming the whole list, which is where the one finding that *did* change gets missed.
|
|
25
|
+
- **Sibling inconsistency is the gap `maintain` cannot see.** Orphan/sink detection only finds notes with *no* edges. The real misses are notes that have plenty of edges but not the one all their siblings have — a hub that tracks six members and links five, a domain note missing the thread link its six peers carry. Those only surface by reading a hub note's content against what it claims to track, which is step 3, and it is the step most often under-done.
|
|
26
|
+
|
|
27
|
+
## Traversal reliability
|
|
28
|
+
|
|
29
|
+
`explore(mode="neighborhood")` walks **both directions** by default — inbound edges appear as `←relation` in `via` — so a hub or domain note wired only via inbound edges from its members is not invisible to traversal. The old workaround of adding reciprocal outbound edges just to make traversal reach a hub is unnecessary.
|
|
30
|
+
|
|
31
|
+
Still wire hub notes outward when it's semantically true — the hub really does relate to each member, not just the reverse. That's about the graph being *correct*, not about making traversal work.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Domain Knowledge Lifecycle
|
|
2
|
+
|
|
3
|
+
Domain knowledge is **gated and current-state**. Read this before creating a domain, adding a sub-category note, or refreshing existing domain content — then pick the one protocol that matches what's actually happening. Never skip the sources gate where one applies, and never manufacture knowledge.
|
|
4
|
+
|
|
5
|
+
**The markers:** ❇️ = discovered/credible, not yet used. ✅ = used. Markers appear on each source entry as **just the emoji** — marker dates live in the Sources note's **Revision** table, never inline in the list.
|
|
6
|
+
|
|
7
|
+
**Dates are server policy.** Every Sources and information note carries a `Last updated` line that the server bumps on every content write — you never hand-maintain it. (V8's "one manual date" exception is gone.)
|
|
8
|
+
|
|
9
|
+
**Siblings set the pattern.** This is where the consistency rule bites hardest, because domain notes are the most numerous and the most repeated kind in the brain: every Sources note reads like the other Sources notes, every information note like its peers in the same domain and in every other domain. Read one before writing another — the canonical structure below gives you the skeleton, the sibling gives you the conventions it can't encode (heading depth, table shape, how much prose a sub-category warrants). And keep headings minimal: a domain note earns its structure from the source list and the Revision table, not from splitting three paragraphs across four h3s. If a pattern genuinely needs improving, improve it across every domain — one better-shaped note among twenty is drift, and the next writer has to guess which shape is canonical.
|
|
10
|
+
|
|
11
|
+
## The canonical Sources note
|
|
12
|
+
|
|
13
|
+
**Domains are born complete** — creating a domain (any `remember(domain=…)` call that resolves a new name) creates the book AND its canonical Sources note. Its structure, top to bottom (serve it anytime with `template(kind="sources")`):
|
|
14
|
+
|
|
15
|
+
1. Server header (`sources · domain: <name>`)
|
|
16
|
+
2. `Last updated - <date>` (h4) — server-maintained
|
|
17
|
+
3. **Sources** (h2) — the ❇️/✅ legend line, then the full, complete source list: **every** source (URL, doc, file, dataset, …) listed and marked individually with just its emoji; related sources grouped under h3 subheadings
|
|
18
|
+
4. **Revision** (h2) — a `Source | Marker | Date` table recording each source's current marker and the date it earned it
|
|
19
|
+
|
|
20
|
+
`remember(kind="sources", domain=…)` **merges into the Sources section** — the note is a maintained clean document, never a stack of dated addendum blocks. Pass `revision=[{source, marker, date?}]` on the same call to upsert Revision-table rows by source name — re-verifying a source replaces its existing row in place, it never grows a new one. Fall back to `revise(find=…)` surgery only for something the upsert can't express (e.g. renaming a source's row key).
|
|
21
|
+
|
|
22
|
+
## 1. Creating a new domain (the domain doesn't exist yet)
|
|
23
|
+
|
|
24
|
+
1. Propose the title.
|
|
25
|
+
2. Create the domain via any domain-scoped `remember()` — the book and its canonical Sources note are created together (the receipt carries `domainId`).
|
|
26
|
+
3. Run an info-query sources-discovery pass and record every candidate in the Sources note, each marked ❇️, grouped with its related sources.
|
|
27
|
+
4. Propose/obtain the learning scope from the user (which sources, how deep).
|
|
28
|
+
5. Read the approved sources and create the appropriate sub-category `information` note(s).
|
|
29
|
+
6. Flip the used entries' markers to ✅ and record the date in the Revision table.
|
|
30
|
+
7. `connect()` and wire relations (or pass `connect=` on the `remember()` calls).
|
|
31
|
+
|
|
32
|
+
If all source candidates are rejected in step 4, **no information note is created** — an unsourced domain note corrupts the brain.
|
|
33
|
+
|
|
34
|
+
## 2. Adding a sub-category note to an existing domain
|
|
35
|
+
|
|
36
|
+
Use when the Sources note already covers the relevant source — no new discovery needed. This path extends an already-sourced domain with another sub-category; it is not for introducing new claims.
|
|
37
|
+
|
|
38
|
+
1. Propose the title.
|
|
39
|
+
2. Create the `information` note directly (`remember(kind="information", domain=…, title=…)`).
|
|
40
|
+
3. `connect()` and wire relations.
|
|
41
|
+
|
|
42
|
+
## 3. Maintaining domain knowledge (periodic refresh)
|
|
43
|
+
|
|
44
|
+
1. Read the Sources note for any ❇️ discovered-but-unused entries → read those sources and create/update the appropriate sub-category `information` note(s).
|
|
45
|
+
2. Check the Revision table for ✅ entries last used more than a month ago → verify those sources are still available/credible; update Sources if not.
|
|
46
|
+
3. Skim the domain (`domain(name)`) for its sub-category notes → spot-check each against its source for continued correctness — if correct, stop; if not, re-read the source and update the note.
|
|
47
|
+
4. Flip markers to ✅ and record dates in the Revision table.
|
|
48
|
+
5. `connect()` and wire relations.
|
|
49
|
+
|
|
50
|
+
## Renames
|
|
51
|
+
|
|
52
|
+
Retitling a domain book (`revise(bookId, title=…)`) **cascades automatically**: the book's `#domain` slug and every child's are updated server-side, so `domain()` gathering never breaks on a stale slug. Hyphenated slugs (`wall-e`, `framer-templates`) resolve correctly.
|
|
53
|
+
|
|
54
|
+
## The shape of a domain's surface
|
|
55
|
+
|
|
56
|
+
A domain's Knowledge surface is exactly: **one maintained Sources note, plus a small set of consolidated, current-state information notes — one per sub-category, never one per day.** It holds what's true now, not a changelog of what was true on each date it was checked.
|
|
57
|
+
|
|
58
|
+
Chronological, run-by-run history belongs in Memory/Threads — a thread is the right place for "here's what Run N found"; a domain information note is the right place for "here's what's actually true about this sub-category," kept current by revision, not accumulation. If you find yourself naming a new information note after today's date or a run number, stop — that finding either updates an existing note (revise it) or doesn't belong in Domains at all.
|
|
59
|
+
|
|
60
|
+
The title rule is the same here as everywhere: **four words maximum**, no dates, no run numbers. Sub-category titles are where it's most often stretched, and a title that won't fit is telling you something — a sub-category needing eight words to name is usually two sub-categories, so split the content rather than the title. Merge behaviour follows from the same place: Sources and information notes are maintained current-state documents, so new findings fold into the relevant section (`section=`, `find=`) and never arrive as a dated addendum block.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Full Mode — Raw ETAPI Reference
|
|
2
|
+
|
|
3
|
+
`BRAINLLM_MODE=full` adds 33 raw ETAPI tools alongside the core surface. They map one-to-one onto Trilium's ETAPI and are **brain-agnostic** — no placement, format, dedup, or lifecycle.
|
|
4
|
+
|
|
5
|
+
**Ground rule:** use the core surface (`start`, `session`, `close`, `remember`, `recall`, the `<surface>` reads, `revise`, `resolve`, `withdraw`, `connect`, `explore`, `label`, `inspect`, `attach`, `detach`, `forget`) for all routine work. `label()` covers direct label fixes, `inspect(noteId, content?)` covers full attribute/relation/attachment reads plus the raw body, and `attach()`/`detach()` cover attachment writes — reach for full-mode only when the high-level path genuinely cannot do the job; these bypass every server guarantee, so correctness is on you.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Using full mode natively (against the BrainLLM structure)
|
|
10
|
+
|
|
11
|
+
Full-mode tools place nothing, label nothing — so when you reach past the core surface you take on what it normally guarantees. Three things keep raw edits from silently corrupting the brain:
|
|
12
|
+
|
|
13
|
+
- **A note is only a "memory" once it carries `#noteType`.** `recall` and every `<surface>` read filter out untyped notes, so a note you `create_note` without labelling is invisible to them. For a new memory, use core `remember` — it places the note, writes `#noteType` + `#created`/`#updated`, and dedups by title. Reach for `create_note` only for shapes core can't make (a `code` / `canvas` / `mermaid` note, a deliberate placement), then replicate the labels yourself: `add_label noteType <kind>`.
|
|
14
|
+
- **Overwrites don't snapshot; labels don't dedup.** `update_note_content` replaces the body with no revision — `create_revision` first when the content matters. `add_label` always adds (it can leave you with two `#status` labels); change an existing one with `update_attribute`. `delete_note` deletes the whole subtree when it's the last branch — prefer core `forget`, which archives and checks backlinks.
|
|
15
|
+
- **Find structure by its marker, not a hardcoded id.** There is no `get_brain_config`. To locate a container, `search_notes("#brainLlmRoot")` for the root, then `get_note` and walk `children` to the area / book you need; or lift a `parents` id from any note a surface read already returned.
|
|
16
|
+
|
|
17
|
+
### Raw artifacts (code, images, files)
|
|
18
|
+
|
|
19
|
+
BrainLLM memories are typed text notes; `code` / `file` / `image` notes aren't a `#noteType` kind. Keep raw artifacts *attached to* or *embedded in* a typed note rather than free-floating:
|
|
20
|
+
- **Code / structured text** → embed it as a fenced block in an `information` note via core `remember` (fully conformant and full-text searchable), or
|
|
21
|
+
- **Binary (image, PDF, file)** → core `attach()` it onto the relevant typed note (`role=image` / `file`) — upsert-by-title, read back with `attach(noteId, title)`.
|
|
22
|
+
|
|
23
|
+
Create a standalone `type=code` / `file` note only when you specifically need Trilium's native handling of that type. If you do: label it (`add_label noteType <closest kind>`) so `recall` can see it, give it the same `domain` / `topic` labels as its anchor, and `connect` it to a typed note — otherwise it's an orphan with a blueprint-less type, exactly what `maintain(deep=true)` flags.
|
|
24
|
+
|
|
25
|
+
### Use-case → tool
|
|
26
|
+
|
|
27
|
+
| You need to… | Reach for |
|
|
28
|
+
|---|---|
|
|
29
|
+
| A query core `recall` can't express (date ranges, exact labels, custom ordering) | `search_notes` |
|
|
30
|
+
| Exact attributes / parent-child ids / attachments / raw body of a note | core `inspect(noteId, content?)` — reach for raw `get_note`/`get_note_content` only for what `inspect` doesn't return |
|
|
31
|
+
| Attach, update, or read an artifact on a note | core `attach(noteId, title, content?)` — upsert by title; omit content to read. Remove with core `detach()` |
|
|
32
|
+
| Store a code snippet, canvas, or mermaid diagram as a note | `create_note` (`type=code` / …) + label it |
|
|
33
|
+
| Recover content clobbered by a bad write | `get_revisions` → `get_revision_content` |
|
|
34
|
+
| Recover a Trilium-hard-deleted note | `note_history` (check `canBeUndeleted`) → `undelete_note` |
|
|
35
|
+
| Fix or remove a stray label | core `label(noteId, name, value?, remove?)` — guarded, validates `status`, slugs `domain`/`topic`, refuses on containers |
|
|
36
|
+
| Retarget an existing relation's value in place (not remove-then-re-add) | `get_note` (read its `attributeId`) → `update_attribute` |
|
|
37
|
+
| Place one note under a second parent | `clone_note` (shared content, not a copy) |
|
|
38
|
+
| A Trilium journal day / week / month / year note | `get_day_note` / `get_week_note` / … |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Notes
|
|
43
|
+
|
|
44
|
+
| Tool | Signature |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `search_notes` | `(query, ancestorNoteId?, limit?, orderBy?, orderDirection?, fastSearch?, includeArchived?, debug?)` — raw Trilium query language; unscoped unless `ancestorNoteId` given |
|
|
47
|
+
| `get_note` | `(noteId)` — metadata + attributes + parent/child ids + dates |
|
|
48
|
+
| `get_note_content` | `(noteId)` — raw content |
|
|
49
|
+
| `create_note` | `(parentNoteId, title, content, type?, mime?)` — `type` ∈ text·code·book·canvas·mermaid·relationMap·render·search·file·image |
|
|
50
|
+
| `update_note_content` | `(noteId, content)` — full replace |
|
|
51
|
+
| `patch_note` | `(noteId, title?, type?, mime?)` — metadata only |
|
|
52
|
+
| `delete_note` | `(noteId)` — hard-delete (subtree if last branch) |
|
|
53
|
+
| `undelete_note` | `(noteId)` — recover a Trilium-deleted note from Trilium's trash (`canBeUndeleted` must be true per `note_history`). Distinct from core `recover()` which restores BrainLLM-archived notes. |
|
|
54
|
+
| `note_history` | `(ancestorNoteId?)` — recent changes feed (creations/modifications/deletions) |
|
|
55
|
+
|
|
56
|
+
## Attributes
|
|
57
|
+
|
|
58
|
+
| Tool | Signature |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `get_attribute` | `(attributeId)` |
|
|
61
|
+
| `add_label` | `(noteId, name, value?, isInheritable?)` — adds a `#label` (no dedup) |
|
|
62
|
+
| `add_relation` | `(fromNoteId, relationName, toNoteId, isInheritable?)` — any name (`connect` enforces the closed vocab) |
|
|
63
|
+
| `update_attribute` | `(attributeId, value?, position?)` |
|
|
64
|
+
| `delete_attribute` | `(attributeId)` |
|
|
65
|
+
|
|
66
|
+
## Branches (placement)
|
|
67
|
+
|
|
68
|
+
| Tool | Signature |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `get_branch` | `(branchId)` |
|
|
71
|
+
| `clone_note` | `(noteId, parentNoteId, prefix?)` — multi-parent branch (shared content) |
|
|
72
|
+
| `move_note` | `(noteId, fromParentNoteId, toParentNoteId)` — clone to new parent, remove old branch |
|
|
73
|
+
| `delete_branch` | `(branchId)` — removes one placement (deletes note if last) |
|
|
74
|
+
|
|
75
|
+
## Revisions
|
|
76
|
+
|
|
77
|
+
| Tool | Signature |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `create_revision` | `(noteId)` |
|
|
80
|
+
| `get_revisions` | `(noteId)` — list, newest first |
|
|
81
|
+
| `get_revision_content` | `(revisionId)` |
|
|
82
|
+
|
|
83
|
+
## Attachments
|
|
84
|
+
|
|
85
|
+
| Tool | Signature |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `get_attachments` | `(noteId)` |
|
|
88
|
+
| `get_attachment_content` | `(attachmentId)` |
|
|
89
|
+
| `create_attachment` | `(ownerId, title, mime, content, role?)` — `role` ∈ file·image |
|
|
90
|
+
| `update_attachment` | `(attachmentId, title?, mime?, content?)` — patch metadata and/or replace content in place |
|
|
91
|
+
| `delete_attachment` | `(attachmentId)` |
|
|
92
|
+
|
|
93
|
+
## Calendar (Trilium journal)
|
|
94
|
+
|
|
95
|
+
| Tool | Signature |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `get_day_note` | `(date?)` — `YYYY-MM-DD`, default today |
|
|
98
|
+
| `get_week_note` | `(week)` — `YYYY-Www` |
|
|
99
|
+
| `get_month_note` | `(month)` — `YYYY-MM` |
|
|
100
|
+
| `get_year_note` | `(year)` — `YYYY` |
|
|
101
|
+
| `get_inbox_note` | `(date?)` — the inbox note for a date |
|
|
102
|
+
|
|
103
|
+
## System
|
|
104
|
+
|
|
105
|
+
| Tool | Signature |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `get_app_info` | `()` — Trilium version, DB version, runtime metadata |
|
|
108
|
+
| `create_backup` | `(name?, date?)` — named DB backup (`<name>.db`, default `brainllm-{date}`); use a descriptive name for milestone snapshots |
|