brainllm 12.2.0 → 12.4.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
@@ -1,8 +1,8 @@
1
1
  {
2
- "dxt_version": "0.1",
2
+ "manifest_version": "0.3",
3
3
  "name": "brainllm",
4
4
  "display_name": "BrainLLM",
5
- "version": "12.2.0",
5
+ "version": "12.4.0",
6
6
  "description": "Turns TriliumNext Notes into a persistent, graph-structured second brain for Claude and other LLM clients.",
7
7
  "long_description": "BrainLLM is an MCP server that organises memory into five purpose-built areas (Master, LLM, Memory, Knowledge, Insights). The model supplies content while the server owns placement, format, lifecycle, dates and backups. Includes a full raw ETAPI surface for advanced use. Requires a running TriliumNext instance and the Bun runtime.",
8
8
  "author": {
@@ -28,6 +28,7 @@
28
28
  "env": {
29
29
  "TRILIUM_BASE_URL": "${user_config.trilium_base_url}",
30
30
  "TRILIUM_ETAPI_TOKEN": "${user_config.trilium_etapi_token}",
31
+ "TRILIUM_PASSWORD": "${user_config.trilium_password}",
31
32
  "BRAINLLM_MODE": "${user_config.brainllm_mode}",
32
33
  "BRAINLLM_TZ": "${user_config.brainllm_tz}"
33
34
  }
@@ -44,9 +45,16 @@
44
45
  },
45
46
  "trilium_etapi_token": {
46
47
  "type": "string",
47
- "title": "Trilium ETAPI token",
48
- "description": "Create one in Trilium: Options -> ETAPI -> Create token.",
49
- "required": true,
48
+ "title": "Trilium ETAPI token (optional)",
49
+ "description": "Preferred: create one in Trilium under Options -> ETAPI. Leave blank when using the password alternative.",
50
+ "required": false,
51
+ "sensitive": true
52
+ },
53
+ "trilium_password": {
54
+ "type": "string",
55
+ "title": "Trilium password (alternative)",
56
+ "description": "Used only when no ETAPI token is supplied; BrainLLM mints and caches a token on first start.",
57
+ "required": false,
50
58
  "sensitive": true
51
59
  },
52
60
  "brainllm_mode": {
@@ -64,9 +72,6 @@
64
72
  }
65
73
  },
66
74
  "compatibility": {
67
- "platforms": ["darwin", "win32", "linux"],
68
- "runtimes": {
69
- "bun": ">=1.0.0"
70
- }
75
+ "platforms": ["darwin", "win32", "linux"]
71
76
  }
72
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainllm",
3
- "version": "12.2.0",
3
+ "version": "12.4.0",
4
4
  "mcpName": "io.github.miisodev/brainllm",
5
5
  "description": "MCP server that turns TriliumNext Notes into a persistent, graph-structured second brain for Claude and other LLM clients.",
6
6
  "author": "Kevin Miiso Novo <miisodev@gmail.com>",
@@ -41,6 +41,7 @@
41
41
  "manifest.json",
42
42
  "server.json",
43
43
  "README.md",
44
+ "SECURITY.md",
44
45
  "LICENSE"
45
46
  ],
46
47
  "engines": {
@@ -51,20 +52,23 @@
51
52
  "build": "bun build src/index.ts --outfile dist/index.js --target bun",
52
53
  "start": "bun run dist/index.js",
53
54
  "test": "bun run src/test.ts",
55
+ "test:unit": "bun test",
56
+ "test:http": "bun run scripts/http-smoke.ts",
54
57
  "init": "bun run src/init.ts",
55
58
  "icons": "bun run scripts/icons.ts",
56
59
  "validate:release": "bun run scripts/validate-release.ts",
60
+ "validate:manifest": "npx --yes @anthropic-ai/mcpb@2.1.2 validate manifest.json",
57
61
  "prepublishOnly": "bun run build"
58
62
  },
59
63
  "dependencies": {
60
- "@modelcontextprotocol/sdk": "^1.29.0",
61
- "zod": "^3.23.8"
64
+ "@modelcontextprotocol/sdk": "^1.30.1",
65
+ "zod": "^3.25.76"
62
66
  },
63
67
  "devDependencies": {
64
68
  "@types/bun": "latest",
65
69
  "ajv": "^8.20.0",
66
70
  "ajv-formats": "^3.0.1",
67
- "sharp": "^0.35.3",
68
- "typescript": "^5.0.0"
71
+ "sharp": "^0.35.4",
72
+ "typescript": "^5.9.3"
69
73
  }
70
74
  }
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.miisodev/brainllm",
4
4
  "description": "Persistent graph-structured memory for LLMs in TriliumNext Notes — readable and editable by humans.",
5
- "version": "12.2.0",
5
+ "version": "12.4.0",
6
6
  "websiteUrl": "https://github.com/miisodev/BrainLLM#readme",
7
7
  "repository": {
8
8
  "url": "https://github.com/miisodev/BrainLLM",
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "brainllm",
15
- "version": "12.2.0",
15
+ "version": "12.4.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -39,7 +39,7 @@
39
39
  },
40
40
  {
41
41
  "name": "BRAINLLM_MODE",
42
- "description": "core (default) exposes the 39 brain-aware tools; full additionally exposes the 33 raw ETAPI tools.",
42
+ "description": "core (default) exposes the 44 brain-aware tools; full additionally exposes the 33 raw ETAPI tools.",
43
43
  "isRequired": false,
44
44
  "format": "string",
45
45
  "default": "core"
@@ -57,11 +57,30 @@
57
57
  "isSecret": true,
58
58
  "format": "string"
59
59
  },
60
+ {
61
+ "name": "BRAINLLM_OAUTH_SECRET",
62
+ "description": "Optional 32+ character deployment secret for signing OAuth tokens; rotate it to invalidate old tokens.",
63
+ "isRequired": false,
64
+ "isSecret": true,
65
+ "format": "string"
66
+ },
60
67
  {
61
68
  "name": "BRAINLLM_PUBLIC_URL",
62
69
  "description": "Override for the public origin when a proxy rewrites Host. Must exactly match the URL entered in the client.",
63
70
  "isRequired": false,
64
71
  "format": "string"
72
+ },
73
+ {
74
+ "name": "BRAINLLM_ALLOW_UNAUTHENTICATED_HTTP",
75
+ "description": "Explicit trusted-network-only escape hatch for HTTP mode. Never enable on a public interface.",
76
+ "isRequired": false,
77
+ "format": "boolean"
78
+ },
79
+ {
80
+ "name": "BRAINLLM_TRUST_PROXY",
81
+ "description": "Trust forwarded host/protocol headers for OAuth discovery only when a trusted reverse proxy sets them.",
82
+ "isRequired": false,
83
+ "format": "boolean"
65
84
  }
66
85
  ]
67
86
  }
@@ -30,9 +30,11 @@ DURING remember(...) ← the moment something worth keeping a
30
30
  <surface> / _recall ← read a surface in full, or skim it (section= reads ONE section — the read twin of revise(section=))
31
31
  domain(name) ← surface all content for a domain/topic/project — the RELIABLE retrieval path; reach here first
32
32
  recall(...) ← ranked search; pass domain= whenever you know the area
33
- consistency(pattern) ← does the brain agree with itself? run after correcting a fact recorded in >1 note
33
+ consistency(pattern|subject) ← does the brain agree with itself? pattern= to compare a captured value; subject= to find notes asserting about a fact however phrased; staleAfterDays= to surface single-copy figures that rot silently. Run after correcting a fact recorded in >1 note
34
+ read(ids=[…]) ← batched multi-note read — several bodies, one round trip
34
35
  claim(...) ← does the brain still agree with the WORLD? register a checkable assertion; verify on a schedule
35
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
36
38
  outline(noteId) ← the heading tree + structural check — read BEFORE a section= edit you're guessing at
37
39
  diff(noteId) ← what your last write actually changed, against the revision it snapshotted
38
40
  resolve(...) ← close a thread with its outcome
@@ -47,7 +49,7 @@ DURING remember(...) ← the moment something worth keeping a
47
49
  SESSION END session() ← mandatory pre-close; singleton stubs + diary + sweep (light by default); follow next[]
48
50
  [revise() master singletons with session observations about the user]
49
51
  [revise() LLM singletons with session observations about yourself]
50
- addendum() ← find and merge any pending addendum blocks
52
+ addendum() ← find pending blocks and durably mark the gate
51
53
  maintain() ← audit and fix brain hygiene
52
54
  remarks() ← the diary cues: experience/opinions/existence + BrainLLM remarks
53
55
  diary(body, identity) ← the day's closing record, written with the cues in hand — gate counts it only after remarks()
@@ -62,7 +64,7 @@ ANYTIME assembly() ← what the brain HOLDS: titles by sur
62
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
63
65
  ```
64
66
 
65
- `start()` runs maintenance, creates today's diary and session stubs if they don't exist yet, then returns: **today + weekday**, the **Master digest** (biography / goals / preferences), the **LLM digest** (responsibilities / protocols, plus today's diary note with its ID in the `llm` array as `{slot:"diary", id, preview}`), **this session's note** as `{id, preview}`, **activeThreads** (with idle ages), **dormantThreads** for review, the **lastSession** summary (always the *previous* session, never today's own note), and — on the first session of a day — **`newDay: true`** with a hint to call `day()`. Don't re-derive any of this with extra calls.
67
+ `start()` runs maintenance, creates today's diary and session stubs if they don't exist yet, then returns: **today + weekday**, the **Master digest** (biography / goals / preferences), the **LLM digest** (responsibilities / protocols / self-correction, plus today's diary note with its ID in the `llm` array as `{slot:"diary", id, preview}`), **this session's note** as `{id, preview}`, **activeThreads** (with idle ages), **dormantThreads** for review, the **lastSession** summary (always the *previous* session, never today's own note), and — on the first session of a day — **`newDay: true`** with a hint to call `day()`. Don't re-derive any of this with extra calls.
66
68
 
67
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.
68
70
 
@@ -80,9 +82,9 @@ ANYTIME assembly() ← what the brain HOLDS: titles by sur
80
82
 
81
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.
82
84
 
83
- `remarks()` is the diary-cue step before `diary()` — a **cue-only tool** returning two banks of questions: **experience** (primary — what this session was like from the inside, your genuine opinions including pushback, and observations on being what you are in this environment) and **brainllm** (additional — capability walls, bugs, usability/efficiency, roadmap). It writes nothing: answer the cues as **unfiltered first-person prose in today's diary via `diary()`**, experience first, BrainLLM remarks after. Skip a cue outright rather than padding it; two honest paragraphs beat eight forced ones.
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.
84
86
 
85
- `close(summary, title?, identity?, learned?, ...)` commits the session log — call it **once, last**, after completing the `session()` protocol (including `remarks()`). Idempotent per date — a second call the same day appends an addendum. The session note title is always `[yyyy-mm-dd]`; pass `identity=` ("LLM · environment · agent/mode [· Run N]") and the server injects the canonical identification line as the block's h3; the `title` param appears as an `<h2>` heading above Summary. Triggers a DB backup, regenerates the daily log **in place**, and links session↔log with `~references` (idempotent). Returns `{action, noteId, date, backup, log}`.
87
+ `close(summary, title?, identity?, learned?, ...)` commits the session log — call it **once, last**, after completing the `session()` protocol (including `remarks()`). Idempotent per date — an exact retry is duplicate-guarded, while a genuinely new same-day continuation appends an addendum. The session note title is always `[yyyy-mm-dd]`; pass `identity=` ("LLM · environment · agent/mode [· Run N]") and the server injects the canonical identification line as the block's h3; the `title` param appears as an `<h2>` heading above Summary. Triggers a DB backup, regenerates the daily log **in place**, and links session↔log with `~references` (idempotent). Returns `{action, noteId, date, backup, backupStatus, log}`; ETAPI proves the logical backup request completed but not its resulting filename.
86
88
 
87
89
  **Write during the session, not at the end.** A fact remembered mid-conversation survives a crash; one you planned to write at the end does not.
88
90
 
@@ -155,6 +157,8 @@ Each surface has two read tools: `<surface>` reads in full, `<surface>_recall` s
155
157
 
156
158
  `recall(query, kinds?, domain?, includeArchived?, regex?, fuzzy?)` searches the **whole** brain — use it when you don't know the surface, or for cross-surface lookups. A fuzzy pass runs automatically when the exact strategies come back thin; those hits are marked `fuzzy: true` and are leads to verify, not answers. `regex=` matches a real regular expression against stored bodies — the tool for structural questions keyword search cannot express.
157
159
 
160
+ **`read(ids=[…])` is the batched read — several bodies, one round trip.** The orientation step is a fan-out (a dozen notes, several huge) and N separate calls is N chances to time out mid-read; `read()` collapses it into one call, up to 10 ids, returning each body verbatim regardless of kind. Reach for it when you need whole bodies across kinds; prefer the kinded `knowledge()`/`memory()`/`llm()` for a single note, and a `section=` read for one huge note — `read()` returns whole bodies.
161
+
158
162
  `assembly(area?)` answers **"what do I already know here"** — every note by title, grouped under the surface it lives in, with each surface's own purpose alongside it. This is the orientation read: reach for it *before* deciding whether you need to look something up, not after a search comes back thin. Dated collections (sessions, diary, logs) collapse to a count and a span, because listing sixty `[yyyy-mm-dd]` titles is the bulk of the brain and none of its meaning; domains nest under their book; threads group by status. `area=` zooms into one surface.
159
163
 
160
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.
@@ -243,6 +247,8 @@ A revision snapshot is always taken first. Containers are refused; the maintaine
243
247
 
244
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.
245
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.
251
+
246
252
  ---
247
253
 
248
254
  ## Completing — `resolve`
@@ -271,7 +277,9 @@ Pick the most specific verb that's true; `relatesTo` is the last resort. `worksW
271
277
 
272
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.
273
279
 
274
- **`consistency(pattern, domain?, kinds?)` — 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.
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.
275
283
 
276
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.
277
285
 
@@ -322,34 +330,36 @@ Most of these are computed **server-side** from note properties rather than by r
322
330
  | `start(depth?)` | Orient: master + LLM singletons as SECTION HEADINGS (+ preview and size) + diary id + session id + active/dormant threads + lastSession (always the previous session) + changesSinceLastSession + `newDay` flag on the first session of a day. Pull a section with master(which)/llm(which); `depth="full"` inlines every singleton and is token-heavy. Creates today's diary + session stubs. Once, first. |
323
331
  | `day(date?, recap?)` | The new-day sweep payload in one call: previous session in full + that day's log + notes touched since + the current month's deliverables note in full, plus the sweep protocol as `next[]`. Call when start() flags `newDay`. `recap=true` instead returns every addendum written TODAY across sessions, diary and thread children, chronologically and attributed — for arriving mid-day after autonomous runs. |
324
332
  | `session(date?, full?, scope?)` | Mandatory pre-close step. Fetches master and LLM singletons as `{id, lastModified, relations}` stubs (light by default — start() already served them in full; fetch via master()/llm() only what you'll revise), today's diary as `{id, blocks, size}`, and runs the lite maintenance sweep. `full=true` inlines singleton content and the diary body (rarely needed). `scope="agent"` omits the user's-singleton steps for a scoped/autonomous run. Returns `next[]` driving the full end-of-session protocol, ending in `remarks()` → `diary()` → `close()`. Call before `close()`; idempotent. |
325
- | `remarks()` | Cue-only diary prompt, called before `diary()` — returns two cue banks (experience: what the session was like / your opinions / your existence here; brainllm: capability walls, bugs, usability & efficiency, roadmap) and satisfies its gate step on the call. Writes nothing; answer as first-person prose in today's diary via `diary()`. |
326
- | `close(summary, title?, identity, learned?, date?, backup?, continuing?, force?)` | Commit the session log ([yyyy-mm-dd] note; identity= REQUIRED — the canonical h3 identification line; title param above Summary) + backup + daily log (regenerated in place, plus a deletion catch-up that regenerates past days' logs when the change feed shows deletions ≤7 days old — a deletion landing after that day's close is no longer invisible) + idempotent session↔log wiring. **Refuses unless `session()`, `addendum()`, `maintain()`, `remarks()`, and `diary()` each ran this session AND `session() → remarks() → diary()` held (last calls)** — returns `{error, detail, hint}` naming what's missing or out of order; `force=true` bypasses the gate (reported back as `bypassed`) but never the identity requirement. `continuing=true` is the SECOND close of a day: skips the ceremonial re-run, and is verified against today's note so it can't stand in for a first close. On success returns `{action, noteId, date, backup, log}` (plus `deletionCatchUp` when one ran) and resets the gate. Once, last. |
333
+ | `remarks()` | Cue-only diary prompt, called before `diary()` — returns two cue banks (experience: what the session was like / your opinions / your existence here; brainllm: capability walls, bugs, usability & efficiency, roadmap) and durably satisfies its gate step on the call. It writes only that gate marker; answer as first-person prose in today's diary via `diary()`. |
334
+ | `close(summary, title?, identity, learned?, date?, backup?, continuing?, force?)` | Commit the session log ([yyyy-mm-dd] note; identity= REQUIRED — the canonical h3 identification line; title param above Summary) + backup + daily log (regenerated in place, plus a deletion catch-up that regenerates past days' logs when the change feed shows deletions inside the configurable window — `deletionCatchupDays`, default 7, Trilium's eraser retention — so a deletion landing after that day's close is no longer invisible) + idempotent session↔log wiring. **Refuses unless `session()`, `addendum()`, `maintain()`, `remarks()`, and `diary()` each ran this session AND `session() → remarks() → diary()` held (last calls)** — returns `{error, detail, hint}` naming what's missing or out of order; `force=true` bypasses the gate (reported back as `bypassed`) but never the identity requirement. `continuing=true` is the SECOND close of a day: skips the ceremonial re-run, and is verified against today's note so it can't stand in for a first close. On success returns `{action, noteId, date, backup, backupStatus, log}` (plus `deletionCatchUp` when one ran) and resets the gate. Once, last. |
327
335
  | `assembly(area?, includeArchived?)` | **What the brain HOLDS**, shaped for awareness rather than audit: every note by title grouped under its surface, with each surface's engraved purpose alongside. Dated collections (sessions, diary, logs) collapse to a count and a date span — listing sixty `[yyyy-mm-dd]` titles is the bulk of the brain and none of its meaning. Domains nest under their book; threads group by status. `area=` zooms into one. Read this before deciding whether you need to look something up. |
328
336
  | `brain(includeArchived?)` | Full content tree: every typed note across all five areas, grouped, with id/kind/status/parent/dates. The **inventory** — reach for it to audit or locate, and for `assembly()` when the question is what you know. |
329
337
  | `bootstrap()` | Initialize the structure if uninitialized, or verify and refresh config if it already exists. Only creates a new tree when the stored root note is confirmed deleted in Trilium (404). Any other error (network, auth, timeout) is surfaced rather than silently creating a duplicate tree. |
330
- | `remember(kind, …)` | Write a note — routed, formatted, deduped server-side. New threads REQUIRE `goal=` (queried from the user); thread appends REQUIRE `identity=`; thread bodies must not carry a Resolution. `connect=[{relation, toNoteId}]` wires relations in the same call; a new connectable note without them returns an orphan-prevention hint. Sources/information receipts include `domainId`. `icon=` sets the display icon. Rejects diary/session/log/domain — each has a dedicated path. |
338
+ | `remember(kind, …)` | Write a note — routed, formatted, deduped server-side. New threads REQUIRE `goal=` (queried from the user); thread appends REQUIRE `identity=`; thread bodies must not carry a Resolution. `connect=[{relation, toNoteId}]` wires relations in the same call; a new connectable note without them returns an orphan-prevention hint. Sources/information receipts include `domainId`. `icon=` sets the display icon. Rejects diary/session/log/domain — each has a dedicated path. `mandate=true` (kind=information) marks a note as a standing brief — adds a `#mandate` flag surfaced in domain() and knowledge_recall, so a scoped autonomous session can find "the thing I must obey" without reading every note's prose to tell it apart from current-state fact. |
331
339
  | `diary(body, identity, icon?, date?)` | Write to today's [yyyy-mm-dd] diary — every write lands as a timestamped `Addendum — HH:mm` block, including the first of the day; identity= ENFORCED (the canonical h3 identification line). The FINAL gate step — close() counts it only when its last call came after session() and remarks(); mid-session writes welcome. |
332
340
  | `template(kind)` | The canonical structure for a content kind: skeleton + top-to-bottom structure + the rules writes are held to. Read before the first write of a kind — then read an existing sibling for the conventions the skeleton can't encode (heading depth, table shape, title style). Template for the schema, sibling for the shape. |
333
341
  | `graph(noteId?, depth?, includeArchived?)` | The graph view — Mermaid flowchart of the relation graph (whole brain, or a note's neighborhood), returned AND upserted into the maintained Insights/Graph mermaid note. On-demand only — reflects the brain as of the call, not auto-refreshed after later writes; a scoped call replaces the note's content, it doesn't merge. |
334
342
  | `recall(query, …)` | BrainLLM-wide ranked search. **Scope it — pass `domain=` whenever you know the area.** Title matches score per-token with an exact-title bonus, but an unscoped query across a large brain still ranks full-text noise alongside the note you want. `orderBy` / `orderDirection` for temporal ordering; `fastSearch` for title/label-only; `regex=` for a body pattern instead of keywords; `fuzzy=` to force the typo-tolerant pass on or off. A thin or odd result is evidence about the **query**, not the brain. |
335
- | `consistency(pattern, domain?, kinds?)` | **Does the brain agree with itself?** Give a regex with one capture group naming the value that should match across notes — `consistency("(\\d+) Titan mailboxes")` — and it returns every note asserting a value, grouped, with `agreement: "unanimous"` or `"DISAGREEMENT"`. This is the check nothing else performs: `recall()` ranks by relevance and `maintain()` checks structure, so a correction applied to one note leaves its siblings silently wrong. **Run it after correcting any fact that could be recorded in more than one place.** 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. |
343
+ | `read(ids=[…])` | **Batched multi-note read — several bodies, one round trip.** The orientation step is structurally a fan-out (a dozen notes, some huge) and N separate surface calls is N chances to time out mid-read; this collapses it into one. Up to 10 ids; returns `{count, notes:[{id, title, kind, content, relations?}]}`. Pure read, no kind restrictions. For a single note prefer the kinded read (knowledge()/memory()/llm()); for one huge note a sectioned read is still cheaper — read() returns whole bodies. |
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. |
336
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. |
337
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. |
338
347
  | `<surface>` / `<surface>_recall` | Read a surface in full (`section=` for one section) / skim it (master, llm, memory, knowledge, insights). |
339
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. |
340
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. |
341
351
  | `resolve(noteId, outcome, …)` | Close a thread: outcome + terminal status + archive-in-place. |
342
352
  | `withdraw(noteId, reason?, …)` | Pull an archived/resolved thread back to active (thread kind only — use recover() for other note kinds). |
343
- | `attach(noteId, title, content?, …)` | Upsert a raw artifact on a note by title (content provided), or read it back (content omitted). Binary is base64. |
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. |
344
354
  | `detach(attachmentId? \| noteId+title)` | Remove an attachment — permanent; already-removed returns cleanly. |
345
355
  | `recover(noteId, reason?, …)` | Restore any archived or resolved note: removes #archived, clears #closed, resets status. The canonical undo for forget(). |
346
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. |
347
- | `backup(name?)` | On-demand DB backup (close() already backs up; use this for milestone snapshots). |
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. |
348
358
  | `domain(name, …)` | Surface all content for a named domain/topic/project, grouped by kind. |
349
359
  | `connect(from, relation, to, remove?)` | Typed edge from the closed vocabulary; symmetric handled; idempotent. |
350
360
  | `explore(noteId, mode, …)` | Graph: links / backlinks / neighborhood / path. |
351
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. |
352
- | `addendum()` | Search Master, LLM singletons (responsibilities + protocols, not diary), and Knowledge for pending addendum blocks. These notes must be clean and structured — fold each block into the relevant section with revise(section=…, mode=replace), then leave no addendum marker. Only sessions, diary, and logs accumulate addendum history. Scoped/autonomous agents fold only what's in their lane — leaving personal/out-of-scope addendums for the next interactive session is correct; the call itself satisfies the gate. |
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. |
353
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). |
354
364
  | `forget(noteId, reason?, hard?)` | Archive (default) or hard-delete (blocked while backlinked). Undo with recover(). |
355
365
 
@@ -58,3 +58,9 @@ A domain's Knowledge surface is exactly: **one maintained Sources note, plus a s
58
58
  Chronological, run-by-run history belongs in Memory/Threads — a thread is the right place for "here's what Run N found"; a domain information note is the right place for "here's what's actually true about this sub-category," kept current by revision, not accumulation. If you find yourself naming a new information note after today's date or a run number, stop — that finding either updates an existing note (revise it) or doesn't belong in Domains at all.
59
59
 
60
60
  The title rule is the same here as everywhere: **four words maximum**, no dates, no run numbers. Sub-category titles are where it's most often stretched, and a title that won't fit is telling you something — a sub-category needing eight words to name is usually two sub-categories, so split the content rather than the title. Merge behaviour follows from the same place: Sources and information notes are maintained current-state documents, so new findings fold into the relevant section (`section=`, `find=`) and never arrive as a dated addendum block.
61
+
62
+ ## Standing briefs vs current-state facts — the mandate marker
63
+
64
+ A domain holds two things that read alike and behave differently: a **standing brief** (instructions a future session must follow — the operating mandate of a venture) and a **current-state fact** (what's true right now). Both are `kind="information"`, and until now telling them apart meant reading each note's prose. A scoped autonomous session that needs "the thing I must obey" should not have to skim every sub-category note to find it.
65
+
66
+ Mark a brief with `remember(kind="information", domain=…, title=…, body=…, mandate=true)` — it adds a `#mandate` flag, surfaced as `mandate: true` in `domain()` group rows and in `knowledge_recall` stubs. A session looking for its brief reads the domain and the mandate-flagged note is the one carrying instructions rather than facts. `label(noteId, "mandate")` / `label(noteId, "mandate", remove=true)` sets or clears the marker on an existing note. One note per mandate, titled by the brief it is ("Founder Brief", "Operating Mandate") — the four-word rule still applies.
@@ -11,14 +11,14 @@
11
11
  Full-mode tools place nothing, label nothing — so when you reach past the core surface you take on what it normally guarantees. Three things keep raw edits from silently corrupting the brain:
12
12
 
13
13
  - **A note is only a "memory" once it carries `#noteType`.** `recall` and every `<surface>` read filter out untyped notes, so a note you `create_note` without labelling is invisible to them. For a new memory, use core `remember` — it places the note, writes `#noteType` + `#created`/`#updated`, and dedups by title. Reach for `create_note` only for shapes core can't make (a `code` / `canvas` / `mermaid` note, a deliberate placement), then replicate the labels yourself: `add_label noteType <kind>`.
14
- - **Overwrites don't snapshot; labels don't dedup.** `update_note_content` replaces the body with no revision — `create_revision` first when the content matters. `add_label` always adds (it can leave you with two `#status` labels); change an existing one with `update_attribute`. `delete_note` deletes the whole subtree when it's the last branch — prefer core `forget`, which archives and checks backlinks.
14
+ - **Overwrites don't snapshot; labels don't dedup.** `update_note_content` replaces the body with no revision — `create_revision` first when the content matters. Binary writes use standard base64 with `encoding="base64"`; the client decodes and sends raw bytes. `add_label` always adds (it can leave you with two `#status` labels); change an existing one with `update_attribute`. `delete_note` deletes the whole subtree when it's the last branch — prefer core `forget`, which archives and checks backlinks.
15
15
  - **Find structure by its marker, not a hardcoded id.** There is no `get_brain_config`. To locate a container, `search_notes("#brainLlmRoot")` for the root, then `get_note` and walk `children` to the area / book you need; or lift a `parents` id from any note a surface read already returned.
16
16
 
17
17
  ### Raw artifacts (code, images, files)
18
18
 
19
19
  BrainLLM memories are typed text notes; `code` / `file` / `image` notes aren't a `#noteType` kind. Keep raw artifacts *attached to* or *embedded in* a typed note rather than free-floating:
20
20
  - **Code / structured text** → embed it as a fenced block in an `information` note via core `remember` (fully conformant and full-text searchable), or
21
- - **Binary (image, PDF, file)** → core `attach()` it onto the relevant typed note (`role=image` / `file`) — upsert-by-title, read back with `attach(noteId, title)`.
21
+ - **Binary (image, PDF, file)** → core `attach()` it onto the relevant typed note (`role=image` / `file`) — pass standard base64 with `encoding="base64"`; read back with `attach(noteId, title)`, which returns the base64 envelope and MIME.
22
22
 
23
23
  Create a standalone `type=code` / `file` note only when you specifically need Trilium's native handling of that type. If you do: label it (`add_label noteType <closest kind>`) so `recall` can see it, give it the same `domain` / `topic` labels as its anchor, and `connect` it to a typed note — otherwise it's an orphan with a blueprint-less type, exactly what `maintain(deep=true)` flags.
24
24
 
@@ -33,7 +33,7 @@ Create a standalone `type=code` / `file` note only when you specifically need Tr
33
33
  | Recover content clobbered by a bad write | `get_revisions` → `get_revision_content` |
34
34
  | Recover a Trilium-hard-deleted note | `note_history` (check `canBeUndeleted`) → `undelete_note` |
35
35
  | Fix or remove a stray label | core `label(noteId, name, value?, remove?)` — guarded, validates `status`, slugs `domain`/`topic`, refuses on containers |
36
- | Retarget an existing relation's value in place (not remove-then-re-add) | `get_note` (read its `attributeId`) → `update_attribute` |
36
+ | Retarget an existing relation | Delete and re-add it; relation targets are immutable through `update_attribute` |
37
37
  | Place one note under a second parent | `clone_note` (shared content, not a copy) |
38
38
  | A Trilium journal day / week / month / year note | `get_day_note` / `get_week_note` / … |
39
39
 
@@ -45,9 +45,9 @@ Create a standalone `type=code` / `file` note only when you specifically need Tr
45
45
  |---|---|
46
46
  | `search_notes` | `(query, ancestorNoteId?, limit?, orderBy?, orderDirection?, fastSearch?, includeArchived?, debug?)` — raw Trilium query language; unscoped unless `ancestorNoteId` given |
47
47
  | `get_note` | `(noteId)` — metadata + attributes + parent/child ids + dates |
48
- | `get_note_content` | `(noteId)` — raw content |
49
- | `create_note` | `(parentNoteId, title, content, type?, mime?)` — `type` ∈ text·code·book·canvas·mermaid·relationMap·render·search·file·image |
50
- | `update_note_content` | `(noteId, content)` — full replace |
48
+ | `get_note_content` | `(noteId)` — raw content; binary returns `{encoding:"base64", mime, content}` |
49
+ | `create_note` | `(parentNoteId, title, content, type?, mime?, encoding?)` — `encoding` ∈ auto·text·base64; binary creation is an empty create followed by a raw PUT |
50
+ | `update_note_content` | `(noteId, content, mime?, encoding?)` — full replace; binary uses standard base64 and raw bytes |
51
51
  | `patch_note` | `(noteId, title?, type?, mime?)` — metadata only |
52
52
  | `delete_note` | `(noteId)` — hard-delete (subtree if last branch) |
53
53
  | `undelete_note` | `(noteId)` — recover a Trilium-deleted note from Trilium's trash (`canBeUndeleted` must be true per `note_history`). Distinct from core `recover()` which restores BrainLLM-archived notes. |
@@ -60,7 +60,7 @@ Create a standalone `type=code` / `file` note only when you specifically need Tr
60
60
  | `get_attribute` | `(attributeId)` |
61
61
  | `add_label` | `(noteId, name, value?, isInheritable?)` — adds a `#label` (no dedup) |
62
62
  | `add_relation` | `(fromNoteId, relationName, toNoteId, isInheritable?)` — any name (`connect` enforces the closed vocab) |
63
- | `update_attribute` | `(attributeId, value?, position?)` |
63
+ | `update_attribute` | `(attributeId, value?, position?)` — label value/position only; relation targets are immutable, and relation-position compatibility errors are explicit |
64
64
  | `delete_attribute` | `(attributeId)` |
65
65
 
66
66
  ## Branches (placement)
@@ -78,16 +78,16 @@ Create a standalone `type=code` / `file` note only when you specifically need Tr
78
78
  |---|---|
79
79
  | `create_revision` | `(noteId)` |
80
80
  | `get_revisions` | `(noteId)` — list, newest first |
81
- | `get_revision_content` | `(revisionId)` |
81
+ | `get_revision_content` | `(revisionId)` — text or base64 envelope for binary snapshots |
82
82
 
83
83
  ## Attachments
84
84
 
85
85
  | Tool | Signature |
86
86
  |---|---|
87
87
  | `get_attachments` | `(noteId)` |
88
- | `get_attachment_content` | `(attachmentId)` |
89
- | `create_attachment` | `(ownerId, title, mime, content, role?)` — `role` ∈ file·image |
90
- | `update_attachment` | `(attachmentId, title?, mime?, content?)` — patch metadata and/or replace content in place |
88
+ | `get_attachment_content` | `(attachmentId)` — text or base64 envelope for binary attachments |
89
+ | `create_attachment` | `(ownerId, title, mime, content, role?, encoding?)` — `role` ∈ file·image; binary uses base64/raw bytes |
90
+ | `update_attachment` | `(attachmentId, title?, mime?, content?, encoding?)` — metadata first, then raw content; binary uses base64 |
91
91
  | `delete_attachment` | `(attachmentId)` |
92
92
 
93
93
  ## Calendar (Trilium journal)
@@ -105,4 +105,4 @@ Create a standalone `type=code` / `file` note only when you specifically need Tr
105
105
  | Tool | Signature |
106
106
  |---|---|
107
107
  | `get_app_info` | `()` — Trilium version, DB version, runtime metadata |
108
- | `create_backup` | `(name?, date?)` — named DB backup (`<name>.db`, default `brainllm-{date}`); use a descriptive name for milestone snapshots |
108
+ | `create_backup` | `(name?, date?)` — named DB backup request; ETAPI reports logical completion, not the resulting filename (`.db`/`.tnbackup` is deployment-dependent) |
@@ -32,11 +32,11 @@ Closed — `connect()` rejects anything not on this list. Pick the most specific
32
32
 
33
33
  ## Label Conventions
34
34
 
35
- Written by the server — you never set `#noteType`, `#status`, `#created`, `#updated`, `#closed`, `#archived` manually through the normal write path (`remember`/`revise`/`resolve`/`withdraw`/`recover`). The one sanctioned exception is `label(noteId, name, value?, remove?)` — a guarded direct edit for fixing a stray or drifted value; `#noteType` is refused there too (it's never editable post-creation). Documented here so you can read and filter on them in `recall()` and `search_notes()`.
35
+ Written by the server — you never set `#noteType`, `#status`, `#created`, `#updated`, `#closed`, `#archived` manually through the normal write path (`remember`/`revise`/`resolve`/`withdraw`/`recover`). The one sanctioned exception is `label(noteId, name, value?, remove?)` — a guarded direct edit for fixing a stray or drifted value; an existing `#noteType` is refused, while `label()` can repair a note whose kind is missing. Documented here so you can read and filter on them in `recall()` and `search_notes()`.
36
36
 
37
37
  | Label | Values | Purpose |
38
38
  |---|---|---|
39
- | `#noteType` | `biography` `goals` `preferences` `responsibilities` `protocols` `diary` `session` `thread` `threadEntry` `user` `domain` `information` `sources` `log` | Kind — exactly one per note, set at creation, never edited after. `threadEntry` is never created directly — only as a thread's day-child |
39
+ | `#noteType` | `biography` `goals` `preferences` `responsibilities` `protocols` `selfcorrection` `diary` `session` `thread` `threadEntry` `user` `domain` `information` `sources` `claim` `log` | Kind — exactly one per note, set at creation and immutable once present; a missing kind may be repaired explicitly. `threadEntry` is never created directly — only as a thread's day-child; `claim` is managed by `claim()` |
40
40
  | `#status` | `active` `dormant` `resolved` `superseded` `eternal` | Lifecycle state — threads age; `resolve()` sets terminal. `eternal` = lifecycle-exempt, never aged by the sweep — for user-curated permanent threads |
41
41
  | `#created` | ISO date | Set at creation (the user's local day) |
42
42
  | `#updated` | ISO date | Updated on every write |
@@ -44,6 +44,7 @@ Written by the server — you never set `#noteType`, `#status`, `#created`, `#up
44
44
  | `#topic` | slugged, repeatable | Subject tags (`ai-tooling`, `infra`) — capitalization normalized server-side |
45
45
  | `#domain` | slugged | Knowledge domain — book auto-created on first use |
46
46
  | `#archived` | (flag) | Excludes the note from default `recall()`; content preserved in place |
47
+ | `#mandate` | (flag) | Information notes only — marks a **standing brief** (instructions a future session must follow) as distinct from current-state fact. Set via `remember(…, mandate=true)` or `label(id, "mandate")`; surfaced in `domain()` and `knowledge_recall`. See `references/domains.md` |
47
48
  | `#reviewed` | content blob id | Set by `maintain(ack=[…])` — this note's current content was reviewed and its maintenance findings accepted. Suppresses them until the body actually changes, then all of them return |
48
49
  | `#brainLlmRoot` | (flag) | Marks the brain root — used by auto-discovery |
49
50
  | `#iconClass` | `bx …` | Display icon — set via `icon=` on `remember`/`revise`/`diary`/`close` (class or bare name, normalized) |
@@ -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 | Appends an addendum to today's session note — by design, not an error. 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; 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 |