From 2122e23d8111acb1e10779eb8da1cdf3d345f0bd Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 1 Aug 2026 21:34:29 +0200 Subject: [PATCH] hive-sh4re/hive-bash-mcp/hive-agent: retype TaskFile timestamps to DateTime, drop now_unix from these crates --- Cargo.lock | 2 ++ hive-agent/Cargo.toml | 1 + hive-agent/src/vacuum.rs | 19 ++++++++++++------- hive-bash-mcp/Cargo.toml | 1 + hive-bash-mcp/src/mcp.rs | 11 ++++++----- hive-bash-mcp/src/runner.rs | 12 +++++------- hive-sh4re/src/lib.rs | 6 +++--- 7 files changed, 30 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf7ac800..0b69f7f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1562,6 +1562,7 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "chrono", "clap", "futures-util", "hive-agent-sock", @@ -1622,6 +1623,7 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "chrono", "clap", "hive-agent-sock", "hive-sh4re", diff --git a/hive-agent/Cargo.toml b/hive-agent/Cargo.toml index 3dd41c25..a4029b5c 100644 --- a/hive-agent/Cargo.toml +++ b/hive-agent/Cargo.toml @@ -10,6 +10,7 @@ workspace = true [dependencies] anyhow.workspace = true axum.workspace = true +chrono.workspace = true reqwest.workspace = true hyper.workspace = true hyper-util.workspace = true diff --git a/hive-agent/src/vacuum.rs b/hive-agent/src/vacuum.rs index 1c77d86d..fa37184f 100644 --- a/hive-agent/src/vacuum.rs +++ b/hive-agent/src/vacuum.rs @@ -17,7 +17,6 @@ use std::path::Path; use std::time::Duration; -use hive_sh4re::wire_time::now_unix; use rusqlite::{Connection, Result, params}; /// How often the sweep runs. @@ -56,7 +55,8 @@ fn sweep_once() { let tasks_dir = harness.join("bash-tasks"); if tasks_dir.is_dir() { - let removed = vacuum_bash_tasks(&tasks_dir, now_unix() - BASH_KEEP_SECS); + let removed = + vacuum_bash_tasks(&tasks_dir, chrono::Utc::now().timestamp() - BASH_KEEP_SECS); if removed > 0 { tracing::info!(removed, "bash-tasks vacuum"); } @@ -92,14 +92,14 @@ fn sweep_once() { /// `Reminders` handle). fn vacuum_reminders(path: &Path) -> anyhow::Result { let store = crate::reminders::Reminders::open(path)?; - store.prune_delivered_older_than(now_unix() - REMINDER_KEEP_SECS) + store.prune_delivered_older_than(chrono::Utc::now().timestamp() - REMINDER_KEEP_SECS) } /// Reap acked todo rows older than [`TODO_ACKED_KEEP_SECS`] via the typed /// store API — same own-short-lived-connection shape as `vacuum_reminders`. fn vacuum_todos(path: &Path) -> anyhow::Result { let store = crate::todos::Todos::open(path)?; - store.reap_acked(now_unix() - TODO_ACKED_KEEP_SECS) + store.reap_acked(chrono::Utc::now().timestamp() - TODO_ACKED_KEEP_SECS) } /// Delete eligible bash-task trios in `dir`. Returns the count of `.json` @@ -140,10 +140,15 @@ fn should_delete(json_path: &Path, cutoff: i64) -> bool { if !TERMINAL_STATUSES.contains(&status) { return false; } + // `completed_at` serializes as an RFC3339 string (`TaskFile.completed_at` + // is `Option>`), not a bare epoch-seconds integer — parse + // it the same way, falling back to "never expired" on anything + // unparseable so a corrupt/legacy field never causes a premature delete. let completed_at = v .get("completed_at") - .and_then(serde_json::Value::as_i64) - .unwrap_or(i64::MAX); + .and_then(serde_json::Value::as_str) + .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) + .map_or(i64::MAX, |dt| dt.timestamp()); completed_at < cutoff } @@ -164,7 +169,7 @@ fn delete_trio(dir: &Path, stem: &str) { /// agent's `events.sqlite`. Returns the number of rows deleted. fn vacuum_events(path: &Path) -> Result { let conn = Connection::open(path)?; - let cutoff = now_unix() - STREAM_KEEP_SECS; + let cutoff = chrono::Utc::now().timestamp() - STREAM_KEEP_SECS; let removed = conn.execute( "DELETE FROM events WHERE kind = 'stream' AND ts < ?1", params![cutoff], diff --git a/hive-bash-mcp/Cargo.toml b/hive-bash-mcp/Cargo.toml index a11592d8..0c4f36eb 100644 --- a/hive-bash-mcp/Cargo.toml +++ b/hive-bash-mcp/Cargo.toml @@ -10,6 +10,7 @@ workspace = true [dependencies] anyhow.workspace = true axum.workspace = true +chrono.workspace = true clap.workspace = true hive-agent-sock.workspace = true hive-sh4re.workspace = true diff --git a/hive-bash-mcp/src/mcp.rs b/hive-bash-mcp/src/mcp.rs index 2653f39c..59545a0a 100644 --- a/hive-bash-mcp/src/mcp.rs +++ b/hive-bash-mcp/src/mcp.rs @@ -38,11 +38,12 @@ fn format_task(task: &TaskFile) -> String { let _ = write!(out, ", exit={code}"); } if let (Some(started), None) = (task.started_at, task.completed_at) { - let now = hive_sh4re::wire_time::now_unix(); - let _ = write!(out, ", running for {}s", now - started); + let elapsed = (chrono::Utc::now() - started).num_seconds(); + let _ = write!(out, ", running for {elapsed}s"); } if let (Some(completed), Some(started)) = (task.completed_at, task.started_at) { - let _ = write!(out, ", took {}s", completed - started); + let took = (completed - started).num_seconds(); + let _ = write!(out, ", took {took}s"); } let out_file = crate::paths::task_out(&task.id); @@ -295,8 +296,8 @@ mod status_hint_tests { cmd: "echo hi".to_owned(), timeout_secs: None, status, - created_at: 1, - started_at: Some(1), + created_at: chrono::DateTime::from_timestamp(1, 0).unwrap_or_default(), + started_at: Some(chrono::DateTime::from_timestamp(1, 0).unwrap_or_default()), completed_at: None, exit_code: None, stdout_tail: None, diff --git a/hive-bash-mcp/src/runner.rs b/hive-bash-mcp/src/runner.rs index 0ddb8874..438bf487 100644 --- a/hive-bash-mcp/src/runner.rs +++ b/hive-bash-mcp/src/runner.rs @@ -133,8 +133,6 @@ fn signal_group(pgid: Option, sig: i32) { // Helpers // --------------------------------------------------------------------------- -use hive_sh4re::wire_time::now_unix; - /// Generate a task ID: ``. #[must_use] pub fn new_task_id() -> String { @@ -271,7 +269,7 @@ pub fn submit_task(cmd: String, timeout_secs: Option, name: Option) cmd, timeout_secs, status: TaskStatus::Pending, - created_at: now_unix(), + created_at: chrono::Utc::now(), started_at: None, completed_at: None, exit_code: None, @@ -377,7 +375,7 @@ pub fn kill_task(id: &str, force: bool) -> (bool, bool) { && task.status == TaskStatus::Pending { task.status = TaskStatus::Killed; - task.completed_at = Some(now_unix()); + task.completed_at = Some(chrono::Utc::now()); let _ = write_task(&task); // A Pending task never reached Running, so it has no keyed "active" // todo to clear and (like the old code) fires no completion signal — @@ -435,7 +433,7 @@ async fn mark_interrupted(socket: &Path) { } tracing::warn!(id = %id, "bash_runner: marking interrupted task"); task.status = TaskStatus::Interrupted; - task.completed_at = Some(now_unix()); + task.completed_at = Some(chrono::Utc::now()); if let Err(e) = write_task(&task) { tracing::warn!(id = %id, error = ?e, "bash_runner: write interrupted state failed"); } @@ -490,7 +488,7 @@ async fn run_task(mut task: TaskFile, socket: &Path) { tracing::info!(id = %id, cmd = %task.cmd, "bash_runner: starting task"); task.status = TaskStatus::Running; - task.started_at = Some(now_unix()); + task.started_at = Some(chrono::Utc::now()); if let Err(e) = write_task(&task) { tracing::warn!(id = %id, error = ?e, "bash_runner: write running state failed"); } @@ -559,7 +557,7 @@ async fn run_task(mut task: TaskFile, socket: &Path) { }; task.status = status; - task.completed_at = Some(now_unix()); + task.completed_at = Some(chrono::Utc::now()); task.exit_code = exit_code; task.stdout_tail = stdout_tail.clone().filter(|s| !s.is_empty()); task.stderr_tail = stderr_tail.clone().filter(|s| !s.is_empty()); diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 8ca7570c..d2226c78 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -337,11 +337,11 @@ pub struct TaskFile { #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout_secs: Option, pub status: TaskStatus, - pub created_at: i64, + pub created_at: DateTime, #[serde(default, skip_serializing_if = "Option::is_none")] - pub started_at: Option, + pub started_at: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - pub completed_at: Option, + pub completed_at: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub exit_code: Option, /// Last `SUMMARY_BYTES` of stdout (see `hive-bash-mcp` runner).