sqlite-graphrag 1.2.8

Persistent GraphRAG memory for Claude Code, Codex, Cursor, and 27 AI agents — one self-contained ~19 MiB Rust binary, zero daemon. Never re-explain your codebase again. Hybrid retrieval (FTS5 BM25 + cosine similarity + multi-hop graph traversal) surfaces the right memory in milliseconds. Embedding and entity enrichment run as parallel REST calls against your cloud LLM — no fragile headless subprocesses, no ONNX runtime, no model downloads. Soft-delete with full version history, transactional atomic writes, BLAKE3-tracked mutations. OAuth-only: raw API keys ABORT the spawn.
Documentation
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
# Headless Invocation — OpenRouter REST without MCP and without Hooks

> How to drive this project headlessly. The local subprocess backends were
> removed, so there is no CLI to isolate: every LLM call is an OpenRouter REST
> request, and MCP servers and hooks are structurally out of reach.

- Portuguese version of this guide lives in [HEADLESS_INVOCATION.pt-BR.md](HEADLESS_INVOCATION.pt-BR.md)
- Back to [README.md](../README.md) for the command reference


## Summary

- The only LLM transport is the OpenRouter REST API (`reqwest` + `rustls-tls`); no subprocess is ever spawned
- `--llm-backend` accepts `openrouter` (default) and `none`; `--llm-fallback` defaults to `none`
- `--embedding-backend openrouter --embedding-model MODEL` routes embedding through `POST /api/v1/embeddings`
- `enrich --mode openrouter --openrouter-model MODEL` routes the JUDGE turn through `POST /api/v1/chat/completions`
- Because nothing is spawned, there is no MCP config to strip, no hooks to zero out, and no CWD to isolate


## v1.2.1 Update — Enrich CAPA for headless agents (sidecar only)

CAPA themes (enrich queue seal; schema **v16**, no main-DB migration):

1. **`dequeue_next_pending`** — the claim filters by `operation` **and** `namespace` (draining `ai-sdd` MUST NOT process rows from `global` / the empty ns; the same holds for `--resume` / `--retry-failed`).
2. **`count_eligible_pending` for `--until-empty`** — counts only pending rows of this **op+ns** (alien ops / ReEmbed zombies elsewhere no longer keep EntityDescriptions looping with `completed=0`).
3. **`reopen_force_redescribe_candidates`** — `--force-redescribe` reopens `skipped`/`done` **once per process** before the first enqueue (so `INSERT OR IGNORE` is not a silent no-op); it **never** reopens `dead` (use `--requeue-dead`).
4. **`reconcile_satisfied_reembed_pending`** — marks a pending ReEmbed as `done` when a live vector with `LENGTH(embedding) = dim*4` already exists, clearing zombies without API calls.
5. **Re-embed eligibility by BLOB LENGTH** — the predicates use `LENGTH(embedding) = dim*4`, **not** the `dim` column alone (CORRUPT / META_AHEAD rows carrying dim=1024 with a 384-d BLOB become eligible again).
6. **`entity:` prefix stripped on enqueue** — entity lookup uses the bare name while the queue key stays `entity:…` (bare names still work; a missing entity is rejected).
7. **Chunk enqueue validates the namespace** — `chunk_id` must exist in a non-deleted memory of the target namespace (rejects invalid / cross-ns keys before dead-letter and circuit-breaker churn).
8. **CAPA-D** — compound "configuration file" markers only (e.g. `is a configuration file`); no bare false positive on legitimate domain prose.

Queue regressions: `enqueue_candidate_accepts_entity_prefixed_reembed_key`, `dequeue_next_pending_isolates_by_namespace`. Queue unit suite: **38** OK (`cargo test --lib commands::enrich::queue` or `cargo test --lib commands::enrich`).

Ready-made formulas for agents:

```bash
DB="${DB:-$HOME/.local/share/sqlite-graphrag/memory.db}"
MODEL="${MODEL:-deepseek/deepseek-v4-flash:nitro}"
NS="${NS:-global}"

sqlite-graphrag enrich --db "$DB" --status --operation re-embed --namespace "$NS" -q
sqlite-graphrag enrich --db "$DB" --operation re-embed --target entities \
  --mode openrouter --openrouter-model "$MODEL" \
  --until-empty --namespace "$NS" -q --wait-lock 60
sqlite-graphrag enrich --db "$DB" --operation entity-descriptions \
  --mode openrouter --openrouter-model "$MODEL" \
  --force-redescribe --until-empty --namespace "$NS" -q
sqlite-graphrag enrich --db "$DB" --list-skipped --operation entity-descriptions --namespace "$NS" -q
```

- Schema stays **v16** (no main-DB migration). The offline gate is still `scripts/e2e_offline_v120.sh` **20/20**. Pin `=1.2.2`.


## v1.2.0 Update — XDG, dim 1024, list-skipped, GAP-SG-139, headless hot-set

- Product-knob configuration: **CLI flag > XDG `config set` > default**. Product environment variables `SQLITE_GRAPHRAG_*` are **not** read on the hot path. Harnesses MUST use an isolated XDG root plus flags — never export product env as a configuration contract.
- **DEFAULT_EMBEDDING_DIM=1024** (override with `--embedding-dim` / XDG `embedding.dim`; existing databases keep `schema_meta.dim` until a re-embed).
- Recover `skipped` / `preservation_failed` queue debt without raw SQL:
  ```bash
  sqlite-graphrag enrich --list-skipped --json
  sqlite-graphrag enrich --requeue-skipped --json
  ```
- **GAP-SG-139:** host/XDG leaves (`config`, `slots`, `cache`, `completions`) accept `--db` as a documented **no-op** — headless agents can attach `--db` to every spawn without a clap exit 2.
- After a curated `remember`, PARSE `entities_created` / `enrich_recommended` and/or PASS `--enqueue-enrich` for priority entity-descriptions ahead of long entity-connect drains.
- Poll quality without an LLM: `enrich --operation entity-descriptions --status --force-redescribe --json` (`scan_backlog_low_quality`, `quality_pct`, `state` including `blocked_dead`).
- Name filters: `--entity-names` for entity-descriptions; `--memory-names` for memory-bindings.
- Audit bindings: `memory-entities --name <mem> --json` includes `entities[].description`.
- entity-connect is fully implemented (it persists relationships). On large databases expect `budget_exhausted` / `preempted_for_gate`; prefer hot ED → cold EC.
- Product offline gate: `bash scripts/e2e_offline_v120.sh` expects **20/20 PASS** (canonical; the historical `e2e_offline_v118.sh` / 16/16 wrapper is superseded).
- **CURRENT note about the historical text below:** sections teaching product env as configuration describe pre-v1.2.0 behaviour — v1.2.0 does **not** read product env on the hot path (XDG plus flags only). The OAuth / custom-provider env whitelist for LLM subprocesses remains valid and is **not** product-knob configuration.
- Secrets: prefer `config add-key --provider openrouter` (stdin) or `--openrouter-api-key`; `OPENROUTER_API_KEY` is not read at runtime.


## stdout/stderr contract and --quiet (v1.1.05) + `-o` alias (v1.1.8)

ADR: [ADR-0065](decisions/adr-0065-v1-1-05-incident-bugs.md). Regression suite: `tests/v1105_incident_bugs_regression.rs` (suite name **v1105**).

- Structured JSON ALWAYS on stdout; tracing logs ALWAYS on stderr
- Use `--quiet`/`-q` (global) to suppress non-error tracing — useful in headless pipelines that parse stdout with `jaq`
- For large `deep-research` envelopes, prefer `-o PATH` or `--output PATH` (atomwrite atomic write) instead of redirecting stdout into a file mixed with stderr. Stdout ack: `written`, `bytes`, `blake3`, `sub_queries_total`, `unique_memories_found`, `elapsed_ms`. Schema: `docs/schemas/deep-research-output-ack.schema.json`
- Single-token `deep-research` queries expand into sub-queries carrying `source: "aspect"` (multi-angle fan-out); manual strategy via `--sub-query-strategy manual --sub-queries-file`
- In headless scripts, use `graph traverse --fuzzy` when the canonical name is unknown; without an exact match, exit 4 carries suggestions
- Prefer `link --from-id`/`--to-id` in automation that only has IDs; NEVER pass bare digits to `--from`/`--to` with `--create-missing`
- `merge-entities` rejects self-references (`--into-id` also listed in `--ids`) before touching the database — useful under malformed zsh/bash loops
- Never use `sqlite-graphrag ... &> file` (it redirects stdout and stderr together and contaminates the JSON)

```bash
# headless deep-research with atomic file output (recommended for agents)
OUTDIR=/tmp/graphrag-out
mkdir -p "$OUTDIR"
sqlite-graphrag --quiet \
  --embedding-backend openrouter --embedding-model qwen/qwen3-embedding-8b --embedding-dim 1024 \
  deep-research "alice" --max-sub-queries 7 --k 20 --with-bodies \
  -o "$OUTDIR/research.json" --json
# Parse the ack on stdout; the full envelope is in the file
# Optional manual facets:
# printf '%s\n' 'alice stack' 'alice projects' > "$OUTDIR/subs.txt"
# sqlite-graphrag --quiet deep-research "alice" \
#   --sub-query-strategy manual --sub-queries-file "$OUTDIR/subs.txt" \
#   --output "$OUTDIR/research.json" --json
```


## v1.0.93 Update — OpenRouter Embedding Backend

- Since v1.0.93, embedding can use the OpenRouter REST API instead of spawning a headless LLM subprocess
- Use `--embedding-backend openrouter --embedding-model MODEL` to route embedding through `POST /api/v1/embeddings`
- This eliminates subprocess cold-start (~200ms API call vs 15-20s subprocess spawn per embedding)
- The OpenRouter path uses `reqwest+rustls-tls` directly — nothing is spawned, so no CWD isolation is needed
- OAuth-only enforcement does NOT apply to OpenRouter — it uses XDG `config add-key` / `--openrouter-api-key` (OPENROUTER_API_KEY is not read at runtime)
- Since v1.0.95 (ADR-0054), `enrich --mode openrouter` runs the JUDGE step through OpenRouter's `/chat/completions` REST endpoint (`reqwest+rustls-tls`). The SCAN→JUDGE→PERSIST pipeline is unchanged; only the JUDGE transport differs.
- The `--enrich-after` flag on `ingest` still spawns a headless subprocess for the enrich phase when the enrich mode is a local CLI; with `--mode openrouter` the enrich phase stays subprocess-free
- See ADR-0052 (OpenRouter embedding) and ADR-0054 (OpenRouter enrich JUDGE) for the full architectural rationale

## v1.0.95 Update — OpenRouter Enrich JUDGE

- `enrich --mode openrouter` routes the JUDGE step to `POST /api/v1/chat/completions` — no local CLI subprocess
- `--openrouter-model` is REQUIRED with `--mode openrouter` (NO default; omitting it → exit 1 before any network call)
- `--openrouter-api-key` reads from XDG via `config add-key` (OPENROUTER_API_KEY is not read at runtime) or `config add-key --provider openrouter`; `--openrouter-timeout` defaults to 300s; `--openrouter-base-url` is optional
- Request uses `response_format` `json_schema` with `strict: true` and `provider.require_parameters: true`; `reasoning.enabled: false` with a one-shot reasoning-mandatory fallback; `usage.cost` is read from the response
- Trade-off: OAuth zero-token (local CLI modes) vs tokens billed to the XDG-stored OpenRouter key (OPENROUTER_API_KEY is not read at runtime) (OpenRouter mode); schema advances v15 → v16 in v1.1.04 (migration V016 required); v15 in earlier releases

```bash
# Headless enrich JUDGE via OpenRouter REST (no subprocess, no CWD isolation)
printf "%s" "sk-or-v1-your-key-here" | sqlite-graphrag config add-key --provider openrouter --from-stdin
# OPENROUTER_API_KEY is not read at runtime (G-T-XDG-04)
sqlite-graphrag enrich --operation memory-bindings \
  --mode openrouter --openrouter-model "qwen/qwen3-235b-a22b" --json
```

## v1.0.96 Update — Backlog Convergence and Read-Only Queue Status (ADR-0055)

- `enrich --until-empty` replaces the external bash retry loop in headless invocation: a single process runs the internal scan→drain loop until the queue has no eligible items left or `--max-runtime <SECONDS>` (default 3600) expires. The dead-letter queue guarantees the live set strictly decreases — transient failures reschedule `next_retry_at` with backoff, an item turns `dead` after `--max-attempts` (default 8) transient retries or on the first hard failure, and `dead` rows are excluded from dequeue.
- `enrich --status --json` is the read-only probe for hooks and timers: it reports the queue counts (`unbound_backlog`, per-operation `scan_backlog`, `queue_pending/done/failed/dead/skipped`, `eligible_now`, `waiting`) and does NOT call the LLM and does NOT acquire the per-namespace singleton. `scan_backlog` (GAP-SG-77, v1.1.0) is the real per-operation database backlog a scan would enqueue — it kills the false `pending=0` for `entity-descriptions`/`body-enrich`/`re-embed`, and `state` derives `pending-scan` from it. A cron or systemd timer can poll it without contending with a running `enrich`.
- `enrich --prune-dead-orphans --json` is a companion read-only inspector (no LLM, no singleton): it deletes dead-letter rows (`status='dead'`, `item_type='memory'`) whose memory name no longer exists in the main DB, mutating only the `.enrich-queue.sqlite` sidecar; entity-keyed dead rows are left untouched. Use it in headless maintenance scripts to clear orphan dead-letter accumulation from memories renamed or purged after they were enqueued (ADR-0058, GAP-SG-66, v1.0.97).
- `enrich --prune-dead-entity-orphans --json` (v1.1.02, ADR-0062) is the entity-keyed counterpart: it deletes dead-letter rows with `item_type='entity'`, and is mutually exclusive with `--prune-dead-orphans`. Run both in sequence for a full orphan sweep after an upgrade that renamed/merged/purged entities.
- `--rest-concurrency <N>` (clamp 1..=16, default 8) sets the in-flight REST fan-out for `--mode openrouter` embedding; raise it for OpenRouter throughput. It is distinct from `--llm-parallelism` (which caps local LLM subprocesses) and from `--max-attempts` (the retry budget).

```bash
# Headless backlog drain — no external while-loop, no subprocess for OpenRouter
printf "%s" "sk-or-v1-your-key-here" | sqlite-graphrag config add-key --provider openrouter --from-stdin
# OPENROUTER_API_KEY is not read at runtime (G-T-XDG-04)
sqlite-graphrag enrich --operation memory-bindings \
  --mode openrouter --openrouter-model "qwen/qwen3-235b-a22b" \
  --until-empty --max-runtime 1800 --max-attempts 8 --rest-concurrency 8 --json

# Hook/timer probe — inspect the queue without spawning the LLM or taking the singleton
sqlite-graphrag enrich --status --json | jaq '{eligible_now, waiting, dead: .queue_dead}'
```

## v1.1.06 Update — Headless entity-connect on Large Namespaces (ADR-0066)

Decision record: [ADR-0066](decisions/adr-0066-v1-1-06-entity-connect-scan.md). Regression suite: `tests/v1106_entity_connect_scan_regression.rs` (suite name **v1106**).

- Closes **GAP-ENTITY-CONNECT-SCAN-CARTESIAN**: headless `enrich --operation entity-connect` (and `cross-domain-bridges`) on large `global` no longer hangs at 100% CPU before `phase: scan`. Pair scan is O(k) (co-occurrence + hub×island), not cartesian O(n²).
- Queue keys are `pair:{id1}:{id2}` with `item_type=entity_pair`; drain resolves by primary key (no re-scan per item). GAP-002 `entity_connect_seen` remains in force.
- **First-scan wall-clock** is covered by `--max-runtime` and a soft 120s ceiling via `InterruptHandle`. Timeout → `AppError::Timeout` exit **1**. Orchestrators MUST NOT treat scan timeout as exit **75** (job singleton / slot lock).
- NDJSON for hooks: expect `phase: "scan_start"` **before** SQL (`operation`, `entities_in_namespace`, `backlog_degree0_proxy`), then `scan` / `scan_meta` (`pairs_enqueued_this_scan`, `scan_elapsed_ms`). Do not equate the two backlog fields.
- Prefer dry-run smoke before long `--until-empty` jobs on dense graphs.
- No schema migration for v1.1.06 (schema stays v16). Pin `=1.1.6`.

```bash
# Headless dry-run must finish quickly and emit scan_start (no cartesian hang)
sqlite-graphrag enrich --operation entity-connect --dry-run --json --limit 50 \
  --mode openrouter --openrouter-model deepseek/deepseek-v4-flash:nitro

# Long converge: --max-runtime covers the FIRST scan too
sqlite-graphrag enrich --operation entity-connect --until-empty --max-runtime 600 \
  --mode openrouter --openrouter-model deepseek/deepseek-v4-flash:nitro --json
```

### v1.2.1 Update — Enrich CAPA for Headless Agents (Sidecar Only)

CAPA themes (enrich-queue seal; schema **v16**, no main-DB migration):

1. **`dequeue_next_pending`** — claim filters by `operation` **and** `namespace` (draining `ai-sdd` MUST NOT process `global` / empty-ns rows; same for `--resume` / `--retry-failed`).
2. **`count_eligible_pending` for `--until-empty`** — counts only this **op+ns** pending (alien ops / ReEmbed zombies elsewhere no longer keep EntityDescriptions spinning with `completed=0`).
3. **`reopen_force_redescribe_candidates`** — `--force-redescribe` reopens `skipped`/`done` **once per process** before first enqueue (so `INSERT OR IGNORE` is not a silent no-op); **never** reopens `dead` (use `--requeue-dead`).
4. **`reconcile_satisfied_reembed_pending`** — marks pending ReEmbed `done` when a live vector already matches `LENGTH(embedding) = dim*4`, clearing zombies without API calls.
5. **Re-embed eligibility by BLOB LENGTH** — predicates use `LENGTH(embedding) = dim*4`, **not** the `dim` column alone (CORRUPT / META_AHEAD rows with dim=1024 and a 384-d BLOB re-embed again).
6. **`entity:` prefix strip on enqueue lookup** — entity lookup uses the bare name; queue key stays `entity:…` (bare names still work; missing entity rejected).
7. **Chunk enqueue validates namespace** — `chunk_id` must exist on a non-deleted memory in the target namespace (rejects invalid / cross-ns keys before dead-letter/circuit-breaker churn).
8. **CAPA-D** — compound "configuration file" markers only (e.g. `is a configuration file`); no bare FP on legitimate domain prose.

Queue regressions: `enqueue_candidate_accepts_entity_prefixed_reembed_key`, `dequeue_next_pending_isolates_by_namespace`. Queue unit suite: **38** OK (`cargo test --lib commands::enrich::queue` or `cargo test --lib commands::enrich`).

Ready agent formulas:

```bash
DB="${DB:-$HOME/.local/share/sqlite-graphrag/memory.db}"
MODEL="${MODEL:-deepseek/deepseek-v4-flash:nitro}"
NS="${NS:-global}"

sqlite-graphrag enrich --db "$DB" --status --operation re-embed --namespace "$NS" -q
sqlite-graphrag enrich --db "$DB" --operation re-embed --target entities \
  --mode openrouter --openrouter-model "$MODEL" \
  --until-empty --namespace "$NS" -q --wait-lock 60
sqlite-graphrag enrich --db "$DB" --operation entity-descriptions \
  --mode openrouter --openrouter-model "$MODEL" \
  --force-redescribe --until-empty --namespace "$NS" -q
sqlite-graphrag enrich --db "$DB" --list-skipped --operation entity-descriptions --namespace "$NS" -q
```

- Schema stays **v16** (no main-DB migration). Offline gate still `scripts/e2e_offline_v120.sh` **20/20**. Pin `=1.2.2`.

### v1.2.0 Update — XDG Config, dim 1024, list-skipped, GAP-SG-139, Headless Hot-Set

- Config for product knobs is **CLI flag > XDG `config set` > default**. Product env `SQLITE_GRAPHRAG_*` is **not** read on the hot path. Harnesses MUST use isolated XDG (`XDG_CONFIG_HOME` / `XDG_DATA_HOME` / …) plus flags — never export product env as the config contract.
- **DEFAULT_EMBEDDING_DIM=1024** (override via `--embedding-dim` / XDG `embedding.dim`; existing DBs keep `schema_meta.dim` until re-embed).
- Recover skipped / `preservation_failed` queue debt without raw SQL:
  ```bash
  sqlite-graphrag enrich --list-skipped --json
  sqlite-graphrag enrich --requeue-skipped --json
  ```
- **GAP-SG-139:** host/XDG leaves (`config`, `slots`, `cache`, `completions`) accept `--db` as a documented **no-op** — headless agents may append `--db` on every spawn without clap exit 2.
- After curated `remember`, PARSE `entities_created` / `enrich_recommended` and/or PASS `--enqueue-enrich` so entity-descriptions runs as a hot set before long entity-connect drains.
- Poll quality without LLM: `enrich --operation entity-descriptions --status --force-redescribe --json` (`scan_backlog_low_quality`, `quality_pct`, `state` including `blocked_dead`).
- Name filters: `--entity-names` for entity-descriptions; `--memory-names` for memory-bindings; do not assume `--names` always means memories.
- Audit bindings: `memory-entities --name <mem> --json` includes `entities[].description`.
- entity-connect is fully implemented (persists relationships). On large DBs expect adaptive budget fields (`budget_exhausted`, `preempted_for_gate`) and prefer ED hot → EC cold.
- Offline product gate: `bash scripts/e2e_offline_v120.sh` expects **20/20 PASS** (canonical; historical wrapper `e2e_offline_v118.sh` / 16/16 is superseded).
- **CURRENT note on historical text below:** sections that teach product env as config describe pre-v1.2.0 behaviour — v1.2.0 does **not** read product env on the hot path (XDG + flags only). The OAuth/custom-provider env whitelist for LLM subprocesses remains valid and is **not** product-knob config.
- Secrets: prefer `config add-key --provider openrouter` (stdin) or `--openrouter-api-key`; `OPENROUTER_API_KEY` is not read at runtime; use `config add-key` or `--openrouter-api-key` only.

## Complete CLI command inventory for headless agents (v1.2.5)

Headless orchestrators must know the full product surface even when spawn recipes focus on `remember` / `enrich` / `deep-research`. Top-level product commands (from `sqlite-graphrag --help`, excluding meta `help`):

- `init` — create/open DB + migrations + LLM smoke-test
- `remember` — write one memory (+ optional graph / `--enqueue-enrich`)
- `remember-batch` — NDJSON batch create (description required)
- `ingest` — bulk-ingest files as memories
- `recall` — semantic KNN search
- `read` / `list` / `forget` / `purge` / `rename` / `split-body` / `edit` / `history` / `restore` — memory CRUD + lifecycle
- `hybrid-search` — FTS5 + vector RRF
- `health` / `migrate` / `namespace-detect` / `optimize` / `stats` — ops & diagnostics
- `sync-safe-copy` / `backup` / `vacuum` — durability & space
- `link` / `unlink` / `related` / `graph` / `export` — graph edges & export
- `deep-research` — multi-hop GraphRAG (`-o` / `--output` atomwrite)
- `fts` / `vec` — index maintenance families
- `prune-relations` / `prune-ner` / `cleanup-orphans` — graph hygiene
- `slots` / `pending` / `embedding` / `pending-embeddings` — concurrency & queues
- `memory-entities` / `delete-entity` / `reclassify` / `rename-entity` / `merge-entities` / `reclassify-relation` / `normalize-entities` — entity admin
- `enrich` — LLM graph quality + queue inspectors (`--list-skipped` / `--requeue-skipped`)
- `cache` / `completions` / `config` — host/XDG leaves (`--db` no-op)

### Nested families (brief)

- `graph` — `traverse`, `stats`, `entities`, `recompute-degree`
- `embedding` — `status`, `list`, `abandon`
- `pending` — `list`, `show`, `cleanup`
- `pending-embeddings` — `list`, `status`, `abandon`
- `slots` — `status`, `release`, `cleanup`
- `cache` — `clear-models`, `list`, `stats`
- `config` — `add-key`, `list-keys`, `remove-key`, `doctor`, `path`, `set`, `get`, `list`, `unset`
- `fts` — `rebuild`, `check`, `stats`
- `vec` — `orphan-list`, `purge-orphan`, `stats`
- `enrich` inspectors: `--status`, `--list-dead`, `--requeue-dead`, `--list-skipped`, `--requeue-skipped`, `--prune-dead-orphans`, `--prune-dead-entity-orphans`; **v1.2.1 write flags:** `--until-empty` (op+ns count), `--force-redescribe` (reopen skipped/done), `--operation re-embed --target …`, `--namespace`, `--mode openrouter`, `--rest-concurrency`
- Global output flags added in **v1.2.2** (apply to every subcommand)
  - `--select <KEYS>` / `--fields` — keep only these keys per result element; dotted paths OK; missing key skipped, never `null`
  - `--filter <EXPR>` — `key=value`, `key!=value`, `key~substring`; `==` synonym of `=`; repeat to conjoin with AND; malformed exits 2
  - `--max-items <N>` — cap on emitted elements, applied after filtering; distinct from per-subcommand `--limit` and from `-k`
  - `--sort <KEY>` — ascending by dotted path; numbers numeric, rest text
  - `--dedupe-by <KEY>` — drop later elements repeating the value
  - `--count-only` — payload becomes `{"count": N}`
  - `--truncate-content <N>` — shorten strings past N characters, never bytes
  - `--max-output-bytes <N>` — cap the envelope by dropping trailing elements, never by slicing JSON
  - Failure envelopes (`error: true` / `ok: false`) and `$schema` documents are never reshaped; NDJSON streams bypass the surface
- Global input flag added in **v1.2.2**
  - `--no-input` — refuse stdin anywhere in the invocation; every stdin reader fails up front with exit 1; precedence flag > XDG `cli.no_input` > `false`
- `schema` — machine-readable catalog of all **76** JSON contracts
  - `schema` — NDJSON listing, one `{"id","invoke"}` per line; `invoke` is the ready-to-copy command
  - `schema --name <ID>` — emit that contract's JSON Schema document
  - Unknown `<ID>` exits **4**; `$schema` documents are exempt from the agent-native output surface, so any global flag can be chained safely

> **Top-level (50 + help):** init, remember, remember-batch, ingest, recall, read, list, forget, purge, rename, split-body, edit, history, restore, hybrid-search, health, migrate, namespace-detect, optimize, stats, sync-safe-copy, backup, vacuum, link, unlink, deep-research, related, graph, export, fts, vec, prune-relations, prune-ner, slots, pending, embedding, pending-embeddings, cleanup-orphans, memory-entities, cache, delete-entity, reclassify, rename-entity, merge-entities, enrich, reclassify-relation, normalize-entities, schema, completions, config, help.

> Full inventory with one-line purposes: [HOW_TO_USE.md](HOW_TO_USE.md#complete-cli-command-inventory-v125) and [COOKBOOK.md](COOKBOOK.md).

### v1.1.05 Update — Headless Pipeline Safety (`--quiet`, `deep-research --output` / `-o`)

Decision record: [ADR-0065](decisions/adr-0065-v1-1-05-incident-bugs.md). Regression suite: `tests/v1105_incident_bugs_regression.rs` (suite name **v1105**).

- Global `--quiet` / `-q` suppresses non-error tracing on stderr so agent harnesses can parse stdout as pure JSON without log noise.
- `deep-research -o PATH` or `--output PATH` writes the full research envelope via atomwrite (tempfile in the same directory → fsync → rename) and prints only a short stdout ack: `written`, `bytes`, `blake3`, `sub_queries_total`, `unique_memories_found`, `elapsed_ms`. Prefer this for large `--with-bodies` jobs under agent orchestrators. Schema: `docs/schemas/deep-research-output-ack.schema.json`.
- Contract: **stdout = JSON** (envelope or ack), **stderr = logs**. NEVER redirect both to the same file with `&>` or `2>&1` into a JSON consumer.
- Single-token queries (e.g. a person name) expand to multi-aspect sub-queries (`source: "aspect"`) so headless research on a subject token is no longer a single hybrid hit. Manual override for orchestrators: `--sub-query-strategy manual --sub-queries-file PATH`.
- `graph traverse --fuzzy` is safe for headless nickname resolution; without `--fuzzy`, exit 4 NotFound includes ranked suggestions for the orchestrator to pick.
- `link --from-id`/`--to-id` and merge self-ref pre-DB rejection reduce silent graph corruption in scripted maintenance.

```bash
# Headless deep-research with atomic file output (recommended for agents)
OUTDIR=/tmp/graphrag-out
mkdir -p "$OUTDIR"
sqlite-graphrag --quiet \
  --embedding-backend openrouter --embedding-model qwen/qwen3-embedding-8b --embedding-dim 1024 \
  deep-research "alice" --max-sub-queries 7 --k 20 --with-bodies \
  -o "$OUTDIR/research.json" --json
# Parse ack from stdout; full envelope from the file
# Optional manual facets:
# printf '%s\n' 'alice stack' 'alice projetos' > "$OUTDIR/subs.txt"
# sqlite-graphrag --quiet deep-research "alice" \
#   --sub-query-strategy manual --sub-queries-file "$OUTDIR/subs.txt" \
#   -o "$OUTDIR/research.json" --json
```

### v1.1.04 Update — Deep-Research Stability + entity-connect Convergence (ADR-0064)

- GAP-001: `deep-research` no longer panics with "Cannot start a runtime from within a runtime" when invoked headless (agent harnesses, CI runners, scheduled jobs). The sync entry point `deep_research::run` now computes per-sub-query embeddings BEFORE building its dedicated Tokio runtime via the new `compute_sub_embeddings` helper, and the three OpenRouter embedding paths in `embedder.rs` (single, serial batch, JoinSet fan-out) adopt the canonical `Handle::try_current` + `block_in_place` reentry pattern already used by the batch path. For headless orchestrators this means long-running `deep-research --with-bodies` jobs that previously crashed mid-flight now complete reliably.
- GAP-002: `entity-connect` now converges in headless long-running loops. A new `entity_connect_seen` table (migration V016, main database schema v15 → v16) records the LLM verdict (`related`/`none`) for each evaluated pair; the `scan_isolated_entity_pairs` scanner excludes already-evaluated pairs and prioritises hub entities; and `call_entity_connect` persists the verdict on both branches. Combined with `--until-empty --max-runtime`, a headless `enrich --operation entity-connect` job now reaches `eligible_remaining == 0` instead of re-evaluating the same rejected pairs forever. Running `migrate --json` once on first open is REQUIRED before the first `entity-connect` invocation.


### v1.1.03 Update — Stale Claim Recovery in Headless Long-Running enrich

- Headless orchestrators (agent harnesses, CI runners, systemd timers) frequently send SIGINT, SIGTERM, and occasionally SIGKILL to long-running `enrich --until-empty` jobs
- SIGKILL is NOT capturable — the `.enrich-queue.sqlite` sidecar may be left with rows stuck in `status='processing'` under the dead PID
- Since v1.1.03 (ADR-0063, Bug 4), the queue sidecar gains a `claimed_at` INTEGER column and the enrich worker emits a per-item heartbeat (`UPDATE queue SET claimed_at = unixepoch() WHERE id = ?`)
- On EVERY enrich startup, the worker calls `reset_stale_processing_claims(conn, 1800)` — items with `status='processing' AND claimed_at < unixepoch() - 1800` are flipped back to `pending` and `claimed_at = NULL`
- The 1800-second (30-minute) threshold is the default; combined with the heartbeat it covers any job that stops making progress for half an hour
- For manual reset (e.g. after a known kill -9 incident), the new flag `enrich --reset-stale-claims --json` flushes stale claims without running the full scan→drain loop
- SIGTERM (capturable) is handled by the existing `signals::handler` graceful path; only SIGKILL relies on the timestamp-based recovery
- No new env var, no telemetry — the recovery is silent and idempotent

```bash
# Force-reset stale claims after a known kill -9 incident (no scan, no LLM)
sqlite-graphrag enrich --reset-stale-claims --json

# Normal headless enrich — stale claims auto-recovered at startup
sqlite-graphrag enrich --operation memory-bindings \
  --mode openrouter --openrouter-model "qwen/qwen3-235b-a22b" \
  --until-empty --max-runtime 1800 --json
```

## v1.0.93 Update — OpenRouter Embedding Backend
- New `--embedding-backend openrouter` flag enables REST API embedding without LLM subprocess
- Eliminates cold-start overhead: ~200ms per embedding vs 15s with subprocess
Set API key via `config add-key --provider openrouter` or `--openrouter-api-key` (OPENROUTER_API_KEY is not read at runtime)
- Requires `--embedding-model MODEL` (no default — user must specify)
- Works with all 8 embedding commands in headless mode
- Example headless invocation:

```bash
sqlite-graphrag \
  --embedding-backend openrouter \
  --embedding-model "qwen/qwen3-embedding-8b" \
  ingest ./docs --pattern "*.md" --recursive --json
```

## Global LLM flags for headless agents

- `--llm-backend <openrouter|none>` — selects the embedding transport. Default `openrouter`; `none` skips embedding
- `--llm-model <MODEL>` — model passed to the selected backend
- `--llm-fallback <chain>` — fallback chain when the primary backend fails. Default `none`
- `--skip-embedding-on-failure` — persist the memory without a vector (exit 0 instead of exit 11)
- `--llm-max-host-concurrency <N>` — cap concurrent LLM calls host-wide
- `--llm-slot-wait-secs <N>` — seconds to wait for a free slot before failing
- `--llm-slot-no-wait` — fail immediately when no slot is available

### sqlite-graphrag with backend and model override

```bash
# Force the OpenRouter backend with a specific embedding model
sqlite-graphrag --llm-backend openrouter --llm-model "qwen/qwen3-embedding-8b" \
  remember --name example --type note --body "text" --json

# Skip embedding entirely (no vector written)
sqlite-graphrag --llm-backend none \
  remember --name no-vector --type note --body "text" --json

# Skip embedding on failure (persist memory without vector)
sqlite-graphrag --skip-embedding-on-failure \
  remember --name resilient --type note --body "text" --json
```


## Headless Patterns Added in v1.0.82
### Shutdown envelope capture pattern (GAP-002, ADR-0037)
```bash
# Wrap a long-running sqlite-graphrag invocation in a signal handler
# that captures the shutdown JSON envelope on stdout at exit 19.
timeout 300 sqlite-graphrag remember --name big-corpus --type document \
  --body-file ./big.md --json 2>/tmp/err.log
EXIT=$?
if [ $EXIT -eq 19 ]; then
  # parse the envelope from the last line of stdout
  jaq -e '.error and .code == 19' /tmp/err.log
  jaq -r '.signal, .graceful' /tmp/err.log
fi
```
### Fallback chain wrap pattern (GAP-003 + GAP-005, ADR-0038 + ADR-0040)
```bash
# Pre-flight: confirm the OpenRouter key resolves before launching
sqlite-graphrag config doctor --json | jaq -e '.openrouter_key_present' >/dev/null \
  || { echo "OpenRouter key missing in XDG (config add-key); OPENROUTER_API_KEY is not read at runtime"; exit 1; }

# Launch with the explicit backend
sqlite-graphrag remember --name foo --type note --body "..." \
  --llm-backend openrouter --json

# If the backend fails, inspect the pending queue
sqlite-graphrag pending-embeddings list --status pending --json
```
### Slot semaphore poll pattern (GAP-004, ADR-0039)
```bash
# Wait until a slot is free before launching a heavy batch
while [ "$(sqlite-graphrag slots status --json | jaq '.acquired')" -gt 0 ]; do
  sleep 5
done
sqlite-graphrag ingest ./big-corpus --recursive --json
```