返回 CodeWhale
liveness.rs
根目录 / crates / tui / src / work_graph / liveness.rs
1 //! Owner-facing lifecycle adapter vocabulary.
2 //!
3 //! Owners keep their existing registries, ledgers, and process handles. They
4 //! translate those records into this small vocabulary; `WorkRuntime`
5 //! persists the resulting observation through the graph reducer. No adapter
6 //! infers liveness from UI state.
7
8 use super::{AcceptanceRequirement, EvidenceRef, OperationObservation, OwnerState, Ts};
9 use crate::fleet::ledger::{FleetTaskLedgerStatus, FleetTaskState};
10 use crate::task_manager::TaskStatus;
11 use chrono::{DateTime, Utc};
12 use codewhale_lane::{LaneRecord, LaneStatus};
13
14 /// Spawn intent registered before an owner starts work.
15 #[derive(Debug, Clone, PartialEq)]
16 pub struct OperationIntent {
17 pub external: String,
18 pub title: String,
19 pub durable: bool,
20 pub source: String,
21 pub call_id: String,
22 pub acceptance: Vec<AcceptanceRequirement>,
23 }
24
25 impl OperationIntent {
26 #[must_use]
27 pub fn new(
28 external: impl Into<String>,
29 title: impl Into<String>,
30 durable: bool,
31 source: impl Into<String>,
32 call_id: impl Into<String>,
33 ) -> Self {
34 Self {
35 external: external.into(),
36 title: title.into(),
37 durable,
38 source: source.into(),
39 call_id: call_id.into(),
40 acceptance: Vec::new(),
41 }
42 }
43 }
44
45 /// One authoritative owner snapshot. `seq` must be monotonic within the
46 /// external binding; replaying the same `(binding, seq)` is a reducer no-op.
47 #[derive(Debug, Clone, PartialEq)]
48 pub struct OperationOwnerSnapshot {
49 pub external: String,
50 pub state: OwnerState,
51 pub seq: u64,
52 pub observed_at: Ts,
53 pub output: Option<EvidenceRef>,
54 }
55
56 impl OperationOwnerSnapshot {
57 #[must_use]
58 pub fn new(external: impl Into<String>, state: OwnerState, seq: u64, observed_at: Ts) -> Self {
59 Self {
60 external: external.into(),
61 state,
62 seq,
63 observed_at,
64 output: None,
65 }
66 }
67
68 #[must_use]
69 pub fn with_output(mut self, output: EvidenceRef) -> Self {
70 self.output = Some(output);
71 self
72 }
73
74 #[must_use]
75 pub fn into_observation(self) -> OperationObservation {
76 OperationObservation::OwnerReported {
77 state: self.state,
78 seq: self.seq,
79 at: self.observed_at,
80 output: self.output,
81 }
82 }
83 }
84
85 /// Translate a durable task record or summary without duplicating lifecycle
86 /// semantics across the model tool, periodic TUI refresh, and engine restore.
87 #[must_use]
88 pub fn task_owner_snapshot(
89 id: &str,
90 status: TaskStatus,
91 lifecycle_seq: u64,
92 created_at: DateTime<Utc>,
93 started_at: Option<DateTime<Utc>>,
94 ended_at: Option<DateTime<Utc>>,
95 ) -> OperationOwnerSnapshot {
96 let state = match status {
97 TaskStatus::Queued => OwnerState::Initializing,
98 TaskStatus::Running => OwnerState::Running,
99 TaskStatus::Completed => OwnerState::Completed,
100 TaskStatus::Failed => OwnerState::Failed,
101 TaskStatus::Canceled => OwnerState::Cancelled,
102 };
103 let observed_at = ended_at
104 .or(started_at)
105 .unwrap_or(created_at)
106 .timestamp_millis();
107 OperationOwnerSnapshot::new(format!("task:{id}"), state, lifecycle_seq, observed_at)
108 }
109
110 /// Translate the replayed Fleet task ledger. Live worker enrichment never
111 /// overrides this durable task projection.
112 #[must_use]
113 pub fn fleet_task_owner_snapshot(task: &FleetTaskState, observed_at: Ts) -> OperationOwnerSnapshot {
114 let state = match task.status {
115 FleetTaskLedgerStatus::Enqueued => OwnerState::Initializing,
116 FleetTaskLedgerStatus::Leased => OwnerState::Running,
117 FleetTaskLedgerStatus::Completed => OwnerState::Completed,
118 FleetTaskLedgerStatus::Failed => OwnerState::Failed,
119 FleetTaskLedgerStatus::Cancelled => OwnerState::Cancelled,
120 };
121 OperationOwnerSnapshot::new(
122 format!("fleet:{}/{}", task.entry.run_id.0, task.entry.task_id),
123 state,
124 task.lifecycle_seq.max(1),
125 observed_at,
126 )
127 }
128
129 /// Translate a durable Lane registry record without inspecting backend
130 /// processes. Backend reconciliation must first update the registry; the
131 /// registry remains the owner presented to the graph.
132 #[must_use]
133 pub fn lane_owner_snapshot(record: &LaneRecord, observed_at: Ts) -> OperationOwnerSnapshot {
134 let state = match record.status {
135 LaneStatus::Pending => OwnerState::Initializing,
136 LaneStatus::Running => OwnerState::Running,
137 LaneStatus::Stopped => OwnerState::Cancelled,
138 LaneStatus::Failed => OwnerState::Failed,
139 LaneStatus::Completed => OwnerState::Completed,
140 };
141 OperationOwnerSnapshot::new(
142 format!("lane:{}", record.id),
143 state,
144 record.lifecycle_seq.max(1),
145 observed_at,
146 )
147 }
148
149 #[cfg(test)]
150 mod tests {
151 use super::*;
152
153 #[test]
154 fn task_owner_snapshot_exhaustively_maps_status_and_prefers_terminal_time() {
155 let created_at = DateTime::from_timestamp_millis(10).expect("created timestamp");
156 let started_at = DateTime::from_timestamp_millis(20).expect("started timestamp");
157 let ended_at = DateTime::from_timestamp_millis(30).expect("ended timestamp");
158 let cases = [
159 (TaskStatus::Queued, OwnerState::Initializing),
160 (TaskStatus::Running, OwnerState::Running),
161 (TaskStatus::Completed, OwnerState::Completed),
162 (TaskStatus::Failed, OwnerState::Failed),
163 (TaskStatus::Canceled, OwnerState::Cancelled),
164 ];
165
166 for (status, expected) in cases {
167 let snapshot = task_owner_snapshot(
168 "task-id",
169 status,
170 7,
171 created_at,
172 Some(started_at),
173 Some(ended_at),
174 );
175 assert_eq!(snapshot.external, "task:task-id");
176 assert_eq!(snapshot.state, expected);
177 assert_eq!(snapshot.seq, 7);
178 assert_eq!(snapshot.observed_at, 30);
179 }
180 }
181
182 #[test]
183 fn task_owner_snapshot_falls_back_from_started_to_created_time() {
184 let created_at = DateTime::from_timestamp_millis(10).expect("created timestamp");
185 let started_at = DateTime::from_timestamp_millis(20).expect("started timestamp");
186
187 let started = task_owner_snapshot(
188 "started",
189 TaskStatus::Running,
190 2,
191 created_at,
192 Some(started_at),
193 None,
194 );
195 let queued = task_owner_snapshot("queued", TaskStatus::Queued, 1, created_at, None, None);
196
197 assert_eq!(started.observed_at, 20);
198 assert_eq!(queued.observed_at, 10);
199 }
200 }
201
201 lines RUST