diff --git a/v2/src-tauri/src/btmsg.rs b/v2/src-tauri/src/btmsg.rs index ca5e4b1..903cb92 100644 --- a/v2/src-tauri/src/btmsg.rs +++ b/v2/src-tauri/src/btmsg.rs @@ -215,38 +215,41 @@ pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result 0 { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64; + let cutoff = now - STALE_HEARTBEAT_SECS; - let recipient_stale: bool = db - .query_row( - "SELECT COALESCE(h.timestamp, 0) < ?1 FROM agents a \ - LEFT JOIN heartbeats h ON a.id = h.agent_id \ - WHERE a.id = ?2", - params![cutoff, to_agent], - |row| row.get(0), - ) - .unwrap_or(false); + let recipient_stale: bool = db + .query_row( + "SELECT COALESCE(h.timestamp, 0) < ?1 FROM agents a \ + LEFT JOIN heartbeats h ON a.id = h.agent_id \ + WHERE a.id = ?2", + params![cutoff, to_agent], + |row| row.get(0), + ) + .unwrap_or(false); - if recipient_stale { - // Queue to dead letter instead of delivering - let error_msg = format!("Recipient '{}' is stale (no heartbeat in {} seconds)", to_agent, STALE_HEARTBEAT_SECS); - db.execute( - "INSERT INTO dead_letter_queue (from_agent, to_agent, content, error) VALUES (?1, ?2, ?3, ?4)", - params![from_agent, to_agent, content, error_msg], - ) - .map_err(|e| format!("Dead letter insert error: {e}"))?; + if recipient_stale { + // Queue to dead letter instead of delivering + let error_msg = format!("Recipient '{}' is stale (no heartbeat in {} seconds)", to_agent, STALE_HEARTBEAT_SECS); + db.execute( + "INSERT INTO dead_letter_queue (from_agent, to_agent, content, error) VALUES (?1, ?2, ?3, ?4)", + params![from_agent, to_agent, content, error_msg], + ) + .map_err(|e| format!("Dead letter insert error: {e}"))?; - // Also log audit event - let _ = db.execute( - "INSERT INTO audit_log (agent_id, event_type, detail) VALUES (?1, 'dead_letter', ?2)", - params![from_agent, format!("Message to '{}' routed to dead letter queue: {}", to_agent, error_msg)], - ); + // Also log audit event + let _ = db.execute( + "INSERT INTO audit_log (agent_id, event_type, detail) VALUES (?1, 'dead_letter', ?2)", + params![from_agent, format!("Message to '{}' routed to dead letter queue: {}", to_agent, error_msg)], + ); - return Err(error_msg); + return Err(error_msg); + } } let msg_id = uuid::Uuid::new_v4().to_string(); diff --git a/v2/src/lib/components/Agent/AgentPane.svelte b/v2/src/lib/components/Agent/AgentPane.svelte index 6031fbc..3191e6a 100644 --- a/v2/src/lib/components/Agent/AgentPane.svelte +++ b/v2/src/lib/components/Agent/AgentPane.svelte @@ -151,14 +151,15 @@ // NOTE: Do NOT stop agents in onDestroy — it fires on layout changes/remounts, // not just explicit close. Stop-on-close is handled by workspace teardown. - // Auto-prompt: pick up externally triggered prompts (e.g. periodic context refresh) + // Auto-prompt: pick up externally triggered prompts (e.g. periodic context refresh, play button) $effect(() => { if (!autoPrompt || isRunning) return; - // Only trigger if session exists and is idle (done/error) - if (!session || (session.status !== 'done' && session.status !== 'error')) return; + // Allow: no session yet (first start) or session completed (done/error) + if (session && session.status !== 'done' && session.status !== 'error') return; const prompt = autoPrompt; + const resume = !!session; // new query if first start, resume if existing session onautopromptconsumed?.(); - startQuery(prompt, true); // resume session with context refresh + startQuery(prompt, resume); }); let promptRef = $state(); diff --git a/v2/src/lib/components/Workspace/AgentSession.svelte b/v2/src/lib/components/Workspace/AgentSession.svelte index 73cd65c..69f7332 100644 --- a/v2/src/lib/components/Workspace/AgentSession.svelte +++ b/v2/src/lib/components/Workspace/AgentSession.svelte @@ -12,6 +12,8 @@ type AgentMessageRecord, } from '../../adapters/groups-bridge'; import { registerSessionProject } from '../../agent-dispatcher'; + import { onAgentStart, onAgentStop } from '../../stores/workspace.svelte'; + import { stopAgent } from '../../adapters/agent-bridge'; import { trackProject, updateProjectSession } from '../../stores/health.svelte'; import { createAgentSession, @@ -97,11 +99,30 @@ lastPromptTime = Date.now(); } + // Listen for play-button start events from GroupAgentsPanel + const unsubAgentStart = onAgentStart((projectId) => { + if (projectId !== project.id) return; + // Only auto-start if not already running and no pending prompt + if (contextRefreshPrompt) return; + const startPrompt = project.isAgent + ? 'Start your work. Check your inbox with `btmsg inbox` and review the task board with `bttask board`. Take action on any pending items.' + : 'Review the instructions above and begin your work.'; + contextRefreshPrompt = startPrompt; + }); + + // Listen for stop-button events from GroupAgentsPanel + const unsubAgentStop = onAgentStop((projectId) => { + if (projectId !== project.id) return; + stopAgent(sessionId).catch(() => {}); + }); + // Start timer and clean up startReinjectionTimer(); onDestroy(() => { if (reinjectionTimer) clearInterval(reinjectionTimer); if (wakeCheckTimer) clearInterval(wakeCheckTimer); + unsubAgentStart(); + unsubAgentStop(); }); // Wake scheduler integration — poll for wake events (Manager agents only) diff --git a/v2/src/lib/components/Workspace/CommsTab.svelte b/v2/src/lib/components/Workspace/CommsTab.svelte index 5cb919b..2202134 100644 --- a/v2/src/lib/components/Workspace/CommsTab.svelte +++ b/v2/src/lib/components/Workspace/CommsTab.svelte @@ -1,6 +1,6 @@