add context window monitor

Writes the current context window to logs/current_context_window (uses a symlink to a session ID).

This PR was unfortunately generated by a different LLM and did a ton of superficial reformating, it's actually a fairly small and benign change, but I don't want to roll back everything. Hope that's ok.
This commit is contained in:
Jochen
2025-11-27 21:00:02 +11:00
parent 93dc4acf86
commit 52f78653b4
89 changed files with 4040 additions and 2576 deletions

View File

@@ -1,19 +1,19 @@
use sysinfo::{System, Pid};
use sysinfo::{Pid, System};
fn main() {
let mut sys = System::new_all();
sys.refresh_processes();
println!("Looking for g3 processes...");
for (pid, process) in sys.processes() {
let cmd = process.cmd();
if cmd.is_empty() {
continue;
}
let cmd_str = cmd.join(" ");
// Check if this contains 'g3'
if cmd_str.contains("g3") {
println!("\nFound potential g3 process:");
@@ -21,15 +21,15 @@ fn main() {
println!(" Name: {}", process.name());
println!(" Cmd[0]: {:?}", cmd.get(0));
println!(" Full cmd: {:?}", cmd);
// Check detection logic
let is_g3_binary = cmd.get(0).map(|s| s.ends_with("g3")).unwrap_or(false);
let is_cargo_run = cmd.get(0).map(|s| s.contains("cargo")).unwrap_or(false)
&& cmd.iter().any(|s| s == "run" || s.contains("g3"));
println!(" is_g3_binary: {}", is_g3_binary);
println!(" is_cargo_run: {}", is_cargo_run);
// Check workspace
let has_workspace = cmd.iter().any(|s| s == "--workspace" || s == "-w");
println!(" has_workspace: {}", has_workspace);

View File

@@ -3,13 +3,15 @@ use g3_console::process::ProcessDetector;
fn main() {
let mut detector = ProcessDetector::new();
match detector.detect_instances() {
Ok(instances) => {
println!("Found {} instances:", instances.len());
for instance in instances {
println!(" - PID: {}, Workspace: {:?}, Type: {:?}",
instance.pid, instance.workspace, instance.instance_type);
println!(
" - PID: {}, Workspace: {:?}, Type: {:?}",
instance.pid, instance.workspace, instance.instance_type
);
}
}
Err(e) => {

View File

@@ -1,12 +1,12 @@
use sysinfo::{System, Pid};
use sysinfo::{Pid, System};
fn main() {
let mut sys = System::new_all();
sys.refresh_processes();
// Test with known PIDs
let pids = vec![68123, 72749];
for pid_num in pids {
let pid = Pid::from_u32(pid_num);
if let Some(process) = sys.process(pid) {

View File

@@ -19,7 +19,7 @@ pub async fn kill_instance(
.ok_or(StatusCode::BAD_REQUEST)?;
let mut controller = controller.lock().await;
match controller.kill_process(pid) {
Ok(_) => {
info!("Successfully killed process {}", pid);
@@ -39,35 +39,38 @@ pub async fn restart_instance(
axum::extract::Path(id): axum::extract::Path<String>,
) -> Result<Json<LaunchResponse>, StatusCode> {
info!("Restarting instance: {}", id);
// Extract PID from instance ID (format: pid_timestamp)
let pid: u32 = id
.split('_')
.next()
.and_then(|s| s.parse().ok())
.ok_or(StatusCode::BAD_REQUEST)?;
let mut controller = controller.lock().await;
// Get stored launch params
let params = controller.get_launch_params(pid)
let params = controller
.get_launch_params(pid)
.ok_or(StatusCode::NOT_FOUND)?;
// Launch new instance with same parameters
let new_pid = controller.launch_g3(
params.workspace.to_str().unwrap(),
&params.provider,
&params.model,
&params.prompt,
params.autonomous,
params.g3_binary_path.as_deref(),
).map_err(|e| {
error!("Failed to restart instance: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let new_pid = controller
.launch_g3(
params.workspace.to_str().unwrap(),
&params.provider,
&params.model,
&params.prompt,
params.autonomous,
params.g3_binary_path.as_deref(),
)
.map_err(|e| {
error!("Failed to restart instance: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let new_id = format!("{}_{}", new_pid, chrono::Utc::now().timestamp());
Ok(Json(LaunchResponse {
id: new_id,
status: "starting".to_string(),
@@ -79,7 +82,7 @@ pub async fn launch_instance(
Json(request): Json<LaunchRequest>,
) -> Result<Json<LaunchResponse>, (StatusCode, Json<serde_json::Value>)> {
info!("Launching new g3 instance: {:?}", request);
// Validate binary path if provided
if let Some(ref binary_path) = request.g3_binary_path {
// Expand relative paths and resolve to absolute
@@ -90,16 +93,19 @@ pub async fn launch_instance(
} else {
std::path::PathBuf::from(binary_path)
};
// Check if file exists
if !path.exists() {
error!("G3 binary not found: {}", binary_path);
return Err((StatusCode::BAD_REQUEST, Json(serde_json::json!({
"error": "G3 binary not found",
"message": format!("The specified g3 binary does not exist: {}", binary_path)
}))));
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "G3 binary not found",
"message": format!("The specified g3 binary does not exist: {}", binary_path)
})),
));
}
// Check if file is executable (Unix only)
#[cfg(unix)]
{
@@ -107,26 +113,32 @@ pub async fn launch_instance(
if let Ok(metadata) = std::fs::metadata(path) {
if metadata.permissions().mode() & 0o111 == 0 {
error!("G3 binary is not executable: {}", binary_path);
return Err((StatusCode::BAD_REQUEST, Json(serde_json::json!({
"error": "G3 binary is not executable",
"message": format!("The specified g3 binary is not executable: {}", binary_path)
}))));
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "G3 binary is not executable",
"message": format!("The specified g3 binary is not executable: {}", binary_path)
})),
));
}
}
}
}
let workspace = request.workspace.to_str().ok_or_else(|| {
(StatusCode::BAD_REQUEST, Json(serde_json::json!({
"error": "Invalid workspace path",
"message": "The workspace path contains invalid characters"
})))
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "Invalid workspace path",
"message": "The workspace path contains invalid characters"
})),
)
})?;
let autonomous = request.mode == LaunchMode::Ensemble;
let g3_binary_path = request.g3_binary_path.as_deref();
let mut controller = controller.lock().await;
match controller.launch_g3(
workspace,
&request.provider,
@@ -145,10 +157,13 @@ pub async fn launch_instance(
}
Err(e) => {
error!("Failed to launch g3 instance: {}", e);
Err((StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
"error": "Failed to launch instance",
"message": format!("Error: {}", e)
}))))
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "Failed to launch instance",
"message": format!("Error: {}", e)
})),
))
}
}
}

View File

@@ -1,7 +1,11 @@
use crate::logs::{LogParser, StatsAggregator};
use crate::models::*;
use crate::process::ProcessDetector;
use axum::{extract::{Query, State}, http::StatusCode, Json};
use axum::{
extract::{Query, State},
http::StatusCode,
Json,
};
use serde::Deserialize;
use std::sync::Arc;
use tokio::sync::Mutex;
@@ -13,11 +17,11 @@ pub async fn list_instances(
State(detector): State<AppState>,
) -> Result<Json<Vec<InstanceDetail>>, StatusCode> {
let mut detector = detector.lock().await;
match detector.detect_instances() {
Ok(instances) => {
let mut details = Vec::new();
for instance in instances {
match get_instance_detail(&instance) {
Ok(detail) => details.push(detail),
@@ -27,7 +31,7 @@ pub async fn list_instances(
}
}
}
Ok(Json(details))
}
Err(e) => {
@@ -42,7 +46,7 @@ pub async fn get_instance(
axum::extract::Path(id): axum::extract::Path<String>,
) -> Result<Json<InstanceDetail>, StatusCode> {
let mut detector = detector.lock().await;
match detector.detect_instances() {
Ok(instances) => {
if let Some(instance) = instances.into_iter().find(|i| i.id == id) {
@@ -69,30 +73,36 @@ fn get_instance_detail(instance: &Instance) -> anyhow::Result<InstanceDetail> {
let log_entries = match LogParser::parse_logs(&instance.workspace) {
Ok(entries) => entries,
Err(e) => {
warn!("Failed to parse logs for instance {}: {}. Instance may be newly started.", instance.id, e);
warn!(
"Failed to parse logs for instance {}: {}. Instance may be newly started.",
instance.id, e
);
Vec::new()
}
};
// Aggregate stats
let is_ensemble = instance.instance_type == crate::models::InstanceType::Ensemble;
let stats = StatsAggregator::aggregate_stats(&log_entries, instance.start_time, is_ensemble);
// Get latest message
let latest_message = StatsAggregator::get_latest_message(&log_entries);
// Get git status - don't fail if not a git repo
let git_status = match get_git_status(&instance.workspace) {
Some(status) => Some(status),
None => {
debug!("No git status available for workspace: {:?}", instance.workspace);
debug!(
"No git status available for workspace: {:?}",
instance.workspace
);
None
}
};
// Get project files
let project_files = get_project_files(&instance.workspace);
Ok(InstanceDetail {
instance: instance.clone(),
stats,
@@ -104,7 +114,7 @@ fn get_instance_detail(instance: &Instance) -> anyhow::Result<InstanceDetail> {
fn get_git_status(workspace: &std::path::Path) -> Option<GitStatus> {
use std::process::Command;
// Get current branch
let branch = Command::new("git")
.arg("-C")
@@ -115,7 +125,7 @@ fn get_git_status(workspace: &std::path::Path) -> Option<GitStatus> {
.ok()
.and_then(|output| String::from_utf8(output.stdout).ok())
.map(|s| s.trim().to_string())?;
// Get status
let status_output = Command::new("git")
.arg("-C")
@@ -125,19 +135,19 @@ fn get_git_status(workspace: &std::path::Path) -> Option<GitStatus> {
.output()
.ok()
.and_then(|output| String::from_utf8(output.stdout).ok())?;
let mut modified_files = Vec::new();
let mut added_files = Vec::new();
let mut deleted_files = Vec::new();
for line in status_output.lines() {
if line.len() < 4 {
continue;
}
let status = &line[0..2];
let file = line[3..].trim();
match status.trim() {
"M" | "MM" => modified_files.push(file.to_string()),
"A" | "AM" => added_files.push(file.to_string()),
@@ -145,9 +155,9 @@ fn get_git_status(workspace: &std::path::Path) -> Option<GitStatus> {
_ => modified_files.push(file.to_string()),
}
}
let uncommitted_changes = modified_files.len() + added_files.len() + deleted_files.len();
Some(GitStatus {
branch,
uncommitted_changes,
@@ -161,7 +171,7 @@ fn get_project_files(workspace: &std::path::Path) -> ProjectFiles {
let requirements = read_file_snippet(workspace, "requirements.md");
let readme = read_file_snippet(workspace, "README.md");
let agents = read_file_snippet(workspace, "AGENTS.md");
ProjectFiles {
requirements,
readme,
@@ -171,22 +181,16 @@ fn get_project_files(workspace: &std::path::Path) -> ProjectFiles {
fn read_file_snippet(workspace: &std::path::Path, filename: &str) -> Option<String> {
use std::fs;
let path = workspace.join(filename);
if !path.exists() {
return None;
}
fs::read_to_string(&path)
.ok()
.map(|content| {
// Return first 10 lines
content
.lines()
.take(10)
.collect::<Vec<_>>()
.join("\n")
})
fs::read_to_string(&path).ok().map(|content| {
// Return first 10 lines
content.lines().take(10).collect::<Vec<_>>().join("\n")
})
}
#[derive(Deserialize)]
@@ -200,20 +204,25 @@ pub async fn get_file_content(
State(detector): State<AppState>,
) -> Result<Json<serde_json::Value>, StatusCode> {
let mut detector = detector.lock().await;
// Find the instance
let instances = detector.detect_instances().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let instance = instances.iter().find(|i| i.id == id).ok_or(StatusCode::NOT_FOUND)?;
let instances = detector
.detect_instances()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let instance = instances
.iter()
.find(|i| i.id == id)
.ok_or(StatusCode::NOT_FOUND)?;
// Read the full file
let file_path = instance.workspace.join(&query.name);
if !file_path.exists() {
return Err(StatusCode::NOT_FOUND);
}
let content = std::fs::read_to_string(&file_path)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let content =
std::fs::read_to_string(&file_path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(serde_json::json!({
"name": query.name,
"content": content,

View File

@@ -12,7 +12,7 @@ pub async fn get_instance_logs(
axum::extract::Path(id): axum::extract::Path<String>,
) -> Result<Json<serde_json::Value>, StatusCode> {
let mut detector = detector.lock().await;
match detector.detect_instances() {
Ok(instances) => {
if let Some(instance) = instances.into_iter().find(|i| i.id == id) {
@@ -20,7 +20,7 @@ pub async fn get_instance_logs(
Ok(entries) => {
let messages = LogParser::extract_chat_messages(&entries);
let tool_calls = LogParser::extract_tool_calls(&entries);
Ok(Json(serde_json::json!({
"messages": messages,
"tool_calls": tool_calls,

View File

@@ -1,4 +1,4 @@
pub mod instances;
pub mod control;
pub mod instances;
pub mod logs;
pub mod state;

View File

@@ -1,8 +1,8 @@
use crate::launch::ConsoleState;
use axum::{http::StatusCode, Json};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use tracing::{error, info};
pub async fn get_state() -> Result<Json<ConsoleState>, StatusCode> {
@@ -52,24 +52,26 @@ pub async fn browse_filesystem(
Json(request): Json<BrowseRequest>,
) -> Result<Json<BrowseResponse>, StatusCode> {
use std::fs;
let path = if let Some(p) = request.path {
PathBuf::from(p)
} else {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
};
let current_path = path.canonicalize()
let current_path = path
.canonicalize()
.map_err(|_| StatusCode::BAD_REQUEST)?
.to_string_lossy()
.to_string();
let parent_path = path.parent()
let parent_path = path
.parent()
.and_then(|p| p.to_str())
.map(|s| s.to_string());
let mut entries = Vec::new();
if let Ok(read_dir) = fs::read_dir(&path) {
for entry in read_dir.flatten() {
if let Ok(metadata) = entry.metadata() {
@@ -82,15 +84,13 @@ pub async fn browse_filesystem(
}
}
}
entries.sort_by(|a, b| {
match (a.is_dir, b.is_dir) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.cmp(&b.name),
}
entries.sort_by(|a, b| match (a.is_dir, b.is_dir) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.cmp(&b.name),
});
Ok(Json(BrowseResponse {
current_path,
parent_path,

View File

@@ -27,7 +27,7 @@ impl Default for ConsoleState {
impl ConsoleState {
pub fn load() -> Self {
let config_path = Self::config_path();
if config_path.exists() {
if let Ok(content) = fs::read_to_string(&config_path) {
return serde_json::from_str(&content).unwrap_or_else(|e| {
@@ -36,31 +36,29 @@ impl ConsoleState {
});
}
}
Self::default()
}
pub fn save(&self) -> anyhow::Result<()> {
let config_path = Self::config_path();
info!("Saving console state to: {:?}", config_path);
// Create parent directory if it doesn't exist
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)?;
}
let content = serde_json::to_string_pretty(self)?;
fs::write(&config_path, content)?;
info!("Console state saved successfully to: {:?}", config_path);
Ok(())
}
fn config_path() -> PathBuf {
// Use explicit ~/.config/g3/console.json path as per requirements
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
home.join(".config")
.join("g3")
.join("console.json")
home.join(".config").join("g3").join("console.json")
}
}

View File

@@ -1,5 +1,5 @@
pub mod api;
pub mod launch;
pub mod logs;
pub mod models;
pub mod process;
pub mod launch;

View File

@@ -36,7 +36,7 @@ impl LogParser {
/// Parse logs from a workspace directory
pub fn parse_logs(workspace: &Path) -> Result<Vec<LogEntry>> {
let logs_dir = workspace.join("logs");
if !logs_dir.exists() {
return Ok(Vec::new());
}
@@ -47,7 +47,7 @@ impl LogParser {
for entry in fs::read_dir(&logs_dir).context("Failed to read logs directory")? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
if let Ok(content) = fs::read_to_string(&path) {
if let Ok(json) = serde_json::from_str::<Value>(&content) {
@@ -55,17 +55,21 @@ impl LogParser {
if let Some(messages) = json.get("messages").and_then(|m| m.as_array()) {
for msg in messages {
entries.push(LogEntry {
timestamp: msg.get("timestamp")
timestamp: msg
.get("timestamp")
.and_then(|t| t.as_str())
.and_then(|s| DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&Utc)),
role: msg.get("role")
role: msg
.get("role")
.and_then(|r| r.as_str())
.map(String::from),
content: msg.get("content")
content: msg
.get("content")
.and_then(|c| c.as_str())
.map(String::from),
tool_calls: msg.get("tool_calls")
tool_calls: msg
.get("tool_calls")
.and_then(|tc| tc.as_array())
.map(|arr| arr.clone()),
raw: msg.clone(),
@@ -78,13 +82,11 @@ impl LogParser {
}
// Sort by timestamp
entries.sort_by(|a, b| {
match (&a.timestamp, &b.timestamp) {
(Some(t1), Some(t2)) => t1.cmp(t2),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
}
entries.sort_by(|a, b| match (&a.timestamp, &b.timestamp) {
(Some(t1), Some(t2)) => t1.cmp(t2),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
});
Ok(entries)
@@ -97,7 +99,7 @@ impl LogParser {
.filter_map(|entry| {
let role = entry.role.clone()?;
let content = entry.content.clone()?;
Some(ChatMessage {
role,
content,
@@ -117,10 +119,12 @@ impl LogParser {
if let Some(name) = call.get("name").and_then(|n| n.as_str()) {
tool_calls.push(ToolCall {
name: name.to_string(),
parameters: call.get("parameters")
parameters: call
.get("parameters")
.cloned()
.unwrap_or(Value::Object(serde_json::Map::new())),
result: call.get("result")
result: call
.get("result")
.and_then(|r| r.as_str())
.map(String::from),
timestamp: entry.timestamp,
@@ -146,7 +150,7 @@ impl StatsAggregator {
let total_tokens = Self::count_tokens(entries);
let tool_calls = Self::count_tool_calls(entries);
let errors = Self::count_errors(entries);
let duration_secs = if let Some(last_entry) = entries.last() {
if let Some(last_time) = last_entry.timestamp {
(last_time - start_time).num_seconds().max(0) as u64
@@ -193,7 +197,9 @@ impl StatsAggregator {
entries
.iter()
.filter_map(|entry| {
entry.raw.get("usage")
entry
.raw
.get("usage")
.and_then(|u| u.get("total_tokens"))
.and_then(|t| t.as_u64())
})
@@ -213,7 +219,11 @@ impl StatsAggregator {
.iter()
.filter(|entry| {
entry.raw.get("error").is_some()
|| entry.content.as_ref().map(|c| c.to_lowercase().contains("error")).unwrap_or(false)
|| entry
.content
.as_ref()
.map(|c| c.to_lowercase().contains("error"))
.unwrap_or(false)
})
.count() as u64
}

View File

@@ -1,11 +1,11 @@
use g3_console::api;
use g3_console::process;
use g3_console::launch;
use g3_console::process;
use api::control::{kill_instance, launch_instance, restart_instance};
use api::instances::{get_instance, get_file_content, list_instances};
use api::instances::{get_file_content, get_instance, list_instances};
use api::logs::get_instance_logs;
use api::state::{get_state, save_state, browse_filesystem};
use api::state::{browse_filesystem, get_state, save_state};
use axum::{
routing::{get, post},
Router,
@@ -39,9 +39,7 @@ struct Args {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Initialize tracing
tracing_subscriber::fmt()
.with_max_level(Level::INFO)
.init();
tracing_subscriber::fmt().with_max_level(Level::INFO).init();
let args = Args::parse();

View File

@@ -1,6 +1,6 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Instance {

View File

@@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {

View File

@@ -1,12 +1,12 @@
use anyhow::{anyhow, Context, Result};
use std::process::{Command, Stdio};
use std::os::unix::process::CommandExt;
use std::collections::HashMap;
use std::sync::Mutex;
use std::path::PathBuf;
use sysinfo::{Pid, Signal, System, Process};
use tracing::{debug, info};
use crate::models::LaunchParams;
use anyhow::{anyhow, Context, Result};
use std::collections::HashMap;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::Mutex;
use sysinfo::{Pid, Process, Signal, System};
use tracing::{debug, info};
pub struct ProcessController {
system: System,
@@ -27,15 +27,15 @@ impl ProcessController {
if let Some(process) = self.system.process(sysinfo_pid) {
info!("Killing process {} ({})", pid, process.name());
// Try SIGTERM first
if process.kill_with(Signal::Term).is_some() {
debug!("Sent SIGTERM to process {}", pid);
// Wait a bit and check if it's still running
std::thread::sleep(std::time::Duration::from_secs(2));
self.system.refresh_processes();
if self.system.process(sysinfo_pid).is_some() {
// Still running, send SIGKILL
if let Some(proc) = self.system.process(sysinfo_pid) {
@@ -43,7 +43,7 @@ impl ProcessController {
debug!("Sent SIGKILL to process {}", pid);
}
}
Ok(())
} else {
Err(anyhow!("Failed to send signal to process {}", pid))
@@ -64,7 +64,7 @@ impl ProcessController {
g3_binary_path: Option<&str>,
) -> Result<u32> {
let binary = g3_binary_path.unwrap_or("g3");
let mut cmd = Command::new(binary);
cmd.arg("--workspace")
.arg(workspace)
@@ -108,36 +108,41 @@ impl ProcessController {
}
info!("Launching g3: {:?}", cmd);
// Spawn and wait for the intermediate process to exit
let mut child = cmd.spawn().context("Failed to spawn g3 process")?;
let intermediate_pid = child.id();
// Wait for intermediate process (it will exit immediately after forking)
child.wait().context("Failed to wait for intermediate process")?;
child
.wait()
.context("Failed to wait for intermediate process")?;
// The actual g3 process is now running as orphan
// We need to scan for it by matching workspace and recent start time
info!("Scanning for newly launched g3 process in workspace: {}", workspace);
info!(
"Scanning for newly launched g3 process in workspace: {}",
workspace
);
// Wait even longer for the process to fully start and appear in process list
std::thread::sleep(std::time::Duration::from_millis(2500));
// Refresh and scan for the process
self.system.refresh_processes();
let workspace_path = PathBuf::from(workspace);
let mut found_pid = None;
for (pid, process) in self.system.processes() {
let cmd = process.cmd();
let cmd_str = cmd.join(" ");
// Check if this is a g3 process
let is_g3 = process.name().contains("g3") || cmd_str.contains("g3");
if !is_g3 {
continue;
}
// Check if it has our workspace
let has_workspace = cmd.iter().any(|arg| {
if let Ok(path) = PathBuf::from(arg).canonicalize() {
@@ -147,11 +152,12 @@ impl ProcessController {
}
false
});
if has_workspace {
// Check if it's recent (started within last 10 seconds)
let now = std::time::SystemTime::now();
let start_time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(process.start_time());
let start_time =
std::time::UNIX_EPOCH + std::time::Duration::from_secs(process.start_time());
if let Ok(duration) = now.duration_since(start_time) {
if duration.as_secs() < 10 {
found_pid = Some(pid.as_u32());
@@ -160,7 +166,7 @@ impl ProcessController {
}
}
}
let pid = if let Some(found) = found_pid {
found
} else {
@@ -168,18 +174,18 @@ impl ProcessController {
info!("Process not found on first scan, trying again...");
std::thread::sleep(std::time::Duration::from_millis(2000));
self.system.refresh_processes();
// Try the scan again with full logic
let mut retry_found = None;
for (pid, process) in self.system.processes() {
let cmd = process.cmd();
let cmd_str = cmd.join(" ");
let is_g3 = process.name().contains("g3") || cmd_str.contains("g3");
if !is_g3 {
continue;
}
let has_workspace = cmd.iter().any(|arg| {
if let Ok(path) = PathBuf::from(arg).canonicalize() {
if let Ok(ws) = workspace_path.canonicalize() {
@@ -188,18 +194,18 @@ impl ProcessController {
}
false
});
if has_workspace {
retry_found = Some(pid.as_u32());
break;
}
}
retry_found.unwrap_or(intermediate_pid)
};
info!("Launched g3 process with PID {}", pid);
// Store launch params for restart
let params = LaunchParams {
workspace: workspace.into(),
@@ -209,14 +215,14 @@ impl ProcessController {
autonomous,
g3_binary_path: g3_binary_path.map(|s| s.to_string()),
};
if let Ok(mut map) = self.launch_params.lock() {
map.insert(pid, params);
}
Ok(pid)
}
pub fn get_launch_params(&mut self, pid: u32) -> Option<LaunchParams> {
// First check if we have stored params (for console-launched instances)
if let Ok(map) = self.launch_params.lock() {
@@ -224,19 +230,19 @@ impl ProcessController {
return Some(params.clone());
}
}
// If not found, try to parse from process command line (for detected instances)
self.system.refresh_processes();
let sysinfo_pid = Pid::from_u32(pid);
if let Some(process) = self.system.process(sysinfo_pid) {
let cmd = process.cmd();
return self.parse_launch_params_from_cmd(cmd);
}
None
}
fn parse_launch_params_from_cmd(&self, cmd: &[String]) -> Option<LaunchParams> {
let mut workspace = None;
let mut provider = None;
@@ -244,7 +250,7 @@ impl ProcessController {
let mut prompt = None;
let mut autonomous = false;
let mut g3_binary_path = None;
let mut i = 0;
while i < cmd.len() {
match cmd[i].as_str() {
@@ -273,7 +279,7 @@ impl ProcessController {
}
}
}
// Try to determine binary path from cmd[0]
if !cmd.is_empty() {
let first = &cmd[0];
@@ -281,9 +287,10 @@ impl ProcessController {
g3_binary_path = Some(first.clone());
}
}
// Only return params if we have the minimum required fields
if let (Some(ws), Some(prov), Some(mdl), Some(prmt)) = (workspace, provider, model, prompt) {
if let (Some(ws), Some(prov), Some(mdl), Some(prmt)) = (workspace, provider, model, prompt)
{
Some(LaunchParams {
workspace: ws,
provider: prov,

View File

@@ -2,7 +2,7 @@ use crate::models::{ExecutionMethod, Instance, InstanceStatus, InstanceType};
use anyhow::Result;
use chrono::{DateTime, Utc};
use std::path::PathBuf;
use sysinfo::{System, Pid, Process};
use sysinfo::{Pid, Process, System};
use tracing::{debug, info, warn};
pub struct ProcessDetector {
@@ -41,36 +41,37 @@ impl ProcessDetector {
Ok(instances)
}
fn parse_g3_process(
&self,
pid: Pid,
process: &Process,
cmd: &[String],
) -> Option<Instance> {
fn parse_g3_process(&self, pid: Pid, process: &Process, cmd: &[String]) -> Option<Instance> {
let cmd_str = cmd.join(" ");
// Exclude g3-console itself
if cmd_str.contains("g3-console") {
return None;
}
// Check if this is a g3 binary (more comprehensive check)
let is_g3_binary = cmd.get(0).map(|s| {
(s.ends_with("g3") || s.ends_with("/g3") || s.contains("/target/release/g3") || s.contains("/target/debug/g3"))
&& !s.contains("g3-") // Exclude other g3-* binaries
}).unwrap_or(false);
let is_g3_binary = cmd
.get(0)
.map(|s| {
(s.ends_with("g3")
|| s.ends_with("/g3")
|| s.contains("/target/release/g3")
|| s.contains("/target/debug/g3"))
&& !s.contains("g3-") // Exclude other g3-* binaries
})
.unwrap_or(false);
// Check if this is cargo run with g3 (not g3-console or other variants)
let is_cargo_run = cmd.get(0).map(|s| s.contains("cargo")).unwrap_or(false)
let is_cargo_run = cmd.get(0).map(|s| s.contains("cargo")).unwrap_or(false)
&& cmd.iter().any(|s| s == "run")
&& !cmd_str.contains("g3-console");
// Also check if command line has g3-specific flags
let has_g3_flags = cmd_str.contains("--workspace") || cmd_str.contains("--autonomous");
// Accept if it's a g3 binary or cargo run with g3, and has typical g3 patterns
let is_g3_process = is_g3_binary || (is_cargo_run && has_g3_flags);
if !is_g3_process {
return None;
}
@@ -97,8 +98,8 @@ impl ProcessDetector {
let model = self.extract_flag_value(cmd, "--model");
// Get start time
let start_time = DateTime::from_timestamp(process.start_time() as i64, 0)
.unwrap_or_else(Utc::now);
let start_time =
DateTime::from_timestamp(process.start_time() as i64, 0).unwrap_or_else(Utc::now);
// Generate instance ID from PID and start time
let id = format!("{}_{}", pid, start_time.timestamp());
@@ -139,7 +140,7 @@ impl ProcessDetector {
return Some(cwd);
}
}
#[cfg(target_os = "macos")]
{
// On macOS, use lsof to get the current working directory
@@ -156,9 +157,12 @@ impl ProcessDetector {
}
}
}
// Final fallback: use current directory of console
warn!("Could not determine workspace for PID {}, using current directory", pid);
warn!(
"Could not determine workspace for PID {}, using current directory",
pid
);
std::env::current_dir().ok()
}
@@ -173,7 +177,7 @@ impl ProcessDetector {
pub fn get_process_status(&mut self, pid: u32) -> Option<InstanceStatus> {
self.system.refresh_all();
let sysinfo_pid = Pid::from_u32(pid);
if self.system.process(sysinfo_pid).is_some() {
Some(InstanceStatus::Running)

View File

@@ -1,5 +1,5 @@
pub mod detector;
pub mod controller;
pub mod detector;
pub use detector::*;
pub use controller::*;
pub use detector::*;