ci: speed up GitHub Actions builds (~14m → ~3-5m warm) (#136)

* chore: add CI profile for faster compilation in Cargo.toml files

- Introduced a new `[profile.ci]` section in both root and Tauri Cargo.toml files to optimize build settings for continuous integration.
- Adjusted compilation parameters to prioritize speed over runtime performance, including reduced optimization level and enabled code generation units.

* refactor(tests): update agent test setup to return temporary directory

- Modified the `build_agent_with` function calls in the agent tests to return a temporary directory alongside the agent instance, improving resource management during tests.
- Ensured consistency in test setup across multiple test functions.

* chore: update .gitignore to include fastembed_cache

- Added 'workflow' and '.fastembed_cache' to the .gitignore file to prevent unnecessary files from being tracked in the repository.

* test: enhance dispatch routing tests with panic handling

Updated the `dispatch_routes_memory_doc_ingest` test to use `AssertUnwindSafe` and `catch_unwind` for better handling of potential panics during execution. This ensures that the test verifies route existence even if the handler encounters a panic, improving robustness against shared state issues in concurrent tests.

* ci: speed up builds with rust-cache, sccache, mold linker, and CI profile

- Replace manual Cargo registry cache with Swatinem/rust-cache@v2 (caches
  target/ directories for both core and Tauri crates)
- Add mozilla-actions/sccache for cross-branch compilation caching
- Install mold linker on Linux for faster linking
- Use --profile ci for sidecar build (opt-level 1, codegen-units 16)
- Override release profile env vars for Tauri build with CI-tuned settings
- Add --bundles none to CI build (skip unused deb/appimage packaging)
- Restrict push triggers to main branch only (PRs already cover feature
  branches, preventing duplicate runs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ci): unset RUSTC_WRAPPER for cargo fmt to avoid sccache errors

The sccache-action sets RUSTC_WRAPPER globally for the job. cargo fmt
invokes rustc through sccache which fails if the GHA cache service is
unavailable. Clear RUSTC_WRAPPER for fmt steps and remove redundant
per-step env overrides (the action already sets them job-wide).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: implement Default trait for various structs and enums

- Added Default implementations for ConnectionStatus, AgentBuilder, NativeRuntime, AutocompleteEngine, CliChannel, ActionTracker, SkillStatus, and ExtractionMode to streamline object initialization.
- Simplified condition checks in several places by replacing map_or with is_some_and and is_none_or for better readability and performance.
- Updated various instances of string handling in message sending to remove unnecessary conversions.

This refactor enhances code clarity and consistency across the codebase.

* style: clean up whitespace and improve code formatting

- Removed unnecessary blank lines in several files to enhance code readability.
- Simplified condition checks by consolidating method calls into single lines for better clarity.
- Improved formatting in various functions to maintain consistency across the codebase.

* fix(ci): use --bundles deb instead of unsupported none value

Tauri CLI on this version doesn't support 'none' as a bundle type.
Use 'deb' as the lightest single bundle to minimize packaging time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(ci): add Dockerfile and CI workflows for building and pushing Docker images

- Introduced a Dockerfile to set up a CI environment with necessary dependencies for Tauri, Rust, Node.js, and sccache.
- Created a new workflow to build and push the CI Docker image to GitHub Container Registry on main branch pushes.
- Updated existing workflows to utilize the new Docker image for building and testing, enhancing consistency and efficiency in CI processes.

* chore(ci): update container image references in CI workflows

- Removed unnecessary permissions for packages in build, test, and typecheck workflows.
- Updated container image references to use a specific digest instead of the latest tag for improved stability and reproducibility in CI processes.

* fix(ci): use correct amd64 Docker image digest

Previous digest was from arm64 build (Mac). Rebuilt with
--platform linux/amd64 for GitHub Actions runners.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ci): use tag instead of digest for Docker image reference

GHCR doesn't support pulling OCI index manifests by digest reliably.
Use the rust-1.93.0 tag which is pinned to a specific Rust version
and resolves correctly on amd64 CI runners.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ci): rename Docker package to openhuman_ci

Move from nested ghcr.io/tinyhumansai/openhuman/ci-runner to
ghcr.io/tinyhumansai/openhuman_ci to avoid GHCR nested package
manifest resolution issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ci): remove sccache env vars from container jobs

sccache can't access the GHA cache API from inside a Docker container
(missing ACTIONS_CACHE_URL/ACTIONS_RUNTIME_TOKEN). Swatinem/rust-cache
already caches target/ which provides the main build speedup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update installer smoke workflow to trigger on main branch pushes

- Added a trigger for the installer smoke workflow to run on pushes to the main branch, enhancing CI coverage for mainline changes.

* fix: enhance Sentry DSN retrieval logic

- Updated the Sentry DSN retrieval process to include an additional fallback option using `option_env!`, ensuring that the DSN can be sourced from both environment variables and optional configuration, improving robustness in observability setup.

* chore: add OPENHUMAN_SENTRY_DSN to release workflow and example secrets

- Included the OPENHUMAN_SENTRY_DSN variable in the release workflow configuration to enhance observability setup.
- Updated the ci-secrets.example.json file to include a placeholder for OPENHUMAN_SENTRY_DSN, providing clarity for developers on required environment variables.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Steven Enamakel
2026-03-31 14:16:12 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 8486a3b02e
commit c5e5ae170c
43 changed files with 253 additions and 237 deletions
+43
View File
@@ -0,0 +1,43 @@
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
# System deps for Tauri + mold linker + clang (for mold)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
curl \
ca-certificates \
git \
pkg-config \
libgtk-3-dev \
libwebkit2gtk-4.1-dev \
libappindicator3-dev \
librsvg2-dev \
patchelf \
mold \
clang \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# Rust 1.93.0 with minimal profile + fmt/clippy
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH="/usr/local/cargo/bin:$PATH"
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- \
-y --default-toolchain 1.93.0 --profile minimal \
-c rustfmt -c clippy \
-t x86_64-unknown-linux-gnu
# Node.js 24.x + yarn
RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& npm install -g yarn \
&& rm -rf /var/lib/apt/lists/*
# sccache (pre-installed so the action only needs to configure it)
RUN curl -fsSL https://github.com/mozilla/sccache/releases/download/v0.10.0/sccache-v0.10.0-x86_64-unknown-linux-musl.tar.gz \
| tar xz -C /usr/local/bin --strip-components=1 sccache-v0.10.0-x86_64-unknown-linux-musl/sccache \
&& chmod +x /usr/local/bin/sccache
# Verify installs
RUN rustc --version && cargo --version && node --version && yarn --version && mold --version && sccache --version
+17 -34
View File
@@ -2,6 +2,7 @@ name: Build
on:
push:
branches: [main]
pull_request:
permissions:
@@ -16,6 +17,8 @@ jobs:
build:
name: Build Tauri App
runs-on: ubuntu-22.04
container:
image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -23,38 +26,13 @@ jobs:
fetch-depth: 1
submodules: true
- name: Setup Node.js 24.x
uses: actions/setup-node@v4
- name: Cache Rust build artifacts
uses: Swatinem/rust-cache@v2
with:
node-version: 24.x
cache: "yarn"
- name: Install Rust (rust-toolchain.toml)
uses: dtolnay/rust-toolchain@1.93.0
with:
targets: x86_64-unknown-linux-gnu
- name: Install Tauri dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
# Skip first 7 lines of Cargo.lock (workspace package version bumps) so the key tracks dependency changes only
- name: Cargo.lock fingerprint (deps only)
id: cargo-lock-fingerprint
shell: bash
run: |
echo "hash=$(tail -n +8 Cargo.lock | openssl dgst -sha256 | awk '{print $2}')" >> "$GITHUB_OUTPUT"
- name: Cache Cargo registry and git sources
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-registry-${{ steps.cargo-lock-fingerprint.outputs.hash }}
restore-keys: |
${{ runner.os }}-cargo-registry-
workspaces: |
. -> target
app/src-tauri -> target
cache-on-failure: true
- name: Cache node modules
id: yarn-cache
@@ -70,18 +48,23 @@ jobs:
run: yarn install --frozen-lockfile
- name: Build sidecar core binary
run: cargo build --manifest-path Cargo.toml --release --target x86_64-unknown-linux-gnu --bin openhuman-core
run: cargo build --profile ci --target x86_64-unknown-linux-gnu --bin openhuman-core
- name: Stage sidecar for Tauri bundler
run: |
mkdir -p app/src-tauri/binaries
cp target/x86_64-unknown-linux-gnu/release/openhuman-core app/src-tauri/binaries/openhuman-core-x86_64-unknown-linux-gnu
cp target/x86_64-unknown-linux-gnu/ci/openhuman-core app/src-tauri/binaries/openhuman-core-x86_64-unknown-linux-gnu
chmod +x app/src-tauri/binaries/openhuman-core-x86_64-unknown-linux-gnu
- name: Build Tauri app
working-directory: app
run: |
TAURI_CONFIG_OVERRIDE='{"plugins":{"updater":{"active":false}}}'
yarn tauri build -c "$TAURI_CONFIG_OVERRIDE"
yarn tauri build -c "$TAURI_CONFIG_OVERRIDE" --bundles deb
env:
NODE_ENV: production
CARGO_PROFILE_RELEASE_OPT_LEVEL: "1"
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "16"
CARGO_PROFILE_RELEASE_LTO: "false"
CARGO_PROFILE_RELEASE_STRIP: "true"
CARGO_PROFILE_RELEASE_DEBUG: "false"
+38
View File
@@ -0,0 +1,38 @@
name: Build CI Docker Image
on:
push:
branches: [main]
paths:
- ".github/Dockerfile"
- ".github/workflows/docker-ci-image.yml"
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build-image:
name: Build and push CI image
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .github
file: .github/Dockerfile
push: true
tags: |
ghcr.io/tinyhumansai/openhuman_ci:latest
ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0
+1
View File
@@ -2,6 +2,7 @@ name: Installer Smoke
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
+1
View File
@@ -375,6 +375,7 @@ jobs:
MATRIX_TARGET: ${{ matrix.settings.target }}
CORE_MANIFEST: ${{ steps.core-paths.outputs.core_manifest }}
CORE_BIN_NAME: ${{ steps.core-paths.outputs.core_bin_name }}
OPENHUMAN_SENTRY_DSN: ${{ vars.OPENHUMAN_SENTRY_DSN }}
- name: Stage sidecar for Tauri bundler
shell: bash
+11 -67
View File
@@ -2,6 +2,7 @@ name: Test
on:
push:
branches: [main]
pull_request:
permissions:
@@ -58,39 +59,21 @@ jobs:
rust-tests:
name: Rust Tests + Quality
runs-on: ubuntu-22.04
container:
image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install Rust (rust-toolchain.toml)
uses: dtolnay/rust-toolchain@1.93.0
- name: Cache Rust build artifacts
uses: Swatinem/rust-cache@v2
with:
components: rustfmt, clippy
targets: x86_64-unknown-linux-gnu
- name: Install Tauri build dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
# Match build.yml: lockfile body only (skip workspace version header noise)
- name: Cargo.lock fingerprint (deps only)
id: cargo-lock-fingerprint
shell: bash
run: |
echo "hash=$(tail -n +8 Cargo.lock | openssl dgst -sha256 | awk '{print $2}')" >> "$GITHUB_OUTPUT"
- name: Cache Cargo registry and git sources
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-rust-test-cargo-${{ steps.cargo-lock-fingerprint.outputs.hash }}
restore-keys: |
${{ runner.os }}-rust-test-cargo-
workspaces: |
. -> target
app/src-tauri -> target
cache-on-failure: true
- name: Check formatting (cargo fmt)
run: cargo fmt --all -- --check
@@ -102,52 +85,13 @@ jobs:
run: cargo test -p openhuman
- name: Build sidecar core binary
run: cargo build --manifest-path Cargo.toml --release --target x86_64-unknown-linux-gnu --bin openhuman-core
run: cargo build --profile ci --target x86_64-unknown-linux-gnu --bin openhuman-core
- name: Stage sidecar for Tauri shell tests
run: |
mkdir -p app/src-tauri/binaries
cp target/x86_64-unknown-linux-gnu/release/openhuman-core app/src-tauri/binaries/openhuman-core-x86_64-unknown-linux-gnu
cp target/x86_64-unknown-linux-gnu/ci/openhuman-core app/src-tauri/binaries/openhuman-core-x86_64-unknown-linux-gnu
chmod +x app/src-tauri/binaries/openhuman-core-x86_64-unknown-linux-gnu
- name: Test Tauri shell (OpenHuman)
run: cargo test --manifest-path app/src-tauri/Cargo.toml
# e2e-macos:
# name: E2E (macOS / Appium)
# runs-on: macos-latest
# timeout-minutes: 90
# steps:
# - name: Checkout code
# uses: actions/checkout@v4
# with:
# fetch-depth: 1
# submodules: recursive
# - name: Setup Node.js 24.x
# uses: actions/setup-node@v4
# with:
# node-version: 24.x
# cache: "yarn"
# - name: Install Rust (rust-toolchain.toml)
# uses: dtolnay/rust-toolchain@1.93.0
# - name: Install dependencies
# run: yarn install --frozen-lockfile
# - name: Ensure .env exists for E2E build
# run: |
# touch .env
# touch app/.env
# - name: Install Appium and mac2 driver
# run: |
# npm install -g appium
# appium driver install mac2
# - name: Build E2E app bundle
# run: yarn workspace openhuman-app test:e2e:build
# - name: Run all E2E flows
# run: yarn workspace openhuman-app test:e2e:all:flows
+8 -24
View File
@@ -2,6 +2,7 @@ name: Type Check
on:
push:
branches: [main]
pull_request:
permissions:
@@ -61,37 +62,20 @@ jobs:
rust-quality:
name: Rust Quality (fmt + clippy)
runs-on: ubuntu-22.04
container:
image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install Rust (rust-toolchain.toml)
uses: dtolnay/rust-toolchain@1.93.0
- name: Cache Rust build artifacts
uses: Swatinem/rust-cache@v2
with:
components: rustfmt, clippy
- name: Install Tauri build dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Cargo.lock fingerprint (deps only)
id: cargo-lock-fingerprint
shell: bash
run: |
echo "hash=$(tail -n +8 Cargo.lock | openssl dgst -sha256 | awk '{print $2}')" >> "$GITHUB_OUTPUT"
- name: Cache Cargo registry and git sources
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-registry-${{ steps.cargo-lock-fingerprint.outputs.hash }}
restore-keys: |
${{ runner.os }}-cargo-registry-
workspaces: |
. -> target
cache-on-failure: true
- name: Check formatting (cargo fmt)
run: cargo fmt --all -- --check
+2 -1
View File
@@ -57,4 +57,5 @@ tauri.key.pub
/target/
src-tauri/target/
workflow
workflow
.fastembed_cache
+10
View File
@@ -120,3 +120,13 @@ landlock = ["sandbox-landlock"]
probe = ["dep:probe-rs"]
rag-pdf = ["dep:pdf-extract"]
whatsapp-web = ["dep:wa-rs", "dep:wa-rs-core", "dep:wa-rs-binary", "dep:wa-rs-proto", "dep:wa-rs-ureq-http", "dep:wa-rs-tokio-transport", "serde-big-array"]
# Fast CI builds: trade runtime perf for compile speed
[profile.ci]
inherits = "release"
opt-level = 1
codegen-units = 16
lto = false
incremental = false
strip = true
debug = false
+10
View File
@@ -40,3 +40,13 @@ env_logger = "0.11"
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
sandbox-bubblewrap = []
# Fast CI builds: trade runtime perf for compile speed
[profile.ci]
inherits = "release"
opt-level = 1
codegen-units = 16
lto = false
incremental = false
strip = true
debug = false
+1
View File
@@ -19,6 +19,7 @@
"UPDATER_ENDPOINT": "",
"VITE_BACKEND_URL": "https://localhost:5005",
"VITE_SKILLS_GITHUB_REPO": "",
"OPENHUMAN_SENTRY_DSN": "",
"VITE_SENTRY_DSN": "",
"VITE_DEBUG": "true"
}
+2 -6
View File
@@ -3,7 +3,9 @@ use serde::{Deserialize, Serialize};
/// Socket connection status
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum ConnectionStatus {
#[default]
Disconnected,
Connecting,
Connected,
@@ -11,12 +13,6 @@ pub enum ConnectionStatus {
Error,
}
impl Default for ConnectionStatus {
fn default() -> Self {
Self::Disconnected
}
}
/// Socket connection state emitted to frontend
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SocketState {
+1 -1
View File
@@ -247,7 +247,7 @@ fn emit_with_aliases(socket: &SocketRef, name: &str, payload: &serde_json::Value
}
fn emit_room_with_aliases(io: &SocketIo, room: &str, name: &str, payload: &serde_json::Value) {
let _ = io.to(room.to_string()).emit(name.to_string(), payload);
let _ = io.to(room.to_string()).emit(name, payload);
if let Some(alias) = event_alias(name) {
let _ = io.to(room.to_string()).emit(alias, payload);
}
+2
View File
@@ -8,6 +8,8 @@ fn main() {
dsn: std::env::var("OPENHUMAN_SENTRY_DSN")
.ok()
.filter(|s| !s.is_empty())
.or_else(|| option_env!("OPENHUMAN_SENTRY_DSN").map(|s| s.to_string()))
.filter(|s| !s.is_empty())
.and_then(|s| s.parse().ok()),
release: Some(std::borrow::Cow::Borrowed(env!("CARGO_PKG_VERSION"))),
environment: Some(if cfg!(debug_assertions) {
+6
View File
@@ -54,6 +54,12 @@ pub struct AgentBuilder {
available_hints: Option<Vec<String>>,
}
impl Default for AgentBuilder {
fn default() -> Self {
Self::new()
}
}
impl AgentBuilder {
pub fn new() -> Self {
Self {
+6
View File
@@ -7,6 +7,12 @@ pub use crate::openhuman::agent::traits::RuntimeAdapter;
pub struct NativeRuntime;
impl Default for NativeRuntime {
fn default() -> Self {
Self::new()
}
}
impl NativeRuntime {
pub const fn new() -> Self {
Self
+2 -2
View File
@@ -27,7 +27,7 @@ pub(crate) fn autosave_memory_key(prefix: &str) -> String {
/// Preserves the system prompt (first message if role=system) and the most recent messages.
pub(crate) fn trim_history(history: &mut Vec<ChatMessage>, max_history: usize) {
// Nothing to trim if within limit
let has_system = history.first().map_or(false, |m| m.role == "system");
let has_system = history.first().is_some_and(|m| m.role == "system");
let non_system_count = if has_system {
history.len() - 1
} else {
@@ -74,7 +74,7 @@ pub(crate) async fn auto_compact_history(
max_history: usize,
config: &Config,
) -> Result<bool> {
let has_system = history.first().map_or(false, |m| m.role == "system");
let has_system = history.first().is_some_and(|m| m.role == "system");
let non_system_count = if has_system {
history.len().saturating_sub(1)
} else {
+1 -1
View File
@@ -226,7 +226,7 @@ pub(crate) async fn run_tool_call_loop(
if r.success {
scrub_credentials(&r.output)
} else {
format!("Error: {}", r.error.unwrap_or_else(|| r.output))
format!("Error: {}", r.error.unwrap_or(r.output))
}
}
Err(e) => {
+1 -4
View File
@@ -364,10 +364,7 @@ fn validate_size(source: &str, size_bytes: usize, max_bytes: usize) -> anyhow::R
}
fn validate_mime(source: &str, mime: &str) -> anyhow::Result<()> {
if ALLOWED_IMAGE_MIME_TYPES
.iter()
.any(|allowed| *allowed == mime)
{
if ALLOWED_IMAGE_MIME_TYPES.contains(&mime) {
return Ok(());
}
+7 -2
View File
@@ -241,6 +241,12 @@ pub struct AutocompleteEngine {
inner: Mutex<EngineState>,
}
impl Default for AutocompleteEngine {
fn default() -> Self {
Self::new()
}
}
impl AutocompleteEngine {
pub fn new() -> Self {
Self {
@@ -908,8 +914,7 @@ fn show_overflow_badge(
fn escape_osascript_text(raw: &str) -> String {
raw.replace('\\', "\\\\")
.replace('\"', "\\\"")
.replace('\n', " ")
.replace('\r', " ")
.replace(['\n', '\r'], " ")
}
#[cfg(target_os = "macos")]
+6
View File
@@ -6,6 +6,12 @@ use uuid::Uuid;
/// Console channel — stdin/stdout, not used in the web UI, zero deps
pub struct CliChannel;
impl Default for CliChannel {
fn default() -> Self {
Self::new()
}
}
impl CliChannel {
pub fn new() -> Self {
Self
@@ -130,7 +130,7 @@ impl ChannelDefinition {
.filter(|f| {
credentials
.get(f.key)
.map_or(true, |v| v.as_str().map_or(false, |s| s.is_empty()))
.is_none_or(|v| v.as_str().is_some_and(|s| s.is_empty()))
})
.map(|f| f.key)
.collect();
+2 -2
View File
@@ -195,7 +195,7 @@ impl Channel for DingTalkChannel {
"data": "",
});
if let Err(e) = write.send(Message::Text(pong.to_string().into())).await {
if let Err(e) = write.send(Message::Text(pong.to_string())).await {
tracing::warn!("DingTalk: failed to send pong: {e}");
break;
}
@@ -262,7 +262,7 @@ impl Channel for DingTalkChannel {
"message": "OK",
"data": "",
});
let _ = write.send(Message::Text(ack.to_string().into())).await;
let _ = write.send(Message::Text(ack.to_string())).await;
let channel_msg = ChannelMessage {
id: Uuid::new_v4().to_string(),
+3 -5
View File
@@ -272,9 +272,7 @@ impl Channel for DiscordChannel {
}
}
});
write
.send(Message::Text(identify.to_string().into()))
.await?;
write.send(Message::Text(identify.to_string())).await?;
tracing::info!("Discord: connected and identified");
@@ -303,7 +301,7 @@ impl Channel for DiscordChannel {
_ = hb_rx.recv() => {
let d = if sequence >= 0 { json!(sequence) } else { json!(null) };
let hb = json!({"op": 1, "d": d});
if write.send(Message::Text(hb.to_string().into())).await.is_err() {
if write.send(Message::Text(hb.to_string())).await.is_err() {
break;
}
}
@@ -331,7 +329,7 @@ impl Channel for DiscordChannel {
1 => {
let d = if sequence >= 0 { json!(sequence) } else { json!(null) };
let hb = json!({"op": 1, "d": d});
if write.send(Message::Text(hb.to_string().into())).await.is_err() {
if write.send(Message::Text(hb.to_string())).await.is_err() {
break;
}
continue;
+4 -4
View File
@@ -288,7 +288,7 @@ impl LarkChannel {
payload: None,
};
if write
.send(WsMsg::Binary(initial_ping.encode_to_vec().into()))
.send(WsMsg::Binary(initial_ping.encode_to_vec()))
.await
.is_err()
{
@@ -309,7 +309,7 @@ impl LarkChannel {
headers: vec![PbHeader { key: "type".into(), value: "ping".into() }],
payload: None,
};
if write.send(WsMsg::Binary(ping.encode_to_vec().into())).await.is_err() {
if write.send(WsMsg::Binary(ping.encode_to_vec())).await.is_err() {
tracing::warn!("Lark: ping failed, reconnecting");
break;
}
@@ -378,7 +378,7 @@ impl LarkChannel {
let mut ack = frame.clone();
ack.payload = Some(br#"{"code":200,"headers":{},"data":[]}"#.to_vec());
ack.headers.push(PbHeader { key: "biz_rt".into(), value: "0".into() });
let _ = write.send(WsMsg::Binary(ack.encode_to_vec().into())).await;
let _ = write.send(WsMsg::Binary(ack.encode_to_vec())).await;
}
// Fragment reassembly
@@ -739,7 +739,7 @@ impl LarkChannel {
let token_ok = payload
.get("token")
.and_then(|t| t.as_str())
.map_or(true, |t| t == state.verification_token);
.is_none_or(|t| t == state.verification_token);
if !token_ok {
return (StatusCode::FORBIDDEN, "invalid token").into_response();
+3 -5
View File
@@ -263,9 +263,7 @@ impl Channel for QQChannel {
}
}
});
write
.send(Message::Text(identify.to_string().into()))
.await?;
write.send(Message::Text(identify.to_string())).await?;
tracing::info!("QQ: connected and identified");
@@ -290,7 +288,7 @@ impl Channel for QQChannel {
let d = if sequence >= 0 { json!(sequence) } else { json!(null) };
let hb = json!({"op": 1, "d": d});
if write
.send(Message::Text(hb.to_string().into()))
.send(Message::Text(hb.to_string()))
.await
.is_err()
{
@@ -322,7 +320,7 @@ impl Channel for QQChannel {
let d = if sequence >= 0 { json!(sequence) } else { json!(null) };
let hb = json!({"op": 1, "d": d});
if write
.send(Message::Text(hb.to_string().into()))
.send(Message::Text(hb.to_string()))
.await
.is_err()
{
+1 -1
View File
@@ -145,7 +145,7 @@ pub(crate) async fn process_channel_message(
// Determine if this channel supports streaming draft updates
let use_streaming = target_channel
.as_ref()
.map_or(false, |ch| ch.supports_draft_updates());
.is_some_and(|ch| ch.supports_draft_updates());
// Set up streaming channel if supported
let (delta_tx, delta_rx) = if use_streaming {
+4 -1
View File
@@ -592,7 +592,10 @@ impl Config {
}
}
if let Ok(dsn) = std::env::var("OPENHUMAN_SENTRY_DSN") {
let dsn_value = std::env::var("OPENHUMAN_SENTRY_DSN")
.ok()
.or_else(|| option_env!("OPENHUMAN_SENTRY_DSN").map(|s| s.to_string()));
if let Some(dsn) = dsn_value {
let dsn = dsn.trim();
if !dsn.is_empty() {
self.observability.sentry_dsn = Some(dsn.to_string());
-1
View File
@@ -150,7 +150,6 @@ fn ensure_fastembed_ort_dylib_path() {
if bundled.exists() {
env::set_var("ORT_DYLIB_PATH", bundled);
return;
}
}
+4 -8
View File
@@ -14,17 +14,13 @@ const DEFAULT_CHUNK_TOKENS: usize = 225;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum ExtractionMode {
#[default]
Sentence,
Chunk,
}
impl Default for ExtractionMode {
fn default() -> Self {
Self::Sentence
}
}
impl ExtractionMode {
fn as_str(self) -> &'static str {
match self {
@@ -282,7 +278,7 @@ fn type_allowed(actual: &str, allowed: &[&str]) -> bool {
fn resolve_person_alias(name: &str, known_people: &HashMap<String, String>) -> String {
let upper = name.to_uppercase();
known_people.get(&upper).cloned().unwrap_or_else(|| upper)
known_people.get(&upper).cloned().unwrap_or(upper)
}
impl ExtractionAccumulator {
@@ -1297,7 +1293,7 @@ async fn parse_document(
for unit in build_units(&chunks, config.extraction_mode) {
if let Some(ref runtime) = relex_runtime {
apply_model_extraction(&runtime, &unit, &mut accumulator, config);
apply_model_extraction(runtime, &unit, &mut accumulator, config);
}
if let Some(captures) = recipient_regex().captures(&unit.text) {
+9 -7
View File
@@ -5,6 +5,7 @@ use std::sync::OnceLock;
use anyhow::{anyhow, Context, Result};
use futures_util::TryStreamExt;
#[cfg(target_os = "windows")]
use glob::glob;
use ndarray::{Array, Array2, Array3, Array4, Ix2, Ix3, Ix4};
use ort::{
@@ -433,10 +434,11 @@ async fn resolve_bundle_dir(model_name: &str) -> Option<PathBuf> {
return Some(bundle_dir);
}
if uses_default_bundle(model_name) {
if ensure_managed_bundle(&managed_dir).await.is_ok() && bundle_complete(&managed_dir) {
return Some(managed_dir);
}
if uses_default_bundle(model_name)
&& ensure_managed_bundle(&managed_dir).await.is_ok()
&& bundle_complete(&managed_dir)
{
return Some(managed_dir);
}
None
@@ -482,6 +484,7 @@ fn model_file_path(bundle_dir: &Path) -> Option<PathBuf> {
None
}
#[allow(unused_variables)]
fn ensure_ort_dylib_path(bundle_dir: &Path) {
if env::var_os("ORT_DYLIB_PATH").is_some() {
return;
@@ -505,7 +508,6 @@ fn ensure_ort_dylib_path(bundle_dir: &Path) {
let runtime_lib = candidate.join(ORT_DYLIB_FILE_NAME);
if runtime_lib.exists() {
env::set_var("ORT_DYLIB_PATH", runtime_lib);
return;
}
}
@@ -788,8 +790,8 @@ fn decode_entity_spans(
) -> Vec<DecodedSpan> {
let mut spans = Vec::new();
let num_words = tokens.len();
let width_count = logits.shape().get(2).copied().unwrap_or_default() as usize;
let class_count = logits.shape().get(3).copied().unwrap_or_default() as usize;
let width_count = logits.shape().get(2).copied().unwrap_or_default();
let class_count = logits.shape().get(3).copied().unwrap_or_default();
for start in 0..num_words {
let actual_max_width = max_width
+4 -6
View File
@@ -385,12 +385,10 @@ fn pick_column_expr<'a>(
}
fn pick_optional_column_expr<'a>(columns: &'a [String], candidates: &[&'a str]) -> Option<&'a str> {
for candidate in candidates {
if columns.iter().any(|c| c.eq_ignore_ascii_case(candidate)) {
return Some(candidate);
}
}
None
candidates
.iter()
.find(|&candidate| columns.iter().any(|c| c.eq_ignore_ascii_case(candidate)))
.map(|v| v as _)
}
async fn next_available_key(memory: &dyn Memory, key: &str) -> Result<String> {
+2 -8
View File
@@ -1138,10 +1138,7 @@ impl Provider for OpenAiCompatibleProvider {
// If tool_calls are present, serialize the full message as JSON
// so parse_tool_calls can handle the OpenAI-style format
if c.message.tool_calls.is_some()
&& c.message
.tool_calls
.as_ref()
.map_or(false, |t| !t.is_empty())
&& c.message.tool_calls.as_ref().is_some_and(|t| !t.is_empty())
{
serde_json::to_string(&c.message)
.unwrap_or_else(|_| c.message.effective_content())
@@ -1245,10 +1242,7 @@ impl Provider for OpenAiCompatibleProvider {
// If tool_calls are present, serialize the full message as JSON
// so parse_tool_calls can handle the OpenAI-style format
if c.message.tool_calls.is_some()
&& c.message
.tool_calls
.as_ref()
.map_or(false, |t| !t.is_empty())
&& c.message.tool_calls.as_ref().is_some_and(|t| !t.is_empty())
{
serde_json::to_string(&c.message)
.unwrap_or_else(|_| c.message.effective_content())
+1 -1
View File
@@ -170,7 +170,7 @@ pub fn create_backend_inference_provider(
) -> anyhow::Result<Box<dyn Provider>> {
let resolved = resolve_provider_credential(INFERENCE_BACKEND_ID, api_key)
.map(|v| String::from_utf8(v.into_bytes()).unwrap_or_default());
let key = resolved.as_ref().map(String::as_str);
let key = resolved.as_deref();
Ok(Box::new(openhuman_backend::OpenHumanBackendProvider::new(
api_url, key, options,
)))
+1 -1
View File
@@ -146,7 +146,7 @@ pub(crate) fn capture_screen_image_ref_for_context(
}
let encoded = BASE64_STANDARD.encode(bytes);
return Ok(format!("data:image/png;base64,{encoded}"));
Ok(format!("data:image/png;base64,{encoded}"))
}
#[cfg(not(target_os = "macos"))]
+6
View File
@@ -39,6 +39,12 @@ pub struct ActionTracker {
actions: Mutex<Vec<Instant>>,
}
impl Default for ActionTracker {
fn default() -> Self {
Self::new()
}
}
impl ActionTracker {
pub fn new() -> Self {
Self {
+2 -2
View File
@@ -33,7 +33,7 @@ pub fn install(config: &Config) -> Result<ServiceStatus> {
#[cfg(target_os = "macos")]
{
macos::install(config)?;
return status(config);
status(config)
}
#[cfg(target_os = "linux")]
{
@@ -72,7 +72,7 @@ pub fn stop(config: &Config) -> Result<ServiceStatus> {
#[cfg(target_os = "macos")]
{
macos::stop(config)?;
return status(config);
status(config)
}
#[cfg(target_os = "linux")]
{
@@ -22,7 +22,7 @@ pub fn register<'js>(
Function::new(
ctx.clone(),
move |name: String, version: u32| -> rquickjs::Result<String> {
let result = s.open_database(&name, version).map_err(|e| js_err(e))?;
let result = s.open_database(&name, version).map_err(js_err)?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
),
@@ -44,7 +44,7 @@ pub fn register<'js>(
ops.set(
"idb_delete_database",
Function::new(ctx.clone(), move |name: String| -> rquickjs::Result<()> {
s.delete_database(&name).map_err(|e| js_err(e))
s.delete_database(&name).map_err(js_err)
}),
)?;
}
@@ -64,7 +64,7 @@ pub fn register<'js>(
let key_path = opts["keyPath"].as_str();
let auto_increment = opts["autoIncrement"].as_bool().unwrap_or(false);
s.create_object_store(&db_name, &store_name, key_path, auto_increment)
.map_err(|e| js_err(e))
.map_err(js_err)
},
),
)?;
@@ -77,8 +77,7 @@ pub fn register<'js>(
Function::new(
ctx.clone(),
move |db_name: String, store_name: String| -> rquickjs::Result<()> {
s.delete_object_store(&db_name, &store_name)
.map_err(|e| js_err(e))
s.delete_object_store(&db_name, &store_name).map_err(js_err)
},
),
)?;
@@ -96,9 +95,7 @@ pub fn register<'js>(
-> rquickjs::Result<String> {
let key_val: serde_json::Value =
serde_json::from_str(&key).map_err(|e| js_err(e.to_string()))?;
let result = s
.get(&db_name, &store_name, &key_val)
.map_err(|e| js_err(e))?;
let result = s.get(&db_name, &store_name, &key_val).map_err(js_err)?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
),
@@ -121,7 +118,7 @@ pub fn register<'js>(
let value_val: serde_json::Value =
serde_json::from_str(&value).map_err(|e| js_err(e.to_string()))?;
s.put(&db_name, &store_name, &key_val, &value_val)
.map_err(|e| js_err(e))
.map_err(js_err)
},
),
)?;
@@ -136,8 +133,7 @@ pub fn register<'js>(
move |db_name: String, store_name: String, key: String| -> rquickjs::Result<()> {
let key_val: serde_json::Value =
serde_json::from_str(&key).map_err(|e| js_err(e.to_string()))?;
s.delete(&db_name, &store_name, &key_val)
.map_err(|e| js_err(e))
s.delete(&db_name, &store_name, &key_val).map_err(js_err)
},
),
)?;
@@ -150,7 +146,7 @@ pub fn register<'js>(
Function::new(
ctx.clone(),
move |db_name: String, store_name: String| -> rquickjs::Result<()> {
s.clear(&db_name, &store_name).map_err(|e| js_err(e))
s.clear(&db_name, &store_name).map_err(js_err)
},
),
)?;
@@ -166,9 +162,7 @@ pub fn register<'js>(
store_name: String,
count: Option<u32>|
-> rquickjs::Result<String> {
let result = s
.get_all(&db_name, &store_name, count)
.map_err(|e| js_err(e))?;
let result = s.get_all(&db_name, &store_name, count).map_err(js_err)?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
),
@@ -187,7 +181,7 @@ pub fn register<'js>(
-> rquickjs::Result<String> {
let result = s
.get_all_keys(&db_name, &store_name, count)
.map_err(|e| js_err(e))?;
.map_err(js_err)?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
),
@@ -201,7 +195,7 @@ pub fn register<'js>(
Function::new(
ctx.clone(),
move |db_name: String, store_name: String| -> rquickjs::Result<u32> {
s.count(&db_name, &store_name).map_err(|e| js_err(e))
s.count(&db_name, &store_name).map_err(js_err)
},
),
)?;
@@ -226,7 +220,7 @@ pub fn register<'js>(
};
let rows = s
.skill_db_exec(&sc.skill_id, &sql, &params)
.map_err(|e| js_err(e))?;
.map_err(js_err)?;
Ok(rows as i64)
},
),
@@ -248,7 +242,7 @@ pub fn register<'js>(
};
let result = s
.skill_db_get(&sc.skill_id, &sql, &params)
.map_err(|e| js_err(e))?;
.map_err(js_err)?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
),
@@ -270,7 +264,7 @@ pub fn register<'js>(
};
let result = s
.skill_db_all(&sc.skill_id, &sql, &params)
.map_err(|e| js_err(e))?;
.map_err(js_err)?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
),
@@ -285,7 +279,7 @@ pub fn register<'js>(
Function::new(
ctx.clone(),
move |key: String| -> rquickjs::Result<String> {
let result = s.skill_kv_get(&sc.skill_id, &key).map_err(|e| js_err(e))?;
let result = s.skill_kv_get(&sc.skill_id, &key).map_err(js_err)?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
),
@@ -302,8 +296,7 @@ pub fn register<'js>(
move |key: String, value_json: String| -> rquickjs::Result<()> {
let value: serde_json::Value =
serde_json::from_str(&value_json).map_err(|e| js_err(e.to_string()))?;
s.skill_kv_set(&sc.skill_id, &key, &value)
.map_err(|e| js_err(e))
s.skill_kv_set(&sc.skill_id, &key, &value).map_err(js_err)
},
),
)?;
@@ -321,9 +314,7 @@ pub fn register<'js>(
Function::new(
ctx.clone(),
move |key: String| -> rquickjs::Result<String> {
let result = s
.skill_store_get(&sc.skill_id, &key)
.map_err(|e| js_err(e))?;
let result = s.skill_store_get(&sc.skill_id, &key).map_err(js_err)?;
serde_json::to_string(&result).map_err(|e| js_err(e.to_string()))
},
),
@@ -341,7 +332,7 @@ pub fn register<'js>(
let value: serde_json::Value =
serde_json::from_str(&value_json).map_err(|e| js_err(e.to_string()))?;
s.skill_store_set(&sc.skill_id, &key, &value)
.map_err(|e| js_err(e))
.map_err(js_err)
},
),
)?;
@@ -353,8 +344,7 @@ pub fn register<'js>(
ops.set(
"store_delete",
Function::new(ctx.clone(), move |key: String| -> rquickjs::Result<()> {
s.skill_store_delete(&sc.skill_id, &key)
.map_err(|e| js_err(e))
s.skill_store_delete(&sc.skill_id, &key).map_err(js_err)
}),
)?;
}
@@ -365,7 +355,7 @@ pub fn register<'js>(
ops.set(
"store_keys",
Function::new(ctx.clone(), move || -> rquickjs::Result<String> {
let keys = s.skill_store_keys(&sc.skill_id).map_err(|e| js_err(e))?;
let keys = s.skill_store_keys(&sc.skill_id).map_err(js_err)?;
serde_json::to_string(&keys).map_err(|e| js_err(e.to_string()))
}),
)?;
+3 -3
View File
@@ -131,8 +131,8 @@ pub async fn registry_search(
let mut results: Vec<RegistrySkillEntry> = Vec::new();
let include_core = category.map_or(true, |c| c == "core");
let include_third_party = category.map_or(true, |c| c == "third_party");
let include_core = category.is_none_or(|c| c == "core");
let include_third_party = category.is_none_or(|c| c == "third_party");
if include_core {
results.extend(
@@ -343,7 +343,7 @@ pub async fn skills_list_available(
.map(|s| s.version.clone());
let update_available = installed_version
.as_ref()
.map_or(false, |v| !v.is_empty() && *v != entry.version);
.is_some_and(|v| !v.is_empty() && *v != entry.version);
available.push(AvailableSkillEntry {
registry: entry,
+2 -2
View File
@@ -327,7 +327,7 @@ async fn run_connection(
// 5. Send Socket.IO CONNECT with auth
let connect_payload = json!({"token": token});
let connect_msg = format!("40{}", serde_json::to_string(&connect_payload).unwrap());
if let Err(e) = ws_write.send(WsMessage::Text(connect_msg.into())).await {
if let Err(e) = ws_write.send(WsMessage::Text(connect_msg)).await {
return ConnectionOutcome::Failed(format!("Send SIO CONNECT: {e}"));
}
@@ -386,7 +386,7 @@ async fn run_connection(
outgoing = emit_rx.recv() => {
match outgoing {
Some(msg) => {
if let Err(e) = ws_write.send(WsMessage::Text(msg.into())).await {
if let Err(e) = ws_write.send(WsMessage::Text(msg)).await {
return ConnectionOutcome::Lost(format!("Send failed: {e}"));
}
}
+2 -6
View File
@@ -6,8 +6,10 @@ use std::collections::HashMap;
/// Status of a running skill instance.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum SkillStatus {
/// Skill is registered but not yet started.
#[default]
Pending,
/// Skill is currently initializing (loading JS, running init()).
Initializing,
@@ -21,12 +23,6 @@ pub enum SkillStatus {
Error,
}
impl Default for SkillStatus {
fn default() -> Self {
Self::Pending
}
}
/// Messages sent to a skill instance's message loop.
#[derive(Debug)]
pub enum SkillMessage {
+1 -1
View File
@@ -62,7 +62,7 @@ where
.enable_all()
.build()
.map_err(|e| format!("Failed to create runtime: {}", e))
.and_then(|rt| Ok(rt.block_on(future)));
.map(|rt| rt.block_on(future));
let result = match runtime_result {
Ok(value) => Ok(value),
+2
View File
@@ -21,7 +21,9 @@ pub async fn try_dispatch(
#[cfg(test)]
mod tests {
use futures_util::FutureExt;
use serde_json::json;
use std::panic::AssertUnwindSafe;
use super::try_dispatch;