refactor(ext): split step-probe.ts → approval-handler.ts (1597→1017+411 lines) #task-414

This commit is contained in:
Variet Worker
2026-03-18 14:34:32 +09:00
parent 0f057c0c95
commit 17978a750c
2 changed files with 459 additions and 530 deletions

View File

@@ -8,6 +8,10 @@ import * as fs from 'fs';
import * as path from 'path';
import { WSBridgeClient } from './ws-client';
import { extractPlannerText, extractToolCommand, extractToolDescription } from './step-utils';
import { initApprovalHandler, setupResponseWatcher } from './approval-handler';
// Re-export from approval-handler for backward compatibility with extension.ts imports
export { handleDiffReviewResponse, tryApprovalStrategies } from './approval-handler';
export interface BridgeContext {
bridgePath: string;
@@ -35,10 +39,7 @@ const recentPendingSteps = new Map<string, number>();
const PENDING_MEMORY_TTL_MS = 60_000;
// generateApprovalObserverScript → extracted to ./observer-script.ts
// Track last seen step per session to avoid re-fetching
const lastSeenStep = new Map<string, number>();
const lastSnapshotText = new Map<string, string>();
const registeredSessions = new Set<string>(); // track which sessions have been registered
/**
* Get current approval context for WS response routing.
@@ -84,99 +85,7 @@ export function resetPendingState(): void {
ctx.sawRunningAfterPending = false;
}
/**
* Handle diff_review Accept all / Reject all response.
* Extracted so both WS onResponse (extension.ts) and processResponseFile can call it.
*
* This was previously only in processResponseFile (file-bridge path).
* When WS was added (v0.4.x), the onResponse handler skipped this logic entirely,
* causing Accept All to stop working — a regression.
*/
export async function handleDiffReviewResponse(data: {
request_id: string;
approved: boolean;
button_index?: number;
step_type?: string;
}): Promise<boolean> {
const btnIdx = data.button_index ?? -1;
const isAccept = btnIdx === 0 || (btnIdx === -1 && data.approved);
const cmd = isAccept
? 'antigravity.prioritized.agentAcceptAllInFile'
: 'antigravity.prioritized.agentRejectAllInFile';
ctx.logToFile(`[DIFF-REVIEW-WS] → ${isAccept ? 'ACCEPT' : 'REJECT'} (btnIdx=${btnIdx}, rid=${data.request_id?.substring(0, 12)})`);
let diffReviewDone = false;
let modifiedFiles: string[] = [];
// Load tracked step indices and modified files from memory cache or pending file
const trackedSteps: number[] = [];
const memMeta = ctx.diffReviewMetadata.get(data.request_id);
if (memMeta) {
trackedSteps.push(...memMeta.edit_step_indices);
modifiedFiles = memMeta.modified_files;
ctx.diffReviewMetadata.delete(data.request_id);
ctx.logToFile(`[DIFF-REVIEW-WS] loaded from memory: steps=[${trackedSteps.join(',')}] files=${modifiedFiles.length}`);
} else {
try {
const pf = path.join(ctx.bridgePath, 'pending', `${data.request_id}.json`);
if (fs.existsSync(pf)) {
const pd = JSON.parse(fs.readFileSync(pf, 'utf-8'));
if (pd.edit_step_indices) trackedSteps.push(...pd.edit_step_indices);
if (pd.modified_files) modifiedFiles = pd.modified_files;
}
} catch { }
}
// Strategy 1: VS Code command — open review panel + focus each file + accept/reject
try {
try {
await vscode.commands.executeCommand('antigravity.openReviewChanges');
ctx.logToFile(`[DIFF-REVIEW-WS] openReviewChanges OK`);
await new Promise(r => setTimeout(r, 500));
} catch { }
if (modifiedFiles.length > 0) {
for (const fp of modifiedFiles) {
try {
const uri = vscode.Uri.file(fp);
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc, { preview: false });
await new Promise(r => setTimeout(r, 300));
await vscode.commands.executeCommand(cmd);
ctx.logToFile(`[DIFF-REVIEW-WS] ✅ ${cmd} on ${fp.split(/[\\/]/).pop()} OK`);
diffReviewDone = true;
} catch (e: any) {
ctx.logToFile(`[DIFF-REVIEW-WS] per-file error on ${fp}: ${e.message?.substring(0, 80)}`);
}
}
} else {
await vscode.commands.executeCommand(cmd);
ctx.logToFile(`[DIFF-REVIEW-WS] ✅ ${cmd} executed (no file list)`);
diffReviewDone = true;
}
} catch (cmdErr: any) {
ctx.logToFile(`[DIFF-REVIEW-WS] Strategy 1 command error: ${cmdErr.message?.substring(0, 200)}`);
}
// Strategy 2: individual hunk accept/reject
if (!diffReviewDone) {
try {
const hunkCmd = isAccept
? 'antigravity.prioritized.agentAcceptFocusedHunk'
: 'antigravity.prioritized.agentRejectFocusedHunk';
await vscode.commands.executeCommand(hunkCmd);
ctx.logToFile(`[DIFF-REVIEW-WS] ✅ ${hunkCmd} fallback OK`);
diffReviewDone = true;
} catch (hunkErr: any) {
ctx.logToFile(`[DIFF-REVIEW-WS] hunk fallback error: ${hunkErr.message?.substring(0, 100)}`);
}
}
if (!diffReviewDone) {
ctx.logToFile(`[DIFF-REVIEW-WS] ❌ ALL strategies failed for rid=${data.request_id}`);
}
return diffReviewDone;
}
// handleDiffReviewResponse → moved to ./approval-handler.ts
/**
* Write a registration file for the Bot to discover session → project mapping.
@@ -993,278 +902,9 @@ function setupMonitor() {
// ─── Response Watcher (Discord approval → Antigravity RPC) ───
function setupResponseWatcher() {
const responseDir = path.join(ctx.bridgePath, 'response');
if (!fs.existsSync(responseDir)) {
fs.mkdirSync(responseDir, { recursive: true });
}
// setupResponseWatcher → moved to ./approval-handler.ts
const processAnyResponse = (filename: string) => {
const fp = path.join(responseDir, filename);
if (fs.existsSync(fp)) {
// Check if this response belongs to our project
const rid = filename.replace('.json', '');
const pendingFile = path.join(ctx.bridgePath, 'pending', `${rid}.json`);
if (fs.existsSync(pendingFile)) {
try {
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
if (pending.project_name && pending.project_name !== ctx.projectName) {
// ctx.logToFile(`[RESPONSE] skip ${rid} (project=${pending.project_name}, we=${ctx.projectName})`);
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 !== ctx.projectName) {
// ctx.logToFile(`[RESPONSE] skip (from resp data) ${rid} (project=${respData.project_name}, we=${ctx.projectName})`);
return;
}
} catch { }
}
setTimeout(() => processResponseFile(fp), 300);
}
};
const pollAllResponses = () => {
try {
if (!fs.existsSync(responseDir)) return;
for (const f of fs.readdirSync(responseDir)) {
if (f.endsWith('.json')) {
processAnyResponse(f);
}
}
} catch { }
};
pollAllResponses(); // Process any existing responses on startup
try {
responseWatcher = fs.watch(responseDir, (event, filename) => {
if (filename && filename.endsWith('.json') && event === 'rename') {
processAnyResponse(filename);
}
});
console.log('Gravity Bridge: response watcher started');
} catch (e: any) {
console.log(`Gravity Bridge: response watcher failed: ${e.message}`);
}
// Polling fallback: fs.watch on Windows can silently fail
setInterval(pollAllResponses, 3000);
}
async function processResponseFile(filePath: string) {
try {
// Gracefully handle files already consumed by HTTP handler
if (!fs.existsSync(filePath)) {
// HTTP GET /response/:rid already served and deleted this file — skip silently
return;
}
const content = fs.readFileSync(filePath, 'utf-8');
const resp = JSON.parse(content);
const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved} step_type=${resp.step_type || '(missing)'} keys=[${Object.keys(resp).join(',')}]`;
console.log(`Gravity Bridge: ${msg}`);
ctx.logToFile(msg);
// Skip stale timeout responses: if pending is old and this is a reject, it's likely a bot timeout
const ridTimestamp = parseInt((resp.request_id || '').split('_')[0], 10);
if (!isNaN(ridTimestamp)) {
const ageMs = Date.now() - ridTimestamp;
const STALE_THRESHOLD_MS = 120_000; // 2 minutes
if (ageMs > STALE_THRESHOLD_MS && !resp.approved) {
ctx.logToFile(`[RESPONSE] SKIPPED stale timeout: rid=${resp.request_id} age=${Math.round(ageMs / 1000)}s (>${STALE_THRESHOLD_MS / 1000}s, reject)`);
try { fs.unlinkSync(filePath); } catch { }
return;
}
}
// Find matching pending request
const pendingDir = path.join(ctx.bridgePath, 'pending');
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
let sessionId = '';
let isDomObserver = false;
let pendingStepType = resp.step_type || ''; // from bot's response (new)
let pendingStepIndex = -1;
if (fs.existsSync(pendingFile)) {
try {
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
// FIX #2: Skip if pending was already resolved locally (auto_resolve or expired)
if (pending.status === 'auto_resolved' || pending.status === 'expired') {
ctx.logToFile(`[RESPONSE] SKIP — pending already ${pending.status} (rid=${resp.request_id})`);
try { fs.unlinkSync(filePath); } catch { }
return;
}
sessionId = pending.conversation_id || '';
isDomObserver = pending.auto_detected === true
|| pending.source === 'dom_observer';
pendingStepType = pending.step_type || '';
pendingStepIndex = pending.step_index ?? ctx.lastPendingStepIndex;
// File permission detection: check command content or explicit step_type
const cmd = (pending.command || '').toLowerCase();
if (pendingStepType === 'file_permission' || cmd.includes('allow') || cmd.includes('파일 접근')) {
// Map button_index → scope: 0=Once, 1=Conversation, 2=Deny
const btnIdx = resp.button_index ?? -1;
if (btnIdx === 1) {
pendingStepType = 'file_permission_conversation';
} else if (btnIdx === 2) {
pendingStepType = 'file_permission_deny';
} else {
pendingStepType = 'file_permission_once';
}
ctx.logToFile(`[RESPONSE] file_permission detected from pending cmd, mapped to ${pendingStepType}`);
}
} catch { }
}
// ═══ MULTI-STRATEGY APPROVAL (v2.1) ═══
// Tries multiple methods sequentially with detailed logging.
// DOM observer: renderer handles clicking via pollResponse
// Step probe/stall: try RPC → VS Code commands → log results
const approved = resp.approved;
// ── diff_review: Accept all / Reject all ──
if (pendingStepType === 'diff_review') {
const btnIdx = resp.button_index ?? -1;
const isAccept = btnIdx === 0 || (btnIdx === -1 && approved);
const cmd = isAccept
? 'antigravity.prioritized.agentAcceptAllInFile'
: 'antigravity.prioritized.agentRejectAllInFile';
ctx.logToFile(`[RESPONSE] diff_review → ${isAccept ? 'ACCEPT' : 'REJECT'} (btnIdx=${btnIdx})`);
let diffReviewDone = false;
const targetSession = sessionId || ctx.activeSessionId;
let modifiedFiles: string[] = [];
// Load tracked step indices and modified files from memory cache or pending file
const trackedSteps: number[] = [];
const memMeta = ctx.diffReviewMetadata.get(resp.request_id);
if (memMeta) {
trackedSteps.push(...memMeta.edit_step_indices);
modifiedFiles = memMeta.modified_files;
ctx.diffReviewMetadata.delete(resp.request_id);
ctx.logToFile(`[DIFF-REVIEW] loaded from memory: steps=[${trackedSteps.join(',')}] files=${modifiedFiles.length}`);
} else {
try {
const pf = path.join(ctx.bridgePath, 'pending', `${resp.request_id}.json`);
if (fs.existsSync(pf)) {
const pd = JSON.parse(fs.readFileSync(pf, 'utf-8'));
if (pd.edit_step_indices) trackedSteps.push(...pd.edit_step_indices);
if (pd.modified_files) modifiedFiles = pd.modified_files;
}
} catch { }
}
if (trackedSteps.length === 0 && pendingStepIndex > 0) {
trackedSteps.push(pendingStepIndex);
}
// ── Strategy 1: VS Code command (confirmed registered at runtime) ──
// agentAcceptAllInFile / agentRejectAllInFile are the ONLY working diff_review
// commands. RPC methods (acknowledgeCodeActionStep → 404, AcknowledgeCascadeCodeEdit
// → no-op {}, submitCodeAcknowledgement → not registered) are all dead ends.
// The command requires the diff review file to be focused in the editor.
try {
// First, open the Review Changes panel to ensure diff UI is active
try {
await vscode.commands.executeCommand('antigravity.openReviewChanges');
ctx.logToFile(`[DIFF-REVIEW] openReviewChanges OK`);
await new Promise(r => setTimeout(r, 500));
} catch { }
if (modifiedFiles.length > 0) {
// Focus each modified file and execute accept/reject
for (const fp of modifiedFiles) {
try {
const uri = vscode.Uri.file(fp);
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc, { preview: false });
await new Promise(r => setTimeout(r, 300));
await vscode.commands.executeCommand(cmd);
ctx.logToFile(`[DIFF-REVIEW] ✅ ${cmd} on ${fp.split(/[\\/]/).pop()} OK`);
diffReviewDone = true;
} catch (e: any) {
ctx.logToFile(`[DIFF-REVIEW] per-file error on ${fp}: ${e.message?.substring(0, 80)}`);
}
}
} else {
// No file list — just execute command (best effort on currently focused file)
await vscode.commands.executeCommand(cmd);
ctx.logToFile(`[DIFF-REVIEW] ✅ ${cmd} executed (no file list)`);
diffReviewDone = true;
}
} catch (cmdErr: any) {
ctx.logToFile(`[DIFF-REVIEW] Strategy 1 command error: ${cmdErr.message?.substring(0, 200)}`);
}
// ── Strategy 2: Try individual hunk accept/reject as fallback ──
if (!diffReviewDone) {
try {
const hunkCmd = isAccept
? 'antigravity.prioritized.agentAcceptFocusedHunk'
: 'antigravity.prioritized.agentRejectFocusedHunk';
await vscode.commands.executeCommand(hunkCmd);
ctx.logToFile(`[DIFF-REVIEW] ✅ ${hunkCmd} fallback OK`);
diffReviewDone = true;
} catch (hunkErr: any) {
ctx.logToFile(`[DIFF-REVIEW] hunk fallback error: ${hunkErr.message?.substring(0, 100)}`);
}
}
if (!diffReviewDone) {
ctx.logToFile(`[DIFF-REVIEW] ❌ ALL strategies failed for rid=${resp.request_id}`);
}
} else if (isDomObserver) {
// DOM observer path: ALSO try RPC strategies (renderer click is unreliable)
// Use sessionId from pending file if available, fallback to ctx.activeSessionId
const targetSession = sessionId || ctx.activeSessionId;
ctx.logToFile(`[RESPONSE] dom_observer → tryApprovalStrategies(${approved}, ${targetSession.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`);
const strategyResult = await tryApprovalStrategies(approved, targetSession, pendingStepType, pendingStepIndex);
ctx.logToFile(`[RESPONSE] dom strategy result: ${strategyResult}`);
} else {
// Step probe path: run ALL approval strategies
// Use sessionId from pending file if available, fallback to ctx.activeSessionId
const targetSession = sessionId || ctx.activeSessionId;
ctx.logToFile(`[RESPONSE] step_probe → tryApprovalStrategies(${approved}, ${targetSession.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`);
const strategyResult = await tryApprovalStrategies(approved, targetSession, pendingStepType, pendingStepIndex);
ctx.logToFile(`[RESPONSE] strategy result: ${strategyResult}`);
}
ctx.logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
// FIX v2 (2026-03-16): Correct state management after response processing.
//
// HISTORY: processResponseFile originally reset ctx.lastPendingStepIndex=-1 and ctx.stallProbed=false.
// v0.3.11 removed both resets entirely → caused infinite pending loop (step_probe re-detects
// same WAITING step because ctx.lastPendingStepIndex=-1 makes si!=ctx.lastPendingStepIndex true).
// v0.3.12 attempt removed resets entirely → conflicts with known-issues.md L479
// (auto_resolve duplicate notification on delta>0 because ctx.sawRunningAfterPending is false).
//
// CORRECT FIX: Set ctx.sawRunningAfterPending=true to close the auto_resolve gate.
// - ctx.lastPendingStepIndex: KEEP (prevents step_probe from re-creating pending for same step)
// - ctx.stallProbed: KEEP (prevents re-probe during same stall)
// - ctx.sawRunningAfterPending = true: CRITICAL — tells auto_resolve "Discord handled this,
// don't generate duplicate 직접 진행됨 notification" (prevents known-issues L479 regression)
// All three reset naturally at delta > 0 (L1972-1980) when step actually progresses.
ctx.sawRunningAfterPending = true;
// Cleanup response file
// CRITICAL: DOM observer responses must NOT be deleted here!
// The renderer polls GET /response/:rid to discover the approval.
// If we delete the file before the renderer polls, it gets ENOENT.
// The HTTP handler (/response/:rid) deletes after serving to renderer.
if (!isDomObserver) {
try { fs.unlinkSync(filePath); } catch { }
}
} catch (e: any) {
const log = `[RESPONSE] error: ${e.message}`;
console.log(`Gravity Bridge: ${log}`);
ctx.logToFile(log);
}
}
// processResponseFile → moved to ./approval-handler.ts
/**
* Extract AI text from a PLANNER_RESPONSE step.
@@ -1418,169 +1058,7 @@ export function writePendingApproval(data: { conversation_id: string; command: s
}
}
// ─── Multi-Strategy Approval Execution ───
/**
* Try multiple approval methods sequentially.
* Returns a string describing which method succeeded (or all failed).
*
* Strategy order (most reliable first):
* 1. HandleCascadeUserInteraction RPC (cross-platform, no focus)
* 2. VS Code accept/reject commands (focus-dependent)
* 3. Log failure for manual intervention
*/
export async function tryApprovalStrategies(approved: boolean, sessionId: string, stepType: string = '', stepIndex: number = -1): Promise<string> {
const action = approved ? 'APPROVE' : 'REJECT';
const effectiveStepIndex = stepIndex >= 0 ? stepIndex : ctx.lastPendingStepIndex;
ctx.logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${effectiveStepIndex}`);
// ── Dynamic Command Discovery (log what's available during WAITING state) ──
let approvalCmdList: string[] = [];
try {
const allCmds = await vscode.commands.getCommands(true);
const agCmds = allCmds.filter((c: string) => c.startsWith('antigravity.'));
approvalCmdList = agCmds.filter((c: string) => {
const lower = c.toLowerCase();
return lower.includes('accept') || lower.includes('reject') || lower.includes('approve')
|| lower.includes('terminal') || lower.includes('run') || lower.includes('step')
|| lower.includes('cascade') || lower.includes('action');
});
ctx.logToFile(`[APPROVAL-CMD-CHECK] ${agCmds.length} total, ${approvalCmdList.length} approval-related:`);
for (const c of approvalCmdList) {
ctx.logToFile(`[APPROVAL-CMD-CHECK] → ${c}`);
}
} catch (e: any) {
ctx.logToFile(`[APPROVAL-CMD-CHECK] error: ${e.message}`);
}
// ══════════════════════════════════════════════════════════
// STRATEGY 0-PROTO: Correct proto-based RPC (decoded from AG source)
// Routes interaction sub-message by step_type:
// run_command → CascadeRunCommandInteraction { confirm }
// write_to_file → AcknowledgeCascadeCodeEdit RPC (separate)
// open_browser_url → CascadeOpenBrowserUrlInteraction { confirm }
// send_command_input → CascadeSendCommandInputInteraction { confirm }
// read_url_content → CascadeReadUrlContentInteraction { confirm }
// mcp_tool → CascadeMcpInteraction { confirm }
// invoke_subagent → CascadeRunExtensionCodeInteraction { confirm }
// ══════════════════════════════════════════════════════════
if (ctx.sdk && approved) {
// Build interaction sub-message based on step_type
const typeLower = stepType.toLowerCase().replace('cortex_step_type_', '');
let interactionPayload: Record<string, any> = {};
if (typeLower.includes('code_edit') || typeLower.includes('write_to_file') || typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')) {
// CODE EDIT: Uses acknowledgeCodeActionStep RPC (correct AG LS method)
// Try VS Code command first (same path as UI Accept all button)
try {
ctx.logToFile(`[APPROVAL-CODE-EDIT] trying submitCodeAcknowledgement command`);
await vscode.commands.executeCommand('antigravity.prioritized.submitCodeAcknowledgement');
ctx.logToFile(`[APPROVAL-CODE-EDIT] ✅ submitCodeAcknowledgement OK`);
return `CMD:submitCodeAcknowledgement(accept=${approved})`;
} catch {
ctx.logToFile(`[APPROVAL-CODE-EDIT] submitCodeAcknowledgement not available, trying RPC`);
}
// Direct LS RPC with correct method name
try {
ctx.logToFile(`[APPROVAL-CODE-EDIT] acknowledgeCodeActionStep(cascadeId=${sessionId.substring(0, 8)}, accept=${approved}, stepIndices=[${effectiveStepIndex}])`);
const ackResult = await ctx.sdk.ls.rawRPC('acknowledgeCodeActionStep', {
cascadeId: sessionId,
accept: approved,
stepIndices: [effectiveStepIndex],
});
ctx.logToFile(`[APPROVAL-CODE-EDIT] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`);
return `RPC:acknowledgeCodeActionStep(accept=${approved})`;
} catch (e: any) {
ctx.logToFile(`[APPROVAL-CODE-EDIT] ❌ ${e.message.substring(0, 200)}`);
// Fallback: try HandleCascadeUserInteraction with runCommand
ctx.logToFile(`[APPROVAL-CODE-EDIT] falling back to HandleCascadeUserInteraction`);
interactionPayload = { runCommand: { confirm: true } };
}
}
// Map step_type to interaction sub-message field
if (typeLower.includes('run_command') || typeLower.includes('shell_exec')) {
interactionPayload = { runCommand: { confirm: true } };
} else if (typeLower.includes('open_browser')) {
interactionPayload = { openBrowserUrl: { confirm: true } };
} else if (typeLower.includes('send_command_input')) {
interactionPayload = { sendCommandInput: { confirm: true } }; // guess field name
} else if (typeLower.includes('read_url')) {
interactionPayload = { readUrlContent: { confirm: true } }; // guess
} else if (typeLower.includes('mcp')) {
interactionPayload = { mcpTool: { confirm: true } }; // guess
} else if (typeLower.includes('invoke_subagent') || typeLower.includes('extension_code')) {
interactionPayload = { runExtensionCode: { confirm: true } };
} else if (typeLower.includes('file_permission')) {
// FilePermissionInteraction: allow=true, scope depends on cmd
// file_permission_once → 1, file_permission_conversation → 2
const scope = typeLower.includes('conversation') ? 2 : 1;
interactionPayload = { filePermission: { allow: true, scope } };
} else if (typeLower.includes('elicitation')) {
interactionPayload = { elicitation: {} }; // ElicitationInteraction (TBD)
} else {
// Default: try run_command (most common)
interactionPayload = { runCommand: { confirm: true } };
}
const protoVariants = [
// Variant A: camelCase with trajectoryId (proven working for run_command)
{
cascadeId: sessionId,
interaction: {
trajectoryId: activeTrajectoryId || sessionId,
stepIndex: effectiveStepIndex,
...interactionPayload,
},
},
// Variant B: snake_case
{
cascade_id: sessionId,
interaction: {
trajectory_id: activeTrajectoryId || sessionId,
step_index: effectiveStepIndex,
...interactionPayload,
},
},
// Variant C: minimal (no trajectoryId)
{
cascadeId: sessionId,
interaction: {
stepIndex: effectiveStepIndex,
...interactionPayload,
},
},
];
for (let i = 0; i < protoVariants.length; i++) {
try {
const payload = protoVariants[i];
ctx.logToFile(`[APPROVAL-PROTO-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 250)})`);
const rpcResult = await ctx.sdk.ls.rawRPC('HandleCascadeUserInteraction', payload);
ctx.logToFile(`[APPROVAL-PROTO-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
return `RPC-PROTO-${i}:HandleCascadeUserInteraction(${typeLower})`;
} catch (e: any) {
ctx.logToFile(`[APPROVAL-PROTO-${i}] ❌ ${e.message.substring(0, 300)}`);
}
}
}
// ── 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';
ctx.logToFile(`[APPROVAL-2] Setting clickTrigger=${triggerAction} for renderer DOM click`);
ctx.setClickTrigger(triggerAction as 'approve' | 'reject');
ctx.logToFile(`[APPROVAL-2] ✅ clickTrigger set — renderer will poll and click within 2s`);
} catch (e: any) {
ctx.logToFile(`[APPROVAL-2] ❌ FAIL: ${e.message}`);
}
ctx.logToFile(`[APPROVAL] strategies complete — check logs for results`);
return `STRATEGIES_DONE:${action}`;
}
// tryApprovalStrategies → moved to ./approval-handler.ts
// ─── Activation ───
@@ -1591,6 +1069,7 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string
*/
export function initStepProbe(context: BridgeContext) {
ctx = context;
initApprovalHandler(context, () => activeTrajectoryId);
setupMonitor();
setupResponseWatcher();
}