Merge pull request #101 from vezuresdotxyz/develop

chore: merge develop into main (v0.41.0)
This commit is contained in:
Steven Enamakel
2026-02-11 14:12:19 +05:30
committed by GitHub
9 changed files with 93 additions and 28 deletions
+15 -15
View File
@@ -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
+11 -3
View File
@@ -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();
+5 -2
View File
@@ -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
+16 -2
View File
@@ -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()
};
+37 -1
View File
@@ -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(())
}
+1 -1
View File
@@ -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') {
+2 -2
View File
@@ -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";
}
+5 -1
View File
@@ -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' }));
}
}
)