fix: atomic Slack approval guard, safe JSON deserialization fallbacks

1. gateway/platforms/slack.py: Replace check-then-set TOCTOU race on
   _approval_resolved with atomic dict.pop(). Two concurrent button
   clicks could both pass the guard before either set it to True,
   causing double resolve_gateway_approval — which can resolve the
   WRONG queued approval when multiple are pending for the same session.

2. hermes_state.py: Add WARNING log and proper fallbacks when
   json.loads fails on tool_calls (→ []), reasoning_details (→ None),
   and codex_reasoning_items (→ None). Previously, failures were
   silently swallowed: tool_calls stayed as a raw string (iterating
   yields characters, not objects), and reasoning fields were simply
   missing from the dict.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
aaronagent 2026-04-10 00:20:35 +08:00 committed by Teknium
parent c6974fd108
commit ab7b407224
2 changed files with 11 additions and 9 deletions

View file

@ -1239,10 +1239,9 @@ class SlackAdapter(BasePlatformAdapter):
}
choice = choice_map.get(action_id, "deny")
# Prevent double-clicks
if self._approval_resolved.get(msg_ts, False):
# Prevent double-clicks — atomic pop; first caller gets False, others get True (default)
if self._approval_resolved.pop(msg_ts, True):
return
self._approval_resolved[msg_ts] = True
# Update the message to show the decision and remove buttons
label_map = {
@ -1297,8 +1296,7 @@ class SlackAdapter(BasePlatformAdapter):
except Exception as exc:
logger.error("Failed to resolve gateway approval from Slack button: %s", exc)
# Clean up stale approval state
self._approval_resolved.pop(msg_ts, None)
# (approval state already consumed by atomic pop above)
# ----- Thread context fetching -----

View file

@ -944,7 +944,8 @@ class SessionDB:
try:
msg["tool_calls"] = json.loads(msg["tool_calls"])
except (json.JSONDecodeError, TypeError):
pass
logger.warning("Failed to deserialize tool_calls in get_messages, falling back to []")
msg["tool_calls"] = []
result.append(msg)
return result
@ -972,7 +973,8 @@ class SessionDB:
try:
msg["tool_calls"] = json.loads(row["tool_calls"])
except (json.JSONDecodeError, TypeError):
pass
logger.warning("Failed to deserialize tool_calls in conversation replay, falling back to []")
msg["tool_calls"] = []
# Restore reasoning fields on assistant messages so providers
# that replay reasoning (OpenRouter, OpenAI, Nous) receive
# coherent multi-turn reasoning context.
@ -983,12 +985,14 @@ class SessionDB:
try:
msg["reasoning_details"] = json.loads(row["reasoning_details"])
except (json.JSONDecodeError, TypeError):
pass
logger.warning("Failed to deserialize reasoning_details, falling back to None")
msg["reasoning_details"] = None
if row["codex_reasoning_items"]:
try:
msg["codex_reasoning_items"] = json.loads(row["codex_reasoning_items"])
except (json.JSONDecodeError, TypeError):
pass
logger.warning("Failed to deserialize codex_reasoning_items, falling back to None")
msg["codex_reasoning_items"] = None
messages.append(msg)
return messages