feat: event-driven parity, Windows shell detection, dirty build timestamps, and gatekeeper integration
This commit is contained in:
1 parent
64857f9d5e
commit
f4dcf3acbf
33 files changed
+3501
-143
No files matched your search
+110
-1
@@ -42,6 +42,78 @@ pub struct TelemetryStores {
|
||||
pub agent_signals: Store<Vec<AgentSignal>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct CachedClipboardImage {
|
||||
pub file_path: String,
|
||||
pub file_path_wsl: String,
|
||||
pub captured_at_epoch_ms: u64,
|
||||
pub age: String,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub size_bytes: usize,
|
||||
pub ocr_text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct CachedClipboardText {
|
||||
pub text: String,
|
||||
pub captured_at_epoch_ms: u64,
|
||||
pub age: String,
|
||||
pub char_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum ClipboardHistoryItem {
|
||||
Image(CachedClipboardImage),
|
||||
Text(CachedClipboardText),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ClipboardCacheState {
|
||||
pub last_image: Option<CachedClipboardImage>,
|
||||
pub last_text: Option<CachedClipboardText>,
|
||||
pub history: std::collections::VecDeque<ClipboardHistoryItem>,
|
||||
}
|
||||
|
||||
impl ClipboardCacheState {
|
||||
pub fn push_image(&mut self, image: CachedClipboardImage) {
|
||||
self.last_image = Some(image.clone());
|
||||
self.history.push_front(ClipboardHistoryItem::Image(image));
|
||||
if self.history.len() > 20 {
|
||||
self.history.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_text(&mut self, text: CachedClipboardText) {
|
||||
if let Some(ref prev) = self.last_text {
|
||||
if prev.text == text.text {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.last_text = Some(text.clone());
|
||||
self.history.push_front(ClipboardHistoryItem::Text(text));
|
||||
if self.history.len() > 20 {
|
||||
self.history.pop_back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_age(epoch_ms: u64) -> String {
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64;
|
||||
let diff_secs = now_ms.saturating_sub(epoch_ms) / 1000;
|
||||
if diff_secs < 60 {
|
||||
format!("{}s ago", diff_secs)
|
||||
} else if diff_secs < 3600 {
|
||||
format!("{}m ago", diff_secs / 60)
|
||||
} else {
|
||||
format!("{}h ago", diff_secs / 3600)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MemoryState {
|
||||
pub base_dir: PathBuf,
|
||||
pub index_commit_notify: Arc<tokio::sync::Notify>,
|
||||
@@ -59,6 +131,7 @@ pub struct MemoryState {
|
||||
pub activity_tx: tokio::sync::broadcast::Sender<String>,
|
||||
pub event_bus_tx: tokio::sync::broadcast::Sender<GenericEvent>,
|
||||
pub ollama: Arc<crate::ollama::OllamaClient>,
|
||||
pub clipboard_cache: Arc<tokio::sync::RwLock<ClipboardCacheState>>,
|
||||
}
|
||||
|
||||
impl MemoryState {
|
||||
@@ -133,6 +206,7 @@ impl MemoryState {
|
||||
|
||||
activity_tx: tokio::sync::broadcast::channel(100).0,
|
||||
event_bus_tx: tokio::sync::broadcast::channel(1000).0,
|
||||
clipboard_cache: Arc::new(tokio::sync::RwLock::new(ClipboardCacheState::default())),
|
||||
};
|
||||
|
||||
// Normalize pre-existing graph entity and relation types
|
||||
@@ -162,6 +236,19 @@ impl MemoryState {
|
||||
|
||||
pub fn modify_graph<F: FnOnce(&mut KnowledgeGraph)>(&self, update_fn: F) {
|
||||
self.graph.modify(update_fn);
|
||||
let threshold: usize = std::env::var("MCP_MEMORY_CONDENSE_THRESHOLD")
|
||||
.unwrap_or_else(|_| "100".to_string())
|
||||
.parse()
|
||||
.unwrap_or(100);
|
||||
let count = self.graph.read_with(|g| g.entities.len());
|
||||
if count >= threshold {
|
||||
self.condense_notify.notify_one();
|
||||
}
|
||||
let _ = self.event_bus_tx.send(GenericEvent {
|
||||
topic: "resource:updated".to_string(),
|
||||
session_id: None,
|
||||
payload: serde_json::json!({ "uri": "memory://graph" }),
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn get_search_index(&self) -> MemoryIndex {
|
||||
@@ -283,7 +370,24 @@ impl MemoryState {
|
||||
}
|
||||
|
||||
pub fn broadcast_task_event(&self, event: TaskEvent) {
|
||||
let payload_val = serde_json::to_value(&event).unwrap_or_default();
|
||||
let mut payload_val = serde_json::to_value(&event).unwrap_or_default();
|
||||
if let Some(obj) = payload_val.as_object_mut() {
|
||||
let active_task = self.project.tasks.read_with(|tasks| {
|
||||
tasks
|
||||
.iter()
|
||||
.find(|t| t.is_active())
|
||||
.or_else(|| {
|
||||
tasks
|
||||
.iter()
|
||||
.find(|t| t.status == "pending" && t.parent_id.is_none())
|
||||
})
|
||||
.map(|t| t.title.clone())
|
||||
});
|
||||
obj.insert(
|
||||
"active_task".to_string(),
|
||||
serde_json::to_value(active_task).unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
let summary_str = format!("Task {} -> {}", event.task_id, event.status);
|
||||
let details_str = payload_val.to_string();
|
||||
@@ -315,6 +419,11 @@ impl MemoryState {
|
||||
payload: payload_val,
|
||||
};
|
||||
let _ = self.event_bus_tx.send(generic_ev);
|
||||
let _ = self.event_bus_tx.send(GenericEvent {
|
||||
topic: "resource:updated".to_string(),
|
||||
session_id: None,
|
||||
payload: serde_json::json!({ "uri": "memory://tasks/active" }),
|
||||
});
|
||||
|
||||
let ws_notification = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
|
||||
Reference in new issue
Block a user