mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Merge pull request #101 from vezuresdotxyz/develop
chore: merge develop into main (v0.41.0)
This commit is contained in:
@@ -10,11 +10,11 @@ name: Package and publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
# push:
|
||||
# branches:
|
||||
# - develop
|
||||
workflow_run:
|
||||
workflows: ['Version Bump']
|
||||
workflows: ["Version Bump"]
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
@@ -170,15 +170,15 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- platform: 'macos-latest'
|
||||
args: '--target aarch64-apple-darwin'
|
||||
target: 'aarch64-apple-darwin'
|
||||
- platform: 'macos-latest'
|
||||
args: '--target x86_64-apple-darwin'
|
||||
target: 'x86_64-apple-darwin'
|
||||
- platform: 'ubuntu-22.04'
|
||||
args: ''
|
||||
target: ''
|
||||
- platform: "macos-latest"
|
||||
args: "--target aarch64-apple-darwin"
|
||||
target: "aarch64-apple-darwin"
|
||||
- platform: "macos-latest"
|
||||
args: "--target x86_64-apple-darwin"
|
||||
target: "x86_64-apple-darwin"
|
||||
- platform: "ubuntu-22.04"
|
||||
args: ""
|
||||
target: ""
|
||||
runs-on: ${{ matrix.settings.platform }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24.x
|
||||
cache: 'yarn'
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -322,7 +322,7 @@ jobs:
|
||||
# macOS 10.15+ required for std::filesystem used by llama.cpp
|
||||
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.settings.platform == 'macos-latest' && '10.15' || '' }}
|
||||
with:
|
||||
args: '-c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }}'
|
||||
args: "-c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }}"
|
||||
includeDebug: ${{ needs.get-version.outputs.should-publish == '' && inputs.forceRelease != 'true' }}
|
||||
includeRelease: ${{ needs.get-version.outputs.should-publish != '' || inputs.forceRelease == 'true' }}
|
||||
# TDLib dylibs are now bundled natively via build.rs + tauri.conf.json macOS.frameworks
|
||||
|
||||
+1
-1
Submodule skills updated: 640518e8cc...dd49a534a7
@@ -23,7 +23,7 @@ use crate::runtime::skill_registry::SkillRegistry;
|
||||
use crate::runtime::types::{events, SkillMessage, SkillStatus};
|
||||
|
||||
/// Interval between ping sweeps.
|
||||
const PING_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const PING_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Per-skill timeout when waiting for a ping reply.
|
||||
const PING_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
@@ -109,11 +109,19 @@ impl PingScheduler {
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Collect running skill IDs
|
||||
// Collect skill IDs that are running AND actively connected.
|
||||
// Skills in setup mode or not yet connected are excluded to avoid
|
||||
// interfering with the setup flow.
|
||||
let running: Vec<String> = registry
|
||||
.list_skills()
|
||||
.into_iter()
|
||||
.filter(|s| s.status == SkillStatus::Running)
|
||||
.filter(|s| {
|
||||
s.status == SkillStatus::Running
|
||||
&& s.state
|
||||
.get("connection_status")
|
||||
.and_then(|v| v.as_str())
|
||||
.is_some_and(|cs| cs == "connected")
|
||||
})
|
||||
.map(|s| s.skill_id)
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -312,7 +312,8 @@ impl RuntimeEngine {
|
||||
.error
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Unknown initialization error".to_string());
|
||||
self.registry.unregister(&skill_id_owned);
|
||||
// Don't unregister — keep the skill with Error status so the
|
||||
// UI can see what happened and allow restart.
|
||||
self.emit_status_change(&skill_id_owned);
|
||||
return Err(format!(
|
||||
"Skill '{}' failed to start: {}",
|
||||
@@ -320,7 +321,9 @@ impl RuntimeEngine {
|
||||
));
|
||||
}
|
||||
SkillStatus::Stopped => {
|
||||
self.registry.unregister(&skill_id_owned);
|
||||
// Don't unregister — keep the skill with Stopped status so the
|
||||
// UI can still query it and allow restart.
|
||||
self.emit_status_change(&skill_id_owned);
|
||||
return Err(format!(
|
||||
"Skill '{}' stopped unexpectedly during initialization",
|
||||
skill_id_owned
|
||||
|
||||
@@ -118,6 +118,13 @@ impl SkillRegistry {
|
||||
let entry = skills
|
||||
.get(skill_id)
|
||||
.ok_or_else(|| format!("Skill '{}' not found", skill_id))?;
|
||||
let status = entry.state.read().status;
|
||||
if status != SkillStatus::Running {
|
||||
return Err(format!(
|
||||
"Skill '{}' is not running (status: {:?})",
|
||||
skill_id, status
|
||||
));
|
||||
}
|
||||
entry.sender.clone()
|
||||
};
|
||||
|
||||
@@ -198,8 +205,8 @@ impl SkillRegistry {
|
||||
// Wait for the skill to acknowledge stopping
|
||||
let _ = tokio::time::timeout(std::time::Duration::from_secs(5), reply_rx).await;
|
||||
|
||||
// Remove from registry
|
||||
self.unregister(skill_id);
|
||||
// Don't unregister — the skill stays in the registry with Stopped status
|
||||
// so the UI can still query it and allow restart without full rediscovery.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -235,6 +242,13 @@ impl SkillRegistry {
|
||||
let entry = skills
|
||||
.get(skill_id)
|
||||
.ok_or_else(|| format!("Skill '{}' not found", skill_id))?;
|
||||
let status = entry.state.read().status;
|
||||
if status != SkillStatus::Running {
|
||||
return Err(format!(
|
||||
"Skill '{}' is not running (status: {:?})",
|
||||
skill_id, status
|
||||
));
|
||||
}
|
||||
entry.sender.clone()
|
||||
};
|
||||
|
||||
|
||||
@@ -101,6 +101,8 @@ pub struct TdLibManager {
|
||||
request_tx: Arc<RwLock<Option<mpsc::Sender<TdRequest>>>>,
|
||||
/// Handle to the worker thread.
|
||||
worker_handle: RwLock<Option<std::thread::JoinHandle<()>>>,
|
||||
/// True while a destroy is in progress (prevents concurrent create_client).
|
||||
is_destroying: AtomicBool,
|
||||
}
|
||||
|
||||
impl TdLibManager {
|
||||
@@ -112,12 +114,20 @@ impl TdLibManager {
|
||||
data_dir: RwLock::new(None),
|
||||
request_tx: Arc::new(RwLock::new(None)),
|
||||
worker_handle: RwLock::new(None),
|
||||
is_destroying: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create and start a TDLib client with the given data directory.
|
||||
/// Returns the client ID. If a client already exists, returns its ID.
|
||||
/// Only one client can exist at a time; blocks if a destroy is in progress.
|
||||
pub fn create_client(&self, data_dir: PathBuf) -> Result<i32, String> {
|
||||
// Block creation while a destroy is in progress to prevent
|
||||
// creating a new C++ client before the old one releases its database lock.
|
||||
if self.is_destroying.load(Ordering::SeqCst) {
|
||||
return Err("TDLib client is currently being destroyed, please retry".to_string());
|
||||
}
|
||||
|
||||
// Check if already initialized - return existing client ID
|
||||
if let Some(existing_id) = *self.client_id.read() {
|
||||
log::info!("[tdlib] Client already exists with ID: {}, reusing", existing_id);
|
||||
@@ -574,15 +584,40 @@ impl TdLibManager {
|
||||
}
|
||||
|
||||
/// Destroy the TDLib client and clean up resources.
|
||||
/// Sends a `close` command to TDLib first to properly release the database lock,
|
||||
/// then tears down the worker thread and clears state.
|
||||
pub async fn destroy(&self) -> Result<(), String> {
|
||||
log::info!("[tdlib] Destroying client");
|
||||
|
||||
self.is_destroying.store(true, Ordering::SeqCst);
|
||||
|
||||
// Close TDLib properly to release the td.binlog database lock.
|
||||
// Without this, a subsequent create_client + setTdlibParameters will fail
|
||||
// with "Can't lock file ... already in use by current program".
|
||||
if self.client_id.read().is_some() {
|
||||
log::info!("[tdlib] Sending close command to release database locks");
|
||||
match tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
self.send(serde_json::json!({ "@type": "close" })),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => {
|
||||
log::info!("[tdlib] TDLib close acknowledged, waiting for lock release");
|
||||
// Give TDLib time to fully close the database and release the file lock.
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
Ok(Err(e)) => log::warn!("[tdlib] TDLib close command failed: {}", e),
|
||||
Err(_) => log::warn!("[tdlib] TDLib close command timed out after 5s"),
|
||||
}
|
||||
}
|
||||
|
||||
// Get the request_tx without holding the lock across await
|
||||
let request_tx = {
|
||||
self.request_tx.read().clone()
|
||||
};
|
||||
|
||||
// Send destroy request
|
||||
// Send destroy request to stop the worker loop
|
||||
if let Some(request_tx) = request_tx {
|
||||
let (reply_tx, reply_rx) = oneshot::channel();
|
||||
|
||||
@@ -600,6 +635,7 @@ impl TdLibManager {
|
||||
let _ = handle.join();
|
||||
}
|
||||
|
||||
self.is_destroying.store(false, Ordering::SeqCst);
|
||||
log::info!("[tdlib] Client destroyed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ function deriveConnectionStatus(
|
||||
setupComplete: boolean | undefined,
|
||||
skillState: Record<string, unknown> | undefined
|
||||
): SkillConnectionStatus {
|
||||
if (!lifecycleStatus || lifecycleStatus === 'installed') {
|
||||
if (!lifecycleStatus || lifecycleStatus === 'installed' || lifecycleStatus === 'stopping') {
|
||||
return 'offline';
|
||||
}
|
||||
if (lifecycleStatus === 'error') {
|
||||
|
||||
@@ -18,8 +18,8 @@ export function deriveConnectionStatus(
|
||||
setupComplete: boolean | undefined,
|
||||
skillState: Record<string, unknown> | undefined,
|
||||
): SkillConnectionStatus {
|
||||
// Skill not registered or not started
|
||||
if (!lifecycleStatus || lifecycleStatus === "installed") {
|
||||
// Skill not registered, not started, or shutting down
|
||||
if (!lifecycleStatus || lifecycleStatus === "installed" || lifecycleStatus === "stopping") {
|
||||
return "offline";
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { skillManager } from '../lib/skills/manager';
|
||||
import type { SkillManifest } from '../lib/skills/types';
|
||||
import { buildManualSentryEvent, enqueueError } from '../services/errorReportQueue';
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import { setSkillError, setSkillState } from '../store/skillsSlice';
|
||||
import { setSkillError, setSkillState, setSkillStatus } from '../store/skillsSlice';
|
||||
import { DEV_AUTO_LOAD_SKILL, IS_DEV } from '../utils/config';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -100,6 +100,10 @@ export default function SkillProvider({ children }: { children: ReactNode }) {
|
||||
{ skill_id, ...(name ? { skill_name: name } : {}) }
|
||||
),
|
||||
});
|
||||
} else if (status === 'stopped' || status === 'pending') {
|
||||
// Skill process has stopped — reset to "installed" so the UI
|
||||
// shows the Enable button instead of staying in setup mode.
|
||||
dispatch(setSkillStatus({ skillId: skill_id, status: 'installed' }));
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user