mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
fix(discord): retry slash sync after failed fingerprint
This commit is contained in:
parent
b84389c625
commit
6d292fd5eb
2 changed files with 85 additions and 9 deletions
|
|
@ -1773,18 +1773,24 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
if retry_after_until > now:
|
||||
remaining = max(1, int(retry_after_until - now))
|
||||
return f"Discord asked us to wait before syncing slash commands; retry in {remaining}s"
|
||||
if entry.get("fingerprint") == fingerprint and entry.get("last_success_at"):
|
||||
last_success_at = float(entry.get("last_success_at") or 0)
|
||||
last_attempt_at = float(entry.get("last_attempt_at") or 0)
|
||||
if (
|
||||
entry.get("fingerprint") == fingerprint
|
||||
and last_success_at
|
||||
and last_success_at >= last_attempt_at
|
||||
):
|
||||
return "same slash-command fingerprint already synced"
|
||||
return None
|
||||
|
||||
def _record_command_sync_attempt(self, app_id: Any, fingerprint: str) -> None:
|
||||
state = self._read_command_sync_state()
|
||||
state[self._command_sync_state_key(app_id)] = {
|
||||
**(
|
||||
state.get(self._command_sync_state_key(app_id))
|
||||
if isinstance(state.get(self._command_sync_state_key(app_id)), dict)
|
||||
else {}
|
||||
),
|
||||
key = self._command_sync_state_key(app_id)
|
||||
entry = state.get(key) if isinstance(state.get(key), dict) else {}
|
||||
entry.pop("last_success_at", None)
|
||||
entry.pop("summary", None)
|
||||
state[key] = {
|
||||
**entry,
|
||||
"fingerprint": fingerprint,
|
||||
"last_attempt_at": time.time(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -754,6 +754,72 @@ async def test_post_connect_initialization_skips_same_fingerprint_after_success(
|
|||
fake_http.upsert_global_command.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_connect_initialization_retries_fingerprint_after_timeout(tmp_path, monkeypatch):
|
||||
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="test-token"))
|
||||
monkeypatch.setattr("hermes_constants.get_hermes_home", lambda: tmp_path)
|
||||
|
||||
class _DesiredCommand:
|
||||
def to_dict(self, tree):
|
||||
return {
|
||||
"name": "skill",
|
||||
"description": "Run a skill",
|
||||
"type": 1,
|
||||
"options": [],
|
||||
}
|
||||
|
||||
adapter._client = SimpleNamespace(
|
||||
tree=SimpleNamespace(get_commands=lambda: [_DesiredCommand()]),
|
||||
application_id=999,
|
||||
user=SimpleNamespace(id=999),
|
||||
)
|
||||
desired_fingerprint = adapter._desired_command_sync_fingerprint()
|
||||
state_path = (
|
||||
tmp_path
|
||||
/ discord_platform._DISCORD_COMMAND_SYNC_STATE_SUBDIR
|
||||
/ discord_platform._DISCORD_COMMAND_SYNC_STATE_FILENAME
|
||||
)
|
||||
state_path.parent.mkdir(parents=True)
|
||||
state_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"999": {
|
||||
"fingerprint": desired_fingerprint,
|
||||
"last_attempt_at": 102.0,
|
||||
"last_success_at": 101.0,
|
||||
"summary": {"total": 1},
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
summary = {
|
||||
"total": 1,
|
||||
"unchanged": 0,
|
||||
"updated": 0,
|
||||
"recreated": 0,
|
||||
"created": 1,
|
||||
"deleted": 0,
|
||||
}
|
||||
sync = AsyncMock(side_effect=[asyncio.TimeoutError(), summary])
|
||||
monkeypatch.setattr(adapter, "_safe_sync_slash_commands", sync)
|
||||
|
||||
await adapter._run_post_connect_initialization()
|
||||
|
||||
timed_out_entry = json.loads(state_path.read_text(encoding="utf-8"))["999"]
|
||||
assert timed_out_entry["fingerprint"] == desired_fingerprint
|
||||
assert "last_success_at" not in timed_out_entry
|
||||
assert "summary" not in timed_out_entry
|
||||
|
||||
await adapter._run_post_connect_initialization()
|
||||
|
||||
assert sync.await_count == 2
|
||||
recovered_entry = json.loads(state_path.read_text(encoding="utf-8"))["999"]
|
||||
assert recovered_entry["last_success_at"] >= recovered_entry["last_attempt_at"]
|
||||
assert recovered_entry["summary"] == summary
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_connect_initialization_respects_discord_retry_after(tmp_path, monkeypatch):
|
||||
adapter = DiscordAdapter(PlatformConfig(enabled=True, token="test-token"))
|
||||
|
|
@ -788,7 +854,7 @@ async def test_post_connect_initialization_respects_discord_retry_after(tmp_path
|
|||
/ discord_platform._DISCORD_COMMAND_SYNC_STATE_SUBDIR
|
||||
/ discord_platform._DISCORD_COMMAND_SYNC_STATE_FILENAME
|
||||
)
|
||||
state = json.loads(state_path.read_text())
|
||||
state = json.loads(state_path.read_text(encoding="utf-8"))
|
||||
entry = state["999"]
|
||||
assert entry["retry_after"] == 123.0
|
||||
assert entry["retry_after_until"] > entry["last_attempt_at"]
|
||||
|
|
@ -828,7 +894,11 @@ async def test_post_connect_initialization_reraises_non_rate_limit_exceptions(tm
|
|||
/ discord_platform._DISCORD_COMMAND_SYNC_STATE_SUBDIR
|
||||
/ discord_platform._DISCORD_COMMAND_SYNC_STATE_FILENAME
|
||||
)
|
||||
state = json.loads(state_path.read_text()) if state_path.exists() else {}
|
||||
state = (
|
||||
json.loads(state_path.read_text(encoding="utf-8"))
|
||||
if state_path.exists()
|
||||
else {}
|
||||
)
|
||||
entry = state.get("4242", {})
|
||||
# Attempt was recorded before the sync call, but no rate-limit cooldown
|
||||
# should have been persisted from the unrelated exception.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue