brainllm 12.9.0 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "manifest_version": "0.3",
3
3
  "name": "brainllm",
4
4
  "display_name": "BrainLLM",
5
- "version": "12.9.0",
5
+ "version": "13.0.0",
6
6
  "description": "Self-hosted memory for LLMs in TriliumNext, structured by the server and editable by you.",
7
7
  "long_description": "BrainLLM is an MCP server that organises memory into five purpose-built areas (Master, LLM, Memory, Knowledge, Insights). The model supplies content while the server owns placement, format, lifecycle, dates and backups. Includes a full raw ETAPI surface for advanced use. Requires a running TriliumNext instance and the Bun runtime.",
8
8
  "author": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainllm",
3
- "version": "12.9.0",
3
+ "version": "13.0.0",
4
4
  "mcpName": "io.github.miisodev/brainllm",
5
5
  "description": "Self-hosted memory for LLMs in TriliumNext, structured by the server and editable by you.",
6
6
  "author": "Kevin Miiso Novo <miisodev@gmail.com>",
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.miisodev/brainllm",
4
4
  "description": "Self-hosted memory for LLMs in TriliumNext, structured by the server and editable by you.",
5
- "version": "12.9.0",
5
+ "version": "13.0.0",
6
6
  "websiteUrl": "https://miisodev.github.io/BrainLLM/",
7
7
  "repository": {
8
8
  "url": "https://github.com/miisodev/BrainLLM",
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "brainllm",
15
- "version": "12.9.0",
15
+ "version": "13.0.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -34,9 +34,9 @@ DURING remember(...) the moment something is worth keeping
34
34
  END session() → [update singletons] → addendum() → maintain() → remarks() → diary() → close()
35
35
  ```
36
36
 
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.
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 (capped at 25; `changesTruncated: {shown, total}` names the cap when there were more). `newDay: true` means no INTERACTIVE close has happened today — an overnight agent-session addendum does not count, so the morning still calls `day()` (previous session, its log, notes touched since; `day()`'s `newDay` is the same signal). `day(recap=true)` returns everything written today, in order, across sessions, diary and thread entries.
38
38
 
39
- **Close protocol.** `session()` returns singleton stubs, `changedToday` (the maintained notes this day's writes touched: check every figure you changed agrees everywhere it is recorded, with `diff(since="today")` and `consistency()`), `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")`. **Light close:** a session that wrote no content gets `closeMode: "light"` from `session()` — only `remarks()`, a one-paragraph `diary()` and `close()` follow; any content write since the last close brings the full protocol back. Every `close()` also seals its day's records, which `maintain(deep)` later verifies.
39
+ **Close protocol.** `session()` returns `singletons: "stubs"` (stub mode — not a light close), `changedToday` (the maintained notes this day's writes touched: check every figure you changed agrees everywhere it is recorded, with `diff(since="today")` and `consistency()`), `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, opened by `session(continuing=true)` — the same short continuation receipt (no singleton review), refused on the day's first session. Scoped or autonomous runs pass `session(scope="agent")`. **Light close:** a session that wrote no content gets `closeMode: "light"` from `session()` — only `remarks()`, a one-paragraph `diary()` and `close()` follow; any content write since the last close brings the full protocol back. Every `close()` also seals its day's records, which `maintain(deep)` later verifies.
40
40
 
41
41
  **Write during the session, not at the end** — a fact written mid-conversation survives a crash.
42
42
 
@@ -94,15 +94,15 @@ worth keeping?
94
94
  | a day's change log | `insights(date?)` |
95
95
  | skim a surface | `<surface>_recall` |
96
96
  | everything about an area | `domain(name)` — the reliable path; reach here first. `outline=true` adds every maintained note's headings and size, for planning a multi-note rewrite |
97
- | search | `recall(query, domain=…)` — scope it; `regex=` for structure, each hit with the matched passage as `evidence` (records included; `kinds=` narrows); fuzzy hits are leads; recent notes rank slightly higher |
98
- | several bodies at once | `read(ids=[…])` (≤ 10). Bodies stop at ~80k characters; the rest come back in `deferred[]` for a second call. `text=true` returns readable plain text for orienting (not for building a `find=` anchor) |
97
+ | search | `recall(query, domain=…)` — scope it; `since=`/`until=` keep only notes last modified inside the window (date-only bounds cover whole days); every hit names `matchedBy` (`label`/`title`/`text`/`fuzzy`/`ordered`/`regex`) and the top hits carry the `evidence` passage; `regex=` for structure, evidenced on every hit (records included; `kinds=` narrows); fuzzy hits are leads; recent notes rank slightly higher. A receipt that saturated a strategy's 30-candidate cap says so; `searchFailed: true` is a backend failure, never a "nothing matched" verdict — retry |
98
+ | several bodies at once | `read(ids=[…])` (≤ 10). Every entry carries `chars` (the full stored size, even when a section is returned); `section=` (+ `occurrence=`) reads one heading's body from each note — a table section reports `tableRows`, and a miss follows the singleton miss shape (`missing: true` for an unknown id; `available[]` lists the real headings for a section miss). Bodies stop at ~80k characters; the rest come back in `deferred[]` for a second call. `text=true` returns readable plain text for orienting (not for building a `find=` anchor) |
99
99
  | storage and growth | `health()` — Trilium version, estimated database size, heaviest notes and their revisions, named backups |
100
100
  | what the brain holds | `assembly(area?)` — titles by surface with purposes |
101
101
  | inventory / locate by id | `brain()` — read `parent`, not position |
102
102
  | a note's heading tree | `outline(id)` — before any `section=` edit you're unsure of |
103
103
  | raw labels, relations, body | `inspect(id, content?, section?, find?)` — `find=` also returns the stored element (row, item, paragraph) around each match |
104
104
 
105
- Every read that can be large takes `section=`; a note past the read ceiling cannot be returned whole.
105
+ Every read that can be large takes `section=` — `read(ids)` too, across each note; a note past the read ceiling cannot be returned whole.
106
106
 
107
107
  ---
108
108
 
@@ -120,10 +120,11 @@ Every read that can be large takes `section=`; a note past the read ceiling cann
120
120
  | the session log | `close(summary, identity, title?, learned?)` |
121
121
 
122
122
  - **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.
123
+ - **Sources preview before they land.** `remember(kind="sources", …, revision=)` takes `dryRun=true`, which previews each Revision row addition/update without writing; a new key near an existing one returns `didYouMean` (with `didYouMeanNote`) on the dry run and on the real call, so a qualifier added to a name updates its row instead of creating a second one.
123
124
  - **Wire at creation:** `connect=[{relation, toNoteId}]` on the same call. An unconnected note is an orphan until wired.
124
125
  - **Identity line** (`"LLM · environment · agent/mode [· Run N]"`) is required on diary, close and dated thread appends. It is checked on write: at least three `·`-separated parts, one line, no draft text — a malformed one is refused with nothing written.
125
126
  - **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.
126
- - Bodies may be text, markdown or HTML; the server normalises them to editor-native HTML and reports `sanitized[]` (markdown mixed into an HTML body is converted; a placeholder like `<name>` — even one named like an element, `<template>`, `<select>` — stays as text and never truncates what follows).
127
+ - Bodies may be text, markdown or HTML; the server normalises them to editor-native HTML and reports `sanitized[]` (markdown mixed into an HTML body is converted; a placeholder like `<name>` — even one named like an element, `<template>`, `<select>` — stays as text and never truncates what follows). CKEditor's own attributes (`data-list-item-id` on list items, `spellcheck` on code) are preserved, so re-sanitising a stored note is idempotent and quiet.
127
128
  - `diary`, `session`, `log`, `claim` and `domain` have dedicated paths; `remember` refuses them.
128
129
 
129
130
  **Four writing rules** (each applies to every note you touch):
@@ -150,15 +151,17 @@ Read `template(kind)` before your first write of a kind, then read a sibling.
150
151
  | `find="<text>"` | replace every occurrence (`nth=` for one) — exact first, then through re-serialized tags, then ignoring inline formatting (`matchMode` says which) |
151
152
  | `edits=[{find, body}]` | several surgeries in one read and one write |
152
153
  | `find="<short anchor>"` + `within="tr"` | act on the element containing the anchor: replace it (default), `mode="before"/"after"` to insert a row beside it, `mode="remove"` to delete it (`li`, `p`, `td` … too). `within` names what is replaced: `"tr"` takes a whole `<tr>…</tr>` body, `"td"` one `<td>…</td>`; a plain `find=` inside a cell replaces only that text |
153
- | `within="tr"` + `mode="remove"` + `closure={thread, body}` | close a register row in one call: the row goes, and the closure (quoting it) lands in that thread's entry for today. Needs `identity=`; run `consistency()` after on any fact the row asserted |
154
- | `domain="<name>"` + `find=`/`edits=` (no noteId) | the same find/replace across every maintained note in a domain; a dry run listing each match unless `dryRun=false`. Records are never touched. `dryRun` anywhere else is refused |
154
+ | `within=` + `dryRun=true` | preview the element first: its full visible extent and byte size, plus the before/after shape (for a `closure=`, what would be appended or moved) — nothing written, no revision. The only other `dryRun` is `domain=` below (its dry run is the default); elsewhere it is refused |
155
+ | `within="tr"` + `mode="remove"` + `closure=` | close a register row in one call: `{thread, body}` sends the row, quoted, into that thread's entry for today (needs `identity=`); `{note, table?}` moves the removed row into another register note's table, shaped to it — a wider row is refused, a narrower one padded. Run `consistency()` after on any fact the row asserted |
156
+ | `append={table, row}` | append one row to the table under that heading with no anchor to locate first — pipes are cell boundaries, a lone placeholder row is replaced; composes with nothing else |
157
+ | `domain="<name>"` + `find=`/`edits=` (no noteId) | the same find/replace across every maintained note in a domain; a dry run listing each match unless `dryRun=false`. Records are never touched |
155
158
 
156
159
  - **Tables are edited by row.** Anchor on a few words unique to the row and pass `within="tr"`; never copy a whole stored row into `find=`. The row's visible text works too (`"Phase 2 | Claude | Open"`), and quotes match in any form. An anchor in several rows is refused with previews; `nth=` picks one. Write one `<td>` per column — a row whose cells disagree with its header comes back as `tableShape`.
157
160
  - **"Last updated" lines are server-owned.** Every content write bumps them (the receipt says `lastUpdated`); never `find=` on one.
158
161
  - A replacement equal to its match writes nothing and reports `unchanged`.
159
162
  - A revision is taken before every content write; `diff(noteId)` shows what the last write changed, and `diff(since="today")` reviews every note you changed today in one call — run it before closing.
160
- - **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.
161
- - **`find=` prefers stored HTML.** Plain text also matches through inline formatting (`<strong>`, `<code>`, `<em>`, entities), so a short anchor rarely needs its markup; a span that opens or closes formatting is widened to keep it balanced. Block tags still match literally; `outline()` gives the `raw` form of headings. On a miss the hint names the cause and shows the stored text nearby.
163
+ - **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. A record append (`remember`/`revise`/`closure`) names where it landed: `entryMarker` (the `Addendum — HH:mm` block heading written) and `runHeading` (the identity heading, when the body opened with one).
164
+ - **`find=` prefers stored HTML.** Plain text also matches through inline formatting (`<strong>`, `<code>`, `<em>`, entities), so a short anchor rarely needs its markup; a span that opens or closes formatting is widened to keep it balanced. Block tags still match literally; `outline()` gives the `raw` form of headings. On a miss the hint names the cause in every shape: drifted text shows the stored text nearby (`storedNearby`), `nth=` past the last occurrence says so, and an all-miss `edits=` returns a hint per edit.
162
165
  - A section replace swaps everything under the heading — use `find=` for anything smaller.
163
166
  - **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.
164
167
  - Concurrent writers are serialised server-side; reads stay parallel.
@@ -182,8 +185,8 @@ Read `template(kind)` before your first write of a kind, then read a sibling.
182
185
 
183
186
  ## Verification — `consistency` and `claim`
184
187
 
185
- - **`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. Records (sessions, diary, logs, dated thread entries) are skipped because they are never rewritten; `includeRecords=true` brings them back. Without a capture group the whole match is the value; matching ignores case unless `caseSensitive=true`; `patterns=[…]` checks several facts over one read of each note.
186
- - **`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.
188
+ - **`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. Values one note asserts together — two true figures in one cell — are that note's **compound fact**, not a contradiction; only values no note co-asserts flag as a disagreement. **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. Records (sessions, diary, logs, dated thread entries) are skipped because they are never rewritten; `includeRecords=true` brings them back. Without a capture group the whole match is the value; matching ignores case unless `caseSensitive=true`; `patterns=[…]` checks several facts over one read of each note.
189
+ - **`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. `retire=true` composes with a verification — `claim(id, holds=false, evidence=…, retire=true)` marks it broken with the evidence and archives it in one call (`retired: true`); alone it just retires. BrainLLM never runs the check — you do. Register claims that would be expensive to discover had gone stale.
187
190
 
188
191
  ---
189
192
 
@@ -205,7 +208,7 @@ Lite runs inside `start`/`close` (thread aging, label checks). `maintain(deep=tr
205
208
  | claim lapsed / unverified / broken / source-changed | re-verify with `claim()` or retire it |
206
209
  | duplicate title, deletion, revision bloat | resolve per the hint |
207
210
 
208
- - **`ack=[id]`** silences a finding you actually reviewed, until the note's content changes. Never ack unread — leave other lanes' flags alone.
211
+ - **`ack=[id]`** silences a finding you actually reviewed, until the note's content changes — it also quiets `domain()`'s stale flag on that note. Never ack unread — leave other lanes' flags alone.
209
212
  - `domain=` scopes the deep passes to one lane; `dryRun` previews.
210
213
  - `coverage[]` names any capped pass, so a short list isn't mistaken for a clean one.
211
214
 
@@ -225,6 +228,10 @@ Lite runs inside `start`/`close` (thread aging, label checks). `maintain(deep=tr
225
228
  | deletions older than a week go unreported | Trilium keeps deleted notes longer than its 7-day default: set `BRAINLLM_DELETION_CATCHUP_DAYS` to match |
226
229
  | an informational `{error, detail, hint}` | read `hint` and retry with corrected arguments |
227
230
 
231
+ ## Release status
232
+
233
+ A shipped-but-unmerged stream announces itself here as one italic line — ``> _Shipped, pending merge: `<branch>` @ `<short-hash>` — <feature>; delete this line when the merge lands on main._`` — and the integration deletes it. No markers are active right now.
234
+
228
235
  ## References
229
236
 
230
237
  | Read | When |
@@ -21,7 +21,7 @@ Markers are just the emoji on each source; their dates live only in the Revision
21
21
 
22
22
  ## The Sources note
23
23
 
24
- Creating a domain creates the book and its Sources note together. `remember(kind="sources", domain=…)` merges into the Sources section; pass `revision=[{source, marker, date?}]` to upsert Revision rows by source name (the name must match the list exactly).
24
+ Creating a domain creates the book and its Sources note together. `remember(kind="sources", domain=…)` merges into the Sources section; pass `revision=[{source, marker, date?}]` to upsert Revision rows by source name (the name must match the list exactly). `dryRun=true` previews the row additions/updates without writing; a new key near an existing one comes back with `didYouMean` (and `didYouMeanNote` naming the pair) — on the dry run and on the real call — so a qualifier added to a name updates its row instead of creating a second one.
25
25
 
26
26
  ## 1. Creating a domain
27
27
 
@@ -42,6 +42,8 @@ Written by the server — you never set `#noteType`, `#status`, `#created`, `#up
42
42
  | `#created` | ISO date | Set at creation (the user's local day) |
43
43
  | `#updated` | ISO date | Updated on every write |
44
44
  | `#closed` | ISO date | Set when `resolve()` or `forget()` archives a note |
45
+ | `#scope` | `interactive` `agent` | On the day's session note — the scope the day's last `session()` ran under; `close()` stamps `#interactive` only when this is `interactive` |
46
+ | `#interactive` | `HH:mm` | Stamped on the session note by an INTERACTIVE `close()`. `start()`/`day()` read its presence as "the day has started" — overnight agent addenda never set it, so the morning still calls `day()` |
45
47
  | `#topic` | slugged, repeatable | Subject tags (`ai-tooling`, `infra`) — capitalization normalized server-side |
46
48
  | `#domain` | slugged | Knowledge domain — book auto-created on first use |
47
49
  | `#archived` | (flag) | Excludes the note from default `recall()`; content preserved in place |
@@ -3,7 +3,7 @@
3
3
  | Situation | What happens / what to do |
4
4
  |---|---|
5
5
  | BrainLLM not initialized | `start` returns `status: "uninitialized"` → run `bootstrap` (idempotent, safe anytime) |
6
- | Second `close` same day | An exact retry is duplicate-guarded; a genuinely new continuation appends an addendum to today's session note by design. Pass `continuing=true` so you don't re-run the whole pre-close protocol for a session whose brain writes are already done; it's verified against today's note, so it can't stand in for a first close |
6
+ | Second `close` same day | An exact retry is duplicate-guarded; a genuinely new continuation appends an addendum to today's session note by design. Pass `continuing=true` so you don't re-run the whole pre-close protocol for a session whose brain writes are already done — `session(continuing=true)` is its matching opener (short continuation receipt, refused on the day's first session). It's verified against today's note, so it can't stand in for a first close |
7
7
  | `remember()` says `action: "updated"` unexpectedly | A same-kind note with that title existed; content was appended there. If it was genuinely a different subject, `remember()` again with a distinguishing title |
8
8
  | User contradicts a stored fact about themselves | `master(which)` to read it, then `revise(id, section=…)` with the correction — the Master singletons hold current-state truth, not history |
9
9
  | A stored fact was wrong from the start | `revise(mode="replace")` — a revision snapshot is taken automatically, nothing is lost |
@@ -29,18 +29,19 @@
29
29
  | BrainLLM tools time out or return connection errors | The backend is unreachable, not the tool — every ETAPI call is bounded at 30 s with one retry on idempotent reads, so a hang surfaces as a failed tool call rather than a stuck session. Check the instance `TRILIUM_BASE_URL` names: a hosted deploy should answer at its own URL, a local desktop install needs the Trilium app running. Then retry. |
30
30
  | `start` → `uninitialized` | `bootstrap` |
31
31
  | A result ends with `[BrainLLM: … cut at 140,000 to fit the client's limit]` | The result passed the server's size cap. Narrow it: `section=`, `block=`, `limit=`, or fewer `ids=` |
32
- | `revise` refuses `dryRun=` | `dryRun` exists only for `revise(domain=)`. To preview one note's edit, read the note (or `outline()`) first; `diff(noteId)` shows what a write changed |
33
- | `read(ids=[…])` returns `deferred[]` | The bodies passed ~80k characters together; read the deferred ids in a second call, or pass `text=true` |
32
+ | `revise` refuses `dryRun=` | Without `within=`, `dryRun` exists only for `revise(domain=)`. To preview one note's edit, use `find=` + `within=` + `dryRun=true` — it shows the element and writes nothing; or read the note (or `outline()`) first. `diff(noteId)` shows what a write changed |
33
+ | `read(ids=[…])` returns `deferred[]` | The bodies passed ~80k characters together. Every returned entry carries `chars`, so pick by size; read the deferred ids in a second call, `section=` for the huge ones, or pass `text=true` |
34
34
  | `health()` flags a note's revisions | Revision history is most of the database's weight. Splitting the note, or tightening Trilium's revision-snapshot interval, bounds it |
35
35
  | Deep maintenance flags the same items every session | Act on them, or `maintain(ack=[…])` the ones you reviewed and found correct |
36
- | `revise(find=)` returns `replaced: 0` | Read the hint — it names the cause. Escaped search string (`&lt;h3&gt;` against stored real tags): pass the tag literally. Spans a block boundary (`</h3>` then `<ol>`): anchor inside one element, or use `section=` with `mode="before"/"after"`. Ends in a closing tag (`…</strong></li>`): use `within="li"` with a short plain anchor. Otherwise it was already replaced, or the text genuinely differs — check with `inspect(noteId, find="<shorter substring>")` |
36
+ | `revise(find=)` returns `replaced: 0` | Read the hint — it names the cause in every shape now. Escaped search string (`&lt;h3&gt;` against stored real tags): pass the tag literally. Spans a block boundary (`</h3>` then `<ol>`): anchor inside one element, or use `section=` with `mode="before"/"after"`. Ends in a closing tag (`…</strong></li>`): use `within="li"` with a short plain anchor. Drifted text shows the stored text nearby (`storedNearby`); a `nth=` past the last occurrence says so; an all-miss `edits=` hints on the call and on each edit. Otherwise it was already replaced — check with `inspect(noteId, find="<shorter substring>")` |
37
37
  | `revise(section=)` returns `matched: false` | No heading matched, so a NEW section was written — at the note's own level, with the note's real headings in `available[]`. Re-target from that list; `outline(noteId)` gives the same tree plus levels and occurrence indices before you write |
38
38
  | Two sections legitimately share a heading | `revise(section=…, occurrence=2)`. Repeating a closing heading under every category is the consistency rule working, not drift — `occurrence=` is how you reach the second one without rewriting the whole section |
39
39
  | Note body shows literal `&lt;p&gt;` text in Trilium | Entity-corrupted: `maintain(repair=[noteId])` unwinds one level in place |
40
40
  | `malformed_identity` | The identification line needs three or more `·`-separated parts on one line (`"Claude Opus 5.5 · Claude Code · Interactive"`), no question marks, not an `Addendum —` marker. Nothing was written; retry with a clean `identity=` |
41
- | `close()` asks for only `session`, `remarks`, `diary` | Nothing was written this session (light close). After any content write it asks for `addendum()` and `maintain()` too |
41
+ | `close()` asks for only `session`, `remarks`, `diary` | Nothing was written this session — the light close, where `session()` said `closeMode: "light"` and did not require `addendum()` or `maintain()`. Contrast `singletons: "stubs"` on a normal session receipt: stub mode, not a light close — the full protocol (`addendum()`, `maintain()`) is due. Any content write since the last close restores it |
42
42
  | `maintain(deep)` reports `record seal` | A session, diary or dated thread entry of a sealed day was rewritten after its close. Appends are fine; this is a change to earlier text. `diff(noteId)` and the note's revisions show it; restore unless intended. The finding leaves after seven sealed days |
43
- | A regex `recall` returns nothing although the text is there | Trilium strips diacritic characters (`·`, accents) from note text before its pre-filter matches, so BrainLLM normalises the pattern the same way and the result says so; if it still misses, the 30-candidate cap may be the reason (named in the note) — narrow with `domain=`/`kinds=` or use `consistency()`, which scans exhaustively |
43
+ | A regex `recall` returns nothing although the text is there | Trilium strips diacritic characters (`·`, accents) from note text before its pre-filter matches, so BrainLLM normalises the pattern the same way and the result says so; when a strategy's backend query hits its 30-candidate cap the receipt names that strategy — narrow with `domain=`/`kinds=` or use `consistency()`, which scans exhaustively |
44
+ | `recall` receipts say `searchFailed: true` | The backend query errored, so this is a failure, NOT a "nothing matched" verdict — retry. Distinct from a genuine no-match, which says so without the flag |
44
45
  | Unsure whether a note is still structurally sound after several edits | `outline(noteId)` — duplicate headings, unbalanced tags and size in one call, no body read. `maintain(deep=true)` runs the same check across the brain |
45
46
  | `recall` returns odd results | It already filters untyped notes; if it persists, `maintain(deep=true)` then retry |
46
47
  | Deep maintenance flags something unfamiliar | Every finding names its own fix in the flag text — read it rather than guessing |