brainllm 12.9.1 → 13.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.1",
5
+ "version": "13.1.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.1",
3
+ "version": "13.1.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>",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@modelcontextprotocol/sdk": "^1.32.0",
66
- "zod": "^3.25.76"
66
+ "zod": "^4.6.6"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/bun": "latest",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
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.1",
5
+ "version": "13.1.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.1",
15
+ "version": "13.1.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,18 @@ 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. `find=` and `edits=` also support `dryRun=true` using the same replacement and sanitizer path; metadata changes cannot be combined with a preview. `domain=` previews by default. Unsupported preview modes are 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?, row?}` moves the row into another register. Column counts must match exactly; `row=["cell", …]` provides explicitly shaped plain-text destination cells. Name `table` when several tables exist. Destination content lands before source removal; same-note moves use one write. On a partial failure, read both notes before retrying; exact destination rows and identical dated blocks are deduplicated. 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
+ | `moves=[{find, nth?, row?}]` + `within="tr"`, `mode="remove"`, `closure=` | batch row closures to one target; all rows are preflighted, then applied in order. `dryRun=true` previews without writes. Stops on failure with completed and remaining rows; it is not an atomic transaction |
158
+ | `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
159
 
156
160
  - **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
161
  - **"Last updated" lines are server-owned.** Every content write bumps them (the receipt says `lastUpdated`); never `find=` on one.
158
162
  - A replacement equal to its match writes nothing and reports `unchanged`.
159
- - 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
+ - A failed revision snapshot stops the content overwrite. System errors are not success receipts. 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.
164
+ - **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).
165
+ - **`find=` replaces text; it does not insert beside an anchor.** To insert, use `within=` with `mode="before"/"after"`, or `section=` with an insertion mode. **`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
166
  - A section replace swaps everything under the heading — use `find=` for anything smaller.
163
167
  - **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
168
  - Concurrent writers are serialised server-side; reads stay parallel.
@@ -182,8 +186,8 @@ Read `template(kind)` before your first write of a kind, then read a sibling.
182
186
 
183
187
  ## Verification — `consistency` and `claim`
184
188
 
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.
189
+ - **`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. The default examines up to 5,000 eligible notes from 10,000 candidates. Always read `coverage`: `complete=false`, `truncated` or `failed` means the scan cannot establish absence or unanimity. Narrow the scope, increase `limit` or retry failures. `fast=true` is explicitly lossy. Case-insensitive equality groups spelling variants while returning their original `spellings`. Backend search failures propagate instead of appearing empty.
190
+ - **`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
191
 
188
192
  ---
189
193
 
@@ -205,7 +209,7 @@ Lite runs inside `start`/`close` (thread aging, label checks). `maintain(deep=tr
205
209
  | claim lapsed / unverified / broken / source-changed | re-verify with `claim()` or retire it |
206
210
  | duplicate title, deletion, revision bloat | resolve per the hint |
207
211
 
208
- - **`ack=[id]`** silences a finding you actually reviewed, until the note's content changes. Never ack unread — leave other lanes' flags alone.
212
+ - **`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
213
  - `domain=` scopes the deep passes to one lane; `dryRun` previews.
210
214
  - `coverage[]` names any capped pass, so a short list isn't mistaken for a clean one.
211
215
 
@@ -225,6 +229,10 @@ Lite runs inside `start`/`close` (thread aging, label checks). `maintain(deep=tr
225
229
  | 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
230
  | an informational `{error, detail, hint}` | read `hint` and retry with corrected arguments |
227
231
 
232
+ ## Release status
233
+
234
+ 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.
235
+
228
236
  ## References
229
237
 
230
238
  | 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,22 @@
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=` | Preview uses `find=`, `edits=`, `within=` or `moves=`; domain edits preview by default. Other modes refuse before writing. Do not combine title/icon changes with a preview |
33
+ | `consistency` is incomplete | Inspect `coverage.failed`, `truncated` and the candidate limit. Narrow domain/kinds or raise `limit`; retry failed reads. A bounded or lossy scan cannot establish absence or unanimity |
34
+ | A register closure partially failed | Read both source and destination. Destination-first ordering preserves the source until the destination is durable; exact rows and identical thread blocks deduplicate on retry. A lost response can follow an accepted write; never assume no mutation |
35
+ | Register destination shape or table ambiguous | Name `closure.table`, and supply exact plain-text destination cells with `closure.row`. Padding is not inferred |
36
+ | `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
37
  | `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
38
  | 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>")` |
39
+ | `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
40
  | `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
41
  | 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
42
  | Note body shows literal `&lt;p&gt;` text in Trilium | Entity-corrupted: `maintain(repair=[noteId])` unwinds one level in place |
40
43
  | `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 |
44
+ | `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
45
  | `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 |
46
+ | 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 |
47
+ | `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
48
  | 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
49
  | `recall` returns odd results | It already filters untyped notes; if it persists, `maintain(deep=true)` then retry |
46
50
  | Deep maintenance flags something unfamiliar | Every finding names its own fix in the flag text — read it rather than guessing |