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
This commit is contained in:
Azreen Jamal
2026-03-06 04:20:02 +08:00
parent b5d8665ac0
commit 28465884fc
+48 -44
View File
@@ -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<string> {
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 <cmd> — 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 <cmd> — 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) => {