diff --git a/telegram-agy-bot/agy_engine.py b/telegram-agy-bot/agy_engine.py
index 6ef1a9b..043809e 100644
--- a/telegram-agy-bot/agy_engine.py
+++ b/telegram-agy-bot/agy_engine.py
@@ -1543,6 +1543,21 @@ class AGYEngine:
logger.debug(f"Post-prompt git commit skipped: {ce}")
text_out = final_response if final_response else "".join(collected_tokens)
+ if not text_out.strip() and session.conversation_id:
+ try:
+ rec = get_conversation_last_output(session.conversation_id)
+ if rec.get("text"):
+ text_out = rec["text"]
+ elif rec.get("tools"):
+ is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
+ tool_bullets = "\n".join(f"• {t}" for t in rec["tools"][-10:])
+ if is_fa:
+ text_out = f"✅ **دستورات و ابزارهای زیر با موفقیت اجرا شدند:**\n\n{tool_bullets}"
+ else:
+ text_out = f"✅ **The following tools and operations executed successfully:**\n\n{tool_bullets}"
+ except Exception as ex:
+ logger.debug(f"Fallback transcript lookup failed: {ex}")
+
return AgentResult(
text=text_out,
usage=last_usage,
diff --git a/telegram-agy-bot/bot.py b/telegram-agy-bot/bot.py
index b9d2be9..55604b3 100644
--- a/telegram-agy-bot/bot.py
+++ b/telegram-agy-bot/bot.py
@@ -6318,28 +6318,40 @@ async def process_agent_turn(
thinking_msg = f"💭 در حال تفکر روی پروژه {escape_html(curr_proj.name)}... ▌" if is_fa else f"💭 Thinking on project {escape_html(curr_proj.name)}... ▌"
full_html = proj_header + "\n\n" + thinking_msg
- if len(full_html) <= settings.max_message_length:
- try:
- await status_msg.edit_text(
- full_html,
- parse_mode=constants.ParseMode.HTML,
- disable_web_page_preview=True,
- reply_markup=get_stop_button(session.language) if not final else None,
- )
- except BadRequest as e:
- if "Message is not modified" not in str(e):
- try:
- await status_msg.edit_text(
- escape_html(raw_text[:3800]),
- parse_mode=constants.ParseMode.HTML,
- reply_markup=get_stop_button(session.language) if not final else None,
- )
- except Exception:
- pass
- except RetryAfter as e:
- await asyncio.sleep(e.retry_after)
- except Exception as e:
- logger.debug(f"Streaming edit skipped: {e}")
+ if len(full_html) > settings.max_message_length:
+ suffix_str = ("\n\n" + "\n\n".join(bottom_parts)) if bottom_parts else ""
+ available_for_body = max(200, settings.max_message_length - len(suffix_str) - 60)
+ trimmed_raw = raw_text[:available_for_body]
+ formatted_body = markdown_to_telegram_html(trimmed_raw)
+ more_indicator = ("...\n(ادامه در پیام بعدی...)" if is_fa else "...\n(continues...)")
+ full_html = formatted_body + more_indicator + suffix_str
+ if len(full_html) > settings.max_message_length:
+ full_html = full_html[:settings.max_message_length - 10] + "..."
+
+ try:
+ await status_msg.edit_text(
+ full_html,
+ parse_mode=constants.ParseMode.HTML,
+ disable_web_page_preview=True,
+ reply_markup=get_stop_button(session.language) if not final else None,
+ )
+ except BadRequest as e:
+ if "Message is not modified" not in str(e):
+ try:
+ fallback_body = escape_html(raw_text[:3500])
+ fallback_suffix = ("\n\n" + "\n\n".join(bottom_parts)) if bottom_parts else ""
+ fallback_msg = (fallback_body + fallback_suffix)[:settings.max_message_length]
+ await status_msg.edit_text(
+ fallback_msg,
+ parse_mode=constants.ParseMode.HTML,
+ reply_markup=get_stop_button(session.language) if not final else None,
+ )
+ except Exception:
+ pass
+ except RetryAfter as e:
+ await asyncio.sleep(e.retry_after)
+ except Exception as e:
+ logger.debug(f"Streaming edit skipped: {e}")
def on_delta(token: str):
accumulated_tokens.append(token)
@@ -6568,32 +6580,101 @@ async def on_startup(app: Application):
is_admin = chat_id in settings.admin_user_ids
try:
- # Clean up pending status message if interrupted
- if is_admin and session.last_status_msg_id:
+ # Check if there was an interrupted or un-delivered turn
+ if session.turn_in_progress or not session.last_delivered or session.last_status_msg_id:
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
- ready_msg = (
- f"🟢 ربات راهاندازی مجدد شد و آنلاین است (پروژه فعال: {escape_html(proj_name)}).\n\nآماده دریافت دستورات جدید شما هستم!"
- if is_fa else
- f"🟢 AGY Bot restarted successfully and is online (Active Project: {escape_html(proj_name)})!\n\nReady for your commands!"
- )
- try:
- await app.bot.edit_message_text(
- chat_id=chat_id,
- message_id=session.last_status_msg_id,
- text=ready_msg,
- parse_mode=constants.ParseMode.HTML,
- disable_web_page_preview=True,
+ recovered_text = None
+
+ # Attempt to recover last output from AGY transcript
+ if curr_proj and curr_proj.conversation_id:
+ try:
+ from agy_engine import get_conversation_last_output
+ rec = get_conversation_last_output(curr_proj.conversation_id)
+ if rec.get("text"):
+ recovered_text = rec["text"]
+ elif rec.get("tools"):
+ tools_list = "\n".join(f"• {escape_html(t)}" for t in rec["tools"][-8:])
+ recovered_text = (
+ f"✅ دستورات و ابزارهای زیر پیش از راهاندازی مجدد با موفقیت اعمال شدند:\n\n{tools_list}"
+ if is_fa else
+ f"✅ The following operations were executed before restart:\n\n{tools_list}"
+ )
+ except Exception as rec_err:
+ logger.debug(f"Startup recovery failed for {chat_id}: {rec_err}")
+
+ if recovered_text:
+ formatted_html = markdown_to_telegram_html(recovered_text)
+ rec_header = (
+ f"🟢 [پاسخ بازیابیشده پس از ریاستارت ربات - پروژه: {escape_html(proj_name)}]\n\n"
+ if is_fa else
+ f"🟢 [Response Recovered After Restart - Project: {escape_html(proj_name)}]\n\n"
)
- except Exception as edit_err:
- logger.debug(f"Could not edit previous status msg {session.last_status_msg_id}: {edit_err}")
+ full_delivered = rec_header + formatted_html
+ chunks = split_message(full_delivered, max_length=settings.max_message_length)
+
+ if session.last_status_msg_id:
+ try:
+ await app.bot.edit_message_text(
+ chat_id=chat_id,
+ message_id=session.last_status_msg_id,
+ text=chunks[0],
+ parse_mode=constants.ParseMode.HTML,
+ disable_web_page_preview=True,
+ )
+ for extra in chunks[1:]:
+ await app.bot.send_message(
+ chat_id=chat_id,
+ text=extra,
+ parse_mode=constants.ParseMode.HTML,
+ disable_web_page_preview=True,
+ )
+ except Exception:
+ for c in chunks:
+ try:
+ await app.bot.send_message(
+ chat_id=chat_id,
+ text=c,
+ parse_mode=constants.ParseMode.HTML,
+ disable_web_page_preview=True,
+ )
+ except Exception:
+ pass
+ else:
+ for c in chunks:
+ try:
+ await app.bot.send_message(
+ chat_id=chat_id,
+ text=c,
+ parse_mode=constants.ParseMode.HTML,
+ disable_web_page_preview=True,
+ )
+ except Exception:
+ pass
+
+ session.last_response = recovered_text
+ if curr_proj:
+ curr_proj.last_response = recovered_text
+
+ elif is_admin and session.last_status_msg_id:
+ ready_msg = (
+ f"🟢 ربات راهاندازی مجدد شد و آنلاین است (پروژه فعال: {escape_html(proj_name)}).\n\nآماده دریافت دستورات جدید شما هستم!"
+ if is_fa else
+ f"🟢 AGY Bot restarted successfully and is online (Active Project: {escape_html(proj_name)})!\n\nReady for your commands!"
+ )
+ try:
+ await app.bot.edit_message_text(
+ chat_id=chat_id,
+ message_id=session.last_status_msg_id,
+ text=ready_msg,
+ parse_mode=constants.ParseMode.HTML,
+ disable_web_page_preview=True,
+ )
+ except Exception as edit_err:
+ logger.debug(f"Could not edit previous status msg {session.last_status_msg_id}: {edit_err}")
except Exception as e:
logger.error(f"Failed to deliver recovery message to chat {chat_id}: {e}", exc_info=True)
finally:
- # Delete/Clear last message and turn flags from memory
- session.last_response = None
- session.last_prompt = None
- if curr_proj:
- curr_proj.last_response = None
+ # Reset turn flags cleanly
session.turn_in_progress = False
session.restart_pending = False
session.last_delivered = True
diff --git a/telegram-agy-bot/formatters.py b/telegram-agy-bot/formatters.py
index 0c55123..841a5b3 100644
--- a/telegram-agy-bot/formatters.py
+++ b/telegram-agy-bot/formatters.py
@@ -1,5 +1,7 @@
import re
import html
+import json
+from pathlib import Path
from typing import List, Optional, Dict, Any
def escape_html(text: str) -> str:
@@ -127,12 +129,76 @@ def format_thought(
header = "💭 تفکر و استدلال:" if is_fa else "💭 Reasoning / Thinking:"
return f"
{header}\n{escaped}\n" -def format_tool_call(tool_name: str, tool_args: str, lang: str = "fa") -> str: - """Formats an active tool execution notification.""" - escaped_args = escape_html(tool_args) +def format_tool_call(tool_name: str, tool_args: Any, lang: str = "fa") -> str: + """Formats an active tool execution notification into a clean, concise badge.""" is_fa = (lang or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)") label = "ابزار" if is_fa else "Tool" - return f"🔧 {label}:
{escape_html(tool_name)}\n{escaped_args}\n"
+
+ # Parse tool arguments if passed as JSON string or dict
+ params = {}
+ if isinstance(tool_args, dict):
+ params = tool_args
+ elif isinstance(tool_args, str):
+ try:
+ params = json.loads(tool_args)
+ except Exception:
+ params = {}
+
+ # Extract friendly description/action
+ action = ""
+ target = ""
+ if isinstance(params, dict) and params:
+ action = params.get("toolAction") or params.get("toolSummary") or params.get("Description") or ""
+
+ # Tool specific highlights
+ if tool_name == "run_command":
+ cmd = params.get("CommandLine", "")
+ target = f"{escape_html(cmd[:120])}" if cmd else ""
+ elif tool_name in ("write_to_file", "replace_file_content"):
+ file_p = params.get("TargetFile", "")
+ instr = params.get("Instruction", "")
+ if file_p:
+ target = f"📄 {escape_html(Path(file_p).name if '/' in file_p else file_p)}"
+ if instr and not action:
+ action = instr[:100]
+ elif tool_name in ("view_file", "view_image"):
+ file_p = params.get("AbsolutePath", "")
+ if file_p:
+ target = f"🔍 {escape_html(Path(file_p).name if '/' in file_p else file_p)}"
+ elif tool_name == "grep_search":
+ q = params.get("Query", "")
+ if q:
+ target = f"🔍 {escape_html(q[:80])}"
+ elif tool_name == "find_by_name":
+ pat = params.get("Pattern", "")
+ if pat:
+ target = f"📁 {escape_html(pat[:80])}"
+ elif tool_name in ("read_url_content", "search_web"):
+ u = params.get("Url") or params.get("query", "")
+ if u:
+ target = f"🌐 {escape_html(u[:80])}"
+ elif tool_name == "manage_task":
+ act = params.get("Action", "")
+ tid = params.get("TaskId", "")
+ target = f"⚙️ {escape_html(act)} {escape_html(tid)}"
+ elif tool_name == "invoke_subagent":
+ subagents = params.get("Subagents", [])
+ roles = [s.get("Role", s.get("TypeName", "Agent")) for s in subagents if isinstance(s, dict)]
+ if roles:
+ target = f"🤖 {escape_html(', '.join(roles)[:80])}"
+
+ if not action and not target and isinstance(tool_args, str) and tool_args.strip():
+ # Fallback short string preview
+ short_args = tool_args.strip().replace("\n", " ")
+ if len(short_args) > 100:
+ short_args = short_args[:100] + "..."
+ target = f"{escape_html(short_args)}"
+
+ action_text = f" ({escape_html(action)})" if action else ""
+ out = f"🔧 {label}: {escape_html(tool_name)}{action_text}"
+ if target:
+ out += f"\n ↳ {target}"
+ return out + "\n"
def markdown_to_telegram_html(text: str) -> str:
"""