20 KiB
OpenHuman — Code Quality & Code Smell Review
Reviewer: AQE v3 Code Reviewer (sampled, evidence-based)
Scope sampled: 25 files across src/ (Rust core), app/src-tauri/src/ (Tauri host), app/src/ (React/TS)
Method: size-ranked sweeps + grep for smell indicators + targeted line reads. Not exhaustive.
Executive Summary
Severity-ranked (Critical → Info):
- [HIGH] One Tauri host file is a 4,450-line god module.
app/src-tauri/src/webview_accounts/mod.rsowns process state, IPC commands, CDP wiring, notification routing, and webview lifecycle in a single file. The single functionwebview_account_openis 840 lines (line 13033). This blocks safe parallel work, hides invariants, and is the single largest maintainability risk in the codebase. - [HIGH]
std::sync::Mutex+.lock().unwrap()pattern in the Tauri host.webview_accounts/mod.rsusesuse std::sync::Mutex;(line 23) and contains 17 production-path.lock().unwrap()calls (e.g., lines 1119, 1636, 1845, 2655, 2669, 2677, 3040, 3054, 3073, 3083). A single panic while a lock is held permanently poisons it — every subsequent IPC command from React will then crash the host.parking_lot::Mutex(which doesn't poison) is already a direct dep (Cargo.toml) and is the obvious fix. - [HIGH] The agent turn loop is a 967-line god-method.
src/openhuman/agent/harness/session/turn.rs:71—Agent::turnruns from line 71 to ~1037. The same file holds three other 250+-line methods (inject_agent_experience_context345 lines L1038,emit_progress265 lines L1383,build_system_prompt308 lines L1838). The orchestrator-of-everything pattern makes it hard to test branches in isolation and concentrates ownership of unrelated concerns (KV-cache prefix policy, prompt construction, integration hydration, memory injection). - [HIGH] React page-level components carry too much state.
app/src/pages/Conversations.tsxis a single 1,919-line component (L206 → L2124) with 29 hook calls inside one function body.app/src/components/settings/panels/AIPanel.tsxis 2,696 lines containing a 690-lineBackgroundLoopControls(L2948) and a 521-lineAIPanel(L4109). These will not survive their next major refactor without regressions. - [MEDIUM] Env-driven config schema loader is a 720-line method.
src/openhuman/config/schema/load.rs:5732—apply_env_overrides_fromis 720 lines in one function. Any new env var lands in a fan-out ofif let Some(...)arms with no test partitioning. - [MEDIUM] Inconsistent error-type strategy. 392 files use
anyhow, only 8 usethiserror, and 1,062 functions returnResult<..., String>(greppable insrc/). Stringly-typed errors propagate context loss and force callers into substring sniffing — this is exactly whatsrc/core/observability.rs::expected_error_kind(L137) andis_session_expired_message(L227) are doing for ~700 lines: classifying strings into error kinds. This is the long-tail cost of stringly-typed errors made concrete. - [MEDIUM] Pervasive
#![allow(dead_code)]suppression.src/openhuman/mod.rs:16blankets the entireopenhumanmodule subtree with#![allow(dead_code)]. 37 occurrences of#[allow(dead_code)]and 110 total#[allow(...)]attributes acrosssrc/mean the compiler's own dead-code detector is silenced — actual dead code is invisible. - [MEDIUM] Comment-driven feature toggles via
[#1123].app/src/App.tsxhas 7 commented-out blocks tagged[#1123](L23, 48, 50, 125, 161, 194, 215) and the same pattern inConversations.tsx(L14, 21, 23) andpages/Accounts.tsx. Dead code preserved as comments is harder to keep current than removing it and pulling from git history. - [LOW] Hardcoded transport endpoints.
src/openhuman/tools/impl/browser/types.rs:33hardcodeshttp://127.0.0.1:8787/v1/actionsas a default.src/openhuman/webview_apis/client.rs:142constructsws://127.0.0.1:{port}/from an env var with no fallback validation. Both are leaky abstractions for tests/dev that have escaped into production defaults. - [LOW] Documentation outweighs code in places.
AGENTS.mdis 649 lines;CLAUDE.mdis 311. Some module docs (src/openhuman/mod.rs,app/src-tauri/src/webview_accounts/mod.rs) are genuinely good; others (e.g.,src/openhuman/agent/harness/session/turn.rs:51-68) read like docstrings written to compensate for function size rather than to document an interface.
Top-level finding: the codebase is not sloppy — naming is good, error handling is principled in most modules, doc-comments are present, and the test scaffolding is substantial (~85k LOC of test code vs ~360k LOC of prod Rust = ~24% test-LOC ratio, healthy for a Rust codebase). The structural problems are concentrated in a small number of hotspot files that have grown organically into god-objects.
Quantitative Metrics
| Metric | Value | Note |
|---|---|---|
| Rust files (prod + test) | 1,323 | under src/, packages/, app/src-tauri/ |
| TS/TSX files (prod + test) | 895 | under app/src/, packages/ |
| Rust prod LOC | ~360k | excludes *_test*.rs and tests.rs |
| Rust test LOC | ~86k | *_test*.rs + tests.rs files |
| TS prod LOC | ~110k | excludes __tests__/ and *.test.* |
| Workspace structure | single crate | no [workspace] in root Cargo.toml; 74 pub mod lines in one openhuman/mod.rs |
| Direct Rust deps | 144 | top-level entries in Cargo.toml |
Result<_, String> signatures |
1,062 | non-test Rust |
anyhow::Result users |
392 files | predominant |
thiserror users |
8 files | the typed-error story is barely adopted |
.unwrap() total |
3,009 | mostly inside #[cfg(test)] blocks (good) |
.unwrap() in prod paths |
~120–150 | concentrated in webview_accounts (49) + a few benchmarks/schemas |
.expect( total |
730 | most are fixture/test code |
panic!( calls |
75 | none in critical hot paths sampled |
let _ = (discarded result) |
640 | many are fire-and-forget event emits — semi-legitimate |
TS as any |
21 | mostly in app/src/polyfills.ts for global injection |
TS @ts-ignore |
4 | all in test files |
TS console.* in prod |
195 | predominantly console.warn/error — acceptable |
| TODO/FIXME/HACK | 20 | low density, mostly with linked issue numbers |
Largest Rust production files (top 10)
4450 app/src-tauri/src/webview_accounts/mod.rs
3846 app/src-tauri/src/lib.rs
2702 src/core/observability.rs
2345 src/openhuman/memory/tree/read_rpc.rs
2181 src/openhuman/agent/harness/session/turn.rs
2093 app/src-tauri/src/whatsapp_scanner/mod.rs
2013 src/openhuman/config/schema/load.rs
2005 src/openhuman/inference/provider/compatible.rs
1889 src/openhuman/composio/ops.rs
1792 src/openhuman/channels/providers/web.rs
Largest TS/TSX production files (top 10)
2696 app/src/components/settings/panels/AIPanel.tsx
2261 app/src/lib/i18n/en.ts (translation strings — legitimate)
2125 app/src/pages/Conversations.tsx
2111 app/src/lib/i18n/ko.ts (translation strings — legitimate)
1489 app/src/services/webviewAccountService.ts
997 app/src/pages/Skills.tsx
923 app/src/providers/ChatRuntimeProvider.tsx
920 app/src/components/composio/ComposioConnectModal.tsx
906 app/src/features/human/Mascot/yellow/MascotCharacter.tsx
857 app/src/components/settings/panels/VoicePanel.tsx
Top 10 Specific Issues (file:line citations)
1. webview_accounts/mod.rs is a 4,450-line god module
app/src-tauri/src/webview_accounts/mod.rs:1-3239 (prod section) + tests below.
The module docstring at lines 1-19 honestly admits the scope: "Hosts third-party web apps … recipe injection … per-account session isolation … notification bypass." That's at least five bounded contexts in one file:
- Provider URL registry (
provider_urlL48) - CDP browser session management
- Notification forwarding (
forward_native_notificationL1112, 329 lines) - Webview lifecycle commands (
webview_account_openL13033, 840 lines) - DND / mute / focus preference state
The single function webview_account_open at 840 lines is the centerpiece — it spawns child webviews, injects scripts, registers CDP sessions, wires notification handlers, and stores state in three different Mutex<HashMap<...>> fields. This cannot be unit-tested.
Fix: Split into mod.rs (re-exports + state struct), commands.rs (Tauri #[command] entries), lifecycle.rs (open/close/prewarm), notifications.rs (forward + bypass prefs), recipes.rs (script injection). Keep the state struct as the only cross-cutting type.
2. std::sync::Mutex + .lock().unwrap() panic-amplifier in Tauri host
app/src-tauri/src/webview_accounts/mod.rs:23 declares use std::sync::Mutex;. The production section (lines 1-3239, before #[cfg(test)]) contains 17 calls to .lock().unwrap():
1119: state.notification_bypass.lock().unwrap().clone();
1636: app_state.inner.lock().unwrap().get(account_id).cloned();
1845: state.inner.lock().unwrap();
2655: state.inner.lock().unwrap().remove(&args.account_id);
2669: state.browser_ids.lock().unwrap().remove(&args.account_id);
2677: state.cdp_sessions.lock().unwrap().remove(&args.account_id);
2745, 2755, 2763, 2896, 2937, 2993, 3010,
3040, 3054, 3073, 3083 (notification bypass prefs)
Pattern recurs in app/src-tauri/src/lib.rs (13 calls), screen_capture/mod.rs (6), meet_call/mod.rs (6).
Risk: If any closure invoked while holding state.inner panics — e.g., a logging macro chokes on a malformed UTF-8 payload from webview_recipe_event (which is fed by untrusted DOM content from third-party sites like Slack/Discord/WhatsApp) — the mutex is poisoned. Every subsequent IPC command that tries to lock it will panic at the .unwrap(). Recovery requires restarting the app process.
Fix: Replace use std::sync::Mutex; with use parking_lot::Mutex; (already in Cargo.toml). parking_lot::Mutex::lock() returns the guard directly — no unwrap() needed, no poisoning. This is a mechanical change with a high blast-radius improvement.
3. Agent::turn is a 967-line method
src/openhuman/agent/harness/session/turn.rs:71 — pub async fn turn(&mut self, user_message: &str) -> Result<String> runs to ~line 1037.
Same file:
- L1038:
inject_agent_experience_context— 345 lines - L1383:
emit_progress— 265 lines - L1838:
build_system_prompt— 308 lines
The docstring at L51-68 lists six numbered responsibilities (initialization, prompt construction, context injection, execution loop, synthesis, background tasks). That's a Single Responsibility Principle violation enumerated in the comment.
Fix: Extract each numbered step into a private method on Agent (e.g., fn maybe_resume_transcript, fn maybe_bake_system_prompt, async fn execution_loop). Each step is independently testable — and a 9-step state machine surfaces naturally.
4. apply_env_overrides_from is 720 lines in config/schema/load.rs:5732
src/openhuman/config/schema/load.rs:5732 — fn apply_env_overrides_from(&mut self, env: &(dyn EnvLookup + Send + Sync)) is 720 lines long. With 144 direct Cargo deps and a sprawling settings surface, the codebase has a lot of env vars to handle — but doing them all in one method means no test partitioning, no documentation per group, and merge conflicts whenever two PRs add env vars.
Fix: Group env vars by domain (e.g., apply_inference_env, apply_observability_env, apply_voice_env) and have the top-level method delegate. Each sub-method can have a corresponding #[test] that pokes specific keys.
5. pages/Conversations.tsx is a 1,919-line single component with 29 hooks
app/src/pages/Conversations.tsx:206 defines const Conversations = ({...}) => { and the body extends to L2124. Inside that single function:
- 29 hook calls (
useState,useEffect,useMemo,useCallback,useRef) — counted via grep within the component body - L2125:
export const AgentChatPanel = () => <Conversations variant="page" />;exists only to expose a variant alias
When this component has 29 stateful concerns mixed with chat send/receive, MIC composer, autocomplete polling (AUTOCOMPLETE_POLL_DEBOUNCE_MS L100), thread management, agent profile editing, prompt-injection guard, voice STT, and Redux dispatch — every renderer rerun touches all of them. Memoization at the top level is masked.
Fix: Pull subsystems into useConversationsChatState, useConversationsAutocomplete, useConversationsVoice hooks under app/src/pages/conversations/hooks/. The shell component becomes a layout coordinator.
6. AIPanel.tsx packs 4 distinct components into 2,696 lines
app/src/components/settings/panels/AIPanel.tsx:
- L2650:
ProviderKeyDialog— 127 lines - L2948:
BackgroundLoopControls— 690 lines - L3727:
CustomRoutingDialog— 326 lines - L4109:
AIPanel— 521 lines
Plus two large hooks (useAISettings L272, useOllamaStatus L387, useInstalledModels L436) that should be in hooks/ rather than embedded in the panel.
Fix: Split into a directory:
panels/ai/
AIPanel.tsx
ProviderKeyDialog.tsx
BackgroundLoopControls.tsx
CustomRoutingDialog.tsx
hooks/useAISettings.ts
hooks/useOllamaStatus.ts
hooks/useInstalledModels.ts
7. Stringly-typed errors infect the entire core
1,062 functions return Result<..., String> in non-test Rust code under src/. The cost is concretely visible:
src/core/observability.rs:137-516 — there are 8 different "is_xxx_message" classifier functions:
expected_error_kindL137is_session_expired_messageL227is_loopback_unavailableL271is_network_unreachable_messageL299is_transient_upstream_http_messageL347is_backend_user_error_messageL384is_provider_user_state_messageL420is_local_ai_capability_unavailable_messageL516
Every one of them does substring matching on error strings. This is the dual of the upstream choice to throw away typed errors. The same substring-matching pattern appears in is_transient_provider_http_failure (L812) and is_max_iterations_event (L845) but reading Sentry events.
Fix: Introduce thiserror enums at the integration boundaries (provider errors, channel errors, memory errors, tool errors). The classifier file then matches on enum variants, not strings, and the matchers can never silently drift when a vendor changes a message.
8. Module-wide #![allow(dead_code)] defeats the compiler
src/openhuman/mod.rs:16 — #![allow(dead_code)] at module level disables dead-code warnings for the entire 75-module subtree (74 pub mod lines in the same file). The justification comment ("Many types/functions are intended for future use or integration with the frontend") is the canonical anti-pattern: the compiler is the only tool that reliably detects truly-unreachable code, and you have turned it off everywhere.
37 separate #[allow(dead_code)] and 110 #[allow(...)] attributes underneath compound the issue — they're locally justified, but the umbrella #![allow(dead_code)] makes them redundant and prevents anyone from cleaning them up without a flag day.
Fix: Remove the module-level allow. Let the compiler emit warnings. Either delete what is unused, mark individual items #[allow(dead_code)] with a comment, or keep public exports referenced by a doctest. Yes, this is a flag-day cleanup, but the long-term cost of not having dead-code signal is steeper.
9. Commented-out code as feature flags ([#1123] markers)
app/src/App.tsx lines 23, 48, 50, 125, 161, 194, 215 — seven commented-out blocks tagged [#1123]. Same pattern in app/src/pages/Conversations.tsx L14, L21, L23, and app/src/pages/Accounts.tsx L8, L10, L111+.
The comment-tag is well-meant (each one references the issue removing welcome-agent onboarding), but git history already records that. Long-lived comments rot faster than code because nobody runs cargo check against them.
Fix: Delete the commented blocks. If a rollback is needed, git revert is one command.
10. Hardcoded loopback URLs/ports in production defaults
src/openhuman/tools/impl/browser/types.rs:33—endpoint: "http://127.0.0.1:8787/v1/actions".into(). A hardcoded loopback URL with a magic port as a struct default. If two browser tools collide on 8787, the second one silently 502s.src/openhuman/webview_apis/client.rs:142—let url = format!("ws://127.0.0.1:{port}/");whereportcomes fromOPENHUMAN_WEBVIEW_APIS_PORT(webview_apis/mod.rs:10). No bounds check on the env var; an empty string will panic the URL parse downstream.
Fix: Lift these into a BrowserToolConfig / WebviewApisConfig populated by the config loader (the same loader that already handles env overrides, see issue #4). Validate at parse time.
Strengths Observed (what they're doing right)
- Test scaffolding is generous. ~86k LOC of Rust test code against ~360k LOC of prod is healthy. Files like
src/openhuman/security/policy_tests.rs(1,497 LOC) andsrc/openhuman/composio/ops_test.rs(1,534 LOC) show real coverage discipline. - Module-level docstrings are real. E.g.,
app/src-tauri/src/webview_accounts/mod.rs:1-19explains the architecture concisely with an ASCII flow diagram.src/openhuman/agent/harness/session/turn.rs:1-18lists the public surface up-front. Many crates would kill for this. - TODOs reference issue numbers. Of the 20 TODO/FIXME markers in the codebase, most have linked issue IDs:
TODO(#1339),TODO(phase-5),TODO(composio-retry-dedup). This is unusually disciplined. - Domain segmentation is broadly correct. 75 modules under
src/openhuman/cleanly map to capability areas (memory, channels, composio, inference, voice, security). The boundaries are real even if individual modules have grown too large. - Error-classification is centralized, not scattered. Even though
observability.rsis 2,702 lines, it is the one place where error-string heuristics live, rather than being duplicated across providers. - TypeScript discipline is high. Only 21
as any(most inpolyfills.tsfor legitimate global injection), 4@ts-ignore(all in tests), and no@ts-nocheck. Compare to typical Electron+React apps where these run into hundreds. - Notification routing is feature-flagged correctly.
webview_accounts/mod.rs:1146-1158— theforward_native_notificationfunction gates onNotificationSettingsState::enabled()and exits early when off. Defensive coding done right. - Test/prod split via
#[cfg(test)]inline modules is consistent. The 3,009.unwrap()calls compress to ~120-150 in actual production paths once#[cfg(test)]blocks are excluded — the test scaffold is doing its job. - No
unimplemented!()ortodo!()macros in prod. Zero occurrences acrosssrc/andapp/src-tauri/src/. This means no half-finished code paths can panic the agent loop. - Inference provider module is decomposed. Even though
compatible.rsis 2,005 lines, the surrounding directory has nine sibling files (compatible_parse.rs,compatible_stream.rs,compatible_types.rs,reliable.rs,router.rs,factory.rs, etc.) — the directory was clearly designed with separation in mind even ifcompatible.rsitself never got split.
Risk Score: 6 / 10
The codebase is in the upper half of its peer group. Test density is good, naming is sound, TS strictness is enforced, and most modules show genuine architectural intent. The risk drivers are concentrated rather than diffuse: a small handful of god-files (webview_accounts/mod.rs, agent/harness/session/turn.rs, pages/Conversations.tsx, panels/AIPanel.tsx, config/schema/load.rs) plus one systemic choice (Result<_, String> everywhere, dead-code warnings disabled module-wide). These are addressable with mechanical refactors — no architectural rewrite needed. The single most acute operational risk is the std::sync::Mutex + .lock().unwrap() pattern in the Tauri host: a one-line panic in a third-party-DOM-driven callback poisons a lock and bricks the IPC layer until the user restarts the app. That alone justifies a sprint of focused cleanup before the next release.