diff --git a/.github/workflows/ci-lite.yml b/.github/workflows/ci-lite.yml index f807ef536c..a00dc98a56 100644 --- a/.github/workflows/ci-lite.yml +++ b/.github/workflows/ci-lite.yml @@ -338,6 +338,7 @@ jobs: with: workspaces: | . -> target + app/src-tauri -> target cache-on-failure: true # shared-key (not key) so the cache name is stable and not suffixed # with the job id. Swatinem caches the full target/, which is why we @@ -349,7 +350,23 @@ jobs: run: cargo fmt --all -- --check - name: Run clippy (core crate) - run: bash scripts/ci-cancel-aware.sh cargo clippy -p openhuman + if: needs.changes.outputs['rust-core'] == 'true' + run: bash scripts/ci-cancel-aware.sh cargo clippy -p openhuman -- -D warnings + + - name: Cache CEF binary distribution + if: needs.changes.outputs['rust-tauri'] == 'true' + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 + with: + path: | + ~/Library/Caches/tauri-cef + ~/.cache/tauri-cef + key: cef-x86_64-unknown-linux-gnu-v2-${{ hashFiles('app/src-tauri/Cargo.toml') }} + restore-keys: | + cef-x86_64-unknown-linux-gnu-v2- + + - name: Run clippy (Tauri shell) + if: needs.changes.outputs['rust-tauri'] == 'true' + run: bash scripts/ci-cancel-aware.sh cargo clippy --manifest-path app/src-tauri/Cargo.toml -- -D warnings # Feature-gate smoke: proves the core still compiles with a domain gate turned # OFF. The disabled build is the ONLY thing that catches stub-facade signature diff --git a/.husky/pre-push b/.husky/pre-push index 521d8b9c14..d39768b34a 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -93,10 +93,10 @@ pnpm compile COMPILE_EXIT=$? set -e -# Run Rust compile checks for both the core and Tauri codebases +# Run Clippy for both Rust codebases; Clippy also performs compile checks. set +e -pnpm rust:check -RUST_CHECK_EXIT=$? +pnpm rust:clippy +RUST_CLIPPY_EXIT=$? set -e # Enforce scoped cmd-* tokens in components/commands/ @@ -106,7 +106,7 @@ CMD_TOKENS_EXIT=$? set -e # Exit with error if any command still fails after fixes -if [ $FORMAT_EXIT -ne 0 ] || [ $LINT_EXIT -ne 0 ] || [ $COMPILE_EXIT -ne 0 ] || [ $RUST_CHECK_EXIT -ne 0 ] || [ $CMD_TOKENS_EXIT -ne 0 ]; then +if [ $FORMAT_EXIT -ne 0 ] || [ $LINT_EXIT -ne 0 ] || [ $COMPILE_EXIT -ne 0 ] || [ $RUST_CLIPPY_EXIT -ne 0 ] || [ $CMD_TOKENS_EXIT -ne 0 ]; then echo echo "============================================================" echo "Pre-push checks failed." @@ -116,10 +116,10 @@ if [ $FORMAT_EXIT -ne 0 ] || [ $LINT_EXIT -ne 0 ] || [ $COMPILE_EXIT -ne 0 ] || echo " git add -A && git commit -m 'chore: apply auto-fixes'" echo " git push" fi - if [ $COMPILE_EXIT -ne 0 ] || [ $RUST_CHECK_EXIT -ne 0 ] || [ $CMD_TOKENS_EXIT -ne 0 ]; then + if [ $COMPILE_EXIT -ne 0 ] || [ $RUST_CLIPPY_EXIT -ne 0 ] || [ $CMD_TOKENS_EXIT -ne 0 ]; then echo "Fix the remaining errors above (TypeScript / Rust / cmd-tokens)" echo "before re-pushing — these have no auto-fix path." fi echo "============================================================" exit 1 -fi \ No newline at end of file +fi diff --git a/Cargo.toml b/Cargo.toml index acff5cdb2b..800f24e9fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -379,6 +379,19 @@ e2e-test-support = [] [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage)'] } +# These project-wide shape/documentation lints describe intentional public APIs +# and long-standing module docs. Keep them explicitly baselined so `-D warnings` +# can make every other Clippy and rustc diagnostic a hard failure. +[lints.clippy] +borrowed_box = "allow" +doc_overindented_list_items = "allow" +field_reassign_with_default = "allow" +large_enum_variant = "allow" +result_large_err = "allow" +should_implement_trait = "allow" +too_many_arguments = "allow" +while_let_loop = "allow" + # Fix whisper-rs-sys CRT mismatch on Windows MSVC (LNK2038). # Upstream cmake build defaults to /MD but Rust uses /MT. # This fork adds config.static_crt(true) to the build script. diff --git a/app/package.json b/app/package.json index 3f3b9c952e..960dbcbe4a 100644 --- a/app/package.json +++ b/app/package.json @@ -62,7 +62,7 @@ "rust:check": "cargo check --manifest-path src-tauri/Cargo.toml", "rust:format": "cargo fmt --manifest-path ../Cargo.toml --all && cargo fmt --manifest-path src-tauri/Cargo.toml --all", "rust:format:check": "cargo fmt --manifest-path ../Cargo.toml --all --check && cargo fmt --manifest-path src-tauri/Cargo.toml --all --check", - "rust:clippy": "cargo clippy -p openhuman -- -D warnings", + "rust:clippy": "cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings", "format": "prettier --write . && pnpm rust:format", "format:check": "prettier --check . && pnpm rust:format:check", "lint": "eslint . --ext .ts,.tsx --cache", diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index b27906524a..8bae10f58e 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -226,7 +226,7 @@ tinyagents = { path = "../../vendor/tinyagents" } # TinyAgents so integration work can test crate changes against OpenHuman before # publishing. tinyflows = { path = "../../vendor/tinyflows" } -tinycortex = { path = "../../vendor/tinycortex", features = ["git-diff"] } +tinycortex = { path = "../../vendor/tinycortex" } tinyjuice = { path = "../../vendor/tinyjuice" } tinychannels = { path = "../../vendor/tinychannels" } tinyplace = { path = "../../vendor/tinyplace/sdk/rust" } diff --git a/app/src-tauri/src/cdp/input.rs b/app/src-tauri/src/cdp/input.rs deleted file mode 100644 index cd97330286..0000000000 --- a/app/src-tauri/src/cdp/input.rs +++ /dev/null @@ -1,236 +0,0 @@ -//! Thin helpers around `Input.dispatchMouseEvent` and -//! `Input.dispatchKeyEvent` so providers can drive web UIs without -//! touching the page's JavaScript. -//! -//! All coordinates are CSS pixels relative to the viewport — the same -//! frame `DOMSnapshot.captureSnapshot(includeDOMRects=true)` returns -//! bounding rects in. Callers typically pair these with -//! [`crate::cdp::Snapshot::rect`] to find the click target. -//! -//! Everything here is CEF-only — CDP requires a remote-debugging port, -//! which wry doesn't expose. -//! -//! # Cookbook -//! -//! ```ignore -//! let snap = Snapshot::capture_with_rects(&mut cdp, &session).await?; -//! let idx = snap.find_descendant(0, |s, i| s.attr(i, "aria-label") == Some("Search mail")) -//! .ok_or("search box not found")?; -//! let rect = snap.rect(idx).ok_or("search box has no layout rect")?; -//! let (cx, cy) = rect.center(); -//! input::click(&mut cdp, &session, cx, cy).await?; -//! input::type_text(&mut cdp, &session, "from:linkedin.com").await?; -//! input::press_key(&mut cdp, &session, Key::Enter).await?; -//! ``` - -use serde_json::{json, Value}; - -use super::CdpConn; - -#[allow(dead_code)] // helper is used by currently gated input paths. -#[cfg(target_os = "macos")] -const SELECT_ALL_MODIFIER: u32 = 4; -#[allow(dead_code)] // helper is used by currently gated input paths. -#[cfg(not(target_os = "macos"))] -const SELECT_ALL_MODIFIER: u32 = 2; - -/// Names recognised by `Input.dispatchKeyEvent`'s `key` field. We -/// hand-pick the ones Gmail's keyboard handlers care about so callers -/// can use a typed value rather than stringly-typed literals scattered -/// across providers. -#[allow(dead_code)] // variants reserved for upcoming providers / write ops. -#[derive(Debug, Clone, Copy)] -pub enum Key { - Enter, - Escape, - Tab, - Backspace, - ArrowDown, - ArrowUp, -} - -impl Key { - /// `(key, code, windowsVirtualKeyCode)` triple. Gmail's listeners - /// branch on different fields depending on browser; we set all three - /// to maximise compatibility. - fn cdp_fields(self) -> (&'static str, &'static str, u32) { - match self { - Key::Enter => ("Enter", "Enter", 13), - Key::Escape => ("Escape", "Escape", 27), - Key::Tab => ("Tab", "Tab", 9), - Key::Backspace => ("Backspace", "Backspace", 8), - Key::ArrowDown => ("ArrowDown", "ArrowDown", 40), - Key::ArrowUp => ("ArrowUp", "ArrowUp", 38), - } - } -} - -/// Click at `(x, y)` — left button, no modifiers, single click. -/// Issues mouseMoved → mousePressed → mouseReleased so hover handlers -/// (Gmail's search-box has one) fire correctly before the click. -pub async fn click(cdp: &mut CdpConn, session: &str, x: f64, y: f64) -> Result<(), String> { - log::debug!("[cdp::input] click session={session} x={x:.1} y={y:.1}"); - let _ = mouse_event(cdp, session, "mouseMoved", x, y, 0).await?; - let _ = mouse_event(cdp, session, "mousePressed", x, y, 1).await?; - let _ = mouse_event(cdp, session, "mouseReleased", x, y, 1).await?; - log::debug!("[cdp::input] click complete session={session}"); - Ok(()) -} - -async fn mouse_event( - cdp: &mut CdpConn, - session: &str, - kind: &str, - x: f64, - y: f64, - click_count: u32, -) -> Result { - log::debug!( - "[cdp::input] mouse_event session={session} kind={kind} x={x:.1} y={y:.1} clicks={click_count}" - ); - cdp.call( - "Input.dispatchMouseEvent", - json!({ - "type": kind, - "x": x, - "y": y, - "button": "left", - "buttons": if kind == "mousePressed" { 1 } else { 0 }, - "clickCount": click_count, - }), - Some(session), - ) - .await - .map_err(|e| format!("Input.dispatchMouseEvent {kind}: {e}")) -} - -/// Type a literal string by dispatching one `keyDown`/`char`/`keyUp` -/// triple per character. CDP's `dispatchKeyEvent type=char` is what -/// actually inserts text into focused editable fields — `keyDown` -/// alone leaves the input empty for most letters. The `keyDown` -/// + `keyUp` pair is still needed so listeners (autocomplete, -/// keystroke counters) see a normal keystroke. -pub async fn type_text(cdp: &mut CdpConn, session: &str, text: &str) -> Result<(), String> { - log::debug!( - "[cdp::input] type_text session={session} chars={}", - text.chars().count() - ); - for ch in text.chars() { - let s = ch.to_string(); - // keyDown — Gmail's command/keyboard router observes these. - cdp.call( - "Input.dispatchKeyEvent", - json!({ - "type": "keyDown", - "text": s, - "unmodifiedText": s, - "key": s, - }), - Some(session), - ) - .await - .map_err(|e| format!("Input.dispatchKeyEvent keyDown {ch:?}: {e}"))?; - // char — actual text insertion into the focused editable. - cdp.call( - "Input.dispatchKeyEvent", - json!({ - "type": "char", - "text": s, - "unmodifiedText": s, - "key": s, - }), - Some(session), - ) - .await - .map_err(|e| format!("Input.dispatchKeyEvent char {ch:?}: {e}"))?; - cdp.call( - "Input.dispatchKeyEvent", - json!({ - "type": "keyUp", - "text": s, - "unmodifiedText": s, - "key": s, - }), - Some(session), - ) - .await - .map_err(|e| format!("Input.dispatchKeyEvent keyUp {ch:?}: {e}"))?; - } - log::debug!("[cdp::input] type_text complete session={session}"); - Ok(()) -} - -/// Press a non-character key (Enter, Esc, …). Sends `rawKeyDown` → -/// `keyUp`; no `char` because non-printables don't insert text. -pub async fn press_key(cdp: &mut CdpConn, session: &str, key: Key) -> Result<(), String> { - let (key_name, code, vk) = key.cdp_fields(); - log::debug!("[cdp::input] press_key session={session} key={key_name}"); - cdp.call( - "Input.dispatchKeyEvent", - json!({ - "type": "rawKeyDown", - "key": key_name, - "code": code, - "windowsVirtualKeyCode": vk, - "nativeVirtualKeyCode": vk, - }), - Some(session), - ) - .await - .map_err(|e| format!("Input.dispatchKeyEvent rawKeyDown {key_name}: {e}"))?; - cdp.call( - "Input.dispatchKeyEvent", - json!({ - "type": "keyUp", - "key": key_name, - "code": code, - "windowsVirtualKeyCode": vk, - "nativeVirtualKeyCode": vk, - }), - Some(session), - ) - .await - .map_err(|e| format!("Input.dispatchKeyEvent keyUp {key_name}: {e}"))?; - log::debug!("[cdp::input] press_key complete session={session} key={key_name}"); - Ok(()) -} - -/// Dispatch Cmd/Ctrl+A to select-all in the focused contenteditable / input. -/// Useful when the search box already has a previous query in it that -/// we need to overwrite — Gmail keeps the last query rendered in the -/// search input so a fresh visit sees stale text. -pub async fn select_all_in_focused(cdp: &mut CdpConn, session: &str) -> Result<(), String> { - log::debug!( - "[cdp::input] select_all_in_focused session={session} modifier={SELECT_ALL_MODIFIER}" - ); - cdp.call( - "Input.dispatchKeyEvent", - json!({ - "type": "rawKeyDown", - "key": "a", - "code": "KeyA", - "windowsVirtualKeyCode": 65, - "nativeVirtualKeyCode": 65, - "modifiers": SELECT_ALL_MODIFIER, - }), - Some(session), - ) - .await - .map_err(|e| format!("Input.dispatchKeyEvent select-all keyDown: {e}"))?; - cdp.call( - "Input.dispatchKeyEvent", - json!({ - "type": "keyUp", - "key": "a", - "code": "KeyA", - "windowsVirtualKeyCode": 65, - "nativeVirtualKeyCode": 65, - "modifiers": SELECT_ALL_MODIFIER, - }), - Some(session), - ) - .await - .map_err(|e| format!("Input.dispatchKeyEvent select-all keyUp: {e}"))?; - log::debug!("[cdp::input] select_all_in_focused complete session={session}"); - Ok(()) -} diff --git a/app/src-tauri/src/cdp/mod.rs b/app/src-tauri/src/cdp/mod.rs index 3c360e5a68..17cede9959 100644 --- a/app/src-tauri/src/cdp/mod.rs +++ b/app/src-tauri/src/cdp/mod.rs @@ -6,29 +6,21 @@ //! and `Webview::on_dev_tools_protocol`. There is no listener and no //! network surface; any same-UID process is shut out by construction. //! -//! Scanners pick up a [`CdpConn`] either via [`conn_for_account`] (for -//! `acct_`-labelled webviews) or [`conn_for_label`] / -//! [`connect_and_attach_matching_in_process_by_label`] (for other +//! Scanners pick up a [`CdpConn`] either via [`target::conn_for_account`] (for +//! `acct_`-labelled webviews) or [`target::conn_for_label`] / +//! [`target::connect_and_attach_matching_in_process_by_label`] (for other //! surfaces such as the Meet call window). pub mod conn; pub mod in_process; -pub mod input; pub mod session; pub mod snapshot; pub mod target; pub use conn::CdpConn; -pub use in_process::{ - install_for_account, install_for_label, install_for_webview, set_cef_app_handle, CdpRegistry, - EventFrame, WebviewCdpTransport, CALL_TIMEOUT, -}; +pub use in_process::{install_for_account, install_for_label, set_cef_app_handle, CdpRegistry}; pub use session::{ placeholder_marker, placeholder_url, spawn_session, target_url_fragment, SpawnedSession, }; -#[allow(unused_imports)] // `Rect` re-export consumed once turn 2 lands; keep stable. -pub use snapshot::{Rect, Snapshot}; -pub use target::{ - conn_for_account, conn_for_label, connect_and_attach_matching_in_process, - connect_and_attach_matching_in_process_by_label, detach_session, find_page_target_where, -}; +pub use snapshot::Snapshot; +pub use target::{detach_session, find_page_target_where}; diff --git a/app/src-tauri/src/cdp/session.rs b/app/src-tauri/src/cdp/session.rs index 92a0626ba9..77a8d429b9 100644 --- a/app/src-tauri/src/cdp/session.rs +++ b/app/src-tauri/src/cdp/session.rs @@ -24,8 +24,8 @@ use tokio::task::JoinHandle; // elapsed check honours `tokio::time::pause()` / `advance()` in unit tests. use tokio::time::{sleep, Instant}; +use super::find_page_target_where; use super::target::conn_for_account; -use super::{find_page_target_where, CdpConn}; use crate::webview_accounts::{emit_load_finished, redact_url_for_log, RevealTrigger}; /// Backoff between failed attach attempts / reconnects. Intentionally @@ -517,7 +517,7 @@ async fn run_session_cycle( // page reaches the CEF helper's notify-IPC, which posts back to // `forward_native_notification` in `webview_accounts`. Without it, // the constructor silently no-ops and no toast ever fires (#1016). - if let Some(origin) = origin_of(&real_url) { + if let Some(origin) = origin_of(real_url) { // Default permission set every embedded provider needs. Origin-scoped // so we don't leak grants across providers running in the same CEF // browser process. diff --git a/app/src-tauri/src/cdp/snapshot.rs b/app/src-tauri/src/cdp/snapshot.rs index 9228e3675e..dc33a1b3ce 100644 --- a/app/src-tauri/src/cdp/snapshot.rs +++ b/app/src-tauri/src/cdp/snapshot.rs @@ -33,35 +33,6 @@ struct CaptureSnapshot { struct DocumentSnap { #[serde(default)] nodes: NodeTreeSnap, - #[serde(default)] - layout: LayoutSnap, -} - -/// Subset of `documents[i].layout` we care about. Each layout node -/// references a DOM node by index and carries its `[x, y, w, h]` bounds -/// in CSS pixels. Only populated when `includeDOMRects: true` is passed -/// to `DOMSnapshot.captureSnapshot`. -#[derive(Deserialize, Debug, Default)] -struct LayoutSnap { - #[serde(rename = "nodeIndex", default)] - node_index: Vec, - #[serde(default)] - bounds: Vec>, -} - -/// Element bounding rect in CSS pixels relative to the viewport. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Rect { - pub x: f64, - pub y: f64, - pub width: f64, - pub height: f64, -} - -impl Rect { - pub fn center(self) -> (f64, f64) { - (self.x + self.width / 2.0, self.y + self.height / 2.0) - } } #[derive(Deserialize, Debug, Default)] @@ -82,46 +53,38 @@ pub struct Snapshot { strings: Vec, nodes: NodeTreeSnap, children: Vec>, - /// `rects[node_idx]` is `Some(Rect)` when layout info was requested - /// AND the node has a layout box (text + element nodes do; pure - /// metadata like `` doesn't). `None` otherwise. - rects: Vec>, } impl Snapshot { /// Run `DOMSnapshot.captureSnapshot` on an attached session and return - /// the parsed main-document tree. Iframes are ignored — none of the - /// migrated providers render chat lists inside iframes. + /// one parsed tree containing the main document and any iframe documents. pub async fn capture(cdp: &mut CdpConn, session: &str) -> Result { - Self::capture_inner(cdp, session, false).await - } - - /// Same as [`capture`] but also requests element bounding rects. - /// Use when the caller needs to drive `Input.dispatchMouseEvent` — - /// pulling rects on every snapshot adds protocol overhead, so the - /// cheap path stays cheap. - pub async fn capture_with_rects(cdp: &mut CdpConn, session: &str) -> Result { - Self::capture_inner(cdp, session, true).await - } - - async fn capture_inner( - cdp: &mut CdpConn, - session: &str, - with_rects: bool, - ) -> Result { + log::debug!("[cdp::snapshot] capture start session={session}"); let raw = cdp .call( "DOMSnapshot.captureSnapshot", - json!({ - "computedStyles": [], - "includePaintOrder": false, - "includeDOMRects": with_rects, - }), + capture_request(), Some(session), ) - .await?; - let snap: CaptureSnapshot = - serde_json::from_value(raw).map_err(|e| format!("decode DOMSnapshot: {e}"))?; + .await + .map_err(|error| { + log::warn!("[cdp::snapshot] capture call failed session={session} error={error}"); + error + })?; + log::debug!("[cdp::snapshot] capture call complete session={session}"); + let snap: CaptureSnapshot = serde_json::from_value(raw).map_err(|error| { + log::warn!("[cdp::snapshot] decode failed session={session} error={error}"); + format!("decode DOMSnapshot: {error}") + })?; + let snapshot = Self::from_capture(snap); + log::debug!( + "[cdp::snapshot] decode complete session={session} nodes={}", + snapshot.len() + ); + Ok(snapshot) + } + + fn from_capture(snap: CaptureSnapshot) -> Self { let strings = snap.strings; // Merge every document (main frame + all iframes) into a single // flat node array. CDP returns each frame as its own document @@ -138,12 +101,9 @@ impl Snapshot { let mut merged_node_name: Vec = Vec::new(); let mut merged_node_value: Vec = Vec::new(); let mut merged_attributes: Vec> = Vec::new(); - let mut merged_layout_node_index: Vec = Vec::new(); - let mut merged_layout_bounds: Vec> = Vec::new(); for document in snap.documents { let doc_offset = merged_node_type.len() as i32; let doc_nodes = document.nodes; - let doc_count = doc_nodes.node_type.len(); for &p in &doc_nodes.parent_index { merged_parent_index.push(if p < 0 { -1 } else { p + doc_offset }); } @@ -162,12 +122,6 @@ impl Snapshot { while merged_attributes.len() < merged_node_type.len() { merged_attributes.push(Vec::new()); } - // Per-document layout indices need the same offset. - for &li in &document.layout.node_index { - merged_layout_node_index.push(if li < 0 { -1 } else { li + doc_offset }); - } - merged_layout_bounds.extend(document.layout.bounds); - let _ = doc_count; } let nodes = NodeTreeSnap { parent_index: merged_parent_index, @@ -176,10 +130,6 @@ impl Snapshot { node_value: merged_node_value, attributes: merged_attributes, }; - let layout = LayoutSnap { - node_index: merged_layout_node_index, - bounds: merged_layout_bounds, - }; let count = nodes.node_type.len(); let mut children: Vec> = vec![Vec::new(); count]; for (i, &p) in nodes.parent_index.iter().enumerate() { @@ -187,34 +137,11 @@ impl Snapshot { children[p as usize].push(i); } } - let mut rects: Vec> = vec![None; count]; - if with_rects { - for (layout_i, &node_i) in layout.node_index.iter().enumerate() { - if node_i < 0 { - continue; - } - let node_i = node_i as usize; - if node_i >= count { - continue; - } - let bounds = match layout.bounds.get(layout_i) { - Some(b) if b.len() >= 4 => b, - _ => continue, - }; - rects[node_i] = Some(Rect { - x: bounds[0], - y: bounds[1], - width: bounds[2], - height: bounds[3], - }); - } - } - Ok(Self { + Self { strings, nodes, children, - rects, - }) + } } pub fn len(&self) -> usize { @@ -268,13 +195,6 @@ impl Snapshot { self.children.get(idx).map(|v| v.as_slice()).unwrap_or(&[]) } - /// Layout rect for `idx`. `None` when the snapshot was captured - /// without `includeDOMRects` OR the node has no layout box (e.g. - /// ``, comment nodes, `display:none` elements). - pub fn rect(&self, idx: usize) -> Option { - self.rects.get(idx).copied().flatten() - } - /// Depth-first pre-order walk of every descendant of `root` (including /// `root` itself). Cheap enough for chat-list scrapes that run every /// 2 seconds — DOM has thousands of nodes, not millions. @@ -336,6 +256,14 @@ impl Snapshot { } } +fn capture_request() -> serde_json::Value { + json!({ + "computedStyles": [], + "includePaintOrder": false, + "includeDOMRects": false, + }) +} + fn collapse_ws(s: &str) -> String { let mut out = String::with_capacity(s.len()); let mut last_space = true; @@ -357,6 +285,51 @@ fn collapse_ws(s: &str) -> String { mod tests { use super::*; + #[test] + fn capture_request_disables_dom_rects() { + let request = capture_request(); + assert_eq!(request["includeDOMRects"], false); + assert_eq!(request["includePaintOrder"], false); + assert_eq!(request["computedStyles"], json!([])); + } + + #[test] + fn from_capture_offsets_documents_and_builds_child_adjacency_without_layout() { + let capture: CaptureSnapshot = serde_json::from_value(json!({ + "strings": ["DIV", "first", "SPAN", "second"], + "documents": [ + { + "nodes": { + "parentIndex": [-1, 0], + "nodeType": [1, 3], + "nodeName": [0, -1], + "nodeValue": [-1, 1] + } + }, + { + "nodes": { + "parentIndex": [-1, 0], + "nodeType": [1, 3], + "nodeName": [2, -1], + "nodeValue": [-1, 3] + } + } + ] + })) + .expect("snapshot fixture should decode without layout data"); + + let snapshot = Snapshot::from_capture(capture); + + assert_eq!(snapshot.len(), 4); + assert_eq!(snapshot.children(0), &[1]); + assert_eq!(snapshot.children(2), &[3]); + assert!(snapshot.children(1).is_empty()); + assert_eq!(snapshot.tag(0), "DIV"); + assert_eq!(snapshot.tag(2), "SPAN"); + assert_eq!(snapshot.text_content(0), "first"); + assert_eq!(snapshot.text_content(2), "second"); + } + #[test] fn collapse_ws_collapses_and_trims() { assert_eq!(collapse_ws(" hello world "), "hello world"); diff --git a/app/src-tauri/src/cdp/target.rs b/app/src-tauri/src/cdp/target.rs index 93e8736213..7a57c8502e 100644 --- a/app/src-tauri/src/cdp/target.rs +++ b/app/src-tauri/src/cdp/target.rs @@ -18,7 +18,6 @@ pub struct CdpTarget { pub id: String, pub kind: String, pub url: String, - pub title: String, } /// Parse the response of a `Target.getTargets` CDP call into a list of @@ -38,11 +37,6 @@ pub fn parse_targets(v: &Value) -> Vec { .and_then(|u| u.as_str()) .unwrap_or("") .to_string(), - title: t - .get("title") - .and_then(|u| u.as_str()) - .unwrap_or("") - .to_string(), }) }) .collect() diff --git a/app/src-tauri/src/claude_code.rs b/app/src-tauri/src/claude_code.rs index f3b3a3deec..c70f960cc0 100644 --- a/app/src-tauri/src/claude_code.rs +++ b/app/src-tauri/src/claude_code.rs @@ -61,7 +61,7 @@ end tell"#; Err(_) => continue, } } - return Err("no terminal emulator found (tried x-terminal-emulator, gnome-terminal, konsole, xfce4-terminal, xterm). Run `claude login` manually.".into()); + Err("no terminal emulator found (tried x-terminal-emulator, gnome-terminal, konsole, xfce4-terminal, xterm). Run `claude login` manually.".into()) } #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] diff --git a/app/src-tauri/src/deep_link_ipc.rs b/app/src-tauri/src/deep_link_ipc.rs index 42fe98b869..857ee4a855 100644 --- a/app/src-tauri/src/deep_link_ipc.rs +++ b/app/src-tauri/src/deep_link_ipc.rs @@ -108,13 +108,15 @@ pub(crate) fn try_forward_deep_links() -> ForwardResult { // Pending URLs collected before setup() has an app handle. static PENDING_URLS: OnceLock>>> = OnceLock::new(); // Live handler installed by drain_pending_urls — dispatches directly to app. -static LIVE_HANDLER: OnceLock>>> = OnceLock::new(); +type LiveHandler = Box; +type LiveHandlerSlot = Mutex>; +static LIVE_HANDLER: OnceLock = OnceLock::new(); fn pending_queue() -> &'static Arc>> { PENDING_URLS.get_or_init(|| Arc::new(Mutex::new(Vec::new()))) } -fn live_handler() -> &'static Mutex>> { +fn live_handler() -> &'static LiveHandlerSlot { LIVE_HANDLER.get_or_init(|| Mutex::new(None)) } diff --git a/app/src-tauri/src/discord_scanner/mod_tests.rs b/app/src-tauri/src/discord_scanner/mod_tests.rs index ad7345d2d8..7278419079 100644 --- a/app/src-tauri/src/discord_scanner/mod_tests.rs +++ b/app/src-tauri/src/discord_scanner/mod_tests.rs @@ -339,7 +339,6 @@ fn page(id: &str, url: &str) -> crate::cdp::target::CdpTarget { id: id.to_string(), kind: "page".to_string(), url: url.to_string(), - title: String::new(), } } @@ -409,7 +408,6 @@ fn resolve_none_when_no_prefix_target() { id: "iframe".to_string(), kind: "iframe".to_string(), url: "https://discord.com/channels/@me".to_string(), - title: String::new(), }, ]; assert!( diff --git a/app/src-tauri/src/imessage_scanner/mod.rs b/app/src-tauri/src/imessage_scanner/mod.rs index 2593190a8f..289fd7cac0 100644 --- a/app/src-tauri/src/imessage_scanner/mod.rs +++ b/app/src-tauri/src/imessage_scanner/mod.rs @@ -465,13 +465,6 @@ impl ScannerRegistry { pub fn new() -> Self { Self } - pub fn ensure_scanner( - self: std::sync::Arc, - _app: tauri::AppHandle, - _account_id: String, - ) { - } - pub fn shutdown(&self) {} } diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index 9cac2be8d8..5ca2b6375b 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -399,9 +399,7 @@ async fn restart_app(app: tauri::AppHandle) -> Result<(), String> { perform_early_teardown_async(&app).await; log::info!("[app] restart_app — early teardown complete, restarting"); - app.restart(); - // restart() does not return, but we must satisfy the signature - Ok(()) + app.restart() } /// Read the authoritative active user id from `active_user.toml` so the @@ -1214,7 +1212,7 @@ fn mascot_native_window_is_open() -> bool { mascot_native_window::is_open() } -#[cfg(not(target_os = "macos"))] +#[cfg(all(not(target_os = "macos"), not(target_os = "linux")))] fn mascot_native_window_is_open() -> bool { false } @@ -1848,7 +1846,9 @@ async fn perform_early_teardown_async(app_handle: &AppHandle) { log::info!("[app] perform_early_teardown_async — early teardown complete"); } -/// Explicitly winds down CEF and Tauri before an app.exit(0) +/// Explicitly wind down CEF and the core before exiting from desktop-owned +/// quit actions. Linux does not build the tray or macOS application menu. +#[cfg(not(target_os = "linux"))] fn shutdown_app_sync(app_handle: &AppHandle, exit_code: i32) { log::info!("[app] shutdown_app_sync — starting early teardown"); perform_early_teardown_sync_once(app_handle, "shutdown_app_sync"); diff --git a/app/src-tauri/src/meet_audio/caption_listener.rs b/app/src-tauri/src/meet_audio/caption_listener.rs index 637c4ee7bc..e7e07878ae 100644 --- a/app/src-tauri/src/meet_audio/caption_listener.rs +++ b/app/src-tauri/src/meet_audio/caption_listener.rs @@ -2,8 +2,8 @@ //! `captions_bridge.js` we install at session start, and forwards each //! new line to core's `meet_agent_push_caption` RPC. //! -//! Replaces the old [`super::listen_capture`] (CEF audio handler → -//! Whisper STT) which proved unreliable: CEF's `cef_audio_handler_t` +//! Replaces the old CEF audio-handler and Whisper STT path, which proved +//! unreliable: CEF's `cef_audio_handler_t` //! is queried lazily on first audio output, so a solo agent in a //! lobby never engaged the pipeline. Captions handle that case for //! free — Meet's STT is already running, speaker-attributed, and @@ -34,7 +34,6 @@ const MAX_CONSECUTIVE_ERRORS: u32 = 30; /// RAII handle. Drop to stop the listener task. pub struct CaptionListener { - pub request_id: String, pub(crate) _shutdown_tx: Option>, } @@ -88,7 +87,6 @@ pub fn start(request_id: String, cdp: CdpConn, session_id: String) -> CaptionLis }); CaptionListener { - request_id, _shutdown_tx: Some(shutdown_tx), } } diff --git a/app/src-tauri/src/meet_audio/listen_capture.rs b/app/src-tauri/src/meet_audio/listen_capture.rs deleted file mode 100644 index 493a52e378..0000000000 --- a/app/src-tauri/src/meet_audio/listen_capture.rs +++ /dev/null @@ -1,327 +0,0 @@ -//! Capture the embedded Meet webview's audio output and forward it to -//! the core meet_agent loop. -//! -//! ## Pipeline -//! -//! 1. `tauri_runtime_cef::audio::register_audio_handler` taps the -//! per-browser `cef_audio_handler_t`. CEF delivers planar -//! float32 PCM at the renderer's native rate (typically 48 kHz, -//! 1–2 channels) directly from the audio output device path — -//! *before* it hits the OS speaker. No system permission needed. -//! -//! 2. Downsample-to-mono runs inline on the CEF audio thread: -//! - average across channels → mono float32 -//! - linear-interpolate down to 16 kHz (the rate `voice::streaming` -//! and the smoke test in `meet_agent::session` expect) -//! - clamp + scale to PCM16LE -//! -//! 3. Accumulate ~100 ms per chunk (1 600 samples @ 16 kHz). We push -//! via the core RPC on every flush boundary; smaller pushes would -//! overload the JSON envelope, larger ones would slow VAD. -//! -//! 4. RPC pushes are spawned on the tokio runtime so the audio -//! callback never blocks on network IO. A bounded channel -//! backpressures: if core is wedged, we drop the oldest queued -//! chunk rather than holding CEF's audio thread. - -use std::sync::{Arc, Mutex}; - -use tauri_runtime_cef::audio::{ - register_audio_handler, AudioHandlerRegistration, AudioStreamEvent, -}; -use tokio::sync::mpsc; - -const TARGET_SAMPLE_RATE: u32 = 16_000; -/// 100 ms @ 16 kHz mono. `meet_agent::ops::Vad` pushes hangover counts -/// based on per-frame cadence, so changing this changes the VAD wall -/// time too. 100 ms feels responsive without burning RPC. -const FLUSH_SAMPLES: usize = (TARGET_SAMPLE_RATE as usize) / 10; -/// Bounded channel between the CEF callback (producer) and the -/// async-runtime forwarder (consumer). 32 chunks ≈ 3.2 s at the flush -/// cadence — generous slack for transient core latency, but bounded -/// so a wedged core can't OOM us. -const FORWARD_CHANNEL_CAPACITY: usize = 32; - -/// RAII handle. Drop to release the CEF audio registration and shut -/// down the forwarder task. Both happen synchronously — the channel -/// closes first, the task exits its recv loop, and the registration -/// drop unhooks CEF in the same tick. -pub struct ListenSession { - pub request_id: String, - _registration: AudioHandlerRegistration, - /// Held so `Drop` closes the channel even if there are no in-flight - /// chunks. The forwarder task observes the close and exits. - _shutdown_tx: mpsc::Sender>, -} - -/// Opens the audio capture for `meet_url`. The same exact URL must -/// have been used to build the CEF window — `register_audio_handler` -/// matches by prefix. -pub fn start(meet_url: &str, request_id: String) -> Result { - let (tx, rx) = mpsc::channel::>(FORWARD_CHANNEL_CAPACITY); - let resampler = Arc::new(Mutex::new(Resampler::new())); - - let resampler_for_handler = resampler.clone(); - let tx_for_handler = tx.clone(); - let request_id_for_log = request_id.clone(); - let registration = register_audio_handler(meet_url.to_string(), move |event| { - on_audio_event( - &request_id_for_log, - event, - &resampler_for_handler, - &tx_for_handler, - ); - }); - - spawn_forwarder(request_id.clone(), rx); - - log::info!( - "[meet-audio] listen registered request_id={} url_chars={}", - request_id, - meet_url.chars().count() - ); - - Ok(ListenSession { - request_id, - _registration: registration, - _shutdown_tx: tx, - }) -} - -/// Process one CEF audio event. Speech/Stopped/Error all flow through -/// here; only `Packet` produces RPC traffic, but the others are logged -/// at info so an aborted call leaves a breadcrumb in the file logs. -fn on_audio_event( - request_id: &str, - event: AudioStreamEvent, - resampler: &Arc>, - tx: &mpsc::Sender>, -) { - match event { - AudioStreamEvent::Started { - sample_rate_hz, - channels, - frames_per_buffer, - } => { - log::info!( - "[meet-audio] cef stream start request_id={request_id} hz={sample_rate_hz} channels={channels} frames_per_buffer={frames_per_buffer}" - ); - if let Ok(mut r) = resampler.lock() { - r.reset(sample_rate_hz as u32); - } - } - AudioStreamEvent::Packet { - channels: planes, - pts_ms: _, - } => { - let pcm_bytes = match resampler.lock() { - Ok(mut r) => r.feed_and_drain(&planes), - Err(_) => return, - }; - for chunk in pcm_bytes.chunks(FLUSH_SAMPLES * 2) { - // `try_send` drops the chunk on a full channel rather - // than blocking the CEF audio thread. Better to lose - // a frame than to stall the renderer. - if tx.try_send(chunk.to_vec()).is_err() { - log::warn!( - "[meet-audio] forward channel full; dropping {} bytes request_id={request_id}", - chunk.len() - ); - } - } - } - AudioStreamEvent::Stopped => { - log::info!("[meet-audio] cef stream stopped request_id={request_id}"); - if let Ok(mut r) = resampler.lock() { - r.reset(0); - } - } - AudioStreamEvent::Error(msg) => { - log::warn!("[meet-audio] cef stream error request_id={request_id} msg={msg}"); - } - } -} - -/// Pull chunks off the bounded channel and POST each to core. Lives in -/// its own task so the CEF callback never blocks on HTTP. -fn spawn_forwarder(request_id: String, mut rx: mpsc::Receiver>) { - tauri::async_runtime::spawn(async move { - use base64::{engine::general_purpose::STANDARD as B64, Engine as _}; - while let Some(chunk) = rx.recv().await { - let pcm_b64 = B64.encode(&chunk); - let res = super::rpc_call( - "openhuman.meet_agent_push_listen_pcm", - serde_json::json!({ - "request_id": request_id, - "pcm_base64": pcm_b64, - }), - ) - .await; - if let Err(err) = res { - log::debug!( - "[meet-audio] push_listen_pcm err request_id={request_id} bytes={} err={err}", - chunk.len() - ); - } - } - log::info!("[meet-audio] forwarder exiting request_id={request_id}"); - }); -} - -/// Stateful float32-planar → PCM16LE mono @ 16 kHz resampler. -/// -/// Uses linear interpolation, which is good enough for speech (the -/// downstream STT does not care about ultrasonics or pristine high -/// frequencies). Carry the previous sample across `feed_and_drain` -/// calls so we don't introduce a tick at every CEF buffer boundary. -/// Pick a source sample by signed index. Negative indices return the -/// carry sample from the previous call (so phase < 0 keeps the -/// interpolation continuous across buffer boundaries); past-the-end -/// indices clamp to the last sample (which is what the next call will -/// install as its own carry, so the output stays smooth even if a -/// caller stops feeding mid-stream). -fn sample_at(mono: &[f32], carry: f32, idx: i64) -> f32 { - if idx < 0 { - carry - } else if (idx as usize) < mono.len() { - mono[idx as usize] - } else { - *mono.last().unwrap_or(&0.0) - } -} - -struct Resampler { - source_rate_hz: u32, - /// Fractional position into the source buffer between calls. - /// 0.0 means "start cleanly with the next sample". Negative is - /// not used — the source rate is always known before we feed. - phase: f64, - /// Last source sample of the previous call, used as the "left" - /// neighbour when we interpolate the first sample of the next call. - last_sample: f32, -} - -impl Resampler { - fn new() -> Self { - Self { - source_rate_hz: 0, - phase: 0.0, - last_sample: 0.0, - } - } - - fn reset(&mut self, source_rate_hz: u32) { - self.source_rate_hz = source_rate_hz; - self.phase = 0.0; - self.last_sample = 0.0; - } - - fn feed_and_drain(&mut self, planes: &[Vec]) -> Vec { - if planes.is_empty() || self.source_rate_hz == 0 { - return Vec::new(); - } - let frames = planes[0].len(); - if frames == 0 { - return Vec::new(); - } - // Mono mix. - let mono: Vec = (0..frames) - .map(|i| { - let mut sum = 0.0_f32; - for plane in planes { - if let Some(v) = plane.get(i) { - sum += *v; - } - } - sum / planes.len() as f32 - }) - .collect(); - - let ratio = self.source_rate_hz as f64 / TARGET_SAMPLE_RATE as f64; - let mut out = Vec::with_capacity((mono.len() as f64 / ratio).ceil() as usize * 2); - // `pos` floats through `mono` indices. `pos < 0` means "still - // sampling the carry sample from the previous call"; `pos = 0` - // means "right at mono[0]". - let mut pos = self.phase; - while pos < mono.len() as f64 { - let idx_f = pos.floor(); - let frac = pos - idx_f; - let idx = idx_f as i64; - let s_left = sample_at(mono.as_slice(), self.last_sample, idx); - let s_right = sample_at(mono.as_slice(), self.last_sample, idx + 1); - let sample = s_left as f64 + (s_right as f64 - s_left as f64) * frac; - // Float32 [-1.0, 1.0] → i16. Clamp because Chromium can - // overshoot a touch on heavy compression. - let s_i16 = (sample.clamp(-1.0, 1.0) * i16::MAX as f64) as i16; - out.extend_from_slice(&s_i16.to_le_bytes()); - pos += ratio; - } - // Carry the trailing fractional position into the next call. - // It will be negative when we overshot (next call resumes - // mid-source-sample), so the next call interpolates between - // `last_sample` and the new mono[0]. - self.phase = pos - mono.len() as f64; - self.last_sample = *mono.last().unwrap_or(&0.0); - out - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn resampler_with_no_source_rate_yields_nothing() { - let mut r = Resampler::new(); - let out = r.feed_and_drain(&[vec![0.5; 100]]); - assert!(out.is_empty(), "no source rate set, must produce nothing"); - } - - #[test] - fn resampler_48k_to_16k_mono_drops_samples_3to1() { - let mut r = Resampler::new(); - r.reset(48_000); - let plane = vec![0.5_f32; 4_800]; // 100ms @ 48k - let bytes = r.feed_and_drain(&[plane]); - // 100ms @ 16k = 1600 samples * 2 bytes. Allow ±2 samples slop - // from the fractional phase carry. - let samples = bytes.len() / 2; - assert!( - (1598..=1602).contains(&samples), - "expected ~1600 samples, got {samples}" - ); - } - - #[test] - fn resampler_stereo_to_mono_averages_channels() { - let mut r = Resampler::new(); - r.reset(16_000); - let left = vec![0.8_f32; 1600]; - let right = vec![-0.2_f32; 1600]; - let bytes = r.feed_and_drain(&[left, right]); - // Avg = 0.3 → ~9830 in i16. First two bytes are LE i16. - let first = i16::from_le_bytes([bytes[0], bytes[1]]); - assert!( - (9000..11000).contains(&first), - "expected mid-amplitude i16, got {first}" - ); - } - - #[test] - fn resampler_clamps_out_of_range_floats() { - let mut r = Resampler::new(); - r.reset(16_000); - let bytes = r.feed_and_drain(&[vec![5.0_f32; 100]]); - let first = i16::from_le_bytes([bytes[0], bytes[1]]); - assert_eq!(first, i16::MAX); - } - - #[test] - fn resampler_passthrough_when_rates_match() { - let mut r = Resampler::new(); - r.reset(16_000); - let plane = vec![0.5_f32; 1600]; - let bytes = r.feed_and_drain(&[plane]); - assert_eq!(bytes.len(), 1600 * 2); - } -} diff --git a/app/src-tauri/src/meet_audio/mod.rs b/app/src-tauri/src/meet_audio/mod.rs index a60f582b9b..5af6674b49 100644 --- a/app/src-tauri/src/meet_audio/mod.rs +++ b/app/src-tauri/src/meet_audio/mod.rs @@ -2,12 +2,8 @@ //! //! ## Pieces //! -//! - [`listen_capture`] — taps the embedded Meet webview's audio output -//! via the per-browser `CefAudioHandler` exposed by our vendored -//! `tauri-runtime-cef::audio` extension, downsamples to 16 kHz mono -//! PCM16LE, batches into ~100 ms chunks, and posts them to core via -//! `openhuman.meet_agent_push_listen_pcm`. Zero OS-level audio -//! permission needed: we read frames straight out of the renderer. +//! - [`caption_listener`] — drains Meet's built-in captions through CDP +//! and forwards speaker-attributed lines to core. //! //! - [`speak_pump`] — drains synthesized PCM the brain enqueued (via //! `openhuman.meet_agent_poll_speech`) and writes it into the @@ -19,15 +15,12 @@ //! //! [`start`] is invoked once the meet-call window has been built (in //! `meet_call::meet_call_open_window`). It opens the core session, -//! registers the audio handler keyed by the call's URL, and spawns the -//! poll-speech loop. [`stop`] runs from the window-destroyed handler: -//! it drops the audio handler registration (which silences capture -//! immediately), stops the speak pump, and tells core to close the -//! session and report counters. +//! starts the caption listener and poll-speech loop. [`stop`] runs from +//! the window-destroyed handler: it stops both tasks and tells core to +//! close the session and report counters. pub mod caption_listener; pub mod inject; -pub mod listen_capture; pub mod speak_pump; use std::collections::HashMap; @@ -54,14 +47,8 @@ impl MeetAudioState { /// teardown synchronously — no async drop needed because the caption /// listener and speak pump both shut down on signal/drop. /// -/// The legacy CEF-audio `listen_capture::ListenSession` is kept as an -/// optional field so the pre-register flow still has somewhere to -/// hand the registration off if a future build re-enables it. In the -/// caption-driven path it stays `None`. pub struct MeetAudioSession { - pub request_id: String, _captions: caption_listener::CaptionListener, - _legacy_listen: Option, _speak: speak_pump::SpeakPump, } @@ -229,9 +216,7 @@ pub async fn start( state.inner.lock().unwrap().insert( request_id.clone(), MeetAudioSession { - request_id: request_id.clone(), _captions: captions, - _legacy_listen: None, _speak: speak, }, ); @@ -353,11 +338,8 @@ pub(crate) async fn rpc_call( /// No-op caption listener used when CDP attach failed at session /// start. Lets the rest of the lifecycle hold a uniform value. -fn caption_listener_disabled(request_id: String) -> caption_listener::CaptionListener { - caption_listener::CaptionListener { - request_id, - _shutdown_tx: None, - } +fn caption_listener_disabled(_request_id: String) -> caption_listener::CaptionListener { + caption_listener::CaptionListener { _shutdown_tx: None } } /// Trim a string for logging without panicking on multi-byte chars. diff --git a/app/src-tauri/src/meet_audio/speak_pump.rs b/app/src-tauri/src/meet_audio/speak_pump.rs index 2cda72a79d..a8ab64a743 100644 --- a/app/src-tauri/src/meet_audio/speak_pump.rs +++ b/app/src-tauri/src/meet_audio/speak_pump.rs @@ -48,7 +48,6 @@ const SPEAKING_STATE_EVENT: &str = "meet-video:speaking-state"; /// RAII handle. Drop to stop the pump task. The shutdown channel /// causes the spawned loop to exit on the next select tick. pub struct SpeakPump { - pub request_id: String, _shutdown_tx: Option>, } @@ -130,7 +129,6 @@ pub fn start( }); SpeakPump { - request_id, _shutdown_tx: Some(shutdown_tx), } } @@ -277,11 +275,8 @@ fn next_speaking_state( /// No-op pump used when bridge install failed at session start. Keeps /// the rest of the session lifecycle uniform — `MeetAudioSession` can /// still hold a `SpeakPump` regardless of speak-path readiness. -pub fn start_disabled(request_id: String) -> SpeakPump { - SpeakPump { - request_id, - _shutdown_tx: None, - } +pub fn start_disabled(_request_id: String) -> SpeakPump { + SpeakPump { _shutdown_tx: None } } /// Run a single pump tick. Returns `true` when the tick actually diff --git a/app/src-tauri/src/meet_call/mod.rs b/app/src-tauri/src/meet_call/mod.rs index 592aa2789b..809b7355ec 100644 --- a/app/src-tauri/src/meet_call/mod.rs +++ b/app/src-tauri/src/meet_call/mod.rs @@ -134,8 +134,8 @@ pub async fn meet_call_open_window( } // Only one meet-call window can be live at a time — concurrent bot - // sessions race the CEF audio handler registration (`listen_capture`) - // and confuse the user with multiple "Meet — OpenHuman" windows in + // sessions race the shared audio bridge and confuse the user with + // multiple "Meet — OpenHuman" windows in // their Dock. Close any stragglers from a prior Join before opening // a fresh one. The CloseRequested handler will tear down their // scanner + audio session via the per-window event listeners below. diff --git a/app/src-tauri/src/meet_scanner/mod.rs b/app/src-tauri/src/meet_scanner/mod.rs index 9e8525fd9d..1e96ee81bc 100644 --- a/app/src-tauri/src/meet_scanner/mod.rs +++ b/app/src-tauri/src/meet_scanner/mod.rs @@ -35,7 +35,7 @@ use std::time::Duration; use serde_json::{json, Value}; -use tauri::{AppHandle, Manager, Runtime}; +use tauri::{AppHandle, Runtime}; use crate::cdp::{self, CdpConn}; diff --git a/app/src-tauri/src/meet_video/frame_bus.rs b/app/src-tauri/src/meet_video/frame_bus.rs index c8454ba542..537d0bd349 100644 --- a/app/src-tauri/src/meet_video/frame_bus.rs +++ b/app/src-tauri/src/meet_video/frame_bus.rs @@ -191,14 +191,16 @@ async fn handle_connection( // while waiting for the producer's next tick. let writer = tokio::spawn(async move { let initial = latest_rx.borrow().clone(); - if !initial.is_empty() { - if sink + if !initial.is_empty() + && sink .send(Message::Binary((*initial).clone())) .await .is_err() - { - return; - } + { + log::debug!( + "[meet-video] initial WebSocket frame send failed; closing writer peer_transport=disconnected" + ); + return; } while latest_rx.changed().await.is_ok() { let frame = latest_rx.borrow().clone(); @@ -237,7 +239,6 @@ async fn handle_connection( #[cfg(test)] mod tests { use super::*; - use futures_util::{SinkExt as _, StreamExt as _}; use tokio_tungstenite::connect_async; #[tokio::test] diff --git a/app/src-tauri/src/ptt_hotkeys.rs b/app/src-tauri/src/ptt_hotkeys.rs index 743ccadb6c..04956e0401 100644 --- a/app/src-tauri/src/ptt_hotkeys.rs +++ b/app/src-tauri/src/ptt_hotkeys.rs @@ -14,8 +14,6 @@ pub(crate) enum PttError { EmptyShortcut, ModifierOnlyShortcut, ConflictsWithDictation(String), - UnsupportedOnWayland, - RegistrationFailed(String), } impl std::fmt::Display for PttError { @@ -29,13 +27,6 @@ impl std::fmt::Display for PttError { PttError::ConflictsWithDictation(s) => { write!(f, "ptt shortcut '{s}' conflicts with the dictation hotkey") } - PttError::UnsupportedOnWayland => write!( - f, - "global shortcuts are not supported in this Wayland session — switch to X11 or use in-app dictation" - ), - PttError::RegistrationFailed(s) => { - write!(f, "failed to register ptt shortcut: {s}") - } } } } diff --git a/app/src-tauri/src/ptt_overlay.rs b/app/src-tauri/src/ptt_overlay.rs index 90bdfeb50d..6cfb824e68 100644 --- a/app/src-tauri/src/ptt_overlay.rs +++ b/app/src-tauri/src/ptt_overlay.rs @@ -23,7 +23,7 @@ pub(crate) fn ensure_window(app: &AppHandle) -> Result<(), String return Ok(()); } let url = WebviewUrl::App("index.html#/ptt-overlay".into()); - let mut builder = WebviewWindowBuilder::new(app, OVERLAY_LABEL, url) + let builder = WebviewWindowBuilder::new(app, OVERLAY_LABEL, url) .title("OpenHuman Push-to-Talk") .inner_size(160.0, 56.0) .decorations(false) @@ -39,11 +39,9 @@ pub(crate) fn ensure_window(app: &AppHandle) -> Result<(), String .visible(false); #[cfg(target_os = "macos")] - { - builder = builder - .visible_on_all_workspaces(true) - .accept_first_mouse(false); - } + let builder = builder + .visible_on_all_workspaces(true) + .accept_first_mouse(false); let _window = builder .build() diff --git a/app/src-tauri/src/screen_capture/mod.rs b/app/src-tauri/src/screen_capture/mod.rs index a18ac07438..3614fd294d 100644 --- a/app/src-tauri/src/screen_capture/mod.rs +++ b/app/src-tauri/src/screen_capture/mod.rs @@ -82,6 +82,7 @@ pub struct ScreenSource { /// What kind of source a parsed DesktopMediaID-format string describes. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(any(target_os = "macos", test))] pub(crate) enum SourceKind { Screen, Window, @@ -93,6 +94,7 @@ pub(crate) enum SourceKind { /// match what the enumerator emits. Pure logic so it can be unit-tested /// without touching platform APIs; macOS callers use it before dispatching /// to the capture backend. +#[cfg(any(target_os = "macos", test))] pub(crate) fn parse_source_id(id: &str) -> Option<(SourceKind, u32)> { let mut parts = id.splitn(3, ':'); let kind = match parts.next()? { diff --git a/app/src-tauri/src/slack_scanner/extract.rs b/app/src-tauri/src/slack_scanner/extract.rs index c2a9e40aa9..9b8600b1d7 100644 --- a/app/src-tauri/src/slack_scanner/extract.rs +++ b/app/src-tauri/src/slack_scanner/extract.rs @@ -35,16 +35,16 @@ pub struct ExtractedMessage { pub ts: String, } -/// Main entry: walks every record in the dump and returns -/// `(messages, user_id → display_name, channel_id → name, workspace_name)`. -pub fn harvest( - dump: &IdbDump, -) -> ( +type HarvestResult = ( Vec, HashMap, HashMap, Option, -) { +); + +/// Main entry: walks every record in the dump and returns +/// `(messages, user_id → display_name, channel_id → name, workspace_name)`. +pub fn harvest(dump: &IdbDump) -> HarvestResult { let mut messages: Vec = Vec::new(); let mut users: HashMap = HashMap::new(); let mut channels: HashMap = HashMap::new(); @@ -173,15 +173,13 @@ fn walk( .or_insert_with(|| n.to_string()); } } - 'T' => { - if workspace.is_none() { - if let Some(n) = map - .get("name") - .and_then(|v| v.as_str()) - .filter(|s| !s.is_empty()) - { - *workspace = Some(n.to_string()); - } + 'T' if workspace.is_none() => { + if let Some(n) = map + .get("name") + .and_then(|v| v.as_str()) + .filter(|s| !s.is_empty()) + { + *workspace = Some(n.to_string()); } } _ => {} @@ -222,24 +220,23 @@ fn walk( ); } } - Value::String(s) => { - // Redux-persist default: values are JSON-encoded strings. If - // this string is plausibly JSON, parse and recurse. + Value::String(s) if s.len() > 20 && (s.starts_with('{') || s.starts_with('[')) - && (s.ends_with('}') || s.ends_with(']')) - { - if let Ok(inner) = serde_json::from_str::(s) { - walk( - &inner, - channel_hint, - messages, - users, - channels, - workspace, - depth + 1, - ); - } + && (s.ends_with('}') || s.ends_with(']')) => + { + // Redux-persist default: values are JSON-encoded strings. If + // this string is plausibly JSON, parse and recurse. + if let Ok(inner) = serde_json::from_str::(s) { + walk( + &inner, + channel_hint, + messages, + users, + channels, + workspace, + depth + 1, + ); } } _ => {} diff --git a/app/src-tauri/src/slack_scanner/idb.rs b/app/src-tauri/src/slack_scanner/idb.rs index 2631284ce5..d4f5a9031f 100644 --- a/app/src-tauri/src/slack_scanner/idb.rs +++ b/app/src-tauri/src/slack_scanner/idb.rs @@ -283,7 +283,7 @@ async fn serialize_values( chunk.len() )); } - for ((idx, _), val) in chunk.iter().zip(serialised.into_iter()) { + for ((idx, _), val) in chunk.iter().zip(serialised) { result[*idx] = val; } } diff --git a/app/src-tauri/src/telegram_scanner/extract.rs b/app/src-tauri/src/telegram_scanner/extract.rs index 40e3b41c09..6f9d02e4b7 100644 --- a/app/src-tauri/src/telegram_scanner/extract.rs +++ b/app/src-tauri/src/telegram_scanner/extract.rs @@ -166,16 +166,15 @@ fn walk(v: &Value, peer_hint: Option<&str>, out: &mut Harvest, depth: u32) { walk(vv, peer_hint, out, depth + 1); } } - Value::String(s) => { - // Some tweb stores persist state as JSON-encoded strings. - // Recurse when the shape looks plausibly JSON. + Value::String(s) if s.len() > 20 && (s.starts_with('{') || s.starts_with('[')) - && (s.ends_with('}') || s.ends_with(']')) - { - if let Ok(inner) = serde_json::from_str::(s) { - walk(&inner, peer_hint, out, depth + 1); - } + && (s.ends_with('}') || s.ends_with(']')) => + { + // Some tweb stores persist state as JSON-encoded strings. + // Recurse when the shape looks plausibly JSON. + if let Ok(inner) = serde_json::from_str::(s) { + walk(&inner, peer_hint, out, depth + 1); } } _ => {} diff --git a/app/src-tauri/src/telegram_scanner/idb.rs b/app/src-tauri/src/telegram_scanner/idb.rs index 84d88dd765..5fd1fe173b 100644 --- a/app/src-tauri/src/telegram_scanner/idb.rs +++ b/app/src-tauri/src/telegram_scanner/idb.rs @@ -283,7 +283,7 @@ async fn serialize_values( chunk.len() )); } - for ((idx, _), val) in chunk.iter().zip(serialised.into_iter()) { + for ((idx, _), val) in chunk.iter().zip(serialised) { result[*idx] = val; } } diff --git a/app/src-tauri/src/wechat_scanner/dom_snapshot.rs b/app/src-tauri/src/wechat_scanner/dom_snapshot.rs index b750d9a1b4..5afe84739a 100644 --- a/app/src-tauri/src/wechat_scanner/dom_snapshot.rs +++ b/app/src-tauri/src/wechat_scanner/dom_snapshot.rs @@ -23,7 +23,6 @@ pub struct MessageRow { pub struct DomScan { pub chat_rows: Vec, pub messages: Vec, - pub active_chat_name: Option, pub unread: u32, pub hash: u64, } @@ -71,7 +70,6 @@ pub async fn scan(cdp: &mut CdpConn, session: &str) -> Result { Ok(DomScan { chat_rows, messages, - active_chat_name, unread, hash, }) diff --git a/app/src-tauri/src/whatsapp_scanner/idb.rs b/app/src-tauri/src/whatsapp_scanner/idb.rs index 9a61b6e251..9b8388ab7e 100644 --- a/app/src-tauri/src/whatsapp_scanner/idb.rs +++ b/app/src-tauri/src/whatsapp_scanner/idb.rs @@ -237,7 +237,7 @@ async fn serialize_values( chunk.len() )); } - for ((idx, _), val) in chunk.iter().zip(serialised.into_iter()) { + for ((idx, _), val) in chunk.iter().zip(serialised) { result[*idx] = val; } } diff --git a/app/src-tauri/src/whatsapp_scanner/mod.rs b/app/src-tauri/src/whatsapp_scanner/mod.rs index c43ecdbdf6..bb71050528 100644 --- a/app/src-tauri/src/whatsapp_scanner/mod.rs +++ b/app/src-tauri/src/whatsapp_scanner/mod.rs @@ -29,8 +29,6 @@ use tauri::{AppHandle, Emitter, Runtime}; use tokio::task::AbortHandle; use tokio::time::sleep; -use crate::cdp::CdpConn; - mod dom_snapshot; #[cfg(test)] mod dom_snapshot_tests; @@ -1209,7 +1207,7 @@ fn merge_dom_into_snapshot( continue; } if let Some(mid) = mid_opt { - let bare_mid = mid.rsplitn(2, '_').next().map(str::to_string); + let bare_mid = mid.rsplit('_').next().map(str::to_string); let lookup = by_msg_id .get(&mid) .cloned() diff --git a/package.json b/package.json index 9a7a0fd980..e87ca4f8cc 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "debug": "bash scripts/debug/cli.sh", "test:install-ps1": "pwsh -NoProfile -File scripts/tests/OpenHumanWindowsInstall.Tests.ps1", "rust:check": "pnpm --filter openhuman-app rust:check", + "rust:clippy": "cargo clippy -p openhuman -- -D warnings && pnpm --filter openhuman-app rust:clippy", "typecheck": "pnpm --filter openhuman-app compile", "tauri:ios:init": "bash scripts/ios-init.sh", "tauri:ios:dev": "pnpm --filter openhuman-app tauri:ios:dev", diff --git a/src/bin/memory_tree_init_smoke.rs b/src/bin/memory_tree_init_smoke.rs index bddd11e9c2..f2c7f05f9d 100644 --- a/src/bin/memory_tree_init_smoke.rs +++ b/src/bin/memory_tree_init_smoke.rs @@ -57,8 +57,10 @@ fn main() -> ExitCode { } }; - let mut cfg = Config::default(); - cfg.workspace_dir = workspace.clone(); + let cfg = Config { + workspace_dir: workspace.clone(), + ..Config::default() + }; let db_path = workspace.join("memory_tree").join("chunks.db"); let cold = !db_path.exists(); diff --git a/src/bin/slack_backfill.rs b/src/bin/slack_backfill.rs index 732ac3c2e7..e5b77d41fc 100644 --- a/src/bin/slack_backfill.rs +++ b/src/bin/slack_backfill.rs @@ -323,8 +323,8 @@ async fn main() -> Result<()> { enum Outcome { Ok, Ratelimit, - OtherFail(String), - Transport(String), + OtherFail, + Transport, } let mut outcomes: Vec<(u32, std::time::Duration, Outcome)> = Vec::with_capacity(n as usize); let probe_started = Instant::now(); @@ -342,7 +342,10 @@ async fn main() -> Result<()> { .await; let dt = t0.elapsed(); let outcome = match resp { - Err(e) => Outcome::Transport(format!("{e:#}")), + Err(e) => { + log::warn!("[probe-ratelimit] transport error on call {i}: {e:#}"); + Outcome::Transport + } Ok(r) if r.successful => Outcome::Ok, Ok(r) => { let err = r.error.as_deref().unwrap_or("provider failure"); @@ -356,7 +359,11 @@ async fn main() -> Result<()> { ); Outcome::Ratelimit } else { - Outcome::OtherFail(err.to_string()) + log::warn!( + "[probe-ratelimit] provider failure on call {i}: {}", + sanitize_probe_error(err) + ); + Outcome::OtherFail } } }; @@ -379,11 +386,11 @@ async fn main() -> Result<()> { .count(); let other = outcomes .iter() - .filter(|(_, _, o)| matches!(o, Outcome::OtherFail(_))) + .filter(|(_, _, o)| matches!(o, Outcome::OtherFail)) .count(); let transport = outcomes .iter() - .filter(|(_, _, o)| matches!(o, Outcome::Transport(_))) + .filter(|(_, _, o)| matches!(o, Outcome::Transport)) .count(); let avg_ms = if !outcomes.is_empty() { outcomes.iter().map(|(_, d, _)| d.as_millis()).sum::() / outcomes.len() as u128 @@ -568,3 +575,27 @@ fn component_status(endpoint: &Option, model: &Option) -> String _ => "off".to_string(), } } + +fn sanitize_probe_error(error: &str) -> String { + let single_line = error.split_whitespace().collect::>().join(" "); + openhuman_core::openhuman::inference::provider::ops::sanitize_api_error(&single_line) +} + +#[cfg(test)] +mod tests { + use super::sanitize_probe_error; + + #[test] + fn probe_error_summary_is_single_line_bounded_and_secret_safe() { + let raw = format!( + "provider failed\nwith xoxb-secret-token {}", + "x".repeat(300) + ); + let summary = sanitize_probe_error(&raw); + + assert!(!summary.contains('\n')); + assert!(!summary.contains("xoxb-secret-token")); + assert!(summary.contains("[REDACTED]")); + assert!(summary.chars().count() <= 203); + } +} diff --git a/src/core/cli.rs b/src/core/cli.rs index 014b75c08a..2d6b55be97 100644 --- a/src/core/cli.rs +++ b/src/core/cli.rs @@ -14,10 +14,6 @@ use crate::core::jsonrpc::{default_state, invoke_method, parse_json_params}; use crate::core::logging::CliLogDefault; use crate::core::{ControllerSchema, TypeSchema}; -/// Debug/e2e agent paths can build deep async poll stacks while assembling -/// prompts, provider requests, and sub-agent tool loops. -const CLI_RUNTIME_THREAD_STACK_SIZE: usize = 8 * 1024 * 1024; - /// The ASCII banner displayed when the CLI starts. const CLI_BANNER: &str = r#" @@ -450,14 +446,6 @@ fn run_namespace_command( Ok(()) } -fn build_cli_runtime() -> Result { - tokio::runtime::Builder::new_multi_thread() - .thread_stack_size(CLI_RUNTIME_THREAD_STACK_SIZE) - .enable_all() - .build() - .map_err(Into::into) -} - /// Parses command-line arguments into a JSON map based on a function's schema. /// /// # Arguments diff --git a/src/openhuman/accessibility/automate.rs b/src/openhuman/accessibility/automate.rs index 51e197d9f7..960b91f648 100644 --- a/src/openhuman/accessibility/automate.rs +++ b/src/openhuman/accessibility/automate.rs @@ -664,7 +664,7 @@ impl AutomateBackend for RealBackend { // `summarization` keeps M1 free of Config-schema churn while still keeping // the chat model out of the loop. use tinyagents::harness::message::Message; - use tinyagents::harness::model::{ChatModel, ModelRequest}; + use tinyagents::harness::model::ModelRequest; let model = crate::openhuman::inference::provider::create_chat_model( "summarization", &self.config, diff --git a/src/openhuman/accessibility/mod.rs b/src/openhuman/accessibility/mod.rs index a7b1de0ab6..a88ba08607 100644 --- a/src/openhuman/accessibility/mod.rs +++ b/src/openhuman/accessibility/mod.rs @@ -28,8 +28,6 @@ mod vision_click; #[cfg(target_os = "windows")] mod uia_interact; -#[cfg(test)] -pub(crate) use automation_state::test_lock as automation_state_test_lock; pub use automation_state::{ clear as clear_automation_denial, mark_system_events_denied, system_events_denied, }; diff --git a/src/openhuman/accessibility/permissions_tests.rs b/src/openhuman/accessibility/permissions_tests.rs index 2009636aac..671cedab3f 100644 --- a/src/openhuman/accessibility/permissions_tests.rs +++ b/src/openhuman/accessibility/permissions_tests.rs @@ -158,8 +158,7 @@ fn permission_state_serde_round_trip() { mod automation_state_stale_cache { use crate::openhuman::accessibility::automation_state; use crate::openhuman::accessibility::{ - automation_state_test_lock, clear_automation_denial, mark_system_events_denied, - system_events_denied, + clear_automation_denial, mark_system_events_denied, system_events_denied, }; #[test] diff --git a/src/openhuman/agent/harness/archivist/mod.rs b/src/openhuman/agent/harness/archivist/mod.rs index 7f9887c7b2..d2943f7ba1 100644 --- a/src/openhuman/agent/harness/archivist/mod.rs +++ b/src/openhuman/agent/harness/archivist/mod.rs @@ -30,8 +30,6 @@ pub use types::ArchivistHook; #[cfg(test)] pub(crate) use crate::openhuman::agent::hooks::PostTurnHook; #[cfg(test)] -pub(crate) use crate::openhuman::config::Config; -#[cfg(test)] pub(crate) use crate::openhuman::memory_store::profile; #[cfg(test)] pub(crate) use helpers::extract_profile_key; diff --git a/src/openhuman/agent/harness/archivist/recap.rs b/src/openhuman/agent/harness/archivist/recap.rs index 2ad95b3ec0..ab7cf2d5bc 100644 --- a/src/openhuman/agent/harness/archivist/recap.rs +++ b/src/openhuman/agent/harness/archivist/recap.rs @@ -64,6 +64,7 @@ impl ArchivistHook { /// - NEVER mutates DB state (no `segment_set_summary`, no embedding). /// - NEVER closes a segment. /// - Safe to call on both open and closed segments. + /// /// Summarize a set of episodic entries into a recap string. /// /// Returns `(text, produced_by_llm)`. `produced_by_llm == false` means the diff --git a/src/openhuman/agent/harness/harness_gap_tests.rs b/src/openhuman/agent/harness/harness_gap_tests.rs index 668f3dbf61..7d624553cb 100644 --- a/src/openhuman/agent/harness/harness_gap_tests.rs +++ b/src/openhuman/agent/harness/harness_gap_tests.rs @@ -23,10 +23,9 @@ //! - `` XML attribute form — the parser does not parse attributes; //! only the tag body (JSON) is used. -use crate::openhuman::agent::error::AgentError; use crate::openhuman::inference::provider::traits::ProviderCapabilities; use crate::openhuman::inference::provider::Provider; -use crate::openhuman::inference::provider::{ChatMessage, ChatRequest, ChatResponse, UsageInfo}; +use crate::openhuman::inference::provider::{ChatRequest, ChatResponse}; use crate::openhuman::tool_timeout::parse_tool_timeout_secs; use crate::openhuman::tools::{Tool, ToolResult}; use async_trait::async_trait; diff --git a/src/openhuman/agent/harness/session/builder/factory.rs b/src/openhuman/agent/harness/session/builder/factory.rs index e454a80b20..2c02861a2a 100644 --- a/src/openhuman/agent/harness/session/builder/factory.rs +++ b/src/openhuman/agent/harness/session/builder/factory.rs @@ -491,7 +491,7 @@ impl Agent { )?; let supports_native = resolved_chat_model .profile() - .map_or(true, |profile| profile.tool_calling); + .is_none_or(|profile| profile.tool_calling); log::info!( "[session-builder] agent_id={} provider_role={} resolved_model={} supports_native_tools={}", agent_id, diff --git a/src/openhuman/agent/harness/session/transcript_tests.rs b/src/openhuman/agent/harness/session/transcript_tests.rs index cb35c7f89a..333d1b9777 100644 --- a/src/openhuman/agent/harness/session/transcript_tests.rs +++ b/src/openhuman/agent/harness/session/transcript_tests.rs @@ -871,7 +871,7 @@ fn read_thread_usage_summary_sums_multiple_transcripts() { let raw = raw_session_dir(ws.path()); std::fs::create_dir_all(&raw).unwrap(); - let mut mk = |stem: &str, input: u64, cost: f64| { + let mk = |stem: &str, input: u64, cost: f64| { let mut meta = sample_meta(); meta.thread_id = Some("thr-multi".into()); meta.input_tokens = input; @@ -928,7 +928,7 @@ fn read_thread_usage_summary_groups_subagents_by_archetype() { .unwrap(); // Sub-agent transcripts (stems contain `__`): coder x2 + researcher x1. - let mut sub = |stem: &str, agent: &str, input: u64, output: u64| { + let sub = |stem: &str, agent: &str, input: u64, output: u64| { let mut m = sample_meta(); m.thread_id = Some("thr-sub".into()); m.agent_name = agent.into(); diff --git a/src/openhuman/agent/harness/session/turn/session_io.rs b/src/openhuman/agent/harness/session/turn/session_io.rs index cce7b01509..e98911a9a2 100644 --- a/src/openhuman/agent/harness/session/turn/session_io.rs +++ b/src/openhuman/agent/harness/session/turn/session_io.rs @@ -5,7 +5,9 @@ use super::super::types::Agent; use crate::openhuman::agent::harness; use crate::openhuman::agent::progress::AgentProgress; use crate::openhuman::context::ARCHIVIST_EXTRACTION_PROMPT; -use crate::openhuman::inference::provider::{ChatMessage, UsageInfo, AGENT_TURN_MAX_OUTPUT_TOKENS}; +use crate::openhuman::inference::provider::{ + ChatMessage, ChatResponse, UsageInfo, AGENT_TURN_MAX_OUTPUT_TOKENS, +}; use futures::StreamExt; use tinyagents::harness::model::{ModelRequest, ModelStreamItem}; @@ -69,8 +71,9 @@ impl Agent { /// Ask the provider for a short wrap-up message with native tools /// **disabled** so the model returns prose rather than another tool call. - /// Streams text deltas to the progress sink (when attached) so the summary - /// appears in the UI like any other reply. + /// Buffers text deltas and forwards them to the progress sink (when + /// attached) only after the completed response is validated as prose, so + /// prompt-formatted tool calls cannot flash in the UI before fallback. /// /// `instruction` is the synthetic user turn that steers the wrap-up — the /// tool-call-cap checkpoint (`MAX_ITER_CHECKPOINT_INSTRUCTION`) or the @@ -128,19 +131,13 @@ impl Agent { }; let mut streamed_text = String::new(); + let mut streamed_deltas = Vec::new(); let mut completed = None; while let Some(item) = stream.next().await { match item { ModelStreamItem::MessageDelta(delta) if !delta.text.is_empty() => { streamed_text.push_str(&delta.text); - if let Some(sink) = &self.on_progress { - let _ = sink - .send(AgentProgress::TextDelta { - delta: delta.text, - iteration: iteration_for_stream, - }) - .await; - } + streamed_deltas.push(delta.text); } ModelStreamItem::Completed(response) => completed = Some(response), ModelStreamItem::Failed(error) => { @@ -163,10 +160,49 @@ impl Agent { let checkpoint = if !text.trim().is_empty() { text } else if response.tool_calls().is_empty() { - streamed_text + streamed_text.clone() } else { String::new() }; + if !checkpoint.trim().is_empty() { + let mut provider_response = ChatResponse { + text: Some(checkpoint.clone()), + tool_calls: Vec::new(), + usage: None, + reasoning_content: None, + }; + let (_, mut prompt_tool_calls) = + self.tool_dispatcher.parse_response(&provider_response); + if streamed_text != checkpoint { + provider_response.text = Some(streamed_text); + let (_, streamed_tool_calls) = + self.tool_dispatcher.parse_response(&provider_response); + prompt_tool_calls.extend(streamed_tool_calls); + } + if !prompt_tool_calls.is_empty() { + tracing::warn!( + parsed_tool_calls = prompt_tool_calls.len(), + "[agent::session] wrap-up model returned a prompt-formatted tool call; using deterministic fallback" + ); + return (String::new(), usage); + } + tracing::debug!( + checkpoint_chars = checkpoint.chars().count(), + buffered_deltas = streamed_deltas.len(), + iteration = iteration_for_stream, + "[agent::session] wrap-up checkpoint validation passed" + ); + if let Some(sink) = &self.on_progress { + for delta in streamed_deltas { + let _ = sink + .send(AgentProgress::TextDelta { + delta, + iteration: iteration_for_stream, + }) + .await; + } + } + } (checkpoint, usage) } diff --git a/src/openhuman/agent/harness/session/turn_tests.rs b/src/openhuman/agent/harness/session/turn_tests.rs index ae4afa6870..c837f21c98 100644 --- a/src/openhuman/agent/harness/session/turn_tests.rs +++ b/src/openhuman/agent/harness/session/turn_tests.rs @@ -1,5 +1,4 @@ use super::*; -use crate::core::event_bus::{global, init_global, DomainEvent}; use crate::openhuman::agent::dispatcher::XmlToolDispatcher; use crate::openhuman::agent::hooks::{PostTurnHook, TurnContext}; use crate::openhuman::agent::tool_policy::{ @@ -8,8 +7,7 @@ use crate::openhuman::agent::tool_policy::{ }; use crate::openhuman::agent_memory::memory_loader::MemoryLoader; use crate::openhuman::inference::provider::{ - ChatMessage, ChatRequest, ChatResponse, ConversationMessage, Provider, ToolResultMessage, - UsageInfo, + ChatMessage, ChatRequest, ChatResponse, ConversationMessage, Provider, UsageInfo, }; use crate::openhuman::memory::Memory; use crate::openhuman::tools::ToolResult; @@ -20,7 +18,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use tokio::sync::Mutex as AsyncMutex; use tokio::sync::Notify; -use tokio::time::{sleep, timeout, Duration}; +use tokio::time::{timeout, Duration}; struct DummyProvider; @@ -1197,6 +1195,49 @@ async fn turn_final_answer_falls_back_to_deterministic_summary_when_reprompt_emp ); } +#[tokio::test] +async fn summarize_turn_wrapup_rejects_prompt_tool_call_and_preserves_usage() { + let provider: Arc = Arc::new(SequenceProvider { + responses: AsyncMutex::new(vec![Ok(ChatResponse { + text: Some("{\"name\":\"echo\",\"arguments\":{}}".into()), + tool_calls: vec![], + usage: Some(UsageInfo { + input_tokens: 13, + output_tokens: 5, + cached_input_tokens: 3, + charged_amount_usd: 0.07, + ..UsageInfo::default() + }), + reasoning_content: None, + })]), + requests: AsyncMutex::new(Vec::new()), + }); + let agent = make_agent_with_builder( + provider, + vec![], + Box::new(FixedMemoryLoader { + context: String::new(), + }), + vec![], + crate::openhuman::config::AgentConfig::default(), + crate::openhuman::config::ContextConfig::default(), + ); + + let (summary, usage) = agent + .summarize_turn_wrapup(&[], "test-model", 1, "write a wrap-up") + .await; + + assert!( + summary.is_empty(), + "prompt-formatted tool calls must trigger the deterministic fallback" + ); + let usage = usage.expect("rejected wrap-up must preserve provider usage"); + assert_eq!(usage.input_tokens, 13); + assert_eq!(usage.output_tokens, 5); + assert_eq!(usage.cached_input_tokens, 3); + assert_eq!(usage.charged_amount_usd, 0.07); +} + #[tokio::test] async fn turn_checkpoint_usage_is_folded_into_transcript_accounting() { // The extra checkpoint provider call costs tokens; those must land in diff --git a/src/openhuman/agent/harness/subagent_runner/ops/mod.rs b/src/openhuman/agent/harness/subagent_runner/ops/mod.rs index c8ba6c102c..9e0ea2692c 100644 --- a/src/openhuman/agent/harness/subagent_runner/ops/mod.rs +++ b/src/openhuman/agent/harness/subagent_runner/ops/mod.rs @@ -61,9 +61,7 @@ pub(super) use provider::LazyToolkitResolver; pub(super) use super::tool_prep::filter_tool_indices; // Types used by tests that were previously in scope via the flat ops.rs imports. #[cfg(test)] -pub(super) use super::types::{ - SubagentMode, SubagentRunError, SubagentRunOptions, SubagentRunOutcome, -}; +pub(super) use super::types::{SubagentMode, SubagentRunError, SubagentRunOptions}; #[cfg(test)] pub(super) use crate::openhuman::agent::harness::definition::{AgentDefinition, PromptSource}; #[cfg(test)] diff --git a/src/openhuman/agent/harness/tests.rs b/src/openhuman/agent/harness/tests.rs index 472e5463d3..aa73dc0e3b 100644 --- a/src/openhuman/agent/harness/tests.rs +++ b/src/openhuman/agent/harness/tests.rs @@ -5,10 +5,9 @@ use super::parse::{ parse_tool_calls, parse_tool_calls_from_json_value, tools_to_openai_format, }; use crate::openhuman::inference::provider::traits::ProviderCapabilities; -use crate::openhuman::inference::provider::{ChatMessage, ChatRequest, ChatResponse, Provider}; -use crate::openhuman::tools::{self, Tool}; +use crate::openhuman::inference::provider::{ChatRequest, ChatResponse, Provider}; +use crate::openhuman::tools; use async_trait::async_trait; -use base64::{engine::general_purpose::STANDARD, Engine as _}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; diff --git a/src/openhuman/agent/harness/tool_filter.rs b/src/openhuman/agent/harness/tool_filter.rs index 7b988c651d..5b375d7f17 100644 --- a/src/openhuman/agent/harness/tool_filter.rs +++ b/src/openhuman/agent/harness/tool_filter.rs @@ -81,7 +81,7 @@ pub fn filter_actions_by_prompt( }) .collect(); - scored.sort_by(|a, b| b.0.cmp(&a.0)); + scored.sort_by_key(|item| std::cmp::Reverse(item.0)); // Only keep positively-scored results. Zero-overlap tools would add noise. scored diff --git a/src/openhuman/agent/progress_tracing.rs b/src/openhuman/agent/progress_tracing.rs index b3d7074b51..87577062fd 100644 --- a/src/openhuman/agent/progress_tracing.rs +++ b/src/openhuman/agent/progress_tracing.rs @@ -774,7 +774,7 @@ impl SpanCollector { ("gen_ai.usage.reasoning_tokens", reasoning_tokens), ("gen_ai.usage.cache_creation_tokens", cache_creation_tokens), ] { - if add == 0 && span.attributes.get(key).is_none() { + if add == 0 && !span.attributes.contains_key(key) { continue; } let prior = span diff --git a/src/openhuman/agent/schemas.rs b/src/openhuman/agent/schemas.rs index bbae99bdec..c095855163 100644 --- a/src/openhuman/agent/schemas.rs +++ b/src/openhuman/agent/schemas.rs @@ -519,7 +519,6 @@ fn local_catalog_models_from_config( fn handle_registry_snapshot(_params: Map) -> ControllerFuture { Box::pin(async { - use crate::openhuman::tools::Tool as _; use tinyagents::registry::{ComponentKind, ComponentMetadata, RegistrySnapshot}; let mut components: Vec = Vec::new(); diff --git a/src/openhuman/agent_meetings/summary.rs b/src/openhuman/agent_meetings/summary.rs index 45dfbf862f..bb8470345d 100644 --- a/src/openhuman/agent_meetings/summary.rs +++ b/src/openhuman/agent_meetings/summary.rs @@ -17,7 +17,7 @@ use crate::core::event_bus::BackendMeetTurn; use crate::openhuman::config::{AutoSummarizePolicy, Config}; use crate::openhuman::inference::provider::create_chat_model; use tinyagents::harness::message::Message; -use tinyagents::harness::model::{ChatModel, ModelRequest}; +use tinyagents::harness::model::ModelRequest; use super::types::{ActionItem, ActionItemKind, MeetingSummary}; diff --git a/src/openhuman/agent_memory/tools.rs b/src/openhuman/agent_memory/tools.rs index 42215af356..468ebf8b7d 100644 --- a/src/openhuman/agent_memory/tools.rs +++ b/src/openhuman/agent_memory/tools.rs @@ -116,7 +116,7 @@ impl Tool for CallMemoryAgentTool { let max_turns = args .get("max_turns") .and_then(|v| v.as_u64()) - .map(|v| v.max(1).min(20) as usize); + .map(|v| v.clamp(1, 20) as usize); let is_async = args.get("async").and_then(|v| v.as_bool()).unwrap_or(false); diff --git a/src/openhuman/agent_orchestration/background_delivery.rs b/src/openhuman/agent_orchestration/background_delivery.rs index b1b0e0df21..bae691c788 100644 --- a/src/openhuman/agent_orchestration/background_delivery.rs +++ b/src/openhuman/agent_orchestration/background_delivery.rs @@ -148,29 +148,30 @@ async fn try_deliver(session: String) { .remove(&session); return; } - match ( + if let (Some(thread_id), Some(notice)) = ( background_completions::batch_thread_id(&batch), background_completions::build_batched_notice(&batch), ) { - (Some(thread_id), Some(notice)) => { - log::info!( - "[background_delivery] delivering {} batched background result(s) \ - session={session} thread_id={thread_id}", - batch.len() + log::info!( + "[background_delivery] delivering {} batched background result(s) \ + session={session} thread_id={thread_id}", + batch.len() + ); + if let Err(e) = crate::openhuman::agent::task_dispatcher::run_system_turn_on_thread( + thread_id, notice, + ) + .await + { + log::warn!( + "[background_delivery] delivery turn failed session={session} error={e}" ); - if let Err(e) = crate::openhuman::agent::task_dispatcher::run_system_turn_on_thread( - thread_id, notice, - ) - .await - { - log::warn!( - "[background_delivery] delivery turn failed session={session} error={e}" - ); - requeue(&session, batch); // don't lose results on a failed turn - } + requeue(&session, batch); // don't lose results on a failed turn } - // Headless (no originating thread to stream into) — drop the batch. - _ => {} + } else { + log::warn!( + "[background_delivery] dropping headless batch session={session} count={}", + batch.len() + ); } } diff --git a/src/openhuman/agent_orchestration/running_subagents.rs b/src/openhuman/agent_orchestration/running_subagents.rs index 4b26363af6..1319585c74 100644 --- a/src/openhuman/agent_orchestration/running_subagents.rs +++ b/src/openhuman/agent_orchestration/running_subagents.rs @@ -847,7 +847,7 @@ pub(crate) fn task_id_for_session_in_workspace( .into_iter() .filter(|record| record_subagent_session_id(record) == Some(subagent_session_id)) .collect(); - matches.sort_by(|a, b| b.updated_at.cmp(&a.updated_at)); + matches.sort_by_key(|item| std::cmp::Reverse(item.updated_at)); for record in matches { if record_parent_session(&record) != Some(parent_session) { diff --git a/src/openhuman/agent_orchestration/spawn_parallel_graph.rs b/src/openhuman/agent_orchestration/spawn_parallel_graph.rs index 69f66533e5..f449314226 100644 --- a/src/openhuman/agent_orchestration/spawn_parallel_graph.rs +++ b/src/openhuman/agent_orchestration/spawn_parallel_graph.rs @@ -409,7 +409,7 @@ fn snapshot_agent_definitions( .collect() } -pub(crate) fn prepare_spawn_parallel_tasks_from_defs( +pub(super) fn prepare_spawn_parallel_tasks_from_defs( tasks: Vec, definitions: &HashMap, parent: &ParentExecutionContext, diff --git a/src/openhuman/agent_registry/agents/loader.rs b/src/openhuman/agent_registry/agents/loader.rs index bcaa34a0cf..1a9111fa13 100644 --- a/src/openhuman/agent_registry/agents/loader.rs +++ b/src/openhuman/agent_registry/agents/loader.rs @@ -1001,18 +1001,20 @@ mod tests { } #[test] - fn workflow_builder_is_registered_worker_with_narrow_propose_or_read_scope() { + fn workflow_builder_is_registered_worker_with_bounded_authoring_scope() { // Phase 5a/5b: the workflow-builder must be a Worker-tier leaf whose - // tool scope is EXACTLY the propose-or-read + Composio discovery/connect - // + confirmed test-run + save-onto-existing belt — no flow creation - // (flows_create/set_enabled), no shell, no file writes, no channel - // sends, and no composio_execute. It can list toolkits/connections, + // tool scope is EXACTLY the bounded authoring/read + Composio + // discovery/connect belt. Creation is limited to `create_workflow` + // and `duplicate_flow`, which always produce disabled flows; the raw + // flows_create/update/set_enabled tools remain unavailable, as do + // shell, file writes, channel sends, and composio_execute. It can list + // toolkits/connections, // raise the inline connect card, `run_flow` a flow the user already // SAVED to test it (a real run the prompt gates behind user // confirmation), and `save_workflow` a built graph onto a flow the host // ALREADY created (the prompt bar's instant-create path) — but it can - // never create/enable a flow or perform an arbitrary raw integration - // action. One narrow, deliberate carve-out (B12): `get_tool_output_sample` + // never enable a flow or perform an arbitrary raw integration action. + // One narrow, deliberate carve-out (B12): `get_tool_output_sample` // DOES make a real Composio call, but only ever a Read-scope one // (hard-refused otherwise, regardless of the user's scope preference) // against an already-connected toolkit — see `builder_tools.rs`'s @@ -1090,14 +1092,12 @@ mod tests { assert_eq!( names.len(), expected.len(), - "workflow_builder scope must be EXACTLY the propose-or-read belt (got {names:?})" + "workflow_builder scope must be EXACTLY the bounded authoring belt (got {names:?})" ); - // Hard exclusions: nothing that reaches the raw flow - // controller directly, executes raw integration actions, or - // touches the host. - // (Persistence onto an EXISTING flow is the deliberate - // `save_workflow` carve-out above; raw `flows_update` — which - // could also rename/re-gate arbitrary flows — stays out.) + // Hard exclusions: no unrestricted flow mutation, raw + // integration actions, or host access. Creation is exposed + // only through the bounded tools above; raw `flows_update` + // could rename or re-gate arbitrary flows, so it stays out. for forbidden in [ "flows_create", "flows_update", @@ -1113,7 +1113,7 @@ mod tests { ] { assert!( !names.iter().any(|n| n == forbidden), - "workflow_builder must NOT have `{forbidden}` — propose/read only" + "workflow_builder must NOT have unrestricted tool `{forbidden}`" ); } } diff --git a/src/openhuman/agentbox/store_tests.rs b/src/openhuman/agentbox/store_tests.rs index 052ffc8d75..10ddd51101 100644 --- a/src/openhuman/agentbox/store_tests.rs +++ b/src/openhuman/agentbox/store_tests.rs @@ -1,5 +1,5 @@ use super::store::JobStore; -use super::types::{JobRecord, JobStatus, RunResult}; +use super::types::{JobStatus, RunResult}; use std::time::Duration; #[test] diff --git a/src/openhuman/artifacts/store.rs b/src/openhuman/artifacts/store.rs index f895f97077..9d21de44b1 100644 --- a/src/openhuman/artifacts/store.rs +++ b/src/openhuman/artifacts/store.rs @@ -208,7 +208,7 @@ pub(crate) async fn list_artifacts( } // Sort descending by created_at (newest first) - all.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + all.sort_by_key(|item| std::cmp::Reverse(item.created_at)); // Apply thread filter BEFORE pagination so `total` reflects the // per-thread count the UI surfaces, and so a small page doesn't get diff --git a/src/openhuman/audio_toolkit/ops.rs b/src/openhuman/audio_toolkit/ops.rs index 22ea57ec15..fb77b99a5b 100644 --- a/src/openhuman/audio_toolkit/ops.rs +++ b/src/openhuman/audio_toolkit/ops.rs @@ -211,7 +211,7 @@ pub fn resolve_email_capture_dir(config: &Config) -> Option { } #[cfg(feature = "e2e-test-support")] { - return Some(config.workspace_dir.join(DEFAULT_CAPTURE_DIR)); + Some(config.workspace_dir.join(DEFAULT_CAPTURE_DIR)) } #[cfg(not(feature = "e2e-test-support"))] { diff --git a/src/openhuman/channels/proactive.rs b/src/openhuman/channels/proactive.rs index 54af0190d2..9298fb9dea 100644 --- a/src/openhuman/channels/proactive.rs +++ b/src/openhuman/channels/proactive.rs @@ -112,10 +112,13 @@ impl ProactiveMessageSubscriber { /// unit tests — so [`set_runtime_active_channel`] is a no-op there and never /// leaks across the parallel test suite. The choice is also persisted to /// `config.channels_config.active_channel`, which seeds the handle on next start. -static ACTIVE_CHANNEL_HANDLE: std::sync::OnceLock>>>>> = +type ActiveChannelHandle = Arc>>; +type ActiveChannelHandleSlot = RwLock>; + +static ACTIVE_CHANNEL_HANDLE: std::sync::OnceLock = std::sync::OnceLock::new(); -fn active_channel_handle_slot() -> &'static RwLock>>>> { +fn active_channel_handle_slot() -> &'static ActiveChannelHandleSlot { ACTIVE_CHANNEL_HANDLE.get_or_init(|| RwLock::new(None)) } diff --git a/src/openhuman/composio/tools/direct.rs b/src/openhuman/composio/tools/direct.rs index 1e35a0f2ad..baeddfafb5 100644 --- a/src/openhuman/composio/tools/direct.rs +++ b/src/openhuman/composio/tools/direct.rs @@ -783,10 +783,10 @@ impl Tool for ComposioTool { // tool call itself has no outbound side effect to gate. // `action="execute"` (or anything unknown / missing) is the // write path and routes through the approval gate. - match args.get("action").and_then(|v| v.as_str()) { - Some("list") | Some("connect") => false, - _ => true, - } + !matches!( + args.get("action").and_then(|v| v.as_str()), + Some("list") | Some("connect") + ) } async fn execute(&self, args: serde_json::Value) -> anyhow::Result { diff --git a/src/openhuman/config/ops/mod.rs b/src/openhuman/config/ops/mod.rs index 53558d3acd..1a24b834dc 100644 --- a/src/openhuman/config/ops/mod.rs +++ b/src/openhuman/config/ops/mod.rs @@ -33,8 +33,8 @@ pub(crate) use crate::openhuman::config::Config; #[cfg(test)] pub(crate) use loader::{ active_workspace_marker_path, config_openhuman_dir, default_openhuman_dir, env_flag_enabled, - fallback_workspace_dir, reset_local_data_for_paths, reset_local_data_remove_error, - BROWSER_ALLOW_ALL_ENV, BROWSER_ALLOW_ALL_RPC_ENABLE_ENV, + fallback_workspace_dir, reset_local_data_for_paths, BROWSER_ALLOW_ALL_ENV, + BROWSER_ALLOW_ALL_RPC_ENABLE_ENV, }; #[cfg(test)] pub(crate) use std::path::PathBuf; diff --git a/src/openhuman/config/schema/load/mod.rs b/src/openhuman/config/schema/load/mod.rs index 828e5d001c..72e52588d5 100644 --- a/src/openhuman/config/schema/load/mod.rs +++ b/src/openhuman/config/schema/load/mod.rs @@ -32,8 +32,6 @@ pub(crate) use super::Config; #[cfg(test)] pub(crate) use dirs::ACTIVE_USER_STATE_FILE; #[cfg(test)] -pub(crate) use env::ProcessEnvWithoutWorkspace; -#[cfg(test)] pub(crate) use impl_load::parse_config_with_recovery; #[cfg(test)] pub(crate) use migrate::{migrate_cloud_provider_slugs, migrate_legacy_inference_url}; diff --git a/src/openhuman/config/schema/load_tests.rs b/src/openhuman/config/schema/load_tests.rs index 1e16eaa594..9e87902ef2 100644 --- a/src/openhuman/config/schema/load_tests.rs +++ b/src/openhuman/config/schema/load_tests.rs @@ -1360,7 +1360,7 @@ async fn test_save_preserves_backup_file() { let config_path = tmp.path().join("config.toml"); let backup_path = tmp.path().join("config.toml.bak"); - let mut config = Config { + let config = Config { config_path: config_path.clone(), workspace_dir: tmp.path().join("workspace"), action_dir: tmp.path().join("workspace"), @@ -1387,7 +1387,7 @@ async fn test_save_then_corrupt_then_recover() { let tmp = tempfile::tempdir().unwrap(); let config_path = tmp.path().join("config.toml"); - let mut config = Config { + let config = Config { config_path: config_path.clone(), workspace_dir: tmp.path().join("workspace"), action_dir: tmp.path().join("workspace"), diff --git a/src/openhuman/config/schemas/mod.rs b/src/openhuman/config/schemas/mod.rs index 9981018657..aa31c8df4a 100644 --- a/src/openhuman/config/schemas/mod.rs +++ b/src/openhuman/config/schemas/mod.rs @@ -17,18 +17,11 @@ use controllers::{ #[cfg(test)] use helpers::{ deserialize_params, json_output, optional_bool, optional_json, optional_string, - required_string, to_json, ActivityLevelSettingsUpdate, AgentPathsUpdate, AgentSettingsUpdate, - AnalyticsSettingsUpdate, AutonomySettingsUpdate, BrowserSettingsUpdate, - ComposioTriggerSettingsUpdate, DictationSettingsUpdate, LocalAiSettingsUpdate, - MeetSettingsUpdate, MemorySettingsUpdate, MemorySyncSettingsUpdate, ModelSettingsUpdate, - OnboardingCompletedSetParams, RuntimeSettingsUpdate, SandboxSettingsUpdate, - ScreenIntelligenceSettingsUpdate, SearchSettingsUpdate, SetBrowserAllowAllParams, - VoiceServerSettingsUpdate, WorkspaceOnboardingFlagParams, WorkspaceOnboardingFlagSetParams, - DEFAULT_ONBOARDING_FLAG_NAME, + required_string, to_json, AutonomySettingsUpdate, LocalAiSettingsUpdate, MemorySettingsUpdate, + ModelSettingsUpdate, OnboardingCompletedSetParams, SetBrowserAllowAllParams, + WorkspaceOnboardingFlagParams, WorkspaceOnboardingFlagSetParams, DEFAULT_ONBOARDING_FLAG_NAME, }; #[cfg(test)] -use schema_defs::schemas; -#[cfg(test)] use serde_json::{Map, Value}; #[cfg(test)] diff --git a/src/openhuman/credentials/profiles.rs b/src/openhuman/credentials/profiles.rs index 8258f1b9f5..5b6b0e7f77 100644 --- a/src/openhuman/credentials/profiles.rs +++ b/src/openhuman/credentials/profiles.rs @@ -70,6 +70,16 @@ const LOCK_TIMEOUT_MS: u64 = STALE_LOCK_AGE_MS + 5_000; const PERSIST_RETRY_ATTEMPTS: u32 = 6; const PERSIST_RETRY_BASE_MS: u64 = 100; +type EncryptedProfileFields = ( + Option, + Option, + Option, + Option, + Option, + Option, + Option, +); + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum AuthProfileKind { @@ -921,18 +931,7 @@ impl AuthProfilesStore { } /// Encrypt a profile's secret fields for JSON storage (keychain-unavailable path). - fn encrypt_for_json( - &self, - profile: &AuthProfile, - ) -> Result<( - Option, - Option, - Option, - Option, - Option, - Option, - Option, - )> { + fn encrypt_for_json(&self, profile: &AuthProfile) -> Result { let (access_token, refresh_token, id_token, expires_at, token_type, scope) = match (&profile.kind, &profile.token_set) { (AuthProfileKind::OAuth, Some(token_set)) => ( diff --git a/src/openhuman/cron/tools/add.rs b/src/openhuman/cron/tools/add.rs index 5f6e76fda0..efe2e75bb1 100644 --- a/src/openhuman/cron/tools/add.rs +++ b/src/openhuman/cron/tools/add.rs @@ -63,7 +63,7 @@ fn validate_delivery(config: &Config, delivery: &DeliveryConfig) -> Result<(), S } match allowed_users_for_channel(config, channel) { - Some(list) if list.is_empty() => Ok(()), + Some([]) => Ok(()), Some(list) => { if list.iter().any(|u| u == to) { Ok(()) diff --git a/src/openhuman/cwd_jail/linux.rs b/src/openhuman/cwd_jail/linux.rs index 8747ce14d4..c7297ec5fa 100644 --- a/src/openhuman/cwd_jail/linux.rs +++ b/src/openhuman/cwd_jail/linux.rs @@ -15,6 +15,12 @@ use super::jail::{Jail, JailBackend}; pub struct LandlockBackend; +impl Default for LandlockBackend { + fn default() -> Self { + Self::new() + } +} + impl LandlockBackend { pub fn new() -> Self { Self @@ -29,7 +35,7 @@ impl JailBackend for LandlockBackend { fn is_available(&self) -> bool { #[cfg(feature = "sandbox-landlock")] { - use landlock::{AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr}; + use landlock::{AccessFs, Ruleset, RulesetAttr}; Ruleset::default() .handle_access(AccessFs::ReadFile) .and_then(|r| r.create()) diff --git a/src/openhuman/desktop_companion/pipeline_tests.rs b/src/openhuman/desktop_companion/pipeline_tests.rs index f5b858e184..70cfa13a1a 100644 --- a/src/openhuman/desktop_companion/pipeline_tests.rs +++ b/src/openhuman/desktop_companion/pipeline_tests.rs @@ -8,7 +8,6 @@ use super::*; use crate::openhuman::desktop_companion::pointing::ScreenGeometry; use crate::openhuman::desktop_companion::session; -use crate::openhuman::desktop_companion::types::*; /// Serialize tests that touch the process-global session state. Shared with /// `session_tests` via `session::lock_test_state()` so transitions in one test diff --git a/src/openhuman/desktop_companion/session_tests.rs b/src/openhuman/desktop_companion/session_tests.rs index d4ca0cd2b1..a2e6ea96a5 100644 --- a/src/openhuman/desktop_companion/session_tests.rs +++ b/src/openhuman/desktop_companion/session_tests.rs @@ -1,7 +1,6 @@ //! Tests for companion session lifecycle and state machine. use super::*; -use crate::openhuman::desktop_companion::types::*; /// Serialize tests that mutate the process-global session state. Shared with /// `pipeline_tests` via `lock_test_state()` (defined in `session`) so a diff --git a/src/openhuman/devices/rpc.rs b/src/openhuman/devices/rpc.rs index 11c3d97dc8..427666b26a 100644 --- a/src/openhuman/devices/rpc.rs +++ b/src/openhuman/devices/rpc.rs @@ -334,7 +334,7 @@ mod tests { fn test_config() -> Config { let dir = tempfile::tempdir().expect("tempdir"); let mut config = Config::default(); - config.workspace_dir = dir.into_path(); + config.workspace_dir = dir.keep(); config } diff --git a/src/openhuman/devices/store.rs b/src/openhuman/devices/store.rs index ab4998ec6d..2b89128ff5 100644 --- a/src/openhuman/devices/store.rs +++ b/src/openhuman/devices/store.rs @@ -154,7 +154,7 @@ mod tests { fn test_config() -> Config { let dir = tempfile::tempdir().expect("tempdir"); let mut config = Config::default(); - config.workspace_dir = dir.into_path(); + config.workspace_dir = dir.keep(); config } diff --git a/src/openhuman/embeddings/ollama_adapter.rs b/src/openhuman/embeddings/ollama_adapter.rs index fe48175c60..0b8162fe12 100644 --- a/src/openhuman/embeddings/ollama_adapter.rs +++ b/src/openhuman/embeddings/ollama_adapter.rs @@ -9,6 +9,7 @@ pub use tinyagents::harness::embeddings::{ DEFAULT_OLLAMA_DIMENSIONS, DEFAULT_OLLAMA_MODEL, DEFAULT_OLLAMA_URL, }; +#[derive(Default)] pub struct OllamaEmbedding { inner: OllamaEmbeddingModel, } @@ -33,14 +34,6 @@ impl OllamaEmbedding { } } -impl Default for OllamaEmbedding { - fn default() -> Self { - Self { - inner: OllamaEmbeddingModel::default(), - } - } -} - #[async_trait] impl EmbeddingProvider for OllamaEmbedding { fn name(&self) -> &str { diff --git a/src/openhuman/file_state/ops.rs b/src/openhuman/file_state/ops.rs index bf8be1133f..1e90d1cdbf 100644 --- a/src/openhuman/file_state/ops.rs +++ b/src/openhuman/file_state/ops.rs @@ -1,6 +1,6 @@ //! Operational API for the file state coordinator. -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock}; use std::time::{Instant, SystemTime}; use tokio::sync::{Mutex, OwnedMutexGuard}; @@ -117,10 +117,10 @@ pub fn check_stale_read(agent_id: &str, resolved_path: &PathBuf) -> Option Option { +pub fn check_partial_read(agent_id: &str, resolved_path: &Path) -> Option { let coord = try_global()?; let reads = coord.reads.read(); - let read_key = (agent_id.to_string(), resolved_path.clone()); + let read_key = (agent_id.to_string(), resolved_path.to_path_buf()); let read_stamp = reads.get(&read_key)?; if read_stamp.partial { let display_path = resolved_path.display(); @@ -138,12 +138,12 @@ pub fn check_partial_read(agent_id: &str, resolved_path: &PathBuf) -> Option Option> { +pub async fn acquire_path_lock(resolved_path: &Path) -> Option> { let coord = try_global()?; let mutex = { let mut locks = coord.path_locks.write(); locks - .entry(resolved_path.clone()) + .entry(resolved_path.to_path_buf()) .or_insert_with(|| Arc::new(Mutex::new(()))) .clone() }; diff --git a/src/openhuman/flows/ops.rs b/src/openhuman/flows/ops.rs index 3704acdad9..72de2d8eae 100644 --- a/src/openhuman/flows/ops.rs +++ b/src/openhuman/flows/ops.rs @@ -3561,7 +3561,6 @@ pub async fn flows_cancel_run(config: &Config, run_id: &str) -> Result match checkpointer.delete_thread(thread_id).await { Ok(()) => { @@ -4371,7 +4370,7 @@ pub async fn flows_build( fn text_looks_like_question(text: &str) -> bool { let trimmed = text .trim() - .trim_end_matches(|c: char| matches!(c, '"' | '\'' | ')' | ']' | '*' | '_' | '`' | '.')) + .trim_end_matches(['"', '\'', ')', ']', '*', '_', '`', '.']) .trim_end(); if trimmed.is_empty() { return false; @@ -4388,8 +4387,7 @@ fn text_looks_like_question(text: &str) -> bool { // invariant this function exists to protect. trimmed .lines() - .filter(|line| !line.trim().is_empty()) - .next_back() + .rfind(|line| !line.trim().is_empty()) .is_some_and(|last_line| last_line.trim_end().ends_with('?')) } diff --git a/src/openhuman/flows/ops_tests.rs b/src/openhuman/flows/ops_tests.rs index 93409a4793..448b35fb12 100644 --- a/src/openhuman/flows/ops_tests.rs +++ b/src/openhuman/flows/ops_tests.rs @@ -2306,7 +2306,7 @@ async fn validate_tool_contracts_rejects_a_hallucinated_slug() { { "id": "t", "kind": "trigger", "name": "Manual" }, { "id": "post", "kind": "tool_call", "name": "Post", "config": { "slug": "SLACK_POST_MESSAGE_TO_CHANNEL", - "args": { "channel": "#general", "text": "hi" } } } + "args": { "channel": "#general", "markdown_text": "hi" } } } ], "edges": [ { "from_node": "t", "to_node": "post" } ] })); diff --git a/src/openhuman/inference/local/install.rs b/src/openhuman/inference/local/install.rs index d6d8c6de6f..ae5ca6b8f3 100644 --- a/src/openhuman/inference/local/install.rs +++ b/src/openhuman/inference/local/install.rs @@ -339,7 +339,6 @@ mod tests { /// (OLLAMA_BIN, PATH) with other local-AI tests that also read these /// variables. Without this, cargo's test runner can interleave set/remove /// calls and cause flakes. - fn env_lock() -> std::sync::MutexGuard<'static, ()> { crate::openhuman::inference::inference_test_guard() } diff --git a/src/openhuman/inference/local/mod.rs b/src/openhuman/inference/local/mod.rs index a643700692..472eba8cdd 100644 --- a/src/openhuman/inference/local/mod.rs +++ b/src/openhuman/inference/local/mod.rs @@ -52,5 +52,6 @@ pub use schemas::{ all_controller_schemas as all_local_inference_controller_schemas, all_registered_controllers as all_local_inference_registered_controllers, }; +#[cfg(feature = "voice")] pub(crate) use service::whisper_engine; pub use service::LocalAiService; diff --git a/src/openhuman/inference/local/ollama.rs b/src/openhuman/inference/local/ollama.rs index e2d8462ab0..486c48e823 100644 --- a/src/openhuman/inference/local/ollama.rs +++ b/src/openhuman/inference/local/ollama.rs @@ -386,6 +386,7 @@ impl OllamaShowResponse { /// (unknown): /// * empty / absent capabilities (older Ollama, or an `/api/show` miss); /// * a tag set we don't recognise (e.g. `["insert"]` only). +/// /// Callers treat `None` as "keep visible" — fail-open, never hide a model /// that might be usable for chat. Mirrors the non-rejecting `Unknown` arm of /// [`super::model_requirements::ContextEligibility`]. See Sentry TAURI-RUST-4P6. diff --git a/src/openhuman/inference/provider/claude_code/driver.rs b/src/openhuman/inference/provider/claude_code/driver.rs index eca47e2fec..f2eccb414f 100644 --- a/src/openhuman/inference/provider/claude_code/driver.rs +++ b/src/openhuman/inference/provider/claude_code/driver.rs @@ -40,6 +40,7 @@ const DISALLOWED_CC_BUILTINS: &[&str] = &[ /// Whether the user opted into FULL access for Claude Code (`bypassPermissions` /// + full native toolset incl. Bash/network). Default is **off** → the safer +/// /// `acceptEdits` posture (file edits only). This is a deliberate user choice, /// not the default — enabling Claude Code alone does not grant shell/network /// power. diff --git a/src/openhuman/inference/provider/factory.rs b/src/openhuman/inference/provider/factory.rs index 58c83858f1..b074a831c0 100644 --- a/src/openhuman/inference/provider/factory.rs +++ b/src/openhuman/inference/provider/factory.rs @@ -1605,10 +1605,10 @@ pub(crate) fn create_turn_chat_model_from_string_with_native_tools( /// `make_omlx_provider` / `make_local_openai_provider` — they share the endpoint /// helpers but each builds its own client, so an endpoint/auth change must touch /// both until the `Provider` path is deleted. -fn try_create_local_runtime_chat_model( - role: &str, - config: &Config, -) -> Option>, String)>> { +type ResolvedChatModel = (Arc>, String); +type OptionalChatModelResult = Option>; + +fn try_create_local_runtime_chat_model(role: &str, config: &Config) -> OptionalChatModelResult { let resolved = provider_for_role(role, config); try_create_local_runtime_chat_model_from_string(role, &resolved, config, true) } @@ -1618,7 +1618,7 @@ fn try_create_local_runtime_chat_model_from_string( provider: &str, config: &Config, require_session: bool, -) -> Option>, String)>> { +) -> OptionalChatModelResult { use crate::openhuman::inference::local::profile::{ LOCAL_OPENAI_PROFILE, MLX_PROFILE, OMLX_PROFILE, }; @@ -2514,10 +2514,7 @@ fn make_cloud_provider_by_slug( /// `verify_session_active`) runs before building. Temperature rides the per-call /// `ModelRequest` (managed/local parity; the `@` suffix still bakes a fixed /// override). -fn try_create_cloud_slug_chat_model( - role: &str, - config: &Config, -) -> Option>, String)>> { +fn try_create_cloud_slug_chat_model(role: &str, config: &Config) -> OptionalChatModelResult { try_create_cloud_slug_chat_model_with_native_tools(role, config, true) } @@ -2525,7 +2522,7 @@ fn try_create_cloud_slug_chat_model_with_native_tools( role: &str, config: &Config, native_tool_calling: bool, -) -> Option>, String)>> { +) -> OptionalChatModelResult { // Resolve the role's provider string, expanding the empty / "cloud" sentinel // to the primary cloud target (mirroring create_chat_provider_from_string). let mut resolved = provider_for_role(role, config); @@ -2544,7 +2541,7 @@ fn try_create_cloud_slug_chat_model_from_string( role: &str, provider: &str, config: &Config, -) -> Option>, String)>> { +) -> OptionalChatModelResult { try_create_cloud_slug_chat_model_from_string_with_native_tools(role, provider, config, true) } @@ -2553,7 +2550,7 @@ fn try_create_cloud_slug_chat_model_from_string_with_native_tools( provider: &str, config: &Config, native_tool_calling: bool, -) -> Option>, String)>> { +) -> OptionalChatModelResult { let p = provider.trim().to_string(); // Only the ":[@temp]" cloud form routes here. The managed diff --git a/src/openhuman/inference/provider/factory_tests.rs b/src/openhuman/inference/provider/factory_tests.rs index 7be21342d8..d98cd1919c 100644 --- a/src/openhuman/inference/provider/factory_tests.rs +++ b/src/openhuman/inference/provider/factory_tests.rs @@ -927,7 +927,7 @@ fn verify_session_active_rejects_when_no_session_token() { #[test] fn verify_session_active_rejects_when_token_is_empty() { let tmp = TempDir::new().expect("tempdir"); - let mut config = config_in_tempdir(&tmp); + let config = config_in_tempdir(&tmp); let auth = AuthService::new(tmp.path(), config.secrets.encrypt); auth.store_provider_token("app-session", "default", "", Default::default(), false) .expect("store empty token"); @@ -941,7 +941,7 @@ fn verify_session_active_rejects_when_token_is_empty() { #[test] fn verify_session_active_passes_when_session_token_present() { let tmp = TempDir::new().expect("tempdir"); - let mut config = config_in_tempdir(&tmp); + let config = config_in_tempdir(&tmp); let auth = AuthService::new(tmp.path(), config.secrets.encrypt); auth.store_provider_token( "app-session", @@ -2780,7 +2780,7 @@ async fn create_chat_model_wraps_provider_and_round_trips() { use async_trait::async_trait; use std::sync::Arc; use tinyagents::harness::message::Message; - use tinyagents::harness::model::{ChatModel, ModelRequest}; + use tinyagents::harness::model::ModelRequest; let _guard = crate::openhuman::inference::inference_test_guard(); @@ -2834,8 +2834,6 @@ fn resolves_to_managed_backend_for_default_config_but_not_for_local() { #[test] fn create_chat_model_routes_managed_backend_to_crate_native() { - use tinyagents::harness::model::ChatModel; - let _guard = crate::openhuman::inference::inference_test_guard(); // No test-provider override installed → the managed short-circuit engages. let config = Config::default(); @@ -2851,8 +2849,6 @@ fn create_chat_model_routes_managed_backend_to_crate_native() { #[test] fn create_chat_model_routes_local_runtime_to_crate_native() { - use tinyagents::harness::model::ChatModel; - let _guard = crate::openhuman::inference::inference_test_guard(); let mut config = Config::default(); config.chat_provider = Some("ollama:qwen2.5".to_string()); @@ -2917,8 +2913,6 @@ fn deepseek_entry(id: &str) -> CloudProviderCreds { #[test] fn create_chat_model_routes_plain_bearer_cloud_slug_to_crate_native() { - use tinyagents::harness::model::ChatModel; - let _guard = crate::openhuman::inference::inference_test_guard(); // DeepSeek is a built-in chat-completions-only Bearer provider: no // `/v1/responses` fallback and no codex-oauth, so it is wire-equivalent and @@ -2961,8 +2955,6 @@ fn explicit_cloud_provider_string_routes_to_crate_native_model() { #[test] fn create_chat_model_routes_anthropic_auth_cloud_slug_to_crate_native() { - use tinyagents::harness::model::ChatModel; - let _guard = crate::openhuman::inference::inference_test_guard(); // Anthropic-auth cloud slugs are always wire-equivalent (their endpoints have // no `/v1/responses`, so the host's dormant fallback is behavior-neutral). @@ -2982,8 +2974,6 @@ fn create_chat_model_routes_anthropic_auth_cloud_slug_to_crate_native() { #[test] fn try_create_cloud_slug_flips_openai_but_declines_non_cloud() { - use tinyagents::harness::model::ChatModel; - let _guard = crate::openhuman::inference::inference_test_guard(); // `openai` (API-key Bearer, no codex OAuth) now flips crate-native on Chat // Completions — the legacy `/v1/responses` fallback is not replicated. diff --git a/src/openhuman/learning/extract/heuristics.rs b/src/openhuman/learning/extract/heuristics.rs index ac44cf6fe0..7cb749bb58 100644 --- a/src/openhuman/learning/extract/heuristics.rs +++ b/src/openhuman/learning/extract/heuristics.rs @@ -413,8 +413,6 @@ mod heuristics_tests { #[test] fn length_ratio_emits_compressed_when_user_msgs_shrink() { let session = fresh_session_id(); - let buf = Buffer::new(1024); - // First 15 turns: high ratio (user talks a lot). let now = now_secs(); for i in 0..15 { diff --git a/src/openhuman/learning/reflection.rs b/src/openhuman/learning/reflection.rs index a46a0e240f..f7a248a05c 100644 --- a/src/openhuman/learning/reflection.rs +++ b/src/openhuman/learning/reflection.rs @@ -60,7 +60,7 @@ async fn invoke_cloud_reflection( prompt: &str, ) -> anyhow::Result { use tinyagents::harness::message::Message; - use tinyagents::harness::model::{ChatModel, ModelRequest}; + use tinyagents::harness::model::ModelRequest; Ok(provider .invoke( &(), diff --git a/src/openhuman/learning/stability_detector.rs b/src/openhuman/learning/stability_detector.rs index 231fb6886b..67f040e4de 100644 --- a/src/openhuman/learning/stability_detector.rs +++ b/src/openhuman/learning/stability_detector.rs @@ -288,7 +288,7 @@ impl StabilityDetector { facet_type: FacetType::Preference, key: full_key.clone(), value, - confidence: (agg_score).min(1.0).max(0.0), + confidence: agg_score.clamp(0.0, 1.0), evidence_count: new_evidence_count, source_segment_ids: existing.and_then(|f| f.source_segment_ids.clone()), first_seen_at: first_seen, @@ -551,9 +551,7 @@ fn state_from_stability(score: f64, user_state: UserState) -> FacetState { return FacetState::Dropped; } - if score.is_infinite() { - FacetState::Active - } else if score >= TAU_PROMOTE { + if score.is_infinite() || score >= TAU_PROMOTE { FacetState::Active } else if score >= TAU_PROVISIONAL { FacetState::Provisional diff --git a/src/openhuman/mcp_registry/connections.rs b/src/openhuman/mcp_registry/connections.rs index fdc964593c..6e41e84cf3 100644 --- a/src/openhuman/mcp_registry/connections.rs +++ b/src/openhuman/mcp_registry/connections.rs @@ -595,6 +595,7 @@ pub async fn call_tool( /// and the re-auth affordance keyed off the status alone. /// - other recorded connect failure in `LAST_ERRORS` → `Error` + message. /// - otherwise → `Disconnected`. +/// /// Pure status decision for one installed server, factored out of /// [`all_status`] so the priority order is unit-testable without a live /// connection registry or store. Inputs: diff --git a/src/openhuman/mcp_registry/registries/mcp_official.rs b/src/openhuman/mcp_registry/registries/mcp_official.rs index 269aad977c..453d4002c9 100644 --- a/src/openhuman/mcp_registry/registries/mcp_official.rs +++ b/src/openhuman/mcp_registry/registries/mcp_official.rs @@ -75,8 +75,11 @@ const MAX_CURSOR_WALK_PAGES: u32 = 50; /// `parking_lot::Mutex` matches the rest of the memory subsystem and keeps /// the critical section synchronous — every access is a `HashMap` op, no /// `.await` while the lock is held. -fn cursor_cache() -> &'static Mutex> { - static CACHE: OnceLock>> = OnceLock::new(); +type CursorCacheKey = (String, u32, u32); +type CursorCache = Mutex>; + +fn cursor_cache() -> &'static CursorCache { + static CACHE: OnceLock = OnceLock::new(); CACHE.get_or_init(|| Mutex::new(HashMap::new())) } diff --git a/src/openhuman/mcp_server/write_dispatch.rs b/src/openhuman/mcp_server/write_dispatch.rs index ae4f1568a8..43c32d0009 100644 --- a/src/openhuman/mcp_server/write_dispatch.rs +++ b/src/openhuman/mcp_server/write_dispatch.rs @@ -142,11 +142,11 @@ pub(super) async fn dispatch_write_tool( fn audit_write(config: &Config, record: NewMcpWriteRecord) { let config = config.clone(); if let Ok(handle) = tokio::runtime::Handle::try_current() { - let _ = handle.spawn_blocking(move || { + std::mem::drop(handle.spawn_blocking(move || { if let Err(err) = mcp_audit::record_write(&config, record) { log::warn!("[mcp_server] mcp write audit insert failed: {err}"); } - }); + })); } else { let _ = std::thread::spawn(move || { if let Err(err) = mcp_audit::record_write(&config, record) { @@ -203,7 +203,7 @@ pub(super) fn audit_write_rejection_without_config( let args_summary = summarize_write_args(&tool_name, audit_arguments); match tokio::runtime::Handle::try_current() { Ok(handle) => { - let _ = handle.spawn(async move { + std::mem::drop(handle.spawn(async move { match config_rpc::load_config_with_timeout().await { Ok(config) => audit_write( &config, @@ -223,7 +223,7 @@ pub(super) fn audit_write_rejection_without_config( err ), } - }); + })); } Err(err) => log::warn!( "[mcp_server] write rejection audit skipped tool={} runtime unavailable error={}", diff --git a/src/openhuman/meet_agent/brain/llm.rs b/src/openhuman/meet_agent/brain/llm.rs index f0fd583743..5d54872bf8 100644 --- a/src/openhuman/meet_agent/brain/llm.rs +++ b/src/openhuman/meet_agent/brain/llm.rs @@ -15,7 +15,7 @@ use crate::openhuman::agent::harness::session::Agent; /// One rolling-history entry handed to the LLM. #[derive(Debug, Clone)] -pub(super) struct ConversationTurn { +pub(crate) struct ConversationTurn { pub role: &'static str, pub content: String, } diff --git a/src/openhuman/meet_agent/brain/speech.rs b/src/openhuman/meet_agent/brain/speech.rs index 88eb70f53b..ac84b5f6ea 100644 --- a/src/openhuman/meet_agent/brain/speech.rs +++ b/src/openhuman/meet_agent/brain/speech.rs @@ -58,7 +58,6 @@ pub(super) async fn tts(text: &str, voice_id: Option<&str>) -> Result, // Per-mascot voice for speaker alternation. `None` preserves the // backend's default-voice pick (single-mascot behavior). voice_id: voice_id.map(str::to_owned), - ..Default::default() }; let outcome = synthesize_reply(&config, text, &opts).await?; let result = outcome.value; diff --git a/src/openhuman/meet_agent/session.rs b/src/openhuman/meet_agent/session.rs index edfff41e78..334b5dce53 100644 --- a/src/openhuman/meet_agent/session.rs +++ b/src/openhuman/meet_agent/session.rs @@ -330,6 +330,7 @@ impl MeetAgentSession { /// * none present → `advance_speaker` yields `None` and the /// reply-speech backend keeps picking its own default voice /// (exact previous behavior). + /// /// The active slot starts at 0 so an idle session reports the primary /// mascot; [`Self::advance_speaker`] keeps the first reply there and /// rotates thereafter. diff --git a/src/openhuman/meet_agent/store.rs b/src/openhuman/meet_agent/store.rs index 600f9223f9..4bf527431f 100644 --- a/src/openhuman/meet_agent/store.rs +++ b/src/openhuman/meet_agent/store.rs @@ -163,7 +163,7 @@ async fn read_recent_from(path: &Path, limit: usize) -> Result Utc::now().timestamp_millis(), "deferred job should be rescheduled into the future" ); + let defer_reason = job.last_error.as_deref().unwrap_or(""); assert!( - job.last_error - .as_deref() - .unwrap_or("") - .contains("re-embed backfill"), - "defer reason should be recorded for visibility" + defer_reason.contains("re-embed backfill") + || defer_reason.contains("llm concurrency gate busy"), + "defer reason should identify the backfill or the shared gate: {defer_reason:?}" ); assert_eq!(count_by_status(&cfg, JobStatus::Ready).unwrap(), 1); } diff --git a/src/openhuman/memory_store/chunks/connection.rs b/src/openhuman/memory_store/chunks/connection.rs index 87fe5a2e4a..f21278718a 100644 --- a/src/openhuman/memory_store/chunks/connection.rs +++ b/src/openhuman/memory_store/chunks/connection.rs @@ -18,6 +18,3 @@ pub(crate) fn recover_corrupt_db(config: &Config) -> Result { log::warn!("[memory:chunks] checking corrupt database recovery"); tinycortex::memory::chunks::recover_corrupt_db(&engine_config(config)) } - -#[cfg(test)] -pub(crate) use tinycortex::memory::chunks::{is_transient_cold_start, try_cleanup_stale_files}; diff --git a/src/openhuman/memory_store/chunks/embeddings.rs b/src/openhuman/memory_store/chunks/embeddings.rs index 703c541a76..beedf1733e 100644 --- a/src/openhuman/memory_store/chunks/embeddings.rs +++ b/src/openhuman/memory_store/chunks/embeddings.rs @@ -7,9 +7,6 @@ use rusqlite::{Connection, Transaction}; use crate::openhuman::config::Config; -#[cfg(test)] -pub use tinycortex::memory::chunks::{embedding_to_blob, REEMBED_SKIP_KEY_MAX_LEN}; - fn engine_config(config: &Config) -> tinycortex::memory::MemoryConfig { crate::openhuman::tinycortex::memory_config_from(config, config.workspace_dir.clone()) } diff --git a/src/openhuman/memory_tools/capture.rs b/src/openhuman/memory_tools/capture.rs index b9934378a8..b885f86ed5 100644 --- a/src/openhuman/memory_tools/capture.rs +++ b/src/openhuman/memory_tools/capture.rs @@ -82,8 +82,10 @@ impl ToolMemoryCaptureHook { // phrases like "I want to stop working" don't trigger false captures. let stop_imperative = lower.starts_with("stop ") || lower.contains(". stop ") || lower.contains("\nstop "); - if !(lower.contains("never ") || lower.contains("don't ") || lower.contains("do not ")) - && !stop_imperative + if !(lower.contains("never ") + || lower.contains("don't ") + || lower.contains("do not ") + || stop_imperative) { return Vec::new(); } diff --git a/src/openhuman/memory_tree/tree/factory.rs b/src/openhuman/memory_tree/tree/factory.rs index e0ba2cc183..2b3f4daf43 100644 --- a/src/openhuman/memory_tree/tree/factory.rs +++ b/src/openhuman/memory_tree/tree/factory.rs @@ -80,8 +80,8 @@ impl<'a> TreeFactory<'a> { match self.kind() { TreeKind::Topic | TreeKind::Global => slugify_source_id(scope), TreeKind::Source => { - if scope.starts_with("gmail:") { - slugify_source_id(&scope["gmail:".len()..]) + if let Some(gmail_scope) = scope.strip_prefix("gmail:") { + slugify_source_id(gmail_scope) } else { slugify_source_id(scope) } diff --git a/src/openhuman/memory_tree/tree/registry.rs b/src/openhuman/memory_tree/tree/registry.rs index ec6b7386a8..d11ff6e943 100644 --- a/src/openhuman/memory_tree/tree/registry.rs +++ b/src/openhuman/memory_tree/tree/registry.rs @@ -76,10 +76,10 @@ pub fn get_or_create_tree(config: &Config, kind: TreeKind, scope: &str) -> Resul /// Matches both the anyhow-wrapped rusqlite error text and the raw SQLite /// error codes in case the wrapping chain is shorter. pub fn is_unique_violation(err: &anyhow::Error) -> bool { - if let Some(rusqlite_err) = err.downcast_ref::() { - if let rusqlite::Error::SqliteFailure(sqlite_err, _) = rusqlite_err { - return sqlite_err.code == rusqlite::ErrorCode::ConstraintViolation; - } + if let Some(rusqlite::Error::SqliteFailure(sqlite_err, _)) = + err.downcast_ref::() + { + return sqlite_err.code == rusqlite::ErrorCode::ConstraintViolation; } // Fallback for chained/wrapped errors: scan the rendered message. let msg = format!("{err:#}"); diff --git a/src/openhuman/memory_tree/tree/rpc.rs b/src/openhuman/memory_tree/tree/rpc.rs index 88301c201a..c277a850f4 100644 --- a/src/openhuman/memory_tree/tree/rpc.rs +++ b/src/openhuman/memory_tree/tree/rpc.rs @@ -832,10 +832,9 @@ pub async fn set_enabled_rpc( mod tests { use super::*; use crate::openhuman::memory_queue as jobs; - use crate::openhuman::memory_queue::store::count_total; use crate::openhuman::memory_store::chunks::types::SourceKind; use crate::openhuman::memory_sync::canonicalize::document::DocumentInput; - use chrono::{Duration as ChronoDuration, Utc}; + use chrono::Utc; use serde_json::json; use tempfile::TempDir; diff --git a/src/openhuman/people/store.rs b/src/openhuman/people/store.rs index b9f9fc8beb..d6ec23372d 100644 --- a/src/openhuman/people/store.rs +++ b/src/openhuman/people/store.rs @@ -16,6 +16,14 @@ use crate::openhuman::people::migrations; use crate::openhuman::people::types::{Handle, Interaction, Person, PersonId}; pub type ConnHandle = Arc>; +type PersonRow = ( + String, + Option, + Option, + Option, + i64, + i64, +); /// Process-global handle to the `PeopleStore`, tagged with the workspace it is /// bound to. Controller handlers are free functions with no `&self`, so they @@ -326,7 +334,7 @@ impl PeopleStore { let conn = self.conn.clone(); tokio::task::spawn_blocking(move || -> SqlResult> { let guard = conn.blocking_lock(); - let row: Option<(String, Option, Option, Option, i64, i64)> = + let row: Option = guard .query_row( "SELECT id, display_name, primary_email, primary_phone, created_at, updated_at \ diff --git a/src/openhuman/runtime_python/bootstrap.rs b/src/openhuman/runtime_python/bootstrap.rs index cfffb2a0a5..3bb729d077 100644 --- a/src/openhuman/runtime_python/bootstrap.rs +++ b/src/openhuman/runtime_python/bootstrap.rs @@ -276,6 +276,7 @@ async fn acquire_install_lock(install_dir: &Path) -> Result { let file = OpenOptions::new() .create(true) + .truncate(false) .read(true) .write(true) .open(&lock_path_for_task) diff --git a/src/openhuman/security/pii/rules.rs b/src/openhuman/security/pii/rules.rs index f3671f15b3..b4b6d3d45f 100644 --- a/src/openhuman/security/pii/rules.rs +++ b/src/openhuman/security/pii/rules.rs @@ -71,7 +71,7 @@ pub(crate) fn is_luhn_valid(raw: &str) -> bool { } sum += v; } - sum % 10 == 0 + sum.is_multiple_of(10) } /// Validate that a dotted-quad match is a real IPv4 address (each octet 0–255). diff --git a/src/openhuman/skills/e2e_plumbing_tests.rs b/src/openhuman/skills/e2e_plumbing_tests.rs index 6281524eb5..ee35174c1e 100644 --- a/src/openhuman/skills/e2e_plumbing_tests.rs +++ b/src/openhuman/skills/e2e_plumbing_tests.rs @@ -39,7 +39,6 @@ use crate::openhuman::skills::ops_create::{ use crate::openhuman::skills::ops_types::WorkflowScope; use crate::openhuman::skills::registry::get_workflow; use crate::openhuman::skills::run_log; -use crate::openhuman::tools::policy::DefaultToolPolicy; use crate::openhuman::tools::traits::Tool; // ── Mock LLM ───────────────────────────────────────────────────────────── diff --git a/src/openhuman/skills/e2e_run_tests.rs b/src/openhuman/skills/e2e_run_tests.rs index a7ff46bc66..03349f3e2c 100644 --- a/src/openhuman/skills/e2e_run_tests.rs +++ b/src/openhuman/skills/e2e_run_tests.rs @@ -42,7 +42,6 @@ use crate::openhuman::skill_runtime::{await_run_outcome, spawn_workflow_run_back use crate::openhuman::skills::schemas::resolve_workspace_dir; use crate::openhuman::todos::ops as board_ops; use crate::openhuman::todos::ops::{BoardLocation, CardPatch}; -use crate::openhuman::tools::policy::DefaultToolPolicy; use crate::openhuman::tools::traits::Tool; /// Serialize this module's tests (each touches process-global state). diff --git a/src/openhuman/skills/ops_tests.rs b/src/openhuman/skills/ops_tests.rs index d49ccfba77..75e2deb6bc 100644 --- a/src/openhuman/skills/ops_tests.rs +++ b/src/openhuman/skills/ops_tests.rs @@ -1678,7 +1678,6 @@ fn uninstall_resolves_agents_skills_legacy_root() { // discover_workflows surfaces ~/.agents/skills/, so uninstall must reach it // too — otherwise a listed workflow can never be deleted via this API. let home = tempfile::tempdir().unwrap(); - let ws = tempfile::tempdir().unwrap(); let dir = home.path().join(".agents").join("skills").join("agenty"); write( &dir.join(SKILL_MD), diff --git a/src/openhuman/skills/registry.rs b/src/openhuman/skills/registry.rs index 580d27b7ca..dc374b71c4 100644 --- a/src/openhuman/skills/registry.rs +++ b/src/openhuman/skills/registry.rs @@ -148,8 +148,9 @@ pub fn prune_legacy_default_workflows(workspace_dir: &Path) { /// `run_workflow`) returned "unknown workflow" for anything created via the UI. /// /// Per dir: `skill.toml` (id / `when_to_use` / `[[inputs]]` / `[github]`) -/// + the `SKILL.md` body as the inline system prompt; or, when there's no -/// `skill.toml`, a synthesized SKILL.md-only definition so a bare workflow is +/// + the `SKILL.md` body as the inline system prompt. +/// +/// Without `skill.toml`, a synthesized SKILL.md-only definition means a bare workflow is /// still runnable. A bad `skill.toml` falls back to the SKILL.md-only form. pub fn load_workflows(workspace_dir: &Path) -> Vec { // Prune any legacy bundled skills an older build left behind so discover's diff --git a/src/openhuman/socket/medulla/mod.rs b/src/openhuman/socket/medulla/mod.rs index 1a961e4c44..0656761ca4 100644 --- a/src/openhuman/socket/medulla/mod.rs +++ b/src/openhuman/socket/medulla/mod.rs @@ -373,10 +373,9 @@ async fn drain_steer(steer_rx: &mut mpsc::UnboundedReceiver) -> Option msg, - Err(_) => None, - } + tokio::time::timeout(STEER_DRAIN_GRACE, steer_rx.recv()) + .await + .unwrap_or_default() } /// Build (or resume) an agent session for a medulla task. diff --git a/src/openhuman/subconscious_triggers/runtime.rs b/src/openhuman/subconscious_triggers/runtime.rs index 941b1024a0..5ed4ac6e28 100644 --- a/src/openhuman/subconscious_triggers/runtime.rs +++ b/src/openhuman/subconscious_triggers/runtime.rs @@ -132,7 +132,8 @@ impl TriggerOrchestrator { /// Non-blocking ingestion entry point for the bus subscriber. Normalizes /// + admits synchronously, then spawns the gate task for admitted - /// triggers so event dispatch is never blocked on an LLM call. + /// + /// Triggers ensure event dispatch is never blocked on an LLM call. pub fn ingest(self: &Arc, event: &DomainEvent) { let now = now_secs(); let Some(trigger) = normalize(event, now) else { diff --git a/src/openhuman/test_support/introspect.rs b/src/openhuman/test_support/introspect.rs index 4bc75daa76..8198deb2c2 100644 --- a/src/openhuman/test_support/introspect.rs +++ b/src/openhuman/test_support/introspect.rs @@ -144,7 +144,7 @@ async fn walk_dir( size: if is_dir { 0 } else { meta.len() }, is_dir, }); - if is_dir && depth + 1 <= max_depth { + if is_dir && depth < max_depth { stack.push((path, depth + 1)); } } @@ -168,7 +168,7 @@ pub async fn list_workspace_files( Ok(RpcOutcome::single_log( ListResult { root: root.display().to_string(), - entries: entries.iter().cloned().collect(), + entries: entries.to_vec(), truncated, }, format!( diff --git a/src/openhuman/tinyagents/mod.rs b/src/openhuman/tinyagents/mod.rs index 4ba7386b55..4412ae314f 100644 --- a/src/openhuman/tinyagents/mod.rs +++ b/src/openhuman/tinyagents/mod.rs @@ -79,7 +79,7 @@ pub(crate) use embeddings::ProviderEmbeddingModel; pub(crate) use middleware::{ HandoffConfig, SuperContextConfig, TranscriptSnapshotSink, TurnContextMiddleware, }; -use model::ProviderModel; +use model::{BuiltTurnModels, ProviderModel, TierRoutes, TurnChatModel}; pub(crate) use observability::SubagentScope; use observability::{ CapPauser, IterationCursor, OpenhumanEventBridge, ProviderUsageCarry, ToolFailureMap, @@ -1172,13 +1172,13 @@ fn tinyagents_depth_error( /// `Provider` → `ChatModel` adaptation is confined to `build_turn_models`. pub(crate) struct TurnModels { /// The turn's effective/primary model (registry default + dispatch target). - primary: Arc>, + primary: TurnChatModel, /// Additive workload-tier routes (registry name → model), excluding the /// primary; the crate registry resolves fallback/selection across them. - routes: Vec<(String, Arc>)>, + routes: TierRoutes, /// A model for the context-window summarizer (a distinct adapter instance so /// its provider errors don't touch the turn's `error_slot`). - summarizer: Arc>, + summarizer: TurnChatModel, /// Recovers the primary's original (downcastable) provider error on failure. error_slot: crate::openhuman::tinyagents::model::ProviderErrorSlot, /// Provider telemetry id (`{provider_id}.{model}` in Langfuse), captured from @@ -1307,26 +1307,25 @@ fn build_turn_models_crate( // The primary honours an explicit provider-string override when the producer's // effective provider differs from `provider_for_role(role)` (triage #1257). - let build_primary = - |m: &str| -> anyhow::Result>> { - match primary_override { - Some(ps) => factory::create_turn_chat_model_from_string_with_native_tools( - role, - ps, - config, - m, - temperature, - !force_text_mode, - ), - None => factory::create_turn_chat_model_with_native_tools( - role, - config, - m, - temperature, - !force_text_mode, - ), - } - }; + let build_primary = |m: &str| -> anyhow::Result { + match primary_override { + Some(ps) => factory::create_turn_chat_model_from_string_with_native_tools( + role, + ps, + config, + m, + temperature, + !force_text_mode, + ), + None => factory::create_turn_chat_model_with_native_tools( + role, + config, + m, + temperature, + !force_text_mode, + ), + } + }; // Build the primary, every workload-tier route, and the summarizer under one // per-turn egress-dedup ledger: each managed construction resolves through the @@ -1334,44 +1333,39 @@ fn build_turn_models_crate( // separate `ExternalTransferPending` for the same logical destination (codex // P2, PR #4812). `dedup_turn_scope` collapses same-destination repeats to one // disclosure per turn while still surfacing each distinct tier model. - #[allow(clippy::type_complexity)] - let (primary, routes, summarizer): ( - Arc>, - Vec<(String, Arc>)>, - Arc>, - ) = crate::openhuman::security::egress::dedup_turn_scope(|| { - let primary = build_primary(model)?; - - // Additive workload-tier routes: one crate-native model per tier (skipping the - // turn's own model, which is registered as the default primary), each pinned to - // the tier alias so the crate registry resolves cross-route fallback across them. - let mut routes: Vec<(String, Arc>)> = - Vec::new(); - for &tier in routes::WORKLOAD_ROUTE_TIERS { - if tier == model { - continue; - } - let tier_role = factory::role_for_model_tier(tier); - match factory::create_turn_chat_model(tier_role, config, tier, temperature) { - Ok(route_model) => routes.push((tier.to_string(), route_model)), - Err(e) => { - // A route that can't be built (e.g. an unconfigured BYOK tier) is - // skipped, not fatal — the primary still dispatches (parity with the - // `Provider` path, where an unresolved tier simply isn't registered). - tracing::debug!( - route = tier, - error = %e, - "[models] skipping crate-native workload route that failed to build" - ); + let (primary, routes, summarizer): BuiltTurnModels = + crate::openhuman::security::egress::dedup_turn_scope(|| { + let primary = build_primary(model)?; + + // Additive workload-tier routes: one crate-native model per tier (skipping the + // turn's own model, which is registered as the default primary), each pinned to + // the tier alias so the crate registry resolves cross-route fallback across them. + let mut routes: TierRoutes = Vec::new(); + for &tier in routes::WORKLOAD_ROUTE_TIERS { + if tier == model { + continue; + } + let tier_role = factory::role_for_model_tier(tier); + match factory::create_turn_chat_model(tier_role, config, tier, temperature) { + Ok(route_model) => routes.push((tier.to_string(), route_model)), + Err(e) => { + // A route that can't be built (e.g. an unconfigured BYOK tier) is + // skipped, not fatal — the primary still dispatches (parity with the + // `Provider` path, where an unresolved tier simply isn't registered). + tracing::debug!( + route = tier, + error = %e, + "[models] skipping crate-native workload route that failed to build" + ); + } } } - } - // The summarizer is a distinct adapter instance (own empty error slot). - let summarizer = build_primary(model)?; + // The summarizer is a distinct adapter instance (own empty error slot). + let summarizer = build_primary(model)?; - anyhow::Ok((primary, routes, summarizer)) - })?; + anyhow::Ok((primary, routes, summarizer)) + })?; Ok(TurnModels { primary, @@ -1671,7 +1665,8 @@ struct AssembledTurnHarness { /// Shared `call_id → (success, failure, elapsed_ms, output_chars)` /// side-channel: the tool-outcome capture middleware classifies each outcome /// + records its duration/output size; the event bridge reads it to project - /// real success + a user-facing failure + timing onto `ToolCallCompleted`. + /// + /// Real success + a user-facing failure + timing onto `ToolCallCompleted`. failure_map: ToolFailureMap, /// Shared FIFO carry of per-call provider `UsageInfo` (charged USD + context /// window): the model adapter pushes, the event bridge pops when recording diff --git a/src/openhuman/tinyagents/model.rs b/src/openhuman/tinyagents/model.rs index dc8807c505..f6ca880061 100644 --- a/src/openhuman/tinyagents/model.rs +++ b/src/openhuman/tinyagents/model.rs @@ -26,6 +26,10 @@ use crate::openhuman::inference::provider::{ }; use crate::openhuman::tools::ToolSpec; +pub(super) type TurnChatModel = Arc>; +pub(super) type TierRoutes = Vec<(String, TurnChatModel)>; +pub(super) type BuiltTurnModels = (TurnChatModel, TierRoutes, TurnChatModel); + /// Translate a harness [`ModelRequest`] into openhuman's message list + tool /// specs (shared by the buffered and streaming paths). fn build_chat_inputs( diff --git a/src/openhuman/tinyagents/observability.rs b/src/openhuman/tinyagents/observability.rs index c38685a983..8c7bdeea25 100644 --- a/src/openhuman/tinyagents/observability.rs +++ b/src/openhuman/tinyagents/observability.rs @@ -52,6 +52,7 @@ pub(crate) type ToolNameMap = Arc) -> Retriever mod tests { use super::*; use async_trait::async_trait; - use tinyagents::harness::events::{EventListener, EventRecord, RecordingListener}; + use tinyagents::harness::events::{EventListener, EventRecord}; use crate::openhuman::memory::{MemoryCategory, MemoryEntry, NamespaceSummary}; diff --git a/src/openhuman/tinyagents/tests.rs b/src/openhuman/tinyagents/tests.rs index 8c54cf9e24..bce571eeeb 100644 --- a/src/openhuman/tinyagents/tests.rs +++ b/src/openhuman/tinyagents/tests.rs @@ -584,7 +584,6 @@ fn adapter_inventory_registers_model_tools_and_middleware() { // Capability profile (issue #4249, Phase 2): derived from the wrapped // provider plus the runner-threaded token limits. - use tinyagents::harness::model::ChatModel; let registered = assembled .harness .models() diff --git a/src/openhuman/tinycortex/embeddings.rs b/src/openhuman/tinycortex/embeddings.rs index c34c7f8439..76f1557a68 100644 --- a/src/openhuman/tinycortex/embeddings.rs +++ b/src/openhuman/tinycortex/embeddings.rs @@ -4,6 +4,7 @@ //! OpenHuman owns the concrete providers (voyage / openai / cohere / ollama / //! cloud / noop) and the `embeddings/factory.rs` construction policy (rate-limit //! + retry). TinyCortex "never makes a network call" — it takes compute through +//! //! [`EmbeddingBackend`] (the vector store) and [`Embedder`] (retrieval / seal //! scoring). This adapter wraps one `Arc` and re-exposes //! it as both, so the engine drives OpenHuman embeddings without cloning any diff --git a/src/openhuman/tinycortex/sync.rs b/src/openhuman/tinycortex/sync.rs index 37967d7b87..482bc63922 100644 --- a/src/openhuman/tinycortex/sync.rs +++ b/src/openhuman/tinycortex/sync.rs @@ -116,7 +116,7 @@ fn source_sync_context(memory: MemoryClientRef, config: &Config, local: bool) -> documents: adapter.clone(), state: adapter.clone(), local_documents: local.then(|| adapter.clone() as std::sync::Arc), - external_sources: local.then(|| adapter as std::sync::Arc), + external_sources: local.then_some(adapter as std::sync::Arc), summariser: local.then(|| { std::sync::Arc::new(super::HostSummariser::new(config.clone())) as std::sync::Arc diff --git a/src/openhuman/tinyflows/caps.rs b/src/openhuman/tinyflows/caps.rs index 12d37105a7..6e2470b06d 100644 --- a/src/openhuman/tinyflows/caps.rs +++ b/src/openhuman/tinyflows/caps.rs @@ -591,6 +591,7 @@ impl LlmProvider for OpenHumanLlm { /// the node builds a real session agent /// ([`Agent::from_config_for_agent`](crate::openhuman::agent::Agent::from_config_for_agent) /// + `set_agent_definition_name`) and drives one full turn via +/// /// [`Agent::run_single`](crate::openhuman::agent::Agent::run_single) — the /// complete tool loop. The definition's `ToolScope` / `sandbox_mode` / /// `max_iterations` govern the turn, so an agent node gains its curated diff --git a/src/openhuman/tinyplace/manifest.rs b/src/openhuman/tinyplace/manifest.rs index 574e27f5fa..5a6a488d2b 100644 --- a/src/openhuman/tinyplace/manifest.rs +++ b/src/openhuman/tinyplace/manifest.rs @@ -2668,7 +2668,6 @@ use std::sync::Arc; use tinyplace::signal::session::SignalSession; use tinyplace::signal::store::SessionStore; -use tinyplace::signal::store::SessionStore as _; /// Get the `Arc` from the client or fail with a clear message. /// diff --git a/src/openhuman/todos/graph_shadow.rs b/src/openhuman/todos/graph_shadow.rs index f74be76068..81b2af4f6d 100644 --- a/src/openhuman/todos/graph_shadow.rs +++ b/src/openhuman/todos/graph_shadow.rs @@ -4,8 +4,9 @@ //! //! ADAPTER-FIRST / SHADOW ONLY — nothing in this module changes product //! behavior. The legacy [`TaskBoardStore`](crate::openhuman::agent::task_board) -//! + [`todos::ops`](crate::openhuman::todos::ops) remain the single source of -//! truth. This module (C2b first slice) mirrors post-mutation card snapshots +//! + [`todos::ops`](crate::openhuman::todos::ops) remain the single source of truth. +//! +//! This module (C2b first slice) mirrors post-mutation card snapshots //! into a crate `Store` and shadow-runs the crate `claim_card` CAS purely to //! prove parity ahead of the C2 cutover, logging any divergence. All work is //! best-effort and fire-and-forget: a mirror/claim failure is logged and diff --git a/src/openhuman/tools/impl/filesystem/glob_search.rs b/src/openhuman/tools/impl/filesystem/glob_search.rs index 982c5474f2..c73f15d3cc 100644 --- a/src/openhuman/tools/impl/filesystem/glob_search.rs +++ b/src/openhuman/tools/impl/filesystem/glob_search.rs @@ -301,7 +301,7 @@ fn collect_matches( } // Newest first. - hits.sort_by(|a, b| b.0.cmp(&a.0)); + hits.sort_by_key(|item| std::cmp::Reverse(item.0)); let truncated = hits.len() > max_results; let paths: Vec = hits.into_iter().take(max_results).map(|(_, p)| p).collect(); (paths, truncated) diff --git a/src/openhuman/tools/impl/network/polymarket.rs b/src/openhuman/tools/impl/network/polymarket.rs index c0b3218f9e..0da511fc46 100644 --- a/src/openhuman/tools/impl/network/polymarket.rs +++ b/src/openhuman/tools/impl/network/polymarket.rs @@ -1192,10 +1192,10 @@ impl Tool for PolymarketTool { // rejected by the CLOB. Credential derivation is similarly // single-flight (see ensure_clob_credentials OnceCell). Reads remain // concurrency-safe. - match args.get("action").and_then(Value::as_str) { - Some("place_order") | Some("cancel_order") => false, - _ => true, - } + !matches!( + args.get("action").and_then(Value::as_str), + Some("place_order") | Some("cancel_order") + ) } async fn execute(&self, args: Value) -> Result { diff --git a/src/openhuman/tools/impl/system/schedule.rs b/src/openhuman/tools/impl/system/schedule.rs index 712bc86050..50501839ca 100644 --- a/src/openhuman/tools/impl/system/schedule.rs +++ b/src/openhuman/tools/impl/system/schedule.rs @@ -90,10 +90,10 @@ impl Tool for ScheduleTool { // (create/add/once/cancel/remove/pause/resume) persist or remove a // scheduled job and must go through the ApprovalGate // (GHSA-f46p-6vf9-64mm). - match args.get("action").and_then(|v| v.as_str()) { - Some("list") | Some("get") => false, - _ => true, - } + !matches!( + args.get("action").and_then(|v| v.as_str()), + Some("list") | Some("get") + ) } async fn execute(&self, args: serde_json::Value) -> Result { diff --git a/src/openhuman/voice/factory/tests.rs b/src/openhuman/voice/factory/tests.rs index a5884ff09c..7ad2d2fa6d 100644 --- a/src/openhuman/voice/factory/tests.rs +++ b/src/openhuman/voice/factory/tests.rs @@ -2,14 +2,12 @@ use super::entry::{ create_stt_provider, create_tts_provider, default_stt_provider, default_tts_provider, - DEFAULT_WHISPER_MODEL, WHISPER_MODEL_PRESETS, + WHISPER_MODEL_PRESETS, }; use super::helpers::{effective_stt_provider, effective_tts_provider, split_slug_model}; use super::stt_providers::WhisperSttProvider; use super::traits::SttProvider; -use crate::openhuman::config::schema::voice_providers::{ - SttApiStyle, TtsApiStyle, VoiceCapability, -}; +use crate::openhuman::config::schema::voice_providers::{SttApiStyle, VoiceCapability}; use crate::openhuman::config::Config; fn cfg() -> Config { diff --git a/src/openhuman/voice/schemas/mod.rs b/src/openhuman/voice/schemas/mod.rs index e4900a2bdc..034276fba0 100644 --- a/src/openhuman/voice/schemas/mod.rs +++ b/src/openhuman/voice/schemas/mod.rs @@ -23,9 +23,8 @@ use helpers::{ }; #[cfg(test)] use params::{ - OverlaySttNotifyParams, OverlaySttState, ReplySynthesizeParams, SetProvidersParams, - SttDispatchParams, TranscribeBytesParams, TranscribeParams, TtsDispatchParams, TtsParams, - VoiceListModelsParams, VoiceProviderCredUpdate, VoiceTestProviderParams, + SetProvidersParams, SttDispatchParams, TranscribeBytesParams, TranscribeParams, + TtsDispatchParams, TtsParams, VoiceListModelsParams, VoiceTestProviderParams, VoiceUpdateProviderSettingsParams, }; diff --git a/src/openhuman/voice/server.rs b/src/openhuman/voice/server.rs index eaa354f84e..d6cc73118d 100644 --- a/src/openhuman/voice/server.rs +++ b/src/openhuman/voice/server.rs @@ -537,7 +537,7 @@ impl HotkeyListenerKind { fn start_hotkey_listener( hotkey_str: &str, mode: hotkey::ActivationMode, - server_cancel: &CancellationToken, + _server_cancel: &CancellationToken, ) -> Result< ( HotkeyListenerKind, @@ -548,7 +548,7 @@ fn start_hotkey_listener( #[cfg(target_os = "macos")] { if hotkey_str.trim().eq_ignore_ascii_case("fn") { - return start_globe_hotkey_listener(mode, server_cancel); + return start_globe_hotkey_listener(mode, _server_cancel); } // rdev calls TSMGetInputSourceProperty off the main thread; macOS 26 // enforces main-queue-only access and crashes the process. Only the diff --git a/src/openhuman/wallet/chains/btc.rs b/src/openhuman/wallet/chains/btc.rs index 1a5d117bce..49f8b50f77 100644 --- a/src/openhuman/wallet/chains/btc.rs +++ b/src/openhuman/wallet/chains/btc.rs @@ -167,7 +167,7 @@ fn select_utxos( fee_sats: u64, ) -> Result<(Vec, u64), String> { let mut sorted = utxos.to_vec(); - sorted.sort_by(|a, b| b.value.cmp(&a.value)); + sorted.sort_by_key(|item| std::cmp::Reverse(item.value)); let target = amount_sats .checked_add(fee_sats) .ok_or_else(|| "amount + fee overflow".to_string())?; diff --git a/src/openhuman/webview_accounts/ops.rs b/src/openhuman/webview_accounts/ops.rs index 7b45682b76..67b9db7309 100644 --- a/src/openhuman/webview_accounts/ops.rs +++ b/src/openhuman/webview_accounts/ops.rs @@ -34,7 +34,7 @@ struct Provider { /// Providers tracked in the webview-account snapshot. Keep this list aligned /// with the webview accounts system in `app/src-tauri/src/webview_accounts/`. -pub(crate) const PROVIDERS: &[Provider] = &[ +const PROVIDERS: &[Provider] = &[ Provider { key: "gmail", host_suffixes: &[".google.com"], diff --git a/src/openhuman/webview_apis/client.rs b/src/openhuman/webview_apis/client.rs index a5bc26ad5c..8435dcf39a 100644 --- a/src/openhuman/webview_apis/client.rs +++ b/src/openhuman/webview_apis/client.rs @@ -34,6 +34,8 @@ pub const PORT_ENV: &str = "OPENHUMAN_WEBVIEW_APIS_PORT"; const REQUEST_TIMEOUT: Duration = Duration::from_secs(15); static CLIENT: OnceLock = OnceLock::new(); +type PendingResponse = oneshot::Sender>; +type PendingRequests = Arc>>; fn client() -> &'static Client { CLIENT.get_or_init(Client::new) @@ -75,7 +77,7 @@ where struct Client { next_id: AtomicU64, - pending: Arc>>>>, + pending: PendingRequests, sink: Arc>>>, } diff --git a/src/openhuman/x402/x402_tests.rs b/src/openhuman/x402/x402_tests.rs index 9cfc74cbc1..d1b8746294 100644 --- a/src/openhuman/x402/x402_tests.rs +++ b/src/openhuman/x402/x402_tests.rs @@ -1,8 +1,6 @@ -use base64::engine::{general_purpose::STANDARD as B64, Engine as _}; -use serde_json::json; - use super::ops::{build_evm_payment_with_signer, eip3009_struct_hash, eip712_domain_separator}; use super::types::*; +use base64::engine::{general_purpose::STANDARD as B64, Engine as _}; #[test] fn parse_payment_required_round_trips() { diff --git a/tests/agent_harness_e2e.rs b/tests/agent_harness_e2e.rs index 9f854b7694..8bbd69760a 100644 --- a/tests/agent_harness_e2e.rs +++ b/tests/agent_harness_e2e.rs @@ -1861,7 +1861,8 @@ async fn approval_gate_timeout_inner() { // ─── Task 7: Max iterations + empty provider response ──────────────────────── // // max_iterations_exceeded: -// Default max_tool_iterations = 10 (tool_loop.rs:15). +// The orchestrator's effective max_tool_iterations comes from its agent +// definition (currently 15), rather than the global default of 10. // Circuit breakers (REPEAT_FAILURE_THRESHOLD=3 on failing calls, // NO_PROGRESS_FAILURE_THRESHOLD=6 on consecutive fails) only fire on // success=false outcomes. We must pick a tool that: @@ -1879,9 +1880,9 @@ async fn approval_gate_timeout_inner() { // resolve_time IS in ops.rs (line 192) and in orchestrator named (agent.toml:173). // It's a pure chrono calculation, no I/O, always succeeds. Varying the // `expression` arg (format!("{i}m ago")) gives a different hash each iteration, -// preventing REPEAT_OUTPUT_THRESHOLD from firing. Queuing 12 calls trips the -// max_tool_iterations cap at 10 (DEFAULT_MAX_TOOL_ITERATIONS, tool_loop.rs:15). -// AgentError::MaxIterationsExceeded → "Agent exceeded maximum tool iterations (10)" +// preventing REPEAT_OUTPUT_THRESHOLD from firing. Queuing beyond the +// definition-derived cap trips max_tool_iterations. AgentError::MaxIterationsExceeded → +// "Agent exceeded maximum tool iterations (N)" // (error.rs:89-90; MAX_ITERATIONS_ERROR_PREFIX at error.rs:176). // // empty_provider_response: @@ -1910,11 +1911,19 @@ fn max_iterations_exceeded() { async fn max_iterations_exceeded_inner() { let _lock = env_lock(); - // 12 tool calls > default max_tool_iterations (10). Each uses a unique + init_agent_def_registry(); + let max_iterations = AgentDefinitionRegistry::global() + .and_then(|registry| registry.get("orchestrator")) + .map(|definition| definition.effective_max_iterations()) + .expect("built-in orchestrator definition must exist"); + + // Queue beyond the definition-derived cap. Deriving this count from the + // same definition used by the session builder keeps the regression valid + // when the orchestrator's policy changes. Each call uses a unique // expression to prevent REPEAT_OUTPUT_THRESHOLD from firing first. // resolve_time is a pure computation tool (no I/O) that always succeeds. // The required parameter name is "expr" (resolve_time.rs schema). - let responses: Vec = (0..12) + let responses: Vec = (0..max_iterations + 5) .map(|i| tool_call_completion("resolve_time", json!({ "expr": format!("{}m ago", i + 1) }))) .collect(); reset_script(responses); diff --git a/tests/json_rpc_e2e.rs b/tests/json_rpc_e2e.rs index 90668c3263..bcd7c0e0d6 100644 --- a/tests/json_rpc_e2e.rs +++ b/tests/json_rpc_e2e.rs @@ -485,12 +485,6 @@ fn mock_upstream_router() -> Router { let checkout_url = "http://127.0.0.1/mock-checkout"; let session_id = "cs_mock_abc"; - if checkout_url.is_empty() || session_id.is_empty() { - return Err(error_json( - StatusCode::BAD_REQUEST, - "missing checkoutUrl or sessionId", - )); - } Ok(Json(json!({ "success": true, @@ -501,9 +495,6 @@ fn mock_upstream_router() -> Router { async fn stripe_portal(headers: HeaderMap) -> Result, (StatusCode, Json)> { require_bearer(&headers, BILLING_TOKEN)?; let portal_url = "http://127.0.0.1/mock-portal"; - if portal_url.is_empty() { - return Err(error_json(StatusCode::BAD_REQUEST, "missing portalUrl")); - } Ok(Json(json!({ "success": true, @@ -1141,7 +1132,7 @@ async fn json_rpc_config_update_browser_settings_persists_backend() { let updated = post_json_rpc( &rpc_base, - 4124_1, + 41_241, "openhuman.config_update_browser_settings", json!({ "enabled": true, @@ -1167,7 +1158,7 @@ async fn json_rpc_config_update_browser_settings_persists_backend() { "browser backend should persist in update response: {updated_snapshot}" ); - let get = post_json_rpc(&rpc_base, 4124_2, "openhuman.config_get", json!({})).await; + let get = post_json_rpc(&rpc_base, 41_242, "openhuman.config_get", json!({})).await; let get_result = assert_no_jsonrpc_error(&get, "config_get"); let snapshot = peel_logs_envelope(get_result); assert_eq!( @@ -1180,7 +1171,7 @@ async fn json_rpc_config_update_browser_settings_persists_backend() { let invalid = post_json_rpc( &rpc_base, - 4124_3, + 41_243, "openhuman.config_update_browser_settings", json!({ "backend": "netscape" @@ -1201,7 +1192,7 @@ async fn json_rpc_tokenjuice_detect_and_cache_stats() { // detect: a JSON array of objects classifies as `json`. let detect = post_json_rpc( &rpc_base, - 1860_1, + 18_601, "openhuman.tokenjuice_detect", json!({ "content": r#"[{"a":1,"b":2},{"a":3,"b":4}]"# }), ) @@ -1215,7 +1206,7 @@ async fn json_rpc_tokenjuice_detect_and_cache_stats() { // cache_stats: returns numeric occupancy fields. let stats = post_json_rpc( &rpc_base, - 1860_2, + 18_602, "openhuman.tokenjuice_cache_stats", json!({}), ) @@ -1240,7 +1231,7 @@ async fn json_rpc_tokenjuice_settings_and_savings() { // CCR token threshold (default 500) and the router master switch. let get = post_json_rpc( &rpc_base, - 1861_1, + 18_611, "openhuman.tokenjuice_settings_get", json!({}), ) @@ -1261,7 +1252,7 @@ async fn json_rpc_tokenjuice_settings_and_savings() { // settings_update: flip the CCR token threshold and confirm it round-trips. let updated = post_json_rpc( &rpc_base, - 1861_2, + 18_612, "openhuman.tokenjuice_settings_update", json!({ "patch": { "ccr_min_tokens": 750 } }), ) @@ -1278,7 +1269,7 @@ async fn json_rpc_tokenjuice_settings_and_savings() { // savings_stats: returns the aggregate shape (total + attribution model + cache). let savings = post_json_rpc( &rpc_base, - 1861_3, + 18_613, "openhuman.tokenjuice_savings_stats", json!({}), ) @@ -1295,7 +1286,7 @@ async fn json_rpc_tokenjuice_settings_and_savings() { // savings_reset: zeroes the totals. let reset = post_json_rpc( &rpc_base, - 1861_4, + 18_614, "openhuman.tokenjuice_savings_reset", json!({}), ) @@ -1312,7 +1303,7 @@ async fn json_rpc_tool_registry_lists_and_gets_entries() { let (rpc_addr, rpc_join) = serve_on_ephemeral(build_core_http_router(false)).await; let rpc_base = format!("http://{rpc_addr}"); - let list = post_json_rpc(&rpc_base, 1848_1, "openhuman.tool_registry_list", json!({})).await; + let list = post_json_rpc(&rpc_base, 18_481, "openhuman.tool_registry_list", json!({})).await; let list_result = assert_no_jsonrpc_error(&list, "tool_registry_list"); let tools = list_result .get("tools") @@ -1359,7 +1350,7 @@ async fn json_rpc_tool_registry_lists_and_gets_entries() { let get = post_json_rpc( &rpc_base, - 1848_2, + 18_482, "openhuman.tool_registry_get", json!({ "tool_id": "tools.web_search" }), ) @@ -1381,7 +1372,7 @@ async fn json_rpc_tool_registry_lists_and_gets_entries() { let missing = post_json_rpc( &rpc_base, - 1848_3, + 18_483, "openhuman.tool_registry_get", json!({ "tool_id": "missing.tool" }), ) @@ -1405,7 +1396,7 @@ async fn json_rpc_monitor_list_and_read_surface() { let (rpc_addr, rpc_join) = serve_on_ephemeral(build_core_http_router(false)).await; let rpc_base = format!("http://{rpc_addr}"); - let list = post_json_rpc(&rpc_base, 3371_1, "openhuman.monitor_list", json!({})).await; + let list = post_json_rpc(&rpc_base, 33_711, "openhuman.monitor_list", json!({})).await; let list_result = assert_no_jsonrpc_error(&list, "monitor_list"); let monitors = list_result .get("monitors") @@ -1418,7 +1409,7 @@ async fn json_rpc_monitor_list_and_read_surface() { let missing = post_json_rpc( &rpc_base, - 3371_2, + 33_712, "openhuman.monitor_read", json!({ "monitor_id": "mon_missing" }), ) @@ -1447,7 +1438,7 @@ async fn json_rpc_harness_init_status_returns_snapshot_envelope() { // attempt real Python/Node/spaCy downloads). let resp = post_json_rpc( &rpc_base, - 4471_1, + 44_711, "openhuman.harness_init_status", json!({}), ) @@ -1500,7 +1491,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let list = post_json_rpc( &rpc_base, - 2862_1, + 28_621, "openhuman.agent_registry_list", json!({ "include_disabled": true }), ) @@ -1525,7 +1516,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let definitions = post_json_rpc( &rpc_base, - 2862_1_1, + 286_211, "openhuman.agent_list_definitions", json!({}), ) @@ -1560,7 +1551,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let missing = post_json_rpc( &rpc_base, - 2862_10, + 286_210, "openhuman.agent_registry_get", json!({ "id": "does_not_exist" }), ) @@ -1572,7 +1563,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let update_default = post_json_rpc( &rpc_base, - 2862_11, + 286_211, "openhuman.agent_registry_update", json!({ "id": "researcher", @@ -1604,7 +1595,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let update_missing = post_json_rpc( &rpc_base, - 2862_12, + 286_212, "openhuman.agent_registry_update", json!({ "id": "missing_agent", "enabled": false }), ) @@ -1622,7 +1613,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let disabled = post_json_rpc( &rpc_base, - 2862_2, + 28_622, "openhuman.agent_registry_set_enabled", json!({ "id": "code_executor", "enabled": false }), ) @@ -1645,7 +1636,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let visible = post_json_rpc( &rpc_base, - 2862_3, + 28_623, "openhuman.agent_registry_list", json!({}), ) @@ -1664,7 +1655,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let all_after_disable = post_json_rpc( &rpc_base, - 2862_13, + 286_213, "openhuman.agent_registry_list", json!({ "include_disabled": true }), ) @@ -1689,7 +1680,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let reenabled = post_json_rpc( &rpc_base, - 2862_14, + 286_214, "openhuman.agent_registry_set_enabled", json!({ "id": "code_executor", "enabled": true }), ) @@ -1704,7 +1695,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let disabled_orchestrator = post_json_rpc( &rpc_base, - 2862_31, + 286_231, "openhuman.agent_registry_set_enabled", json!({ "id": "orchestrator", "enabled": false }), ) @@ -1724,7 +1715,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let update_orchestrator_disabled = post_json_rpc( &rpc_base, - 2862_32, + 286_232, "openhuman.agent_registry_update", json!({ "id": "orchestrator", "enabled": false }), ) @@ -1744,7 +1735,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let created = post_json_rpc( &rpc_base, - 2862_4, + 28_624, "openhuman.agent_registry_create_custom", json!({ "id": "custom_writer", @@ -1780,7 +1771,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let get_custom = post_json_rpc( &rpc_base, - 2862_5, + 28_625, "openhuman.agent_registry_get", json!({ "id": "custom_writer" }), ) @@ -1797,7 +1788,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let updated_custom = post_json_rpc( &rpc_base, - 2862_15, + 286_215, "openhuman.agent_registry_update", json!({ "id": "custom_writer", @@ -1838,7 +1829,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let reenabled_custom = post_json_rpc( &rpc_base, - 2862_16, + 286_216, "openhuman.agent_registry_set_enabled", json!({ "id": "custom_writer", "enabled": true }), ) @@ -1853,7 +1844,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let full_upsert = post_json_rpc( &rpc_base, - 2862_17, + 286_217, "openhuman.agent_registry_upsert_custom", json!({ "agent": { @@ -1901,7 +1892,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let visible_after_custom_disable = post_json_rpc( &rpc_base, - 2862_18, + 286_218, "openhuman.agent_registry_list", json!({}), ) @@ -1922,7 +1913,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let default_collision = post_json_rpc( &rpc_base, - 2862_6, + 28_626, "openhuman.agent_registry_create_custom", json!({ "id": "orchestrator", @@ -1946,7 +1937,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let removed_reviewer = post_json_rpc( &rpc_base, - 2862_19, + 286_219, "openhuman.agent_registry_remove", json!({ "id": "custom_reviewer" }), ) @@ -1960,7 +1951,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let removed_custom = post_json_rpc( &rpc_base, - 2862_7, + 28_627, "openhuman.agent_registry_remove", json!({ "id": "custom_writer" }), ) @@ -1976,7 +1967,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let removed_missing = post_json_rpc( &rpc_base, - 2862_20, + 286_220, "openhuman.agent_registry_remove", json!({ "id": "missing_agent" }), ) @@ -1990,7 +1981,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let reset_default = post_json_rpc( &rpc_base, - 2862_21, + 286_221, "openhuman.agent_registry_remove", json!({ "id": "researcher" }), ) @@ -2004,7 +1995,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let reset_code_executor = post_json_rpc( &rpc_base, - 2862_22, + 286_222, "openhuman.agent_registry_remove", json!({ "id": "code_executor" }), ) @@ -2021,7 +2012,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { let code_executor = post_json_rpc( &rpc_base, - 2862_23, + 286_223, "openhuman.agent_registry_get", json!({ "id": "code_executor" }), ) @@ -2038,7 +2029,7 @@ async fn json_rpc_agent_registry_manages_defaults_and_custom_agents() { // a {name, description} pair whose name is a valid tool_allowlist value. let available_tools = post_json_rpc( &rpc_base, - 2862_24, + 286_224, "openhuman.agent_registry_available_tools", json!({}), ) @@ -2201,12 +2192,11 @@ async fn json_rpc_protocol_auth_and_agent_hello_inner() { Some(thread_id) ); assert!( - sse_event + !sse_event .get("full_response") .and_then(Value::as_str) .unwrap_or_default() - .len() - > 0, + .is_empty(), "expected non-empty chat_done response payload: {sse_event}" ); @@ -3011,7 +3001,7 @@ async fn json_rpc_thread_goal_lifecycle() { // Pull the `{ goal }` payload out of either response shape: a bare // `{ goal }` (get) or the `{ result: { goal }, logs }` CLI envelope // (mutations). - fn goal_of<'a>(result: &'a Value) -> Option<&'a Value> { + fn goal_of(result: &Value) -> Option<&Value> { let envelope = result.get("result").unwrap_or(result); envelope.get("goal").filter(|g| !g.is_null()) } @@ -5535,9 +5525,9 @@ async fn json_rpc_web_chat_custom_chat_provider_with_auth_none_omits_auth_header ) .await; assert_no_jsonrpc_error(&update, "update_model_settings"); - let cfg = post_json_rpc(&rpc_base, 6102_1, "openhuman.config_get", json!({})).await; + let cfg = post_json_rpc(&rpc_base, 61_021, "openhuman.config_get", json!({})).await; let cfg_outer = assert_no_jsonrpc_error(&cfg, "config_get auth-none"); - let cfg_payload = cfg_outer.get("result").unwrap_or(&cfg_outer); + let cfg_payload = cfg_outer.get("result").unwrap_or(cfg_outer); let config = cfg_payload.get("config").unwrap_or(cfg_payload); assert_eq!( config.get("chat_provider").and_then(Value::as_str), @@ -6079,7 +6069,7 @@ async fn json_rpc_app_state_update_local_state_round_trips_into_snapshot() { ) .await; let update_result = assert_no_jsonrpc_error(&update, "app_state_update_local_state"); - let updated_state = update_result.get("result").unwrap_or(&update_result); + let updated_state = update_result.get("result").unwrap_or(update_result); assert_eq!( updated_state.get("encryptionKey").and_then(Value::as_str), Some("secret-key") @@ -6087,7 +6077,7 @@ async fn json_rpc_app_state_update_local_state_round_trips_into_snapshot() { let snapshot = post_json_rpc(&rpc_base, 10042, "openhuman.app_state_snapshot", json!({})).await; let snapshot_result = assert_no_jsonrpc_error(&snapshot, "app_state_snapshot after update"); - let body = snapshot_result.get("result").unwrap_or(&snapshot_result); + let body = snapshot_result.get("result").unwrap_or(snapshot_result); let local_state = body .get("localState") .and_then(Value::as_object) @@ -6275,7 +6265,7 @@ async fn json_rpc_wallet_execution_surface_round_trips() { ) .await; let body = assert_no_jsonrpc_error(&assets, "wallet_supported_assets"); - let result = body.get("result").unwrap_or(&body); + let result = body.get("result").unwrap_or(body); let list = result.as_array().expect("supported_assets array"); // Pin the actual expected multi-chain catalog (not just a lower bound) so a // regression that silently drops a network is caught. @@ -6320,7 +6310,7 @@ async fn json_rpc_wallet_execution_surface_round_trips() { // chain_status: every chain is configured, so the provider row is ready. let cs = post_json_rpc(&rpc_base, 2003, "openhuman.wallet_chain_status", json!({})).await; let body = assert_no_jsonrpc_error(&cs, "wallet_chain_status"); - let result = body.get("result").unwrap_or(&body); + let result = body.get("result").unwrap_or(body); let rows = result.as_array().expect("chain_status array"); // 6 EVM rows (one per L2 / mainnet, incl. BNB Chain) + 3 non-EVM chains. assert_eq!(rows.len(), 9); @@ -6335,7 +6325,7 @@ async fn json_rpc_wallet_execution_surface_round_trips() { // BTC + Solana + Tron = 6. let balances = post_json_rpc(&rpc_base, 2004, "openhuman.wallet_balances", json!({})).await; let body = assert_no_jsonrpc_error(&balances, "wallet_balances"); - let result = body.get("result").unwrap_or(&body); + let result = body.get("result").unwrap_or(body); let rows = result.as_array().expect("balances array"); assert_eq!(rows.len(), 6); assert_eq!( @@ -6369,7 +6359,7 @@ async fn json_rpc_wallet_execution_surface_round_trips() { ) .await; let body = assert_no_jsonrpc_error(&prep, "wallet_prepare_transfer"); - let result = body.get("result").unwrap_or(&body); + let result = body.get("result").unwrap_or(body); let quote_id = result .get("quoteId") .and_then(Value::as_str) @@ -6406,7 +6396,7 @@ async fn json_rpc_wallet_execution_surface_round_trips() { ) .await; let body = assert_no_jsonrpc_error(&exec, "wallet_execute_prepared"); - let result = body.get("result").unwrap_or(&body); + let result = body.get("result").unwrap_or(body); assert_eq!( result.get("status").and_then(Value::as_str), Some("broadcasted"), @@ -6482,7 +6472,7 @@ async fn json_rpc_wallet_tx_reads_and_web3_gates_round_trip() { ) .await; let body = assert_no_jsonrpc_error(&status, "wallet_tx_status"); - let result = body.get("result").unwrap_or(&body); + let result = body.get("result").unwrap_or(body); assert_eq!( result.get("state").and_then(Value::as_str), Some("confirmed") @@ -6497,7 +6487,7 @@ async fn json_rpc_wallet_tx_reads_and_web3_gates_round_trip() { ) .await; let body = assert_no_jsonrpc_error(&receipt, "wallet_tx_receipt"); - let result = body.get("result").unwrap_or(&body); + let result = body.get("result").unwrap_or(body); assert_eq!(result.get("success").and_then(Value::as_bool), Some(true)); assert_eq!(result.get("gasUsed").and_then(Value::as_str), Some("21000")); @@ -6510,7 +6500,7 @@ async fn json_rpc_wallet_tx_reads_and_web3_gates_round_trip() { ) .await; let body = assert_no_jsonrpc_error(&lookup, "wallet_lookup_tx"); - let result = body.get("result").unwrap_or(&body); + let result = body.get("result").unwrap_or(body); assert_eq!(result.get("found").and_then(Value::as_bool), Some(true)); // web3_bridge rejects same-chain requests. This gate runs *before* any @@ -7110,7 +7100,7 @@ async fn json_rpc_wallet_tron_prepare_execute_round_trips() { ); assert_eq!( exec_result.get("transactionHash").and_then(Value::as_str), - Some(format!("{}", "cd".repeat(32)).as_str()), + Some("cd".repeat(32).to_string().as_str()), ); // Native TRX must go through createtransaction, NOT triggersmartcontract. let create_hits = *tron_mock.state.create_hits.lock().unwrap(); @@ -12158,8 +12148,7 @@ async fn json_rpc_task_sources_crud_and_status() { mod task_sources_stub { use async_trait::async_trait; use openhuman_core::openhuman::memory_sync::composio::providers::{ - ComposioProvider, NormalizedTask, ProviderContext, ProviderUserProfile, SyncOutcome, - SyncReason, TaskFetchFilter, + ComposioProvider, NormalizedTask, ProviderContext, ProviderUserProfile, TaskFetchFilter, }; pub struct StubGithubProvider { @@ -13785,7 +13774,7 @@ async fn json_rpc_memory_sync_settings_roundtrip_interval_and_manual() { ) .await; let initial_outer = assert_no_jsonrpc_error(&initial, "get_memory_sync_settings initial"); - let initial_result = initial_outer.get("result").unwrap_or(&initial_outer); + let initial_result = initial_outer.get("result").unwrap_or(initial_outer); assert!( initial_result.get("sync_interval_secs").map(Value::is_null) == Some(true), "default stored value should be null, envelope: {initial_outer}" @@ -13810,7 +13799,7 @@ async fn json_rpc_memory_sync_settings_roundtrip_interval_and_manual() { ) .await; let update_outer = assert_no_jsonrpc_error(&update, "update_memory_sync_settings 4h"); - let update_result = update_outer.get("result").unwrap_or(&update_outer); + let update_result = update_outer.get("result").unwrap_or(update_outer); assert_eq!( update_result .get("sync_interval_secs") @@ -13832,7 +13821,7 @@ async fn json_rpc_memory_sync_settings_roundtrip_interval_and_manual() { ) .await; let after_outer = assert_no_jsonrpc_error(&after, "get_memory_sync_settings after 4h"); - let after_result = after_outer.get("result").unwrap_or(&after_outer); + let after_result = after_outer.get("result").unwrap_or(after_outer); assert_eq!( after_result .get("sync_interval_secs") @@ -13850,7 +13839,7 @@ async fn json_rpc_memory_sync_settings_roundtrip_interval_and_manual() { ) .await; let manual_outer = assert_no_jsonrpc_error(&manual, "update_memory_sync_settings manual"); - let manual_result = manual_outer.get("result").unwrap_or(&manual_outer); + let manual_result = manual_outer.get("result").unwrap_or(manual_outer); assert_eq!( manual_result.get("is_manual").and_then(Value::as_bool), Some(true), @@ -13866,7 +13855,7 @@ async fn json_rpc_memory_sync_settings_roundtrip_interval_and_manual() { ) .await; let manual_get_outer = assert_no_jsonrpc_error(&manual_get, "get_memory_sync_settings manual"); - let manual_get_result = manual_get_outer.get("result").unwrap_or(&manual_get_outer); + let manual_get_result = manual_get_outer.get("result").unwrap_or(manual_get_outer); assert_eq!( manual_get_result.get("is_manual").and_then(Value::as_bool), Some(true), @@ -13918,7 +13907,7 @@ async fn json_rpc_memory_sync_settings_env_override_is_reflected() { ) .await; let outer = assert_no_jsonrpc_error(&resp, "get_memory_sync_settings env-override"); - let result = outer.get("result").unwrap_or(&outer); + let result = outer.get("result").unwrap_or(outer); assert_eq!( result.get("sync_interval_secs").and_then(Value::as_u64), Some(28_800), @@ -14517,7 +14506,7 @@ async fn json_rpc_workflow_run_engine_executes_builtin_to_completion_inner() { // Authenticate so the child agents' provider has a backend session. let store = post_json_rpc( &rpc_base, - 37_500_1, + 375_001, "openhuman.auth_store_session", json!({ "token": "e2e-test-jwt", "user_id": "e2e-user" }), ) @@ -14527,7 +14516,7 @@ async fn json_rpc_workflow_run_engine_executes_builtin_to_completion_inner() { // Sanity: the builtin definition is listed. let defs = post_json_rpc( &rpc_base, - 37_500_2, + 375_002, "openhuman.workflow_run_list_definitions", json!({}), ) @@ -14548,7 +14537,7 @@ async fn json_rpc_workflow_run_engine_executes_builtin_to_completion_inner() { // `modelOverride` pin → every phase hits the mock chat-completions route. let start = post_json_rpc( &rpc_base, - 37_500_3, + 375_003, "openhuman.workflow_run_start", json!({ "definitionId": definition_id, @@ -14662,7 +14651,7 @@ async fn json_rpc_agent_team_live_member_run_roundtrip_inner() { // Authenticate so the spawned workers' provider has a backend session. let store = post_json_rpc( &rpc_base, - 38_000_1, + 380_001, "openhuman.auth_store_session", json!({ "token": "e2e-test-jwt", "user_id": "e2e-user" }), ) @@ -14672,7 +14661,7 @@ async fn json_rpc_agent_team_live_member_run_roundtrip_inner() { // Lead creates a team with two named teammates. let created = post_json_rpc( &rpc_base, - 38_000_2, + 380_002, "openhuman.agent_team_create", json!({ "leadAgentId": "lead", @@ -14709,7 +14698,7 @@ async fn json_rpc_agent_team_live_member_run_roundtrip_inner() { // Task A (no deps) owned by alice; Task B depends on A, owned by bob. let assign_a = post_json_rpc( &rpc_base, - 38_000_3, + 380_003, "openhuman.agent_team_assign_task", json!({ "teamId": team_id, "title": "Task A", "ownerMemberId": alice_id, "dependsOn": [] }), ) @@ -14722,7 +14711,7 @@ async fn json_rpc_agent_team_live_member_run_roundtrip_inner() { .to_string(); let assign_b = post_json_rpc( &rpc_base, - 38_000_4, + 380_004, "openhuman.agent_team_assign_task", json!({ "teamId": team_id, "title": "Task B", "ownerMemberId": bob_id, "dependsOn": [task_a_id.clone()] }), ) @@ -14737,7 +14726,7 @@ async fn json_rpc_agent_team_live_member_run_roundtrip_inner() { // Lead messages a named teammate (fromMemberId omitted → lead origin). let msg = post_json_rpc( &rpc_base, - 38_000_5, + 380_005, "openhuman.agent_team_message_member", json!({ "teamId": team_id, "toMemberId": alice_id, "content": "please start Task A" }), ) @@ -14747,7 +14736,7 @@ async fn json_rpc_agent_team_live_member_run_roundtrip_inner() { // Start alice live on Task A → kind "started". let start_a = post_json_rpc( &rpc_base, - 38_000_6, + 380_006, "openhuman.agent_team_start_member", json!({ "teamId": team_id, "memberId": alice_id, "taskId": task_a_id, "modelOverride": "e2e-mock-model" }), ) @@ -14770,7 +14759,7 @@ async fn json_rpc_agent_team_live_member_run_roundtrip_inner() { // With A done, start bob live on Task B. let start_b = post_json_rpc( &rpc_base, - 38_000_7, + 380_007, "openhuman.agent_team_start_member", json!({ "teamId": team_id, "memberId": bob_id, "taskId": task_b_id, "modelOverride": "e2e-mock-model" }), ) @@ -14792,7 +14781,7 @@ async fn json_rpc_agent_team_live_member_run_roundtrip_inner() { // lead message is in the team timeline. let got = post_json_rpc( &rpc_base, - 38_000_8, + 380_008, "openhuman.agent_team_get", json!({ "teamId": team_id }), ) @@ -14833,7 +14822,7 @@ async fn json_rpc_agent_team_live_member_run_roundtrip_inner() { let messages = post_json_rpc( &rpc_base, - 38_000_9, + 380_009, "openhuman.agent_team_list_messages", json!({ "teamId": team_id }), ) diff --git a/tests/keyring_secretstore_e2e.rs b/tests/keyring_secretstore_e2e.rs index 5adaea2b6f..a5d3fa1cd5 100644 --- a/tests/keyring_secretstore_e2e.rs +++ b/tests/keyring_secretstore_e2e.rs @@ -1,10 +1,12 @@ use openhuman_core::openhuman::config::schema::{Config, StreamMode, TelegramConfig}; use openhuman_core::openhuman::keyring; -use std::sync::{Mutex, OnceLock}; +use std::sync::OnceLock; -fn env_lock() -> std::sync::MutexGuard<'static, ()> { - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())).lock().unwrap() +async fn env_lock() -> tokio::sync::MutexGuard<'static, ()> { + static LOCK: OnceLock> = OnceLock::new(); + LOCK.get_or_init(|| tokio::sync::Mutex::new(())) + .lock() + .await } struct EnvGuard { @@ -45,7 +47,7 @@ impl Drop for EnvGuard { #[tokio::test] async fn config_secrets_roundtrip_via_keyring_backed_master_key_migration() { - let _guard = env_lock(); + let _guard = env_lock().await; let tmp = tempfile::tempdir().expect("tempdir"); let openhuman_dir = tmp.path().join("user-123"); let workspace_dir = openhuman_dir.join("workspace"); diff --git a/tests/raw_coverage/agent_session_round24_raw_coverage_e2e.rs b/tests/raw_coverage/agent_session_round24_raw_coverage_e2e.rs index 0e0d455b69..37de29f215 100644 --- a/tests/raw_coverage/agent_session_round24_raw_coverage_e2e.rs +++ b/tests/raw_coverage/agent_session_round24_raw_coverage_e2e.rs @@ -411,13 +411,21 @@ async fn max_iteration_checkpoint_uses_deterministic_fallback_and_hooks() { let calls = Arc::new(AtomicUsize::new(0)); let hook_calls = Arc::new(AtomicUsize::new(0)); let hook_contexts = Arc::new(Mutex::new(Vec::new())); - // The wrap-up model call yields nothing (empty text + no streamed deltas), so - // `summarize_turn_wrapup` returns an empty summary and the cap path falls back - // to `build_deterministic_checkpoint` — the exact path this test covers. (A - // non-empty wrap-up would instead be surfaced verbatim as the answer.) + // The wrap-up ignores the no-tools instruction and emits another + // prompt-formatted tool call plus a streamed delta. Validation must reject + // both before progress consumers see them, then use the deterministic + // checkpoint fallback. let provider = ScriptedProvider::with_stream( - vec![xml_tool_response("alpha"), text_response("", None)], - vec![], + vec![ + xml_tool_response("alpha"), + text_response( + "{\"name\":\"round24_echo\",\"arguments\":{\"value\":\"again\"}}", + None, + ), + ], + vec![ProviderDelta::TextDelta { + delta: "checkpoint delta".to_string(), + }], ); let mut agent = Agent::builder() @@ -476,12 +484,13 @@ async fn max_iteration_checkpoint_uses_deterministic_fallback_and_hooks() { while let Ok(event) = progress_rx.try_recv() { streamed.push(event); } - // The wrap-up produced no text, so no iteration-2 wrap-up delta is streamed; - // the deterministic fallback (asserted above) becomes the answer instead. assert!(!streamed.iter().any(|event| matches!( event, - openhuman_core::openhuman::agent::progress::AgentProgress::TextDelta { iteration: 2, .. } - ))); + openhuman_core::openhuman::agent::progress::AgentProgress::TextDelta { + delta, + iteration: 2 + } if delta == "checkpoint delta" + )), "rejected checkpoint deltas must not leak to progress consumers"); } #[tokio::test] diff --git a/tests/subconscious_conversation_e2e.rs b/tests/subconscious_conversation_e2e.rs index 8b4e2fd6cb..713c940ceb 100644 --- a/tests/subconscious_conversation_e2e.rs +++ b/tests/subconscious_conversation_e2e.rs @@ -217,11 +217,11 @@ impl SessionExecutor for ScriptedSession { // ───────────────────────────────────────────────────────────────────────────── /// Serializes tests that touch the process-global event bus. -fn bus_lock() -> std::sync::MutexGuard<'static, ()> { - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| StdMutex::new(())) +async fn bus_lock() -> tokio::sync::MutexGuard<'static, ()> { + static LOCK: OnceLock> = OnceLock::new(); + LOCK.get_or_init(|| tokio::sync::Mutex::new(())) .lock() - .unwrap_or_else(|p| p.into_inner()) + .await } struct Harness { @@ -350,7 +350,7 @@ fn human_msg(channel: &str, sender: &str, message: &str) -> DomainEvent { #[tokio::test] async fn conversation_human_delegates_then_subagent_reports_back() { - let _g = bus_lock(); + let _g = bus_lock().await; let h = Harness::new(OrchestratorConfig::default()); // Human asks for deep work. @@ -382,7 +382,7 @@ async fn conversation_human_delegates_then_subagent_reports_back() { #[tokio::test] async fn conversation_subagent_failure_recovers_with_retry() { - let _g = bus_lock(); + let _g = bus_lock().await; let h = Harness::new(OrchestratorConfig::default()); // Inject a sub-agent FAILURE conclusion directly (as if a prior spawn failed). @@ -408,7 +408,7 @@ async fn conversation_subagent_failure_recovers_with_retry() { #[tokio::test] async fn conversation_interleaved_traffic_is_handled() { - let _g = bus_lock(); + let _g = bus_lock().await; let h = Harness::new(OrchestratorConfig::default()); // Routine cron tick — gate should drop it (no session run). @@ -446,7 +446,7 @@ async fn conversation_interleaved_traffic_is_handled() { #[tokio::test] async fn conversation_promotion_budget_caps_a_burst() { - let _g = bus_lock(); + let _g = bus_lock().await; // The scripted gate intentionally has no promotion budget (that lives in // the real GatePass), so this scenario isolates the *rate limiter*: a // flood of distinct user messages from one source is capped by the token diff --git a/tests/transcript_search_e2e.rs b/tests/transcript_search_e2e.rs index c2cecc013c..84b1f8b9f0 100644 --- a/tests/transcript_search_e2e.rs +++ b/tests/transcript_search_e2e.rs @@ -13,7 +13,7 @@ //! Run with: `cargo test --test transcript_search_e2e` use std::path::Path; -use std::sync::{Mutex, OnceLock}; +use std::sync::OnceLock; use serde_json::json; use tempfile::tempdir; @@ -53,13 +53,13 @@ impl Drop for EnvVarGuard { } /// Serialises tests: `HOME` + `OPENHUMAN_WORKSPACE` are process-global. -static ENV_LOCK: OnceLock> = OnceLock::new(); +static ENV_LOCK: OnceLock> = OnceLock::new(); -fn env_lock() -> std::sync::MutexGuard<'static, ()> { +async fn env_lock() -> tokio::sync::MutexGuard<'static, ()> { ENV_LOCK - .get_or_init(|| Mutex::new(())) + .get_or_init(|| tokio::sync::Mutex::new(())) .lock() - .expect("env lock poisoned") + .await } // ── Fixture helpers ────────────────────────────────────────────────────────── @@ -143,7 +143,7 @@ fn seed_workspace(workspace: &Path) -> ConversationStore { /// scopes the hit to the thread that actually contains it. #[tokio::test] async fn transcript_search_op_finds_message_in_prior_thread() { - let _lock = env_lock(); + let _lock = env_lock().await; let tmp = tempdir().expect("tempdir"); let _home = EnvVarGuard::set_to_path("HOME", tmp.path()); let workspace = tmp.path().join("workspace"); @@ -174,7 +174,7 @@ async fn transcript_search_op_finds_message_in_prior_thread() { /// orchestrator can use to omit the active chat it already has in hand. #[tokio::test] async fn transcript_search_op_honours_exclude_thread() { - let _lock = env_lock(); + let _lock = env_lock().await; let tmp = tempdir().expect("tempdir"); let _home = EnvVarGuard::set_to_path("HOME", tmp.path()); let workspace = tmp.path().join("workspace"); @@ -195,7 +195,7 @@ async fn transcript_search_op_honours_exclude_thread() { /// A query that matches nothing returns no hits (not an error). #[tokio::test] async fn transcript_search_op_returns_empty_on_no_match() { - let _lock = env_lock(); + let _lock = env_lock().await; let tmp = tempdir().expect("tempdir"); let _home = EnvVarGuard::set_to_path("HOME", tmp.path()); let workspace = tmp.path().join("workspace"); @@ -215,7 +215,7 @@ async fn transcript_search_op_returns_empty_on_no_match() { /// source thread and quotes a snippet of the matched message. #[tokio::test] async fn transcript_search_tool_formats_hits_for_the_agent() { - let _lock = env_lock(); + let _lock = env_lock().await; let tmp = tempdir().expect("tempdir"); let _home = EnvVarGuard::set_to_path("HOME", tmp.path()); let workspace = tmp.path().join("workspace"); @@ -247,7 +247,7 @@ async fn transcript_search_tool_formats_hits_for_the_agent() { /// can record "nothing in past chats" and move on. #[tokio::test] async fn transcript_search_tool_reports_no_match_cleanly() { - let _lock = env_lock(); + let _lock = env_lock().await; let tmp = tempdir().expect("tempdir"); let _home = EnvVarGuard::set_to_path("HOME", tmp.path()); let workspace = tmp.path().join("workspace"); @@ -276,7 +276,7 @@ async fn transcript_search_tool_reports_no_match_cleanly() { /// clean no-match line. #[tokio::test] async fn transcript_search_tool_excludes_named_thread() { - let _lock = env_lock(); + let _lock = env_lock().await; let tmp = tempdir().expect("tempdir"); let _home = EnvVarGuard::set_to_path("HOME", tmp.path()); let workspace = tmp.path().join("workspace"); @@ -301,7 +301,7 @@ async fn transcript_search_tool_excludes_named_thread() { /// searches all — this pins the empty-string contract regardless.) #[tokio::test] async fn transcript_search_tool_empty_exclude_searches_all_threads() { - let _lock = env_lock(); + let _lock = env_lock().await; let tmp = tempdir().expect("tempdir"); let _home = EnvVarGuard::set_to_path("HOME", tmp.path()); let workspace = tmp.path().join("workspace");