diff --git a/src-rust/crates/core/src/coven_daemon.rs b/src-rust/crates/core/src/coven_daemon.rs index 6eef4f6..494c881 100644 --- a/src-rust/crates/core/src/coven_daemon.rs +++ b/src-rust/crates/core/src/coven_daemon.rs @@ -106,6 +106,8 @@ pub struct RegisterExternalSession { pub harness: String, pub title: String, pub transcript_path: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub labels: Vec, } // --------------------------------------------------------------------------- @@ -1140,26 +1142,36 @@ mod tests { harness: "coven-code".to_string(), title: "My session".to_string(), transcript_path: Some("/home/user/.coven-code/sessions/sess-1.jsonl".to_string()), + labels: vec!["source:psyche-build".to_string()], }; - let json = serde_json::to_string(&req).unwrap(); - assert!( - json.contains("\"projectRoot\""), - "expected camelCase projectRoot, got: {json}" - ); - assert!( - json.contains("\"transcriptPath\""), - "expected camelCase transcriptPath, got: {json}" - ); - assert!( - !json.contains("\"project_root\""), - "snake_case leaked: {json}" - ); - assert!( - !json.contains("\"transcript_path\""), - "snake_case leaked: {json}" + let json = serde_json::to_value(&req).unwrap(); + assert_eq!( + json, + serde_json::json!({ + "id": "sess-1", + "projectRoot": "/home/user/repo", + "harness": "coven-code", + "title": "My session", + "transcriptPath": "/home/user/.coven-code/sessions/sess-1.jsonl", + "labels": ["source:psyche-build"], + }) ); } + #[test] + fn register_external_session_omits_empty_labels() { + let req = RegisterExternalSession { + id: "sess-1".to_string(), + project_root: "/home/user/repo".to_string(), + harness: "coven-code".to_string(), + title: "My session".to_string(), + transcript_path: None, + labels: Vec::new(), + }; + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json.get("labels"), None); + } + #[test] fn complete_session_body_serializes_exit_code() { // Verify the exitCode key (camelCase) is what the daemon expects. diff --git a/src-rust/crates/core/src/coven_ledger.rs b/src-rust/crates/core/src/coven_ledger.rs index 5958419..8ff033b 100644 --- a/src-rust/crates/core/src/coven_ledger.rs +++ b/src-rust/crates/core/src/coven_ledger.rs @@ -3,6 +3,16 @@ //! Every failure is swallowed to a debug log — a dead or absent daemon must //! never affect the TUI. +const COVEN_SESSION_SOURCE: &str = "COVEN_SESSION_SOURCE"; +const PSYCHE_SESSION_SOURCE: &str = "psyche-build"; + +fn registration_labels(source: Option<&str>) -> Vec { + match source { + Some(PSYCHE_SESSION_SOURCE) => vec![format!("source:{PSYCHE_SESSION_SOURCE}")], + _ => Vec::new(), + } +} + #[cfg(unix)] pub fn notify_session_start(id: &str, project_root: &std::path::Path, title: &str) { let Some(client) = crate::coven_daemon::DaemonClient::new() else { @@ -17,6 +27,7 @@ pub fn notify_session_start(id: &str, project_root: &std::path::Path, title: &st harness: "coven-code".to_string(), title: title.to_string(), transcript_path, + labels: registration_labels(std::env::var(COVEN_SESSION_SOURCE).ok().as_deref()), }; if let Err(e) = client.register_external_session(&req) { tracing::debug!("coven ledger register failed (ignored): {e}"); @@ -38,3 +49,29 @@ pub fn notify_session_start(_id: &str, _project_root: &std::path::Path, _title: #[cfg(not(unix))] pub fn notify_session_complete(_id: &str, _exit_code: Option) {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_only_the_exact_psyche_source() { + assert_eq!( + registration_labels(Some(PSYCHE_SESSION_SOURCE)), + vec![format!("source:{PSYCHE_SESSION_SOURCE}")] + ); + + for source in [ + None, + Some(""), + Some("Psyche-Build"), + Some("foreign"), + Some("psyche-build-extra"), + ] { + assert!( + registration_labels(source).is_empty(), + "unexpected label for {source:?}" + ); + } + } +}