feat: immediate pending detection for all step types

This commit is contained in:
2026-03-08 10:19:27 +09:00
parent 7a38e7ecc9
commit 2574ce6f08
3 changed files with 228 additions and 150 deletions

View File

@@ -286,17 +286,13 @@ function setupMonitor() {
let lastKnownStepCount = 0; let lastKnownStepCount = 0;
let lastNotifyStepIndex = -1; let lastNotifyStepIndex = -1;
let lastTaskStepIndex = -1; let lastTaskStepIndex = -1;
// WAITING detection
let stalledPolls = 0;
let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step
setInterval(async () => { setInterval(async () => {
pollCount++; pollCount++;
try { try {
// Single RPC: GetAllCascadeTrajectories
const allTraj = await sdk.ls.rawRPC('GetAllCascadeTrajectories', {}); const allTraj = await sdk.ls.rawRPC('GetAllCascadeTrajectories', {});
if (!allTraj?.trajectorySummaries) if (!allTraj?.trajectorySummaries)
return; return;
// Find the most recently modified session (or current active)
let bestSession = null; let bestSession = null;
let bestSessionId = ''; let bestSessionId = '';
let bestModTime = ''; let bestModTime = '';
@@ -320,88 +316,140 @@ function setupMonitor() {
lastKnownStepCount = currentCount; lastKnownStepCount = currentCount;
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1; lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1; lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1;
stalledPolls = 0;
lastPendingStepIndex = -1; lastPendingStepIndex = -1;
writeRegistration(activeSessionId); writeRegistration(activeSessionId);
console.log(`Gravity Bridge: [POLL#${pollCount}] session: ${activeSessionId.substring(0, 8)} "${currentTitle}" steps=${currentCount} ${isRunning ? 'RUNNING' : 'idle'}`); console.log(`Gravity Bridge: [POLL#${pollCount}] session: ${activeSessionId.substring(0, 8)} "${currentTitle}" steps=${currentCount} ${isRunning ? 'RUNNING' : 'idle'}`);
return; return;
} }
// ── WAITING Detection ──
// stepCount frozen + session still RUNNING = likely WAITING for user approval
if (currentCount === lastKnownStepCount && isRunning) {
stalledPolls++;
if (stalledPolls >= 2) {
// Query last steps to check for WAITING/PENDING
try {
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', { cascadeId: bestSessionId });
const steps = stepsResp?.steps || [];
if (steps.length > 0) {
const lastStep = steps[steps.length - 1];
const stepStatus = (lastStep.status || '').replace('CORTEX_STEP_STATUS_', '');
const stepType = (lastStep.type || '').replace('CORTEX_STEP_TYPE_', '');
const stepIdx = lastStep.metadata?.sourceTrajectoryStepInfo?.stepIndex ?? steps.length - 1;
// Non-DONE step while session is RUNNING = user action needed
if (stepStatus !== 'DONE' && stepStatus !== 'REJECTED' && stepIdx > lastPendingStepIndex) {
// Extract command info from step
let cmd = 'unknown';
let desc = `${stepType} (${stepStatus})`;
const toolName = lastStep.metadata?.toolCall?.name || '';
if (lastStep.terminalCommand?.command) {
cmd = lastStep.terminalCommand.command;
desc = `Terminal: ${cmd}`;
}
else if (toolName === 'run_command') {
// Extract from toolCall arguments
try {
const args = JSON.parse(lastStep.metadata.toolCall.argumentsJson || '{}');
cmd = args.CommandLine || args.command || toolName;
desc = `Command: ${cmd}`;
}
catch {
cmd = toolName;
}
}
else if (toolName) {
cmd = toolName;
desc = `Tool: ${toolName}`;
}
// Auto-create pending file
const rid = Date.now().toString();
const pending = {
request_id: rid,
conversation_id: bestSessionId,
command: cmd.substring(0, 200),
description: desc.substring(0, 200),
timestamp: Date.now() / 1000,
status: 'pending',
project_name: projectName,
step_index: stepIdx,
step_type: stepType,
step_status: stepStatus,
auto_detected: true,
};
const pendingDir = path.join(bridgePath, 'pending');
fs.writeFileSync(path.join(pendingDir, `${rid}.json`), JSON.stringify(pending, null, 2));
lastPendingStepIndex = stepIdx;
stalledPolls = 0; // reset stall counter after pending created
logToFile(`[WAITING] detected step=${stepIdx} type=${stepType} status=${stepStatus} cmd=${cmd.substring(0, 60)}`);
console.log(`Gravity Bridge: [POLL#${pollCount}] WAITING detected! step=${stepIdx} cmd=${cmd.substring(0, 40)}`);
}
}
}
catch (e) {
logToFile(`[WAITING] step query failed: ${e.message}`);
}
}
}
else {
stalledPolls = 0;
}
const delta = currentCount - lastKnownStepCount; const delta = currentCount - lastKnownStepCount;
lastKnownStepCount = currentCount; lastKnownStepCount = currentCount;
if (delta > 0) { if (delta > 0) {
console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`); console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`);
} }
// ── IMMEDIATE PENDING DETECTION ──
// On EVERY poll: check last 3 steps for non-DONE status
// This catches: file review, file access permission, command approval
if (isRunning) {
try {
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', { cascadeId: bestSessionId });
const steps = stepsResp?.steps || [];
if (steps.length > 0) {
// Check last 3 steps (some may be in-flight)
const checkCount = Math.min(3, steps.length);
for (let i = steps.length - checkCount; i < steps.length; i++) {
const step = steps[i];
const stepStatus = (step.status || '').replace('CORTEX_STEP_STATUS_', '');
const stepType = (step.type || '').replace('CORTEX_STEP_TYPE_', '');
const stepIdx = step.metadata?.sourceTrajectoryStepInfo?.stepIndex ?? i;
// Skip already-handled steps
if (stepIdx <= lastPendingStepIndex)
continue;
// Skip completed/rejected steps
if (stepStatus === 'DONE' || stepStatus === 'REJECTED')
continue;
// ── Non-DONE step found! Create pending based on type ──
let cmd = '';
let desc = '';
const toolName = step.metadata?.toolCall?.name || '';
let argsJson = '';
try {
argsJson = step.metadata?.toolCall?.argumentsJson || '';
}
catch { }
if (toolName === 'run_command' || toolName === 'send_command_input') {
// Command execution approval
try {
const args = JSON.parse(argsJson || '{}');
cmd = args.CommandLine || args.command || args.Input || toolName;
}
catch {
cmd = toolName;
}
desc = `명령어 실행 승인 (${stepType})`;
}
else if (toolName === 'browser_subagent') {
// Browser subagent
try {
const args = JSON.parse(argsJson || '{}');
cmd = args.Task?.substring(0, 150) || 'browser task';
}
catch {
cmd = 'browser_subagent';
}
desc = `브라우저 서브에이전트 실행`;
}
else if (stepType === 'CODE_ACTION' || toolName === 'replace_file_content' || toolName === 'multi_replace_file_content' || toolName === 'write_to_file') {
// File modification review
try {
const args = JSON.parse(argsJson || '{}');
cmd = args.TargetFile || args.target_file || toolName;
}
catch {
cmd = toolName;
}
desc = `파일 수정 검토 요청`;
}
else if (toolName === 'view_file' || toolName === 'view_file_outline' || toolName === 'view_code_item') {
// File access (usually auto-approved, but handle if pending)
try {
const args = JSON.parse(argsJson || '{}');
cmd = args.AbsolutePath || args.File || toolName;
}
catch {
cmd = toolName;
}
desc = `파일 접근 권한 요청`;
}
else if (toolName === 'notify_user') {
// AI asking for user feedback — this needs a different response
try {
const args = JSON.parse(argsJson || '{}');
cmd = args.Message?.substring(0, 200) || 'notify_user';
}
catch {
cmd = 'notify_user';
}
desc = `사용자 피드백 요청`;
}
else if (toolName) {
cmd = toolName;
desc = `도구 실행: ${toolName}`;
}
else {
cmd = stepType || 'unknown';
desc = `${stepType} (${stepStatus})`;
}
// Create pending for Discord
const rid = Date.now().toString();
const pending = {
request_id: rid,
conversation_id: bestSessionId,
command: cmd.substring(0, 500),
description: desc.substring(0, 200),
timestamp: Date.now() / 1000,
status: 'pending',
project_name: projectName,
step_index: stepIdx,
step_type: stepType,
step_status: stepStatus,
tool_name: toolName,
auto_detected: true,
};
const pendingDir = path.join(bridgePath, 'pending');
fs.writeFileSync(path.join(pendingDir, `${rid}.json`), JSON.stringify(pending, null, 2));
lastPendingStepIndex = stepIdx;
logToFile(`[PENDING] step=${stepIdx} type=${stepType} status=${stepStatus} tool=${toolName} cmd=${cmd.substring(0, 60)}`);
console.log(`Gravity Bridge: [POLL#${pollCount}] PENDING! step=${stepIdx} ${toolName || stepType} → pending/${rid}.json`);
}
}
}
catch (e) {
// Only log occasionally to avoid spam
if (pollCount % 10 === 0) {
logToFile(`[PENDING] step query error: ${e.message}`);
}
}
}
// ── Process latestNotifyUserStep ── // ── Process latestNotifyUserStep ──
const notifyStep = bestSession.latestNotifyUserStep; const notifyStep = bestSession.latestNotifyUserStep;
if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) { if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) {

File diff suppressed because one or more lines are too long

View File

@@ -254,18 +254,14 @@ function setupMonitor() {
let lastKnownStepCount = 0; let lastKnownStepCount = 0;
let lastNotifyStepIndex = -1; let lastNotifyStepIndex = -1;
let lastTaskStepIndex = -1; let lastTaskStepIndex = -1;
// WAITING detection
let stalledPolls = 0;
let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step
setInterval(async () => { setInterval(async () => {
pollCount++; pollCount++;
try { try {
// Single RPC: GetAllCascadeTrajectories
const allTraj = await sdk.ls.rawRPC('GetAllCascadeTrajectories', {}); const allTraj = await sdk.ls.rawRPC('GetAllCascadeTrajectories', {});
if (!allTraj?.trajectorySummaries) return; if (!allTraj?.trajectorySummaries) return;
// Find the most recently modified session (or current active)
let bestSession: any = null; let bestSession: any = null;
let bestSessionId = ''; let bestSessionId = '';
let bestModTime = ''; let bestModTime = '';
@@ -290,87 +286,121 @@ function setupMonitor() {
lastKnownStepCount = currentCount; lastKnownStepCount = currentCount;
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1; lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1; lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1;
stalledPolls = 0;
lastPendingStepIndex = -1; lastPendingStepIndex = -1;
writeRegistration(activeSessionId); writeRegistration(activeSessionId);
console.log(`Gravity Bridge: [POLL#${pollCount}] session: ${activeSessionId.substring(0, 8)} "${currentTitle}" steps=${currentCount} ${isRunning ? 'RUNNING' : 'idle'}`); console.log(`Gravity Bridge: [POLL#${pollCount}] session: ${activeSessionId.substring(0, 8)} "${currentTitle}" steps=${currentCount} ${isRunning ? 'RUNNING' : 'idle'}`);
return; return;
} }
// ── WAITING Detection ──
// stepCount frozen + session still RUNNING = likely WAITING for user approval
if (currentCount === lastKnownStepCount && isRunning) {
stalledPolls++;
if (stalledPolls >= 2) {
// Query last steps to check for WAITING/PENDING
try {
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', { cascadeId: bestSessionId });
const steps = stepsResp?.steps || [];
if (steps.length > 0) {
const lastStep = steps[steps.length - 1];
const stepStatus = (lastStep.status || '').replace('CORTEX_STEP_STATUS_', '');
const stepType = (lastStep.type || '').replace('CORTEX_STEP_TYPE_', '');
const stepIdx = lastStep.metadata?.sourceTrajectoryStepInfo?.stepIndex ?? steps.length - 1;
// Non-DONE step while session is RUNNING = user action needed
if (stepStatus !== 'DONE' && stepStatus !== 'REJECTED' && stepIdx > lastPendingStepIndex) {
// Extract command info from step
let cmd = 'unknown';
let desc = `${stepType} (${stepStatus})`;
const toolName = lastStep.metadata?.toolCall?.name || '';
if (lastStep.terminalCommand?.command) {
cmd = lastStep.terminalCommand.command;
desc = `Terminal: ${cmd}`;
} else if (toolName === 'run_command') {
// Extract from toolCall arguments
try {
const args = JSON.parse(lastStep.metadata.toolCall.argumentsJson || '{}');
cmd = args.CommandLine || args.command || toolName;
desc = `Command: ${cmd}`;
} catch { cmd = toolName; }
} else if (toolName) {
cmd = toolName;
desc = `Tool: ${toolName}`;
}
// Auto-create pending file
const rid = Date.now().toString();
const pending = {
request_id: rid,
conversation_id: bestSessionId,
command: cmd.substring(0, 200),
description: desc.substring(0, 200),
timestamp: Date.now() / 1000,
status: 'pending',
project_name: projectName,
step_index: stepIdx,
step_type: stepType,
step_status: stepStatus,
auto_detected: true,
};
const pendingDir = path.join(bridgePath, 'pending');
fs.writeFileSync(path.join(pendingDir, `${rid}.json`), JSON.stringify(pending, null, 2));
lastPendingStepIndex = stepIdx;
stalledPolls = 0; // reset stall counter after pending created
logToFile(`[WAITING] detected step=${stepIdx} type=${stepType} status=${stepStatus} cmd=${cmd.substring(0, 60)}`);
console.log(`Gravity Bridge: [POLL#${pollCount}] WAITING detected! step=${stepIdx} cmd=${cmd.substring(0, 40)}`);
}
}
} catch (e: any) {
logToFile(`[WAITING] step query failed: ${e.message}`);
}
}
} else {
stalledPolls = 0;
}
const delta = currentCount - lastKnownStepCount; const delta = currentCount - lastKnownStepCount;
lastKnownStepCount = currentCount; lastKnownStepCount = currentCount;
if (delta > 0) { if (delta > 0) {
console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`); console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`);
} }
// ── IMMEDIATE PENDING DETECTION ──
// On EVERY poll: check last 3 steps for non-DONE status
// This catches: file review, file access permission, command approval
if (isRunning) {
try {
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', { cascadeId: bestSessionId });
const steps = stepsResp?.steps || [];
if (steps.length > 0) {
// Check last 3 steps (some may be in-flight)
const checkCount = Math.min(3, steps.length);
for (let i = steps.length - checkCount; i < steps.length; i++) {
const step = steps[i];
const stepStatus = (step.status || '').replace('CORTEX_STEP_STATUS_', '');
const stepType = (step.type || '').replace('CORTEX_STEP_TYPE_', '');
const stepIdx = step.metadata?.sourceTrajectoryStepInfo?.stepIndex ?? i;
// Skip already-handled steps
if (stepIdx <= lastPendingStepIndex) continue;
// Skip completed/rejected steps
if (stepStatus === 'DONE' || stepStatus === 'REJECTED') continue;
// ── Non-DONE step found! Create pending based on type ──
let cmd = '';
let desc = '';
const toolName = step.metadata?.toolCall?.name || '';
let argsJson = '';
try { argsJson = step.metadata?.toolCall?.argumentsJson || ''; } catch { }
if (toolName === 'run_command' || toolName === 'send_command_input') {
// Command execution approval
try {
const args = JSON.parse(argsJson || '{}');
cmd = args.CommandLine || args.command || args.Input || toolName;
} catch { cmd = toolName; }
desc = `명령어 실행 승인 (${stepType})`;
} else if (toolName === 'browser_subagent') {
// Browser subagent
try {
const args = JSON.parse(argsJson || '{}');
cmd = args.Task?.substring(0, 150) || 'browser task';
} catch { cmd = 'browser_subagent'; }
desc = `브라우저 서브에이전트 실행`;
} else if (stepType === 'CODE_ACTION' || toolName === 'replace_file_content' || toolName === 'multi_replace_file_content' || toolName === 'write_to_file') {
// File modification review
try {
const args = JSON.parse(argsJson || '{}');
cmd = args.TargetFile || args.target_file || toolName;
} catch { cmd = toolName; }
desc = `파일 수정 검토 요청`;
} else if (toolName === 'view_file' || toolName === 'view_file_outline' || toolName === 'view_code_item') {
// File access (usually auto-approved, but handle if pending)
try {
const args = JSON.parse(argsJson || '{}');
cmd = args.AbsolutePath || args.File || toolName;
} catch { cmd = toolName; }
desc = `파일 접근 권한 요청`;
} else if (toolName === 'notify_user') {
// AI asking for user feedback — this needs a different response
try {
const args = JSON.parse(argsJson || '{}');
cmd = args.Message?.substring(0, 200) || 'notify_user';
} catch { cmd = 'notify_user'; }
desc = `사용자 피드백 요청`;
} else if (toolName) {
cmd = toolName;
desc = `도구 실행: ${toolName}`;
} else {
cmd = stepType || 'unknown';
desc = `${stepType} (${stepStatus})`;
}
// Create pending for Discord
const rid = Date.now().toString();
const pending = {
request_id: rid,
conversation_id: bestSessionId,
command: cmd.substring(0, 500),
description: desc.substring(0, 200),
timestamp: Date.now() / 1000,
status: 'pending',
project_name: projectName,
step_index: stepIdx,
step_type: stepType,
step_status: stepStatus,
tool_name: toolName,
auto_detected: true,
};
const pendingDir = path.join(bridgePath, 'pending');
fs.writeFileSync(path.join(pendingDir, `${rid}.json`), JSON.stringify(pending, null, 2));
lastPendingStepIndex = stepIdx;
logToFile(`[PENDING] step=${stepIdx} type=${stepType} status=${stepStatus} tool=${toolName} cmd=${cmd.substring(0, 60)}`);
console.log(`Gravity Bridge: [POLL#${pollCount}] PENDING! step=${stepIdx} ${toolName || stepType} → pending/${rid}.json`);
}
}
} catch (e: any) {
// Only log occasionally to avoid spam
if (pollCount % 10 === 0) {
logToFile(`[PENDING] step query error: ${e.message}`);
}
}
}
// ── Process latestNotifyUserStep ── // ── Process latestNotifyUserStep ──
const notifyStep = bestSession.latestNotifyUserStep; const notifyStep = bestSession.latestNotifyUserStep;
if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) { if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) {