Skip to content

Commit 9323edc

Browse files
committed
fix config persistence, PowerShell bypass, WS 404 race, feishu panic, Revolt self-hosted
1 parent 0bccba1 commit 9323edc

File tree

8 files changed

+358
-15
lines changed

8 files changed

+358
-15
lines changed

crates/openfang-api/src/channel_bridge.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,8 +1470,15 @@ pub async fn start_channel_bridge_with_config(
14701470
// Revolt
14711471
if let Some(ref rv_config) = config.revolt {
14721472
if let Some(token) = read_token(&rv_config.bot_token_env, "Revolt") {
1473-
let adapter = Arc::new(RevoltAdapter::new(token));
1474-
adapters.push((adapter, rv_config.default_agent.clone()));
1473+
let mut adapter = RevoltAdapter::with_urls(
1474+
token,
1475+
rv_config.api_url.clone(),
1476+
rv_config.ws_url.clone(),
1477+
);
1478+
if !rv_config.allowed_channels.is_empty() {
1479+
adapter.set_allowed_channels(rv_config.allowed_channels.clone());
1480+
}
1481+
adapters.push((Arc::new(adapter), rv_config.default_agent.clone()));
14751482
}
14761483
}
14771484

crates/openfang-api/src/routes.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5847,6 +5847,11 @@ pub async fn patch_agent(
58475847
// Persist updated entry to SQLite
58485848
if let Some(entry) = state.kernel.registry.get(agent_id) {
58495849
let _ = state.kernel.memory.save_agent(&entry);
5850+
5851+
// Write updated manifest to agent.toml on disk so disk doesn't override
5852+
// dashboard changes on next boot (#996, #1018).
5853+
state.kernel.persist_manifest_to_disk(agent_id);
5854+
58505855
(
58515856
StatusCode::OK,
58525857
Json(
@@ -8992,6 +8997,10 @@ pub async fn patch_agent_config(
89928997
}
89938998
}
89948999

9000+
// Write updated manifest to agent.toml on disk so disk doesn't override
9001+
// dashboard changes on next boot (#996, #1018).
9002+
state.kernel.persist_manifest_to_disk(agent_id);
9003+
89959004
(
89969005
StatusCode::OK,
89979006
Json(serde_json::json!({"status": "ok", "agent_id": id})),

crates/openfang-api/src/ws.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,29 @@ pub async fn agent_ws(
197197
}
198198
};
199199

200-
// Verify agent exists
201-
if state.kernel.registry.get(agent_id).is_none() {
202-
return axum::http::StatusCode::NOT_FOUND.into_response();
200+
// Verify agent exists.
201+
// Retry up to 5 times with 200ms backoff to handle a timing race where
202+
// the client connects before the agent finishes registering (#804).
203+
{
204+
let mut found = state.kernel.registry.get(agent_id).is_some();
205+
if !found {
206+
for attempt in 1..=4 {
207+
debug!(
208+
agent_id = %id,
209+
attempt,
210+
"Agent not found yet, retrying in 200ms"
211+
);
212+
tokio::time::sleep(Duration::from_millis(200)).await;
213+
if state.kernel.registry.get(agent_id).is_some() {
214+
found = true;
215+
break;
216+
}
217+
}
218+
}
219+
if !found {
220+
warn!(agent_id = %id, "Agent not found after 5 lookup attempts");
221+
return axum::http::StatusCode::NOT_FOUND.into_response();
222+
}
203223
}
204224

205225
let id_str = id.clone();

crates/openfang-channels/src/feishu.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,14 @@ impl DedupCache {
160160

161161
/// Returns `true` if the ID was already seen (duplicate).
162162
fn check_and_insert(&self, id: &str) -> bool {
163-
let mut ids = self.ids.lock().unwrap();
163+
let mut ids = match self.ids.lock() {
164+
Ok(guard) => guard,
165+
Err(poisoned) => {
166+
// Recover from a poisoned mutex rather than panicking.
167+
warn!("Dedup cache mutex was poisoned, recovering");
168+
poisoned.into_inner()
169+
}
170+
};
164171
if ids.iter().any(|s| s == id) {
165172
return true;
166173
}
@@ -1030,7 +1037,10 @@ fn combine_payload(
10301037
*entry = vec![Vec::new(); sum];
10311038
}
10321039

1033-
entry[seq] = payload;
1040+
match entry.get_mut(seq) {
1041+
Some(slot) => *slot = payload,
1042+
None => return None,
1043+
}
10341044

10351045
if entry.iter().any(|part| part.is_empty()) {
10361046
return None;
@@ -1105,7 +1115,10 @@ fn extract_text_from_post(content: &serde_json::Value) -> Option<String> {
11051115
let mut text_parts = Vec::new();
11061116

11071117
for paragraph in paragraphs {
1108-
let elements = paragraph.as_array()?;
1118+
let elements = match paragraph.as_array() {
1119+
Some(elems) => elems,
1120+
None => continue,
1121+
};
11091122
for element in elements {
11101123
let tag = element["tag"].as_str().unwrap_or("");
11111124
match tag {
@@ -1165,8 +1178,10 @@ fn should_respond_in_group(text: &str, mentions: &serde_json::Value, bot_names:
11651178

11661179
/// Strip @mention placeholders from text (`@_user_N` format).
11671180
fn strip_mention_placeholders(text: &str) -> String {
1168-
let re = regex_lite::Regex::new(r"@_user_\d+\s*").unwrap();
1169-
re.replace_all(text, "").trim().to_string()
1181+
match regex_lite::Regex::new(r"@_user_\d+\s*") {
1182+
Ok(re) => re.replace_all(text, "").trim().to_string(),
1183+
Err(_) => text.trim().to_string(),
1184+
}
11701185
}
11711186

11721187
/// Decrypt an AES-256-CBC encrypted event payload.

crates/openfang-channels/src/revolt.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ impl RevoltAdapter {
9494
adapter
9595
}
9696

97+
/// Set allowed channel IDs (empty = all channels the bot is in).
98+
pub fn set_allowed_channels(&mut self, channels: Vec<String>) {
99+
self.allowed_channels = channels;
100+
}
101+
97102
/// Add the bot token header to a request builder.
98103
fn auth_header(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
99104
builder.header("x-bot-token", self.bot_token.as_str())

crates/openfang-kernel/src/kernel.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2986,6 +2986,36 @@ impl OpenFangKernel {
29862986
);
29872987
}
29882988

2989+
/// Persist an agent's manifest to its `agent.toml` on disk so that
2990+
/// dashboard-driven config changes (model, provider, fallback, etc.)
2991+
/// survive a restart. The on-disk file lives at
2992+
/// `<home_dir>/agents/<name>/agent.toml`.
2993+
///
2994+
/// This is best-effort: a failure to write is logged but does not
2995+
/// propagate as an error — the authoritative copy lives in SQLite.
2996+
pub fn persist_manifest_to_disk(&self, agent_id: AgentId) {
2997+
if let Some(entry) = self.registry.get(agent_id) {
2998+
let dir = self.config.home_dir.join("agents").join(&entry.name);
2999+
let toml_path = dir.join("agent.toml");
3000+
match toml::to_string_pretty(&entry.manifest) {
3001+
Ok(toml_str) => {
3002+
if let Err(e) = std::fs::create_dir_all(&dir) {
3003+
warn!(agent = %entry.name, "Failed to create agent dir for manifest persist: {e}");
3004+
return;
3005+
}
3006+
if let Err(e) = std::fs::write(&toml_path, toml_str) {
3007+
warn!(agent = %entry.name, "Failed to persist manifest to disk: {e}");
3008+
} else {
3009+
debug!(agent = %entry.name, path = %toml_path.display(), "Persisted manifest to disk");
3010+
}
3011+
}
3012+
Err(e) => {
3013+
warn!(agent = %entry.name, "Failed to serialize manifest to TOML: {e}");
3014+
}
3015+
}
3016+
}
3017+
}
3018+
29893019
/// Switch an agent's model.
29903020
///
29913021
/// When `explicit_provider` is `Some`, that provider name is used as-is
@@ -3072,6 +3102,9 @@ impl OpenFangKernel {
30723102
let _ = self.memory.save_agent(&entry);
30733103
}
30743104

3105+
// Write updated manifest to agent.toml so changes survive restart (#996, #1018)
3106+
self.persist_manifest_to_disk(agent_id);
3107+
30753108
// Clear canonical session to prevent memory poisoning from old model's responses
30763109
let _ = self.memory.delete_canonical_session(agent_id);
30773110
debug!(agent_id = %agent_id, "Cleared canonical session after model switch");

0 commit comments

Comments
 (0)