inkhaven 1.2.21

Inkhaven — TUI literary work editor for Typst books
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
# Maintenance

Inkhaven's database is robust under normal use, but a manuscript is too
valuable to leave to chance. This document covers the four maintenance
operations you should know about:

1. **Backup** — create a `.zip` snapshot of an entire project.
2. **Restore** — unpack a backup into a fresh directory.
3. **Auto-backup on exit** — the TUI's safety net for users who forget.
4. **Reindex** — reconcile bdslib with the `.typ` files on disk after
   external edits or recovery.

Plus a few smaller tools: the log file, embedding model cache, and a
short troubleshooting checklist.

## Table of contents

- [Backup: `inkhaven backup`]#backup-inkhaven-backup
- [Restore: `inkhaven restore`]#restore-inkhaven-restore
- [Auto-backup on TUI exit]#auto-backup-on-tui-exit
- [Reindex: reconcile with disk]#reindex-reconcile-with-disk
- [The runtime log file]#the-runtime-log-file
- [Embedding model cache]#embedding-model-cache
- [Troubleshooting]#troubleshooting
- [Recovery patterns]#recovery-patterns

## Backup: `inkhaven backup`

```bash
inkhaven --project ~/Books/my-novel backup --out ~/Backups
```

What this does:

- Walks every file under the project root.
- Skips `.inkhaven.log` (the runtime log; not interesting in a backup)
  and any directory you have configured as the backup output target
  (avoids zipping zips of zips of …).
- Streams the rest into a deflate-compressed zip named
  `blackinkhaven_YYYYDDMM_HHMMSS.zip` inside `--out`.
- Updates `.inkhaven-backup.json` in the project root with the
  current timestamp so the on-exit auto-backup hook knows the last
  successful run.
- Prints `wrote backup: <path>` on success.

The backup is filesystem-level — it does **not** open the bdslib store.
That is by design: backups should be safe to run while the TUI is
closed, and the database files (`metadata.db`, `blobs.db`, `vectors/`)
ship as-is. A restore reproduces an exact working tree.

### Backup format

Each archive contains, relative to its root:

```
inkhaven.hjson           ← restore uses this as the "is this an Inkhaven backup?" marker
prompts.hjson
metadata.db
blobs.db
frequency.db
vectors/…
books/…
.session.json            ← cursor + open paragraph state (optional)
.inkhaven-backup.json    ← timestamp marker
```

The archive is portable — drag it to another machine, restore, and the
project comes back with identical UUIDs, identical paragraph paths,
identical embeddings.

### Tips

- Run `inkhaven backup` before a destructive operation (`rm books/foo`,
  switching `embeddings.model`, a major reorganisation).
- Pipe `backup --out` to a directory under version control (git-lfs is
  comfortable with the binary blobs) for offsite history.
- The filename format is `YYYYDDMM_HHMMSS` (not ISO order) — this
  matches the spec but does not sort chronologically across months
  on filename alone. The on-disk modification time still does.

## Restore: `inkhaven restore`

```bash
inkhaven restore ~/Backups/blackinkhaven_20260519_143010.zip --to ~/Books/my-novel-restored
```

What this does:

- Opens the archive. If it does not contain `inkhaven.hjson` at the
  root, the restore aborts: this protects you from accidentally
  unpacking a non-Inkhaven zip on top of your work.
- Refuses if `--to` already contains an `inkhaven.hjson` (i.e. another
  Inkhaven project). Pick a fresh directory or wipe the old one first.
- Creates `--to` if missing.
- Extracts every entry preserving paths.
- Reports `restored backup … into …` on success.

The restored project is independent of the source — UUIDs are
identical, but the two projects share no state going forward.

If you want to restore in place over the same directory, do it
manually:

```bash
inkhaven --project ~/Books/my-novel backup --out /tmp        # safety net
rm -rf ~/Books/my-novel
inkhaven restore /tmp/blackinkhaven_<date>.zip --to ~/Books/my-novel
```

## Auto-backup on TUI exit

The TUI checks `.inkhaven-backup.json` against `backup.max_age` from
your config on every clean exit:

```hjson
backup: {
  out_dir: "backups"
  max_age: "7d"
}
```

If the last successful backup is older than `max_age`, the exit
sequence:

1. Drops the App (which drops the Store handle, flushing DuckDB
   checkpoints and the HNSW WAL).
2. Renders a centred splash with a live progress bar:
   ```
   ┌── Inkhaven · backup ──────────────────┐
   │  Performing database backup…          │
   │  Project: /home/you/Books/my-novel    │
   │  [████████····]  321/512 ( 63%)       │
   └───────────────────────────────────────┘
   ```
3. Streams every project file into the dated zip in `out_dir`.
4. Updates `.inkhaven-backup.json` with the new timestamp.
5. Tears down the terminal and returns to your shell.

If anything goes wrong during the backup, the error is written to the
runtime log file (`.inkhaven.log`) and the user is still returned to
the shell — the backup hook is a safety net, not a blocker.

### Disabling auto-backup

`out_dir: ""` (empty) or `max_age: "0s"` either turns it off entirely.
The manual `inkhaven backup` command still works regardless.

### Choosing `max_age`

The interval depends on how often you write:

- **Daily writer**`"24h"` or `"12h"`
- **Weekly writer**`"7d"` (default)
- **Long-form, sporadic**`"30d"` if you also commit to a separate
  archive habit

Inkhaven also gives you per-paragraph snapshots (F5) for in-session
versioning; the zip backup is for project-level disaster recovery.

## Reindex: reconcile with disk

```bash
inkhaven --project ~/Books/my-novel reindex [--prune] [--adopt]
```

`reindex` walks the `.typ` files under `books/` and synchronises bdslib
with what it finds.

Plain `reindex` (no flags) re-reads every `.typ` file's content and
re-embeds it. This is what you run after switching `embeddings.model`
or after restoring a project and wanting to verify the index is fresh.

| Flag | Effect |
| ---- | ------ |
| `--prune` | Remove bdslib records whose `.typ` file is missing from disk. Use after manually deleting files or directories outside the TUI. |
| `--adopt` | Find `.typ` files on disk that bdslib doesn't know about and register them as paragraphs under the deepest hierarchy branch whose path matches their parent directory. Use after dropping new files into `books/<some-chapter>/`. |

You can combine the flags: `--prune --adopt` does both passes.

### When you need it

- After editing a `.typ` file in a different editor.
- After a `git checkout` that brought back paragraphs the database had
  forgotten.
- After a manual `mv` / `rm` inside `books/` outside the TUI.
- After switching the embedding model in `inkhaven.hjson`.
- As a "are my files and database aligned?" sanity check.

`reindex` is idempotent and safe to run repeatedly.

## The runtime log file

When the TUI is running, the `tracing` subscriber writes to
`<project-root>/.inkhaven.log` (append mode, no ANSI colours). CLI
commands log to stderr instead — they don't disturb the TUI's
alternate-screen rendering.

What lands in the log:

- Provider warnings from genai (e.g.
  `WARN genai::adapter::adapters::openai::streamer: EMPTY CHOICE CONTENT`
  is a normal DeepSeek streaming quirk and can be ignored).
- bdslib operations whose timing or error message is worth recording.
- Auto-backup failures, focus-loss-save errors, and similar non-fatal
  background errors.

If something goes wrong, look at the tail of `.inkhaven.log` first:

```bash
tail -n 200 ~/Books/my-novel/.inkhaven.log
```

To turn the log up to debug:

```bash
INKHAVEN_LOG=inkhaven=debug,bdslib=info inkhaven --project ~/Books/my-novel
```

(The TUI honours the standard `RUST_LOG` style filter via tracing's
`EnvFilter`. The actual env var name is whatever you have set as
`RUST_LOG` — `INKHAVEN_LOG` is just a convention.)

## Search model

A common question — "does inkhaven have full-text search?"  Worth
spelling out exactly what the search subsystem is and isn't.

### What's there

Inkhaven runs **two persistent stores** under the project root,
plus an embedding model that lives in a per-user cache.

| Store          | Backend                  | Holds                                 |
| -------------- | ------------------------ | ------------------------------------- |
| `metadata.db`  | DuckDB                   | Hierarchy rows: nodes, slugs, titles, parents, tags, event metadata, comments-sidecar references. |
| `blobs.db`     | DuckDB                   | Paragraph bodies + image bytes, content-addressed by node UUID. |
| `vectors/`    | [`vecstore`]https://crates.io/crates/vecstore — HNSW index | One embedding per paragraph (and per image with extracted text); built at save time via [`fastembed`]https://github.com/Anush008/fastembed-rs. |

`Store::search_text(query, limit)` computes the embedding of
`query` and asks the HNSW index for the nearest paragraphs.
That's **semantic search**: "the moment the lighthouse fails"
matches a paragraph about a darkening keeper's station even if
the literal word `lighthouse` isn't there, because the embedding
puts the two near each other in vector space.

### What's NOT there

Inkhaven has **no inverted full-text index** (no Tantivy, no
Lucene, no DuckDB `fts` extension activated).  If you want
"every paragraph literally containing the word `lighthouse`",
inkhaven's search subsystem can't give you that — semantic
results may or may not coincide with literal matches.

Workarounds for literal-match needs today:

* `Ctrl+F` inside the editor — regex find/replace on the open
  paragraph's buffer.
* `rg <pattern> books/` from a shell against the on-disk `.typ`
  files (which the editor keeps in sync via atomic save —
  see [`SECURITY_WARNING.md`]SECURITY_WARNING.md §4.1 H2 for
  why those writes are atomic in 1.2.15+).
* `inkhaven search "<phrase>"` — passes through to the
  semantic search; phrase queries land near but may miss exact
  matches.

A proper inverted index is a possible 1.2.17+ direction but
not currently scheduled.

### History — bdslib

Pre-1.2 inkhaven used [`bdslib`](https://github.com/vulogov/bdslib)
(DuckDB + Tantivy + HNSW + fastembed) as a single document-
storage backend.  At some point in the 1.2.x cycle the storage
layer was reimplemented in-tree under `src/storage/` and the
bdslib dependency was dropped — see `src/storage/mod.rs` module
docs.  The current code keeps DuckDB + HNSW + fastembed; the
Tantivy part was not re-implemented because semantic search
covers the inkhaven editor's primary "find that prose I half-
remember" use case.  The name `bdslib` survives in:

* the doctor scan class `bdslib-only` (paragraph row present
  in the store but no on-disk `.typ` file) — see Tutorial 52
  §"Doctor scan classes" — kept as a stable identifier
  rather than renamed to `store-only`.
* a handful of `src/storage/mod.rs` doc comments that
  preserve the migration trail.

If you see "bdslib" in older Documentation/RELEASE_NOTES (e.g.
1.2.5 through 1.2.12), it's referring to the dependency that
was vendored / replaced.

## Embedding model cache

The first time you initialise a project, fastembed downloads the chosen
embedding model. It lands in a per-user cache:

| OS      | Cache location |
| ------- | -------------- |
| macOS   | `~/Library/Caches/dev.inkhaven.inkhaven/embeddings/` |
| Linux   | `$XDG_CACHE_HOME/inkhaven/embeddings/` (defaults to `~/.cache/inkhaven/`) |
| Windows | `%LOCALAPPDATA%\inkhaven\inkhaven\cache\embeddings\` |

Subsequent projects reuse the cache. Switching `embeddings.model`
downloads the new model the next time you open a project; the old one
stays on disk until you `rm -rf` the cache directory.

If the download hangs or fails (slow connection, server hiccup), the
splash screen reports the elapsed time. `Ctrl+Q` aborts startup so you
can retry.

## Troubleshooting

### "command not found: inkhaven"

The binary is in `./target/release/inkhaven` after `cargo build`. Either
invoke it with the full path, install it to a directory on your
`PATH`, or alias `inkhaven=~/path/to/inkhaven`.

### "first-run model download is hanging"

- Confirm internet connectivity.
- Look at the splash screen's elapsed counter — anything under 2 min
  on a normal connection is expected for `MultilingualE5Small`.
- `Ctrl+Q` to abort; restart later.
- For an air-gapped install, pre-populate the embedding cache from
  another machine.

### "config error: missing field `foo`"

You opened a config written by an older release. Either add the
missing field by hand (see [`CONFIGURATION.md`](CONFIGURATION.md)) or
delete the obsolete file and run `inkhaven init --force` to regenerate
from the current template.

### Tree pane and disk are out of sync

```bash
inkhaven --project <root> reindex --prune --adopt
```

### Editor pane shows old content

A previous session crashed without flushing. Run:

```bash
inkhaven --project <root> reindex
```

to re-read the `.typ` files into the database. The session-state file
(`.session.json`) will reopen the previously-focused paragraph next
launch.

### Search results look wrong after switching embedding models

```bash
inkhaven --project <root> reindex
```

re-embeds every paragraph with the new model.

### "no usable AI" or 401 errors

Check the relevant env var is set (`echo $GEMINI_API_KEY`). The status
bar reports which one is missing when an inference fails. See
[`CONFIGURATION.md`](CONFIGURATION.md#llm).

### Database file got corrupted

```bash
# 1. Take a fresh manual backup (just in case)
mv ~/Books/my-novel/metadata.db ~/Books/my-novel/metadata.db.corrupt
mv ~/Books/my-novel/blobs.db    ~/Books/my-novel/blobs.db.corrupt

# 2. Restore a known-good backup
inkhaven restore ~/Backups/blackinkhaven_<date>.zip --to ~/Books/my-novel-restored

# 3. If you have no backup but the books/ directory survived:
mkdir ~/Books/my-novel-rebuild
cp -r ~/Books/my-novel/books ~/Books/my-novel-rebuild/
inkhaven init --force ~/Books/my-novel-rebuild
inkhaven --project ~/Books/my-novel-rebuild reindex --adopt
```

The `--adopt` pass registers every `.typ` it finds under the matching
hierarchy branch derived from the directory layout — your prose is
recovered, hierarchy regenerated.

## Recovery patterns

### Restoring to a different machine

```bash
# On the source machine
inkhaven --project ~/Books/my-novel backup --out /tmp

# Copy /tmp/blackinkhaven_<date>.zip to the new machine

# On the new machine
inkhaven restore /tmp/blackinkhaven_<date>.zip --to ~/Books/my-novel
```

The embedding model cache is per-user — the first launch on the new
machine downloads the model again unless you pre-seeded the cache.

### Branching: derive a new project from an existing one

```bash
inkhaven --project ~/Books/my-novel backup --out /tmp
inkhaven restore /tmp/blackinkhaven_<date>.zip --to ~/Books/my-novel-fork
```

Both projects then evolve independently. UUIDs are identical, which
means if you ever merge content back you can correlate paragraphs by
ID.

### Rebuilding from `.typ` files alone

If you have lost the database but kept the `books/` tree, point
`reindex --adopt` at a fresh init:

```bash
mkdir ~/Books/rebuild
cp -r path/to/old/books ~/Books/rebuild/books
inkhaven init --force ~/Books/rebuild
inkhaven --project ~/Books/rebuild reindex --adopt
```

The reindexer scans every `.typ`, finds the deepest legal branch in the
existing hierarchy whose filesystem path matches the orphan file's
parent directory, and registers the paragraph there. New UUIDs are
assigned — this is a fresh project.