Show Utility Model rename failures

Notify users when automatic chat renaming cannot reach the Utility Model, while keeping the rename task best-effort. Normalize saved generated titles, mark WebUI state dirty after successful saves, and cover the success and failure paths with focused tests.

Alessandro committed Jun 28, 2026 at 18:46 UTC 0b38258d6cfec6bb3d68d28d000f3c3aea68f14e
3 files changed +89 -6
extensions/python/monologue_start/AGENTS.md
+2 -1
@@ -12,10 +12,11 @@
12
13 - Keep automatic rename behavior bounded and non-destructive.
14 - Do not override explicit user chat names without the intended guard conditions.
15 +- Surface Utility Model rename failures with one scoped error notification per chat.
16
17 ## Work Guidance
18
18 -- Coordinate rename behavior with chat persistence and WebUI refresh.
19 +- Coordinate rename behavior with chat persistence and WebUI refresh after successful saves.
20
21 ## Verification
22
extensions/python/monologue_start/_60_rename_chat.py
+26 -5
@@ -1,5 +1,7 @@
1 from helpers import persist_chat, tokens
2 from helpers.extension import Extension
3 +from helpers.notification import NotificationManager, NotificationPriority, NotificationType
4 +from helpers.state_monitor_integration import mark_dirty_all
5 from agent import LoopData
6 import asyncio
7
@@ -29,16 +31,35 @@ class RenameChat(Extension):
31 "fw.rename_chat.msg.md", current_name=current_name, history=history_text
32 )
33 # call utility model
32 - new_name = await self.agent.call_utility_model(
33 - system=system, message=message, background=True
34 - )
34 + try:
35 + new_name = await self.agent.call_utility_model(
36 + system=system, message=message, background=True
37 + )
38 + except Exception:
39 + NotificationManager.send_notification(
40 + type=NotificationType.ERROR,
41 + priority=NotificationPriority.NORMAL,
42 + title="Chat Rename Failed",
43 + message="Automatic chat renaming failed because the Utility Model was not reachable.",
44 + detail=(
45 + "Automatic chat renaming uses the Utility Model. Check Settings > Models > "
46 + "Utility Model, provider/API key, and network reachability."
47 + ),
48 + display_time=10,
49 + group="chat_rename",
50 + id=f"chat_rename_failed_{self.agent.context.id}",
51 + )
52 + return
53 # update name
54 if new_name:
37 - # trim name to max length if needed
55 + new_name = " ".join(str(new_name).split())
56 if len(new_name) > 40:
57 new_name = new_name[:40] + "..."
58 + if not new_name:
59 + return
60 # apply to context and save
61 self.agent.context.name = new_name
62 persist_chat.save_tmp_chat(self.agent.context)
43 - except Exception as e:
63 + mark_dirty_all(reason="monologue_start.RenameChat.change_name")
64 + except Exception:
65 pass # non-critical
tests/test_chat_rename_extension.py new
+61
@@ -0,0 +1,61 @@
1 +from types import SimpleNamespace
2 +
3 +import pytest
4 +
5 +from extensions.python.monologue_start import _60_rename_chat as rename_chat
6 +from plugins._model_config.helpers import model_config
7 +
8 +
9 +pytestmark = pytest.mark.asyncio
10 +
11 +
12 +class _History:
13 + def output_text(self) -> str:
14 + return "User: Please help me plan the launch."
15 +
16 +
17 +class _Agent:
18 + def __init__(self, *, response: str | None = None, error: Exception | None = None):
19 + self.context = SimpleNamespace(id="ctx-rename", name="")
20 + self.history = _History()
21 + self._response = response
22 + self._error = error
23 +
24 + def read_prompt(self, name: str, **kwargs) -> str:
25 + return name
26 +
27 + async def call_utility_model(self, **kwargs) -> str:
28 + if self._error:
29 + raise self._error
30 + return self._response or ""
31 +
32 +
33 +async def test_rename_failure_sends_utility_model_error_notification(monkeypatch):
34 + sent: list[dict] = []
35 +
36 + monkeypatch.setattr(model_config, "get_utility_model_config", lambda agent: {"ctx_length": 1000})
37 + monkeypatch.setattr(rename_chat.NotificationManager, "send_notification", lambda **kwargs: sent.append(kwargs))
38 +
39 + await rename_chat.RenameChat(agent=_Agent(error=RuntimeError("offline"))).change_name()
40 +
41 + assert len(sent) == 1
42 + assert sent[0]["type"] == rename_chat.NotificationType.ERROR
43 + assert sent[0]["title"] == "Chat Rename Failed"
44 + assert "Utility Model was not reachable" in sent[0]["message"]
45 + assert sent[0]["id"] == "chat_rename_failed_ctx-rename"
46 +
47 +
48 +async def test_successful_rename_saves_clean_name_and_marks_state_dirty(monkeypatch):
49 + saved_names: list[str] = []
50 + dirty_reasons: list[str | None] = []
51 +
52 + monkeypatch.setattr(model_config, "get_utility_model_config", lambda agent: {"ctx_length": 1000})
53 + monkeypatch.setattr(rename_chat.persist_chat, "save_tmp_chat", lambda context: saved_names.append(context.name))
54 + monkeypatch.setattr(rename_chat, "mark_dirty_all", lambda *, reason=None: dirty_reasons.append(reason))
55 +
56 + agent = _Agent(response="\n\nLaunch Readiness Notes\n")
57 + await rename_chat.RenameChat(agent=agent).change_name()
58 +
59 + assert agent.context.name == "Launch Readiness Notes"
60 + assert saved_names == ["Launch Readiness Notes"]
61 + assert dirty_reasons == ["monologue_start.RenameChat.change_name"]