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}