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

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