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}