Remove all alternate update paths — fleet redeploy is release + upgrade all
Killed: - rebuild_and_restart (in-container cargo self-compile): the MCP tool, the `rebuild` chat command in all four transports, the web-ui bot command, and the underlying function. This was the path that caused the exec() deadlocks. - upgrade_sled gateway MCP tool: second entry point to sled upgrades, defaulted to serving the gateway's own macOS binary to Linux sleds. - GET /api/huskies-binary (both sled and gateway route trees): served current_exe(), wrong platform when the gateway is macOS. Superseded by /api/artifacts/ which now also serves on the gateway route tree. - `huskies upgrade` CLI subcommand and --source flag: third way of doing the same download-and-replace. Escape hatch for a bricked sled is `docker cp` + restart. Kept, distinct jobs: `project-rebuild` (container/image updates), `rebuild gateway` + script/local-release (gateway self-update). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019fHdm92yjvguPi2LiXfLB9
This commit is contained in:
@@ -224,11 +224,6 @@ pub fn commands() -> &'static [BotCommand] {
|
||||
description: "Clear the current Claude Code session and start fresh",
|
||||
handler: handle_reset_fallback,
|
||||
},
|
||||
BotCommand {
|
||||
name: "rebuild",
|
||||
description: "Rebuild the server binary and restart",
|
||||
handler: handle_rebuild_fallback,
|
||||
},
|
||||
BotCommand {
|
||||
name: "timer",
|
||||
description: "Schedule a deferred agent start: `timer <story_id> <HH:MM>`, `timer list`, `timer cancel <story_id>`",
|
||||
@@ -425,16 +420,6 @@ fn handle_reset_fallback(_ctx: &CommandContext) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Fallback handler for the `rebuild` command when it is not intercepted by
|
||||
/// the async handler in `on_room_message`. In practice this is never called —
|
||||
/// rebuild is detected and handled before `try_handle_command` is invoked.
|
||||
/// The entry exists in the registry only so `help` lists it.
|
||||
///
|
||||
/// Returns `None` to prevent the LLM from receiving "rebuild" as a prompt.
|
||||
fn handle_rebuild_fallback(_ctx: &CommandContext) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Fallback handler for the `cleanup_worktrees` command when it is not
|
||||
/// intercepted by the async handler in `on_room_message`. In practice this is
|
||||
/// never called — cleanup_worktrees is detected and handled before
|
||||
|
||||
@@ -161,27 +161,6 @@ pub(super) async fn handle_incoming_message(
|
||||
return;
|
||||
}
|
||||
|
||||
if crate::chat::transport::matrix::rebuild::extract_rebuild_command(
|
||||
message,
|
||||
&ctx.services.bot_name,
|
||||
&ctx.services.bot_user_id,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
slog!("[discord] Handling rebuild command from {user} in {channel}");
|
||||
let ack = "Rebuilding server… this may take a moment.";
|
||||
let _ = ctx.transport.send_message(channel, ack, "").await;
|
||||
let response = crate::chat::transport::matrix::rebuild::handle_rebuild(
|
||||
&ctx.services.bot_name,
|
||||
&ctx.services.project_root,
|
||||
&ctx.services.agents,
|
||||
)
|
||||
.await;
|
||||
let response = markdown_to_discord(&response);
|
||||
let _ = ctx.transport.send_message(channel, &response, "").await;
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(rmtree_cmd) = crate::chat::transport::matrix::rmtree::extract_rmtree_command(
|
||||
message,
|
||||
&ctx.services.bot_name,
|
||||
@@ -525,16 +504,6 @@ mod tests {
|
||||
assert!(result.unwrap().contains("Pipeline Status"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuild_command_extracted_from_discord_message() {
|
||||
let result = crate::chat::transport::matrix::rebuild::extract_rebuild_command(
|
||||
"Huskies rebuild",
|
||||
"Huskies",
|
||||
"discord-bot",
|
||||
);
|
||||
assert!(result.is_some(), "'Huskies rebuild' should be recognised");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_command_extracted_from_discord_message() {
|
||||
let result = crate::chat::transport::matrix::reset::extract_reset_command(
|
||||
|
||||
@@ -306,10 +306,8 @@ pub(in crate::chat::transport::matrix::bot) async fn on_room_message(
|
||||
&& (crate::chat::commands::commands()
|
||||
.iter()
|
||||
.any(|c| c.name == cmd)
|
||||
|| [
|
||||
"assign", "start", "delete", "rebuild", "rmtree", "htop", "timer",
|
||||
]
|
||||
.contains(&cmd.as_str()));
|
||||
|| ["assign", "start", "delete", "rmtree", "htop", "timer"]
|
||||
.contains(&cmd.as_str()));
|
||||
|
||||
if is_known_command {
|
||||
// Proxy to the active project server.
|
||||
@@ -1006,45 +1004,6 @@ pub(in crate::chat::transport::matrix::bot) async fn on_room_message(
|
||||
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(
|
||||
&user_message,
|
||||
&ctx.services.bot_name,
|
||||
ctx.matrix_user_id.as_str(),
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
slog!("[matrix-bot] Handling rebuild command from {sender}");
|
||||
// Acknowledge immediately — the rebuild may take a while or re-exec.
|
||||
let ack = "Rebuilding server… 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 response = super::super::super::rebuild::handle_rebuild(
|
||||
&ctx.services.bot_name,
|
||||
&ctx.services.project_root,
|
||||
&ctx.services.agents,
|
||||
)
|
||||
.await;
|
||||
let html = markdown_to_html(&response);
|
||||
if let Ok(msg_id) = ctx
|
||||
.transport
|
||||
.send_message(&room_id_str, &response, &html)
|
||||
.await
|
||||
&& let Ok(event_id) = msg_id.parse()
|
||||
{
|
||||
ctx.bot_sent_event_ids.lock().await.insert(event_id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// In gateway mode, handle the "switch <project>" command to change the
|
||||
// active project without invoking the LLM.
|
||||
if let Some(ref active_project) = ctx.gateway_active_project {
|
||||
|
||||
@@ -35,7 +35,7 @@ pub mod new_project;
|
||||
pub mod project_rebuild;
|
||||
/// `projects` chat command — list all registered gateway projects.
|
||||
pub mod projects;
|
||||
/// Rebuild command — triggers a server rebuild/restart via a bot command.
|
||||
/// `rebuild gateway` command parsing (gateway self-rebuild).
|
||||
pub mod rebuild;
|
||||
/// `release` gateway chat command — build the sled binary and publish it.
|
||||
pub mod release;
|
||||
|
||||
@@ -1,51 +1,19 @@
|
||||
//! Rebuild command: trigger a server rebuild and restart.
|
||||
//! `rebuild gateway` command parsing.
|
||||
//!
|
||||
//! `{bot_name} rebuild` stops all running agents, rebuilds the server binary
|
||||
//! with `cargo build`, and re-execs the process with the new binary. If the
|
||||
//! build fails the error is reported back to the room and the server keeps
|
||||
//! running.
|
||||
//! The old `rebuild` command (in-container cargo build + restart of a sled)
|
||||
//! was removed: fleet binary updates go through `release` + `upgrade all`
|
||||
//! exclusively. Only the gateway self-rebuild command remains here.
|
||||
|
||||
use crate::agents::AgentPool;
|
||||
use crate::chat::util::strip_bot_mention;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A parsed rebuild command.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct RebuildCommand;
|
||||
|
||||
/// Parse a rebuild command from a raw message body.
|
||||
///
|
||||
/// Strips the bot mention prefix and checks whether the command word is
|
||||
/// `rebuild`. Returns `None` when the message is not a rebuild command.
|
||||
pub fn extract_rebuild_command(
|
||||
message: &str,
|
||||
bot_name: &str,
|
||||
bot_user_id: &str,
|
||||
) -> Option<RebuildCommand> {
|
||||
let stripped = strip_bot_mention(message, bot_name, bot_user_id);
|
||||
let trimmed = stripped
|
||||
.trim()
|
||||
.trim_start_matches(|c: char| !c.is_alphanumeric());
|
||||
|
||||
let cmd = match trimmed.split_once(char::is_whitespace) {
|
||||
Some((c, _)) => c,
|
||||
None => trimmed,
|
||||
};
|
||||
|
||||
if cmd.eq_ignore_ascii_case("rebuild") {
|
||||
Some(RebuildCommand)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a "rebuild gateway" command from a raw message body.
|
||||
///
|
||||
/// Returns `Some(RebuildCommand)` only when the stripped message begins with
|
||||
/// "rebuild gateway" (case-insensitive). A plain "rebuild" without the
|
||||
/// "gateway" qualifier returns `None` so it falls through to the standard
|
||||
/// server rebuild handler.
|
||||
/// "rebuild gateway" (case-insensitive).
|
||||
pub fn extract_rebuild_gateway_command(
|
||||
message: &str,
|
||||
bot_name: &str,
|
||||
@@ -77,23 +45,6 @@ pub fn extract_rebuild_gateway_command(
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a rebuild command: trigger server rebuild and restart.
|
||||
///
|
||||
/// Returns a string describing the outcome. On build failure the error
|
||||
/// message is returned so it can be posted to the room; the server keeps
|
||||
/// running. On success this function never returns (the process re-execs).
|
||||
pub async fn handle_rebuild(
|
||||
bot_name: &str,
|
||||
project_root: &Path,
|
||||
agents: &Arc<AgentPool>,
|
||||
) -> String {
|
||||
crate::slog!("[matrix-bot] rebuild command received (bot={bot_name})");
|
||||
match crate::rebuild::rebuild_and_restart(agents, project_root, None).await {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => format!("Rebuild failed: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -103,46 +54,21 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn extract_with_display_name() {
|
||||
let cmd = extract_rebuild_command("Timmy rebuild", "Timmy", "@timmy:home.local");
|
||||
assert_eq!(cmd, Some(RebuildCommand));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_with_full_user_id() {
|
||||
fn extract_gateway_rebuild() {
|
||||
let cmd =
|
||||
extract_rebuild_command("@timmy:home.local rebuild", "Timmy", "@timmy:home.local");
|
||||
extract_rebuild_gateway_command("Timmy rebuild gateway", "Timmy", "@timmy:home.local");
|
||||
assert_eq!(cmd, Some(RebuildCommand));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_with_localpart() {
|
||||
let cmd = extract_rebuild_command("@timmy rebuild", "Timmy", "@timmy:home.local");
|
||||
assert_eq!(cmd, Some(RebuildCommand));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_case_insensitive() {
|
||||
let cmd = extract_rebuild_command("Timmy REBUILD", "Timmy", "@timmy:home.local");
|
||||
assert_eq!(cmd, Some(RebuildCommand));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_non_rebuild_returns_none() {
|
||||
let cmd = extract_rebuild_command("Timmy help", "Timmy", "@timmy:home.local");
|
||||
fn plain_rebuild_is_not_gateway_rebuild() {
|
||||
let cmd = extract_rebuild_gateway_command("Timmy rebuild", "Timmy", "@timmy:home.local");
|
||||
assert_eq!(cmd, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_ignores_extra_args() {
|
||||
// "rebuild" with trailing text is still a rebuild command
|
||||
let cmd = extract_rebuild_command("Timmy rebuild now", "Timmy", "@timmy:home.local");
|
||||
assert_eq!(cmd, Some(RebuildCommand));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_no_match_returns_none() {
|
||||
let cmd = extract_rebuild_command("Timmy status", "Timmy", "@timmy:home.local");
|
||||
fn non_rebuild_returns_none() {
|
||||
let cmd = extract_rebuild_gateway_command("Timmy status", "Timmy", "@timmy:home.local");
|
||||
assert_eq!(cmd, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,27 +208,6 @@ pub(super) async fn handle_incoming_message(
|
||||
return;
|
||||
}
|
||||
|
||||
if crate::chat::transport::matrix::rebuild::extract_rebuild_command(
|
||||
message,
|
||||
&ctx.services.bot_name,
|
||||
&ctx.services.bot_user_id,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
slog!("[slack] Handling rebuild command from {user} in {channel}");
|
||||
let ack = "Rebuilding server… this may take a moment.";
|
||||
let _ = ctx.transport.send_message(channel, ack, "").await;
|
||||
let response = crate::chat::transport::matrix::rebuild::handle_rebuild(
|
||||
&ctx.services.bot_name,
|
||||
&ctx.services.project_root,
|
||||
&ctx.services.agents,
|
||||
)
|
||||
.await;
|
||||
let response = markdown_to_slack(&response);
|
||||
let _ = ctx.transport.send_message(channel, &response, "").await;
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(rmtree_cmd) = crate::chat::transport::matrix::rmtree::extract_rmtree_command(
|
||||
message,
|
||||
&ctx.services.bot_name,
|
||||
@@ -488,42 +467,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ── rebuild command extraction ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn rebuild_command_extracted_from_slack_message() {
|
||||
let result = crate::chat::transport::matrix::rebuild::extract_rebuild_command(
|
||||
"Huskies rebuild",
|
||||
"Huskies",
|
||||
"slack-bot",
|
||||
);
|
||||
assert!(result.is_some(), "'Huskies rebuild' should be recognised");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuild_command_extracted_plain_no_mention() {
|
||||
// Slack slash-command synthetic messages may not include a bot mention.
|
||||
let result = crate::chat::transport::matrix::rebuild::extract_rebuild_command(
|
||||
"rebuild",
|
||||
"Huskies",
|
||||
"slack-bot",
|
||||
);
|
||||
assert!(result.is_some(), "plain 'rebuild' should be recognised");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_rebuild_slack_message_not_extracted() {
|
||||
let result = crate::chat::transport::matrix::rebuild::extract_rebuild_command(
|
||||
"Huskies status",
|
||||
"Huskies",
|
||||
"slack-bot",
|
||||
);
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"'status' should not be recognised as rebuild"
|
||||
);
|
||||
}
|
||||
|
||||
// ── reset command extraction ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -122,26 +122,6 @@ pub(super) async fn handle_incoming_message(
|
||||
return;
|
||||
}
|
||||
|
||||
if crate::chat::transport::matrix::rebuild::extract_rebuild_command(
|
||||
message,
|
||||
&ctx.services.bot_name,
|
||||
&ctx.services.bot_user_id,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
slog!("[whatsapp] Handling rebuild command from {sender}");
|
||||
let ack = "Rebuilding server… this may take a moment.";
|
||||
let _ = ctx.transport.send_message(sender, ack, "").await;
|
||||
let response = crate::chat::transport::matrix::rebuild::handle_rebuild(
|
||||
&ctx.services.bot_name,
|
||||
&ctx.services.project_root,
|
||||
&ctx.services.agents,
|
||||
)
|
||||
.await;
|
||||
let _ = ctx.transport.send_message(sender, &response, "").await;
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(rmtree_cmd) = crate::chat::transport::matrix::rmtree::extract_rmtree_command(
|
||||
message,
|
||||
&ctx.services.bot_name,
|
||||
@@ -397,43 +377,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ── rebuild command extraction ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn rebuild_command_extracted_from_plain_message() {
|
||||
// WhatsApp messages arrive without a bot mention prefix.
|
||||
// extract_rebuild_command must recognise "rebuild" by itself.
|
||||
let result = crate::chat::transport::matrix::rebuild::extract_rebuild_command(
|
||||
"rebuild",
|
||||
"Timmy",
|
||||
"@timmy:home.local",
|
||||
);
|
||||
assert!(result.is_some(), "plain 'rebuild' should be recognised");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuild_command_extracted_with_bot_name_prefix() {
|
||||
let result = crate::chat::transport::matrix::rebuild::extract_rebuild_command(
|
||||
"Timmy rebuild",
|
||||
"Timmy",
|
||||
"@timmy:home.local",
|
||||
);
|
||||
assert!(result.is_some(), "'Timmy rebuild' should be recognised");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_rebuild_whatsapp_message_not_extracted() {
|
||||
let result = crate::chat::transport::matrix::rebuild::extract_rebuild_command(
|
||||
"status",
|
||||
"Timmy",
|
||||
"@timmy:home.local",
|
||||
);
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"'status' should not be recognised as rebuild"
|
||||
);
|
||||
}
|
||||
|
||||
// ── reset command extraction ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user