fix(bridge): resolve missing Discord embed bodies by extracting detailed RPC payload from step_probe

This commit is contained in:
Variet Worker
2026-04-10 08:02:41 +09:00
parent fadd39424b
commit 89c95de18c
5 changed files with 91 additions and 49 deletions

View File

@@ -10,6 +10,12 @@
---
### [2026-04-09] [Bridge] Discord Body Content Missing Due to Step Probe Dummy Payload
- **증상**: 대규모 UI 마이그레이션 후, 디스코드 승인 메시지 본문에 실행할 코드/명령어가 완전히 누락되고 "Step #15"와 같은 디폴트 텍스트만 전송됨.
- **원인**: Native UI 변경으로 인해 DOM observer가 추출한 버튼 텍스트("Always run")가 `http-bridge.ts` 필터 우회 및 bot.py에서 지연(defer) 처리됨. 반면 `step-probe.ts``GetAllCascadeTrajectories` 폴링을 통해 동시에 발생시킨 dummy pending payload (명령어 상세 내용이 없이 `Step #XX` 라는 텍스트만 포함)가 봇에 의해 먼저 자동 승인되면서 정작 실제 코드 영역 정보가 증발함.
- **해결**: `step-probe.ts` 내에 `formatStepProbeCommand` 헬퍼 함수를 추가하여, WAITING 상태 스텝의 `argumentsJson` 데이터를 직접 파싱하고 `CommandLine`, `TargetFile` 등 실제 명령어와 상세 인자/코드를 `command``description`으로 할당하여 브릿지로 넘기도록 패치함. DOM 옵저버의 불안정성과 관계없이 일관된 본문 전달 보장.
- **주의**: UI 스크래핑에 의존하는 DOM Observer 방식은 UI 레이아웃, 아이콘 삽입 등에 취약하므로, 상세 페이로드 추출은 항상 100% 신뢰 가능한 SDK RPC(`step-probe.ts`) 데이터를 우선 사용하도록 구성해야 함.
## 포맷
### [2026-03-23] [Extension] Cross-Project DOM Observer Leakage

View File

@@ -6,3 +6,4 @@
| 002 | 22:30 | Agent UI 버튼 무시 버그 긴급수정 (CodeLens 필터교정) | `HEAD` | ✅ |
| 003 | 23:15 | Native UI 아이콘 글루잉 대응 스캐너 픽스 (DOM Regex 매칭 강화) | `HEAD` | ✅ |
| 004 | 00:10 | Discord Signal Relay & Auto-Approve Body Null 버그 수정 (False Positive 차단) | "HEAD" | ✅ |
| 005 | 23:00 | fix: Resolve empty Discord embed body by populating detailed step-probe payload | \\ | ? |

View File

@@ -213,7 +213,7 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
}
// "Run" button → step_probe handles these with full command detail
// Only let through if session is stalled AND step_probe hasn't created a pending yet
if (/^Run\b/i.test(cmd)) {
if (/^(?:Always\s*)?Run\b/i.test(cmd)) {
if (!ctx.sessionStalled || ctx.lastPendingStepIndex >= 0) {
ctx.logToFile(`[HTTP] filtered "Run" — ${!ctx.sessionStalled ? 'not stalled' : 'step_probe pending exists'}`);
res.writeHead(200, { 'Content-Type': 'application/json' });

View File

@@ -102,6 +102,36 @@ export function resetPendingStateForReconnect(): void {
// handleDiffReviewResponse → moved to ./approval-handler.ts
export function formatStepProbeCommand(toolName: string, actualIndex: number, stepType: string, toolCall: any): { cmd: string, desc: string, isSafe: boolean } {
let cmd = toolName;
let desc = `Step #${actualIndex} (${stepType.replace('CORTEX_STEP_TYPE_', '')})`;
let isSafe = false;
if (toolCall?.argumentsJson) {
try {
const args = JSON.parse(toolCall.argumentsJson);
isSafe = args.SafeToAutoRun === true;
if (args.CommandLine) {
cmd = toolName;
desc = args.CommandLine;
} else if (args.TargetFile) {
cmd = `${toolName}: ${args.TargetFile.split(/[\\/]/).pop()}`;
if (args.CodeContent) desc = args.CodeContent;
else if (args.ReplacementChunks) desc = JSON.stringify(args.ReplacementChunks, null, 2);
else desc = toolCall.argumentsJson;
} else {
const val = args.DirectoryPath || args.SearchPath || args.AbsolutePath || args.Url || args.Query || args.Prompt || Object.values(args).find((v: any) => typeof v === 'string' && v.length > 2);
if (val) {
cmd = toolName;
desc = String(val);
} else {
cmd = `${toolName}: ${Object.keys(args).join(', ')}`;
}
}
} catch { }
}
return { cmd, desc, isSafe };
}
/**
* Write a registration file for the Bot to discover session → project mapping.
* Called automatically on first step event per session.
@@ -481,20 +511,8 @@ function setupMonitor() {
if (oStep?.status === 'CORTEX_STEP_STATUS_WAITING') {
const toolCall = oStep?.metadata?.toolCall;
const toolName = toolCall?.name || (oStep.type || '').replace('CORTEX_STEP_TYPE_', '').toLowerCase();
let command = toolName;
if (toolCall?.argumentsJson) {
try {
const args = JSON.parse(toolCall.argumentsJson);
if (args.CommandLine) command = `${toolName}: ${args.CommandLine.substring(0, 1500)}`;
else if (args.TargetFile) command = `${toolName}: ${args.TargetFile}`;
else {
// Show first meaningful value (path, query, etc.)
const val = args.DirectoryPath || args.SearchPath || args.AbsolutePath || args.Url || args.Query || args.Prompt || Object.values(args).find((v: any) => typeof v === 'string' && v.length > 2);
command = val ? `${toolName}: ${String(val).substring(0, 500)}` : `${toolName}: ${Object.keys(args).join(', ')}`;
}
} catch { command = toolName; }
}
const actualIndex = offset + osi;
const { cmd: command, desc: detailedDescription, isSafe: isSafeToAutoRun } = formatStepProbeCommand(toolName, actualIndex, oStep.type || '', toolCall);
ctx.logToFile(`[STEP-PROBE] ★ WAITING (via offset)! step=${actualIndex} type=${oStep.type} cmd='${command}'`);
if (actualIndex !== ctx.lastPendingStepIndex) {
ctx.stallProbed = true;
@@ -512,13 +530,14 @@ function setupMonitor() {
writePendingApproval({
conversation_id: ctx.activeSessionId,
command,
description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`,
description: detailedDescription,
step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission'
: ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName) ? 'code_edit'
: ['browser_subagent', 'open_browser_url'].includes(toolName) ? 'browser_subagent'
: toolName,
step_index: actualIndex,
source: 'step_probe_offset',
safe_to_auto_run: isSafeToAutoRun,
});
}
}
@@ -543,27 +562,9 @@ function setupMonitor() {
// Extract command from metadata.toolCall or direct fields
const toolCall = step?.metadata?.toolCall;
const toolName = toolCall?.name || stepType.replace('CORTEX_STEP_TYPE_', '').toLowerCase();
let command = toolName;
let isSafeToAutoRun = false;
const { cmd: command, desc: description, isSafe: isSafeToAutoRun } = formatStepProbeCommand(toolName, si, stepType, toolCall);
// Parse argumentsJson for command details
if (toolCall?.argumentsJson) {
try {
const args = JSON.parse(toolCall.argumentsJson);
isSafeToAutoRun = args.SafeToAutoRun === true;
if (args.CommandLine) {
command = `${toolName}: ${args.CommandLine.substring(0, 1500)}`;
} else if (args.TargetFile) {
command = `${toolName}: ${args.TargetFile}`;
} else {
const val = args.DirectoryPath || args.SearchPath || args.AbsolutePath || args.Url || args.Query || args.Prompt || Object.values(args).find((v: any) => typeof v === 'string' && v.length > 2);
command = val ? `${toolName}: ${String(val).substring(0, 500)}` : `${toolName}: ${Object.keys(args).join(', ')}`;
}
} catch { command = toolName; }
}
const description = `Step #${si} (${stepType.replace('CORTEX_STEP_TYPE_', '')})`;
ctx.logToFile(`[STEP-PROBE] ★ WAITING! step=${si} type=${stepType} cmd='${command}'`);
if (si !== ctx.lastPendingStepIndex) {
@@ -629,21 +630,8 @@ function setupMonitor() {
foundWaitingInOffset = true;
const toolCall = oStep?.metadata?.toolCall;
const toolName = toolCall?.name || (oStep.type || '').replace('CORTEX_STEP_TYPE_', '').toLowerCase();
let command = toolName;
let isSafeToAutoRun = false;
if (toolCall?.argumentsJson) {
try {
const args = JSON.parse(toolCall.argumentsJson);
isSafeToAutoRun = args.SafeToAutoRun === true;
if (args.CommandLine) command = `${toolName}: ${args.CommandLine.substring(0, 1500)}`;
else if (args.TargetFile) command = `${toolName}: ${args.TargetFile}`;
else {
const val = args.DirectoryPath || args.SearchPath || args.AbsolutePath || args.Url || args.Query || args.Prompt || Object.values(args).find((v: any) => typeof v === 'string' && v.length > 2);
command = val ? `${toolName}: ${String(val).substring(0, 500)}` : `${toolName}: ${Object.keys(args).join(', ')}`;
}
} catch { command = toolName; }
}
const actualIndex = utf8Offset + osi;
const { cmd: command, desc: detailedDescription, isSafe: isSafeToAutoRun } = formatStepProbeCommand(toolName, actualIndex, oStep.type || '', toolCall);
ctx.logToFile(`[STEP-PROBE] ★ WAITING (via UTF-8 offset)! step=${actualIndex} type=${oStep.type} cmd='${command}'`);
if (actualIndex !== ctx.lastPendingStepIndex) {
ctx.stallProbed = true;
@@ -654,7 +642,7 @@ function setupMonitor() {
writePendingApproval({
conversation_id: ctx.activeSessionId,
command,
description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`,
description: detailedDescription,
step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission'
: ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName) ? 'code_edit'
: ['browser_subagent', 'open_browser_url'].includes(toolName) ? 'browser_subagent'

47
test.js Normal file
View File

@@ -0,0 +1,47 @@
const jsdom = require('jsdom');
const { JSDOM } = jsdom;
const dom = new JSDOM($html);
const document = dom.window.document;
function extractContext(b){
var curr = b.parentElement;
var bestDesc = '';
var btnText = (b.innerText || b.textContent || '').trim();
for (var i = 0; i < 15 && curr; i++) {
var codeEl = curr.querySelector('pre, code, [class*="command"], [class*="terminal"], [class*="code"]');
if (codeEl && codeEl !== b && !b.contains(codeEl)) {
var codeText = (codeEl.innerText || codeEl.textContent || '').trim();
if (codeText.length > 0 && codeText !== btnText) {
return codeText.substring(0, 500);
}
}
var full = (curr.innerText || curr.textContent || '');
var btnRawText = (b.textContent || '');
var desc = full.replace(btnRawText, '').trim();
if (desc.length > 5 && desc !== btnText && bestDesc.length < desc.length) {
bestDesc = desc;
}
var cname = curr.className;
if (typeof cname === 'string' && (cname.includes('message') || cname.includes('step') || cname.includes('markdown'))) {
break;
}
curr = curr.parentElement;
}
return bestDesc.substring(0, 500);
}
const btns = document.querySelectorAll('button');
let ran = false;
for(let b of btns) {
let t = (b.textContent||'').trim();
if(t === 'Always run' || t === 'Run') {
const desc = extractContext(b);
console.log("Found button: " + t);
console.log("Extracted Description: " + desc);
ran = true;
}
}
if(!ran) console.log("No matching button found");