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    /// STEP_12 telemetry — short (16 hex chars) form of the
104    /// `WorkspaceKey::workspace_id` digest, suitable for human-scale
105    /// log lines and CLI columns. `None` for anonymous (per-source-root)
106    /// keys that carry no logical-workspace identity. Display only —
107    /// **do not** key on this for cross-process identity comparisons;
108    /// see [`Self::workspace_id_full`] for that.
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub workspace_id_short: Option<String>,
111
112    /// STEP_12 telemetry — full 64-hex-char form of the
113    /// `WorkspaceKey::workspace_id` digest. Machine identity. Scripts
114    /// consuming the JSON payload **MUST** key on this field rather
115    /// than [`Self::workspace_id_short`] to avoid the (remote, but
116    /// non-zero) possibility of short-hex collisions across hundreds
117    /// of thousands of distinct workspaces. `None` for anonymous keys.
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub workspace_id_full: Option<String>,
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn daemon_status_round_trips_through_json() {
128        let status = DaemonStatus {
129            uptime_seconds: 120,
130            daemon_version: "8.0.6".into(),
131            memory: MemoryStatus {
132                limit_bytes: 2 * 1024 * 1024 * 1024,
133                current_bytes: 450 * 1024 * 1024,
134                reserved_bytes: 0,
135                high_water_bytes: 1_200 * 1024 * 1024,
136            },
137            workspaces: vec![WorkspaceStatus {
138                index_root: PathBuf::from("/repos/example"),
139                state: WorkspaceState::Loaded,
140                pinned: true,
141                current_bytes: 320 * 1024 * 1024,
142                high_water_bytes: 890 * 1024 * 1024,
143                last_good_at: None,
144                last_error: None,
145                retry_count: 0,
146                workspace_id_short: None,
147                workspace_id_full: None,
148            }],
149        };
150        let json = serde_json::to_string(&status).expect("serialise");
151        let back: DaemonStatus = serde_json::from_str(&json).expect("round-trip");
152        assert_eq!(back, status);
153    }
154}