feat: step_type routing for all approval interaction types

This commit is contained in:
2026-03-10 07:56:36 +09:00
parent 1f63f60280
commit 0e3a896c86
3 changed files with 141 additions and 77 deletions

View File

@@ -1775,12 +1775,16 @@ async function processResponseFile(filePath) {
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
let sessionId = '';
let isDomObserver = false;
let pendingStepType = '';
let pendingStepIndex = -1;
if (fs.existsSync(pendingFile)) {
try {
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
sessionId = pending.conversation_id || '';
isDomObserver = pending.auto_detected === true
|| pending.source === 'dom_observer';
pendingStepType = pending.step_type || '';
pendingStepIndex = pending.step_index ?? lastPendingStepIndex;
}
catch { }
}
@@ -1794,9 +1798,9 @@ async function processResponseFile(filePath) {
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
}
else {
// Step probe path: run ALL approval strategies (5 vectors → 30+ methods)
logToFile(`[RESPONSE] step_probe → running tryApprovalStrategies(${approved}, ${activeSessionId.substring(0, 8)})`);
const strategyResult = await tryApprovalStrategies(approved, activeSessionId);
// 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);
logToFile(`[RESPONSE] strategy result: ${strategyResult}`);
}
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
@@ -2010,9 +2014,10 @@ function writePendingApproval(data) {
* 2. VS Code accept/reject commands (focus-dependent)
* 3. Log failure for manual intervention
*/
async function tryApprovalStrategies(approved, sessionId) {
async function tryApprovalStrategies(approved, sessionId, stepType = '', stepIndex = -1) {
const action = approved ? 'APPROVE' : 'REJECT';
logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)}`);
const effectiveStepIndex = stepIndex >= 0 ? stepIndex : lastPendingStepIndex;
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 = [];
try {
@@ -2034,67 +2039,96 @@ async function tryApprovalStrategies(approved, sessionId) {
}
// ══════════════════════════════════════════════════════════
// STRATEGY 0-PROTO: Correct proto-based RPC (decoded from AG source)
// HandleCascadeUserInteractionRequest:
// cascade_id: string
// interaction: CascadeUserInteraction {
// trajectory_id, step_index,
// oneof: { run_command: CascadeRunCommandInteraction { confirm: bool } }
// }
// ConnectRPC uses camelCase for JSON encoding of snake_case proto fields
// 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 (sdk && approved) {
// Build interaction sub-message based on step_type
const typeLower = stepType.toLowerCase().replace('cortex_step_type_', '');
let interactionPayload = {};
if (typeLower.includes('write_to_file') || typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')) {
// CODE EDIT: Uses separate AcknowledgeCascadeCodeEdit RPC
try {
logToFile(`[APPROVAL-PROTO-ACK] AcknowledgeCascadeCodeEdit(cascadeId=${sessionId.substring(0, 8)}, accept=true, stepIndices=[${effectiveStepIndex}])`);
const ackResult = await sdk.ls.rawRPC('AcknowledgeCascadeCodeEdit', {
cascadeId: sessionId,
accept: true,
stepIndices: [effectiveStepIndex],
});
logToFile(`[APPROVAL-PROTO-ACK] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`);
return `RPC-PROTO-ACK:AcknowledgeCascadeCodeEdit`;
}
catch (e) {
logToFile(`[APPROVAL-PROTO-ACK] ❌ ${e.message.substring(0, 200)}`);
// Fall through to HandleCascadeUserInteraction
}
}
// 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 {
// Default: try run_command (most common)
interactionPayload = { runCommand: { confirm: true } };
}
const protoVariants = [
// Variant A: camelCase JSON (ConnectRPC default)
// Variant A: camelCase with trajectoryId (proven working for run_command)
{
cascadeId: sessionId,
interaction: {
trajectoryId: activeTrajectoryId || sessionId,
stepIndex: lastPendingStepIndex,
runCommand: { confirm: true },
stepIndex: effectiveStepIndex,
...interactionPayload,
},
},
// Variant B: snake_case JSON (proto native)
// Variant B: snake_case
{
cascade_id: sessionId,
interaction: {
trajectory_id: activeTrajectoryId || sessionId,
step_index: lastPendingStepIndex,
run_command: { confirm: true },
step_index: effectiveStepIndex,
...interactionPayload,
},
},
// Variant C: camelCase, without trajectoryId (maybe optional)
// Variant C: minimal (no trajectoryId)
{
cascadeId: sessionId,
interaction: {
stepIndex: lastPendingStepIndex,
runCommand: { confirm: true },
},
},
// Variant D: camelCase, confirm only (minimal)
{
cascadeId: sessionId,
interaction: {
runCommand: { confirm: true },
},
},
// Variant E: snake_case minimal
{
cascade_id: sessionId,
interaction: {
run_command: { confirm: true },
stepIndex: effectiveStepIndex,
...interactionPayload,
},
},
];
for (let i = 0; i < protoVariants.length; i++) {
try {
const payload = protoVariants[i];
logToFile(`[APPROVAL-PROTO-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 200)})`);
logToFile(`[APPROVAL-PROTO-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 250)})`);
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', payload);
logToFile(`[APPROVAL-PROTO-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
return `RPC-PROTO-${i}:HandleCascadeUserInteraction`;
return `RPC-PROTO-${i}:HandleCascadeUserInteraction(${typeLower})`;
}
catch (e) {
// Capture FULL error message (critical for diagnostics)
logToFile(`[APPROVAL-PROTO-${i}] ❌ ${e.message.substring(0, 300)}`);
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1753,12 +1753,16 @@ async function processResponseFile(filePath: string) {
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
let sessionId = '';
let isDomObserver = false;
let pendingStepType = '';
let pendingStepIndex = -1;
if (fs.existsSync(pendingFile)) {
try {
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
sessionId = pending.conversation_id || '';
isDomObserver = pending.auto_detected === true
|| pending.source === 'dom_observer';
pendingStepType = pending.step_type || '';
pendingStepIndex = pending.step_index ?? lastPendingStepIndex;
} catch { }
}
@@ -1773,9 +1777,9 @@ async function processResponseFile(filePath: string) {
// DOM observer path: renderer polls /response/:rid and clicks directly
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
} else {
// Step probe path: run ALL approval strategies (5 vectors → 30+ methods)
logToFile(`[RESPONSE] step_probe → running tryApprovalStrategies(${approved}, ${activeSessionId.substring(0, 8)})`);
const strategyResult = await tryApprovalStrategies(approved, activeSessionId);
// 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);
logToFile(`[RESPONSE] strategy result: ${strategyResult}`);
}
@@ -1976,9 +1980,10 @@ function writePendingApproval(data: { conversation_id: string; command: string;
* 2. VS Code accept/reject commands (focus-dependent)
* 3. Log failure for manual intervention
*/
async function tryApprovalStrategies(approved: boolean, sessionId: string): Promise<string> {
async function tryApprovalStrategies(approved: boolean, sessionId: string, stepType: string = '', stepIndex: number = -1): Promise<string> {
const action = approved ? 'APPROVE' : 'REJECT';
logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)}`);
const effectiveStepIndex = stepIndex >= 0 ? stepIndex : lastPendingStepIndex;
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[] = [];
@@ -2001,66 +2006,91 @@ async function tryApprovalStrategies(approved: boolean, sessionId: string): Prom
// ══════════════════════════════════════════════════════════
// STRATEGY 0-PROTO: Correct proto-based RPC (decoded from AG source)
// HandleCascadeUserInteractionRequest:
// cascade_id: string
// interaction: CascadeUserInteraction {
// trajectory_id, step_index,
// oneof: { run_command: CascadeRunCommandInteraction { confirm: bool } }
// }
// ConnectRPC uses camelCase for JSON encoding of snake_case proto fields
// 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 (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('write_to_file') || typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')) {
// CODE EDIT: Uses separate AcknowledgeCascadeCodeEdit RPC
try {
logToFile(`[APPROVAL-PROTO-ACK] AcknowledgeCascadeCodeEdit(cascadeId=${sessionId.substring(0,8)}, accept=true, stepIndices=[${effectiveStepIndex}])`);
const ackResult = await sdk.ls.rawRPC('AcknowledgeCascadeCodeEdit', {
cascadeId: sessionId,
accept: true,
stepIndices: [effectiveStepIndex],
});
logToFile(`[APPROVAL-PROTO-ACK] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`);
return `RPC-PROTO-ACK:AcknowledgeCascadeCodeEdit`;
} catch (e: any) {
logToFile(`[APPROVAL-PROTO-ACK] ❌ ${e.message.substring(0, 200)}`);
// Fall through to HandleCascadeUserInteraction
}
}
// 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 {
// Default: try run_command (most common)
interactionPayload = { runCommand: { confirm: true } };
}
const protoVariants = [
// Variant A: camelCase JSON (ConnectRPC default)
// Variant A: camelCase with trajectoryId (proven working for run_command)
{
cascadeId: sessionId,
interaction: {
trajectoryId: activeTrajectoryId || sessionId,
stepIndex: lastPendingStepIndex,
runCommand: { confirm: true },
stepIndex: effectiveStepIndex,
...interactionPayload,
},
},
// Variant B: snake_case JSON (proto native)
// Variant B: snake_case
{
cascade_id: sessionId,
interaction: {
trajectory_id: activeTrajectoryId || sessionId,
step_index: lastPendingStepIndex,
run_command: { confirm: true },
step_index: effectiveStepIndex,
...interactionPayload,
},
},
// Variant C: camelCase, without trajectoryId (maybe optional)
// Variant C: minimal (no trajectoryId)
{
cascadeId: sessionId,
interaction: {
stepIndex: lastPendingStepIndex,
runCommand: { confirm: true },
},
},
// Variant D: camelCase, confirm only (minimal)
{
cascadeId: sessionId,
interaction: {
runCommand: { confirm: true },
},
},
// Variant E: snake_case minimal
{
cascade_id: sessionId,
interaction: {
run_command: { confirm: true },
stepIndex: effectiveStepIndex,
...interactionPayload,
},
},
];
for (let i = 0; i < protoVariants.length; i++) {
try {
const payload = protoVariants[i];
logToFile(`[APPROVAL-PROTO-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 200)})`);
logToFile(`[APPROVAL-PROTO-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 250)})`);
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', payload);
logToFile(`[APPROVAL-PROTO-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
return `RPC-PROTO-${i}:HandleCascadeUserInteraction`;
return `RPC-PROTO-${i}:HandleCascadeUserInteraction(${typeLower})`;
} catch (e: any) {
// Capture FULL error message (critical for diagnostics)
logToFile(`[APPROVAL-PROTO-${i}] ❌ ${e.message.substring(0, 300)}`);
}
}