Skip to main content

khive_db/checkpoint/
task_loop.rs

1//! Periodic checkpoint work and pressure-episode transitions.
2
3use std::sync::atomic::Ordering;
4use std::sync::Arc;
5use std::time::Instant;
6
7use crate::pool::ConnectionPool;
8
9#[cfg(unix)]
10use super::{
11    complete_walpin_attribution, run_walpin_housekeeping_if_due, WalpinSidecarState,
12    DEFAULT_SESSION_SWEEP_INTERVAL,
13};
14use super::{
15    crossing_warn, log_tx_age_emission, log_tx_registry_oldest_debug, log_tx_registry_oldest_warn,
16    log_tx_registry_snapshot_warn, log_wal_high_water_warn, note_checkpoint_pressure_observation,
17    note_checkpoint_skipped, off_worker, CheckpointConfig, CheckpointConnection,
18    CheckpointLifecycleEmitter, CheckpointLifecycleOwner, CheckpointPressureEpisode,
19    CheckpointRunTaskGuard, CheckpointSeverityRung, CheckpointSeverityState, CheckpointTick,
20    ReaderCheckoutSweepState, TruncateState, TxAgeSweepState, CHECKPOINT_LIFECYCLE_ENQUEUE_DROPS,
21};
22
23/// Run one due FTS5 maintenance step off this task's Tokio worker thread.
24///
25/// A due step can issue up to `config.merge_pages` pages of synchronous
26/// SQLite incremental-merge I/O against a trigram index over a corpus of
27/// hundreds of thousands of rows — the same class of blocking work the
28/// WAL-pin beacon writes above already move off the worker via
29/// `tokio::task::spawn_blocking`. `conn` and `state` are moved into the
30/// blocking closure and handed back to the caller whenever the step returns,
31/// so the checkpoint task can restore its dedicated connection and scheduler
32/// state on every non-panicking path. A panic inside the step surfaces as the
33/// `JoinError`, like the beacon writes above: the connection and scheduler
34/// state moved into the task are gone with it, and the caller reopens both
35/// on the next tick instead of taking the checkpoint task down.
36pub(super) async fn run_fts_maintenance_off_worker(
37    conn: rusqlite::Connection,
38    config: crate::fts_maintenance::FtsMaintenanceConfig,
39    mut state: crate::fts_maintenance::FtsMaintenanceState,
40    now: Instant,
41) -> Result<
42    (
43        rusqlite::Connection,
44        crate::fts_maintenance::FtsMaintenanceState,
45        Result<Option<crate::fts_maintenance::FtsMaintenanceStep>, String>,
46    ),
47    tokio::task::JoinError,
48> {
49    tokio::task::spawn_blocking(move || {
50        let result = crate::fts_maintenance::run_if_due(&conn, &config, &mut state, now);
51        (conn, state, result)
52    })
53    .await
54}
55
56/// Run the WAL checkpoint background task.
57///
58/// Long-running async task — spawn with `tokio::spawn`. Loops until
59/// `shutdown_rx` observes a change (or its sender is dropped). Callers MUST
60/// hold the paired `tokio::sync::watch::Sender` for the daemon's run scope
61/// and send on it to shut down — do NOT rely on `pool`'s `Arc` refcount
62/// reaching zero; a sibling owner (e.g. `event_store`) holding its own clone
63/// makes that check unreachable (issue #774).
64///
65/// Issues `PRAGMA wal_checkpoint(PASSIVE)` every tick on the task's dedicated
66/// `CheckpointConnection` — never the pool's writer mutex, so a concurrent
67/// `pool.writer()` checkout can never queue behind a checkpoint tick. That
68/// guarantee is admission-only: an armed TRUNCATE still takes SQLite's writer
69/// lock and can block new write transactions, on any connection, for up to
70/// `truncate_busy_timeout` (see `CheckpointConnection`'s contract). The
71/// checkpoint call itself runs on `spawn_blocking`, so that wait never holds
72/// one of the runtime's worker threads. A tick is
73/// `Skipped` when that connection is unavailable or SQLite returns a busy
74/// PASSIVE row without a usable pressure observation. A
75/// WARNING fires once per below→above threshold crossing, not every tick.
76///
77/// `lifecycle_owner` (ADR-094): exactly one task in a multi-backend fan-out
78/// should receive `Some`. That task appends a best-effort
79/// `CheckpointOutcomeRecorded` event on the elevation transition and one
80/// recovery summary when pressure falls back below `warn_pages`. Sustained
81/// elevated ticks aggregate in memory and in `db_diagnostics`; they never
82/// write one primary-store row per checkpoint attempt. `None` explicitly
83/// marks a non-owner. See `crates/khive-db/docs/api/checkpoint.md` for the
84/// full shutdown-mechanism and event-emission design history.
85///
86/// `is_main` (ADR-091 Amendment 3): whether `pool` is the deployment's main
87/// backend. A daemon owning several file-backed backends spawns one task per
88/// backend, each with its own pool and shutdown-channel clone (the sender
89/// broadcasts to every receiver clone alike). Lifecycle ownership is selected
90/// independently through `lifecycle_owner`; `is_main` only controls registry
91/// filtering. See the `tx_filter` construction below.
92pub async fn run_checkpoint_task(
93    pool: Arc<ConnectionPool>,
94    config: CheckpointConfig,
95    lifecycle_owner: Option<CheckpointLifecycleOwner>,
96    mut shutdown_rx: tokio::sync::watch::Receiver<()>,
97    is_main: bool,
98) {
99    let _checkpoint_run_guard = CheckpointRunTaskGuard::start(&pool, config.interval);
100    // This task IS the dedicated checkpoint owner: claim the pool so writer
101    // connections drop the bounded autocheckpoint fallback and routine
102    // checkpoint I/O stays off application commit paths. Pools without a
103    // running checkpoint task never claim and keep SQLite's bounded WAL
104    // reclamation. A failed claim leaves connections on the bounded fallback
105    // — safe, just not the low-latency posture — so it warns and continues.
106    match pool.claim_checkpoint_ownership() {
107        Ok(()) => {
108            if let Err(e) = pool.propagate_checkpoint_claim_to_writer_task().await {
109                tracing::warn!(target: "khive_db::checkpoint",
110                    error = %e,
111                    "checkpoint task could not reach the writer task's connection; it keeps the \
112                     bounded autocheckpoint fallback"
113                );
114            }
115        }
116        Err(e) => {
117            tracing::warn!(target: "khive_db::checkpoint",
118                error = %e,
119                "checkpoint task could not re-apply the ownership pragma on the pooled writer; \
120                 writer connections keep the bounded autocheckpoint fallback unless ownership is \
121                 claimed later"
122            );
123        }
124    }
125    let mut interval = tokio::time::interval(config.interval);
126    interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
127    let mut severity_state = CheckpointSeverityState::default();
128    let mut tx_age_state = TxAgeSweepState::default();
129    let mut reader_checkout_state = ReaderCheckoutSweepState::default();
130    let mut was_above_high_water = false;
131    #[cfg(unix)]
132    let legacy_walpin_fallback_interval = DEFAULT_SESSION_SWEEP_INTERVAL;
133    #[cfg(unix)]
134    let mut truncate_state =
135        TruncateState::with_legacy_walpin_fallback(legacy_walpin_fallback_interval);
136    #[cfg(not(unix))]
137    let mut truncate_state = TruncateState::default();
138    let mut lifecycle_emitter = CheckpointLifecycleEmitter::new(lifecycle_owner);
139    // Independent of `severity_state` (which owns the WARN ladder): this
140    // tracks the lifecycle sink's accepted elevation state. A full queue
141    // leaves it unchanged, so an opening or recovery transition is retried
142    // without admitting more than one primary-store write for that edge.
143    let mut event_elevation_open = false;
144    let mut pressure_episode: Option<CheckpointPressureEpisode> = None;
145    // A recovery row whose `try_emit` lost the race against a full queue.
146    // Retried on later ticks (before that tick's own transition handling)
147    // instead of leaving `pressure_episode` open for a stale episode to
148    // absorb the next, genuinely separate, pressure incident (#1857).
149    let mut pending_recovery: Option<khive_storage::CheckpointOutcomeRecordedPayload> = None;
150    let mut was_observed_above_warn = false;
151    // ADR-091 Amendment 3: this task's own backend-scoped view of the
152    // registry. `is_main` selects which `TxOriginFilter` variant applies —
153    // the caller passes `true` for exactly the one checkpoint task covering
154    // the deployment's main backend, so only that task also observes legacy
155    // `Unscoped` spans from any call site not yet threaded to an origin, the
156    // designed never-silently-drop fallback. A secondary backend's task
157    // never falls back to `Unscoped`: those spans belong to the main view or
158    // to no view, never to a database they were never registered against.
159    // `None` only when this pool's own origin isn't `Database` (an in-memory
160    // checkpoint pool) — degrades to "no open span observed" for the tick
161    // rather than panicking a long-running daemon loop on an
162    // assumed-impossible state.
163    let tx_filter = match pool.origin() {
164        khive_storage::tx_registry::TxOrigin::Database(id) => Some(if is_main {
165            khive_storage::tx_registry::TxOriginFilter::Main(id)
166        } else {
167            khive_storage::tx_registry::TxOriginFilter::Secondary(id)
168        }),
169        khive_storage::tx_registry::TxOrigin::Memory
170        | khive_storage::tx_registry::TxOrigin::Unscoped => None,
171    };
172    // ADR-091 Amendment 2 Plank B: the checkpoint pool is only ever wired for
173    // file-backed backends (`checkpoint_pool_for`), so `is_file_backed: true`
174    // is always correct here. `canonical_path()` (not `pool.config().path`)
175    // so the sidecar directory is keyed off the same minted identity every
176    // alias of this backend's configured path converges to.
177    #[cfg(unix)]
178    let mut walpin_state =
179        WalpinSidecarState::new(pool.canonical_path(), true, "daemon", config.interval);
180    #[cfg(unix)]
181    if let Some(sidecar) = walpin_state.as_mut() {
182        sidecar.register_beacon().await;
183    }
184
185    // Opened once here, at task startup; `ensure_open` is a no-op in steady
186    // state and only reopens after a connection-level failure or (for an
187    // in-memory/read-only pool) retries the open on every subsequent tick.
188    let mut checkpoint_conn = CheckpointConnection::new();
189    checkpoint_conn.ensure_open(&pool);
190    // FTS5 segment maintenance shares this task's standalone connection, but
191    // not its 500 ms cadence. Each due call performs at most one bounded
192    // merge step on one table and refuses immediately when another writer
193    // owns SQLite's write lock.
194    let mut fts_maintenance_config = crate::fts_maintenance::FtsMaintenanceConfig::from_env();
195    // Secondary backends have independent schemas (for example the code-map
196    // database) and are not required to contain the substrate FTS tables.
197    // Exactly the main backend owns this derived-index maintenance.
198    fts_maintenance_config.enabled &= is_main;
199    let mut fts_maintenance_state =
200        crate::fts_maintenance::FtsMaintenanceState::new(Instant::now());
201
202    loop {
203        // A closed sender (the daemon returning without an explicit send)
204        // makes `changed()` resolve with `Err` immediately, which `select!`
205        // treats as ready — so shutdown is observed either way, not just on
206        // an explicit send.
207        tokio::select! {
208            _ = interval.tick() => {}
209            _ = shutdown_rx.changed() => break,
210        }
211
212        #[cfg(unix)]
213        truncate_state.begin_tick();
214
215        #[cfg(unix)]
216        let mut pending_sidecar_attribution = None;
217
218        let tick = if checkpoint_conn.ensure_open(&pool).is_none() {
219            note_checkpoint_skipped();
220            CheckpointTick::Skipped
221        } else {
222            // `ensure_open` above just confirmed a connection is open. Take
223            // ownership of it so the checkpoint cycle and the FTS maintenance
224            // step below can each move it onto a blocking thread; every path
225            // either restores it to `checkpoint_conn` or lets it drop, which is
226            // the moved-ownership equivalent of the former `drop_connection()`
227            // call.
228            let conn = checkpoint_conn
229                .conn
230                .take()
231                .expect("ensure_open just confirmed a connection is open");
232            match off_worker::run_checkpoint_core_off_worker(
233                Arc::clone(&pool),
234                conn,
235                config.clone(),
236                truncate_state,
237            )
238            .await
239            {
240                Ok((conn, state, Ok(outcome))) => {
241                    truncate_state = state;
242                    #[cfg(unix)]
243                    {
244                        pending_sidecar_attribution = outcome.sidecar_attribution;
245                    }
246                    #[cfg(not(unix))]
247                    let _ = outcome.sidecar_attribution;
248
249                    // Only a due tick moves the connection onto a blocking
250                    // thread; an ordinary tick between maintenance intervals
251                    // keeps it here and records a no-op.
252                    let fts_result = if !fts_maintenance_state
253                        .is_due(&fts_maintenance_config, Instant::now())
254                    {
255                        checkpoint_conn.conn = Some(conn);
256                        Ok(None)
257                    } else {
258                        match run_fts_maintenance_off_worker(
259                            conn,
260                            fts_maintenance_config.clone(),
261                            fts_maintenance_state,
262                            Instant::now(),
263                        )
264                        .await
265                        {
266                            Ok((conn, state, fts_result)) => {
267                                fts_maintenance_state = state;
268                                checkpoint_conn.conn = Some(conn);
269                                fts_result
270                            }
271                            Err(join_err) => {
272                                // The step panicked on the blocking thread. The
273                                // connection and scheduler state moved into it
274                                // are gone; `ensure_open` reopens the connection
275                                // next tick and the schedule restarts from now.
276                                // The checkpoint pragma itself already succeeded.
277                                fts_maintenance_state =
278                                    crate::fts_maintenance::FtsMaintenanceState::new(Instant::now());
279                                Err(format!(
280                                    "bounded FTS5 segment maintenance task panicked: {join_err}"
281                                ))
282                            }
283                        }
284                    };
285
286                    match fts_result {
287                        Ok(Some(step)) => match step.outcome {
288                            crate::fts_maintenance::FtsMaintenanceOutcome::Worked => {
289                                tracing::info!(target: "khive_db::checkpoint",
290                                    table = step.table,
291                                    requested_pages = step.requested_pages,
292                                    segments_before = step.segments_before,
293                                    segments_after = step.segments_after,
294                                    "bounded FTS5 segment maintenance made progress"
295                                );
296                            }
297                            crate::fts_maintenance::FtsMaintenanceOutcome::Busy => {
298                                tracing::debug!(target: "khive_db::checkpoint",
299                                    table = step.table,
300                                    requested_pages = step.requested_pages,
301                                    segments = step.segments_before,
302                                    "bounded FTS5 segment maintenance skipped a busy writer"
303                                );
304                            }
305                            crate::fts_maintenance::FtsMaintenanceOutcome::Noop
306                            | crate::fts_maintenance::FtsMaintenanceOutcome::BelowThreshold => {
307                                tracing::debug!(target: "khive_db::checkpoint",
308                                    table = step.table,
309                                    outcome = ?step.outcome,
310                                    segments = step.segments_before,
311                                    "bounded FTS5 segment maintenance had no work"
312                                );
313                            }
314                        },
315                        Ok(None) => {}
316                        Err(error) => {
317                            // The checkpoint pragma already succeeded. An FTS
318                            // structure/read/merge error is an independent,
319                            // best-effort maintenance failure and must not make
320                            // the task discard an otherwise healthy connection.
321                            tracing::warn!(target: "khive_db::checkpoint",
322                                error = %error,
323                                "bounded FTS5 segment maintenance failed"
324                            );
325                        }
326                    }
327                    match outcome.wal_pages {
328                        Some(wal_pages) => CheckpointTick::Observed(wal_pages),
329                        None => {
330                            note_checkpoint_skipped();
331                            CheckpointTick::Skipped
332                        }
333                    }
334                }
335                Ok((_conn, state, Err(e))) => {
336                    truncate_state = state;
337                    tracing::warn!(target: "khive_db::checkpoint",
338                        error = %e,
339                        "dedicated checkpoint connection failed a pragma; \
340                         dropping it for a fresh reopen next tick"
341                    );
342                    note_checkpoint_skipped();
343                    CheckpointTick::Skipped
344                }
345                Err(panicked) => {
346                    // The cycle panicked: its connection is gone (reopened next tick) and the
347                    // escalation state is the one it was handed, counted as a TRUNCATE attempt.
348                    truncate_state = panicked.truncate_state;
349                    tracing::warn!(target: "khive_db::checkpoint",
350                        error = %panicked.join_error,
351                        "WAL checkpoint cycle panicked on its blocking thread; \
352                         dropping the connection for a fresh reopen next tick"
353                    );
354                    note_checkpoint_skipped();
355                    CheckpointTick::Skipped
356                }
357            }
358        };
359
360        // A no-progress or unmeasured TRUNCATE returns a bounded attribution
361        // request alongside the core outcome. Consume it before any
362        // report-derived decision or ordinary housekeeping for this tick.
363        // The await is intentional: enumeration may perform up to 512
364        // filesystem reads/classifications, so none of that work is allowed
365        // to run on this Tokio worker, while one-pass-per-tick ordering still
366        // requires the result (or an honest worker/enumeration failure) before
367        // the fallback housekeeping arm is considered.
368        #[cfg(unix)]
369        if let Err(error) =
370            complete_walpin_attribution(pending_sidecar_attribution, &mut truncate_state).await
371        {
372            tracing::warn!(target: "khive_db::checkpoint",
373                error = %error,
374                failure_kind = error.kind(),
375                "ADR-091 Amendment 2 Plank B: no-progress sidecar attribution failed"
376            );
377        }
378
379        // ADR-091 Plank 1: age-based sweep over the registry's oldest entry
380        // MUST run on every tick, including a Skipped one — deliberately
381        // BEFORE the Skipped early-continue below. Since the dedicated
382        // checkpoint connection amendment, a `Skipped` tick means that
383        // connection was unavailable or its PASSIVE result was busy, not that
384        // some registered span held the pool's writer mutex (a checkpoint
385        // tick no longer touches it at all) — but the sweep must not go blind for the
386        // duration of that outage, and the two failure surfaces are
387        // independent: a registry span can go stale
388        // (KHIVE_TX_WARN_SECS / KHIVE_TX_MAX_AGE_SECS) while wal_pages sits
389        // well under warn_pages, or while the checkpoint connection itself is
390        // down. Edge-triggered per rung, same debounce idiom as the severity
391        // ladder below, so a sustained stale span logs once per rung rather
392        // than once per tick.
393        reader_checkout_state.sweep_at(&pool, Instant::now());
394        let oldest_tx = tx_filter
395            .as_ref()
396            .and_then(khive_storage::tx_registry::oldest_for);
397        for emission in tx_age_state.observe(
398            oldest_tx.as_ref().map(|s| (s.id, s.age, s.label.clone())),
399            config.tx_warn_secs,
400            config.tx_max_age_secs,
401        ) {
402            log_tx_age_emission(&emission);
403        }
404        // ADR-091 Amendment 2 Plank B: refresh (or clear) this daemon
405        // process's own walpin heartbeat on the same cadence, so its own
406        // pin — if any — is attributable the same way a session's is.
407        #[cfg(unix)]
408        if let Some(sidecar) = walpin_state.as_mut() {
409            sidecar
410                .observe(oldest_tx.clone(), config.tx_warn_secs)
411                .await;
412            let _ = run_walpin_housekeeping_if_due(
413                sidecar,
414                &mut truncate_state,
415                legacy_walpin_fallback_interval,
416            )
417            .await;
418        }
419
420        // Skipped ticks leave crossing state unchanged — a busy tick must not
421        // re-arm the rate limit while WAL pressure is still elevated.
422        let wal_pages = match tick {
423            CheckpointTick::Skipped => continue,
424            CheckpointTick::Observed(n) => n,
425        };
426
427        let above_warn = wal_pages >= config.warn_pages;
428        let above_high_water = wal_pages >= config.high_water_pages;
429        let above_truncate_high_water = wal_pages >= config.truncate_high_water_pages;
430        note_checkpoint_pressure_observation(above_warn, was_observed_above_warn);
431        was_observed_above_warn = above_warn;
432
433        // Per-tick debug for the oldest open entry always fires (cheap —
434        // reuses this tick's already-computed `oldest_tx`); the two
435        // `warn!`-level registry logs below are gated on the SAME crossing
436        // state as the WAL-threshold WARNs above, so sustained pressure
437        // logs once per crossing, not once per tick.
438        log_tx_registry_oldest_debug(wal_pages, oldest_tx.as_ref());
439
440        // ADR-091 severity ladder: INFO on the first below→above crossing,
441        // WARN once `warn_sustained_cycles` consecutive ticks stay elevated.
442        // The oldest-entry registry WARN rides the same INFO edge the old
443        // binary crossing_warn used to gate on.
444        for emission in severity_state.observe_wal_pages(wal_pages, &config) {
445            match emission.rung {
446                CheckpointSeverityRung::Info => {
447                    log_tx_registry_oldest_warn(wal_pages, oldest_tx.as_ref());
448                    tracing::info!(target: "khive_db::checkpoint",
449                        wal_pages = emission.wal_pages,
450                        warn_threshold = emission.threshold_pages,
451                        "WAL page count crossed warn threshold"
452                    );
453                }
454                CheckpointSeverityRung::Warn => {
455                    tracing::warn!(target: "khive_db::checkpoint",
456                        wal_pages = emission.wal_pages,
457                        warn_threshold = emission.threshold_pages,
458                        consecutive_cycles = emission.consecutive_cycles,
459                        "WAL page count failed to drain below warn threshold"
460                    );
461                }
462                CheckpointSeverityRung::Alarm => {
463                    // Never produced by `observe_wal_pages`; see its doc.
464                }
465            }
466        }
467
468        let high_water_crossed = crossing_warn(above_high_water, &mut was_above_high_water);
469        if high_water_crossed {
470            log_tx_registry_snapshot_warn(wal_pages);
471            log_wal_high_water_warn(
472                wal_pages,
473                config.high_water_pages,
474                oldest_tx.as_ref(),
475                config.tx_warn_secs,
476            );
477        }
478
479        // ADR-094/#1838, #1857: one elevation row and one recovery summary
480        // per genuinely continuous episode. Sustained elevated ticks update
481        // only the bounded in-memory aggregate and process diagnostics
482        // above; a dropped recovery handoff must not fold the next,
483        // separate, pressure incident into this episode's aggregate.
484        observe_checkpoint_pressure_tick(
485            above_warn,
486            wal_pages,
487            above_high_water,
488            above_truncate_high_water,
489            &config,
490            &mut event_elevation_open,
491            &mut pressure_episode,
492            &mut pending_recovery,
493            |payload| {
494                lifecycle_emitter
495                    .try_emit(khive_types::EventKind::CheckpointOutcomeRecorded, payload)
496            },
497        );
498    }
499
500    lifecycle_emitter.shutdown().await;
501
502    #[cfg(unix)]
503    if let Some(sidecar) = walpin_state.as_mut() {
504        sidecar.shutdown().await;
505    }
506}
507
508/// Whether a `CheckpointOutcomeRecorded` transition should be enqueued for
509/// this tick. Repeated observations in either state aggregate in memory;
510/// only elevation and recovery edges reach the primary store.
511pub(super) fn checkpoint_outcome_should_emit(above_warn: bool, was_elevated: bool) -> bool {
512    above_warn != was_elevated
513}
514
515/// Advance the pressure-episode/lifecycle-emission state machine for one
516/// observed tick. `try_emit` mirrors [`CheckpointLifecycleEmitter::try_emit`]
517/// — `true` means the row was handed off, `false` means the queue was full
518/// or closed.
519///
520/// #1857: on a dropped recovery handoff (`try_emit` returns `false` while
521/// `above_warn` is `false`), the closed episode's summary is stashed in
522/// `pending_recovery` for retry on later ticks — flushed here before this
523/// tick's own transition is evaluated — instead of leaving
524/// `event_elevation_open` and `pressure_episode` open for the next elevated
525/// tick to silently extend, which would report two separate pressure
526/// incidents as one merged episode.
527#[allow(clippy::too_many_arguments)]
528pub(super) fn observe_checkpoint_pressure_tick(
529    above_warn: bool,
530    wal_pages: u64,
531    above_high_water: bool,
532    above_truncate_high_water: bool,
533    config: &CheckpointConfig,
534    event_elevation_open: &mut bool,
535    pressure_episode: &mut Option<CheckpointPressureEpisode>,
536    pending_recovery: &mut Option<khive_storage::CheckpointOutcomeRecordedPayload>,
537    mut try_emit: impl FnMut(khive_storage::CheckpointOutcomeRecordedPayload) -> bool,
538) {
539    // An undelivered recovery summary is a BARRIER, not merely a retry:
540    // lifecycle consumers assert on the ordered event history (ADR-094), so
541    // a later episode's opening must never be appended ahead of an earlier
542    // episode's recovery. If the retry fails, the in-memory aggregate still
543    // advances below, but no other emission is attempted this tick — a
544    // deferred opening or recovery re-derives from state on a later tick,
545    // after the pending summary has been delivered in order.
546    let pending_blocks_emission = if let Some(payload) = pending_recovery.clone() {
547        if try_emit(payload) {
548            *pending_recovery = None;
549            false
550        } else {
551            true
552        }
553    } else {
554        false
555    };
556
557    if above_warn {
558        match pressure_episode.as_mut() {
559            Some(episode) => episode.observe(wal_pages),
560            None => *pressure_episode = Some(CheckpointPressureEpisode::start(wal_pages)),
561        }
562    } else if !*event_elevation_open {
563        // No elevation row reached the bounded handoff, so from any
564        // consumer's view this episode never opened; discarding it keeps
565        // the delivered history self-consistent. When the discard happens
566        // because the barrier suppressed the opening attempt entirely, the
567        // loss would otherwise be invisible even to the drop counters that
568        // record failed attempts, so it is counted and logged here.
569        if pending_blocks_emission && pressure_episode.is_some() {
570            CHECKPOINT_LIFECYCLE_ENQUEUE_DROPS.fetch_add(1, Ordering::Relaxed);
571            tracing::warn!(target: "khive_db::checkpoint",
572                wal_pages,
573                "checkpoint pressure episode elapsed unreported behind an undelivered recovery summary"
574            );
575        }
576        *pressure_episode = None;
577    }
578
579    if pending_blocks_emission || !checkpoint_outcome_should_emit(above_warn, *event_elevation_open)
580    {
581        return;
582    }
583    let Some(episode) = *pressure_episode else {
584        tracing::warn!(target: "khive_db::checkpoint",
585            above_warn,
586            event_elevation_open = *event_elevation_open,
587            "checkpoint pressure transition has no episode aggregate"
588        );
589        return;
590    };
591    let payload = khive_storage::CheckpointOutcomeRecordedPayload {
592        wal_pages,
593        warn_pages: config.warn_pages,
594        high_water_pages: config.high_water_pages,
595        truncate_high_water_pages: config.truncate_high_water_pages,
596        above_warn,
597        above_high_water,
598        above_truncate_high_water,
599        episode_elevated_ticks: Some(episode.elevated_ticks),
600        episode_peak_wal_pages: Some(episode.peak_wal_pages),
601    };
602    if try_emit(payload.clone()) {
603        *event_elevation_open = above_warn;
604        if !above_warn {
605            *pressure_episode = None;
606        }
607    } else if !above_warn {
608        // The recovery handoff was dropped. Close this episode locally
609        // anyway — `event_elevation_open` MUST NOT stay true, or the next
610        // elevated tick would extend this (already finished) episode's
611        // aggregate instead of starting a fresh one for what is genuinely a
612        // new pressure incident. The dropped summary itself isn't thrown
613        // away: it is stashed in `pending_recovery` and delivered on a
614        // later tick, ahead of (and as a barrier to) every subsequent
615        // emission, so lifecycle ordering survives the retry. The slot is
616        // structurally empty here: a tick that entered with an undelivered
617        // summary returned at the barrier above and never reached this arm.
618        debug_assert!(
619            pending_recovery.is_none(),
620            "recovery emission attempted while an earlier summary was still pending"
621        );
622        *event_elevation_open = false;
623        *pressure_episode = None;
624        *pending_recovery = Some(payload);
625    }
626}