use super::{
common::CommonAction,
protocol_trait::{ActionResult, Server},
};
use crate::protocol::log_action_result;
use crate::state::app_state::AppState;
use anyhow::Result;
use tracing::{debug, error, warn};
pub const FAILED_ACTION_TYPE_PREFIX: &str = "FAILED: ";
pub const MAX_SERVER_MEMORY_CHARS: usize = 8000;
const MEMORY_TRIMMED_NOTICE: &str = "[older memory dropped: over the size limit]";
pub fn bound_server_memory(memory: String) -> String {
if memory.len() <= MAX_SERVER_MEMORY_CHARS {
return memory;
}
let budget = MAX_SERVER_MEMORY_CHARS.saturating_sub(MEMORY_TRIMMED_NOTICE.len() + 1);
let mut kept: Vec<&str> = Vec::new();
let mut size = 0usize;
for line in memory.lines().rev() {
let cost = line.len() + 1;
if size + cost > budget {
break;
}
size += cost;
kept.push(line);
}
kept.reverse();
if kept.is_empty() {
let start = memory.len().saturating_sub(budget);
let start = memory
.char_indices()
.map(|(i, _)| i)
.find(|i| *i >= start)
.unwrap_or(memory.len());
return format!("{}\n{}", MEMORY_TRIMMED_NOTICE, &memory[start..]);
}
format!("{}\n{}", MEMORY_TRIMMED_NOTICE, kept.join("\n"))
}
#[derive(Clone, Debug, serde::Serialize)]
pub struct ActionFailure {
pub index: usize,
pub action: String,
pub error: String,
}
pub struct ExecutionResult {
pub messages: Vec<String>,
pub protocol_results: Vec<ActionResult>,
pub protocol_result_actions: Vec<String>,
pub raw_actions: Vec<serde_json::Value>,
pub failures: Vec<ActionFailure>,
}
impl Default for ExecutionResult {
fn default() -> Self {
Self::new()
}
}
impl ExecutionResult {
pub fn new() -> Self {
Self {
messages: Vec::new(),
protocol_results: Vec::new(),
protocol_result_actions: Vec::new(),
raw_actions: Vec::new(),
failures: Vec::new(),
}
}
pub fn add_message(&mut self, message: String) {
self.messages.push(message);
}
pub fn add_protocol_result(&mut self, result: ActionResult) {
self.add_named_protocol_result(String::new(), result);
}
pub fn add_named_protocol_result(&mut self, action_name: String, result: ActionResult) {
self.protocol_results.push(result);
self.protocol_result_actions.push(action_name);
}
pub fn chosen_reply(&self, preferred: &[&str]) -> Option<usize> {
let writes = |i: &usize| !self.protocol_results[*i].get_all_output().is_empty();
(0..self.protocol_results.len())
.filter(writes)
.find(|i| {
self.protocol_result_actions
.get(*i)
.is_some_and(|name| preferred.contains(&name.as_str()))
})
.or_else(|| (0..self.protocol_results.len()).find(writes))
}
pub fn add_failure(
&mut self,
index: usize,
action: impl Into<String>,
error: impl Into<String>,
) {
self.failures.push(ActionFailure {
index,
action: action.into(),
error: error.into(),
});
}
pub fn has_failures(&self) -> bool {
!self.failures.is_empty()
}
pub fn failure_summary(&self) -> Option<String> {
if self.failures.is_empty() {
return None;
}
let parts: Vec<String> = self
.failures
.iter()
.map(|f| format!("{} ({})", f.action, f.error))
.collect();
Some(parts.join("; "))
}
pub fn access_log_actions(&self) -> Vec<serde_json::Value> {
if self.failures.is_empty() {
return self.raw_actions.clone();
}
let mut out = self.raw_actions.clone();
for failure in &self.failures {
let original = out
.get(failure.index)
.cloned()
.unwrap_or(serde_json::Value::Null);
let envelope = serde_json::json!({
"type": format!("{}{}", FAILED_ACTION_TYPE_PREFIX, failure.action),
"error": failure.error,
"action": original,
});
if let Some(slot) = out.get_mut(failure.index) {
*slot = envelope;
} else {
out.push(envelope);
}
}
out
}
}
pub async fn execute_actions(
actions: Vec<serde_json::Value>,
state: &AppState,
protocol: Option<&dyn Server>,
server_id: Option<crate::state::ServerId>,
client_id: Option<crate::state::ClientId>,
) -> Result<ExecutionResult> {
if !crate::utils::json_budget::within_values_budget(
&actions,
crate::scripting::event_handler::MAX_INTERPOLATION_BYTES,
crate::scripting::event_handler::MAX_INTERPOLATION_NODES,
crate::scripting::event_handler::MAX_INTERPOLATION_DEPTH,
) {
for action in actions {
crate::utils::json_budget::drop_iteratively(action);
}
anyhow::bail!("action batch exceeds the shared JSON budget");
}
let mut result = ExecutionResult::new();
result.raw_actions = actions.clone();
for (i, action) in actions.iter().enumerate() {
debug!(
"Executing action {}: {:?}",
i,
crate::utils::redact::redact_sensitive(action)
);
let action_name = action
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
if action_name == "create_pipe" || action_name == "remove_pipe" {
if let Err(e) =
crate::pipe::execute_pipe_action(&action_name, action, state, server_id).await
{
warn!(
"Pipe action '{}' failed: {} (action: {})",
action_name, e, action
);
result.add_failure(i, action_name, e.to_string());
}
continue;
}
if let Ok(common_action) = CommonAction::from_json(action) {
if let Err(e) =
execute_common_action(common_action, state, &mut result, server_id, client_id).await
{
error!(
"Action {} '{}' failed: {} (action: {})",
i, action_name, e, action
);
result.add_failure(i, action_name, e.to_string());
}
continue;
}
if let Some(proto) = protocol {
match proto
.execute_action_with_state(action.clone(), state.clone(), server_id)
.await
{
Ok(action_result) => {
let action_def = proto
.get_sync_actions()
.into_iter()
.chain(proto.get_async_actions(state).into_iter())
.find(|a| a.name == action_name);
log_action_result(
&action_name,
action,
&action_result,
action_def
.as_ref()
.and_then(|def| def.log_template.as_ref()),
None, );
result.add_named_protocol_result(action_name.clone(), action_result);
continue;
}
Err(e) => {
error!(
"Action {} '{}' failed on protocol {}: {} — the peer receives the \
protocol default instead (action: {})",
i,
action_name,
proto.protocol_name(),
e,
action
);
result.add_failure(i, action_name, e.to_string());
continue;
}
}
}
let detail = "unknown action type and no protocol in context".to_string();
error!(
"Action {} '{}' skipped: {} ({})",
i, action_name, detail, action
);
result.add_failure(i, action_name, detail);
}
if let Some(summary) = result.failure_summary() {
error!(
"{} of {} action(s) failed: {}",
result.failures.len(),
actions.len(),
summary
);
}
Ok(result)
}
async fn execute_common_action(
action: CommonAction,
state: &AppState,
_result: &mut ExecutionResult,
server_id: Option<crate::state::ServerId>,
client_id: Option<crate::state::ClientId>,
) -> Result<()> {
match action {
CommonAction::ShowMessage { .. } => {
}
CommonAction::OpenServer { .. } => {
warn!("open_server action cannot be executed by action executor - must be handled by caller");
}
CommonAction::CloseServer { .. } => {
warn!("close_server action cannot be executed by action executor - must be handled by caller");
}
CommonAction::CloseAllServers => {
warn!("close_all_servers action cannot be executed by action executor - must be handled by caller");
}
CommonAction::UpdateInstruction { .. } => {
warn!("update_instruction action cannot be executed by action executor - must be handled by caller");
}
CommonAction::ChangeModel { .. } => {
warn!("change_model action cannot be executed by action executor - must be handled by caller");
}
CommonAction::SetMemory { value } => {
if server_id.is_none() {
if let Some(cid) = client_id {
let bounded = bound_server_memory(value);
debug!(
"Client #{} memory set ({} chars)",
cid.as_u32(),
bounded.len()
);
state
.with_client_mut(cid, |client| client.memory = bounded)
.await;
return Ok(());
}
}
let sid = server_id.or_else(|| state.get_first_server_id_sync());
if let Some(server_id) = sid {
let bounded = bound_server_memory(value);
debug!(
"Server #{} memory set ({} chars)",
server_id.as_u32(),
bounded.len()
);
state.set_memory(server_id, bounded).await;
}
}
CommonAction::AppendMemory { value } => {
if server_id.is_none() {
if let Some(cid) = client_id {
let bounded = state
.with_client_mut(cid, |client| {
let combined = if client.memory.is_empty() {
value
} else {
format!("{}\n{}", client.memory, value)
};
client.memory = bound_server_memory(combined);
client.memory.len()
})
.await;
if let Some(len) = bounded {
debug!("Client #{} memory appended ({} chars)", cid.as_u32(), len);
}
return Ok(());
}
}
let sid = server_id.or_else(|| state.get_first_server_id_sync());
if let Some(server_id) = sid {
let current = state.get_memory(server_id).await.unwrap_or_default();
let new_memory = if current.is_empty() {
value
} else {
format!("{}\n{}", current, value)
};
let bounded = bound_server_memory(new_memory);
debug!(
"Server #{} memory appended ({} chars)",
server_id.as_u32(),
bounded.len()
);
state.set_memory(server_id, bounded).await;
}
}
CommonAction::AppendToLog {
output_name,
content,
} => {
let sid = server_id.or_else(|| state.get_first_server_id_sync());
if let Some(server_id) = sid {
let log_path = state
.with_server_mut(server_id, |server| {
server.get_or_create_log_path(&output_name)
})
.await;
let log_path = match log_path {
Some(Ok(path)) => Some(path),
Some(Err(e)) => {
warn!("append_to_log refused: {e}");
None
}
None => None,
};
if let Some(log_path) = log_path {
match tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.await
{
Ok(mut file) => {
use tokio::io::AsyncWriteExt;
let log_line = format!("{}\n", content);
if let Err(e) = file.write_all(log_line.as_bytes()).await {
warn!("Failed to write to log file {:?}: {}", log_path, e);
} else {
debug!("Appended to log file {:?}", log_path);
}
}
Err(e) => {
warn!("Failed to open log file {:?}: {}", log_path, e);
}
}
} else {
warn!("No server found to append log for");
}
}
}
CommonAction::ScheduleTask { .. } | CommonAction::CancelTask { .. } => {
}
CommonAction::OpenClient { .. }
| CommonAction::CloseClient { .. }
| CommonAction::CloseAllClients
| CommonAction::CloseConnectionById { .. }
| CommonAction::ReconnectClient { .. }
| CommonAction::UpdateClientInstruction { .. } => {
}
CommonAction::UpdateServer { .. } | CommonAction::UpdateClient { .. } => {
warn!("update_server/update_client action must be handled by caller");
}
CommonAction::ProvideFeedback { feedback } => {
if let Some(sid) = server_id {
state
.add_server_feedback(sid, feedback)
.await
.unwrap_or_else(|e| {
warn!("Failed to add server feedback: {}", e);
});
debug!("Server #{} feedback accumulated", sid.as_u32());
} else if let Some(cid) = client_id {
state
.add_client_feedback(cid, feedback)
.await
.unwrap_or_else(|e| {
warn!("Failed to add client feedback: {}", e);
});
debug!("Client #{} feedback accumulated", cid.as_u32());
} else {
warn!("provide_feedback action called without server_id or client_id context");
}
}
#[cfg(feature = "sqlite")]
CommonAction::CreateDatabase { .. } | CommonAction::DeleteDatabase { .. } => {
}
}
Ok(())
}
pub fn extract_server_management_actions(actions: &[serde_json::Value]) -> Vec<CommonAction> {
actions
.iter()
.filter_map(|action| {
if let Ok(common_action) = CommonAction::from_json(action) {
match common_action {
CommonAction::OpenServer { .. }
| CommonAction::CloseServer { .. }
| CommonAction::UpdateInstruction { .. }
| CommonAction::ChangeModel { .. } => Some(common_action),
_ => None,
}
} else {
None
}
})
.collect()
}