brainllm 12.4.1 → 12.5.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 +5 -4
- package/dist/index.js +271 -557
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/brainllm/SKILL.md +143 -308
- package/skills/brainllm/references/domains.md +30 -46
- package/skills/brainllm/references/taxonomy.md +3 -2
- package/skills/brainllm/references/troubleshooting.md +5 -3
package/skills/brainllm/SKILL.md
CHANGED
|
@@ -7,386 +7,221 @@ description: "Persistent memory and knowledge graph via the BrainLLM (Trilium) M
|
|
|
7
7
|
|
|
8
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
9
|
|
|
10
|
-
**
|
|
10
|
+
**You supply content; the server owns form** — placement, labels, dedup, relations, dates, backups, archival and each kind's skeleton (`template(kind)`). If you find yourself doing bookkeeping, a tool does it for you. The shape *inside* a note is yours, held to the four writing rules below.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
**Operate from it natively.** `start()` loads who the user is and who you are here — act from both unprompted. When the topic is the user's world, read the brain before answering from training; it is authoritative where it speaks.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
---
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
## The two content rules
|
|
17
17
|
|
|
18
|
-
**
|
|
18
|
+
1. **Timeless by default.** Singletons, thread books, user-knowledge notes, domain books and information notes hold what is true regardless of date. **No state, version, status, incident or decision history in them.** State lives in each domain's **Current State** note (latest measured value, with its measurement date). History and decisions live in **thread entries and sessions**, where the date is native. This keeps knowledge correct and cheap to maintain.
|
|
19
|
+
2. **Universal usefulness.** If one sentence says it, one sentence is enough. If it is rarely relevant to the interactions, automations and workflows the brain serves, it goes. Every note is read by some session; its size is paid on every read.
|
|
19
20
|
|
|
20
21
|
---
|
|
21
22
|
|
|
22
23
|
## The Protocol
|
|
23
24
|
|
|
24
25
|
```
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
DURING
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
read(ids=[…]) ← batched multi-note read — several bodies, one round trip
|
|
35
|
-
claim(...) ← does the brain still agree with the WORLD? register a checkable assertion; verify on a schedule
|
|
36
|
-
revise(...) ← edit a note (section= for whole sections, find=/edits= for surgery, mode=before/after to insert, mode=remove to delete)
|
|
37
|
-
split(noteId, sections, into) ← the trim affordance: lift whole sections into a new note, leaving a pointer — the write half of the oversized-note problem
|
|
38
|
-
outline(noteId) ← the heading tree + structural check — read BEFORE a section= edit you're guessing at
|
|
39
|
-
diff(noteId) ← what your last write actually changed, against the revision it snapshotted
|
|
40
|
-
resolve(...) ← close a thread with its outcome
|
|
41
|
-
withdraw(noteId) ← pull an archived/resolved thread back to active
|
|
42
|
-
recover(noteId) ← restore any archived/resolved note (undo forget)
|
|
43
|
-
label(noteId, name, …) ← fix a stray/drifted label directly — the guarded escape hatch
|
|
44
|
-
connect(...) ← wire a real relation the moment you notice it
|
|
45
|
-
graph(noteId?, depth?) ← the graph view: Mermaid render of the relation graph → Insights/Graph note
|
|
46
|
-
attach(noteId, title, …) ← upsert or read a raw artifact (file/image/blob) on a note
|
|
47
|
-
detach(...) ← remove an attachment (by id, or noteId + title)
|
|
48
|
-
backup(name?) ← milestone snapshot before a large restructure
|
|
49
|
-
SESSION END session() ← mandatory pre-close; singleton stubs + diary + sweep (light by default); follow next[]
|
|
50
|
-
[revise() master singletons with session observations about the user]
|
|
51
|
-
[revise() LLM singletons with session observations about yourself]
|
|
52
|
-
addendum() ← find pending blocks and durably mark the gate
|
|
53
|
-
maintain() ← audit and fix brain hygiene
|
|
54
|
-
remarks() ← the diary cues: experience/opinions/existence + BrainLLM remarks
|
|
55
|
-
diary(body, identity) ← the day's closing record, written with the cues in hand — gate counts it only after remarks()
|
|
56
|
-
close(summary, title?, identity) ← refuses until all steps ran AND session → remarks → diary held; identity= ENFORCED; force=true to override
|
|
57
|
-
close(..., continuing=true) ← the SECOND close of a day: skips the ceremonial re-run, verified against today's note
|
|
58
|
-
PERIODIC maintain(deep=true) ← when start flags items, or ~weekly (also dedupes exact-duplicate relation edges)
|
|
59
|
-
maintain(ack=[noteId]) ← a flag you've judged correct: quiet until that note's content changes
|
|
60
|
-
maintain(repair=[noteId]) ← fix an entity-corrupted body in place; revision taken first
|
|
61
|
-
ANYTIME assembly() ← what the brain HOLDS: titles by surface, each surface's purpose alongside — the awareness read
|
|
62
|
-
brain() ← the full inventory: id/kind/status/parent/dates per note (auditing, locating)
|
|
63
|
-
day(recap=true) ← everything written TODAY across every surface, chronologically
|
|
64
|
-
inspect(noteId, content?, section?, find?) ← every label/relation/attachment; content=true adds the body (section= narrows it); find= counts a literal flag and shows the nearest text on a miss
|
|
26
|
+
START start() once, before responding
|
|
27
|
+
[day()] when start() reports newDay
|
|
28
|
+
DURING remember(...) the moment something is worth keeping
|
|
29
|
+
revise(...) / split(...) edit in place; split an oversized note on its seams
|
|
30
|
+
resolve / withdraw / recover thread and note lifecycle
|
|
31
|
+
connect(...) wire a real relation when you see one
|
|
32
|
+
read: <surface>, domain, recall, read(ids), outline, inspect, assembly, brain
|
|
33
|
+
verify: consistency(...), claim(...)
|
|
34
|
+
END session() → [update singletons] → addendum() → maintain() → remarks() → diary() → close()
|
|
65
35
|
```
|
|
66
36
|
|
|
67
|
-
`start()`
|
|
68
|
-
|
|
69
|
-
**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.
|
|
70
|
-
|
|
71
|
-
`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).
|
|
72
|
-
|
|
73
|
-
`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.
|
|
74
|
-
|
|
75
|
-
`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.
|
|
76
|
-
|
|
77
|
-
**`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.
|
|
78
|
-
|
|
79
|
-
**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.
|
|
80
|
-
|
|
81
|
-
**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.
|
|
82
|
-
|
|
83
|
-
**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.
|
|
84
|
-
|
|
85
|
-
`remarks()` is the diary-cue step before `diary()` — a **cue-only content 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 durably marks the pre-close gate; 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.
|
|
37
|
+
`start()` returns the date, **preferences and protocols in full**, the other singletons as section headings (pull one with `master(which)`/`llm(which)`, `section=` for one section; `depth="full"` inlines all), today's diary and session ids, active and dormant threads, the previous session and notes changed since. `newDay: true` on the first session of a day → call `day()` (previous session, its log, notes touched since). `day(recap=true)` returns everything written today, in order, across sessions, diary and thread entries.
|
|
86
38
|
|
|
87
|
-
`
|
|
39
|
+
**Close protocol.** `session()` returns singleton stubs, `pending=` (what each remaining step has to do), `audit=` (do the singletons agree, and do the LLM ones still serve the master ones) and `next[]`. Update master singletons with what you learned about the user and LLM singletons with what you learned about yourself, fold any addenda (`addendum()`), `maintain()`, `remarks()` for diary cues, `diary()` your closing record, then `close(summary, identity)`. `close()` refuses until those steps ran and `session → remarks → diary` held; the gate is durable across restarts. `force=true` bypasses a step with nothing to do (reported). `continuing=true` is a second close on the same day. Scoped or autonomous runs pass `session(scope="agent")`.
|
|
88
40
|
|
|
89
|
-
**Write during the session, not at the end
|
|
41
|
+
**Write during the session, not at the end** — a fact written mid-conversation survives a crash.
|
|
90
42
|
|
|
91
43
|
---
|
|
92
44
|
|
|
93
|
-
## The Structure
|
|
45
|
+
## The Structure
|
|
94
46
|
|
|
95
47
|
```
|
|
96
48
|
BrainLLM
|
|
97
|
-
├── Master biography · goals · preferences
|
|
98
|
-
├── LLM responsibilities · protocols · selfcorrection · Diary/
|
|
99
|
-
├── Memory Sessions/ · Threads/
|
|
100
|
-
├── Knowledge Master · Domains
|
|
101
|
-
└── Insights Logs/ · Graph · Claims/
|
|
49
|
+
├── Master biography · goals · preferences (singletons about the user)
|
|
50
|
+
├── LLM responsibilities · protocols · selfcorrection · Diary/ (singletons about you + one diary note per day)
|
|
51
|
+
├── Memory Sessions/ · Threads/ (one session note per day + multi-session threads)
|
|
52
|
+
├── Knowledge Master/ (user knowledge) · Domains/<domain>/{Sources, information notes}
|
|
53
|
+
└── Insights Logs/ · Graph · Claims/
|
|
102
54
|
```
|
|
103
55
|
|
|
104
|
-
|
|
56
|
+
**Singletons derive from each other.** Master holds the user: biography, goals, preferences. LLM holds you, **derived from master**: responsibilities serve the goals and preferences, protocols are the operating rules that meet the responsibilities, self-correction holds only general rules learned from your mistakes. When master changes, re-derive LLM; `session()`'s `audit=` asks this every close.
|
|
105
57
|
|
|
106
|
-
**
|
|
58
|
+
**Threads have two shapes.** A thread book holds its **Goal** (what the thread is for, a sentence or two) and optional standing constraints; never progress (a register thread such as Escalations also keeps its one maintained register table there). Its children carry the content:
|
|
107
59
|
|
|
108
|
-
|
|
60
|
+
| Shape | Children | Use for |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| **dated** (default) | one `[yyyy-mm-dd]` entry per active day, `Addendum — HH:mm` blocks with an identification line | a line of work: the history lives here |
|
|
63
|
+
| **collection** | one **titled** entry per item, each a maintained document edited in place | a list of things: ideas, candidates, specs |
|
|
109
64
|
|
|
110
|
-
|
|
65
|
+
Create with `remember(kind="thread", title, goal, shape?)`. Dated: append with `remember(kind="thread", title, body, identity)` or `revise(threadId, body, identity)`; `memory(id)` indexes the children newest first, `memory(id, date=)` resolves a day. Collection: add with `remember(kind="threadEntry", thread=<id>, title, body)`, change with `revise(<entry id>)`; `memory(id)` lists entries alphabetically. Appending to a collection book is refused with those two paths.
|
|
111
66
|
|
|
112
|
-
|
|
67
|
+
**Records** — sessions, diary, logs and dated thread entries — are one note per day, every write a timestamped `Addendum — HH:mm` block. They are never trimmed or rewritten.
|
|
113
68
|
|
|
114
|
-
|
|
69
|
+
---
|
|
115
70
|
|
|
116
|
-
|
|
71
|
+
## Where things go
|
|
117
72
|
|
|
118
73
|
```
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
↓ neither → do not capture (passing remark / already covered by training)
|
|
74
|
+
worth keeping?
|
|
75
|
+
├─ about the user ─→ biography / goals / preferences (revise the section, in place)
|
|
76
|
+
│ otherwise a Knowledge/Master note (remember kind="user")
|
|
77
|
+
├─ world knowledge beyond or against training ─→ a domain (sources gate — references/domains.md)
|
|
78
|
+
│ measured state → that domain's Current State · timeless knowledge → an information note
|
|
79
|
+
├─ what happened / what was decided ─→ the relevant thread entry (or the session log)
|
|
80
|
+
└─ passing remark or already known from training ─→ don't capture
|
|
127
81
|
```
|
|
128
82
|
|
|
129
|
-
**
|
|
130
|
-
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.)
|
|
131
|
-
2. `revise(id, section="<heading>", body=…, mode="replace")` — rewrite the relevant section. Replace, don't append a dated log.
|
|
132
|
-
|
|
133
|
-
**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).
|
|
134
|
-
|
|
135
|
-
**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:
|
|
136
|
-
|
|
137
|
-
- 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.
|
|
138
|
-
- **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.
|
|
139
|
-
- 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.
|
|
140
|
-
- `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.
|
|
83
|
+
**Domains** are one maintained **Sources** note plus one information note per sub-category, including **Current State**, revised in place. Every claim traces to a Sources entry (❇️ discovered, ✅ used); `remember(kind="sources", revision=[{source, marker, date}])` upserts the Revision row. Creating a domain creates its Sources note. Read `references/domains.md` before creating a domain, adding a sub-category or refreshing one.
|
|
141
84
|
|
|
142
85
|
---
|
|
143
86
|
|
|
144
|
-
## Reading
|
|
145
|
-
|
|
146
|
-
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.
|
|
87
|
+
## Reading
|
|
147
88
|
|
|
148
|
-
|
|
|
89
|
+
| Need | Tool |
|
|
149
90
|
|---|---|
|
|
150
|
-
| `master(which)` / `
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
**`assembly()` and `brain()` answer different questions and are not interchangeable.** Assembly is *awareness* — titles and shape, sized to be read. Brain is *inventory* — ids, kinds, statuses, parents, dates, for auditing or locating one note. Reaching for the inventory when the question was awareness is the common mistake, and it costs several times the tokens to answer worse.
|
|
165
|
-
|
|
166
|
-
`brain(includeArchived?)` surfaces **every content note** across all five areas — id, title, kind, status, dates, **and `parent`** — grouped by area. Use to audit the full picture or locate a note. Each group is a **flat list of every descendant**, so a domain book and its information/sources children arrive interleaved and the ordering *looks* nested when it is not; read `parent`, never position. (Inferring parentage from the ordering once cost an 8,259-byte note, overwritten by a "fix" for a gap that was not there.)
|
|
167
|
-
|
|
168
|
-
**When you need to find where a claim lives inside a big note, reach for `outline()` first — not `recall()` or `consistency()`.** Those two answer "which notes mention this" and "do the notes agree"; neither tells you *which section of one note* to edit, which is the question you actually have before a surgical write. The heading tree costs one call and turns a guessed `section=` into a chosen one.
|
|
91
|
+
| a singleton | `master(which)` / `llm(which)` — `section=` for one section |
|
|
92
|
+
| a thread or session | `memory(id)` — dated index or collection entries; `date=` for one day |
|
|
93
|
+
| a knowledge note | `knowledge(id)` — `section=` for one section |
|
|
94
|
+
| a day's change log | `insights(date?)` |
|
|
95
|
+
| skim a surface | `<surface>_recall` |
|
|
96
|
+
| everything about an area | `domain(name)` — the reliable path; reach here first |
|
|
97
|
+
| search | `recall(query, domain=…)` — scope it; `regex=` for structure; fuzzy hits are leads |
|
|
98
|
+
| several bodies at once | `read(ids=[…])` (≤ 10) |
|
|
99
|
+
| what the brain holds | `assembly(area?)` — titles by surface with purposes |
|
|
100
|
+
| inventory / locate by id | `brain()` — read `parent`, not position |
|
|
101
|
+
| a note's heading tree | `outline(id)` — before any `section=` edit you're unsure of |
|
|
102
|
+
| raw labels, relations, body | `inspect(id, content?, section?, find?)` |
|
|
103
|
+
|
|
104
|
+
Every read that can be large takes `section=`; a note past the read ceiling cannot be returned whole.
|
|
169
105
|
|
|
170
106
|
---
|
|
171
107
|
|
|
172
|
-
## Writing — `remember`, `diary
|
|
173
|
-
|
|
174
|
-
`remember(kind, ...)` is the write path for most content; the server places and formats it:
|
|
175
|
-
|
|
176
|
-
| The content is… | kind | Notes |
|
|
177
|
-
|---|---|---|
|
|
178
|
-
| New/contradicting world knowledge | `information` | sources gate first; pass `domain=` and sub-category `title` |
|
|
179
|
-
| 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 |
|
|
180
|
-
| A durable fact about the user (not bio/goals/prefs) | `user` | titled note in Knowledge/Master |
|
|
181
|
-
| 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 |
|
|
182
|
-
|
|
183
|
-
`kind="diary"`, `"session"`, `"log"`, `"domain"` and `"claim"` are rejected — each has a dedicated path (`diary()`, `close()`, auto-generated, `information`, and `claim()` respectively).
|
|
184
|
-
|
|
185
|
-
**Dedup-by-title is what makes `remember()` idempotent, and on a generic title it is a loaded weapon.** *Current State*, *Sources*, *Technology Stack* and *Product and Business* each exist in four or more domains, so a call you believe is creating a note can silently **replace** one — with a clean receipt reading `action: "updated"`. **Pass `mustCreate=true` whenever you intend to create rather than add**, and it refuses instead, naming the note that already exists. Read `action` on every receipt regardless: `updated` where you expected `created` means something was overwritten.
|
|
186
|
-
|
|
187
|
-
**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.
|
|
188
|
-
|
|
189
|
-
**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.
|
|
190
|
-
|
|
191
|
-
`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")`.
|
|
192
|
-
|
|
193
|
-
**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.
|
|
194
|
-
|
|
195
|
-
**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.
|
|
196
|
-
|
|
197
|
-
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:
|
|
198
|
-
|
|
199
|
-
- **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.
|
|
200
|
-
- **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.
|
|
201
|
-
- **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.
|
|
202
|
-
- **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.
|
|
203
|
-
|
|
204
|
-
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.
|
|
205
|
-
|
|
206
|
-
**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.
|
|
108
|
+
## Writing — `remember`, `diary`, `close`
|
|
207
109
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
110
|
+
| Content | Call |
|
|
111
|
+
|---|---|
|
|
112
|
+
| about the user, not bio/goals/prefs | `remember(kind="user", title, body)` |
|
|
113
|
+
| domain knowledge | `remember(kind="information", domain, title, body)` |
|
|
114
|
+
| a domain source | `remember(kind="sources", domain, body, revision?)` |
|
|
115
|
+
| a new line of work | `remember(kind="thread", title, goal, shape?)` — ask the user for the goal |
|
|
116
|
+
| a dated thread append | `remember(kind="thread", title, body, identity)` |
|
|
117
|
+
| a collection entry | `remember(kind="threadEntry", thread, title, body)` |
|
|
118
|
+
| your daily record | `diary(body, identity)` |
|
|
119
|
+
| the session log | `close(summary, identity, title?, learned?)` |
|
|
120
|
+
|
|
121
|
+
- **Dedup is by title.** Generic titles (Current State, Sources) exist in many domains — pass `mustCreate=true` when you mean to create, and read `action` on every receipt.
|
|
122
|
+
- **Wire at creation:** `connect=[{relation, toNoteId}]` on the same call. An unconnected note is an orphan until wired.
|
|
123
|
+
- **Identity line** (`"LLM · environment · agent/mode [· Run N]"`) is required on diary, close and dated thread appends.
|
|
124
|
+
- **Every note carries an icon except logs.** The server sets the kind default at creation (a thread entry takes its thread's icon) and the sweep backfills any missing; `icon=` picks a better one (boxicons class or bare name). Removing an icon is refused.
|
|
125
|
+
- Bodies may be text, markdown or HTML; the server normalises them and reports `sanitized[]`.
|
|
126
|
+
- `diary`, `session`, `log`, `claim` and `domain` have dedicated paths; `remember` refuses them.
|
|
127
|
+
|
|
128
|
+
**Four writing rules** (each applies to every note you touch):
|
|
129
|
+
|
|
130
|
+
- **Minimal headings.** Only headings that earn their place; depth comes from tables, lists and emphasis. Headings are also what `section=` addresses.
|
|
131
|
+
- **Match your siblings.** Read an existing note of the same kind and follow its shape; improve a pattern everywhere or nowhere.
|
|
132
|
+
- **Merge, don't stack.** Everything except records is a clean document: fold new content into its section (`section=`/`find=`), never a dated addendum.
|
|
133
|
+
- **Titles ≤ 4 words, no dates or run numbers** (they defeat dedup). A title that won't trim means the content wants splitting.
|
|
134
|
+
|
|
135
|
+
Read `template(kind)` before your first write of a kind, then read a sibling.
|
|
217
136
|
|
|
218
137
|
---
|
|
219
138
|
|
|
220
139
|
## Updating — `revise`
|
|
221
140
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
**
|
|
235
|
-
|
|
236
|
-
A
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
**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. On notes 15k+ the headings also carry a first-block **preview** of their section — the orientation read for oversized notes, where the heading list alone assumes you already know which section you want. 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.
|
|
241
|
-
|
|
242
|
-
**A `find=` miss names its own cause AND shows you the stored text — read the hint rather than trying variants.** Four distinct causes, each with a different fix: an **entity-escaped search string** (see the asymmetry above), a find that **spans a block boundary** (anchor inside one element, or use `section=`), a string **already consumed by an earlier edit in the same `edits=` array** (the expected result, not a failure), or the text genuinely differing. In that last case the receipt carries `matchedUpTo` — the longest fragment of your string that *is* present — and `storedNearby`, the stored text around it, so you can see how it really differs instead of guessing a variant. Whitespace between elements is never the cause; the tolerant pass handles it.
|
|
243
|
-
|
|
244
|
-
**Verify a run of surgical edits with `diff(noteId)`, not another full read.** It compares the note against the revision your last write snapshotted, so "what did that actually change" is one call. Trusting receipts alone is how a section replace that displaced four subsections went unnoticed.
|
|
245
|
-
|
|
246
|
-
**Concurrent sessions serialize automatically.** Every write-classified tool runs behind a process-wide FIFO lock, so two agents — an interactive session and an automated run, or two automated lanes — writing through the same hosted instance queue in arrival order instead of racing, and a read-modify-write can no longer silently discard the other's write. Reads stay fully parallel. You never manage this; it is why several sessions can work the brain at once without lost writes. Scope, stated honestly: the lock is per process — a human editing in the Trilium UI inside a handler's read→PUT window is outside it, and revisions remain the recovery story there.
|
|
247
|
-
|
|
248
|
-
**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.
|
|
249
|
-
|
|
250
|
-
**When a note is past the read ceiling, the right move is `split()`, not more surgery.** A note too big to return whole and flagged by `maintain()`'s oversized check has usually grown two or three subjects under one title — that is a structural problem surgery can't fix, only a split can. `split(noteId, sections=[…], into="<title>")` lifts whole sections (heading + body, nested sub-sections with their parent) into a new note under the same parent, typed and labelled like the source, and leaves a pointer plus a `~references` edge in the source. Splitting on a semantic seam (a live queue from a do-not-re-raise index, one sub-category from another) turns two months of brittle `find=` surgery into one call. Outline the note first to name the seams; sections that don't match come back in `missed=` and stay put.
|
|
141
|
+
| Mode | Effect |
|
|
142
|
+
|---|---|
|
|
143
|
+
| default | dated addendum — right only for records; a dated thread's append lands in today's entry |
|
|
144
|
+
| `mode="replace"` | whole body |
|
|
145
|
+
| `section="<heading>"` | replace that section's **whole** body (h2→h3→h4; `occurrence=` for repeats) |
|
|
146
|
+
| `section=` + `mode="before"/"after"` | insert a sibling block around the whole section |
|
|
147
|
+
| `section=` + `mode="prepend"` | insert at the top of the section's body |
|
|
148
|
+
| `section=` + `mode="remove"` | delete the section |
|
|
149
|
+
| `find="<exact stored text>"` | replace every occurrence (`nth=` for one) |
|
|
150
|
+
| `edits=[{find, body}]` | several surgeries in one read and one write |
|
|
151
|
+
|
|
152
|
+
- A revision is taken before every content write; `diff(noteId)` shows what the last write changed.
|
|
153
|
+
- **Check the receipt.** `matched: false` means a new section was written — `available[]` lists real headings and `didYouMean` catches typos; `strict=true` refuses instead. `headingCount > 1` means only the first match was touched. `replacedSubsections[]` names nested headings a section replace took with it.
|
|
154
|
+
- **`find=` matches stored HTML, not rendered text.** Pass tags literally; `outline()` gives the `raw` form of headings with inline markup. On a miss the hint names the cause and shows the stored text nearby.
|
|
155
|
+
- A section replace swaps everything under the heading — use `find=` for anything smaller.
|
|
156
|
+
- **Oversized note (past the read ceiling)?** It usually holds two subjects: `split(noteId, sections=[…], into="<title>")` moves whole sections to a new sibling and leaves a pointer.
|
|
157
|
+
- Concurrent writers are serialised server-side; reads stay parallel.
|
|
251
158
|
|
|
252
159
|
---
|
|
253
160
|
|
|
254
|
-
## Completing
|
|
161
|
+
## Completing and lifecycle
|
|
255
162
|
|
|
256
|
-
`resolve(noteId, outcome)`
|
|
163
|
+
- `resolve(noteId, outcome)` closes a thread with a substantive outcome ("done" is not one) and archives it in place.
|
|
164
|
+
- `withdraw(threadId)` reopens an archived or resolved thread; `recover(noteId)` restores any archived note; `forget(noteId)` archives (default) or hard-deletes when nothing links to it.
|
|
165
|
+
- Threads age **active → dormant → archived** by idle time; `status=eternal` exempts one. Singletons don't age; records are never trimmed.
|
|
166
|
+
- `label(noteId, name, value?)` fixes a stray label; it can type an untyped note (`noteType`) but never change a typed one.
|
|
257
167
|
|
|
258
168
|
---
|
|
259
169
|
|
|
260
|
-
## Interconnection — `connect
|
|
261
|
-
|
|
262
|
-
The brain is a graph; wire real relations as you notice them.
|
|
263
|
-
|
|
264
|
-
`connect(fromNoteId, relation, toNoteId, remove?)` — closed vocabulary:
|
|
265
|
-
`relatesTo · extends · contradicts · supports · causes · references · partOf · worksWith · mentors · instanceOf · supersedes · implements · inspiredBy · sourceOf · derivedFrom`
|
|
266
|
-
Pick the most specific verb that's true; `relatesTo` is the last resort. `worksWith` is symmetric. Calling twice is safe.
|
|
267
|
-
|
|
268
|
-
`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`.
|
|
170
|
+
## Interconnection — `connect`, `explore`, `graph`
|
|
269
171
|
|
|
270
|
-
`
|
|
271
|
-
|
|
272
|
-
**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`.
|
|
172
|
+
`connect(from, relation, to)` from a closed vocabulary: `relatesTo · extends · contradicts · supports · causes · references · partOf · worksWith · mentors · instanceOf · supersedes · implements · inspiredBy · sourceOf · derivedFrom`. Pick the most specific true verb; `worksWith` is symmetric; idempotent. `explore(noteId, mode)` walks links, backlinks, a neighborhood or a path. `graph()` renders Mermaid into Insights/Graph. For a full connection audit follow `references/connections.md`.
|
|
273
173
|
|
|
274
174
|
---
|
|
275
175
|
|
|
276
|
-
## Verification — `consistency`
|
|
277
|
-
|
|
278
|
-
A brain can be wrong in two directions, and they need different tools. **`consistency()` asks whether the brain agrees with itself. `claim()` asks whether it still agrees with the world.** Nothing else in the surface answers either question: `recall()` ranks by relevance and `maintain()` checks structure, so both failures sit in notes that read as authoritative.
|
|
279
|
-
|
|
280
|
-
**`consistency(pattern | subject, domain?, kinds?, staleAfterDays?)` — the same fact, recorded differently in several places.** Give a regex with one capture group naming the value that should agree — `consistency("([0-9]+) Titan mailboxes")` — and it returns every note asserting a value, grouped, with `agreement: "unanimous"` or `"DISAGREEMENT"`. **Run it after correcting any fact that could be recorded in more than one place**, because revising one note leaves its siblings silently wrong and every copy still looks authoritative. It matches both the stored HTML and a tag-stripped projection, so a phrase broken by an inline `<strong>` is found; it scans every in-scope note by default, because on a contradiction sweep a falsely clean result is worse than a slow one.
|
|
281
|
-
|
|
282
|
-
**Two additions make it the tool to reach for when you re-measure a fact.** `subject="<prose>"` is the no-regex-guessing mode: give it the fact in plain words and it returns every note asserting about that subject, however phrased — tokenized and matched on significant content words, so the word-order a hand-built pattern anchored on no longer hides the answer. `staleAfterDays=N` reports the **single-copy** figures: values asserted in exactly one note whose sole note was untouched N+ days. Those are the ones that rot silently — a single copy can only agree with itself, so `consistency()`'s verdict of "unanimous" is no evidence of currentness; the `staleSingles` list is where re-measurement is owed.
|
|
283
|
-
|
|
284
|
-
**`claim()` — an assertion that was true when written and may not be now.** Register one with `assertion=` and `check=`; record an outcome with `claimId=`, `holds=` and `evidence=`; read one with `claimId=` alone; list with no arguments at all. `maintain(deep=true)` then surfaces the lapsed, the never-verified and the broken, so "is the brain stale" becomes a query instead of something caught by luck.
|
|
176
|
+
## Verification — `consistency` and `claim`
|
|
285
177
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
**A verification without evidence is refused.** A verdict on its own is an assertion about an assertion, and a register full of those is worse than no register, because it looks like verification. Register the claims that would be *expensive to discover had gone stale* — not everything checkable.
|
|
178
|
+
- **`consistency(pattern | subject)`** — does the brain agree with itself? A regex with one capture group (or a fact in prose) returns every asserting note grouped by value. **Run it after correcting any fact that could be recorded in more than one place.** `staleAfterDays=N` reports values held in only one note untouched N+ days.
|
|
179
|
+
- **`claim(...)`** — does the brain still agree with the world? Register `assertion` + `check`; verify with `claimId` + `holds` + `evidence` (evidence required); read with `claimId`; list with no arguments. BrainLLM never runs the check — you do. Register claims that would be expensive to discover had gone stale.
|
|
289
180
|
|
|
290
181
|
---
|
|
291
182
|
|
|
292
|
-
##
|
|
293
|
-
|
|
294
|
-
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.
|
|
295
|
-
|
|
296
|
-
`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.
|
|
297
|
-
|
|
298
|
-
**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.
|
|
299
|
-
|
|
300
|
-
**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.
|
|
183
|
+
## Maintenance — `maintain`
|
|
301
184
|
|
|
302
|
-
|
|
185
|
+
Lite runs inside `start`/`close` (thread aging, label checks). `maintain(deep=true)` adds:
|
|
303
186
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
| Flag | What it means | What to do |
|
|
307
|
-
|---|---|---|
|
|
308
|
-
| `entity-corrupted` | The body stores doubly-escaped markup (`&lt;`, `&nbsp;`) that renders as visible literal text. The producer — a blanket escape on the markdown path — is fixed as of V10.3, but existing damage does not heal itself. Matches that live only **inside code spans** are a note documenting the trap, not carrying it — those are exempt and never flagged | `maintain(repair=[noteId])` — one level unwound outside code spans, revision taken first. Compose with `dryRun` to preview |
|
|
309
|
-
| `claim source-changed` | A claim's `~derivedFrom` source note was revised **after** the claim's last verification — the check no longer covers what the note now says. Nobody predicts at write time which assertions will age; this surfaces staleness as "the fact's home moved" instead | Re-verify with `claim(claimId=…, holds=…, evidence=…)` or `retire=true` the claim |
|
|
310
|
-
| `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 |
|
|
311
|
-
| `long title` | Past the four-word rule | Usually the signal to split the **content**, not to shorten the words |
|
|
312
|
-
| `stub` | Labelled, structured, and empty | Write it or `forget()` it — an empty note is worse than an absent one, because `recall()` still surfaces it |
|
|
313
|
-
| `revision bloat` | Dozens of revisions on one note | Almost always repeated full-body rewrites where `section=`/`find=` would have been surgical |
|
|
314
|
-
| `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 |
|
|
315
|
-
| `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}])` |
|
|
316
|
-
| `oversized` | Past the read ceiling | Read via `section=`/`find=`/`outline()`, or split |
|
|
317
|
-
| `deletion` | A note under the brain root was soft-deleted within the last 7 days — the window before Trilium's eraser removes both the note and the feed's own record of the deletion. Unscoped deep runs only (a deleted note's labels are gone with it, so it cannot be attributed to a lane) | `undelete_note` if unintended, or leave it; the trace self-destructs after Trilium's retention window |
|
|
318
|
-
| `size trajectory` | The note grew ≥30% since the last deep run that read it and is 20k+ — corrections compounding faster than the note can absorb. A rate, kept alongside the `oversized` threshold precisely because a permanently-oversized note stops hearing that one | Trim or split deliberately now, per the Note Trimming protocol — not under pressure later |
|
|
319
|
-
|
|
320
|
-
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.
|
|
321
|
-
|
|
322
|
-
`start()`'s `dormantThreads` field surfaces dormant threads — mention what's relevant, then `resolve()`, `revise()` (any touch reactivates), or let it age.
|
|
323
|
-
|
|
324
|
-
---
|
|
325
|
-
|
|
326
|
-
## Tool Reference (core surface)
|
|
327
|
-
|
|
328
|
-
| Tool | One-liner |
|
|
187
|
+
| Finding | Action |
|
|
329
188
|
|---|---|
|
|
330
|
-
|
|
|
331
|
-
|
|
|
332
|
-
|
|
|
333
|
-
|
|
|
334
|
-
|
|
|
335
|
-
|
|
|
336
|
-
|
|
|
337
|
-
|
|
|
338
|
-
|
|
|
339
|
-
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
| `consistency(pattern \| subject, domain?, kinds?, staleAfterDays?)` | **Does the brain agree with itself?** Two modes. *Pattern:* 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"`. *Subject:* give a fact in prose — `consistency(subject="the two scheduled agents")` — and get back every note asserting about it, however phrased, no regex guessing; the tool tokenizes the prose and matches a majority of its significant words, so the phrasing that defeated a hand-built pattern is found. `staleAfterDays=N` adds the **single-copy** report: figures asserted in exactly one note whose sole note was untouched N+ days — the ones that rot silently, because a single copy can only agree with itself. 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.** Matched against **both** the stored HTML and a tag-stripped projection, so a phrase split by an inline `<strong>` or `<code>` is found and so is a pattern anchored on tags. Scans every in-scope note by default — Trilium's `%=` pre-filter reads a striptags'd copy and silently drops notes, and on a contradiction sweep a falsely clean result is worse than a slow one; `fast=true` opts back into it. |
|
|
345
|
-
| `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. |
|
|
346
|
-
| `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. |
|
|
347
|
-
| `<surface>` / `<surface>_recall` | Read a surface in full (`section=` for one section) / skim it (master, llm, memory, knowledge, insights). |
|
|
348
|
-
| `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), `mode="prepend"` to insert at the top of the section body below its 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. `strict=true` refuses a section miss AND a body needing structural repair. Retitling a domain book cascades `#domain` to its children. `Last updated` lines bumped server-side. Snapshot taken on content writes (not metadata-only). |
|
|
349
|
-
| `split(noteId, sections=[…], into="<title>")` | **The trim affordance — the write half of the oversized-note problem.** Lifts whole sections (heading + body, nested sub-sections go with their parent) out of one note into a new note created under the same parent, typed and labelled like the source, and leaves a pointer in the source plus a `~references` edge. A note past the read ceiling becomes two readable notes instead of one unusable one. Sections are matched by heading TEXT (the same contract `section=` uses); repeated headings are consumed first-match-per-request; sections that don't exist come back in `missed=` and stay in place. A revision is taken of the source first. Refused on containers and singletons — for content notes. |
|
|
350
|
-
| `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`). On notes 15k+ each heading also carries a first-block `preview` of its section — the orientation read for oversized notes. 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. |
|
|
351
|
-
| `resolve(noteId, outcome, …)` | Close a thread: outcome + terminal status + archive-in-place. |
|
|
352
|
-
| `withdraw(noteId, reason?, …)` | Pull an archived/resolved thread back to active (thread kind only — use recover() for other note kinds). |
|
|
353
|
-
| `attach(noteId, title, content?, mime?, role?, encoding?)` | Upsert a raw artifact on a note by title (content provided), or read it back (content omitted). Binary is standard base64 with `encoding="base64"`; reads return a base64 envelope and MIME. |
|
|
354
|
-
| `detach(attachmentId? \| noteId+title)` | Remove an attachment — permanent; already-removed returns cleanly. |
|
|
355
|
-
| `recover(noteId, reason?, …)` | Restore any archived or resolved note: removes #archived, clears #closed, resets status. The canonical undo for forget(). |
|
|
356
|
-
| `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. |
|
|
357
|
-
| `backup(name?)` | On-demand DB backup (close() already backs up; use this for milestone snapshots). Returns the logical name and `backupStatus`; ETAPI does not expose the resulting `.db`/`.tnbackup` path. |
|
|
358
|
-
| `domain(name, …)` | Surface all content for a named domain/topic/project, grouped by kind. |
|
|
359
|
-
| `connect(from, relation, to, remove?)` | Typed edge from the closed vocabulary; symmetric handled; idempotent. |
|
|
360
|
-
| `explore(noteId, mode, …)` | Graph: links / backlinks / neighborhood / path. |
|
|
361
|
-
| `inspect(noteId, content?, section?, 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, `section=` narrows `content=true` to one section, and `find=` counts a literal string (total + per-addendum-block + which heading each match sits under, in `sections`) for flag-staleness tracking — returning the nearest present fragment and the stored text around it when the count is zero. The deep-dive counterpart to explore() and the surface reads. |
|
|
362
|
-
| `addendum()` | Search Master, LLM singletons (responsibilities + protocols + self-correction, not diary), and Knowledge for pending addendum blocks. The call durably marks the pre-close gate; fold each block into the relevant section with revise(section=…, mode=replace), then leave no addendum marker. 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. |
|
|
363
|
-
| `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). `repair=[noteId]` unwinds one level of entity corruption in place, revision first. `domain=` narrows the deep passes to one lane — and a scoped run says so in `coverage`, so a clean scoped report is never mistaken for a clean brain. An unscoped run that finds things returns `laneHint`: **leave flags that are not yours rather than acking them** — an acknowledgement asserts you read the note. `coverage[]` names any capped pass. Report includes `policy` (active thresholds). |
|
|
364
|
-
| `forget(noteId, reason?, hard?)` | Archive (default) or hard-delete (blocked while backlinked). Undo with recover(). |
|
|
189
|
+
| stale | revise, resolve, or `ack=[id]` if correct as it stands |
|
|
190
|
+
| orphan / sink | `connect()` |
|
|
191
|
+
| dated prose | a timeless note carrying dates — move state to Current State, history to a thread entry |
|
|
192
|
+
| duplicate heading / unbalanced tags / incomplete | fix with `revise` |
|
|
193
|
+
| oversized / section-edit-risk / size trajectory | `section=` reads, `split()` |
|
|
194
|
+
| dated / long title | retitle, fold into the note it should have updated, or split |
|
|
195
|
+
| stub | write it or `forget()` it |
|
|
196
|
+
| entity-corrupted | `maintain(repair=[id])` |
|
|
197
|
+
| claim lapsed / unverified / broken / source-changed | re-verify with `claim()` or retire it |
|
|
198
|
+
| duplicate title, deletion, revision bloat | resolve per the hint |
|
|
199
|
+
|
|
200
|
+
- **`ack=[id]`** silences a finding you actually reviewed, until the note's content changes. Never ack unread — leave other lanes' flags alone.
|
|
201
|
+
- `domain=` scopes the deep passes to one lane; `dryRun` previews.
|
|
202
|
+
- `coverage[]` names any capped pass, so a short list isn't mistaken for a clean one.
|
|
365
203
|
|
|
366
204
|
---
|
|
367
205
|
|
|
368
|
-
##
|
|
369
|
-
|
|
370
|
-
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.
|
|
206
|
+
## Other tools
|
|
371
207
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
---
|
|
208
|
+
`attach`/`detach` — raw artifacts on a note. `backup(name)` — milestone snapshot (close already backs up). `bootstrap()` — create or refresh the structure. `template(kind)` — the canonical skeleton. **Full mode** (`BRAINLLM_MODE=full`) adds raw ETAPI tools with none of the server's guarantees. Read `references/fullmode.md` before any raw work.
|
|
375
209
|
|
|
376
|
-
## Quick-
|
|
210
|
+
## Quick-fix
|
|
377
211
|
|
|
378
212
|
| Situation | Fix |
|
|
379
213
|
|---|---|
|
|
380
|
-
|
|
|
381
|
-
| `start()` → `uninitialized` |
|
|
382
|
-
|
|
|
214
|
+
| timeouts or connection errors | the Trilium instance at `TRILIUM_BASE_URL` is unreachable; tools don't need restarting |
|
|
215
|
+
| `start()` → `uninitialized` | `bootstrap()` |
|
|
216
|
+
| dates look off on a hosted deploy | set `BRAINLLM_TZ` (IANA) |
|
|
217
|
+
| an informational `{error, detail, hint}` | read `hint` and retry with corrected arguments |
|
|
383
218
|
|
|
384
|
-
## References
|
|
219
|
+
## References
|
|
385
220
|
|
|
386
|
-
| Read
|
|
221
|
+
| Read | When |
|
|
387
222
|
|---|---|
|
|
388
|
-
| `references/
|
|
389
|
-
| `references/
|
|
390
|
-
| `references/
|
|
391
|
-
| `references/fullmode.md` | before any raw ETAPI work
|
|
392
|
-
| `references/troubleshooting.md` | errors, edge cases,
|
|
223
|
+
| `references/domains.md` | creating, extending or refreshing a domain |
|
|
224
|
+
| `references/connections.md` | a connection audit |
|
|
225
|
+
| `references/taxonomy.md` | choosing a relation verb; reading server labels |
|
|
226
|
+
| `references/fullmode.md` | before any raw ETAPI work |
|
|
227
|
+
| `references/troubleshooting.md` | errors, edge cases, unexpected behavior |
|