#[derive(Debug, Default)]
pub(in crate::daemon) struct DaemonEvent {
pub(in crate::daemon) kind: &'static str,
pub(in crate::daemon) event_id: Option<String>,
pub(in crate::daemon) trace_id: Option<String>,
pub(in crate::daemon) query: Option<String>,
pub(in crate::daemon) output_summary: Option<String>,
pub(in crate::daemon) outcome: Option<String>,
pub(in crate::daemon) used: Option<Vec<String>>,
pub(in crate::daemon) feedback: Option<String>,
pub(in crate::daemon) nomination: Option<String>,
pub(in crate::daemon) priority: i64,
}
pub(in crate::daemon) fn parse_log_event(line: &str) -> Option<DaemonEvent> {
if let Ok(value) = serde_json::from_str::<serde_json::Value>(line) {
let event_type = value.get("event_type").and_then(ValueExt::string)?;
let (kind, default_outcome) = match event_type {
"session_start" => ("start", None),
"tool_success" => ("ok", Some("ok")),
"tool_error" => ("fail", Some("fail")),
"session_end" => ("end", None),
"user_feedback" => ("feedback", None),
_ => return None,
};
return Some(DaemonEvent {
kind,
event_id: value.get("event_id").and_then(ValueExt::owned_string),
trace_id: value.get("trace_id").and_then(ValueExt::owned_string),
query: value.get("query").and_then(ValueExt::owned_string),
output_summary: value.get("output_summary").and_then(ValueExt::owned_string),
outcome: value
.get("outcome")
.and_then(ValueExt::owned_string)
.or_else(|| default_outcome.map(str::to_string)),
used: value
.get("used")
.and_then(|used| used.as_array())
.map(|used| {
used.iter()
.filter_map(ValueExt::owned_string)
.collect::<Vec<_>>()
}),
feedback: value.get("feedback").and_then(ValueExt::owned_string),
nomination: value.get("nomination").and_then(ValueExt::owned_string),
priority: value.get("priority").and_then(|v| v.as_i64()).unwrap_or(0),
});
}
let kind = classify_text_line(line)?;
Some(DaemonEvent {
kind,
query: (kind == "start").then(|| line.to_string()),
outcome: match kind {
"ok" => Some("ok".to_string()),
"fail" => Some("fail".to_string()),
_ => None,
},
..DaemonEvent::default()
})
}
fn classify_text_line(line: &str) -> Option<&'static str> {
let start_patterns = [
"Starting ",
"Running ",
"Executing ",
"BEGIN ",
"Task started",
];
let success_patterns = ["Build successful", "Tests passed", "✓ ", " passed"];
let fail_patterns = ["SyntaxError", "Error:", "FAILED", "test result: FAILED"];
let end_patterns = [
"Session ended",
"Session End",
"Conversation closed",
"IDE exited",
];
for p in &end_patterns {
if line.contains(p) {
return Some("end");
}
}
for p in &start_patterns {
if line.contains(p) {
return Some("start");
}
}
for p in &success_patterns {
if line.contains(p) {
return Some("ok");
}
}
for p in &fail_patterns {
if line.contains(p) {
return Some("fail");
}
}
None
}
trait ValueExt {
fn string(&self) -> Option<&str>;
fn owned_string(&self) -> Option<String>;
}
impl ValueExt for serde_json::Value {
fn string(&self) -> Option<&str> {
self.as_str().filter(|value| !value.is_empty())
}
fn owned_string(&self) -> Option<String> {
self.string().map(str::to_string)
}
}
pub(in crate::daemon) fn event_id_for_line(
watch_path: &str,
inode: &str,
offset: i64,
line: &str,
) -> String {
use sha2::{Digest, Sha256};
let mut hash = Sha256::new();
hash.update(watch_path.as_bytes());
hash.update(b":");
hash.update(inode.as_bytes());
hash.update(b":");
hash.update(offset.to_string().as_bytes());
hash.update(b":");
hash.update(line.as_bytes());
crate::utils::hex(&hash.finalize())
}
pub(in crate::daemon) fn record_daemon_error(
state_db: &rusqlite::Connection,
watch_path: &str,
operation: &str,
message: &str,
) {
let _ = state_db.execute(
"INSERT INTO daemon_errors(watch_path, operation, message, ts)
VALUES (?,?,?,?)",
rusqlite::params![watch_path, operation, message, crate::utils::utc_now_iso()],
);
}
pub(in crate::daemon) fn call_cli_record(
db_path: &str,
trace_id: &str,
event: &DaemonEvent,
) -> anyhow::Result<()> {
let self_exe = std::env::current_exe()?;
let run = || {
let mut command = std::process::Command::new(&self_exe);
command.args(["--db", db_path, "record", trace_id]);
if let Some(query) = &event.query {
command.args(["--query", query]);
}
if let Some(outcome) = &event.outcome {
command.args(["--outcome", outcome]);
}
if let Some(used) = &event.used {
command.args(["--used", &used.join(",")]);
}
if let Some(summary) = &event.output_summary {
command.args(["--output-summary", summary]);
}
if let Some(feedback) = &event.feedback {
command.args(["--feedback", feedback]);
}
if let Some(nomination) = &event.nomination {
command.args(["--nomination", nomination]);
}
command.args(["--priority", &event.priority.to_string()]);
command.args(["--source", "daemon"]);
command.status()
};
let first = run()?;
if first.success() {
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(200));
let second = run()?;
if second.success() {
Ok(())
} else {
anyhow::bail!("record exited {:?} after retry", second.code())
}
}
pub(in crate::daemon) fn call_cli_recall(db_path: &str, query: &str) -> anyhow::Result<String> {
let self_exe = std::env::current_exe()?;
let output = std::process::Command::new(&self_exe)
.args([
"--db", db_path, "recall", query, "--format", "json", "--source", "daemon",
])
.output()?;
if !output.status.success() {
anyhow::bail!(
"recall exited non-zero: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let parsed: serde_json::Value = serde_json::from_slice(&output.stdout)
.map_err(|e| anyhow::anyhow!("recall json parse error: {e}"))?;
parsed
.get("trace_id")
.and_then(|v| v.as_str())
.map(str::to_string)
.ok_or_else(|| anyhow::anyhow!("no trace_id in recall output"))
}
pub(in crate::daemon) fn call_cli_backup(db_path: &str) -> anyhow::Result<()> {
let self_exe = std::env::current_exe()?;
let status = std::process::Command::new(&self_exe)
.args(["--db", db_path, "backup", "run"])
.status()?;
if status.success() {
Ok(())
} else {
anyhow::bail!("innate backup run exited {:?}", status.code())
}
}
pub(in crate::daemon) fn call_cli_evolve(db_path: &str, trigger: &str) -> anyhow::Result<()> {
let self_exe = std::env::current_exe()?;
let run = || {
std::process::Command::new(&self_exe)
.args(["--db", db_path, "evolve", "--trigger", trigger])
.status()
};
let first = run()?;
if first.success() {
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(200));
let second = run()?;
if second.success() {
Ok(())
} else {
anyhow::bail!("evolve exited {:?} after retry", second.code())
}
}