diff --git a/.github/workflows/ci-lite.yml b/.github/workflows/ci-lite.yml index 97ef5bbec3..a5162b892c 100644 --- a/.github/workflows/ci-lite.yml +++ b/.github/workflows/ci-lite.yml @@ -380,11 +380,16 @@ jobs: needs: [changes] if: needs.changes.outputs['rust-core'] == 'true' || needs.changes.outputs['rust-tauri'] == 'true' runs-on: ubuntu-22.04 - timeout-minutes: 25 + # Was 25; raised because the lane now links (--all-targets) and runs a test + # binary, not just `cargo check`. + timeout-minutes: 40 container: image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0 env: CARGO_INCREMENTAL: "0" + # Deep-async-stack belt (same as the coverage lanes) — virtual reservation, + # zero cost. Keeps a gate-contract test with a deep stack from tripping. + RUST_MIN_STACK: "67108864" steps: - name: Checkout code uses: actions/checkout@v7 @@ -402,8 +407,68 @@ jobs: shared-key: pr-rust-feature-gate-smoke - name: Check core builds with the default domain gates disabled + # Lib-scoped on purpose. `--all-targets` would additionally compile the + # `tests/` integration crates, which today name gated symbols + # (voice::reply_speech::test_seam, mcp_registry::store, x402::tools) without + # a #[cfg] and so do not compile with the gates off. That cleanup is tracked + # in #5021 alongside running the full gates-off suite; until it lands, this + # lane checks the lib and RUNS the gate-contract lib tests below. run: bash scripts/ci-cancel-aware.sh cargo check --manifest-path Cargo.toml --no-default-features --features tokenjuice-treesitter + - name: Run the gate-contract tests with the default domain gates disabled + # `cargo check` (even --all-targets) never RUNS tests, so a gated family that + # is asserted WITHOUT a matching #[cfg] compiles fine and only fails at + # runtime — exactly how the two ungated `voice` asserts sat red on main + # (#5022). This step executes the gate-contract test modules so that class of + # regression fails CI, not just a local pre-push. + # + # Scope is deliberate: the full gates-off `--lib` run aborts on a pre-existing + # task_local stack overflow (`agent::harness::session::tests:: + # turn_dispatches_spawn_subagent_through_full_path`) that reproduces in the + # gates-ON build too, and also OOMs unscoped. These filters route AROUND that + # test by scope, not by skip. Running the full suite is tracked in #5021. + run: | + bash scripts/ci-cancel-aware.sh cargo test --manifest-path Cargo.toml \ + --no-default-features --features tokenjuice-treesitter --lib -- \ + core::all:: core::cli:: core::jsonrpc:: core::legacy_aliases:: agent_registry::agents::loader:: + + - name: Guard — new feature-gated test modules must be acknowledged + # Self-maintaining coverage: the set of source files that #[cfg]-gate a test on + # a domain feature must equal the allowlist below. When a new gate (or a new + # test in an existing gate's domain) adds a gated test file, this fails — the + # author then adds it here AND, if it can hold an ungated-assert regression of + # the #5022 class, extends the scoped `cargo test` filter above. Prevents the + # smoke lane from silently under-covering as the gate surface grows. + run: | + set -euo pipefail + EXPECTED=$(cat <<'EOF' + core/all_tests.rs + core/cli_tests.rs + core/jsonrpc_tests.rs + core/legacy_aliases.rs + openhuman/agent/harness/builtin_definitions.rs + openhuman/agent/harness/definition_tests.rs + openhuman/agent/harness/session/tests.rs + openhuman/agent/harness/subagent_runner/tool_prep.rs + openhuman/agent_registry/agents/loader.rs + openhuman/inference/local/mod.rs + openhuman/tool_registry/ops_tests.rs + openhuman/tool_registry/schemas.rs + openhuman/tools/ops_tests.rs + openhuman/voice/compile_status.rs + openhuman/wallet/stub.rs + openhuman/web3/stub.rs + openhuman/x402/stub.rs + EOF + ) + ACTUAL=$(grep -rlE '#\[cfg\((not\()?feature = "(voice|media|web3|meet|mcp|skills|flows)"' src --include='*.rs' \ + | xargs grep -lE '#\[test\]|#\[tokio::test\]|fn .*_test' 2>/dev/null | sed 's|^src/||' | sort -u) + if ! diff <(echo "$EXPECTED" | sed 's/^ *//' | sort -u) <(echo "$ACTUAL"); then + echo "::error::Gated-test file set changed. Update the EXPECTED allowlist in the rust-feature-gate-smoke lane, and extend the scoped 'cargo test' filter if the new module can carry an ungated-assert regression (see #5022)." + exit 1 + fi + echo "gate-contract test coverage allowlist is current" + rust-core-coverage: name: Rust Core Coverage (cargo-llvm-cov) needs: [changes, rust-quality] diff --git a/app/src-tauri/src/claude_code.rs b/app/src-tauri/src/claude_code.rs index c70f960cc0..7ff7a98ed3 100644 --- a/app/src-tauri/src/claude_code.rs +++ b/app/src-tauri/src/claude_code.rs @@ -43,7 +43,7 @@ end tell"#; .args(["-e", script]) .spawn() .map_err(|e| format!("failed to open Terminal.app: {e}"))?; - return Ok("Terminal.app".into()); + Ok("Terminal.app".into()) } #[cfg(target_os = "linux")] diff --git a/app/src-tauri/src/imessage_scanner/mod.rs b/app/src-tauri/src/imessage_scanner/mod.rs index 289fd7cac0..e8cefd9929 100644 --- a/app/src-tauri/src/imessage_scanner/mod.rs +++ b/app/src-tauri/src/imessage_scanner/mod.rs @@ -372,7 +372,7 @@ fn extract_text_from_attributed_body(blob: &[u8]) -> Option { .filter_map(|r| String::from_utf8(r).ok()) .filter(|s| { let trimmed = s.trim(); - trimmed.len() >= 2 && !ignored_markers.iter().any(|m| trimmed == *m) + trimmed.len() >= 2 && !ignored_markers.contains(&trimmed) }) .max_by_key(|s| s.len()) .map(|s| s.trim().to_string()) diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index ee15b7897e..b597458325 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -1195,7 +1195,7 @@ fn mascot_window_show(app: AppHandle) -> Result<(), String> { log::info!("[mascot-window] show requested"); #[cfg(target_os = "macos")] { - return mascot_native_window::show(&app); + mascot_native_window::show(&app) } #[cfg(not(target_os = "macos"))] { @@ -1258,11 +1258,11 @@ fn notch_window_show(app: AppHandle) -> Result<(), String> { log::info!("[notch-window] show requested"); #[cfg(target_os = "macos")] { - return dispatch_notch_on_main(app, |app| { + dispatch_notch_on_main(app, |app| { if let Err(e) = notch_window::show(app) { log::warn!("[notch-window] show failed: {e}"); } - }); + }) } #[cfg(not(target_os = "macos"))] { @@ -1277,7 +1277,7 @@ fn notch_window_hide(app: AppHandle) -> Result<(), String> { log::info!("[notch-window] hide requested"); #[cfg(target_os = "macos")] { - return dispatch_notch_on_main(app, |_app| notch_window::hide()); + dispatch_notch_on_main(app, |_app| notch_window::hide()) } #[cfg(not(target_os = "macos"))] { diff --git a/app/src-tauri/src/mascot_native_window.rs b/app/src-tauri/src/mascot_native_window.rs index 063361a356..5c27a36ea2 100644 --- a/app/src-tauri/src/mascot_native_window.rs +++ b/app/src-tauri/src/mascot_native_window.rs @@ -52,6 +52,9 @@ const DRAG_POLL_SECONDS: f64 = 0.016; /// dropped webview. struct MascotPanel { panel: Retained, + // RAII keep-alive: never read, but dropping it deallocates the WKWebView and + // blanks the panel. Must outlive the show/hide cycle — do not remove. + #[allow(dead_code)] webview: Retained, drag_timer: Retained, } @@ -389,7 +392,7 @@ unsafe fn build_webview( let _: () = msg_send![&*webview, setAutoresizingMask: 18u64]; // width|height // Make the webview the panel's content view so it fills the frame. - let webview_ref: &objc2::runtime::AnyObject = &*webview; + let webview_ref: &objc2::runtime::AnyObject = &webview; let webview_view: *mut objc2::runtime::AnyObject = webview_ref as *const _ as *mut objc2::runtime::AnyObject; let _: () = msg_send![panel, setContentView: webview_view]; diff --git a/app/src-tauri/src/native_notifications/mod.rs b/app/src-tauri/src/native_notifications/mod.rs index 7fb5281e7d..18e23669a7 100644 --- a/app/src-tauri/src/native_notifications/mod.rs +++ b/app/src-tauri/src/native_notifications/mod.rs @@ -37,7 +37,7 @@ use crate::AppRuntime; pub fn notification_permission_state() -> Result { #[cfg(target_os = "macos")] { - return macos::permission_state(); + macos::permission_state() } #[cfg(not(target_os = "macos"))] { @@ -51,7 +51,7 @@ pub fn notification_permission_state() -> Result { pub fn notification_permission_request() -> Result { #[cfg(target_os = "macos")] { - return macos::request_permission(); + macos::request_permission() } #[cfg(not(target_os = "macos"))] { diff --git a/app/src-tauri/src/notch_window.rs b/app/src-tauri/src/notch_window.rs index 78653688cb..4fc3f7202a 100644 --- a/app/src-tauri/src/notch_window.rs +++ b/app/src-tauri/src/notch_window.rs @@ -61,10 +61,6 @@ thread_local! { static NOTCH: RefCell> = const { RefCell::new(None) }; } -pub(crate) fn is_open() -> bool { - NOTCH.with(|cell| cell.borrow().is_some()) -} - pub(crate) fn hide() { NOTCH.with(|cell| { if let Some(existing) = cell.borrow_mut().take() { @@ -262,7 +258,7 @@ unsafe fn build_webview( // Auto-resize to fill the panel content view. let _: () = msg_send![&*webview, setAutoresizingMask: 18u64]; // width|height - let webview_ref: &objc2::runtime::AnyObject = &*webview; + let webview_ref: &objc2::runtime::AnyObject = &webview; let webview_view = webview_ref as *const _ as *mut objc2::runtime::AnyObject; let _: () = msg_send![panel, setContentView: webview_view]; diff --git a/app/src/services/chatService.ts b/app/src/services/chatService.ts index 6fb128bb5c..f6b25e34d6 100644 --- a/app/src/services/chatService.ts +++ b/app/src/services/chatService.ts @@ -966,7 +966,7 @@ export function subscribeChatEvents(listeners: ChatEventListeners): () => void { } // Artifact lifecycle events (#2779). The Rust subscriber in - // `channels/providers/web::ArtifactSurfaceSubscriber` packs the + // `web_chat::ArtifactSurfaceSubscriber` packs the // artifact payload into the generic `args` field of the wire // envelope (kept the WebChannelEvent struct shape stable to avoid // touching ~10 existing call sites with `..Default::default()`). diff --git a/docs/TEST-COVERAGE-MATRIX.md b/docs/TEST-COVERAGE-MATRIX.md index 171ad792a9..51b9a18b1a 100644 --- a/docs/TEST-COVERAGE-MATRIX.md +++ b/docs/TEST-COVERAGE-MATRIX.md @@ -182,7 +182,7 @@ Canonical mapping of every product feature to its test source(s). Drives gap-fil | 4.2.1 | User Message Handling | WD+RI | `conversations-web-channel-flow.spec.ts`, `tests/json_rpc_e2e.rs` | ✅ | | | 4.2.2 | AI Response Generation | WD | `agent-review.spec.ts` | ✅ | Mock LLM | | 4.2.3 | Streaming Responses | RI | `tests/json_rpc_e2e.rs`, `tests/agent_harness_e2e.rs` | ✅ | `tests/agent_harness_e2e.rs` adds provider-level SSE tool-arg accumulation (chunked args reassembled + parsed) and engine-level delta forwarding (#3471) | -| 4.2.4 | Parallel inference (cross-thread + within-thread forked turns) | RU+VU | `src/openhuman/channels/providers/web_tests.rs`, `app/src/store/__tests__/chatRuntimeSlice.test.ts`, `app/src/providers/__tests__/ChatRuntimeProvider.test.tsx` | 🟡 | Concurrent same-/cross-thread dispatch, cooperative `CancellationToken` teardown, and parallel-lane stream routing covered; dedicated WD E2E is a follow-up | +| 4.2.4 | Parallel inference (cross-thread + within-thread forked turns) | RU+VU | `src/openhuman/web_chat/web_tests.rs`, `app/src/store/__tests__/chatRuntimeSlice.test.ts`, `app/src/providers/__tests__/ChatRuntimeProvider.test.tsx` | 🟡 | Concurrent same-/cross-thread dispatch, cooperative `CancellationToken` teardown, and parallel-lane stream routing covered; dedicated WD E2E is a follow-up | | 4.2.5 | Per-thread todo list (plan strip above composer) | RU+VU+WD | `src/openhuman/agent/tools/todo.rs`, `app/src/pages/conversations/components/ThreadTodoStrip.test.tsx`, `app/test/e2e/specs/chat-thread-todo-strip.spec.ts` | ✅ | Read-only thread-scoped todo strip fed by `task_board_updated`; agent `todo` tool guidance + thread binding; E2E drives a `todo` tool call and asserts the card renders | | 4.2.6 | Background-activity panel (chat-header Background tasks button) | VU+WD | `app/src/pages/conversations/hooks/useBackgroundActivity.test.ts`, `app/src/pages/conversations/components/__tests__/BackgroundActivityRows.test.tsx`, `app/test/e2e/specs/chat-background-activity-panel.spec.ts` | ✅ | View-only panel surfacing this chat's async sub-agents + global cron jobs, subconscious/heartbeat status, and memory syncing; freshness-only "Syncing now" labeling; E2E opens the panel and asserts its sections render and close | | 4.2.7 | Plan-mode review (Approve / Reject / Send-feedback before execute) | RU+RI+VU | `src/openhuman/plan_review/gate.rs`, `src/openhuman/plan_review/tool.rs`, `src/openhuman/plan_review/schemas.rs`, `tests/json_rpc_e2e.rs`, `app/src/pages/conversations/components/PlanReviewCard.test.tsx`, `app/src/pages/__tests__/Conversations.render.test.tsx` | ✅ | Interactive turns call `request_plan_review`, which parks the LIVE turn on the in-memory `PlanReviewGate` (oneshot) until the user decides; `plan_review_request` socket event drives `PlanReviewCard`, which resolves via `openhuman.plan_review_decide` (approve resumes-and-executes / reject resumes-and-stops / revise resumes-with-feedback). RU covers gate park/resolve/timeout + tool auto-approve + parking; RI covers the decide RPC; VU covers the card + provider wiring. WD E2E (agent-driven park flow) tracked as follow-up | diff --git a/src/core/all.rs b/src/core/all.rs index d686517cca..8ead22c1df 100644 --- a/src/core/all.rs +++ b/src/core/all.rs @@ -388,7 +388,7 @@ fn build_registered_controllers() -> Vec { push( &mut controllers, DomainGroup::Channels, - crate::openhuman::channels::providers::web::all_web_channel_registered_controllers(), + crate::openhuman::web_chat::all_web_channel_registered_controllers(), ); push( &mut controllers, diff --git a/src/core/all_tests.rs b/src/core/all_tests.rs index 10be165c8d..a562c711e5 100644 --- a/src/core/all_tests.rs +++ b/src/core/all_tests.rs @@ -823,6 +823,9 @@ async fn harness_excludes_gated_namespaces() { .collect(); #[cfg(feature = "flows")] assert!(full_ns.contains("flows"), "full() must expose flows"); + // `voice` was the pathfinder gate (#4803) and predates this per-assert cfg + // convention; gate it like its siblings so the disabled build passes (#5022). + #[cfg(feature = "voice")] assert!(full_ns.contains("voice"), "full() must expose voice"); let ctx = CoreContext::for_test(DomainSet::harness(), None); @@ -958,6 +961,9 @@ fn group_mapping_smoke() { // has no controller to map. Skip when its Cargo feature is off. #[cfg(feature = "skills")] assert_eq!(group_for_namespace("skills"), Some(DomainGroup::Skills)); + // `voice` predates the per-assert cfg convention (#4803); registry-derived, so + // it has no entry to map when the feature is off. Gate like its siblings (#5022). + #[cfg(feature = "voice")] assert_eq!(group_for_namespace("voice"), Some(DomainGroup::Voice)); #[cfg(feature = "web3")] assert_eq!(group_for_namespace("wallet"), Some(DomainGroup::Web3)); diff --git a/src/core/event_bus/events.rs b/src/core/event_bus/events.rs index 2d2ee80fe7..bc9a7af811 100644 --- a/src/core/event_bus/events.rs +++ b/src/core/event_bus/events.rs @@ -579,7 +579,7 @@ pub enum DomainEvent { /// flow-approval-surface, PR2/PR3). Unlike `ApprovalRequested`, this /// event carries no `thread_id`/`client_id` — a flow run has neither, so /// the generic chat-routed socket bridge - /// (`channels::providers::web::event_bus::ApprovalSurfaceSubscriber`) + /// (`web_chat::event_bus::ApprovalSurfaceSubscriber`) /// silently drops it (that gap was the original silent-deadlock bug). /// Published by `ApprovalGate::intercept_audited` alongside the existing /// `ApprovalRequested`, bridged by `core::socketio` directly to a @@ -611,7 +611,7 @@ pub enum DomainEvent { /// /// Bridged to the `external_transfer_pending` web-channel socket event by /// `EgressSurfaceSubscriber` (defined in - /// `src/openhuman/channels/providers/web/event_bus.rs`) when the emitting + /// `src/openhuman/web_chat/event_bus.rs`) when the emitting /// turn carries chat routing. `thread_id` / `client_id` come from the /// ambient `APPROVAL_CHAT_CONTEXT` and are `None` for CLI / cron / /// background transfers (no chat surface to route to). diff --git a/src/core/jsonrpc.rs b/src/core/jsonrpc.rs index 8bcabec2f3..2b0b10ef29 100644 --- a/src/core/jsonrpc.rs +++ b/src/core/jsonrpc.rs @@ -1479,7 +1479,7 @@ async fn events_handler( } let client_id = query.client_id; - let rx = crate::openhuman::channels::providers::web::subscribe_web_channel_events(); + let rx = crate::openhuman::web_chat::subscribe_web_channel_events(); let stream = tokio_stream::wrappers::BroadcastStream::new(rx).filter_map( move |item| -> Option> { let event = match item { @@ -1990,6 +1990,15 @@ fn register_domain_subscribers( // peer-status events, and register acks. Must be live before any // tunnel:frame events can arrive. crate::openhuman::devices::bus::register_device_tunnel_subscriber(); + // Always-on learning subscribers (email-signature producer, rebuild + // trigger + periodic loop, ProfileMdRenderer). Previously wired only + // in `channels::runtime::startup::start_channels`, which is skipped + // when no channel is configured — silently dropping ALL learning for + // channel-less users (#5003). Registered here on the unconditional + // Platform boot path; idempotent, so it never double-registers. + crate::openhuman::learning::startup::register_learning_subscribers( + workspace_dir.clone(), + ); } } else { log::debug!( @@ -2350,18 +2359,18 @@ pub async fn bootstrap_core_runtime( // `start_channels` is skipped for web-chat-only cores. Without this an // unguarded standalone/CLI/Docker core would park a plan review that never // reaches the UI and dies at the gate TTL. Idempotent (Once-guarded). - crate::openhuman::channels::providers::web::register_approval_surface_subscriber(); + crate::openhuman::web_chat::register_approval_surface_subscriber(); // Bridge emergency-stop halt/resume → the `automation_halt` broadcast on the // same always-run boot path. `start_channels` (which also registers this) // is skipped on a web-chat-only desktop with no listening integrations, so // without this a halt/resume initiated from the CLI or another client would // never reach the UI. Idempotent (Once-guarded). (#4255) - crate::openhuman::channels::providers::web::register_automation_halt_subscriber(); + crate::openhuman::web_chat::register_automation_halt_subscriber(); // Egress-surface bridge (privacy epic S2, #4436) — registered // unconditionally alongside the approval surface so external-transfer // disclosures reach the UI even on cores that skip `start_channels` or run // with the approval gate disabled. Idempotent (OnceLock-guarded). - crate::openhuman::channels::providers::web::register_egress_surface_subscriber(); + crate::openhuman::web_chat::register_egress_surface_subscriber(); if decision.install_gate { // Per-launch correlation token for the approval gate. This is @@ -2382,7 +2391,7 @@ pub async fn bootstrap_core_runtime( ); // (The approval/plan-review surface bridge is registered unconditionally // above — it must run even when this gate-install branch is skipped.) - crate::openhuman::channels::providers::web::register_artifact_surface_subscriber(); + crate::openhuman::web_chat::register_artifact_surface_subscriber(); } else { log::error!( "[runtime] approval gate DISABLED (OPENHUMAN_APPROVAL_GATE=0 honored on host={}) — \ @@ -2402,7 +2411,7 @@ pub async fn bootstrap_core_runtime( // `if approval_gate` block so artifact events still publish when the user // sets OPENHUMAN_APPROVAL_GATE=0 (CR #3328947323 on PR #3026). Idempotent // (OnceLock-guarded inside register_artifact_surface_subscriber). - crate::openhuman::channels::providers::web::register_artifact_surface_subscriber(); + crate::openhuman::web_chat::register_artifact_surface_subscriber(); // --- Workspace migrations -------------------------------------------- crate::openhuman::startup::run_workspace_migrations(&workspace_dir); diff --git a/src/core/observability.rs b/src/core/observability.rs index d81d5b529e..1ae514c17d 100644 --- a/src/core/observability.rs +++ b/src/core/observability.rs @@ -211,7 +211,7 @@ pub enum ExpectedErrorKind { /// `agent::run_single` already suppresses the **agent-layer** Sentry /// event for this condition via the typed /// `AgentError::EmptyProviderResponse` + `AgentError::skips_sentry()` - /// (PR #2790, TAURI-RUST-4JX). But `channels::providers::web:: + /// (PR #2790, TAURI-RUST-4JX). But `web_chat:: /// run_chat_task` **re-reports** the same failure under /// `domain=web_channel operation=run_chat_task` after the typed error /// has been flattened to a `String` at the native-bus boundary — so the @@ -302,7 +302,7 @@ pub enum ExpectedErrorKind { /// (`api_error`) already demotes its own per-attempt event; this catches /// the **re-report** when the same flattened error is raised again under /// `domain=web_channel` / `agent` (the path - /// `channels::providers::web::run_chat_task` → + /// `web_chat::run_chat_task` → /// `report_error_or_expected`). The FE surfaces actionable copy via /// `classify_inference_error`; Sentry must not double-report (F2/F4). /// @@ -975,7 +975,7 @@ fn is_memory_store_breaker_open(lower: &str) -> bool { /// - `"OpenHuman API error (401 Unauthorized): {…\"Session expired. Please /// log in again.\"…}"` — emitted by `providers::ops::api_error` from the /// OpenHuman backend and re-raised through `agent::run_single` / -/// `channels::providers::web::run_chat_task` (OPENHUMAN-TAURI-26). The +/// `web_chat::run_chat_task` (OPENHUMAN-TAURI-26). The /// `"session expired"` substring anchors the match to the OpenHuman /// backend's session-renewal body, not the bare numeric status. /// - `"OpenHuman API error (401 Unauthorized): {…\"error\":\"Invalid token\"…}"` @@ -1870,7 +1870,7 @@ fn is_filesystem_user_path_invalid_message(lower: &str) -> bool { /// `text_chars=0 thinking_chars=0 tool_calls=0`. /// /// This catches the **web-channel re-report** (Sentry TAURI-RUST-4Z1): -/// `channels::providers::web::run_chat_task` wraps the failure as +/// `web_chat::run_chat_task` wraps the failure as /// `"run_chat_task failed client_id=… error=The model returned an empty /// response. Please try again."` and routes it through /// `report_error_or_expected` after the typed @@ -2598,7 +2598,7 @@ fn all_provider_attempts_are_transient(message: &str) -> bool { /// Defense-in-depth filter for the Sentry `before_send` hook: the primary /// suppression lives at the call sites in `agent::harness::session:: /// runtime::run_single`, `channels::runtime::dispatch`, and -/// `channels::providers::web::run_chat_task`, all of which now skip +/// `web_chat::run_chat_task`, all of which now skip /// `report_error` when this variant is detected. This filter catches any /// future call site that re-emits the message without going through those /// funnels — e.g. a new wrapper that calls `tracing::error!` directly with diff --git a/src/core/socketio.rs b/src/core/socketio.rs index 84afc6fc84..9459c21ddf 100644 --- a/src/core/socketio.rs +++ b/src/core/socketio.rs @@ -505,7 +505,7 @@ pub fn attach_socketio() -> (socketioxide::layer::SocketIoLayer, SocketIo) { ); // Trigger the web channel's chat logic. - match crate::openhuman::channels::providers::web::start_chat( + match crate::openhuman::web_chat::start_chat( &client_id, &payload.thread_id, &payload.message, @@ -514,7 +514,7 @@ pub fn attach_socketio() -> (socketioxide::layer::SocketIoLayer, SocketIo) { payload.profile_id, payload.locale, payload.queue_mode, - crate::openhuman::channels::providers::web::ChatRequestMetadata::default(), + crate::openhuman::web_chat::ChatRequestMetadata::default(), ) .await { @@ -556,7 +556,7 @@ pub fn attach_socketio() -> (socketioxide::layer::SocketIoLayer, SocketIo) { client_id, payload.thread_id ); - let _ = crate::openhuman::channels::providers::web::cancel_chat_scoped( + let _ = crate::openhuman::web_chat::cancel_chat_scoped( &client_id, &payload.thread_id, payload.request_id.as_deref(), @@ -607,7 +607,7 @@ pub fn spawn_web_channel_bridge(io: SocketIo) { // 1. Web channel events → per-client rooms. let io_web = io.clone(); tokio::spawn(async move { - let mut rx = crate::openhuman::channels::providers::web::subscribe_web_channel_events(); + let mut rx = crate::openhuman::web_chat::subscribe_web_channel_events(); loop { let event = match rx.recv().await { Ok(event) => event, diff --git a/src/main.rs b/src/main.rs index 45324defe9..fbd3b41672 100644 --- a/src/main.rs +++ b/src/main.rs @@ -119,7 +119,7 @@ fn main() { // slipped past the call-site filters in // `agent::harness::session::runtime::run_single`, // `channels::runtime::dispatch`, and - // `channels::providers::web::run_chat_task`. The cap is a + // `web_chat::run_chat_task`. The cap is a // deterministic agent-state outcome surfaced to the user via // the chat-rendered "Error: …" message — Sentry is the wrong // surface for it (OPENHUMAN-TAURI-99 / -98). diff --git a/src/openhuman/agent/README.md b/src/openhuman/agent/README.md index eb36cba9ad..38bd21ae24 100644 --- a/src/openhuman/agent/README.md +++ b/src/openhuman/agent/README.md @@ -27,7 +27,7 @@ Multi-agent orchestration domain. Owns the LLM tool-calling loop, sub-agent disp ## Called by -- `src/openhuman/channels/runtime/dispatch.rs` and `channels/providers/web.rs` — drive chat turns from inbound channel messages. +- `src/openhuman/channels/runtime/dispatch.rs` and `web_chat/` — drive chat turns from inbound channel messages. - `src/openhuman/cron/scheduler.rs` — fire scheduled triggers through `triage::run_triage` + `apply_decision`. - `src/openhuman/webhooks/ops.rs` — webhook ingestion routes through triage. - `src/openhuman/composio/bus.rs` — Composio trigger envelopes go through `agent::triage`. diff --git a/src/openhuman/agent/progress_tracing/journal_projection.rs b/src/openhuman/agent/progress_tracing/journal_projection.rs index f15acb8812..5665df51c5 100644 --- a/src/openhuman/agent/progress_tracing/journal_projection.rs +++ b/src/openhuman/agent/progress_tracing/journal_projection.rs @@ -4,7 +4,7 @@ //! [`AgentObservation`] journal (the crate `AgentEvent` record) and folds it //! through the existing [`SpanCollector`], so trace spans no longer require the //! *live* in-run `AgentProgress` side-observer -//! (`channels/providers/web/progress_bridge.rs`). A UI/supervisor can attach +//! (`web_chat/progress_bridge.rs`). A UI/supervisor can attach //! after a run, read the journal, and rebuild identical spans. //! //! This is deliberately built on `SpanCollector` (not a re-derivation) so span diff --git a/src/openhuman/agent/task_dispatcher/executor.rs b/src/openhuman/agent/task_dispatcher/executor.rs index d54571bfca..466f5ab99d 100644 --- a/src/openhuman/agent/task_dispatcher/executor.rs +++ b/src/openhuman/agent/task_dispatcher/executor.rs @@ -195,13 +195,13 @@ pub(super) async fn run_autonomous( if let Some(thread_id) = session_thread_id.as_deref() { let (progress_tx, progress_rx) = tokio::sync::mpsc::channel(64); agent.set_on_progress(Some(progress_tx)); - crate::openhuman::channels::providers::web::spawn_progress_bridge( + crate::openhuman::web_chat::spawn_progress_bridge( progress_rx, "system".to_string(), thread_id.to_string(), run_id.to_string(), crate::openhuman::threads::turn_state::TurnStateStore::new(workspace_dir.clone()), - crate::openhuman::channels::providers::web::ChatRequestMetadata { + crate::openhuman::web_chat::ChatRequestMetadata { // Trace attribution: mark the run autonomous and carry the // resolved executor agent so Langfuse traces read // `agent.turn:` with channel.source=autonomous. @@ -251,7 +251,7 @@ pub(super) async fn run_autonomous( if let Some(thread_id) = session_thread_id.as_deref() { match &result { Ok(response) => { - crate::openhuman::channels::providers::web::presentation::deliver_response( + crate::openhuman::web_chat::presentation::deliver_response( "system", thread_id, run_id, @@ -265,7 +265,7 @@ pub(super) async fn run_autonomous( .await; } Err(err) => { - crate::openhuman::channels::providers::web::publish_web_channel_event( + crate::openhuman::web_chat::publish_web_channel_event( crate::core::socketio::WebChannelEvent { event: "chat_error".to_string(), client_id: "system".to_string(), diff --git a/src/openhuman/agent/task_dispatcher/registry.rs b/src/openhuman/agent/task_dispatcher/registry.rs index 375f369397..2d88c34fc6 100644 --- a/src/openhuman/agent/task_dispatcher/registry.rs +++ b/src/openhuman/agent/task_dispatcher/registry.rs @@ -106,17 +106,15 @@ fn cancel_taken_run(thread_id: &str, run: ActiveRun) { &run.run_id, Err("Cancelled by user".to_string()), ); - crate::openhuman::channels::providers::web::publish_web_channel_event( - crate::core::socketio::WebChannelEvent { - event: "chat_error".to_string(), - client_id: "system".to_string(), - thread_id: thread_id.to_string(), - request_id: run.run_id.clone(), - message: Some("Cancelled".to_string()), - error_type: Some("cancelled".to_string()), - ..Default::default() - }, - ); + crate::openhuman::web_chat::publish_web_channel_event(crate::core::socketio::WebChannelEvent { + event: "chat_error".to_string(), + client_id: "system".to_string(), + thread_id: thread_id.to_string(), + request_id: run.run_id.clone(), + message: Some("Cancelled".to_string()), + error_type: Some("cancelled".to_string()), + ..Default::default() + }); tracing::info!( thread_id = %thread_id, card_id = %run.card_id, diff --git a/src/openhuman/agent/triage/evaluator.rs b/src/openhuman/agent/triage/evaluator.rs index 351e95e89e..e7a6fbcba2 100644 --- a/src/openhuman/agent/triage/evaluator.rs +++ b/src/openhuman/agent/triage/evaluator.rs @@ -647,7 +647,7 @@ fn is_prompt_guard_rejection(message: &str) -> bool { /// /// The vocabulary matches the OpenHuman backend's error copy and common /// third-party provider phrasing. It does **not** mirror the -/// *semantics* of `channels/providers/web.rs` (a different code path); +/// *semantics* of `web_chat/` (a different code path); /// it is an independent, conservative allowlist evaluated inline so the /// triage evaluator carries no cross-domain import. /// diff --git a/src/openhuman/agent/turn_origin.rs b/src/openhuman/agent/turn_origin.rs index 49955a63ed..ef712b5837 100644 --- a/src/openhuman/agent/turn_origin.rs +++ b/src/openhuman/agent/turn_origin.rs @@ -4,7 +4,7 @@ //! consistent decisions across web, channel, subconscious, and cron entry //! points without relying on the *absence* of other task-locals as a signal. //! -//! Every entry point that drives the agent loop ([`crate::openhuman::channels::providers::web`], +//! Every entry point that drives the agent loop ([`crate::openhuman::web_chat`], //! [`crate::openhuman::channels::runtime::dispatch`], [`crate::openhuman::subconscious`], //! [`crate::openhuman::cron`], CLI) MUST scope a real [`AgentTurnOrigin`] //! around its `run_turn` invocation. Any path that fails to do so is treated diff --git a/src/openhuman/agent_orchestration/command_center/mod.rs b/src/openhuman/agent_orchestration/command_center/mod.rs index 13b937da23..d1e6aa0447 100644 --- a/src/openhuman/agent_orchestration/command_center/mod.rs +++ b/src/openhuman/agent_orchestration/command_center/mod.rs @@ -5,7 +5,7 @@ //! a normalized status model (needs-input / working / completed / failed / //! stopped) so users can see what is in flight, what is blocked on them, and //! what finished. Live run state already persists to the ledger via the spawn -//! tools + `channels::providers::web::progress_bridge`; this module only +//! tools + `web_chat::progress_bridge`; this module only //! projects and groups it for display. //! //! Control verbs (stop / retry / continue / follow-up) live in [`control`]: diff --git a/src/openhuman/agent_orchestration/run_ledger_finalize.rs b/src/openhuman/agent_orchestration/run_ledger_finalize.rs index be4a7cf7ae..4133a42114 100644 --- a/src/openhuman/agent_orchestration/run_ledger_finalize.rs +++ b/src/openhuman/agent_orchestration/run_ledger_finalize.rs @@ -12,7 +12,7 @@ //! — the *spawning turn's* progress channel. //! //! The run-ledger's terminal transition used to live **only** in the per-turn -//! [`progress_bridge`](crate::openhuman::channels::providers::web::progress_bridge), +//! [`progress_bridge`](crate::openhuman::web_chat::progress_bridge), //! which consumes that progress channel. That works for synchronous subagents //! (they finish inside the turn, sink still alive) but **leaks** for detached //! `spawn_async_subagent` runs: they outlive the parent turn, so when they diff --git a/src/openhuman/agentbox/invoker.rs b/src/openhuman/agentbox/invoker.rs index fca699ad2c..8c6983d001 100644 --- a/src/openhuman/agentbox/invoker.rs +++ b/src/openhuman/agentbox/invoker.rs @@ -10,11 +10,9 @@ use std::sync::Arc; use tokio::sync::broadcast::error::RecvError; use crate::core::socketio::WebChannelEvent; -use crate::openhuman::channels::providers::web::{ - start_chat, subscribe_web_channel_events, ChatRequestMetadata, -}; use crate::openhuman::memory::rpc_models::CreateConversationThreadRequest; use crate::openhuman::threads::ops::thread_create_new; +use crate::openhuman::web_chat::{start_chat, subscribe_web_channel_events, ChatRequestMetadata}; /// Outcome of inspecting one broadcast event against the request we're /// awaiting. Extracted as a pure function so the request-id filtering and diff --git a/src/openhuman/approval/README.md b/src/openhuman/approval/README.md index 18c0b48d73..416227fc2a 100644 --- a/src/openhuman/approval/README.md +++ b/src/openhuman/approval/README.md @@ -56,7 +56,7 @@ None. This module gates other domains' tools; it owns no tools of its own (no `t Published via `publish_global` (domain `approval`, defined in `src/core/event_bus/events.rs`): -- `DomainEvent::ApprovalRequested { request_id, tool_name, action_summary, args_redacted, session_id, thread_id, client_id }` — emitted when a call is parked. Bridged to the `approval_request` web-channel socket event by `ApprovalSurfaceSubscriber` (defined in `src/openhuman/channels/providers/web.rs`). +- `DomainEvent::ApprovalRequested { request_id, tool_name, action_summary, args_redacted, session_id, thread_id, client_id }` — emitted when a call is parked. Bridged to the `approval_request` web-channel socket event by `ApprovalSurfaceSubscriber` (defined in `src/openhuman/web_chat/`). - `DomainEvent::ApprovalDecided { request_id, tool_name, decision }` — emitted when a decision is applied. No `bus.rs` in this module — it only publishes; the subscriber lives in the `channels` web provider. @@ -80,7 +80,7 @@ SQLite DB at `{workspace_dir}/approval/approval.db`, table `pending_approvals` ( - `src/core/jsonrpc.rs` — installs the global gate (`ApprovalGate::init_global`) at startup; wires the approval RPCs. - `src/core/all.rs` — registers the controller schemas. - `src/openhuman/tinyagents/middleware.rs` (`ApprovalSecurityMiddleware`, a `wrap_tool` middleware on every turn path) — routes external-effect tool calls through the gate before `execute()` and records the terminal audit row. -- `src/openhuman/channels/providers/web.rs` — sets `APPROVAL_CHAT_CONTEXT`, hosts `ApprovalSurfaceSubscriber`, and routes typed yes/no replies to `approval_decide`. +- `src/openhuman/web_chat/` — sets `APPROVAL_CHAT_CONTEXT`, hosts `ApprovalSurfaceSubscriber`, and routes typed yes/no replies to `approval_decide`. - `src/openhuman/channels/proactive.rs`, `src/openhuman/agent/triage/escalation.rs`, `src/openhuman/tools/impl/system/install_tool.rs`, `src/openhuman/wallet/execution.rs` — interact with the gate / approval types. ## Notes / gotchas diff --git a/src/openhuman/approval/gate.rs b/src/openhuman/approval/gate.rs index 3b11a213b0..afde88c84b 100644 --- a/src/openhuman/approval/gate.rs +++ b/src/openhuman/approval/gate.rs @@ -71,7 +71,7 @@ const IN_CALL_APPROVAL_TTL: Duration = Duration::from_secs(120); /// Per-turn chat context for routing a parked approval's yes/no reply back to /// the originating thread. The web channel scopes this task-local around the -/// agent run (`channels::providers::web`); because the `run_turn` handler, the +/// agent run (`web_chat`); because the `run_turn` handler, the /// tool loop, and `intercept` all run inline (`.await`) within that spawned /// task, it propagates down to `intercept` with no signature plumbing. Absent /// for non-chat callers (CLI, sub-agents) — their approvals are simply not @@ -815,7 +815,7 @@ impl ApprovalGate { // Flow-origin surface bridge (flow-approval-surface, PR3): a flow run // has no chat thread/client to route the generic `ApprovalRequested` // through (both are `None` above, so the web-channel bridge silently - // drops it — see `channels::providers::web::event_bus`'s + // drops it — see `web_chat::event_bus`'s // `ApprovalSurfaceSubscriber`), which is exactly the silent-deadlock // bug this correlation fixes. Broadcast a dedicated // `flow_approval_request` socket event (no thread/client required, @@ -1340,7 +1340,7 @@ mod tests { /// A matching web-chat origin for the chat context fixture. Tests /// exercising the parking flow scope BOTH task-locals — production - /// callers in `channels/providers/web` do the same. + /// callers in `web_chat` do the same. fn web_origin() -> AgentTurnOrigin { AgentTurnOrigin::WebChat { thread_id: "t-test".into(), diff --git a/src/openhuman/artifacts/store.rs b/src/openhuman/artifacts/store.rs index 9d21de44b1..9255a871a9 100644 --- a/src/openhuman/artifacts/store.rs +++ b/src/openhuman/artifacts/store.rs @@ -616,7 +616,7 @@ pub async fn fail_artifact( } /// Read the active [`ApprovalChatContext`] task-local (set by -/// `channels::providers::web` around each chat turn) and return its +/// `web_chat` around each chat turn) and return its /// thread + client ids. Returns `(None, None)` for non-chat callers /// (CLI, cron, sub-agent runners) so artifact emit hooks degrade /// gracefully — the event is still published but the web subscriber diff --git a/src/openhuman/channels/bus.rs b/src/openhuman/channels/bus.rs index e82267d400..6ee6d355ba 100644 --- a/src/openhuman/channels/bus.rs +++ b/src/openhuman/channels/bus.rs @@ -87,10 +87,9 @@ impl EventHandler for ChannelInboundSubscriber { // here so the surface stays segregated end-to-end. let client_id = derive_inbound_client_id(channel, sender.as_deref()); - let mut event_rx = - crate::openhuman::channels::providers::web::subscribe_web_channel_events(); + let mut event_rx = crate::openhuman::web_chat::subscribe_web_channel_events(); - let request_id = match crate::openhuman::channels::providers::web::start_chat( + let request_id = match crate::openhuman::web_chat::start_chat( &client_id, &thread_id, message, @@ -99,7 +98,7 @@ impl EventHandler for ChannelInboundSubscriber { None, None, None, - crate::openhuman::channels::providers::web::ChatRequestMetadata { + crate::openhuman::web_chat::ChatRequestMetadata { // Tag inbound provider messages so traces classify as // run:channel_inbound instead of interactive chat. source: Some("channel_inbound".to_string()), diff --git a/src/openhuman/channels/host/adapters.rs b/src/openhuman/channels/host/adapters.rs index 8a017aad1c..8b0367ddc4 100644 --- a/src/openhuman/channels/host/adapters.rs +++ b/src/openhuman/channels/host/adapters.rs @@ -352,7 +352,7 @@ impl EventSink for OpenHumanEventSink { "{LOG_PREFIX} web event payload not a WebChannelEvent ({kind}): {e}" ) })?; - crate::openhuman::channels::providers::web::publish_web_channel_event(event); + crate::openhuman::web_chat::publish_web_channel_event(event); } "channel" => { use crate::core::event_bus::{publish_global, DomainEvent}; diff --git a/src/openhuman/channels/mod.rs b/src/openhuman/channels/mod.rs index e869a3876a..f28dc7d379 100644 --- a/src/openhuman/channels/mod.rs +++ b/src/openhuman/channels/mod.rs @@ -30,7 +30,6 @@ pub use providers::qq; pub use providers::signal; pub use providers::slack; pub use providers::telegram; -pub use providers::web; pub use providers::whatsapp; #[cfg(feature = "whatsapp-web")] pub use providers::whatsapp_web; diff --git a/src/openhuman/channels/proactive.rs b/src/openhuman/channels/proactive.rs index 9298fb9dea..fe9f762d42 100644 --- a/src/openhuman/channels/proactive.rs +++ b/src/openhuman/channels/proactive.rs @@ -21,8 +21,8 @@ use crate::core::event_bus::{DomainEvent, EventHandler}; use crate::core::socketio::WebChannelEvent; -use crate::openhuman::channels::providers::web::publish_web_channel_event; use crate::openhuman::channels::{Channel, ChannelSendExt, SendMessage}; +use crate::openhuman::web_chat::publish_web_channel_event; use async_trait::async_trait; use std::collections::HashMap; use std::sync::{Arc, RwLock}; diff --git a/src/openhuman/channels/providers/mod.rs b/src/openhuman/channels/providers/mod.rs index 37534b47b4..37f9bac96a 100644 --- a/src/openhuman/channels/providers/mod.rs +++ b/src/openhuman/channels/providers/mod.rs @@ -12,7 +12,6 @@ pub mod qq; pub mod signal; pub mod slack; pub mod telegram; -pub mod web; pub mod whatsapp; #[cfg(feature = "whatsapp-web")] pub mod whatsapp_web; diff --git a/src/openhuman/channels/providers/telegram/remote_control.rs b/src/openhuman/channels/providers/telegram/remote_control.rs index 93c35c232f..fe2d81c7bf 100644 --- a/src/openhuman/channels/providers/telegram/remote_control.rs +++ b/src/openhuman/channels/providers/telegram/remote_control.rs @@ -270,7 +270,7 @@ async fn build_new_session_response(ctx: &ChannelRuntimeContext, msg: &ChannelMe ); } - crate::openhuman::channels::providers::web::invalidate_thread_sessions(&thread_id).await; + crate::openhuman::web_chat::invalidate_thread_sessions(&thread_id).await; tracing::info!( "{LOG_PREFIX} new session thread_id={thread_id} reply_target={} sender_key={sender_key}", diff --git a/src/openhuman/channels/runtime/dispatch/processor.rs b/src/openhuman/channels/runtime/dispatch/processor.rs index d393109edb..973bf5d37b 100644 --- a/src/openhuman/channels/runtime/dispatch/processor.rs +++ b/src/openhuman/channels/runtime/dispatch/processor.rs @@ -89,7 +89,7 @@ pub(crate) fn channel_has_approval_surface(channel: &str) -> bool { /// Otherwise return `false` so the caller can dispatch the message as a /// fresh turn (which intentionally cancels any parked approval — the /// user is redirecting). Mirrors the web channel intercept at -/// `channels/providers/web.rs:493-525`. +/// `web_chat/`. /// /// [`ApprovalGate::decide`]: crate::openhuman::approval::ApprovalGate::decide async fn try_route_approval_reply(msg: &traits::ChannelMessage) -> bool { @@ -185,7 +185,7 @@ pub(crate) async fn process_channel_runtime_message( // history key, route it to `ApprovalGate::decide` and return — running // a fresh agent turn would cancel the parked tool call. Any other text // falls through to the normal dispatch (the user is redirecting). Mirrors - // the same intercept in `channels/providers/web.rs:493-525`. + // the same intercept in `web_chat/`. if channel_has_approval_surface(&msg.channel) && try_route_approval_reply(&msg).await { return; } diff --git a/src/openhuman/channels/runtime/startup.rs b/src/openhuman/channels/runtime/startup.rs index b3a13b5a11..bc777e88a2 100644 --- a/src/openhuman/channels/runtime/startup.rs +++ b/src/openhuman/channels/runtime/startup.rs @@ -166,20 +166,20 @@ pub async fn start_channels(mut config: Config) -> Result<()> { crate::openhuman::agent_meetings::bus::register_meeting_event_subscriber(); // Surface parked ApprovalGate requests as chat messages so the user can // answer yes/no in the thread (chat-native approval, issue #1339). - crate::openhuman::channels::providers::web::register_approval_surface_subscriber(); + crate::openhuman::web_chat::register_approval_surface_subscriber(); // Surface generated-artifact lifecycle events (ArtifactReady / // ArtifactFailed) as `artifact_ready` / `artifact_failed` web-channel // events so the frontend ArtifactCard can render in chat (#2779). - crate::openhuman::channels::providers::web::register_artifact_surface_subscriber(); + crate::openhuman::web_chat::register_artifact_surface_subscriber(); // Bridge emergency-stop halt/resume (AutomationHalted / AutomationResumed) // to the `automation_halt` web-channel socket event, broadcast to every // client via the "system" room, so the frontend kill-switch UI updates // globally (#4255). - crate::openhuman::channels::providers::web::register_automation_halt_subscriber(); + crate::openhuman::web_chat::register_automation_halt_subscriber(); // Surface external-egress disclosure events (ExternalTransferPending) as // `external_transfer_pending` web-channel events so the frontend can show a // per-action "what leaves, to where, why" card (privacy epic S2, #4436). - crate::openhuman::channels::providers::web::register_egress_surface_subscriber(); + crate::openhuman::web_chat::register_egress_surface_subscriber(); // Spawn the per-toolkit provider periodic sync scheduler. This is // a thin tokio task that ticks every minute and dispatches into // any provider whose `sync_interval_secs` has elapsed for an @@ -202,79 +202,11 @@ pub async fn start_channels(mut config: Config) -> Result<()> { // channel dispatch calls `request_native_global("agent.run_turn", …)` // for every inbound message. crate::openhuman::agent::bus::register_agent_handlers(); - // Phase 2 learning producers: email-signature subscriber reacts to - // DocumentCanonicalized events and emits Identity candidates into the buffer. - // The handle is intentionally leaked into a static so the subscription stays - // alive for the lifetime of the process (same pattern as TracingSubscriber). - { - use crate::core::event_bus::SubscriptionHandle; - use std::sync::OnceLock; - static EMAIL_SIG_HANDLE: OnceLock> = OnceLock::new(); - EMAIL_SIG_HANDLE.get_or_init(|| { - crate::openhuman::learning::extract::signature::register_email_signature_subscriber() - }); - } - - // Phase 3 learning: register the event-driven rebuild trigger. - // The stability detector is wired up only when the global memory client is - // already initialised (it may not be in the channel runtime path — the - // client is initialised later in `start_channels`). - { - use crate::core::event_bus::SubscriptionHandle; - use std::sync::OnceLock; - static REBUILD_TRIGGER_HANDLE: OnceLock> = OnceLock::new(); - REBUILD_TRIGGER_HANDLE.get_or_init(|| { - if let Some(client) = crate::openhuman::memory::global::client_if_ready() { - use crate::openhuman::learning::cache::FacetCache; - use crate::openhuman::learning::scheduler::register_event_trigger; - use crate::openhuman::learning::StabilityDetector; - use std::sync::Arc; - let cache = FacetCache::new(client.profile_conn()); - let detector = Arc::new(StabilityDetector::new(cache)); - // Also spawn the periodic rebuild loop (30-minute cadence). - let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false); - // Leak the sender so the loop never receives a shutdown signal - // until the process exits. This matches the pattern used by - // other always-on background tasks. - Box::leak(Box::new(shutdown_tx)); - crate::openhuman::learning::scheduler::spawn_rebuild_loop( - Arc::clone(&detector), - crate::openhuman::learning::scheduler::DEFAULT_REBUILD_INTERVAL, - shutdown_rx, - ); - register_event_trigger(detector) - } else { - tracing::debug!("[learning::scheduler] memory client not ready at channel startup, skipping event-trigger registration"); - None - } - }); - } - - // Phase 4 learning: register the ProfileMdRenderer subscriber. - // Subscribes to CacheRebuilt events and re-renders the five cache-derived - // PROFILE.md blocks (style, identity, tooling, vetoes, goals). - { - use crate::core::event_bus::SubscriptionHandle; - use std::sync::OnceLock; - static PROFILE_MD_RENDERER_HANDLE: OnceLock> = OnceLock::new(); - PROFILE_MD_RENDERER_HANDLE.get_or_init(|| { - if let Some(client) = crate::openhuman::memory::global::client_if_ready() { - use crate::openhuman::learning::cache::FacetCache; - use crate::openhuman::learning::ProfileMdRenderer; - use std::sync::Arc; - let cache = Arc::new(FacetCache::new(client.profile_conn())); - let renderer = - Arc::new(ProfileMdRenderer::new(cache, config.workspace_dir.clone())); - ProfileMdRenderer::subscribe(renderer) - } else { - tracing::debug!( - "[learning::profile_md_renderer] memory client not ready at startup, \ - skipping ProfileMdRenderer registration" - ); - None - } - }); - } + // The Phase 2/3/4 self-improvement subscribers (email-signature producer, + // rebuild trigger, ProfileMdRenderer) are registered in + // core::jsonrpc::register_domain_subscribers instead. start_channels is + // skipped when no channel is configured, so wiring them here silently + // dropped user-profile inference for channel-less users (#5003). tracing::debug!("[event_bus] global singleton initialized in start_channels"); diff --git a/src/openhuman/cron/scheduler.rs b/src/openhuman/cron/scheduler.rs index 1c869beb21..115e28fdc5 100644 --- a/src/openhuman/cron/scheduler.rs +++ b/src/openhuman/cron/scheduler.rs @@ -76,7 +76,7 @@ fn agent_error_to_user_message(err: &AgentError) -> &'static str { // contract, for clean notification-drawer rendering) names // the two highest-signal remedies — credits and model // configuration. The richer three-remedy copy lives on the - // chat-surface side (`channels/providers/web_errors.rs`'s + // chat-surface side (`web_chat/web_errors.rs`'s // empty_response arm) where there's no drawer-width limit. "Empty model response. Out of credits (Settings \u{2192} Billing) or try another model in Connections \u{2192} API keys \u{2192} LLM." } @@ -764,15 +764,13 @@ fn permanent_halt_message(credits_exhausted: bool, budget_exhausted: bool) -> &' /// deep-link action even though no chat thread is active. fn publish_cron_user_error(kind: &str) { log::debug!("[cron] action=surface_user_error kind={kind}"); - crate::openhuman::channels::providers::web::publish_web_channel_event( - crate::core::socketio::WebChannelEvent { - event: "user_error".to_string(), - client_id: "system".to_string(), - error_type: Some(kind.to_string()), - error_source: Some("cron".to_string()), - ..Default::default() - }, - ); + crate::openhuman::web_chat::publish_web_channel_event(crate::core::socketio::WebChannelEvent { + event: "user_error".to_string(), + client_id: "system".to_string(), + error_type: Some(kind.to_string()), + error_source: Some("cron".to_string()), + ..Default::default() + }); } async fn process_due_jobs(config: &Config, security: &Arc, jobs: Vec) { diff --git a/src/openhuman/cron/scheduler_tests.rs b/src/openhuman/cron/scheduler_tests.rs index fc5b31df3b..f14212282f 100644 --- a/src/openhuman/cron/scheduler_tests.rs +++ b/src/openhuman/cron/scheduler_tests.rs @@ -1866,7 +1866,7 @@ fn next_user_error( #[test] fn publish_cron_user_error_broadcasts_metadata_only_for_each_kind() { - use crate::openhuman::channels::providers::web::subscribe_web_channel_events; + use crate::openhuman::web_chat::subscribe_web_channel_events; // Folded from two tests that both published `api_key_missing` to the // process-global bus and could false-pass off each other's broadcast under diff --git a/src/openhuman/flows/ops.rs b/src/openhuman/flows/ops.rs index 1ee18d0563..62a2968054 100644 --- a/src/openhuman/flows/ops.rs +++ b/src/openhuman/flows/ops.rs @@ -4018,13 +4018,13 @@ fn attach_flow_progress_bridge( source = %source, "[flows] progress bridge: attaching (streaming copilot/scout turn)" ); - crate::openhuman::channels::providers::web::spawn_progress_bridge( + crate::openhuman::web_chat::spawn_progress_bridge( progress_rx, "system".to_string(), target.thread_id.clone(), target.request_id.clone(), crate::openhuman::threads::turn_state::TurnStateStore::new(config.workspace_dir.clone()), - crate::openhuman::channels::providers::web::ChatRequestMetadata { + crate::openhuman::web_chat::ChatRequestMetadata { source: Some(source.to_string()), ..Default::default() }, @@ -4046,7 +4046,7 @@ async fn finalize_flow_stream( ) { match result { Ok(text) => { - crate::openhuman::channels::providers::web::presentation::deliver_response( + crate::openhuman::web_chat::presentation::deliver_response( "system", &target.thread_id, &target.request_id, @@ -4060,7 +4060,7 @@ async fn finalize_flow_stream( .await; } Err(err) => { - crate::openhuman::channels::providers::web::publish_web_channel_event( + crate::openhuman::web_chat::publish_web_channel_event( crate::core::socketio::WebChannelEvent { event: "chat_error".to_string(), client_id: "system".to_string(), diff --git a/src/openhuman/inference/provider/error_code.rs b/src/openhuman/inference/provider/error_code.rs index d2825ac37f..a69e838d52 100644 --- a/src/openhuman/inference/provider/error_code.rs +++ b/src/openhuman/inference/provider/error_code.rs @@ -13,7 +13,7 @@ //! runs their own provider key, no `errorCode` is present). The presence of an //! `errorCode` is therefore the single load-bearing signal for two decisions: //! -//! 1. **Classification** ([`super::super::super::channels::providers::web_errors::classify_inference_error`]): +//! 1. **Classification** ([`super::super::super::web_chat::web_errors::classify_inference_error`]): //! when an `errorCode` is present, branch on it FIRST and ignore the //! substring heuristics; when it is absent, fall back to the substring //! ladder (the BYO / direct-provider path, whose "check your API key" / diff --git a/src/openhuman/learning/mod.rs b/src/openhuman/learning/mod.rs index be61740002..ae92327647 100644 --- a/src/openhuman/learning/mod.rs +++ b/src/openhuman/learning/mod.rs @@ -31,6 +31,7 @@ pub mod reflection; pub mod scheduler; pub mod schemas; pub mod stability_detector; +pub mod startup; pub mod tool_tracker; pub mod tools; pub mod transcript_ingest; diff --git a/src/openhuman/learning/startup.rs b/src/openhuman/learning/startup.rs new file mode 100644 index 0000000000..2da5a5de62 --- /dev/null +++ b/src/openhuman/learning/startup.rs @@ -0,0 +1,320 @@ +//! Always-on learning subscriber wiring. +//! +//! Registers the Phase 2/3/4 learning subscribers on the global event bus: +//! +//! - **Phase 2** — the email-signature producer (reacts to +//! `DocumentCanonicalized` events and emits Identity candidates into the +//! learning buffer). Needs no memory client. +//! - **Phase 3** — the event-driven rebuild trigger plus the periodic 30-minute +//! rebuild loop. Needs the global memory client. +//! - **Phase 4** — the `ProfileMdRenderer` (re-renders the five cache-derived +//! `PROFILE.md` blocks on `CacheRebuilt`). Needs the global memory client. +//! +//! # Why this lives here (#5003) +//! +//! These three subscriptions used to be wired inside +//! `channels::runtime::startup::start_channels`. That function is a misnamed +//! process-wide bootstrap that `core::runtime::services::spawn_channels_service` +//! **skips entirely** when no chat integration is configured (or when +//! `OPENHUMAN_DISABLE_CHANNEL_LISTENERS` is set) — logging only at debug. As a +//! result, channel-less users silently got **no** learning at all. +//! +//! [`register_learning_subscribers`] is invoked from the always-on Platform +//! boot path (`core::jsonrpc::register_domain_subscribers`, the unconditional +//! `DomainGroup::Platform` block), where the memory client and workspace dir are +//! already available. Registration is idempotent, so both boot paths (and repeat +//! calls) install each subscriber exactly once. + +use std::path::Path; +use std::sync::OnceLock; + +use crate::core::event_bus::SubscriptionHandle; +use crate::openhuman::memory::global::client_if_ready; +use crate::openhuman::memory_store::MemoryClientRef; + +/// Register the always-on learning subscribers on the global event bus. +/// +/// Idempotent for any caller: every subscription is guarded by a process-wide +/// `OnceLock`, so wiring this from multiple boot paths (or calling it twice) +/// registers each subscriber exactly once. The returned `SubscriptionHandle`s +/// are intentionally leaked into statics so the subscriptions stay alive for the +/// lifetime of the process (same pattern as `TracingSubscriber`). +/// +/// `workspace_dir` is the resolved workspace directory used by the +/// `ProfileMdRenderer` to locate `PROFILE.md`. +pub fn register_learning_subscribers(workspace_dir: std::path::PathBuf) { + // Phase 2 learning producer: email-signature subscriber reacts to + // DocumentCanonicalized events and emits Identity candidates into the + // buffer. Needs no memory client, so it always registers. + static EMAIL_SIG_HANDLE: OnceLock> = OnceLock::new(); + EMAIL_SIG_HANDLE.get_or_init(|| { + let handle = + crate::openhuman::learning::extract::signature::register_email_signature_subscriber(); + if handle.is_some() { + tracing::info!( + "[learning] email-signature subscriber registered (channel-independent boot path)" + ); + } else { + tracing::warn!( + "[learning] email-signature subscriber NOT registered — event bus not initialised" + ); + } + handle + }); + + // Phase 3 + Phase 4 learning: rebuild trigger + periodic loop + the + // ProfileMdRenderer. All three need the global memory client. The + // client-dependent work is split into `register_with_client` so both the + // ready and not-ready arms are unit-testable without touching process + // globals. + static CLIENT_HANDLES: OnceLock<(Option, Option)> = + OnceLock::new(); + CLIENT_HANDLES.get_or_init(|| register_with_client(client_if_ready(), &workspace_dir)); +} + +/// Register the client-dependent learning subscribers. +/// +/// Returns `(rebuild_trigger_handle, profile_md_renderer_handle)`. +/// +/// When `client` is `Some`, both the Phase 3 rebuild trigger (plus its periodic +/// 30-minute loop) and the Phase 4 `ProfileMdRenderer` are registered. When +/// `client` is `None` (the memory client is not yet initialised) both are +/// skipped and the skip is logged at **warn** — the *silent* skip was the #5003 +/// bug, so this must be loud. +/// +/// Taking the client as a parameter (rather than reading +/// `memory::global::client_if_ready()` internally) keeps both arms testable +/// without initialising the process-global memory singleton. +fn register_with_client( + client: Option, + workspace_dir: &Path, +) -> (Option, Option) { + let Some(client) = client else { + tracing::warn!( + "[learning::scheduler] memory client not ready at boot — skipping event-trigger + \ + periodic-rebuild registration; learning rebuilds will not fire until the client \ + initialises (#5003)" + ); + tracing::warn!( + "[learning::profile_md_renderer] memory client not ready at boot — skipping \ + ProfileMdRenderer registration; PROFILE.md will not be re-rendered until the client \ + initialises (#5003)" + ); + return (None, None); + }; + + // Phase 3 learning: event-driven rebuild trigger + periodic 30-minute loop. + let rebuild_trigger = { + use crate::openhuman::learning::cache::FacetCache; + use crate::openhuman::learning::scheduler::register_event_trigger; + use crate::openhuman::learning::StabilityDetector; + use std::sync::Arc; + let cache = FacetCache::new(client.profile_conn()); + let detector = Arc::new(StabilityDetector::new(cache)); + // Also spawn the periodic rebuild loop (30-minute cadence). + let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false); + // Leak the sender so the loop never receives a shutdown signal until the + // process exits. This matches the pattern used by other always-on + // background tasks. + Box::leak(Box::new(shutdown_tx)); + crate::openhuman::learning::scheduler::spawn_rebuild_loop( + Arc::clone(&detector), + crate::openhuman::learning::scheduler::DEFAULT_REBUILD_INTERVAL, + shutdown_rx, + ); + let handle = register_event_trigger(detector); + if handle.is_some() { + tracing::info!( + "[learning::scheduler] rebuild trigger + periodic loop registered \ + (channel-independent boot path)" + ); + } + handle + }; + + // Phase 4 learning: ProfileMdRenderer subscribes to CacheRebuilt events and + // re-renders the five cache-derived PROFILE.md blocks (style, identity, + // tooling, vetoes, goals). + let profile_md = { + use crate::openhuman::learning::cache::FacetCache; + use crate::openhuman::learning::ProfileMdRenderer; + use std::sync::Arc; + let cache = Arc::new(FacetCache::new(client.profile_conn())); + let renderer = Arc::new(ProfileMdRenderer::new(cache, workspace_dir.to_path_buf())); + let handle = ProfileMdRenderer::subscribe(renderer); + if handle.is_some() { + tracing::info!( + "[learning::profile_md_renderer] ProfileMdRenderer registered \ + (channel-independent boot path)" + ); + } + handle + }; + + (rebuild_trigger, profile_md) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::event_bus::{init_global, publish_global, DomainEvent, DEFAULT_CAPACITY}; + use crate::openhuman::learning::candidate::{self, EvidenceRef}; + use crate::openhuman::learning::extract::signature::parse_signature; + use crate::openhuman::memory_store::MemoryClient; + use std::sync::atomic::{AtomicU64, Ordering}; + use std::sync::Arc; + use std::time::Duration; + use tempfile::TempDir; + + /// Build a real `MemoryClient` against a fresh temp workspace. The temp dir + /// is returned so callers keep it alive for the client's lifetime. + fn test_client() -> (TempDir, MemoryClientRef) { + let tmp = TempDir::new().expect("tempdir"); + let client = Arc::new( + MemoryClient::from_workspace_dir(tmp.path().join("workspace")) + .expect("client should initialise against a fresh workspace"), + ); + (tmp, client) + } + + /// Process-unique email source id so buffer assertions never collide with + /// candidates pushed by other tests running in parallel against the shared + /// global buffer. + fn unique_source_id(tag: &str) -> String { + static COUNTER: AtomicU64 = AtomicU64::new(0); + format!( + "gmail:5003-{tag}-{}-{}", + std::process::id(), + COUNTER.fetch_add(1, Ordering::Relaxed) + ) + } + + /// A body whose trailing lines form a clear email signature — yields several + /// Identity candidates (name/role/timezone/employer). + fn signature_body() -> String { + "Hi, great to hear from you!\n\n\ + Thanks,\n\ + Alice Johnson\n\ + Senior Software Engineer\n\ + Acme Corp\n\ + San Francisco, CA\n\ + PST" + .to_string() + } + + fn publish_email_doc(source_id: &str, body: &str) { + publish_global(DomainEvent::DocumentCanonicalized { + source_id: source_id.to_string(), + source_kind: "email".to_string(), + chunks_written: 1, + chunk_ids: vec![format!("{source_id}-c1")], + canonicalized_at: 0.0, + body_preview: Some(body.to_string()), + }); + } + + /// Count candidates in the global buffer whose evidence points at + /// `source_id`. Isolates this test's assertions from concurrent producers. + fn candidates_for(source_id: &str) -> usize { + candidate::global() + .peek() + .iter() + .filter(|c| { + matches!( + &c.evidence, + EvidenceRef::EmailMessage { source_id: sid, .. } if sid == source_id + ) + }) + .count() + } + + /// Poll the global buffer until at least `expected` candidates for + /// `source_id` appear (async bus delivery), then settle briefly and return + /// the final count so an accidental double-registration would surface. + async fn wait_for_candidates(source_id: &str, expected: usize) -> usize { + for _ in 0..200 { + if candidates_for(source_id) >= expected { + break; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + // Settle: let any (unexpected) duplicate subscriber also deliver. + tokio::time::sleep(Duration::from_millis(40)).await; + candidates_for(source_id) + } + + #[tokio::test] + async fn register_with_client_registers_both_handles_when_ready() { + init_global(DEFAULT_CAPACITY); + let (tmp, client) = test_client(); + let (trigger, renderer) = register_with_client(Some(client), tmp.path()); + assert!( + trigger.is_some(), + "rebuild trigger must register when the memory client is ready" + ); + assert!( + renderer.is_some(), + "ProfileMdRenderer must register when the memory client is ready" + ); + } + + #[tokio::test] + async fn register_with_client_skips_and_warns_when_client_absent() { + // No memory client → both client-dependent subscribers are skipped and + // the (now loud) warn path is exercised. This is the else-arm the #5003 + // fix upgraded from a silent debug-level skip. + let tmp = TempDir::new().expect("tempdir"); + let (trigger, renderer) = register_with_client(None, tmp.path()); + assert!(trigger.is_none(), "no trigger without a client"); + assert!(renderer.is_none(), "no renderer without a client"); + } + + #[tokio::test] + async fn learning_subscriber_fires_with_no_channel_configured() { + init_global(DEFAULT_CAPACITY); + let (tmp, _client) = test_client(); + // Make the memory client ready so the full Platform wiring runs — no + // channel runtime is ever constructed in this test. + let _ = crate::openhuman::memory::global::init(tmp.path().join("workspace")); + register_learning_subscribers(tmp.path().to_path_buf()); + + let source_id = unique_source_id("e2e"); + let body = signature_body(); + let expected = parse_signature(&body, &source_id, &source_id).len(); + assert!( + expected > 0, + "signature body must yield at least one identity candidate" + ); + + publish_email_doc(&source_id, &body); + let got = wait_for_candidates(&source_id, expected).await; + assert_eq!( + got, expected, + "email-signature subscriber must push the parsed identity candidates \ + with no channel configured anywhere (#5003)" + ); + } + + #[tokio::test] + async fn register_learning_subscribers_is_idempotent() { + init_global(DEFAULT_CAPACITY); + let tmp = TempDir::new().expect("tempdir"); + let _ = crate::openhuman::memory::global::init(tmp.path().join("workspace")); + // Call twice — the process-wide OnceLock guards must keep exactly one + // email-signature subscriber alive, so a single event is handled once. + register_learning_subscribers(tmp.path().to_path_buf()); + register_learning_subscribers(tmp.path().to_path_buf()); + + let source_id = unique_source_id("idem"); + let body = signature_body(); + let expected = parse_signature(&body, &source_id, &source_id).len(); + assert!(expected > 0); + + publish_email_doc(&source_id, &body); + let got = wait_for_candidates(&source_id, expected).await; + assert_eq!( + got, expected, + "double registration must not double the pushed candidates (#5003 idempotency)" + ); + } +} diff --git a/src/openhuman/meet_agent/brain/llm.rs b/src/openhuman/meet_agent/brain/llm.rs index 5d54872bf8..47124b7db2 100644 --- a/src/openhuman/meet_agent/brain/llm.rs +++ b/src/openhuman/meet_agent/brain/llm.rs @@ -146,7 +146,7 @@ async fn get_or_build_agent_for_meet(request_id: &str) -> Result &str { - "channels::web::egress_surface" + "web_chat::egress_surface" } fn domains(&self) -> Option<&[&str]> { @@ -178,7 +178,7 @@ struct ArtifactSurfaceSubscriber; #[async_trait] impl EventHandler for ArtifactSurfaceSubscriber { fn name(&self) -> &str { - "channels::web::artifact_surface" + "web_chat::artifact_surface" } fn domains(&self) -> Option<&[&str]> { @@ -326,7 +326,7 @@ struct ApprovalSurfaceSubscriber; #[async_trait] impl EventHandler for ApprovalSurfaceSubscriber { fn name(&self) -> &str { - "channels::web::approval_surface" + "web_chat::approval_surface" } fn domains(&self) -> Option<&[&str]> { @@ -411,7 +411,7 @@ struct AutomationHaltSubscriber; #[async_trait] impl EventHandler for AutomationHaltSubscriber { fn name(&self) -> &str { - "channels::web::automation_halt" + "web_chat::automation_halt" } fn domains(&self) -> Option<&[&str]> { diff --git a/src/openhuman/channels/providers/web/mod.rs b/src/openhuman/web_chat/mod.rs similarity index 98% rename from src/openhuman/channels/providers/web/mod.rs rename to src/openhuman/web_chat/mod.rs index 4c979e8a78..2e8527413d 100644 --- a/src/openhuman/channels/providers/web/mod.rs +++ b/src/openhuman/web_chat/mod.rs @@ -9,7 +9,6 @@ mod schemas; mod session; mod types; -#[path = "../web_errors.rs"] mod web_errors; pub(crate) use web_errors::classify_inference_error; #[cfg(any(test, debug_assertions))] @@ -140,5 +139,5 @@ pub mod test_support { pub(crate) use types::SessionCacheFingerprint; #[cfg(test)] -#[path = "../web_tests.rs"] +#[path = "web_tests.rs"] mod tests; diff --git a/src/openhuman/channels/providers/web/ops.rs b/src/openhuman/web_chat/ops.rs similarity index 99% rename from src/openhuman/channels/providers/web/ops.rs rename to src/openhuman/web_chat/ops.rs index bc7a2a44c3..5a46c33067 100644 --- a/src/openhuman/channels/providers/web/ops.rs +++ b/src/openhuman/web_chat/ops.rs @@ -474,7 +474,7 @@ pub async fn start_chat( let prompt_decision = enforce_prompt_input( &message, PromptEnforcementContext { - source: "channels.providers.web.start_chat", + source: "web_chat.start_chat", request_id: Some(&request_id), user_id: Some(&client_id), session_id: Some(&thread_id), @@ -728,7 +728,7 @@ pub async fn start_chat( match result { Ok(chat_result) => { - crate::openhuman::channels::providers::web::presentation::deliver_response( + crate::openhuman::web_chat::presentation::deliver_response( &client_id_task, &thread_id_task, &request_id_task, @@ -931,7 +931,7 @@ async fn spawn_parallel_turn( match result { Some(Ok(chat_result)) => { - crate::openhuman::channels::providers::web::presentation::deliver_response( + crate::openhuman::web_chat::presentation::deliver_response( &client_id_task, &thread_id_task, &request_id_task, diff --git a/src/openhuman/channels/providers/web/presentation.rs b/src/openhuman/web_chat/presentation.rs similarity index 100% rename from src/openhuman/channels/providers/web/presentation.rs rename to src/openhuman/web_chat/presentation.rs diff --git a/src/openhuman/channels/providers/web/presentation_tests.rs b/src/openhuman/web_chat/presentation_tests.rs similarity index 100% rename from src/openhuman/channels/providers/web/presentation_tests.rs rename to src/openhuman/web_chat/presentation_tests.rs diff --git a/src/openhuman/channels/providers/web/progress_bridge.rs b/src/openhuman/web_chat/progress_bridge.rs similarity index 100% rename from src/openhuman/channels/providers/web/progress_bridge.rs rename to src/openhuman/web_chat/progress_bridge.rs diff --git a/src/openhuman/channels/providers/web/run_task.rs b/src/openhuman/web_chat/run_task.rs similarity index 100% rename from src/openhuman/channels/providers/web/run_task.rs rename to src/openhuman/web_chat/run_task.rs diff --git a/src/openhuman/channels/providers/web/schemas.rs b/src/openhuman/web_chat/schemas.rs similarity index 100% rename from src/openhuman/channels/providers/web/schemas.rs rename to src/openhuman/web_chat/schemas.rs diff --git a/src/openhuman/channels/providers/web/session.rs b/src/openhuman/web_chat/session.rs similarity index 100% rename from src/openhuman/channels/providers/web/session.rs rename to src/openhuman/web_chat/session.rs diff --git a/src/openhuman/channels/providers/web/types.rs b/src/openhuman/web_chat/types.rs similarity index 100% rename from src/openhuman/channels/providers/web/types.rs rename to src/openhuman/web_chat/types.rs diff --git a/src/openhuman/channels/providers/web_errors.rs b/src/openhuman/web_chat/web_errors.rs similarity index 100% rename from src/openhuman/channels/providers/web_errors.rs rename to src/openhuman/web_chat/web_errors.rs diff --git a/src/openhuman/channels/providers/web_tests.rs b/src/openhuman/web_chat/web_tests.rs similarity index 100% rename from src/openhuman/channels/providers/web_tests.rs rename to src/openhuman/web_chat/web_tests.rs diff --git a/tests/agent_harness_e2e.rs b/tests/agent_harness_e2e.rs index 8bbd69760a..0b1b82b866 100644 --- a/tests/agent_harness_e2e.rs +++ b/tests/agent_harness_e2e.rs @@ -1361,7 +1361,7 @@ encrypt = false /// same binary lose the bridge silently. This per-test helper avoids the issue by /// registering a fresh subscription on each test's own runtime. fn register_approval_bridge() -> Option { - openhuman_core::openhuman::channels::providers::web::fresh_approval_surface_subscription() + openhuman_core::openhuman::web_chat::fresh_approval_surface_subscription() } /// Pre-create a file in the action_dir so file_write sees it as an existing @@ -1473,7 +1473,7 @@ async fn approval_gate_approve_flow_inner() { .await; // Wait for the approval_request SSE event. - // Actual shape (src/openhuman/channels/providers/web/event_bus.rs:195-224): + // Actual shape (src/openhuman/web_chat/event_bus.rs:195-224): // { "event": "approval_request", "data": { "request_id": "...", "tool_name": "...", // "action_summary": "...", "args_redacted": {...} }, ... } let approval = wait_for_event(&mut events, "approval_request", Duration::from_secs(60)).await; diff --git a/tests/agentbox_e2e.rs b/tests/agentbox_e2e.rs index a44ca3b973..5521499e6a 100644 --- a/tests/agentbox_e2e.rs +++ b/tests/agentbox_e2e.rs @@ -14,7 +14,7 @@ //! //! The AgentBox `/run` invoker (`agentbox::invoker::CoreAgentInvoker`) drives //! the **live agent runtime** through the web-channel pipeline -//! (`channels::providers::web::start_chat`). End-to-end completion against a +//! (`web_chat::start_chat`). End-to-end completion against a //! freshly-bootstrapped tempdir workspace requires: //! //! 1. A logged-in user session on disk — `start_chat` and several upstream diff --git a/tests/composio_list_tools_stack_overflow_regression.rs b/tests/composio_list_tools_stack_overflow_regression.rs index 85c21916ec..565b08f583 100644 --- a/tests/composio_list_tools_stack_overflow_regression.rs +++ b/tests/composio_list_tools_stack_overflow_regression.rs @@ -22,7 +22,7 @@ //! ← subagent_runner::run_inner_loop / run_typed_mode / run_subagent //! ← SkillDelegationTool::execute (delegate_to_integrations_agent) //! ← Agent::execute_tool_call / execute_tools / turn -//! ← channels::providers::web::run_chat_task +//! ← web_chat::run_chat_task //! ``` //! //! The crash trigger is `composio_list_tools` reloading the config from @@ -50,7 +50,7 @@ //! ## What this test does //! //! Faithful reproduction in cargo-test is awkward: we can't easily -//! rebuild the upper chat-channel layers (`channels::providers::web:: +//! rebuild the upper chat-channel layers (`web_chat:: //! run_chat_task → Agent::turn → execute_tools → SkillDelegationTool`) //! without standing up an HTTP + Socket.IO stack. We drive the production //! path from `run_subagent` downward — i.e. everything below diff --git a/tests/raw_coverage/channels_bus_presentation_raw_coverage_e2e.rs b/tests/raw_coverage/channels_bus_presentation_raw_coverage_e2e.rs index 3e5cdb944c..5d1263e0f5 100644 --- a/tests/raw_coverage/channels_bus_presentation_raw_coverage_e2e.rs +++ b/tests/raw_coverage/channels_bus_presentation_raw_coverage_e2e.rs @@ -8,8 +8,8 @@ use std::time::Duration; use openhuman_core::core::event_bus::{DomainEvent, EventHandler}; use openhuman_core::openhuman::agent_memory::memory_loader::MemoryCitation; use openhuman_core::openhuman::channels::bus::ChannelInboundSubscriber; -use openhuman_core::openhuman::channels::providers::web::presentation::test_support as presentation_test_support; -use openhuman_core::openhuman::channels::providers::web::{ +use openhuman_core::openhuman::web_chat::presentation::test_support as presentation_test_support; +use openhuman_core::openhuman::web_chat::{ subscribe_web_channel_events, test_support as web_test_support, }; use serde_json::json; diff --git a/tests/raw_coverage/channels_large_round25_raw_coverage_e2e.rs b/tests/raw_coverage/channels_large_round25_raw_coverage_e2e.rs index 11632f99bf..c0c11f97a0 100644 --- a/tests/raw_coverage/channels_large_round25_raw_coverage_e2e.rs +++ b/tests/raw_coverage/channels_large_round25_raw_coverage_e2e.rs @@ -17,7 +17,7 @@ use openhuman_core::openhuman::channels::providers::mattermost::{ test_support as mattermost_support, MattermostChannel, }; use openhuman_core::openhuman::channels::providers::telegram::test_support as telegram_support; -use openhuman_core::openhuman::channels::providers::web::{self, test_support as web_support}; +use openhuman_core::openhuman::web_chat::{self as web, test_support as web_support}; use openhuman_core::openhuman::channels::test_support::{ build_channel_context_block_for_test, run_dispatch_harness, select_acknowledgment_reaction_for_test, DispatchHarnessOptions, TestMemoryEntry, diff --git a/tests/raw_coverage/channels_provider_deep_raw_coverage_e2e.rs b/tests/raw_coverage/channels_provider_deep_raw_coverage_e2e.rs index fb03729d8b..a69d01a252 100644 --- a/tests/raw_coverage/channels_provider_deep_raw_coverage_e2e.rs +++ b/tests/raw_coverage/channels_provider_deep_raw_coverage_e2e.rs @@ -8,7 +8,7 @@ use axum::{ routing::post, Router, }; -use openhuman_core::openhuman::channels::providers::web::{ +use openhuman_core::openhuman::web_chat::{ cancel_chat, start_chat, subscribe_web_channel_events, ChatRequestMetadata, }; use openhuman_core::openhuman::channels::providers::yuanbao::{YuanbaoChannel, YuanbaoConfig}; @@ -310,7 +310,7 @@ async fn web_channel_validation_cancel_and_classifier_snapshots_are_publicly_exe assert!(blocked.is_err()); let rate_limited = - openhuman_core::openhuman::channels::web::test_support::classify_error_for_test( + openhuman_core::openhuman::web_chat::test_support::classify_error_for_test( r#"openrouter API error (429 Too Many Requests): {"error":{"message":"slow down","retry_after":1.2}}"#, ); assert_eq!(rate_limited.error_type, "rate_limited"); @@ -320,38 +320,38 @@ async fn web_channel_validation_cancel_and_classifier_snapshots_are_publicly_exe assert!(rate_limited.retryable); let business_429 = - openhuman_core::openhuman::channels::web::test_support::classify_error_for_test( + openhuman_core::openhuman::web_chat::test_support::classify_error_for_test( "zai API error (429): code 1311 no available package", ); assert_eq!(business_429.error_type, "rate_limited"); assert!(!business_429.retryable); let action_budget = - openhuman_core::openhuman::channels::web::test_support::classify_error_for_test( + openhuman_core::openhuman::web_chat::test_support::classify_error_for_test( "rate limit exceeded: action budget exhausted", ); assert_eq!(action_budget.error_type, "action_budget_exceeded"); assert_eq!(action_budget.source, "openhuman_budget"); assert_eq!(action_budget.provider, None); - let exhausted = openhuman_core::openhuman::channels::web::test_support::classify_error_for_test( + let exhausted = openhuman_core::openhuman::web_chat::test_support::classify_error_for_test( "All providers/models failed. Attempts: openai API error (503 Service Unavailable)", ); assert_eq!(exhausted.fallback_available, Some(false)); assert_eq!( - openhuman_core::openhuman::channels::web::test_support::retry_after_secs_for_test( + openhuman_core::openhuman::web_chat::test_support::retry_after_secs_for_test( r#"{"retry_after": 0.1}"# ), Some(1) ); - assert!(openhuman_core::openhuman::channels::web::test_support::extracted_provider_detail_for_test( + assert!(openhuman_core::openhuman::web_chat::test_support::extracted_provider_detail_for_test( r#"openai API error (404): {"error":{"message":"Project does not have access to model x"}}"# ) .expect("provider detail") .contains("model")); assert!( - openhuman_core::openhuman::channels::web::test_support::is_non_retryable_rate_limit_for_test( + openhuman_core::openhuman::web_chat::test_support::is_non_retryable_rate_limit_for_test( "plan does not include this model" ) ); diff --git a/tests/raw_coverage/channels_provider_leftovers_raw_coverage_e2e.rs b/tests/raw_coverage/channels_provider_leftovers_raw_coverage_e2e.rs index 89bebebda1..8dc6d1d95e 100644 --- a/tests/raw_coverage/channels_provider_leftovers_raw_coverage_e2e.rs +++ b/tests/raw_coverage/channels_provider_leftovers_raw_coverage_e2e.rs @@ -15,7 +15,7 @@ use axum::{ Router, }; use openhuman_core::openhuman::channels::providers::telegram::TelegramChannel; -use openhuman_core::openhuman::channels::providers::web::{ +use openhuman_core::openhuman::web_chat::{ cancel_chat, start_chat, subscribe_web_channel_events, test_support as web_test_support, ChatRequestMetadata, }; diff --git a/tests/raw_coverage/channels_runtime_raw_coverage_e2e.rs b/tests/raw_coverage/channels_runtime_raw_coverage_e2e.rs index 7f97cf0d22..882f37bea7 100644 --- a/tests/raw_coverage/channels_runtime_raw_coverage_e2e.rs +++ b/tests/raw_coverage/channels_runtime_raw_coverage_e2e.rs @@ -9,7 +9,7 @@ use axum::{ Router, }; use openhuman_core::core::event_bus::{DomainEvent, EventHandler}; -use openhuman_core::openhuman::channels::providers::web::{ +use openhuman_core::openhuman::web_chat::{ cancel_chat, start_chat, subscribe_web_channel_events, ChatRequestMetadata, }; use openhuman_core::openhuman::channels::providers::yuanbao::{YuanbaoChannel, YuanbaoConfig}; diff --git a/tests/raw_coverage/channels_web_startup_raw_coverage_e2e.rs b/tests/raw_coverage/channels_web_startup_raw_coverage_e2e.rs index a6d57ad4c9..2e0ff56143 100644 --- a/tests/raw_coverage/channels_web_startup_raw_coverage_e2e.rs +++ b/tests/raw_coverage/channels_web_startup_raw_coverage_e2e.rs @@ -10,7 +10,7 @@ use openhuman_core::openhuman::channels::start_channels; use openhuman_core::openhuman::channels::test_support::{ lock_agent_handler, run_dispatch_harness, DispatchHarnessOptions, TestMemoryEntry, }; -use openhuman_core::openhuman::channels::web::{ +use openhuman_core::openhuman::web_chat::{ all_web_channel_controller_schemas, all_web_channel_registered_controllers, channel_web_cancel, channel_web_chat, schemas, start_chat, subscribe_web_channel_events, test_support as web_test_support, ChatRequestMetadata, diff --git a/tests/raw_coverage/channels_web_telegram_raw_coverage_e2e.rs b/tests/raw_coverage/channels_web_telegram_raw_coverage_e2e.rs index 7784797cce..ef1675d42a 100644 --- a/tests/raw_coverage/channels_web_telegram_raw_coverage_e2e.rs +++ b/tests/raw_coverage/channels_web_telegram_raw_coverage_e2e.rs @@ -16,7 +16,7 @@ use axum::{ }; use openhuman_core::core::event_bus::{init_global, publish_global, DomainEvent}; use openhuman_core::openhuman::channels::providers::telegram::TelegramChannel; -use openhuman_core::openhuman::channels::providers::web::{ +use openhuman_core::openhuman::web_chat::{ cancel_chat, register_approval_surface_subscriber, start_chat, subscribe_web_channel_events, test_support as web_test_support, ChatRequestMetadata, }; diff --git a/tests/raw_coverage/channels_web_yuanbao_round22_raw_coverage_e2e.rs b/tests/raw_coverage/channels_web_yuanbao_round22_raw_coverage_e2e.rs index 687005c5d9..11872a3f0c 100644 --- a/tests/raw_coverage/channels_web_yuanbao_round22_raw_coverage_e2e.rs +++ b/tests/raw_coverage/channels_web_yuanbao_round22_raw_coverage_e2e.rs @@ -13,7 +13,7 @@ use axum::{ Router, }; use openhuman_core::openhuman::channels::providers::telegram::TelegramChannel; -use openhuman_core::openhuman::channels::providers::web::{ +use openhuman_core::openhuman::web_chat::{ cancel_chat, start_chat, subscribe_web_channel_events, test_support as web_test_support, ChatRequestMetadata, }; diff --git a/tests/raw_coverage/tools_approval_channels_raw_coverage_e2e.rs b/tests/raw_coverage/tools_approval_channels_raw_coverage_e2e.rs index f7ed574df5..7402dc39f1 100644 --- a/tests/raw_coverage/tools_approval_channels_raw_coverage_e2e.rs +++ b/tests/raw_coverage/tools_approval_channels_raw_coverage_e2e.rs @@ -2067,8 +2067,8 @@ async fn channel_provider_public_paths_cover_pre_network_errors_and_utilities() #[tokio::test] async fn web_channel_public_paths_cover_event_delivery_and_validation_errors() { - let mut rx = openhuman_core::openhuman::channels::web::subscribe_web_channel_events(); - openhuman_core::openhuman::channels::web::publish_web_channel_event(WebChannelEvent { + let mut rx = openhuman_core::openhuman::web_chat::subscribe_web_channel_events(); + openhuman_core::openhuman::web_chat::publish_web_channel_event(WebChannelEvent { event: "coverage_event".to_string(), client_id: "client-1".to_string(), thread_id: "thread-1".to_string(), @@ -2086,7 +2086,7 @@ async fn web_channel_public_paths_cover_event_delivery_and_validation_errors() { assert_eq!(event.message.as_deref(), Some("hello web channel")); assert_eq!( - openhuman_core::openhuman::channels::web::start_chat( + openhuman_core::openhuman::web_chat::start_chat( "", "thread-1", "hello", @@ -2095,14 +2095,14 @@ async fn web_channel_public_paths_cover_event_delivery_and_validation_errors() { None, None, None, - openhuman_core::openhuman::channels::web::ChatRequestMetadata::default(), + openhuman_core::openhuman::web_chat::ChatRequestMetadata::default(), ) .await .expect_err("blank client_id"), "client_id is required" ); assert_eq!( - openhuman_core::openhuman::channels::web::start_chat( + openhuman_core::openhuman::web_chat::start_chat( "client-1", "", "hello", @@ -2111,14 +2111,14 @@ async fn web_channel_public_paths_cover_event_delivery_and_validation_errors() { None, None, None, - openhuman_core::openhuman::channels::web::ChatRequestMetadata::default(), + openhuman_core::openhuman::web_chat::ChatRequestMetadata::default(), ) .await .expect_err("blank thread_id"), "thread_id is required" ); assert_eq!( - openhuman_core::openhuman::channels::web::start_chat( + openhuman_core::openhuman::web_chat::start_chat( "client-1", "thread-1", " ", @@ -2127,7 +2127,7 @@ async fn web_channel_public_paths_cover_event_delivery_and_validation_errors() { None, None, None, - openhuman_core::openhuman::channels::web::ChatRequestMetadata::default(), + openhuman_core::openhuman::web_chat::ChatRequestMetadata::default(), ) .await .expect_err("blank message"), @@ -2135,26 +2135,26 @@ async fn web_channel_public_paths_cover_event_delivery_and_validation_errors() { ); assert_eq!( - openhuman_core::openhuman::channels::web::cancel_chat("", "thread-1") + openhuman_core::openhuman::web_chat::cancel_chat("", "thread-1") .await .expect_err("blank cancel client_id"), "client_id is required" ); assert_eq!( - openhuman_core::openhuman::channels::web::cancel_chat("client-1", "") + openhuman_core::openhuman::web_chat::cancel_chat("client-1", "") .await .expect_err("blank cancel thread_id"), "thread_id is required" ); assert!( - openhuman_core::openhuman::channels::web::cancel_chat("client-1", "thread-1") + openhuman_core::openhuman::web_chat::cancel_chat("client-1", "thread-1") .await .expect("cancel with no in-flight request") .is_none() ); - openhuman_core::openhuman::channels::web::invalidate_thread_sessions("thread-1").await; + openhuman_core::openhuman::web_chat::invalidate_thread_sessions("thread-1").await; assert!( - openhuman_core::openhuman::channels::web::in_flight_entries_for_test() + openhuman_core::openhuman::web_chat::in_flight_entries_for_test() .await .is_empty() ); @@ -2181,7 +2181,7 @@ async fn proactive_subscriber_routes_web_and_active_external_channel_without_net } } - let mut rx = openhuman_core::openhuman::channels::web::subscribe_web_channel_events(); + let mut rx = openhuman_core::openhuman::web_chat::subscribe_web_channel_events(); let capture = Arc::new(CapturingChannel::default()); let mut channels: HashMap> = HashMap::new(); channels.insert("capture".into(), capture.clone()); diff --git a/tests/raw_coverage/tools_network_channels_raw_coverage_e2e.rs b/tests/raw_coverage/tools_network_channels_raw_coverage_e2e.rs index 97cb277dd2..5a9b254611 100644 --- a/tests/raw_coverage/tools_network_channels_raw_coverage_e2e.rs +++ b/tests/raw_coverage/tools_network_channels_raw_coverage_e2e.rs @@ -18,7 +18,7 @@ use tempfile::{tempdir, TempDir}; use tokio::time::timeout; use openhuman_core::core::socketio::WebChannelEvent; -use openhuman_core::openhuman::channels::providers::web::{ +use openhuman_core::openhuman::web_chat::{ all_web_channel_controller_schemas, all_web_channel_registered_controllers, cancel_chat, channel_web_cancel, publish_web_channel_event, schemas as web_channel_schema, start_chat, subscribe_web_channel_events, ChatRequestMetadata, diff --git a/tests/web_cancel_request_scoping.rs b/tests/web_cancel_request_scoping.rs index 4b79df0fab..c90f78c228 100644 --- a/tests/web_cancel_request_scoping.rs +++ b/tests/web_cancel_request_scoping.rs @@ -16,7 +16,7 @@ //! These assertions pin that decision. `cancel_should_target` is a pure //! predicate, so this is a fast, deterministic unit test with no runtime setup. -use openhuman_core::openhuman::channels::web::cancel_should_target; +use openhuman_core::openhuman::web_chat::cancel_should_target; #[test] fn unscoped_cancel_always_targets_the_in_flight_turn() {