From 0fae7e32aa058a1cf48ab61aa61b65af2d6a3cf0 Mon Sep 17 00:00:00 2001 From: Variet Worker Date: Tue, 17 Mar 2026 21:30:05 +0900 Subject: [PATCH] =?UTF-8?q?fix(ext,bot):=20=ED=86=B5=EC=8B=A0=20=EC=95=84?= =?UTF-8?q?=ED=82=A4=ED=85=8D=EC=B2=98=20=EA=B0=90=EC=82=AC=20=E2=80=94=20?= =?UTF-8?q?writeRegistration=20=EC=9D=B4=EC=A4=91=EC=93=B0=EA=B8=B0=20+=20?= =?UTF-8?q?ApprovalView=20fallback=20+=20scanner=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - step-probe.ts: writeRegistration WS 후 return 추가 (파일 이중쓰기 방지) - bot.py: ApprovalView approve/reject/choice — send_response_to_pending_owner 반환값 확인 + file bridge fallback (5곳) - bot.py: scanner 주기 3s/5s → 30s (Hub 모드 불필요 I/O 감소) --- .agents/references/known-issues.md | 11 +++++++++++ bot.py | 29 ++++++++++++++++------------- docs/devlog/2026-03-17.md | 3 ++- extension/src/step-probe.ts | 5 +++-- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/.agents/references/known-issues.md b/.agents/references/known-issues.md index 1af505f..4254310 100644 --- a/.agents/references/known-issues.md +++ b/.agents/references/known-issues.md @@ -718,3 +718,14 @@ - **해결**: `handleWSCommand`에 `ctx.recentDiscordSentTexts.set(text.trim(), Date.now())` 추가 - **주의**: 파일 기반 `_processCommandFile`에는 이미 마킹 있었음 (L178). WS 경로 추가 시 동일 패턴 적용 필수 +### [2026-03-17] writeRegistration 이중 쓰기 — WS 전송 후 파일도 작성 +- **증상**: WS 연결 상태에서도 `bridge/register/` 디렉토리에 등록 파일이 계속 생성됨 +- **원인**: `writeRegistration()` (step-probe.ts L162)이 WS 전송 후 `return` 없이 파일 쓰기도 실행. `writeChatSnapshot`/`writePendingApproval`은 WS→return 패턴인데 `writeRegistration`만 누락 +- **해결**: WS 전송 후 `return` 추가하여 파일 쓰기 건너뛰기 (v0.4.5) +- **주의**: 새 WS 전송 함수 추가 시 반드시 file fallback과 상호 배타적 `return` 확인 + +### [2026-03-17] ApprovalView Hub 응답 실패 시 silent loss — fallback 없음 +- **증상**: Extension이 pending 생성 후 WS 연결 끊김 → Discord에서 승인 클릭 → AG에 전달 안 됨 (무한 대기) +- **원인**: `ApprovalView` approve/reject/choice 콜백이 `if self.hub:` → `send_response_to_pending_owner()` 실행. 반환값 미확인. Hub가 있으면 file bridge를 아예 건너뛰므로 Hub 전달 실패 시 응답 소실 +- **해결**: `delivered = await hub.send_response_to_pending_owner()` 반환값 확인 → `False`면 `bridge.write_response()` fallback (4곳 + `_auto_approve_via_hub` 1곳, 총 5곳) +- **주의**: `send_response_to_pending_owner`는 `pending_owners`에 conn_id가 없으면 `False` 반환 (Extension 재연결/disconnect 시). Hub 존재 ≠ 전달 성공 diff --git a/bot.py b/bot.py index d268c58..6168c26 100644 --- a/bot.py +++ b/bot.py @@ -98,12 +98,13 @@ class ApprovalView(discord.ui.View): "project_name": getattr(self.request, 'project_name', ''), } # Hub WS route (primary — reaches remote Extensions) + delivered = False if self.hub: - await self.hub.send_response_to_pending_owner(self.request.request_id, { + delivered = await self.hub.send_response_to_pending_owner(self.request.request_id, { "type": "response", "data": response_data, }) - else: - # File bridge (fallback — only when Hub is unavailable) + if not delivered: + # File bridge fallback (Hub unavailable OR owner disconnected) self.bridge.write_response(UserResponse(**response_data)) embed = interaction.message.embeds[0] if interaction.message.embeds else None if embed: @@ -130,11 +131,12 @@ class ApprovalView(discord.ui.View): "step_type": getattr(self.request, 'step_type', ''), "project_name": getattr(self.request, 'project_name', ''), } + delivered = False if self.hub: - await self.hub.send_response_to_pending_owner(self.request.request_id, { + delivered = await self.hub.send_response_to_pending_owner(self.request.request_id, { "type": "response", "data": response_data, }) - else: + if not delivered: self.bridge.write_response(UserResponse(**response_data)) embed = interaction.message.embeds[0] if interaction.message.embeds else None if embed: @@ -156,11 +158,12 @@ class ApprovalView(discord.ui.View): "step_type": getattr(self.request, 'step_type', ''), "project_name": getattr(self.request, 'project_name', ''), } + delivered = False if self.hub: - await self.hub.send_response_to_pending_owner(self.request.request_id, { + delivered = await self.hub.send_response_to_pending_owner(self.request.request_id, { "type": "response", "data": response_data, }) - else: + if not delivered: self.bridge.write_response(UserResponse(**response_data)) embed = interaction.message.embeds[0] if interaction.message.embeds else None if embed: @@ -608,7 +611,7 @@ class GravityBot(commands.Bot): # ─── Approval Scanner ──────────────────────────────────────────── - @tasks.loop(seconds=3) + @tasks.loop(seconds=30) # Hub mode: WS is primary, file scan is fallback only async def pending_approval_scanner(self): """Scan bridge/pending/ for new approval requests + reload registrations. @@ -1081,8 +1084,9 @@ class GravityBot(commands.Bot): """Auto-approve a pending request via Hub.""" self._sent_approval_ids.add(request.request_id) + delivered = False if self.hub: - await self.hub.send_response_to_pending_owner(request.request_id, { + delivered = await self.hub.send_response_to_pending_owner(request.request_id, { "type": "response", "data": { "request_id": request.request_id, @@ -1092,9 +1096,8 @@ class GravityBot(commands.Bot): "project_name": request.project_name, }, }) - # Hub delivered — skip file bridge to prevent dual delivery - else: - # File bridge fallback (only when Hub is unavailable) + if not delivered: + # File bridge fallback (Hub unavailable OR owner disconnected) self.bridge.write_response(UserResponse( request_id=request.request_id, approved=True, step_type=request.step_type, @@ -1230,7 +1233,7 @@ class GravityBot(commands.Bot): # ─── Chat Snapshot Scanner ───────────────────────────────────────── - @tasks.loop(seconds=5) + @tasks.loop(seconds=30) # Hub mode: WS is primary, file scan is fallback only async def chat_snapshot_scanner(self): """Scan bridge/chat_snapshots/ for AI response dumps.""" try: diff --git a/docs/devlog/2026-03-17.md b/docs/devlog/2026-03-17.md index 5be4cac..8bfd0e7 100644 --- a/docs/devlog/2026-03-17.md +++ b/docs/devlog/2026-03-17.md @@ -8,7 +8,8 @@ | 012 | 09:00~17:44 | VSIX E2E: workspaceUri, 이중발송, ApprovalRequest, ApprovalView WS, 응답 라우팅 | `2eea5fa` | ✅ | | 013 | 18:05~18:45 | Extension 모듈 분리 #398: http-bridge, html-patcher, command-handler 추출 (1296→650줄) | `6640d42` | ✅ | | 014 | 18:45~20:35 | WS+File dual-delivery 수정 + 에코 릴레이 수정 + VSIX v0.4.4 빌드 | `0da6291` | ✅ | -| 015 | 20:45~21:00 | Accept All WS regression 수정 + auto_approve 이중쓰기 수정 + VSIX v0.4.5 | — | 🔧 | +| 015 | 20:45~21:00 | Accept All WS regression 수정 + auto_approve 이중쓰기 수정 + VSIX v0.4.5 | `47cc838` | ✅ | +| 016 | 21:00~21:27 | 통신 아키텍처 나노단위 감사: writeRegistration 이중쓰기 + ApprovalView fallback + scanner 최적화 | — | ✅ | ### #010 상세 - **문서**: architecture.md(250줄), tech-stack.md(100줄), conventions.md(100줄) 전면 재작성 + Wiki 동기화 diff --git a/extension/src/step-probe.ts b/extension/src/step-probe.ts index 47b1671..2c5193a 100644 --- a/extension/src/step-probe.ts +++ b/extension/src/step-probe.ts @@ -161,14 +161,15 @@ export async function handleDiffReviewResponse(data: { */ export function writeRegistration(sessionId: string) { try { - // WS route (preferred) + // WS route (preferred) — skip file write to prevent duplicate if (ctx.wsBridge && ctx.wsBridge.isConnected()) { ctx.wsBridge.sendRegister({ conversation_id: sessionId, project_name: ctx.projectName, }); + return; // WS delivered — skip file write } - // File route (fallback) + // File route (fallback — only when WS is NOT connected) const regDir = path.join(ctx.bridgePath, 'register'); if (!fs.existsSync(regDir)) { fs.mkdirSync(regDir, { recursive: true }); } const regFile = path.join(regDir, `${sessionId}.json`);