huskies: merge 1144 story Gateway trampoline-restart: detached helper survives the gateway's own death

This commit is contained in:
dave
2026-05-19 18:13:26 +00:00
parent 20ec690e22
commit de638603cd
12 changed files with 656 additions and 1 deletions
@@ -19,6 +19,28 @@ use super::super::verification::check_sender_verified;
use super::handle_message;
/// Return `true` when the message is a "rebuild gateway" command addressed to the bot.
///
/// The command is recognised case-insensitively as `rebuild gateway` after stripping
/// the bot mention prefix so both `@Timmy rebuild gateway` and `Timmy rebuild gateway`
/// match.
fn extract_rebuild_gateway_command(message: &str, bot_name: &str, bot_user_id: &str) -> bool {
let stripped = crate::chat::util::strip_bot_mention(message, bot_name, bot_user_id);
let trimmed = stripped
.trim()
.trim_start_matches(|c: char| !c.is_alphanumeric());
let (cmd, rest) = match trimmed.split_once(char::is_whitespace) {
Some((c, r)) => (c, r.trim()),
None => return false,
};
cmd.eq_ignore_ascii_case("rebuild")
&& rest
.split_whitespace()
.next()
.map(|w| w.eq_ignore_ascii_case("gateway"))
.unwrap_or(false)
}
/// Evaluate a `switch <arg>` command against the live project store.
///
/// Reads valid project names from the store at call time so newly added
@@ -657,6 +679,87 @@ pub(in crate::chat::transport::matrix::bot) async fn on_room_message(
return;
}
// In gateway mode, intercept "rebuild gateway" and route it through the
// detached trampoline so the process swap survives any bash-tool kill cascade.
if ctx.gateway_active_project.is_some()
&& extract_rebuild_gateway_command(
&user_message,
&ctx.services.bot_name,
ctx.matrix_user_id.as_str(),
)
{
slog!("[matrix-bot] Handling 'rebuild gateway' command from {sender}");
let ack = "Rebuilding gateway\u{2026} this may take a moment.";
let ack_html = markdown_to_html(ack);
if let Ok(msg_id) = ctx
.transport
.send_message(&room_id_str, ack, &ack_html)
.await
&& let Ok(event_id) = msg_id.parse()
{
ctx.bot_sent_event_ids.lock().await.insert(event_id);
}
let config_dir = ctx.services.project_root.clone();
let gateway_port: u16 = ctx.gateway_port.unwrap_or(3000);
match crate::gateway::rebuild::rebuild_gateway(&config_dir, gateway_port).await {
Ok(()) => {
// Trampoline is running detached — it kills this gateway and starts
// the new one, which will post "gateway X.Y.Z ready" on startup.
}
Err(e) => {
let msg = format!("Gateway rebuild failed: {e}");
let html = markdown_to_html(&msg);
if let Ok(msg_id) = ctx.transport.send_message(&room_id_str, &msg, &html).await
&& let Ok(event_id) = msg_id.parse()
{
ctx.bot_sent_event_ids.lock().await.insert(event_id);
}
}
}
return;
}
// In gateway mode, intercept "rebuild gateway" before the plain "rebuild"
// handler so the trampoline path is used instead of a direct re-exec.
if ctx.gateway_port.is_some()
&& super::super::super::rebuild::extract_rebuild_gateway_command(
&user_message,
&ctx.services.bot_name,
ctx.matrix_user_id.as_str(),
)
.is_some()
{
slog!("[matrix-bot] Handling rebuild-gateway command from {sender}");
let ack = "Rebuilding gateway… this may take a moment. \
The gateway will announce itself when the new version is ready.";
let ack_html = markdown_to_html(ack);
if let Ok(msg_id) = ctx
.transport
.send_message(&room_id_str, ack, &ack_html)
.await
&& let Ok(event_id) = msg_id.parse()
{
ctx.bot_sent_event_ids.lock().await.insert(event_id);
}
let port = ctx.gateway_port.unwrap_or(3000);
match crate::gateway::rebuild::rebuild_gateway(&ctx.services.project_root, port).await {
Ok(()) => {
// Trampoline is running — this gateway will be killed shortly.
// No further reply needed; the new gateway posts "gateway X.Y.Z ready".
}
Err(e) => {
let msg = format!("Gateway rebuild failed: {e}");
let html = markdown_to_html(&msg);
if let Ok(msg_id) = ctx.transport.send_message(&room_id_str, &msg, &html).await
&& let Ok(event_id) = msg_id.parse()
{
ctx.bot_sent_event_ids.lock().await.insert(event_id);
}
}
}
return;
}
// Check for the rebuild command, which requires async agent and process ops
// and cannot be handled by the sync command registry.
if super::super::super::rebuild::extract_rebuild_command(