sqry_daemon/workspace/status.rs
1//! `daemon/status` payload types.
2//!
3//! Phase 6b surfaces these from [`super::WorkspaceManager::status`].
4//! Task 8 serialises them over the IPC envelope; Task 10 (`sqry daemon
5//! status`) renders them into the human-readable and `--json`
6//! formats from Amendment 2 §D.
7//!
8//! Every byte count is a `u64` so the JSON-RPC payload is
9//! platform-independent on 32-bit hosts.
10
11use std::{path::PathBuf, time::SystemTime};
12
13use serde::{Deserialize, Serialize};
14
15use super::state::WorkspaceState;
16
17/// Top-level snapshot returned by `daemon/status`.
18///
19/// Construction is a single pass over the workspaces map plus a
20/// handful of atomic reads; the snapshot is *not* transactional w.r.t.
21/// publishes landing concurrently, but every field is individually
22/// consistent. Callers treat it as a best-effort point-in-time view.
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct DaemonStatus {
25 /// Seconds since the `WorkspaceManager` was constructed.
26 pub uptime_seconds: u64,
27
28 /// `env!("CARGO_PKG_VERSION")` at build time — whatever the
29 /// running daemon binary was compiled with.
30 pub daemon_version: String,
31
32 /// Aggregate memory accounting.
33 pub memory: MemoryStatus,
34
35 /// One entry per loaded or otherwise-tracked workspace, sorted
36 /// by `index_root` for deterministic CLI output.
37 pub workspaces: Vec<WorkspaceStatus>,
38}
39
40/// Aggregate memory accounting readout. Mirrors Amendment 2 §D.
41///
42/// Task 7 Phase 7c: marked `#[non_exhaustive]` so future field
43/// additions (e.g., `retained_bytes` for visibility into the
44/// retention reaper pipeline) do not force downstream struct-literal
45/// callers through a source-breaking change. External callers must
46/// construct instances through [`DaemonStatus`] observations (via
47/// `WorkspaceManager::status`) rather than literals.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[non_exhaustive]
50pub struct MemoryStatus {
51 /// Hard cap (`memory_limit_mb * 1 MiB`).
52 pub limit_bytes: u64,
53
54 /// Currently attributed memory:
55 /// `loaded_bytes + reserved_bytes + sum(retained_old bytes)`.
56 /// This is the left-hand side of the §G.5 invariant.
57 pub current_bytes: u64,
58
59 /// Reservation-only subset of `current_bytes` — the sum of every
60 /// in-flight [`crate::workspace::RebuildReservation`]. Task 7
61 /// Phase 7c: exposed so integration tests can assert refund on
62 /// the cancellation path. At rest (no rebuild in flight) this is
63 /// always `0`.
64 pub reserved_bytes: u64,
65
66 /// Monotonic peak of `current_bytes` over the daemon's uptime.
67 /// Updated on every publish via `fetch_max`.
68 pub high_water_bytes: u64,
69}
70
71/// Per-workspace status row.
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct WorkspaceStatus {
74 /// Canonical absolute path (from [`super::WorkspaceKey::index_root`]).
75 pub index_root: PathBuf,
76
77 /// Current lifecycle state.
78 pub state: WorkspaceState,
79
80 /// Whether this workspace is LRU-exempt.
81 pub pinned: bool,
82
83 /// Live graph size (from [`super::LoadedWorkspace::memory_bytes`]).
84 pub current_bytes: u64,
85
86 /// Monotonic peak over this workspace's loaded lifetime. Resets
87 /// only on unload/eviction (fresh `LoadedWorkspace`), never on
88 /// rebuilds — per Amendment 2 §D.
89 pub high_water_bytes: u64,
90
91 /// Wall-clock of the most recent successful build. `None` if the
92 /// workspace has never built successfully.
93 pub last_good_at: Option<SystemTime>,
94
95 /// Display string of the most recent error, if any. `None` in
96 /// the steady-state Loaded case.
97 pub last_error: Option<String>,
98
99 /// Count of consecutive failed rebuilds since the last successful
100 /// publish. Resets to 0 on `record_success`.
101 pub retry_count: u32,
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn daemon_status_round_trips_through_json() {
110 let status = DaemonStatus {
111 uptime_seconds: 120,
112 daemon_version: "8.0.6".into(),
113 memory: MemoryStatus {
114 limit_bytes: 2 * 1024 * 1024 * 1024,
115 current_bytes: 450 * 1024 * 1024,
116 reserved_bytes: 0,
117 high_water_bytes: 1_200 * 1024 * 1024,
118 },
119 workspaces: vec![WorkspaceStatus {
120 index_root: PathBuf::from("/repos/example"),
121 state: WorkspaceState::Loaded,
122 pinned: true,
123 current_bytes: 320 * 1024 * 1024,
124 high_water_bytes: 890 * 1024 * 1024,
125 last_good_at: None,
126 last_error: None,
127 retry_count: 0,
128 }],
129 };
130 let json = serde_json::to_string(&status).expect("serialise");
131 let back: DaemonStatus = serde_json::from_str(&json).expect("round-trip");
132 assert_eq!(back, status);
133 }
134}