Skip to main content

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}