fix: scheduled task notifications not delivered on cancellation

GenerationCancelled extends BaseException, not Exception, so it bypassed
the except handler in _run_initial. The finally block ran but
_extract_last_assistant_content returned "" (response never appended to
messages), and _fire_notify_targets bailed on the empty content guard.

Fixes:
- Catch BaseException (not just Exception) in _run_initial so
  GenerationCancelled is handled and the UI state is cleaned up
- Remove the empty-content suppression in _fire_notify_targets —
  scheduled tasks should always deliver, even with a fallback message
  when no output was captured
This commit is contained in:
Patrick Buckley
2026-04-06 09:53:44 -07:00
parent 6980ba5aae
commit 5dc98f75fb
2 changed files with 9 additions and 4 deletions
+5 -2
View File
@@ -349,11 +349,14 @@ class TestFireNotifyTargets:
mock_deliver.assert_not_called()
@patch("turnstone.server._deliver_notification")
def test_empty_content_skipped(self, mock_deliver):
def test_empty_content_delivers_fallback(self, mock_deliver):
"""Empty content should still deliver with a fallback message."""
ws = MagicMock()
ws.notify_targets = '[{"channel_type":"discord","channel_id":"1"}]'
_fire_notify_targets(ws, "")
mock_deliver.assert_not_called()
mock_deliver.assert_called_once()
payload = mock_deliver.call_args[0][1]
assert "no output captured" in payload["message"]
@patch("turnstone.server._deliver_notification")
def test_invalid_json_targets_skipped(self, mock_deliver):
+4 -2
View File
@@ -1735,8 +1735,10 @@ def _extract_last_assistant_content(session: Any) -> str:
def _fire_notify_targets(ws: Any, content: str) -> None:
"""Send completion notifications to all configured targets."""
if not content or not ws.notify_targets:
if not ws.notify_targets:
return
if not content:
content = "(Task completed — no output captured)"
try:
targets = json.loads(ws.notify_targets)
@@ -2032,7 +2034,7 @@ async def create_workstream(request: Request) -> JSONResponse:
def _run_initial() -> None:
try:
session.send(initial_message)
except Exception:
except BaseException:
if isinstance(ws.ui, WebUI):
ws.ui.on_stream_end()
ws.ui.on_state_change("idle")