Populate the session tables from a provenance graph. Called during `save_provenance_graph` for provenance produced by any** agent — Sherpa, Claude Code, OpenCode, the generic `atomic-agent`, etc. Sherpa graphs (`profile == "sherpa-trace/1.0.0"`) carry richer structured `detail` JSON (intent/todo/phase/verification), so they populate the intent and phase tables more fully; graphs from other agents
(
&mut self,
provenance_id: u64,
graph: &crate::change::ProvenanceGraph,
)
| 75 | /// |
| 76 | /// Best-effort: parse errors on individual nodes are logged and skipped. |
| 77 | pub fn populate_session_tables( |
| 78 | &mut self, |
| 79 | provenance_id: u64, |
| 80 | graph: &crate::change::ProvenanceGraph, |
| 81 | ) -> PristineResult<()> { |
| 82 | use crate::change::provenance_graph::ProvenanceNodeKind; |
| 83 | use crate::change::session::*; |
| 84 | |
| 85 | // No profile gate: provenance from any agent is indexed into the |
| 86 | // session tables. The per-node extraction below is profile-agnostic |
| 87 | // and falls back to node summary / kind labels when the richer |
| 88 | // Sherpa-specific `detail` keys are absent. |
| 89 | |
| 90 | // Open all four tables. |
| 91 | let mut events_table = self.txn.open_table(SESSION_EVENTS)?; |
| 92 | let mut todos_table = self.txn.open_table(SESSION_TODOS)?; |
| 93 | let mut phases_table = self.txn.open_table(SESSION_PHASES)?; |
| 94 | let mut intents_table = self.txn.open_table(SESSION_INTENTS)?; |
| 95 | |
| 96 | for (seq, node) in graph.nodes.iter().enumerate() { |
| 97 | let seq = seq as u64; |
| 98 | let detail: Option<serde_json::Value> = if let Some(s) = node.detail.as_deref() { |
| 99 | match serde_json::from_str(s) { |
| 100 | Ok(v) => Some(v), |
| 101 | Err(e) => { |
| 102 | log::warn!( |
| 103 | "Warning: failed to parse provenance node detail JSON (node id={}, kind={}): {}", |
| 104 | node.id, |
| 105 | node.kind.label(), |
| 106 | e |
| 107 | ); |
| 108 | None |
| 109 | } |
| 110 | } |
| 111 | } else { |
| 112 | None |
| 113 | }; |
| 114 | |
| 115 | // Write every node as a SessionEvent regardless of kind. |
| 116 | // |
| 117 | // `event_kind` and `record_type` are populated from the original |
| 118 | // Sherpa/agent-trace `record_type` field stored in the detail JSON |
| 119 | // (e.g. "intent", "todo_status", "phase_transition"). We fall back |
| 120 | // to `node.kind.label()` only when no such field is present so that |
| 121 | // non-Sherpa nodes still get a meaningful discriminant. |
| 122 | let sherpa_record_type = detail |
| 123 | .as_ref() |
| 124 | .and_then(|d| d["record_type"].as_str()) |
| 125 | .map(|s| s.to_string()); |
| 126 | let event_kind = sherpa_record_type |
| 127 | .clone() |
| 128 | .unwrap_or_else(|| node.kind.label().to_string()); |
| 129 | let event = SessionEvent { |
| 130 | seq, |
| 131 | timestamp: format_timestamp_ms(node.timestamp), |
| 132 | event_kind, |
| 133 | place: None, |
| 134 | transition: None, |