Gateway bot: proxy commands to active project instead of reading local state

In gateway mode the bot has no local CRDT or project filesystem, so all
bot commands (status, backlog, start, assign, etc.) returned empty or
broken results. Now the gateway bot proxies non-local commands via HTTP
to the active project's /api/bot/command endpoint, which already exists
on every project server.

Only a small set of gateway-local commands (help, ambient, reset, switch)
are still handled directly by the gateway. Everything else is forwarded
automatically, so new commands added in the future will work through the
proxy without additional gateway changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Timmy
2026-04-21 11:47:06 +01:00
parent b77e139347
commit 45f1096b96
6 changed files with 130 additions and 1 deletions
@@ -179,6 +179,57 @@ pub(super) async fn on_room_message(
// a subdirectory named after the project. Standalone mode is unaffected.
let effective_root = ctx.effective_project_root().await;
// ── Gateway command proxy ───────────────────────────────────────────
// In gateway mode the bot has no local CRDT or project filesystem, so most
// commands must be forwarded to the active project's `/api/bot/command`
// endpoint. Only a small set of gateway-local commands are handled here.
if ctx.is_gateway() {
// Commands that are meaningful on the gateway itself (no project state needed).
const GATEWAY_LOCAL_COMMANDS: &[&str] = &["help", "ambient", "reset", "switch"];
let stripped = crate::chat::util::strip_bot_mention(
&user_message,
&ctx.bot_name,
ctx.bot_user_id.as_str(),
)
.trim()
.trim_start_matches(|c: char| !c.is_alphanumeric())
.to_string();
let (cmd, args) = match stripped.split_once(char::is_whitespace) {
Some((c, a)) => (c.to_ascii_lowercase(), a.trim().to_string()),
None => (stripped.to_ascii_lowercase(), String::new()),
};
if !cmd.is_empty() && !GATEWAY_LOCAL_COMMANDS.contains(&cmd.as_str()) {
// Proxy to the active project server.
let response = match ctx.proxy_bot_command(&cmd, &args).await {
Some(r) => r,
None => "No active project selected or project URL not configured.".to_string(),
};
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);
}
// If the command was recognized by the project server, we're done.
// If it was not a command at all (freeform text), fall through to the LLM.
if crate::chat::commands::commands()
.iter()
.any(|c| c.name == cmd)
|| ["assign", "start", "delete", "rebuild", "rmtree", "htop", "timer"]
.contains(&cmd.as_str())
{
return;
}
}
// Gateway-local commands and freeform text fall through to normal handling below.
}
// Check for bot-level commands (help, status, ambient, …) before invoking
// the LLM. All commands are registered in commands.rs — no special-casing
// needed here.