Agent bug fixes

This commit is contained in:
MrFadiAi
2026-02-19 02:02:09 +01:00
parent 97b826aa1e
commit ffb0863dc5
5 changed files with 316 additions and 146 deletions
+62 -75
View File
@@ -2488,89 +2488,76 @@ pub async fn save_agent(agent: AgentInfo) -> Result<String, String> {
Vec::new()
};
// Ensure agent directory exists and has basic files (SOUL.md)
// Resolving workspace path: use provided workspace OR openclaw_home/agent.agent_dir OR openclaw_home/agents/agent.id
let openclaw_home = platform::get_config_dir();
let workspace_path = if let Some(ws) = &agent.workspace {
ws.clone()
} else if let Some(adir) = &agent.agent_dir {
// agent_dir might be relative to openclaw_home
if std::path::Path::new(adir).is_absolute() {
adir.clone()
// For NEW agents: use `openclaw agents add <id> --workspace <dir>` to create proper directory structure
// The --workspace flag is required to make the CLI non-interactive
let is_new_agent = !list.iter().any(|a| a.get("id").and_then(|v| v.as_str()) == Some(&agent.id));
if is_new_agent {
let openclaw_home = platform::get_config_dir();
let workspace_dir = if let Some(ws) = &agent.workspace {
ws.clone()
} else if agent.default == Some(true) {
std::path::Path::new(&openclaw_home).join("workspace").to_string_lossy().to_string()
} else {
let path = std::path::Path::new(&openclaw_home).join(adir);
path.to_string_lossy().to_string()
}
} else {
// Default: ~/.openclaw/workspace for main, ~/.openclaw/workspace-{id} for others
let path = if agent.id == "main" {
std::path::Path::new(&openclaw_home).join("workspace")
} else {
std::path::Path::new(&openclaw_home).join(format!("workspace-{}", agent.id))
std::path::Path::new(&openclaw_home).join(format!("workspace-{}", agent.id)).to_string_lossy().to_string()
};
path.to_string_lossy().to_string()
};
// Also ensure agentDir directory exists: ~/.openclaw/agents/{id}/agent
let agent_dir_path = std::path::Path::new(&openclaw_home).join("agents").join(&agent.id).join("agent");
if !agent_dir_path.exists() {
info!("[Agents] Creating agent dir: {}", agent_dir_path.display());
let _ = std::fs::create_dir_all(&agent_dir_path);
}
// Also create sessions directory
let sessions_dir = std::path::Path::new(&openclaw_home).join("agents").join(&agent.id).join("sessions");
if !sessions_dir.exists() {
let _ = std::fs::create_dir_all(&sessions_dir);
}
// Auto-create directory
if !file::file_exists(&workspace_path) {
info!("[Agents] Creating agent workspace: {}", workspace_path);
if let Err(e) = std::fs::create_dir_all(&workspace_path) {
error!("[Agents] Failed to create workspace directory: {}", e);
}
}
// Auto-create SOUL.md if missing
let soul_path = std::path::Path::new(&workspace_path).join("SOUL.md");
if !soul_path.exists() {
info!("[Agents] SOUL.md missing, creating default for agent: {}", agent.id);
// Try to copy from root SOUL.md first
let root_soul = std::path::Path::new(&openclaw_home).join("SOUL.md");
if root_soul.exists() {
if let Err(e) = std::fs::copy(&root_soul, &soul_path) {
warn!("[Agents] Failed to copy root SOUL.md: {}", e);
// Fallback to default content
let default_soul = format!("# Identity for {}\n\nYou are an AI agent named {}.", agent.id, agent.id);
let _ = std::fs::write(&soul_path, default_soul);
}
} else {
// Write default content
let default_soul = format!("# Identity for {}\n\nYou are an AI agent named {}.", agent.id, agent.id);
if let Err(e) = std::fs::write(&soul_path, default_soul) {
error!("[Agents] Failed to write default SOUL.md: {}", e);
info!("[Agents] New agent '{}' — running `openclaw agents add --workspace {}`", agent.id, workspace_dir);
match shell::run_openclaw(&["agents", "add", &agent.id, "--workspace", &workspace_dir]) {
Ok(output) => {
info!("[Agents] openclaw agents add succeeded: {}", output);
}
Err(e) => {
// NOTE: The CLI may exit with code 1 due to TUI stdin issues in non-interactive mode,
// but it still writes the agent entry to openclaw.json successfully.
warn!("[Agents] openclaw agents add exited with error (may still have written config): {}", e);
}
}
// CRITICAL: Always reload config after CLI runs — it may have written the entry
// even if exit code was non-zero (TUI library issue in non-interactive mode)
config = load_openclaw_config()?;
list = if let Some(arr) = config["agents"].get("list").and_then(|v| v.as_array()) {
arr.clone()
} else if let Some(obj) = config["agents"].get("list").and_then(|v| v.as_object()) {
obj.iter().map(|(id, val)| {
let mut entry = val.clone();
entry["id"] = json!(id);
entry
}).collect()
} else {
Vec::new()
};
}
// Update config object with resolving defaults if needed (e.g. if we auto-created, maybe we should save the path?)
// Creating the object again to include updates if any
let mut final_agent_obj = agent_obj.clone();
// If workspace was not explicit in input but we determined it, should we save it?
// OpenClaw Core conventions:
// - if 'workspace' is set, it uses that absolute path.
// - if 'agentDir' is set, it uses ~/.openclaw/{agentDir}
// - if neither, it defaults to ~/.openclaw/agents/{id}
// Changing the logic: We won't force 'workspace' into the config if it wasn't there,
// to rely on Core's default behavior, but we ensured the directory exists.
// Update or add the agent
// Update or add the agent — for CLI-created entries, merge our fields into existing entry
if let Some(existing) = list.iter_mut().find(|a| a.get("id").and_then(|v| v.as_str()) == Some(&agent.id)) {
*existing = agent_obj;
// Merge: only overwrite fields the user explicitly set (non-empty)
if let Some(model) = &agent.model {
if !model.is_empty() {
existing["model"] = json!({ "primary": model });
}
}
if let Some(is_default) = agent.default {
if is_default {
existing["default"] = json!(true);
}
}
if let Some(sub) = &agent.subagents {
if let Some(allow) = &sub.allow_agents {
if !allow.is_empty() {
existing["subagents"] = json!({ "allowAgents": allow });
}
}
}
if let Some(sandbox) = agent.sandbox {
existing["sandbox"] = json!(sandbox);
}
if let Some(heartbeat) = &agent.heartbeat {
if !heartbeat.is_empty() {
existing["heartbeat"] = json!({ "every": heartbeat });
}
}
} else {
list.push(agent_obj);
}
+196 -1
View File
@@ -20,6 +20,8 @@ pub struct EnvironmentStatus {
pub openclaw_installed: bool,
/// OpenClaw version
pub openclaw_version: Option<String>,
/// Whether gateway service is installed
pub gateway_service_installed: bool,
/// Whether config directory exists
pub config_dir_exists: bool,
/// Whether everything is ready
@@ -75,12 +77,22 @@ pub async fn check_environment() -> Result<EnvironmentStatus, String> {
info!("[Environment Check] OpenClaw: installed={}, version={:?}",
openclaw_installed, openclaw_version);
// Check Gateway Service (only if OpenClaw is installed)
let gateway_service_installed = if openclaw_installed {
info!("[Environment Check] Checking Gateway Service...");
let installed = check_gateway_installed();
info!("[Environment Check] Gateway Service: installed={}", installed);
installed
} else {
false
};
// Check config directory
let config_dir = platform::get_config_dir();
let config_dir_exists = std::path::Path::new(&config_dir).exists();
info!("[Environment Check] Config directory: {}, exists={}", config_dir, config_dir_exists);
let ready = node_installed && node_version_ok && openclaw_installed;
let ready = node_installed && node_version_ok && openclaw_installed && gateway_service_installed;
info!("[Environment Check] Environment ready status: ready={}", ready);
Ok(EnvironmentStatus {
@@ -91,6 +103,7 @@ pub async fn check_environment() -> Result<EnvironmentStatus, String> {
git_version,
openclaw_installed,
openclaw_version,
gateway_service_installed,
config_dir_exists,
ready,
os,
@@ -321,6 +334,188 @@ fn check_node_version_requirement(version: &Option<String>) -> bool {
}
}
/// Check if gateway service is installed
fn check_gateway_installed() -> bool {
match shell::run_openclaw(&["gateway", "status"]) {
Ok(output) => {
let lower = output.to_lowercase();
// If output contains "not installed" or "not found", it's not installed
if lower.contains("not installed") || lower.contains("not found") {
return false;
}
// If the command succeeded, consider it installed
true
}
Err(e) => {
let lower = e.to_lowercase();
// Some versions return error when not installed
if lower.contains("not installed") || lower.contains("not found") {
return false;
}
// If the command itself failed (e.g. openclaw not found), not installed
debug!("[Environment Check] Gateway status check failed: {}", e);
false
}
}
}
/// Install gateway service (opens elevated terminal)
#[command]
pub async fn install_gateway_service() -> Result<String, String> {
info!("[Gateway Install] Starting gateway service installation...");
let os = platform::get_os();
info!("[Gateway Install] Detected operating system: {}", os);
match os.as_str() {
"windows" => install_gateway_windows().await,
"macos" => install_gateway_macos().await,
"linux" => install_gateway_linux().await,
_ => Err(format!("Unsupported operating system: {}", os)),
}
}
/// Install gateway service on Windows (elevated PowerShell)
async fn install_gateway_windows() -> Result<String, String> {
info!("[Gateway Install] Opening elevated PowerShell for gateway install...");
// Find openclaw path to use in the script
let openclaw_path = shell::get_openclaw_path().unwrap_or_else(|| "openclaw".to_string());
let escaped_path = openclaw_path.replace('\\', "\\\\");
let script = format!(r#"
Start-Process powershell -ArgumentList '-NoExit', '-Command', '
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " OpenClaw Gateway Service Installer" -ForegroundColor White
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Installing OpenClaw Gateway as a system service..." -ForegroundColor Yellow
Write-Host ""
try {{
& "{}" gateway install
Write-Host ""
Write-Host "Gateway service installed successfully!" -ForegroundColor Green
}} catch {{
Write-Host "Installation failed: $_" -ForegroundColor Red
}}
Write-Host ""
Write-Host "You can close this window and click Refresh in OpenClaw Manager." -ForegroundColor Cyan
Write-Host ""
Read-Host "Press Enter to close this window"
' -Verb RunAs
"#, escaped_path);
match shell::run_powershell_output(&script) {
Ok(_) => {
info!("[Gateway Install] Elevated terminal launched successfully");
Ok("Gateway install terminal opened with administrator privileges. Please complete the installation and click Refresh.".to_string())
}
Err(e) => {
warn!("[Gateway Install] Failed to launch elevated terminal: {}", e);
Err(format!("Failed to open administrator terminal: {}. Please open PowerShell as Administrator and run: openclaw gateway install", e))
}
}
}
/// Install gateway service on macOS (Terminal with sudo)
async fn install_gateway_macos() -> Result<String, String> {
info!("[Gateway Install] Opening terminal for gateway install on macOS...");
let script_content = r#"#!/bin/bash
clear
echo "========================================"
echo " OpenClaw Gateway Service Installer"
echo "========================================"
echo ""
echo "Installing OpenClaw Gateway as a system service..."
echo "You may be prompted for your password."
echo ""
sudo openclaw gateway install
echo ""
if [ $? -eq 0 ]; then
echo "✅ Gateway service installed successfully!"
else
echo "❌ Installation failed. Please check the error above."
fi
echo ""
echo "You can close this window and click Refresh in OpenClaw Manager."
read -p "Press Enter to close this window..."
"#;
let script_path = "/tmp/openclaw_gateway_install.command";
std::fs::write(script_path, script_content)
.map_err(|e| format!("Failed to create script: {}", e))?;
std::process::Command::new("chmod")
.args(["+x", script_path])
.output()
.map_err(|e| format!("Failed to set permissions: {}", e))?;
std::process::Command::new("open")
.arg(script_path)
.spawn()
.map_err(|e| format!("Failed to launch terminal: {}", e))?;
info!("[Gateway Install] Terminal launched successfully on macOS");
Ok("Gateway install terminal opened. Please enter your password when prompted and click Refresh after completion.".to_string())
}
/// Install gateway service on Linux (terminal with sudo)
async fn install_gateway_linux() -> Result<String, String> {
info!("[Gateway Install] Opening terminal for gateway install on Linux...");
let script_content = r#"#!/bin/bash
clear
echo "========================================"
echo " OpenClaw Gateway Service Installer"
echo "========================================"
echo ""
echo "Installing OpenClaw Gateway as a system service..."
echo "You may be prompted for your password."
echo ""
sudo openclaw gateway install
echo ""
if [ $? -eq 0 ]; then
echo "✅ Gateway service installed successfully!"
else
echo "❌ Installation failed. Please check the error above."
fi
echo ""
echo "You can close this window and click Refresh in OpenClaw Manager."
read -p "Press Enter to close this window..."
"#;
let script_path = "/tmp/openclaw_gateway_install.sh";
std::fs::write(script_path, script_content)
.map_err(|e| format!("Failed to create script: {}", e))?;
std::process::Command::new("chmod")
.args(["+x", script_path])
.output()
.map_err(|e| format!("Failed to set permissions: {}", e))?;
// Try different terminal emulators
let terminals = ["gnome-terminal", "xfce4-terminal", "konsole", "xterm"];
for term in terminals {
if std::process::Command::new(term)
.args(["--", script_path])
.spawn()
.is_ok()
{
info!("[Gateway Install] Terminal '{}' launched successfully on Linux", term);
return Ok("Gateway install terminal opened. Please enter your password when prompted and click Refresh after completion.".to_string());
}
}
warn!("[Gateway Install] No terminal emulator found on Linux");
Err("Unable to launch terminal. Please open a terminal and run: sudo openclaw gateway install".to_string())
}
/// Install Node.js
#[command]
pub async fn install_nodejs() -> Result<InstallResult, String> {
+1
View File
@@ -91,6 +91,7 @@ fn main() {
installer::init_openclaw_config,
installer::open_install_terminal,
installer::uninstall_openclaw,
installer::install_gateway_service,
// Version update
installer::check_openclaw_update,
installer::update_openclaw,
+31 -70
View File
@@ -140,7 +140,6 @@ export function Agents() {
const [wizardForm, setWizardForm] = useState({
botAccountId: '',
agentId: '',
systemPrompt: '',
model: '',
isDefault: false,
});
@@ -285,24 +284,12 @@ export function Agents() {
const handleWizardSubmit = async () => {
setSaving(true);
try {
// 1. Save agent
// Auto-configure workspace path based on Default Agent toggle
const agentWorkspace = openclawHomeDir
? (wizardForm.isDefault
? `${openclawHomeDir}/workspace`
: `${openclawHomeDir}/workspace-${wizardForm.agentId}`
).replace(/\\/g, '/')
: null;
// Agent directory: agents/{id}/agent
const agentDirPath = openclawHomeDir
? `${openclawHomeDir}/agents/${wizardForm.agentId}/agent`.replace(/\\/g, '/')
: null;
// Save agent — backend will run `openclaw agents add` for proper structure
const agent: AgentInfo = {
id: wizardForm.agentId,
name: null,
workspace: agentWorkspace,
agent_dir: agentDirPath,
workspace: null,
agent_dir: null,
model: wizardForm.model || null,
sandbox: null,
heartbeat: null,
@@ -311,21 +298,11 @@ export function Agents() {
};
await invoke('save_agent', { agent });
// 2. Save system prompt if provided
if (wizardForm.systemPrompt.trim()) {
await invoke('save_agent_system_prompt', {
agentId: wizardForm.agentId,
workspace: openclawHomeDir || null,
content: wizardForm.systemPrompt
});
}
// Note: save_agent auto-creates binding if matching account exists
setShowWizardDialog(false);
setWizardStep(0);
setWizardStep(0);
setWizardForm({ botAccountId: '', agentId: '', systemPrompt: '', model: '', isDefault: false });
setWizardForm({ botAccountId: '', agentId: '', model: '', isDefault: false });
fetchData();
} catch (e) {
setError(String(e));
@@ -420,7 +397,6 @@ export function Agents() {
setWizardForm({
botAccountId: telegramAccounts[0]?.id || '',
agentId: '',
systemPrompt: '',
model: '',
isDefault: false,
});
@@ -820,27 +796,29 @@ export function Agents() {
/>
</div>
{/* System Prompt Editor (actually SOUL.md in OpenClaw) */}
<div>
<label className="block text-sm text-gray-400 mb-1 flex items-center gap-2">
<FileText size={14} />
Personality (SOUL.md)
</label>
{loadingPrompt ? (
<div className="flex items-center gap-2 text-gray-500 text-sm py-4">
<Loader2 className="animate-spin" size={14} /> Loading...
</div>
) : (
<textarea
value={systemPrompt}
onChange={e => setSystemPrompt(e.target.value)}
className="input-base font-mono text-sm"
rows={6}
placeholder="You are a coding expert specializing in..."
/>
)}
<p className="text-xs text-gray-500 mt-1">Defines the agent's personality. Saved to <code className="text-gray-400">{agentForm.workspace || `workspace-${agentForm.id || '...'}`}/SOUL.md</code></p>
</div>
{/* System Prompt Editor — only shown when editing existing agents */}
{editingAgent && (
<div>
<label className="block text-sm text-gray-400 mb-1 flex items-center gap-2">
<FileText size={14} />
Personality (SOUL.md)
</label>
{loadingPrompt ? (
<div className="flex items-center gap-2 text-gray-500 text-sm py-4">
<Loader2 className="animate-spin" size={14} /> Loading...
</div>
) : (
<textarea
value={systemPrompt}
onChange={e => setSystemPrompt(e.target.value)}
className="input-base font-mono text-sm"
rows={6}
placeholder="You are a coding expert specializing in..."
/>
)}
<p className="text-xs text-gray-500 mt-1">Defines the agent's personality. Saved to <code className="text-gray-400">{agentForm.workspace || `workspace-${agentForm.id || '...'}`}/SOUL.md</code></p>
</div>
)}
<div className="flex items-center gap-2 pt-2">
<input
@@ -1017,7 +995,7 @@ export function Agents() {
{/* Step indicators */}
<div className="px-6 pt-4 flex gap-2">
{['Bot Account', 'Agent Config', 'Personality'].map((label, i) => (
{['Bot Account', 'Agent Config'].map((label, i) => (
<div key={i} className="flex-1">
<div className={`h-1 rounded-full transition-colors ${i <= wizardStep ? 'bg-claw-500' : 'bg-dark-600'}`} />
<p className={`text-xs mt-1 ${i <= wizardStep ? 'text-claw-400' : 'text-gray-600'}`}>{label}</p>
@@ -1086,24 +1064,7 @@ export function Agents() {
</div>
)}
{wizardStep === 2 && (
<div className="space-y-4">
<p className="text-sm text-gray-300">Define the personality for <span className="text-claw-400 font-medium">{wizardForm.agentId}</span>.</p>
<div>
<label className="block text-sm text-gray-400 mb-1 flex items-center gap-2">
<FileText size={14} />
Personality (SOUL.md)
</label>
<textarea
value={wizardForm.systemPrompt}
onChange={e => setWizardForm({ ...wizardForm, systemPrompt: e.target.value })}
className="input-base font-mono text-sm"
rows={6}
placeholder="You are a coding expert. You help users write clean, efficient code..."
/>
</div>
</div>
)}
</div>
<div className="px-6 py-4 border-t border-dark-600 flex justify-between">
@@ -1113,10 +1074,10 @@ export function Agents() {
>
{wizardStep === 0 ? 'Cancel' : 'Back'}
</button>
{wizardStep < 2 ? (
{wizardStep < 1 ? (
<button
onClick={() => setWizardStep(wizardStep + 1)}
disabled={wizardStep === 0 ? !wizardForm.botAccountId : !wizardForm.agentId}
disabled={!wizardForm.botAccountId}
className="btn-primary flex items-center gap-2"
>
Next
+26
View File
@@ -11,6 +11,7 @@ import {
Package,
Shield,
RefreshCw,
Server,
} from 'lucide-react';
import { isTauri } from '../../lib/tauri';
@@ -22,6 +23,7 @@ interface EnvironmentStatus {
git_version: string | null;
openclaw_installed: boolean;
openclaw_version: string | null;
gateway_service_installed: boolean;
config_dir_exists: boolean;
ready: boolean;
os: string;
@@ -115,6 +117,20 @@ export function SystemInfo() {
}
};
const handleInstallGateway = async () => {
setInstalling('gateway');
setError(null);
try {
await invoke<string>('install_gateway_service');
// Gateway install opens an elevated terminal — user needs to complete it there
// Don't auto-refresh; user clicks Refresh when done
} catch (e) {
setError(`Failed to install Gateway Service: ${e}`);
} finally {
setInstalling(null);
}
};
const handleOpenUrl = async (url: string) => {
try {
await open(url);
@@ -179,6 +195,16 @@ export function SystemInfo() {
installAction: handleInstallOpenclaw,
canAutoInstall: true,
},
...(envStatus.openclaw_installed ? [{
id: 'gateway',
name: 'Gateway Service',
description: 'System service (requires admin)',
icon: <Server size={18} />,
installed: envStatus.gateway_service_installed,
version: null,
installAction: handleInstallGateway,
canAutoInstall: true,
}] : []),
];
const installedCount = requirements.filter(r => r.installed).length;