fix: retire clarify cards on an explicit no-answer signal, not the '[' prefix

`_clarify_callback_sync` decided "no answer arrived" by testing whether the
response text starts with '[' (the shape of the timeout / undeliverable
sentinels). A real answer can start with '[' too — a "[A] staging" choice
label picked by number, or "[urgent] ..." free text after Other — so the
clarify resolved and the agent got the answer, yet the Slack card was
rewritten to "This prompt expired" and typing was never re-armed.
`_clarify_send_then_wait` now returns `(response, answered)` and the runner
branches on that flag only.

The Slack click handler popped the retire entry as soon as Other was
clicked, but Other is not terminal: the clarify stays pending for typed
text, so a later timeout or /new reset found nothing to retire and the card
stayed stuck on "Awaiting typed answer". The entry is now popped only on a
terminal outcome (a choice click, or Other on an already-dead entry).

A typed answer to a native card (numeric pick, or text after Other) never
reaches the click handler, so the card kept its buttons forever; the
TEXT_RESOLVED intercept now retires it with the answer.

Review finding: '[' prefix mistaken for the timeout sentinel; Other click dropped the retire entry; typed answers never rewrote the card.
This commit is contained in:
teknium1
2026-09-14 21:29:26 -07:00
committed by Teknium
parent 71cac9426d
commit 6a22abe5ba
8 changed files with 99 additions and 17 deletions

View File

@@ -5445,7 +5445,6 @@ class SlackAdapter(BasePlatformAdapter):
# Double-click guard — atomic pop (mirrors approval).
if self._clarify_resolved.pop(msg_ts, True):
return
self._clarify_messages.pop(clarify_id, None)
original_text = self._section_text(message, limit=None)
from tools import clarify_gateway as _clarify_mod
# "Other" → text-capture mode: mark_awaiting_text flips the entry and the
@@ -5454,8 +5453,11 @@ class SlackAdapter(BasePlatformAdapter):
if action_id == "hermes_clarify_other" or token == "other":
if not _clarify_mod.mark_awaiting_text(clarify_id):
# Entry evicted/gateway restarted — a typed answer would go nowhere.
self._clarify_messages.pop(clarify_id, None)
await self._update_clarify_message(channel_id, msg_ts, original_text, expired_text)
return
# Not terminal: the clarify stays pending for typed text, so keep the card entry —
# the gateway still has to retire it on timeout / reset / typed answer.
await self._update_clarify_message(
channel_id, msg_ts, original_text, f"✏️ Awaiting typed answer from {user_name}…")
return
@@ -5464,6 +5466,8 @@ class SlackAdapter(BasePlatformAdapter):
except (ValueError, TypeError):
logger.warning("[Slack] Invalid clarify choice token: %s", token)
return
# A choice click is terminal either way (✅ or expired): the card no longer needs retiring.
self._clarify_messages.pop(clarify_id, None)
# Canonical choice text from the entry; positional fallback on timeout/reset race.
resolved_text: Optional[str] = None
try: