fix(steering): skip phantom zero-turn result on --resume; guard empty replies
On --resume, a background task stopped by the previous restart is replayed as its own zero-turn turn with an empty result, before the real turn. consume_stream took it as the answer -> empty Discord message (400) -> "Sorry, something went wrong", while the real turn ran orphaned. - stream_json: skip result with num_turns=0, empty, non-error - router: empty Claude response becomes a visible notice, not an adapter crash Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014y1sJNe6mkWwFMwkWmCg8J
This commit is contained in:
@@ -616,6 +616,11 @@ def route_message(
|
||||
# Same pattern as the existing __AUDIO__: sentinel — no
|
||||
# _set_last_response, the adapter reacts instead of replying.
|
||||
return response, False
|
||||
if not (response or "").strip():
|
||||
# Adapters can't send an empty message (Discord 400 → generic
|
||||
# "something went wrong"); say so instead of failing opaquely.
|
||||
log.warning("channel=%s: Claude returned an empty response", channel_id)
|
||||
return "⚠️ Claude a terminat turul fără niciun răspuns text. Mai trimite o dată mesajul.", False
|
||||
_set_last_response(channel_id, response)
|
||||
return response, False
|
||||
except Exception as e:
|
||||
|
||||
@@ -102,6 +102,14 @@ def consume_stream(
|
||||
logger.exception("on_init callback error")
|
||||
|
||||
elif msg_type == "result":
|
||||
# On --resume, a leftover background-task notification is
|
||||
# replayed as its own zero-turn "turn" with an empty result,
|
||||
# BEFORE the real one. Taking it as the answer returned "" (Discord
|
||||
# 400 "Cannot send an empty message") and orphaned the real turn.
|
||||
if (event.get("num_turns") == 0 and not event.get("is_error")
|
||||
and not event.get("result") and not text_blocks):
|
||||
logger.info("skipping zero-turn result (task notification replay)")
|
||||
continue
|
||||
result_obj = event
|
||||
break # never read past the turn boundary
|
||||
|
||||
@@ -146,6 +154,16 @@ def demo() -> None:
|
||||
# Blank/garbage lines are skipped, not fatal.
|
||||
assert consume_stream(["", "not json", json.dumps({"type": "result", "result": "ok"})])["result"] == "ok"
|
||||
|
||||
# --resume replays a stopped background task as a zero-turn empty result
|
||||
# before the real turn — skip it, return the real one.
|
||||
phantom = [
|
||||
json.dumps({"type": "system", "subtype": "task_notification", "status": "stopped"}),
|
||||
json.dumps({"type": "result", "result": "", "num_turns": 0, "is_error": False}),
|
||||
json.dumps({"type": "assistant", "message": {"content": [{"type": "text", "text": "PONG"}]}}),
|
||||
json.dumps({"type": "result", "result": "PONG", "num_turns": 1, "is_error": False}),
|
||||
]
|
||||
assert consume_stream(phantom)["result"] == "PONG"
|
||||
|
||||
print("stream_json self-check OK")
|
||||
|
||||
|
||||
|
||||
@@ -349,3 +349,8 @@ def test_max_live_non_positive_logs_and_always_degrades(caplog):
|
||||
registry = claude_runner.RunnerRegistry(max_live=0, idle_minutes=20)
|
||||
assert any("max_live" in rec.message for rec in caplog.records)
|
||||
assert registry.get("any-channel") is None
|
||||
|
||||
|
||||
def test_stream_json_self_check_skips_phantom_resume_result():
|
||||
from src import stream_json
|
||||
stream_json.demo()
|
||||
|
||||
@@ -195,6 +195,13 @@ class TestRegularMessage:
|
||||
assert is_cmd is False
|
||||
mock_send.assert_called_once_with("ch-1", "hello", model="sonnet", on_text=None, voice_mode=False, adapter_name=None)
|
||||
|
||||
@patch("src.router._get_channel_config", return_value=None)
|
||||
@patch("src.router.send_message", return_value=" ")
|
||||
def test_empty_response_becomes_notice(self, mock_send, _chan):
|
||||
response, is_cmd = route_message("ch-1", "user-1", "hello", model="sonnet")
|
||||
assert response.strip()
|
||||
assert is_cmd is False
|
||||
|
||||
@patch("src.router.send_message")
|
||||
def test_model_override(self, mock_send):
|
||||
mock_send.return_value = "Response"
|
||||
|
||||
Reference in New Issue
Block a user