sqlite_graphrag/constants/enrich.rs
1//! Token budgets, fan-out and pacing for the `enrich` pipeline.
2//!
3//! Split out of the former single-file `constants.rs` in v1.2.5;
4//! every item is re-exported by the parent module, so `crate::constants::X`
5//! resolves exactly as before.
6
7/// GAP-SG-185 / v1.2.4: default keyset page size for enrich scan collectors.
8///
9/// SQL row buffers and production page→enqueue key buffers are this wide; the
10/// sidecar queue still stores one row per eligible item. Override via CLI
11/// `--scan-page-size` or XDG `enrich.scan_page_size`.
12pub const DEFAULT_ENRICH_SCAN_PAGE_SIZE: usize = 512;
13
14/// Accepted range for `enrich.scan_page_size` / `--scan-page-size`.
15pub const ENRICH_SCAN_PAGE_SIZE_RANGE: std::ops::RangeInclusive<usize> = 1..=4096;
16
17/// Initial `max_tokens` budget sent on an `enrich` chat-completion request
18/// (GAP-SG-70/71).
19///
20/// Chosen well below [`ENRICH_MAX_TOKENS_CEILING`] so a well-formed response
21/// completes in one attempt for the common case; only bodies that need more
22/// room trigger the growth loop below.
23pub const ENRICH_INITIAL_MAX_TOKENS: u32 = 4_096;
24
25/// Multiplier applied to `max_tokens` each time OpenRouter reports
26/// `finish_reason: "length"` on an `enrich` chat-completion (GAP-SG-70/71).
27pub const ENRICH_MAX_TOKENS_GROWTH_FACTOR: u32 = 2;
28
29/// Upper bound on `max_tokens` growth for an `enrich` chat-completion
30/// (GAP-SG-70/71).
31///
32/// Kept with margin under the ~32K-token context ceiling of
33/// `deepseek/deepseek-v4-flash:nitro` (see [`crate::constants::EMBEDDING_REQUEST_MAX_TOKENS`]
34/// for the equivalent embedding-side ceiling) so growth never requests a
35/// budget the model cannot honour.
36pub const ENRICH_MAX_TOKENS_CEILING: u32 = 16_384;
37
38/// Maximum number of `max_tokens`-growth re-attempts after a truncated
39/// (`finish_reason: "length"`) `enrich` chat-completion, before giving up and
40/// returning the truncation as an error (GAP-SG-70/71).
41pub const ENRICH_MAX_LENGTH_RETRIES: u32 = 2;
42
43/// Default REST fan-out for `enrich --mode openrouter` when `--rest-concurrency`
44/// is omitted (GAP-SG-141).
45///
46/// The clap parser clamps the flag to `1..=16`; this constant is the value used
47/// when the operator passes nothing, keeping the default out of an inline
48/// `unwrap_or` at the call site.
49pub const DEFAULT_ENRICH_REST_CONCURRENCY: u32 = 8;
50
51/// Lowest REST fan-out `enrich --mode openrouter` accepts (GAP-SG-266).
52///
53/// One means serial. Zero would mean "no worker", which is not a slower drain
54/// but an absent one, so the floor is a refusal and not a preference.
55pub const MIN_ENRICH_REST_CONCURRENCY: u32 = 1;
56
57/// Highest REST fan-out `enrich --mode openrouter` accepts (GAP-SG-266).
58///
59/// The ceiling protects the shared OpenRouter quota, which is a HOST-scoped
60/// scarcity: the key lives once in `~/.config/sqlite-graphrag/config.toml` and
61/// every folder on the machine spends from it. It is enforced twice on purpose
62/// — the clap parser REFUSES an out-of-range flag, and
63/// `commands::enrich::events::parallelism` clamps whatever reaches it, so a
64/// caller that bypasses the parser still cannot exceed the ceiling. That module
65/// is an internal hook with no public page, so this is a code span and not a
66/// link.
67pub const MAX_ENRICH_REST_CONCURRENCY: u32 = 16;
68
69/// Default subprocess worker count for `enrich` when `--llm-parallelism` is
70/// omitted (GAP-SG-141).
71///
72/// `1` means serial. The flag is inert under `--mode openrouter`, where
73/// [`DEFAULT_ENRICH_REST_CONCURRENCY`] governs fan-out instead.
74pub const DEFAULT_ENRICH_LLM_PARALLELISM: u32 = 1;
75
76/// Linked entities pulled into the prompt context when enriching a body.
77pub const K_ENRICH_BODY_CONTEXT_ENTITIES_LIMIT: usize = 10;
78
79/// Cooldown, in seconds, before a tripped per-worker circuit breaker allows the
80/// next attempt (`enrich` parallel drain).
81///
82/// One minute is the smallest window in which a provider outage plausibly
83/// resolves; shorter turns the breaker into a no-op, longer strands healthy
84/// workers. The trip *threshold* is the per-run knob
85/// (`--circuit-breaker-threshold`); this window is the host-tuning companion,
86/// so it is XDG-only: `enrich.circuit_breaker_reset_secs`, resolved by
87/// [`crate::runtime_config::enrich_circuit_breaker_reset_secs`].
88pub const DEFAULT_ENRICH_CIRCUIT_BREAKER_RESET_SECS: u64 = 60;
89
90/// Poll interval, in milliseconds, of the watchdog that interrupts an
91/// over-budget enrich scan.
92///
93/// Bounds how far past its deadline a scan can run. Fifty milliseconds is
94/// imperceptible against a scan measured in seconds while keeping the watchdog
95/// thread idle. Coordination wait, so it takes no XDG key.
96pub const ENRICH_SCAN_WATCHDOG_POLL_MS: u64 = 50;
97
98/// Nap, in seconds, taken by an `--until-empty` drain when every remaining item
99/// is still serving its backoff.
100///
101/// The loop has nothing to claim, so it sleeps rather than re-querying SQLite in
102/// a tight loop; the real stopping conditions are `--max-runtime` and
103/// convergence. Coordination wait, so it takes no XDG key.
104pub const ENRICH_UNTIL_EMPTY_IDLE_NAP_SECS: u64 = 1;
105
106/// Ceiling, in seconds, on the exponential backoff a rate-limited drain waits
107/// between attempts.
108///
109/// The doubling in both drains used to stop at a bare `900` written twice, once
110/// per drain. Two literals of the same policy is one edit away from two
111/// policies, and the serial and parallel paths would then disagree about how
112/// long a rate limit is allowed to stall a run. Coordination wait against a
113/// remote limit, so it takes no XDG key: the tunable the operator actually has
114/// is `enrich.rate_limit_deadline_secs`, which bounds the whole wait rather
115/// than one step of it.
116pub const ENRICH_BACKOFF_CEILING_SECS: u64 = 900;
117
118/// Default wall-clock budget, in seconds, for one `enrich` run when
119/// `--max-runtime` is omitted.
120///
121/// Previously an unnamed `3600` inside `unwrap_or`, contradicted by its own
122/// doc-comment two lines above; naming it is what lets the help text and the
123/// default be read from the same place.
124pub const DEFAULT_ENRICH_MAX_RUNTIME_SECS: u64 = 3_600;
125
126/// Number of chars of a memory body shown to the model as a PREVIEW.
127///
128/// GAP-SG-279 measured six body truncations across the enrich modules carrying
129/// three different literals — 500, 2000 and 200 — none of them named. The
130/// divergence was invisible because each site read as a local decision; taken
131/// together they meant the same operation class showed the model wildly
132/// different amounts of the same corpus. Preview is the smallest of the three
133/// roles: enough to identify a memory, never enough to reason from.
134pub const ENRICH_BODY_PREVIEW_CHARS: usize = 500;
135
136/// Number of chars of a memory body sent when the body ITSELF is the subject.
137///
138/// Used where the model must reason over the body rather than recognise it —
139/// synthesis and extraction — so it is four times the preview budget.
140pub const ENRICH_BODY_SUBJECT_CHARS: usize = 2_000;
141
142/// Number of chars of a memory body kept in a LOG or diagnostic line.
143///
144/// Smallest of the three roles: this text is never sent to a model, it only
145/// has to let a human recognise which memory a line refers to.
146pub const ENRICH_BODY_LOG_PREVIEW_CHARS: usize = 200;
147
148/// Minimum description length, in chars, below which a description is judged
149/// generic and eligible for rewriting.
150///
151/// Lived inside the text of `GENERIC_DESCRIPTION_PREDICATE` as a bare `30`. A
152/// number embedded in a SQL string is neither typed nor greppable: changing the
153/// policy meant editing prose, and nothing connected it to the quality report
154/// that acts on the same idea.
155pub const ENRICH_GENERIC_DESCRIPTION_MAX_CHARS: usize = 30;
156
157/// Relationship weight at or above which an edge counts as HIGH weight.
158///
159/// Same defect as the constant above: it lived as `0.7` inside a
160/// `const &str` holding SQL, so it was formally a constant and practically a
161/// literal — untyped, and impossible to reuse from the Rust side.
162pub const ENRICH_HIGH_WEIGHT_THRESHOLD: f64 = 0.7;
163
164/// Weight given to the accept rate when blending it with mean grounding score
165/// into a single quality figure.
166///
167/// The blend was written as `0.5 * accept_rate + 0.5 * mean_score` with both
168/// halves anonymous. Naming one of them states that the two signals are
169/// deliberately equal rather than accidentally so, and gives the pair a single
170/// place to change.
171pub const ENRICH_QUALITY_ACCEPT_RATE_WEIGHT: f64 = 0.5;
172
173/// Weight given to the mean grounding score in the same blend.
174///
175/// Must sum to one with [`ENRICH_QUALITY_ACCEPT_RATE_WEIGHT`]; the test beside
176/// the blend asserts it.
177pub const ENRICH_QUALITY_MEAN_SCORE_WEIGHT: f64 = 0.5;