1use super::{
4 MCP_TASK_CONTRACT_ID, MCP_TASK_PROGRESS_CONTRACT_MESSAGE, MCP_TASK_REGISTRY_CAPACITY,
5 MCP_TOOL_ATLAS_TASK_STATUS, mcp_unix_time_ms,
6};
7use projectatlas_core::IndexWorkControl;
8use serde::Serialize;
9use std::collections::VecDeque;
10
11#[derive(Debug, Clone)]
13pub(super) struct McpTaskRegistry {
14 records: VecDeque<McpTaskRecord>,
16}
17
18impl McpTaskRegistry {
19 pub(super) fn new() -> Self {
21 let now = mcp_unix_time_ms();
22 let mut registry = Self {
23 records: VecDeque::new(),
24 };
25 registry.insert(McpTaskRecord {
26 task_id: MCP_TASK_CONTRACT_ID.to_string(),
27 operation: McpTaskOperation::Contract,
28 state: McpTaskState::Complete,
29 created_at_ms: now,
30 updated_at_ms: now,
31 progress: Some(McpTaskProgress {
32 current: Some(1),
33 total: Some(1),
34 message: Some(MCP_TASK_PROGRESS_CONTRACT_MESSAGE.to_string()),
35 }),
36 error: None,
37 result_ref: Some(MCP_TOOL_ATLAS_TASK_STATUS.to_string()),
38 cancelable: false,
39 control: None,
40 });
41 registry
42 }
43
44 pub(super) fn active_count(&self) -> usize {
46 self.records
47 .iter()
48 .filter(|record| !record.is_terminal_state())
49 .count()
50 }
51
52 #[cfg(test)]
54 pub(super) fn len(&self) -> usize {
55 self.records.len()
56 }
57
58 #[cfg(test)]
60 pub(super) fn latest_task_id(&self, operation: &McpTaskOperation) -> Option<String> {
61 self.records
62 .iter()
63 .rev()
64 .find(|record| &record.operation == operation)
65 .map(|record| record.task_id.clone())
66 }
67
68 pub(super) fn insert(&mut self, record: McpTaskRecord) {
70 if let Some(existing_index) = self
71 .records
72 .iter()
73 .position(|current| current.task_id == record.task_id)
74 {
75 let _removed = self.records.remove(existing_index);
76 }
77 while self.records.len() >= MCP_TASK_REGISTRY_CAPACITY {
78 if let Some(finished_index) = self
79 .records
80 .iter()
81 .position(McpTaskRecord::is_terminal_state)
82 {
83 let _evicted = self.records.remove(finished_index);
84 } else {
85 let _evicted = self.records.pop_front();
86 }
87 }
88 self.records.push_back(record);
89 }
90
91 pub(super) fn get(&self, task_id: &str) -> Option<McpTaskRecord> {
93 self.records
94 .iter()
95 .find(|record| record.task_id == task_id)
96 .cloned()
97 }
98
99 pub(super) fn update<F>(&mut self, task_id: &str, update: F) -> Option<McpTaskRecord>
101 where
102 F: FnOnce(&mut McpTaskRecord),
103 {
104 let record = self
105 .records
106 .iter_mut()
107 .find(|record| record.task_id == task_id)?;
108 update(record);
109 Some(record.clone())
110 }
111}
112
113#[derive(Debug, Clone, Serialize)]
115pub(super) struct McpTaskRecord {
116 pub(super) task_id: String,
118 pub(super) operation: McpTaskOperation,
120 pub(super) state: McpTaskState,
122 pub(super) created_at_ms: u128,
124 pub(super) updated_at_ms: u128,
126 pub(super) progress: Option<McpTaskProgress>,
128 pub(super) error: Option<String>,
130 pub(super) result_ref: Option<String>,
132 pub(super) cancelable: bool,
134 #[serde(skip)]
136 pub(super) control: Option<IndexWorkControl>,
137}
138
139impl McpTaskRecord {
140 pub(super) fn is_terminal_state(&self) -> bool {
142 matches!(
143 self.state,
144 McpTaskState::Complete | McpTaskState::Failed | McpTaskState::Canceled
145 )
146 }
147}
148
149#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
151#[serde(rename_all = "snake_case")]
152pub(super) enum McpTaskOperation {
153 Contract,
155 Scan,
157 WatchOnce,
159 SymbolsBuild,
161 Search,
163}
164
165#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize)]
167#[serde(rename_all = "snake_case")]
168pub(super) enum McpTaskState {
169 Pending,
171 Running,
173 Complete,
175 Failed,
177 Canceled,
179}
180
181#[derive(Debug, Clone, Serialize)]
183pub(super) struct McpTaskProgress {
184 pub(super) current: Option<u64>,
186 pub(super) total: Option<u64>,
188 pub(super) message: Option<String>,
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn evictions_prefer_old_terminal_records() {
198 let mut registry = McpTaskRegistry {
199 records: VecDeque::new(),
200 };
201 registry.insert(McpTaskRecord {
202 task_id: "running-0".to_string(),
203 operation: McpTaskOperation::Search,
204 state: McpTaskState::Running,
205 created_at_ms: 0,
206 updated_at_ms: 0,
207 progress: None,
208 error: None,
209 result_ref: None,
210 cancelable: true,
211 control: None,
212 });
213 for index in 1..MCP_TASK_REGISTRY_CAPACITY {
214 registry.insert(McpTaskRecord {
215 task_id: format!("complete-{index}"),
216 operation: McpTaskOperation::Search,
217 state: McpTaskState::Complete,
218 created_at_ms: index as u128,
219 updated_at_ms: index as u128,
220 progress: None,
221 error: None,
222 result_ref: None,
223 cancelable: false,
224 control: None,
225 });
226 }
227
228 registry.insert(McpTaskRecord {
229 task_id: "new-complete".to_string(),
230 operation: McpTaskOperation::Search,
231 state: McpTaskState::Complete,
232 created_at_ms: 100,
233 updated_at_ms: 100,
234 progress: None,
235 error: None,
236 result_ref: Some(MCP_TOOL_ATLAS_TASK_STATUS.to_string()),
237 cancelable: false,
238 control: None,
239 });
240
241 assert_eq!(registry.len(), MCP_TASK_REGISTRY_CAPACITY);
242 assert!(registry.get("running-0").is_some());
243 assert!(registry.get("complete-1").is_none());
244 assert!(registry.get("new-complete").is_some());
245 }
246}