Skip to main content

khive_runtime/
phase_events.rs

1//! Shared helpers for ADR-103 phase-span event emission.
2//!
3//! `PhaseStarted` / `PhaseCompleted` / `PhaseCancelled` are ADR-094's
4//! additive `EventKind` mechanism, extended by ADR-103 Decision (c) for
5//! background work that is not itself a verb dispatch. `khive-pack-memory`'s
6//! ANN background-rebuild task (`ann.rs`) originated the emission and
7//! terminal-selection pattern this module lifts into a shared home so
8//! ADR-103 Amendment 1 Part 2's two daemon-startup embedder-warmup hooks
9//! (`KgPack::warm`, `KnowledgePack::warm`) can reuse it without duplicating
10//! either the event-append plumbing or the shutdown-cancellation
11//! classification.
12
13use crate::error::RuntimeError;
14use crate::runtime::{KhiveRuntime, NamespaceToken};
15use khive_storage::{Event, StorageError, SubstrateKind};
16
17/// True when `err` is the direct result of a `spawn_blocking` cancellation,
18/// e.g. a short-lived process (or daemon shutdown) tearing the runtime down
19/// mid-operation, rather than a genuine backend/driver failure.
20///
21/// Matches the concrete `tokio::task::JoinError` boxed inside
22/// `StorageError::Driver` (the shape `with_reader`/`with_writer` produce
23/// when their `spawn_blocking(...).await` is cut short) via a typed
24/// downcast, not a message substring, so a real driver/SQL error is never
25/// misclassified as benign.
26///
27/// Every ADR-103 phase-span emitter that must pick between `PhaseCompleted`
28/// and `PhaseCancelled` on a shutdown-adjacent error path uses this same
29/// check, so a benign shutdown is classified identically everywhere.
30pub fn is_benign_shutdown_cancellation(err: &RuntimeError) -> bool {
31    let RuntimeError::Storage(StorageError::Driver { source, .. }) = err else {
32        return false;
33    };
34    source
35        .downcast_ref::<tokio::task::JoinError>()
36        .is_some_and(tokio::task::JoinError::is_cancelled)
37}
38
39/// Append one ADR-103 phase-span event (`PhaseStarted` / `PhaseCompleted` /
40/// `PhaseCancelled`), logging and swallowing store/serialize failures.
41///
42/// Best-effort exactly like every other ADR-094/ADR-103 lifecycle-event
43/// emitter in this codebase: telemetry must never interrupt or slow the
44/// background phase it observes. `label` identifies the phase's owner in
45/// the audit trail (e.g. `"kg.embedder_warm"`): it is a fixed label, not a
46/// dispatched verb name, since no dispatch is happening.
47pub async fn emit_phase_event<P: serde::Serialize>(
48    rt: &KhiveRuntime,
49    token: &NamespaceToken,
50    label: &str,
51    kind: khive_types::EventKind,
52    payload: P,
53) {
54    // Best-effort exactly like ADR-094's other lifecycle-event emitters: a
55    // backend that cannot resolve an `EventStore` for this token's namespace
56    // is treated as an unconfigured audit sink, not an error to propagate.
57    let Ok(store) = rt.events(token) else {
58        return;
59    };
60    let payload_value = match serde_json::to_value(&payload) {
61        Ok(v) => v,
62        Err(e) => {
63            tracing::warn!(
64                error = %e,
65                event_kind = %kind.name(),
66                label,
67                "failed to serialize ADR-103 phase-span event payload"
68            );
69            return;
70        }
71    };
72    let actor = format!("{}:{}", token.actor().kind, token.actor().id);
73    let event = Event::new(
74        token.namespace().as_str(),
75        label,
76        kind,
77        SubstrateKind::Event,
78        actor,
79    )
80    .with_payload(payload_value);
81    if let Err(err) = store.append_event(event).await {
82        tracing::warn!(
83            error = %err,
84            event_kind = %kind.name(),
85            label,
86            "ADR-103 phase-span event append failed"
87        );
88    }
89}