fix(ext,bot): 통신 아키텍처 감사 — writeRegistration 이중쓰기 + ApprovalView fallback + scanner 최적화
- 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 감소)
This commit is contained in:
@@ -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 존재 ≠ 전달 성공
|
||||
|
||||
29
bot.py
29
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:
|
||||
|
||||
@@ -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 동기화
|
||||
|
||||
@@ -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`);
|
||||
|
||||
Reference in New Issue
Block a user