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:
+48
-44
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user