Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -11203,16 +11203,30 @@ async def _flush_buffer() -> None:
exit_code_raw = exit_code_path.read_text().strip() or "1"
exit_code = int(exit_code_raw)
if exit_code == 0:
await adapter.send(chat_id, "✅ Hermes update finished.", metadata=metadata)
result = await adapter.send(
chat_id,
"✅ Hermes update finished.",
metadata=metadata,
)
else:
await adapter.send(
result = await adapter.send(
chat_id,
"❌ Hermes update failed (exit code {}).".format(exit_code),
metadata=metadata,
)
if result is not None and getattr(result, "success", True) is False:
logger.warning(
"Update final notification to %s was not delivered: %s",
session_key,
getattr(result, "error", "send returned success=False"),
)
await asyncio.sleep(poll_interval)
continue
logger.info("Update finished (exit=%s), notified %s", exit_code, session_key)
except Exception as e:
logger.warning("Update final notification failed: %s", e)
await asyncio.sleep(poll_interval)
continue

# Cleanup
for p in (pending_path, claimed_path, output_path,
Expand Down Expand Up @@ -11402,7 +11416,25 @@ async def _send_update_notification(self) -> bool:
msg = "✅ Hermes update finished successfully."
else:
msg = "❌ Hermes update failed. Check the gateway logs or run `hermes update` manually for details."
await adapter.send(chat_id, msg, metadata=metadata)
result = await adapter.send(chat_id, msg, metadata=metadata)
if result is not None and getattr(result, "success", True) is False:
logger.warning(
"Post-update notification to %s:%s was not delivered: %s",
platform_str,
chat_id,
getattr(result, "error", "send returned success=False"),
)
cleanup = False
active_pending_path = pending_path
try:
if claimed_path.exists():
claimed_path.replace(pending_path)
except OSError as move_err:
logger.warning(
"Failed to preserve post-update notification markers: %s",
move_err,
)
return False
logger.info(
"Sent post-update notification to %s:%s (exit=%s)",
platform_str,
Expand Down
36 changes: 35 additions & 1 deletion tests/gateway/test_update_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import pytest

from gateway.config import Platform
from gateway.platforms.base import MessageEvent
from gateway.platforms.base import MessageEvent, SendResult
from gateway.session import SessionSource


Expand Down Expand Up @@ -763,6 +763,40 @@ async def test_cleans_up_files_after_notification(self, tmp_path):
assert not output_path.exists()
assert not exit_code_path.exists()

@pytest.mark.asyncio
async def test_send_result_failure_preserves_markers_for_retry(self, tmp_path):
"""SendResult(success=False) não confirma entrega e mantém retry pendente."""
runner = _make_runner()
hermes_home = tmp_path / "hermes"
hermes_home.mkdir()

pending_path = hermes_home / ".update_pending.json"
output_path = hermes_home / ".update_output.txt"
exit_code_path = hermes_home / ".update_exit_code"
pending_path.write_text(json.dumps({
"platform": "telegram", "chat_id": "111", "user_id": "222",
}))
output_path.write_text("✓ Done")
exit_code_path.write_text("0")

mock_adapter = AsyncMock()
mock_adapter.send.return_value = SendResult(
success=False,
error="telegram timed out",
retryable=True,
)
runner.adapters = {Platform.TELEGRAM: mock_adapter}

with patch("gateway.run._hermes_home", hermes_home):
result = await runner._send_update_notification()

assert result is False
mock_adapter.send.assert_called_once()
assert pending_path.exists()
assert output_path.exists()
assert exit_code_path.exists()
assert not (hermes_home / ".update_pending.claimed.json").exists()

@pytest.mark.asyncio
async def test_cleans_up_on_error(self, tmp_path):
"""Files are cleaned up even if notification fails."""
Expand Down
37 changes: 36 additions & 1 deletion tests/gateway/test_update_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import pytest

from gateway.config import Platform
from gateway.platforms.base import MessageEvent
from gateway.platforms.base import MessageEvent, SendResult
from gateway.session import SessionSource


Expand Down Expand Up @@ -412,6 +412,41 @@ async def test_cleans_up_on_completion(self, tmp_path):
assert not output_path.exists()
assert not exit_code_path.exists()

@pytest.mark.asyncio
async def test_keeps_markers_when_final_notification_send_fails(self, tmp_path):
"""Falha retornada por adapter.send deve preservar estado para retry."""
runner = _make_runner()
hermes_home = tmp_path / "hermes"
hermes_home.mkdir()

pending = {"platform": "telegram", "chat_id": "111", "user_id": "222",
"session_key": "agent:main:telegram:dm:111"}
pending_path = hermes_home / ".update_pending.json"
output_path = hermes_home / ".update_output.txt"
exit_code_path = hermes_home / ".update_exit_code"
pending_path.write_text(json.dumps(pending))
output_path.write_text("done\n")
exit_code_path.write_text("0")

mock_adapter = AsyncMock()
mock_adapter.send.return_value = SendResult(
success=False,
error="telegram timed out",
retryable=True,
)
runner.adapters = {Platform.TELEGRAM: mock_adapter}

with patch("gateway.run._hermes_home", hermes_home):
await runner._watch_update_progress(
poll_interval=0.05,
stream_interval=0.1,
timeout=0.15,
)

assert pending_path.exists()
assert output_path.exists()
assert exit_code_path.exists()

@pytest.mark.asyncio
async def test_failure_exit_code(self, tmp_path):
"""Non-zero exit code sends failure message."""
Expand Down