fix(bridge): v0.3.11 approval flow architecture fix — eliminate double-fire auto-approve, strip 30+ failed RPC strategies, add project_name DEDUP guard
- Remove Extension-side auto-approve (was double-firing with Bot auto-approve) - Strip failed strategies 0A-1 from tryApprovalStrategies (~150 lines) - Keep only Strategy 0-PROTO (proto RPC) + Strategy 2 (clickTrigger) - Add bot.py AUTO-RESOLVED logging for diagnostics - Update known-issues with 3 new entries - Clean deployment: v0.3.8→v0.3.10→v0.3.11
This commit is contained in:
@@ -477,3 +477,22 @@
|
||||
- **원인 2**: `processResponseFile()`이 pending의 `auto_resolved`/`expired` 상태를 체크하지 않음 → 상태 확인 후 skip 로직 추가
|
||||
- **원인 3**: Bot의 auto_resolved 스캐너가 `discord_message_id`에만 의존 — Extension은 이 값을 모름 → `_approval_messages` dict (rid→msg_id) 추가, fallback 조회
|
||||
- **주의**: `processResponseFile` L2534의 `lastPendingStepIndex = -1` 리셋이 Discord 승인 경로에서 auto_resolve 중복 진입을 방지하는 핵심 gate. 이 줄을 삭제하면 중복 알림 발생
|
||||
|
||||
### [2026-03-15] Extension 버전 미배포 — source ≠ deployed
|
||||
- **증상**: 소스(v0.3.10)에 수정한 코드가 실제 동작하지 않음. 로그에서 수정 전 동작 확인됨
|
||||
- **원인**: Extension 빌드/배포 누락. `~/.antigravity/extensions/`에 구 버전(v0.3.8) 남아있음
|
||||
- **해결**: VSIX 빌드 → 설치 → 구 버전 디렉토리 삭제 → AG 전체 재시작 (Reload Window 불충분)
|
||||
- **주의**: Extension 코드 수정 후 **반드시** `npm run compile && npx vsce package` → 배포까지 확인. AG는 전체 File→Exit 후 재시작 필요
|
||||
|
||||
### [2026-03-15] 크로스 프로젝트 DEDUP MERGE — Deriva→gravity_control 오염
|
||||
- **증상**: Deriva의 step_probe 데이터(step_index, command)가 gravity_control의 DOM observer pending에 MERGE됨. Discord에 Deriva 명령이 gravity_control 채널에 표시
|
||||
- **원인**: `writePendingApproval()` DEDUP MERGE 조건에 `project_name` 가드 없음 — `source === 'dom_observer' && status === 'pending'`만 검사하므로 타 프로젝트 pending에도 MERGE
|
||||
- **해결**: MERGE 조건에 `existing.project_name === projectName` 추가 (v0.3.10)
|
||||
- **주의**: `bridge/pending/` 디렉토리는 모든 Extension 인스턴스가 공유. 파일 읽기/쓰기 시 반드시 `project_name` 기반 필터링 필수
|
||||
|
||||
### [2026-03-15] Double-Fire Auto-Approve — AI 세션 중단
|
||||
- **증상**: auto-approve ON 시 AI 세션이 간헐적으로 중단/멈춤
|
||||
- **원인**: step_probe가 WAITING 감지 시 `autoApproveEnabled`면 직접 `tryApprovalStrategies()` 호출(경로A). 동시에 `writePendingApproval()` → Bot auto_approve_scanner → response 파일 → `processResponseFile()` → `tryApprovalStrategies()` 호출(경로B). 같은 step에 대해 2번 RPC 호출 → 충돌
|
||||
- **해결**: Extension auto-approve 경로 A 제거. Bot만 auto-approve 담당 (v0.3.11). Extension은 항상 `writePendingApproval()` 경로 사용
|
||||
- **주의**: 향후 Extension에서 직접 approve 로직을 추가할 때는 Bot auto-approve와의 경합을 반드시 고려. 단일 경로 원칙 유지
|
||||
|
||||
|
||||
6
bot.py
6
bot.py
@@ -622,6 +622,7 @@ class GravityBot(commands.Bot):
|
||||
# FIX #5: Use _approval_messages as fallback when discord_message_id is 0
|
||||
msg_id = data.get("discord_message_id", 0) or self._approval_messages.get(rid, 0)
|
||||
project = data.get("project_name", Config.PROJECT_NAME)
|
||||
logger.info(f"[AUTO-RESOLVED] rid={rid[:12]} project={project} msg_id={msg_id} cmd='{data.get('command', '')[:60]}'")
|
||||
if msg_id:
|
||||
channel = await self._get_channel(project)
|
||||
if channel:
|
||||
@@ -634,8 +635,11 @@ class GravityBot(commands.Bot):
|
||||
)
|
||||
embed.set_footer(text=f"ID: {rid}")
|
||||
await msg.edit(embed=embed, view=None)
|
||||
logger.info(f"[AUTO-RESOLVED] ✅ Discord message {msg_id} updated")
|
||||
except discord.NotFound:
|
||||
pass
|
||||
logger.warning(f"[AUTO-RESOLVED] Discord message {msg_id} not found")
|
||||
else:
|
||||
logger.warning(f"[AUTO-RESOLVED] No msg_id for rid={rid[:12]} — cannot edit Discord message")
|
||||
f.unlink()
|
||||
self._deferred_ids.pop(rid, None)
|
||||
self._sent_commands.pop(rid, None)
|
||||
|
||||
@@ -6,3 +6,5 @@
|
||||
| 002 | 08:25~08:31 | Extension v0.3.10 버전 범프 & VSIX 빌드 | `10caae1` | ✅ |
|
||||
| 003 | 10:00~10:41 | 승인 라이프사이클 race condition 4건 수정 (HTML lock, pending status skip, auto_resolve Discord 알림, Bot approval_messages) | `f962036` | ✅ |
|
||||
| 004 | 10:41~10:53 | 성능 최적화 3건 (pollResponseGroup 1500ms, renderer adaptive idle, Bot single-pass scanner) + VSIX 빌드 | `ae0509f` | ✅ |
|
||||
| 005 | 15:17~17:09 | 크로스 프로젝트 신호 오염 진단 & 승인 플로우 아키텍처 수정 — DEDUP project_name 가드, double-fire auto-approve 제거, 실패 RPC 전략 30+개 삭제 (v0.3.11) | - | 🔧 |
|
||||
|
||||
|
||||
@@ -2045,12 +2045,8 @@ function setupMonitor() {
|
||||
if (projectName === 'default') {
|
||||
logToFile(`[STEP-PROBE] skip pending: projectName=default (no workspace)`);
|
||||
}
|
||||
else if (autoApproveEnabled) {
|
||||
// Auto-approve: skip Discord, approve directly
|
||||
logToFile(`[AUTO] auto-approving step=${actualIndex} cmd='${command.substring(0, 60)}'`);
|
||||
tryApprovalStrategies(true, activeSessionId, ['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, actualIndex);
|
||||
}
|
||||
else {
|
||||
// Always write pending — Bot decides auto-approve (prevents double-fire)
|
||||
writePendingApproval({
|
||||
conversation_id: activeSessionId,
|
||||
command,
|
||||
@@ -2112,12 +2108,8 @@ function setupMonitor() {
|
||||
if (projectName === 'default') {
|
||||
logToFile(`[STEP-PROBE] skip pending: projectName=default (no workspace)`);
|
||||
}
|
||||
else if (autoApproveEnabled) {
|
||||
// Auto-approve: skip Discord, approve directly
|
||||
logToFile(`[AUTO] auto-approving step=${si} cmd='${command.substring(0, 60)}'`);
|
||||
tryApprovalStrategies(true, activeSessionId, ['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, si);
|
||||
}
|
||||
else {
|
||||
// Always write pending — Bot decides auto-approve (prevents double-fire)
|
||||
writePendingApproval({
|
||||
conversation_id: activeSessionId,
|
||||
command,
|
||||
@@ -2974,155 +2966,10 @@ async function tryApprovalStrategies(approved, sessionId, stepType = '', stepInd
|
||||
}
|
||||
}
|
||||
}
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0A: executeCascadeAction
|
||||
// ══════════════════════════════════════════════════════════
|
||||
const cascadeActionVariants = [
|
||||
{ action: approved ? 'accept' : 'reject' },
|
||||
{ action: approved ? 'ACCEPT' : 'REJECT' },
|
||||
{ action: approved ? 'approve' : 'deny' },
|
||||
{ action: approved ? 'run' : 'reject' },
|
||||
{ cascadeId: sessionId, action: approved ? 'accept' : 'reject' },
|
||||
{ cascadeId: sessionId, type: approved ? 'accept' : 'reject' },
|
||||
approved ? 'accept' : 'reject', // plain string arg
|
||||
approved ? 1 : 0, // numeric arg
|
||||
];
|
||||
for (let i = 0; i < cascadeActionVariants.length; i++) {
|
||||
try {
|
||||
const arg = cascadeActionVariants[i];
|
||||
logToFile(`[APPROVAL-0A-${i}] executeCascadeAction(${JSON.stringify(arg)})`);
|
||||
const result = await vscode.commands.executeCommand('antigravity.executeCascadeAction', arg);
|
||||
logToFile(`[APPROVAL-0A-${i}] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||
return `CMD-0A:executeCascadeAction(variant=${i})`;
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-0A-${i}] ❌ ${e.message.substring(0, 100)}`);
|
||||
}
|
||||
}
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0B: Direct approval commands (brute-force try all 7 SDK commands)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
const commandsToTry = approved
|
||||
? [
|
||||
'antigravity.terminalCommand.run',
|
||||
'antigravity.terminalCommand.accept',
|
||||
'antigravity.agent.acceptAgentStep',
|
||||
'antigravity.command.accept',
|
||||
]
|
||||
: [
|
||||
'antigravity.terminalCommand.reject',
|
||||
'antigravity.agent.rejectAgentStep',
|
||||
'antigravity.command.reject',
|
||||
];
|
||||
for (const cmd of commandsToTry) {
|
||||
// Try with no args, then with sessionId
|
||||
for (const args of [[], [sessionId], [{ cascadeId: sessionId }]]) {
|
||||
try {
|
||||
logToFile(`[APPROVAL-0B] ${cmd}(${JSON.stringify(args)})`);
|
||||
const result = await vscode.commands.executeCommand(cmd, ...args);
|
||||
logToFile(`[APPROVAL-0B] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||
return `CMD-0B:${cmd}`;
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-0B] ❌ ${e.message.substring(0, 80)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0C: Electron webContents access (pierce webview isolation)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
try {
|
||||
logToFile(`[APPROVAL-0C] Attempting Electron webContents access...`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const electron = require('electron');
|
||||
const remote = electron.remote;
|
||||
if (remote) {
|
||||
logToFile(`[APPROVAL-0C] electron.remote available!`);
|
||||
const allWC = remote.webContents.getAllWebContents();
|
||||
logToFile(`[APPROVAL-0C] Found ${allWC.length} webContents`);
|
||||
for (const wc of allWC) {
|
||||
const wcUrl = wc.getURL() || '';
|
||||
logToFile(`[APPROVAL-0C] wc: ${wcUrl.substring(0, 100)}`);
|
||||
if (wcUrl.includes('vscode-webview://') || wcUrl.includes('webview')) {
|
||||
const clickScript = approved
|
||||
? `(function(){ var btns = document.querySelectorAll('button'); for(var b of btns){ if(/Run|Accept|Allow/i.test(b.textContent)){ b.click(); return 'clicked:'+b.textContent; } } return 'no-match'; })()`
|
||||
: `(function(){ var btns = document.querySelectorAll('button'); for(var b of btns){ if(/Reject|Deny|Cancel/i.test(b.textContent)){ b.click(); return 'clicked:'+b.textContent; } } return 'no-match'; })()`;
|
||||
const clickResult = await wc.executeJavaScript(clickScript);
|
||||
logToFile(`[APPROVAL-0C] executeJavaScript result: ${clickResult}`);
|
||||
if (clickResult && clickResult.startsWith('clicked:')) {
|
||||
return `ELECTRON-0C:webContents(${clickResult})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
logToFile(`[APPROVAL-0C] electron.remote NOT available`);
|
||||
// Try without remote (main process context)
|
||||
const wc = electron.webContents;
|
||||
if (wc && typeof wc.getAllWebContents === 'function') {
|
||||
logToFile(`[APPROVAL-0C] Direct electron.webContents available!`);
|
||||
const allWC = wc.getAllWebContents();
|
||||
logToFile(`[APPROVAL-0C] Found ${allWC.length} webContents`);
|
||||
}
|
||||
else {
|
||||
logToFile(`[APPROVAL-0C] No webContents access from Extension Host`);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-0C] ❌ ${e.message.substring(0, 150)}`);
|
||||
}
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0D: sendChatActionMessage (may route to agent)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
const chatActionVariants = [
|
||||
{ action: approved ? 'accept' : 'reject', cascadeId: sessionId },
|
||||
{ message: approved ? 'accept' : 'reject', conversationId: sessionId },
|
||||
approved ? 'accept' : 'reject',
|
||||
];
|
||||
for (let i = 0; i < chatActionVariants.length; i++) {
|
||||
try {
|
||||
logToFile(`[APPROVAL-0D-${i}] sendChatActionMessage(${JSON.stringify(chatActionVariants[i])})`);
|
||||
const result = await vscode.commands.executeCommand('antigravity.sendChatActionMessage', chatActionVariants[i]);
|
||||
logToFile(`[APPROVAL-0D-${i}] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||
return `CMD-0D:sendChatActionMessage(variant=${i})`;
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-0D-${i}] ❌ ${e.message.substring(0, 80)}`);
|
||||
}
|
||||
}
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 1: HandleCascadeUserInteraction RPC (expanded variants)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
if (sdk) {
|
||||
const rpcVariants = [
|
||||
// Original variants
|
||||
{ cascadeId: sessionId, approved: approved },
|
||||
{ cascadeId: sessionId, stepAction: approved ? 'STEP_ACTION_ACCEPT' : 'STEP_ACTION_REJECT' },
|
||||
{ cascadeId: sessionId, userAction: approved ? 'USER_ACTION_APPROVED' : 'USER_ACTION_REJECTED' },
|
||||
// New variants — ConnectRPC protobuf field naming conventions
|
||||
{ cascade_id: sessionId, accepted: approved },
|
||||
{ cascadeId: sessionId, accepted: approved },
|
||||
{ cascade_id: sessionId, user_action: approved ? 1 : 2 }, // enum as int
|
||||
{ cascadeId: sessionId, interaction: approved ? 'ACCEPT' : 'REJECT' },
|
||||
{ cascadeId: sessionId, action: approved ? 'accept' : 'reject' },
|
||||
// With step index from last known waiting step
|
||||
{ cascadeId: sessionId, stepIndex: lastPendingStepIndex, accepted: approved },
|
||||
{ cascadeId: sessionId, stepIndex: lastPendingStepIndex, approved: approved },
|
||||
];
|
||||
for (let i = 0; i < rpcVariants.length; i++) {
|
||||
try {
|
||||
logToFile(`[APPROVAL-1-${i}] HandleCascadeUserInteraction(${JSON.stringify(rpcVariants[i]).substring(0, 120)})`);
|
||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', rpcVariants[i]);
|
||||
logToFile(`[APPROVAL-1-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-1-${i}:HandleCascadeUserInteraction`;
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-1-${i}] ❌ ${e.message.substring(0, 80)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ── Strategy 2: Renderer DOM Click via HTTP Bridge (kept as fallback) ──
|
||||
// ── Strategies 0A-1 REMOVED (v0.3.11) — all confirmed failing, caused log spam + AG interference ──
|
||||
// Kept: Strategy 0-PROTO (above) for correct proto-based RPC
|
||||
// Kept: Strategy 2 (below) for renderer DOM click fallback
|
||||
// ── Strategy 2: Renderer DOM Click via HTTP Bridge (primary fallback) ──
|
||||
try {
|
||||
const triggerAction = approved ? 'approve' : 'reject';
|
||||
logToFile(`[APPROVAL-2] Setting clickTrigger=${triggerAction} for renderer DOM click`);
|
||||
@@ -3132,14 +2979,8 @@ async function tryApprovalStrategies(approved, sessionId, stepType = '', stepInd
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-2] ❌ FAIL: ${e.message}`);
|
||||
}
|
||||
// ── Strategy 3: ResolveOutstandingSteps — DISABLED (too destructive!) ──
|
||||
// This was cancelling AG work when bot sent approved=false.
|
||||
// DO NOT enable without explicit user confirmation.
|
||||
if (!approved && sdk) {
|
||||
logToFile(`[APPROVAL-3] ResolveOutstandingSteps DISABLED — reject only logs, no cancel`);
|
||||
}
|
||||
logToFile(`[APPROVAL] ⚠️ ALL strategies attempted — check logs for results`);
|
||||
return `ALL_ATTEMPTED:${action}`;
|
||||
logToFile(`[APPROVAL] strategies complete — check logs for results`);
|
||||
return `STRATEGIES_DONE:${action}`;
|
||||
}
|
||||
// ─── Activation ───
|
||||
async function activate(context) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
"name": "gravity-bridge",
|
||||
"displayName": "Gravity Bridge",
|
||||
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
||||
"version": "0.3.10",
|
||||
"version": "0.3.11",
|
||||
"publisher": "variet",
|
||||
"engines": {
|
||||
"vscode": "^1.100.0"
|
||||
|
||||
@@ -2034,11 +2034,8 @@ function setupMonitor() {
|
||||
// Skip pending for workspace-less AG windows (project=default)
|
||||
if (projectName === 'default') {
|
||||
logToFile(`[STEP-PROBE] skip pending: projectName=default (no workspace)`);
|
||||
} else if (autoApproveEnabled) {
|
||||
// Auto-approve: skip Discord, approve directly
|
||||
logToFile(`[AUTO] auto-approving step=${actualIndex} cmd='${command.substring(0, 60)}'`);
|
||||
tryApprovalStrategies(true, activeSessionId, ['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, actualIndex);
|
||||
} else {
|
||||
// Always write pending — Bot decides auto-approve (prevents double-fire)
|
||||
writePendingApproval({
|
||||
conversation_id: activeSessionId,
|
||||
command,
|
||||
@@ -2098,11 +2095,8 @@ function setupMonitor() {
|
||||
// Skip pending for workspace-less AG windows (project=default)
|
||||
if (projectName === 'default') {
|
||||
logToFile(`[STEP-PROBE] skip pending: projectName=default (no workspace)`);
|
||||
} else if (autoApproveEnabled) {
|
||||
// Auto-approve: skip Discord, approve directly
|
||||
logToFile(`[AUTO] auto-approving step=${si} cmd='${command.substring(0, 60)}'`);
|
||||
tryApprovalStrategies(true, activeSessionId, ['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, si);
|
||||
} else {
|
||||
// Always write pending — Bot decides auto-approve (prevents double-fire)
|
||||
writePendingApproval({
|
||||
conversation_id: activeSessionId,
|
||||
command,
|
||||
@@ -2919,153 +2913,11 @@ async function tryApprovalStrategies(approved: boolean, sessionId: string, stepT
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0A: executeCascadeAction
|
||||
// ══════════════════════════════════════════════════════════
|
||||
const cascadeActionVariants = [
|
||||
{ action: approved ? 'accept' : 'reject' },
|
||||
{ action: approved ? 'ACCEPT' : 'REJECT' },
|
||||
{ action: approved ? 'approve' : 'deny' },
|
||||
{ action: approved ? 'run' : 'reject' },
|
||||
{ cascadeId: sessionId, action: approved ? 'accept' : 'reject' },
|
||||
{ cascadeId: sessionId, type: approved ? 'accept' : 'reject' },
|
||||
approved ? 'accept' : 'reject', // plain string arg
|
||||
approved ? 1 : 0, // numeric arg
|
||||
];
|
||||
for (let i = 0; i < cascadeActionVariants.length; i++) {
|
||||
try {
|
||||
const arg = cascadeActionVariants[i];
|
||||
logToFile(`[APPROVAL-0A-${i}] executeCascadeAction(${JSON.stringify(arg)})`);
|
||||
const result = await vscode.commands.executeCommand('antigravity.executeCascadeAction', arg);
|
||||
logToFile(`[APPROVAL-0A-${i}] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||
return `CMD-0A:executeCascadeAction(variant=${i})`;
|
||||
} catch (e: any) {
|
||||
logToFile(`[APPROVAL-0A-${i}] ❌ ${e.message.substring(0, 100)}`);
|
||||
}
|
||||
}
|
||||
// ── Strategies 0A-1 REMOVED (v0.3.11) — all confirmed failing, caused log spam + AG interference ──
|
||||
// Kept: Strategy 0-PROTO (above) for correct proto-based RPC
|
||||
// Kept: Strategy 2 (below) for renderer DOM click fallback
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0B: Direct approval commands (brute-force try all 7 SDK commands)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
const commandsToTry = approved
|
||||
? [
|
||||
'antigravity.terminalCommand.run',
|
||||
'antigravity.terminalCommand.accept',
|
||||
'antigravity.agent.acceptAgentStep',
|
||||
'antigravity.command.accept',
|
||||
]
|
||||
: [
|
||||
'antigravity.terminalCommand.reject',
|
||||
'antigravity.agent.rejectAgentStep',
|
||||
'antigravity.command.reject',
|
||||
];
|
||||
for (const cmd of commandsToTry) {
|
||||
// Try with no args, then with sessionId
|
||||
for (const args of [[], [sessionId], [{ cascadeId: sessionId }]]) {
|
||||
try {
|
||||
logToFile(`[APPROVAL-0B] ${cmd}(${JSON.stringify(args)})`);
|
||||
const result = await vscode.commands.executeCommand(cmd, ...args);
|
||||
logToFile(`[APPROVAL-0B] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||
return `CMD-0B:${cmd}`;
|
||||
} catch (e: any) {
|
||||
logToFile(`[APPROVAL-0B] ❌ ${e.message.substring(0, 80)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0C: Electron webContents access (pierce webview isolation)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
try {
|
||||
logToFile(`[APPROVAL-0C] Attempting Electron webContents access...`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const electron = require('electron');
|
||||
const remote = (electron as any).remote;
|
||||
if (remote) {
|
||||
logToFile(`[APPROVAL-0C] electron.remote available!`);
|
||||
const allWC = remote.webContents.getAllWebContents();
|
||||
logToFile(`[APPROVAL-0C] Found ${allWC.length} webContents`);
|
||||
for (const wc of allWC) {
|
||||
const wcUrl = wc.getURL() || '';
|
||||
logToFile(`[APPROVAL-0C] wc: ${wcUrl.substring(0, 100)}`);
|
||||
if (wcUrl.includes('vscode-webview://') || wcUrl.includes('webview')) {
|
||||
const clickScript = approved
|
||||
? `(function(){ var btns = document.querySelectorAll('button'); for(var b of btns){ if(/Run|Accept|Allow/i.test(b.textContent)){ b.click(); return 'clicked:'+b.textContent; } } return 'no-match'; })()`
|
||||
: `(function(){ var btns = document.querySelectorAll('button'); for(var b of btns){ if(/Reject|Deny|Cancel/i.test(b.textContent)){ b.click(); return 'clicked:'+b.textContent; } } return 'no-match'; })()`;
|
||||
const clickResult = await wc.executeJavaScript(clickScript);
|
||||
logToFile(`[APPROVAL-0C] executeJavaScript result: ${clickResult}`);
|
||||
if (clickResult && clickResult.startsWith('clicked:')) {
|
||||
return `ELECTRON-0C:webContents(${clickResult})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logToFile(`[APPROVAL-0C] electron.remote NOT available`);
|
||||
// Try without remote (main process context)
|
||||
const wc = (electron as any).webContents;
|
||||
if (wc && typeof wc.getAllWebContents === 'function') {
|
||||
logToFile(`[APPROVAL-0C] Direct electron.webContents available!`);
|
||||
const allWC = wc.getAllWebContents();
|
||||
logToFile(`[APPROVAL-0C] Found ${allWC.length} webContents`);
|
||||
} else {
|
||||
logToFile(`[APPROVAL-0C] No webContents access from Extension Host`);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
logToFile(`[APPROVAL-0C] ❌ ${e.message.substring(0, 150)}`);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 0D: sendChatActionMessage (may route to agent)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
const chatActionVariants = [
|
||||
{ action: approved ? 'accept' : 'reject', cascadeId: sessionId },
|
||||
{ message: approved ? 'accept' : 'reject', conversationId: sessionId },
|
||||
approved ? 'accept' : 'reject',
|
||||
];
|
||||
for (let i = 0; i < chatActionVariants.length; i++) {
|
||||
try {
|
||||
logToFile(`[APPROVAL-0D-${i}] sendChatActionMessage(${JSON.stringify(chatActionVariants[i])})`);
|
||||
const result = await vscode.commands.executeCommand('antigravity.sendChatActionMessage', chatActionVariants[i]);
|
||||
logToFile(`[APPROVAL-0D-${i}] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||
return `CMD-0D:sendChatActionMessage(variant=${i})`;
|
||||
} catch (e: any) {
|
||||
logToFile(`[APPROVAL-0D-${i}] ❌ ${e.message.substring(0, 80)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════
|
||||
// STRATEGY 1: HandleCascadeUserInteraction RPC (expanded variants)
|
||||
// ══════════════════════════════════════════════════════════
|
||||
if (sdk) {
|
||||
const rpcVariants = [
|
||||
// Original variants
|
||||
{ cascadeId: sessionId, approved: approved },
|
||||
{ cascadeId: sessionId, stepAction: approved ? 'STEP_ACTION_ACCEPT' : 'STEP_ACTION_REJECT' },
|
||||
{ cascadeId: sessionId, userAction: approved ? 'USER_ACTION_APPROVED' : 'USER_ACTION_REJECTED' },
|
||||
// New variants — ConnectRPC protobuf field naming conventions
|
||||
{ cascade_id: sessionId, accepted: approved },
|
||||
{ cascadeId: sessionId, accepted: approved },
|
||||
{ cascade_id: sessionId, user_action: approved ? 1 : 2 }, // enum as int
|
||||
{ cascadeId: sessionId, interaction: approved ? 'ACCEPT' : 'REJECT' },
|
||||
{ cascadeId: sessionId, action: approved ? 'accept' : 'reject' },
|
||||
// With step index from last known waiting step
|
||||
{ cascadeId: sessionId, stepIndex: lastPendingStepIndex, accepted: approved },
|
||||
{ cascadeId: sessionId, stepIndex: lastPendingStepIndex, approved: approved },
|
||||
];
|
||||
for (let i = 0; i < rpcVariants.length; i++) {
|
||||
try {
|
||||
logToFile(`[APPROVAL-1-${i}] HandleCascadeUserInteraction(${JSON.stringify(rpcVariants[i]).substring(0, 120)})`);
|
||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', rpcVariants[i]);
|
||||
logToFile(`[APPROVAL-1-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-1-${i}:HandleCascadeUserInteraction`;
|
||||
} catch (e: any) {
|
||||
logToFile(`[APPROVAL-1-${i}] ❌ ${e.message.substring(0, 80)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Strategy 2: Renderer DOM Click via HTTP Bridge (kept as fallback) ──
|
||||
// ── Strategy 2: Renderer DOM Click via HTTP Bridge (primary fallback) ──
|
||||
try {
|
||||
const triggerAction = approved ? 'approve' : 'reject';
|
||||
logToFile(`[APPROVAL-2] Setting clickTrigger=${triggerAction} for renderer DOM click`);
|
||||
@@ -3075,15 +2927,8 @@ async function tryApprovalStrategies(approved: boolean, sessionId: string, stepT
|
||||
logToFile(`[APPROVAL-2] ❌ FAIL: ${e.message}`);
|
||||
}
|
||||
|
||||
// ── Strategy 3: ResolveOutstandingSteps — DISABLED (too destructive!) ──
|
||||
// This was cancelling AG work when bot sent approved=false.
|
||||
// DO NOT enable without explicit user confirmation.
|
||||
if (!approved && sdk) {
|
||||
logToFile(`[APPROVAL-3] ResolveOutstandingSteps DISABLED — reject only logs, no cancel`);
|
||||
}
|
||||
|
||||
logToFile(`[APPROVAL] ⚠️ ALL strategies attempted — check logs for results`);
|
||||
return `ALL_ATTEMPTED:${action}`;
|
||||
logToFile(`[APPROVAL] strategies complete — check logs for results`);
|
||||
return `STRATEGIES_DONE:${action}`;
|
||||
}
|
||||
|
||||
// ─── Activation ───
|
||||
|
||||
Reference in New Issue
Block a user