diff --git a/.agents/references/known-issues.md b/.agents/references/known-issues.md index 74c0439..5d3e19b 100644 --- a/.agents/references/known-issues.md +++ b/.agents/references/known-issues.md @@ -327,3 +327,15 @@ - **해결**: `trajectoryMetadata.workspaces[0].workspaceFolderAbsoluteUri`를 `vscode.workspace.workspaceFolders[0].uri.fsPath`와 비교하여 자기 workspace 세션만 처리. register 파일은 metadata 없는 레거시 세션용 fallback으로 유지 - **주의**: workspace URI는 `file:///c:/Users/...` 형식이므로 normalize 필수 (protocol strip, %3A decode, 슬래시 통일, lowercase). 양쪽 AG 풀 재시작 필요 +### [2026-03-10] 크로스 프로젝트 Response Watcher 우회 +- **증상**: Deriva 세션에 대한 승인 시도가 gravity_control에서 `stepIndex:-1`로 반복 실패 +- **원인**: (1) pending 파일 삭제 후 response watcher가 project_name 체크 건너뜀, (2) `processResponseFile`이 전역 `activeSessionId` 사용 +- **해결**: (1) response JSON의 `project_name`으로 fallback 필터, (2) 세션ID를 pending에서 우선 사용, (3) `logToFile`에 `[projectName]` 접두사, (4) `UserResponse`에 `project_name` 추가 +- **주의**: response 데이터 자체에 project_name 필수. auto_resolve로 pending 삭제 시 우회됨 + +### [2026-03-10] file_permission — write 도구 3-button 미주입 +- **증상**: `replace_file_content` 등 파일 수정 시 AG에선 3개 버튼(Deny/Allow Once/Allow This Conversation) 표시, Discord에선 2개(승인/거부)만 표시 +- **원인**: step_probe의 `file_permission` 도구 리스트에 read 도구만 포함 (`view_file`, `grep_search` 등). `replace_file_content`, `write_to_file`, `multi_replace_file_content` 누락 +- **해결**: 세 write 도구를 file_permission 리스트에 추가 (2곳: offset/normal scan) +- **주의**: AG가 파일 접근 권한을 요청하는 모든 도구는 이 리스트에 포함되어야 함 + diff --git a/bot.py b/bot.py index a36025c..11fa6f4 100644 --- a/bot.py +++ b/bot.py @@ -87,6 +87,7 @@ class ApprovalView(discord.ui.View): approved=not is_reject, button_index=btn_index, step_type=getattr(self.request, 'step_type', ''), + project_name=getattr(self.request, 'project_name', ''), )) embed = interaction.message.embeds[0] if interaction.message.embeds else None if embed: @@ -111,6 +112,7 @@ class ApprovalView(discord.ui.View): self.bridge.write_response(UserResponse( request_id=self.request.request_id, approved=True, step_type=getattr(self.request, 'step_type', ''), + project_name=getattr(self.request, 'project_name', ''), )) embed = interaction.message.embeds[0] if interaction.message.embeds else None if embed: @@ -130,6 +132,7 @@ class ApprovalView(discord.ui.View): self.bridge.write_response(UserResponse( request_id=self.request.request_id, approved=False, step_type=getattr(self.request, 'step_type', ''), + project_name=getattr(self.request, 'project_name', ''), )) embed = interaction.message.embeds[0] if interaction.message.embeds else None if embed: @@ -142,6 +145,7 @@ class ApprovalView(discord.ui.View): self.bridge.write_response(UserResponse( request_id=self.request.request_id, approved=False, step_type=getattr(self.request, 'step_type', ''), + project_name=getattr(self.request, 'project_name', ''), )) diff --git a/bridge.py b/bridge.py index 48f937e..84e4cfa 100644 --- a/bridge.py +++ b/bridge.py @@ -56,6 +56,7 @@ class UserResponse: timestamp: float = 0 button_index: int = -1 # -1 = legacy (approve/reject), 0+ = specific button index step_type: str = "" # pass through from pending for extension routing + project_name: str = "" # for multi-project: extension uses this when pending file is missing class BridgeProtocol: diff --git a/docs/devlog/2026-03-10.md b/docs/devlog/2026-03-10.md index 609fd30..904d4cc 100644 --- a/docs/devlog/2026-03-10.md +++ b/docs/devlog/2026-03-10.md @@ -13,3 +13,4 @@ | 009 | 17:20~17:47 | v0.3.6 릴리스 — VSIX 빌드 + start_bot.bat 런처 | `bd46bea` | ✅ | | 010 | 18:00~18:30 | v0.3.7 — file_permission 3-button 주입 + active_project.lock 제거 (멀티프로젝트) | `27deb2a` | ✅ | | 011 | 18:50~19:29 | v0.3.8 — workspace URI 기반 세션 필터링 (멀티프로젝트 격리 완성) | `ae91134` | ✅ | +| 012 | 19:30~20:35 | 크로스 프로젝트 response watcher 우회 수정 + file_permission write 도구 3-button 매핑 | `3b834e0` | ✅ | diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 779d661..2d474c0 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -20,8 +20,10 @@ import * as crypto from 'crypto'; // ─── File-based logging (AI can read directly) ─── function logToFile(msg: string) { const ts = new Date().toISOString().replace('T', ' ').substring(0, 19); - const line = `${ts} ${msg}`; - console.log(`Gravity Bridge: ${msg}`); + // Include projectName prefix so shared log can distinguish which extension instance logged + const prefix = projectName ? `[${projectName}]` : ''; + const line = `${ts} ${prefix} ${msg}`; + console.log(`Gravity Bridge: ${prefix} ${msg}`); try { if (!bridgePath) return; const logFile = path.join(bridgePath, 'extension.log'); @@ -1708,7 +1710,7 @@ function setupMonitor() { conversation_id: activeSessionId, command, description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`, - step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission' : toolName, + step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search', 'replace_file_content', 'write_to_file', 'multi_replace_file_content'].includes(toolName) ? 'file_permission' : toolName, step_index: actualIndex, source: 'step_probe_offset', }); @@ -1763,7 +1765,7 @@ function setupMonitor() { conversation_id: activeSessionId, command, description, - step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission' : toolName, + step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search', 'replace_file_content', 'write_to_file', 'multi_replace_file_content'].includes(toolName) ? 'file_permission' : toolName, step_index: si, source: 'step_probe', }); @@ -2052,6 +2054,15 @@ function setupResponseWatcher() { return; // Not our project } } catch { } + } else { + // Pending file missing (deleted or auto_resolved) — check response data itself + try { + const respData = JSON.parse(fs.readFileSync(fp, 'utf-8')); + if (respData.project_name && respData.project_name !== projectName) { + logToFile(`[RESPONSE] skip (from resp data) ${rid} (project=${respData.project_name}, we=${projectName})`); + return; + } + } catch { } } setTimeout(() => processResponseFile(fp), 300); } @@ -2160,13 +2171,17 @@ async function processResponseFile(filePath: string) { } } else if (isDomObserver) { // DOM observer path: ALSO try RPC strategies (renderer click is unreliable) - logToFile(`[RESPONSE] dom_observer → tryApprovalStrategies(${approved}, ${activeSessionId.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`); - const strategyResult = await tryApprovalStrategies(approved, activeSessionId, pendingStepType, pendingStepIndex); + // Use sessionId from pending file if available, fallback to activeSessionId + const targetSession = sessionId || activeSessionId; + logToFile(`[RESPONSE] dom_observer → tryApprovalStrategies(${approved}, ${targetSession.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`); + const strategyResult = await tryApprovalStrategies(approved, targetSession, pendingStepType, pendingStepIndex); logToFile(`[RESPONSE] dom strategy result: ${strategyResult}`); } else { // Step probe path: run ALL approval strategies - logToFile(`[RESPONSE] step_probe → tryApprovalStrategies(${approved}, ${activeSessionId.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`); - const strategyResult = await tryApprovalStrategies(approved, activeSessionId, pendingStepType, pendingStepIndex); + // Use sessionId from pending file if available, fallback to activeSessionId + const targetSession = sessionId || activeSessionId; + logToFile(`[RESPONSE] step_probe → tryApprovalStrategies(${approved}, ${targetSession.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`); + const strategyResult = await tryApprovalStrategies(approved, targetSession, pendingStepType, pendingStepIndex); logToFile(`[RESPONSE] strategy result: ${strategyResult}`); }