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:
2026-03-15 17:11:38 +09:00
parent 75289b3ec5
commit 6739f8f30c
7 changed files with 44 additions and 333 deletions

View File

@@ -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
View File

@@ -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)

View File

@@ -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) | - | 🔧 |

View File

@@ -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

View File

@@ -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"

View File

@@ -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 ───