diff --git a/hive-agent/src/db_migrate.rs b/hive-agent/src/db_migrate.rs index 503a001b..9feca95b 100644 --- a/hive-agent/src/db_migrate.rs +++ b/hive-agent/src/db_migrate.rs @@ -172,7 +172,7 @@ mod tests { let pending = migrated.list_pending().unwrap(); assert_eq!(pending.len(), 1); assert_eq!(pending[0].id, orig_id, "id preserved across the copy"); - assert_eq!(pending[0].due_at, 1000); + assert_eq!(pending[0].due_at.timestamp(), 1000); } #[test] diff --git a/hive-agent/src/events.rs b/hive-agent/src/events.rs index 8cf1f6c3..cf585f5f 100644 --- a/hive-agent/src/events.rs +++ b/hive-agent/src/events.rs @@ -12,8 +12,8 @@ use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; +use chrono::Utc; use hive_claude::TokenUsage; -use hive_sh4re::wire_time::now_unix; use rusqlite::{Connection, params}; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast; @@ -390,7 +390,7 @@ impl Bus { tx: Arc::new(tx), event_seq: Arc::new(AtomicU64::new(0)), store, - state: Arc::new(Mutex::new((TurnState::Idle, now_unix()))), + state: Arc::new(Mutex::new((TurnState::Idle, Utc::now().timestamp()))), model: Arc::new(Mutex::new(initial_model)), effort: Arc::new(Mutex::new(initial_effort)), last_ctx_usage: Arc::new(Mutex::new(None)), @@ -575,7 +575,7 @@ impl Bus { *self.last_ctx_usage.lock().unwrap() = Some(ctx); *self.last_cost_usage.lock().unwrap() = Some(cost); self.last_turn_ended_unix - .store(now_unix(), Ordering::Relaxed); + .store(Utc::now().timestamp(), Ordering::Relaxed); self.emit(LiveEvent::TokenUsageChanged { ctx, cost }); } @@ -778,7 +778,7 @@ impl Bus { if guard.0 == next { return; } - *guard = (next, now_unix()); + *guard = (next, Utc::now().timestamp()); since = guard.1; } self.emit(LiveEvent::TurnStateChanged { @@ -861,7 +861,7 @@ impl Bus { } let envelope = BusEvent { seq: self.next_seq(), - ts: now_unix(), + ts: Utc::now().timestamp(), event, }; // Lagged subscribers drop events — fine; the UI is a tail, not a log. diff --git a/hive-agent/src/main.rs b/hive-agent/src/main.rs index 6d578de8..a22f2722 100644 --- a/hive-agent/src/main.rs +++ b/hive-agent/src/main.rs @@ -770,7 +770,7 @@ async fn handle_turn( unread, }); bus.set_state(TurnState::Thinking); - let started_at = serve_common::now_unix(); + let started_at = chrono::Utc::now().timestamp(); let started_instant = std::time::Instant::now(); let model_at_start = bus.model(); let prompt = serve_common::format_wake_prompt(msg_id, &from, &body, unread, redelivered); @@ -842,7 +842,7 @@ async fn handle_turn( let sid = stats.start_session(started_at, &model_at_start); bus.set_session_id(sid); } - let ended_at = serve_common::now_unix(); + let ended_at = chrono::Utc::now().timestamp(); let duration_ms = i64::try_from(started_instant.elapsed().as_millis()).unwrap_or(i64::MAX); let (open_threads, open_reminders) = S::post_turn_counts(socket).await; let row = serve_common::build_row(serve_common::TurnRowArgs { diff --git a/hive-agent/src/questions.rs b/hive-agent/src/questions.rs index c2c5f8ca..62a3fd95 100644 --- a/hive-agent/src/questions.rs +++ b/hive-agent/src/questions.rs @@ -32,7 +32,7 @@ use std::path::Path; use std::sync::Mutex; use anyhow::{Context, Result}; -use hive_sh4re::wire_time::now_unix; +use chrono::{DateTime, Utc}; use rusqlite::{Connection, params}; const SCHEMA: &str = r" @@ -78,7 +78,7 @@ pub struct QuestionMirror { pub role: Role, pub peer: String, pub question: String, - pub asked_at: i64, + pub asked_at: DateTime, } /// The harness-local questions mirror. Same sharing/locking shape as @@ -120,7 +120,7 @@ impl Questions { conn.execute( "INSERT OR REPLACE INTO questions (id, role, peer, question, asked_at) \ VALUES (?1, ?2, ?3, ?4, ?5)", - params![id, role.as_str(), peer, question, now_unix()], + params![id, role.as_str(), peer, question, Utc::now().timestamp()], )?; Ok(()) } @@ -170,12 +170,14 @@ impl Questions { tracing::warn!(%id, %role_str, "questions mirror: unknown role, skipping row"); continue; }; + let asked_at_secs: i64 = row.get(4)?; out.push(QuestionMirror { id, role, peer: row.get(2)?, question: row.get(3)?, - asked_at: row.get(4)?, + asked_at: chrono::DateTime::from_timestamp(asked_at_secs, 0) + .unwrap_or_else(Utc::now), }); } Ok(out) diff --git a/hive-agent/src/reminder_timer.rs b/hive-agent/src/reminder_timer.rs index 326f8ef5..8d857343 100644 --- a/hive-agent/src/reminder_timer.rs +++ b/hive-agent/src/reminder_timer.rs @@ -74,7 +74,7 @@ pub async fn run( } fn tick(store: &Reminders, tx: &mpsc::UnboundedSender) { - let now = hive_sh4re::wire_time::now_unix(); + let now = chrono::Utc::now().timestamp(); let due = match store.due(now, REMINDER_BATCH_LIMIT) { Ok(rows) => rows, Err(e) => { @@ -269,7 +269,7 @@ mod tests { #[test] fn resolve_due_at_in_seconds_is_close_to_now_plus_n() { let due = resolve_due_at(&hive_sh4re::ReminderTiming::InSeconds { seconds: 60 }).unwrap(); - let now = hive_sh4re::wire_time::now_unix(); + let now = chrono::Utc::now().timestamp(); assert!((due - now - 60).abs() <= 2, "due={due} now={now}"); } diff --git a/hive-agent/src/reminders.rs b/hive-agent/src/reminders.rs index 4f4936f3..f5837d91 100644 --- a/hive-agent/src/reminders.rs +++ b/hive-agent/src/reminders.rs @@ -17,8 +17,9 @@ use std::path::Path; use std::sync::Mutex; use anyhow::{Context, Result}; +use chrono::{DateTime, Utc}; use hive_sh4re::ReminderStats; -use hive_sh4re::wire_time::now_unix; +use hive_sh4re::wire_time; use rusqlite::{Connection, params}; const SCHEMA: &str = r" @@ -42,8 +43,8 @@ pub struct Reminder { pub id: i64, pub message: String, pub file_path: Option, - pub due_at: i64, - pub created_at: i64, + pub due_at: DateTime, + pub created_at: DateTime, } /// The harness-local reminder store. Cheap to share behind an `Arc`; the @@ -81,7 +82,7 @@ impl Reminders { /// Panics if the connection mutex is poisoned. pub fn store(&self, message: &str, file_path: Option<&str>, due_at: i64) -> Result { let conn = self.conn.lock().unwrap(); - let now = now_unix(); + let now = Utc::now().timestamp(); conn.execute( "INSERT INTO reminders (message, file_path, due_at, created_at, sent_at) \ VALUES (?1, ?2, ?3, ?4, NULL)", @@ -170,7 +171,7 @@ impl Reminders { let conn = self.conn.lock().unwrap(); conn.execute( "UPDATE reminders SET sent_at = ?1 WHERE id = ?2", - params![now_unix(), id], + params![Utc::now().timestamp(), id], )?; Ok(()) } @@ -210,7 +211,7 @@ impl Reminders { pub fn rollup(&self, since_secs: i64) -> Result { let conn = self.conn.lock().unwrap(); let cutoff = if since_secs > 0 { - now_unix().saturating_sub(since_secs) + Utc::now().timestamp().saturating_sub(since_secs) } else { i64::MIN }; @@ -258,12 +259,14 @@ impl Reminders { } fn row_to_reminder(row: &rusqlite::Row) -> rusqlite::Result { + let due_at_secs: i64 = row.get(3)?; + let created_at_secs: i64 = row.get(4)?; Ok(Reminder { id: row.get(0)?, message: row.get(1)?, file_path: row.get(2)?, - due_at: row.get(3)?, - created_at: row.get(4)?, + due_at: wire_time::from_secs(due_at_secs), + created_at: wire_time::from_secs(created_at_secs), }) } @@ -287,7 +290,7 @@ mod tests { let pending = s.list_pending().unwrap(); assert_eq!(pending.len(), 1); assert_eq!(pending[0].id, id); - assert_eq!(pending[0].due_at, 1000); + assert_eq!(pending[0].due_at.timestamp(), 1000); assert_eq!(s.count_pending().unwrap(), 1); } @@ -348,7 +351,7 @@ mod tests { ) .unwrap(); } - let cutoff = now_unix() - 10; + let cutoff = Utc::now().timestamp() - 10; let n = s.prune_delivered_older_than(cutoff).unwrap(); assert_eq!(n, 1, "only the backdated row is older than cutoff"); let remaining_ids: Vec = { diff --git a/hive-agent/src/serve_common.rs b/hive-agent/src/serve_common.rs index c3fdcf6f..778aee51 100644 --- a/hive-agent/src/serve_common.rs +++ b/hive-agent/src/serve_common.rs @@ -7,7 +7,6 @@ use crate::events::Bus; use crate::turn::{TurnError, TurnOutcome}; use crate::turn_stats::TurnStatRow; -pub use hive_sh4re::wire_time::now_unix; /// Assemble the per-turn wake prompt string. The role/tools/etc. live in the /// system prompt; this is just the wake signal body. `id` is the broker row diff --git a/hive-agent/src/stats.rs b/hive-agent/src/stats.rs index 91563fdb..ff018018 100644 --- a/hive-agent/src/stats.rs +++ b/hive-agent/src/stats.rs @@ -11,11 +11,11 @@ use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; +use chrono::Utc; use rusqlite::{Connection, OpenFlags}; use serde::Serialize; use hive_sh4re::ReminderStats; -use hive_sh4re::wire_time::now_unix; /// Window param accepted by `/api/stats?window=`. Each maps to a /// total span + the bucket width used to roll up trend series. @@ -225,7 +225,7 @@ fn default_path() -> PathBuf { } fn empty_snapshot(window: Window) -> Snapshot { - let now = now_unix(); + let now = Utc::now().timestamp(); let from = now - window.span_secs(); let buckets = fill_buckets(from, now, window.bucket_secs(), &HashMap::new()); Snapshot { @@ -305,7 +305,7 @@ fn snapshot(path: &Path, window: Window) -> Result { // and blanks the whole stats page. Wait out the brief write instead. conn.busy_timeout(std::time::Duration::from_millis(500)) .with_context(|| format!("set busy_timeout on {}", path.display()))?; - let now = now_unix(); + let now = Utc::now().timestamp(); // Fixed windows look back a constant span; `all` starts at the earliest // recorded turn and sizes its buckets adaptively from that span. let (from, bucket_secs) = match window { @@ -677,7 +677,7 @@ mod tests { fn snapshot_aggregates_rows() { let db = tmp_db(); let _ = std::fs::remove_file(&db); - let now = now_unix(); + let now = Utc::now().timestamp(); seed_db( &db, &[ @@ -766,7 +766,17 @@ mod tests { fn bash_breakdown_empty_without_table() { let db = tmp_db(); let _ = std::fs::remove_file(&db); - seed_db(&db, &[(now_unix() - 100, 1000, "opus", "recv", "ok", "{}")]); + seed_db( + &db, + &[( + Utc::now().timestamp() - 100, + 1000, + "opus", + "recv", + "ok", + "{}", + )], + ); let s = snapshot(&db, Window::Day).unwrap(); assert!(s.bash_breakdown.is_empty()); } @@ -778,7 +788,7 @@ mod tests { let db = tmp_db(); let _ = std::fs::remove_file(&db); seed_db(&db, &[]); - let now = now_unix(); + let now = Utc::now().timestamp(); let conn = Connection::open(&db).unwrap(); conn.execute_batch("CREATE TABLE bash_commands (ts INTEGER NOT NULL, head TEXT NOT NULL);") .unwrap(); diff --git a/hive-agent/src/todo_server.rs b/hive-agent/src/todo_server.rs index f9a81167..c307e027 100644 --- a/hive-agent/src/todo_server.rs +++ b/hive-agent/src/todo_server.rs @@ -479,8 +479,8 @@ fn no_questions_store() -> Response { /// `target` are derived from `role` — this agent's own label fills whichever /// side `role` says is us, `peer` fills the other. fn question_to_loose_end(q: QuestionMirror) -> LooseEnd { - let now = hive_sh4re::wire_time::now_unix(); - let age = u64::try_from(now.saturating_sub(q.asked_at)).unwrap_or(0); + let now = chrono::Utc::now().timestamp(); + let age = u64::try_from(now.saturating_sub(q.asked_at.timestamp())).unwrap_or(0); let me = crate::identity::label(); let (asker, target) = match q.role { Role::Asked => (me, Some(q.peer)), @@ -500,13 +500,13 @@ fn question_to_loose_end(q: QuestionMirror) -> LooseEnd { /// "age" is how long the reminder has been *scheduled*, not how soon /// it's due). fn reminder_to_loose_end(r: Reminder) -> LooseEnd { - let now = hive_sh4re::wire_time::now_unix(); - let age = u64::try_from(now.saturating_sub(r.created_at)).unwrap_or(0); + let now = chrono::Utc::now().timestamp(); + let age = u64::try_from(now.saturating_sub(r.created_at.timestamp())).unwrap_or(0); LooseEnd::Reminder { id: r.id, owner: crate::identity::label(), message: r.message, - due_at: hive_sh4re::wire_time::from_secs(r.due_at), + due_at: r.due_at, age_seconds: age, } } @@ -521,8 +521,8 @@ fn err(e: &anyhow::Error) -> Response { /// Map a stored [`Todo`] to a [`LooseEnd::Todo`], deriving `age_seconds` /// from `updated_at` (saturating so a backwards clock step reads 0). fn to_loose_end(t: Todo) -> LooseEnd { - let now = hive_sh4re::wire_time::now_unix(); - let age = u64::try_from(now.saturating_sub(t.updated_at)).unwrap_or(0); + let now = chrono::Utc::now().timestamp(); + let age = u64::try_from(now.saturating_sub(t.updated_at.timestamp())).unwrap_or(0); LooseEnd::Todo { id: t.id, subsystem: t.subsystem, diff --git a/hive-agent/src/todos.rs b/hive-agent/src/todos.rs index 63deb11d..4fc08465 100644 --- a/hive-agent/src/todos.rs +++ b/hive-agent/src/todos.rs @@ -32,7 +32,8 @@ use std::path::Path; use std::sync::Mutex; use anyhow::{Context, Result}; -use hive_sh4re::wire_time::now_unix; +use chrono::{DateTime, Utc}; +use hive_sh4re::wire_time; use rusqlite::{Connection, params}; /// SQL bootstrap. `CREATE TABLE IF NOT EXISTS` so first-boot agents and @@ -66,8 +67,8 @@ const MIGRATIONS: &[&str] = &[ "ALTER TABLE todos ADD COLUMN acked_at INTEGER", ]; -/// One dynamic, subsystem-pushed todo. Timestamps are unix seconds; the -/// consumer derives `age_seconds` from `updated_at`. +/// One dynamic, subsystem-pushed todo. The consumer derives `age_seconds` +/// from `updated_at`. #[derive(Debug, Clone)] pub struct Todo { pub id: i64, @@ -80,7 +81,7 @@ pub struct Todo { pub summary: String, /// Optional free-text provenance (e.g. the room name / task label). pub source: Option, - pub updated_at: i64, + pub updated_at: DateTime, } /// The harness-local todo store. Cheap to share behind an `Arc`; the inner @@ -144,7 +145,7 @@ impl Todos { source: Option<&str>, ) -> Result<(i64, bool)> { let conn = self.conn.lock().unwrap(); - let now = now_unix(); + let now = Utc::now().timestamp(); let existing: Option<(i64, String, Option)> = if key.is_some() { conn.query_row( "SELECT id, summary, source FROM todos \ @@ -239,7 +240,7 @@ impl Todos { let conn = self.conn.lock().unwrap(); let n = conn.execute( "UPDATE todos SET acked = 1, acked_at = ?1 WHERE id = ?2 AND acked = 0", - params![now_unix(), id], + params![Utc::now().timestamp(), id], )?; Ok(n) } @@ -266,13 +267,14 @@ impl Todos { )?; let rows = stmt .query_map(params![subsystem], |row| { + let updated_at_secs: i64 = row.get(5)?; Ok(Todo { id: row.get(0)?, subsystem: row.get(1)?, subsystem_key: row.get(2)?, summary: row.get(3)?, source: row.get(4)?, - updated_at: row.get(5)?, + updated_at: wire_time::from_secs(updated_at_secs), }) })? .collect::>>()?; @@ -491,12 +493,12 @@ mod tests { let conn = s.conn.lock().unwrap(); conn.execute( "UPDATE todos SET acked_at = ?1 WHERE id = ?2", - params![now_unix() - 1000, old_id], + params![Utc::now().timestamp() - 1000, old_id], ) .unwrap(); } - let removed = s.reap_acked(now_unix() - 500).unwrap(); + let removed = s.reap_acked(Utc::now().timestamp() - 500).unwrap(); assert_eq!(removed, 1, "only the backdated row is past the cutoff"); let conn = Connection::open(dir.path().join("todos.sqlite")).unwrap();