The decision log: every judgment and what became of it, appended as one JSON object per line, so thresholds can be tuned from what actually happened rather than from guesses.
One deciding event and the daemon's answer to it.
bash or stop.
21 pub kind: String,
What was judged: the command, or the end of the final message.
23 pub subject: String,
Present for a tool call, so its outcome can be joined to it.
Jev's answers to the judgment's questions, with every option's probability; absent when Jev was not asked or did not answer.
30 pub answers: Option<Value>,
The whole decision, Jev included.
32 pub ms: u64,
Jev's own round trip, when it answered.
What became of a judged tool call.
How far back recent looks; a line is a few hundred bytes.
64const TAIL_BYTES: u64 = 512 * 1024;
Appends one record. A log that cannot be written is reported, and the decision it describes stands: the log is evidence, not a guard.
78 pub fn append(&self, record: &Record) { 79 let mut line = match serde_json::to_vec(record) { 80 Ok(line) => line, 81 Err(e) => return eprintln!("jevhooks: decision not logged: {e}"), 82 }; 83 line.push(b'\n'); 84 let mut file = self.file.lock().unwrap_or_else(std::sync::PoisonError::into_inner); 85 if let Err(e) = file.write_all(&line) { 86 eprintln!("jevhooks: decision not logged to {}: {e}", self.path.display()); 87 } 88 }
The last count records, oldest first.
91 pub fn recent(&self, count: usize) -> std::io::Result<Vec<Value>> { 92 let mut file = File::open(&self.path)?; 93 let len = file.metadata()?.len(); 94 let from = len.saturating_sub(TAIL_BYTES); 95 file.seek(SeekFrom::Start(from))?; 96 let mut text = String::new(); 97 file.take(TAIL_BYTES).read_to_string(&mut text).or_else(|e| { 98 // A tail that starts inside a multi-byte character is not UTF-8; 99 // nothing in it is worth an error. 100 if e.kind() == std::io::ErrorKind::InvalidData { Ok(0) } else { Err(e) } 101 })?; 102 let mut lines: Vec<&str> = text.lines().collect(); 103 if from > 0 && !lines.is_empty() { 104 lines.remove(0); // the first line of a tail is usually cut 105 } 106 let start = lines.len().saturating_sub(count); 107 Ok(lines[start..].iter().filter_map(|line| serde_json::from_str(line).ok()).collect()) 108 } 109}