review: a session log that cannot be read now is waited out

The worker marked a turn failed on any error reading its log. A missing log,
a damaged line or a turn not found still fail it; any other I/O error is now
waited out with the outage's doubling wait, logged once when it starts and
once when the log reads again (pointer session-log-damaged, whose runbook
entry now covers it), shown by bxctl memory sync, and the turn stays waiting.

read_records tells an unreadable file from a damaged line (read_log is
unchanged). The message helpers move from worker.rs to honcho/messages.rs,
re-exported, to keep worker.rs under 500 lines.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 14:15:04 -07:00
co-authored by Claude Opus 5.5
parent bd4cf9fb7f
commit 5e57cd69a3
8 changed files with 306 additions and 69 deletions
+28 -4
View File
@@ -57,14 +57,38 @@ pub fn log_path(sessions: &Path, session: &SessionId) -> PathBuf {
sessions.join(session.as_str()).join("0.jsonl")
}
/// Why a session log did not read. Each names the file; `Display` adds the runbook pointer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LogError {
/// The file exists but could not be read (a permission, an I/O error): it may read later.
Io(String),
/// A complete line that does not parse, and its number: it will not read later.
Damaged(String),
}
impl std::fmt::Display for LogError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogError::Io(why) | LogError::Damaged(why) => write!(f, "{why}; {LOG_RUNBOOK}"),
}
}
}
impl std::error::Error for LogError {}
/// A session log's complete lines as records. A missing file has none. A last line without its
/// newline is one `loopd` is still writing, and is left out. A complete line that does not parse
/// is an error naming the file and the line, with the runbook pointer.
pub fn read_log(path: &Path) -> Result<Vec<LogRecord>, String> {
read_records(path).map_err(|e| e.to_string())
}
/// `read_log`, telling a file that could not be read from a damaged line.
pub fn read_records(path: &Path) -> Result<Vec<LogRecord>, LogError> {
let text = match std::fs::read(path) {
Ok(bytes) => bytes,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(format!("{}: {e}; {LOG_RUNBOOK}", path.display())),
Err(e) => return Err(LogError::Io(format!("{}: {e}", path.display()))),
};
let whole = match text.iter().rposition(|b| *b == b'\n') {
Some(last) => last + 1,
@@ -80,11 +104,11 @@ pub fn read_log(path: &Path) -> Result<Vec<LogRecord>, String> {
match serde_json::from_slice::<LogRecord>(line) {
Ok(record) => records.push(record),
Err(e) => {
return Err(format!(
"{}:{}: {e}; {LOG_RUNBOOK}",
return Err(LogError::Damaged(format!(
"{}:{}: {e}",
path.display(),
index + 1
));
)));
}
}
}