diff --git a/docs/TEST-COVERAGE-MATRIX.md b/docs/TEST-COVERAGE-MATRIX.md
index c3562812b..cb2076f0b 100644
--- a/docs/TEST-COVERAGE-MATRIX.md
+++ b/docs/TEST-COVERAGE-MATRIX.md
@@ -286,7 +286,7 @@ Canonical mapping of every product feature to its test source(s). Drives gap-fil
| ID | Feature | Layer | Test path(s) | Status | Notes |
| ----- | ------------------ | ----- | ----------------------------------------- | ------ | --------------------------------- |
| 8.2.1 | Context Injection | RI | `tests/autocomplete_memory_e2e.rs` | ✅ | |
-| 8.2.2 | Memory Consistency | RI | `tests/memory_graph_sync_e2e.rs` | ✅ | |
+| 8.2.2 | Memory Consistency | RI | `tests/memory_graph_sync_e2e.rs`, `tests/worker_c_modules_e2e.rs` | ✅ | Worker C RPC E2E verifies memory-tree ingest is reflected by `memory_sync_status_list` |
| 8.2.3 | Memory Scaling | RU | `src/openhuman/memory/ingestion_tests.rs` | 🟡 | Soak/scale benchmark not asserted |
### 8.3 Memory Retrieval Benchmarks
@@ -386,7 +386,7 @@ Canonical mapping of every product feature to its test source(s). Drives gap-fil
| ID | Feature | Layer | Test path(s) | Status | Notes |
| ------ | ---------------------- | ----- | ------------------------------------------ | ------ | -------------------- |
| 10.5.1 | Channel Isolation | RU | `src/openhuman/channels/tests/identity.rs` | ✅ | |
-| 10.5.2 | Unified Inbox Handling | WD | `channels-smoke.spec.ts` | 🟡 | UI assertion shallow |
+| 10.5.2 | Unified Inbox Handling | WD+RI | `channels-smoke.spec.ts`, `tests/worker_c_modules_e2e.rs` | 🟡 | UI assertion shallow; RI covers config-only channel status after connect/disconnect |
| 10.5.3 | Context Preservation | RU | `src/openhuman/channels/tests/context.rs` | ✅ | |
### 10.6 Permission Enforcement
@@ -401,7 +401,7 @@ Canonical mapping of every product feature to its test source(s). Drives gap-fil
| ID | Feature | Layer | Test path(s) | Status | Notes |
| ------ | ---------------------- | ----- | ------------------------------------------- | ------ | -------------------------------- |
-| 10.7.1 | Integration Disconnect | WD | `gmail-flow.spec.ts` | ✅ | |
+| 10.7.1 | Integration Disconnect | WD+RI | `gmail-flow.spec.ts`, `tests/worker_c_modules_e2e.rs` | ✅ | RI covers `channels_disconnect` clearing config-only iMessage state |
| 10.7.2 | Token Revocation | RU | `src/openhuman/credentials/` | ✅ | |
| 10.7.3 | Re-Authorization Flow | WD | `skill-oauth.spec.ts` | 🟡 | Re-auth post-revoke not asserted |
| 10.7.4 | Permission Re-Sync | WD | _missing_ — tracked #968 | ❌ | |
@@ -418,7 +418,7 @@ Canonical mapping of every product feature to its test source(s). Drives gap-fil
| 11.1.2 | Actionable Item Extraction | VU | `app/src/components/intelligence/__tests__/utils.test.ts` (this PR) | ✅ | Was ❌ |
| 11.1.3 | Analyze Trigger | WD | `app/test/e2e/specs/insights-dashboard.spec.ts` mounts the route (this PR); explicit analyze-handler invocation TBD | 🟡 | Route mounts and search/filter UI assert — full analyze trigger flow tracked as follow-up |
| 11.1.4 | MCP server (stdio + HTTP) | RU | `src/openhuman/mcp_server/` | ✅ | Stdio framing plus Streamable HTTP/SSE session lifecycle; `McpHttpClient` round-trip tests |
-| 11.1.5 | Global tool registry | RI | `src/openhuman/tool_registry/`, `tests/json_rpc_e2e.rs` | ✅ | Read-only MCP/controller discovery with routes, schemas, version, allowed agents, and health |
+| 11.1.5 | Global tool registry | RI | `src/openhuman/tool_registry/`, `tests/json_rpc_e2e.rs`, `tests/domain_modules_e2e.rs`, `tests/worker_b_domain_e2e.rs` | ✅ | Read-only MCP/controller discovery with routes, schemas, version, allowed agents, and health |
| 11.1.6 | SearXNG MCP search | RU | `src/openhuman/integrations/searxng.rs`, `src/openhuman/mcp_server/tools.rs`, `src/openhuman/tools/schemas.rs` | ✅ | Self-hosted search config, normalized results, MCP argument validation, and mocked HTTP execution |
| 11.1.7 | Bundled prompt resources | RU | `src/openhuman/mcp_server/resources.rs`, `src/openhuman/mcp_server/protocol.rs` | ✅ | `resources/list` catalog + `resources/read` happy path, -32002 unknown URI, -32602 missing param, catalog-mirrors-BUILTINS parity test |
| 11.1.8 | Resource templates list | RU | `src/openhuman/mcp_server/resources.rs`, `src/openhuman/mcp_server/protocol.rs` | ✅ | `resources/templates/list` returns `{resourceTemplates: []}` (static catalog), tolerates unknown/cursor params |
diff --git a/package.json b/package.json
index 5eb28072c..f325aa858 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"test:coverage": "pnpm --filter openhuman-app test:coverage",
"test:rust": "pnpm --filter openhuman-app test:rust",
"test:rust:e2e": "bash scripts/test-rust-e2e.sh",
+ "test:rust:e2e:coverage": "node scripts/check-domain-e2e-coverage.mjs",
"test:e2e": "pnpm --filter openhuman-app test:e2e:all",
"test:e2e:flows": "pnpm --filter openhuman-app test:e2e:all:flows",
"mascot:render": "pnpm --dir remotion render:runtime-assets",
diff --git a/scripts/check-domain-e2e-coverage.mjs b/scripts/check-domain-e2e-coverage.mjs
new file mode 100644
index 000000000..69ce83098
--- /dev/null
+++ b/scripts/check-domain-e2e-coverage.mjs
@@ -0,0 +1,130 @@
+#!/usr/bin/env node
+import fs from 'node:fs';
+import path from 'node:path';
+
+const ROOT = process.cwd();
+const rawThreshold = process.env.DOMAIN_E2E_COVERAGE_THRESHOLD ?? '90';
+const THRESHOLD = Number(rawThreshold);
+if (!Number.isFinite(THRESHOLD) || THRESHOLD < 0 || THRESHOLD > 100) {
+ // A non-numeric value would make THRESHOLD NaN, turning every `percent <
+ // THRESHOLD` comparison false and silently disabling the gate. Fail loudly.
+ console.error(
+ `Invalid DOMAIN_E2E_COVERAGE_THRESHOLD="${rawThreshold}". Expected a number between 0 and 100.`,
+ );
+ process.exit(2);
+}
+
+const MODULES = [
+ { label: 'config', namespaces: ['config'] },
+ { label: 'credentials', namespaces: ['auth'] },
+ { label: 'app_state', namespaces: ['app_state'] },
+ { label: 'connectivity', namespaces: ['connectivity'] },
+ { label: 'inference', namespaces: ['inference'] },
+ { label: 'agent', namespaces: ['agent'] },
+ { label: 'tools', namespaces: ['tools'] },
+ { label: 'tool_registry', namespaces: ['tool_registry'] },
+ { label: 'approval', namespaces: ['approval'] },
+ { label: 'memory', namespaces: ['memory'] },
+ { label: 'memory_tree', namespaces: ['memory_tree'] },
+ { label: 'memory_sync', namespaces: ['memory_sync'] },
+ { label: 'memory_sources', namespaces: ['memory_sources'] },
+ { label: 'embeddings', namespaces: ['embeddings'] },
+ { label: 'channels', namespaces: ['channels'] },
+ { label: 'composio', namespaces: ['composio'] },
+ { label: 'threads', namespaces: ['threads'] },
+];
+
+const TARGET_NAMESPACES = new Set(MODULES.flatMap((module) => module.namespaces));
+
+function walk(dir, predicate, out = []) {
+ if (!fs.existsSync(dir)) return out;
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ walk(full, predicate, out);
+ } else if (predicate(full)) {
+ out.push(full);
+ }
+ }
+ return out;
+}
+
+function read(file) {
+ return fs.readFileSync(file, 'utf8');
+}
+
+function collectInvokedMethods() {
+ const methods = new Set();
+ const testsDir = path.join(ROOT, 'tests');
+ const files = walk(testsDir, (file) => file.endsWith('_e2e.rs'));
+
+ for (const file of files) {
+ const text = read(file);
+ for (const match of text.matchAll(/"((?:openhuman)\.[A-Za-z0-9_]+)"/g)) {
+ methods.add(match[1]);
+ }
+ }
+
+ return methods;
+}
+
+function collectSchemaMethods() {
+ const methodsByNamespace = new Map([...TARGET_NAMESPACES].map((namespace) => [namespace, new Set()]));
+ const files = walk(path.join(ROOT, 'src', 'openhuman'), (file) => {
+ const normalized = file.split(path.sep).join('/');
+ return file.endsWith('.rs') && /(^|\/)schemas?(\.rs|\/)/.test(normalized);
+ });
+
+ for (const file of files) {
+ const text = read(file);
+ const constNamespace = text.match(/const\s+NAMESPACE:\s*&str\s*=\s*"([a-z_]+)"/)?.[1];
+ for (const match of text.matchAll(/ControllerSchema\s*\{([\s\S]*?)\n\s*\}/g)) {
+ const block = match[1];
+ const namespaceToken = block.match(/namespace:\s*(?:NAMESPACE|"([a-z_]+)")/);
+ const functionName = block.match(/function:\s*"([A-Za-z0-9_]+)"/)?.[1];
+ const namespace = namespaceToken?.[1] ?? (namespaceToken ? constNamespace : undefined);
+ if (!namespace || !functionName || functionName === 'unknown') continue;
+ if (!TARGET_NAMESPACES.has(namespace)) continue;
+ methodsByNamespace.get(namespace).add(`openhuman.${namespace}_${functionName}`);
+ }
+ }
+
+ return methodsByNamespace;
+}
+
+const invoked = collectInvokedMethods();
+const schemas = collectSchemaMethods();
+let failed = false;
+
+console.log(`Domain Rust E2E controller coverage threshold: ${THRESHOLD}%`);
+console.log('');
+console.log('| Module | Namespace(s) | Covered | Percent | Missing |');
+console.log('| --- | --- | ---: | ---: | --- |');
+
+for (const module of MODULES) {
+ const expected = new Set();
+ const covered = new Set();
+ for (const namespace of module.namespaces) {
+ for (const method of schemas.get(namespace) ?? []) expected.add(method);
+ }
+ for (const method of expected) {
+ if (invoked.has(method)) covered.add(method);
+ }
+
+ const missing = [...expected].filter((method) => !covered.has(method)).sort();
+ const percent = expected.size === 0 ? 100 : (covered.size / expected.size) * 100;
+ const missingText = missing.length === 0 ? '-' : missing.join('
');
+
+ console.log(
+ `| ${module.label} | ${module.namespaces.join(', ')} | ${covered.size}/${expected.size} | ${percent.toFixed(1)}% | ${missingText} |`,
+ );
+
+ if (expected.size > 0 && percent < THRESHOLD) failed = true;
+}
+
+if (failed) {
+ console.error(`\nDomain Rust E2E controller coverage is below ${THRESHOLD}% for one or more modules.`);
+ process.exit(1);
+}
+
+console.log(`\nAll named modules meet the ${THRESHOLD}% Rust E2E controller coverage threshold.`);
diff --git a/scripts/test-rust-e2e.sh b/scripts/test-rust-e2e.sh
index f863a1683..6afd1e6e2 100755
--- a/scripts/test-rust-e2e.sh
+++ b/scripts/test-rust-e2e.sh
@@ -34,15 +34,31 @@ ALL_E2E_SUITES=(
agent_retrieval_e2e
autocomplete_memory_e2e
calendar_grounding_e2e
+ config_auth_app_state_connectivity_e2e
+ composio_post_oauth_retry_e2e
+ cwd_jail_e2e
+ domain_modules_e2e
+ embeddings_rpc_e2e
+ inference_provider_e2e
json_rpc_e2e
keyring_secretstore_fresh_e2e
keyring_secretstore_e2e
linux_cef_deb_runtime_e2e
live_routing_e2e
+ mcp_registry_e2e
+ mcp_setup_e2e
+ memory_artifacts_e2e
memory_graph_sync_e2e
memory_roundtrip_e2e
+ memory_sources_e2e
+ memory_tree_summarizer_e2e
+ memory_tree_walk_e2e
+ ollama_embeddings_fallback_e2e
screen_intelligence_vision_e2e
subconscious_e2e
+ vault_sync_e2e
+ worker_b_domain_e2e
+ worker_c_modules_e2e
)
# Parse args: --suite can be passed multiple times to filter.
diff --git a/src/openhuman/agent/harness/mod.rs b/src/openhuman/agent/harness/mod.rs
index 1fee0268f..9f76fc0f4 100644
--- a/src/openhuman/agent/harness/mod.rs
+++ b/src/openhuman/agent/harness/mod.rs
@@ -19,7 +19,7 @@
//! - **[`fork_context`]**: Task-local storage for parent context sharing.
//! - **[`interrupt`]**: Infrastructure for graceful cancellation of agent loops.
-pub(crate) mod archivist;
+pub mod archivist;
pub(crate) mod builtin_definitions;
mod credentials;
pub mod definition;
diff --git a/src/openhuman/agent/harness/subagent_runner/extract_tool.rs b/src/openhuman/agent/harness/subagent_runner/extract_tool.rs
index 1d07f216d..65dbf94b4 100644
--- a/src/openhuman/agent/harness/subagent_runner/extract_tool.rs
+++ b/src/openhuman/agent/harness/subagent_runner/extract_tool.rs
@@ -44,11 +44,27 @@ const EXTRACT_MODEL_ID: &str = "summarization-v1";
/// answer, without straying into creative territory.
const EXTRACT_TEMPERATURE: f64 = 0.2;
-/// Char budget per extraction call. Chosen so a single chunk + prompt
-/// scaffolding + output stays well below the extraction model's context
-/// window (~196k tokens) — at ~4 chars/token that leaves comfortable
-/// headroom for the extraction contract and response.
-const EXTRACT_CHUNK_CHAR_BUDGET: usize = 60_000;
+/// Char budget per extraction call, derived from the extraction model's
+/// context window. A payload at or under this budget is extracted in a single
+/// shot over its **entire** content — higher quality than the chunk+concat
+/// fallback, which has no reduce stage and can miss facts that span a chunk
+/// boundary or need global context. `summarization-v1` resolves to a
+/// long-context flash model (~1M tokens), so this is large; only payloads that
+/// exceed it fall back to parallel chunked extraction. Headroom is reserved for
+/// the extraction contract, the query, and the response.
+fn extract_chunk_char_budget() -> usize {
+ /// Fallback window (tokens) when the model id is unknown to the registry.
+ const FALLBACK_WINDOW_TOKENS: u64 = 128_000;
+ /// Approximate chars per token used for budgeting.
+ const CHARS_PER_TOKEN: u64 = 4;
+ /// Fraction of the window spent on the payload slice; the remainder covers
+ /// the prompt scaffolding, query, and model response.
+ const USABLE_PCT: u64 = 70;
+
+ let window_tokens = crate::openhuman::inference::context_window_for_model(EXTRACT_MODEL_ID)
+ .unwrap_or(FALLBACK_WINDOW_TOKENS);
+ (window_tokens * USABLE_PCT / 100 * CHARS_PER_TOKEN) as usize
+}
/// System prompt fed to the provider on every `extract_from_result`
/// call. Lifted in spirit from the old `summarizer` agent's prompt but
@@ -168,8 +184,16 @@ impl Tool for ExtractFromResultTool {
}
};
+ // Allow test harnesses to lower the chunk budget so multi-chunk
+ // extraction can be exercised on compacted payloads. Never consulted
+ // in production (env var absent).
+ let effective_chunk_budget = std::env::var("OPENHUMAN_TEST_EXTRACT_CHUNK_BUDGET")
+ .ok()
+ .and_then(|v| v.parse::().ok())
+ .unwrap_or_else(extract_chunk_char_budget);
+
// Fast path: payload fits in a single provider turn.
- if cached.content.len() <= EXTRACT_CHUNK_CHAR_BUDGET {
+ if cached.content.len() <= effective_chunk_budget {
tracing::debug!(
tool = %cached.tool_name,
bytes = cached.content.len(),
@@ -195,12 +219,12 @@ impl Tool for ExtractFromResultTool {
// failure when the upstream provider stalls. For
// listing/extraction queries concatenation is equivalent; for
// top-N / global-ordering queries the caller can post-process.
- let chunks = chunk_content(&cached.content, EXTRACT_CHUNK_CHAR_BUDGET);
+ let chunks = chunk_content(&cached.content, effective_chunk_budget);
tracing::info!(
tool = %cached.tool_name,
total_bytes = cached.content.len(),
chunk_count = chunks.len(),
- chunk_budget = EXTRACT_CHUNK_CHAR_BUDGET,
+ chunk_budget = effective_chunk_budget,
"[extract_from_result] chunked extraction"
);
diff --git a/src/openhuman/agent/harness/subagent_runner/ops.rs b/src/openhuman/agent/harness/subagent_runner/ops.rs
index 97dd61853..18cd8a239 100644
--- a/src/openhuman/agent/harness/subagent_runner/ops.rs
+++ b/src/openhuman/agent/harness/subagent_runner/ops.rs
@@ -1387,7 +1387,15 @@ fn apply_handoff(
cleaned
};
let tokens = cleaned.len().div_ceil(4);
- if !skip_cleaning && tokens > HANDOFF_OVERSIZE_THRESHOLD_TOKENS {
+ // Allow test harnesses (lib tests AND integration test binaries) to lower
+ // the threshold so the handoff path can be exercised on payloads that
+ // survive tokenjuice's compaction cap. Never consulted in production
+ // (the env var is absent) so there is zero runtime cost.
+ let effective_threshold = std::env::var("OPENHUMAN_TEST_HANDOFF_THRESHOLD_TOKENS")
+ .ok()
+ .and_then(|v| v.parse::().ok())
+ .unwrap_or(HANDOFF_OVERSIZE_THRESHOLD_TOKENS);
+ if !skip_cleaning && tokens > effective_threshold {
let id = cache.store(tool_name.to_string(), cleaned.clone());
let placeholder = build_handoff_placeholder(tool_name, &id, &cleaned);
tracing::info!(
@@ -1396,7 +1404,7 @@ fn apply_handoff(
tool = %tool_name,
raw_tokens = tokens,
raw_bytes = cleaned.len(),
- threshold_tokens = HANDOFF_OVERSIZE_THRESHOLD_TOKENS,
+ threshold_tokens = effective_threshold,
result_id = %id,
"[subagent_runner:handoff] stashed oversized tool output; substituted placeholder into history"
);
diff --git a/src/openhuman/app_state/schemas.rs b/src/openhuman/app_state/schemas.rs
index d051c737e..bae42fa99 100644
--- a/src/openhuman/app_state/schemas.rs
+++ b/src/openhuman/app_state/schemas.rs
@@ -1,3 +1,4 @@
+use serde::de::{self, DeserializeOwned};
use serde::Deserialize;
use serde_json::{Map, Value};
@@ -9,9 +10,9 @@ use super::ops::StoredAppStatePatch;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct UpdateLocalStateParams {
- #[serde(default)]
+ #[serde(default, deserialize_with = "deserialize_nullable_patch")]
encryption_key: Option