Skip to content

Commit abe8a63

Browse files
bb-connorclaude
andauthored
refactor(agent): split openclaw/manager.rs into focused submodules (#344)
`openclaw/manager.rs` was a 2,830-line file mixing the OpenClawManager struct/impl, the tungstenite WebSocket loop, identity loading, Ed25519 signing, URL/IP validation, exponential backoff, and ~1,090 lines of integration tests. Split it into focused siblings, all production files now under 500 lines: - manager.rs (315) - OpenClawManager struct + public API surface - connection.rs (454) - WebSocket session/reconnect loop - runtime_state.rs (187) - per-gateway runtime snapshot mutators - identity.rs (241) - device identity loading + filesystem fallback - device_proof.rs (202) - Ed25519 signing + payload + scope validation - url_validation.rs (150) - URL parsing, DNS resolution, IP allowlist - dto.rs (110) - public DTOs (status, snapshot, view, requests, events) - command.rs (82) - `openclaw` external binary invocation - backoff.rs (40) - reconnect attempt + jittered sleep helpers - util.rs (34) - shared scalar helpers (now_ms, normalize_*) - session.rs (26) - internal channel types - tests.rs (1116) - integration tests (tests-only, may exceed 600) `mod.rs` declares the new submodules and preserves the existing public re-exports. `manager::GatewayView` is re-exported on the manager path so `api_server.rs` `crate::openclaw::manager::GatewayView` keeps resolving. All 27 openclaw tests still pass. Co-authored-by: bb-connor <bb-connor@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 92cbfd7 commit abe8a63

13 files changed

Lines changed: 2680 additions & 2542 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//! Reconnect backoff and connection-stability helpers.
2+
3+
use std::time::Duration;
4+
5+
pub(super) fn was_connected_long_enough(
6+
connected_at_ms: Option<u64>,
7+
stable_reset: Duration,
8+
now_ms_value: u64,
9+
) -> bool {
10+
connected_at_ms.is_some_and(|connected_at| {
11+
now_ms_value.saturating_sub(connected_at) >= stable_reset.as_millis() as u64
12+
})
13+
}
14+
15+
pub(super) fn next_reconnect_attempt(current_attempt: u32, was_stable: bool) -> u32 {
16+
if was_stable {
17+
1
18+
} else {
19+
current_attempt.saturating_add(1)
20+
}
21+
}
22+
23+
/// Compute the next reconnect sleep duration, applying exponential growth plus
24+
/// ±20% jitter to prevent thundering-herd reconnect storms.
25+
pub(super) fn compute_reconnect_sleep_ms(reconnect_attempt: u32) -> u64 {
26+
let base_backoff_ms = (400.0_f64 * 1.6_f64.powi(reconnect_attempt as i32)).round() as u64;
27+
let base_backoff_ms = base_backoff_ms.clamp(250, 12_000);
28+
let jitter_range = (base_backoff_ms as f64 * 0.2) as u64;
29+
let jitter = if jitter_range > 0 {
30+
use std::collections::hash_map::DefaultHasher;
31+
use std::hash::{Hash, Hasher};
32+
let mut hasher = DefaultHasher::new();
33+
std::time::SystemTime::now().hash(&mut hasher);
34+
reconnect_attempt.hash(&mut hasher);
35+
(hasher.finish() % (jitter_range * 2 + 1)) as i64 - jitter_range as i64
36+
} else {
37+
0
38+
};
39+
(base_backoff_ms as i64 + jitter).max(100) as u64
40+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//! Invocation of the `openclaw` external binary with JSON output parsing.
2+
3+
use anyhow::Result;
4+
use serde_json::Value;
5+
6+
pub(super) fn extract_json_payload(output: &str) -> Result<Value> {
7+
let mut saw_candidate = false;
8+
let mut best: Option<(Value, usize)> = None;
9+
let mut last_error: Option<String> = None;
10+
11+
for (idx, ch) in output.char_indices() {
12+
if ch != '{' && ch != '[' {
13+
continue;
14+
}
15+
saw_candidate = true;
16+
let json = &output[idx..];
17+
let deser = serde_json::Deserializer::from_str(json);
18+
let mut stream = deser.into_iter::<Value>();
19+
match stream.next() {
20+
Some(Ok(value)) => {
21+
let remainder = &json[stream.byte_offset()..];
22+
let remainder_len = remainder.trim().len();
23+
if remainder_len == 0 {
24+
return Ok(value);
25+
}
26+
27+
match &best {
28+
Some((_, best_len)) if remainder_len >= *best_len => {}
29+
_ => best = Some((value, remainder_len)),
30+
}
31+
}
32+
Some(Err(err)) => {
33+
last_error = Some(format!("Failed to parse OpenClaw JSON: {}", err));
34+
}
35+
None => {}
36+
}
37+
}
38+
39+
if let Some((value, _)) = best {
40+
return Ok(value);
41+
}
42+
43+
Err(anyhow::anyhow!(last_error.unwrap_or_else(|| {
44+
if saw_candidate {
45+
"Failed to parse OpenClaw JSON".to_string()
46+
} else {
47+
"OpenClaw returned no JSON payload".to_string()
48+
}
49+
})))
50+
}
51+
52+
pub(super) async fn run_openclaw_json(args: Vec<String>) -> Result<Value> {
53+
let output = tokio::task::spawn_blocking(move || {
54+
let mut full_args = vec!["--no-color".to_string()];
55+
full_args.extend(args);
56+
57+
std::process::Command::new("openclaw")
58+
.args(full_args)
59+
.output()
60+
.map_err(|e| anyhow::anyhow!("Failed to execute openclaw: {}", e))
61+
})
62+
.await
63+
.map_err(|e| anyhow::anyhow!("Failed to join openclaw task: {}", e))??;
64+
65+
if !output.status.success() {
66+
let stderr = String::from_utf8_lossy(&output.stderr);
67+
let stdout = String::from_utf8_lossy(&output.stdout);
68+
return Err(anyhow::anyhow!(
69+
"OpenClaw exited with {}: {}{}",
70+
output.status,
71+
stderr.trim(),
72+
if stderr.trim().is_empty() && !stdout.trim().is_empty() {
73+
format!(" (stdout: {})", stdout.trim())
74+
} else {
75+
"".to_string()
76+
}
77+
));
78+
}
79+
80+
let stdout = String::from_utf8_lossy(&output.stdout);
81+
extract_json_payload(&stdout)
82+
}

0 commit comments

Comments
 (0)