diff --git a/routes/session_routes.py b/routes/session_routes.py
index 1b38e4b..58cb8ae 100644
--- a/routes/session_routes.py
+++ b/routes/session_routes.py
@@ -37,6 +37,26 @@ def _public_model(name: str, model: str) -> str:
return model
+def _content_to_text(content) -> str:
+ """Flatten a message's content to plain text for text-based exports.
+
+ History entries carry three shapes: a plain string, a multimodal list of
+ content blocks (vision/image attachments), or None (assistant turns that
+ persisted only native tool_calls). The txt/html/md exporters join and
+ string-munge this value, so a list crashed the export (TypeError on join,
+ AttributeError on .replace) and None rendered as the literal "None".
+ Coerce to the text blocks, returning "" for anything without text.
+ """
+ if isinstance(content, str):
+ return content
+ if isinstance(content, list):
+ return "\n".join(
+ b.get("text", "") for b in content
+ if isinstance(b, dict) and b.get("text")
+ )
+ return ""
+
+
def _verify_session_owner(request: Request, session_id: str, session_manager=None):
"""Verify the current user owns the session. Raises 404 if not.
@@ -708,7 +728,7 @@ def setup_session_routes(session_manager: SessionManager, config: dict, webhook_
lines = []
for m in session.history:
lines.append(f"[{m.role.upper()}]")
- lines.append(m.content)
+ lines.append(_content_to_text(m.content))
lines.append("")
out_name = filename or f"conversation_{safe_name}_{timestamp}.txt"
return Response(
@@ -731,7 +751,7 @@ def setup_session_routes(session_manager: SessionManager, config: dict, webhook_
]
for m in session.history:
cls = "user" if m.role == "user" else "ai"
- content = m.content.replace("&", "&").replace("<", "<").replace(">", ">")
+ content = _content_to_text(m.content).replace("&", "&").replace("<", "<").replace(">", ">")
content = content.replace("\n", "
")
html_parts.append(f'