fix(bridge): approval flow robustness — pending cleanup, MERGE dedup, false positive filter, auto_resolve, 30min timeout

This commit is contained in:
2026-03-10 00:35:41 +09:00
parent 7fdefb0c63
commit 373c0f7ddc
8 changed files with 340 additions and 22 deletions

49
bot.py
View File

@@ -35,7 +35,7 @@ class ApprovalView(discord.ui.View):
"""Discord buttons for approving/rejecting Antigravity actions."""
def __init__(self, bridge: BridgeProtocol, request: ApprovalRequest):
super().__init__(timeout=300)
super().__init__(timeout=1800) # 30 minutes
self.bridge = bridge
self.request = request
self.responded = False
@@ -100,6 +100,7 @@ class GravityBot(commands.Bot):
self.channel_to_project: dict[int, str] = {} # channel.id → project
self.session_status_messages: dict[str, int] = {} # conv_id → msg_id
self._sent_approval_ids: set[str] = set()
self._deferred_ids: dict[str, int] = {} # request_id → defer count
self._ready_event = asyncio.Event()
self._channel_lock = asyncio.Lock()
self.bridge = BridgeProtocol()
@@ -450,6 +451,25 @@ class GravityBot(commands.Bot):
if req.discord_message_id != 0:
continue
# Defer short-command pendings (e.g. "Run") by 4 cycles (~12s)
# to give step_probe time to merge detailed command info
# (step_probe MERGE happens ~10s after pending creation)
if len(req.command) <= 15:
if req.request_id not in self._deferred_ids:
self._deferred_ids[req.request_id] = 1
continue # skip this cycle
elif self._deferred_ids[req.request_id] < 4:
self._deferred_ids[req.request_id] += 1
# Re-read from file (step_probe may have merged)
fresh = self.bridge.read_pending_request(req.request_id)
if fresh and len(fresh.command) > 15:
req = fresh # use merged version — send now!
else:
continue # wait one more cycle
# Clean up defer tracking
self._deferred_ids.pop(req.request_id, None)
# Learn project mapping from pending approval
project = req.project_name or Config.PROJECT_NAME
if req.conversation_id and req.conversation_id != '__global__':
@@ -459,6 +479,33 @@ class GravityBot(commands.Bot):
if channel:
self._sent_approval_ids.add(req.request_id)
await self._send_approval_request(channel, req)
# ── Check for auto_resolved pendings (approved directly in AG) ──
for f in self.bridge.pending_dir.glob("*.json"):
try:
data = json.loads(f.read_text(encoding="utf-8-sig"))
if data.get("status") == "auto_resolved":
msg_id = data.get("discord_message_id", 0)
project = data.get("project_name", Config.PROJECT_NAME)
if msg_id:
channel = await self._get_channel(project)
if channel:
try:
msg = await channel.fetch_message(msg_id)
embed = discord.Embed(
title="✅ AG에서 직접 승인됨",
description=f"```\n{data.get('command', '')[:500]}\n```",
color=discord.Color.green(),
)
embed.set_footer(text=f"ID: {data.get('request_id', '')}")
await msg.edit(embed=embed, view=None)
except discord.NotFound:
pass
f.unlink()
self._deferred_ids.pop(data.get("request_id", ""), None)
except (json.JSONDecodeError, OSError):
pass
except Exception as e:
logger.error(f"Error scanning approvals: {e}")