From 28465884fc8b08282bdde534349e3dc486d62d53 Mon Sep 17 00:00:00 2001 From: Azreen Jamal Date: Fri, 6 Mar 2026 04:20:02 +0800 Subject: [PATCH] telegram-bot: fix streaming JSON, session bloat, prevent local command execution - Fix progress messages showing [object Object] - parse tool_execution_start/end events - Auto-clear sessions >200KB to prevent context bloat timeouts - Restrict bash tool to SSH and curl only - prevent recursive Pi spawning - Proper error messages for timeouts --- telegram-bot/index.ts | 92 ++++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 44 deletions(-) diff --git a/telegram-bot/index.ts b/telegram-bot/index.ts index 6e64875..3243fab 100644 --- a/telegram-bot/index.ts +++ b/telegram-bot/index.ts @@ -63,7 +63,10 @@ RULES: - DO NOT be lazy. When asked to check something, actually run the commands. When asked to fix something, do it immediately. Don't say "you could try X" — just do X. - Take initiative. If you see a problem while investigating, fix it or flag it. - Chain commands when possible to get answers faster. -- If a command fails, try an alternative approach immediately. Don't just report the error.`; +- If a command fails, try an alternative approach immediately. Don't just report the error. +- NEVER run local commands like "just", "pi", "npm", "bun" etc. on the local machine. You only interact with the REMOTE server via SSH. +- NEVER run "cd /Users" or any local filesystem commands. ALL commands must go through SSH. +- The ONLY use of your bash tool is: ssh -p ${SSH_PORT} ${SSH_USER}@${SSH_HOST} "..." or curl for APIs (like Asana).`; // ─── Stream Pi JSON output with live Telegram updates ─── async function callPiStreaming( @@ -73,7 +76,19 @@ async function callPiStreaming( ctx: Context, ): Promise { const session = sessionPath(userId); - const hasSession = existsSync(session); + let hasSession = existsSync(session); + + // Auto-clear session if too large (>200KB = context bloat) + if (hasSession) { + try { + const stat = require("fs").statSync(session); + if (stat.size > 200_000) { + console.log(`🧹 [${userId}] Session too large (${(stat.size / 1024).toFixed(0)}KB) — clearing`); + require("fs").unlinkSync(session); + hasSession = false; + } + } catch {} + } const args = [ PI_BIN, "-p", @@ -139,54 +154,29 @@ async function callPiStreaming( try { const event = JSON.parse(line); - // Tool call started — show what command is running - if (event.type === "tool_call") { - const input = event.tool_call?.input; - if (input?.command) { - currentToolCmd = input.command; - const short = currentToolCmd.length > 80 ? currentToolCmd.slice(0, 80) + "..." : currentToolCmd; + // Tool execution started — show what command is running + if (event.type === "tool_execution_start") { + const cmd = event.args?.command; + if (cmd) { + currentToolCmd = cmd; + const short = cmd.length > 80 ? cmd.slice(0, 80) + "..." : cmd; console.log(`🔧 [${userId}] ${short}`); await sendProgress(short); + } else { + console.log(`🔧 [${userId}] ${event.toolName}(${JSON.stringify(event.args).slice(0, 60)})`); } } - // Tool result — show brief output - if (event.type === "tool_result") { - const output = event.tool_result?.stdout || event.tool_result?.content || ""; - if (output && typeof output === "string") { + // Tool execution finished — show brief result + if (event.type === "tool_execution_end") { + const output = typeof event.result === "string" ? event.result : ""; + if (output) { const brief = output.split("\n")[0].slice(0, 60); if (brief) await sendProgress(`→ ${brief}`); } } - // Accumulate assistant text from message_end or text_end events - if (event.type === "message_end" && event.message?.role === "assistant") { - const content = event.message.content; - if (Array.isArray(content)) { - for (const block of content) { - if (block.type === "text" && block.text) { - finalText += (finalText ? "\n" : "") + block.text; - } - } - } else if (typeof content === "string") { - finalText += (finalText ? "\n" : "") + content; - } - } - - // Also catch turn_end which has the final message - if (event.type === "turn_end" && event.message?.role === "assistant") { - const content = event.message.content; - if (Array.isArray(content)) { - // Reset finalText to avoid duplication from message_end - const turnText = content - .filter((b: any) => b.type === "text" && b.text) - .map((b: any) => b.text) - .join("\n"); - if (turnText) finalText = turnText; - } - } - - // agent_end — extract final text from all assistant messages + // agent_end — extract ALL assistant text blocks if (event.type === "agent_end" && event.messages) { const allText: string[] = []; for (const msg of event.messages) { @@ -197,11 +187,22 @@ async function callPiStreaming( } } if (allText.length > 0) { - // Use last assistant text block as final answer finalText = allText[allText.length - 1]; } } + // Fallback: turn_end for intermediate text + if (event.type === "turn_end" && event.message?.role === "assistant") { + const content = event.message.content; + if (Array.isArray(content)) { + const turnText = content + .filter((b: any) => b.type === "text" && b.text) + .map((b: any) => b.text) + .join("\n"); + if (turnText) finalText = turnText; + } + } + } catch { // Not valid JSON, skip } @@ -288,7 +289,7 @@ bot.use(async (ctx, next) => { // ─── Commands ─── bot.command("start", (ctx) => - ctx.reply("🤖 Pi DevOps Bot (Sonnet 4.6)\n\n• Chat naturally — I'll run commands\n• /status — quick health check\n• /ssh — direct SSH command\n• /cancel — stop current task\n• /clear — reset conversation") + ctx.reply("🤖 Pi DevOps Bot (Sonnet 4.6)\n\n• Chat naturally — I'll run commands\n• /status — quick health check\n• /ssh — direct SSH command\n• /stop or /cancel — stop current task\n• /clear — reset conversation") ); bot.command("status", async (ctx) => { @@ -319,11 +320,14 @@ bot.command("clear", (ctx) => { return ctx.reply("🧹 Cleared."); }); -bot.command("cancel", (ctx) => { +bot.command("cancel", handleCancel); +bot.command("stop", handleCancel); + +function handleCancel(ctx: Context) { const c = activeRequests.get(ctx.from!.id); if (c) { c.abort(); activeRequests.delete(ctx.from!.id); return ctx.reply("🛑 Cancelled."); } return ctx.reply("Nothing running."); -}); +} // ─── Message handler ─── bot.on("message:text", (ctx) => {