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:
Timmy
2026-07-15 16:46:11 +01:00
co-authored by Claude Fable 5
parent 83f941b77e
commit f39c4b7c4b
24 changed files with 36 additions and 824 deletions
@@ -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 {
+1 -1
View File
@@ -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;
+11 -85
View File
@@ -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);
}
}