fix(extension): guitar_score step-probe UTF-8 loop + approval stepIndex guard (v0.5.11)

This commit is contained in:
Variet Worker
2026-03-28 09:15:11 +09:00
parent d5fdc41f35
commit 7bbd8749d7
4 changed files with 93 additions and 3 deletions

View File

@@ -29,6 +29,18 @@
## 🔴 Active/Recent Issues ## 🔴 Active/Recent Issues
### [2026-03-28] [step-probe] GetCascadeTrajectorySteps UTF-8 에러 무한 루프
- **증상**: `guitar_score` 프로젝트에서 `[STEP-PROBE] error: ...invalid UTF-8` 에러가 5초마다 반복되며 Discord 승인 신호가 전달되지 않음.
- **원인**: AG LS 서버에서 특정 step의 `CortexStepEphemeralMessage.content`에 바이너리 데이터(이미지 등) 포함 → proto UTF-8 직렬화 500 에러. `catch(e)` 블록에서 `stallProbed=true`를 설정하지 않아 `!ctx.stallProbed` 조건이 항상 true → 5초마다 동일 요청 무한 재시도.
- **해결** (v0.5.11): `catch` 블록에서 UTF-8 에러 감지 시 `stepOffset=currentCount-20`으로 fallback 요청. offset도 실패 시 `stallProbed=true` 설정하여 루프 차단. `delta>0` 이벤트 발생 시 L433에서 자동 리셋.
- **주의**: `stallProbed=true`는 영구 Lock이 아님 — `delta>0` 시 자동 리셋. UTF-8 에러는 AG 서버 측 문제(이미지/바이너리 데이터가 ephemeral message에 포함)이므로 Extension에서 graceful fallback만 처리.
### [2026-03-28] [approval-handler] stepIndex 미확정 시 wrong-stepIndex RPC 낭비
- **증상**: DOM observer 경로로 `terminal_command` pending 생성 후 Discord 승인 시 `HandleCascadeUserInteraction(stepIndex=0)` → `"input not registered for step 0"` → LS reconnect → 재시도 → DOM click fallback으로 저하. (wrong-LS와 동일한 증상이나 다른 원인)
- **원인**: `ctx.lastPendingStepIndex=-1` (step-probe가 UTF-8 에러로 WAITING 미감지)임에도 `Math.max(0, -1)=0`으로 clamp되어 존재하지 않는 step 0에 RPC 전송.
- **해결** (v0.5.11): `effectiveStepIndex = stepIndex >= 0 ? stepIndex : (lastPendingStepIndex >= 0 ? lastPendingStepIndex : -1)`. `effectiveStepIndex < 0`이면 RPC 블록 전체 skip → DOM click 직행 (기존과 동작 동일, LS reconnect 낭비 제거).
- **주의**: 기존 규칙 #14(`uint32`에 음수 금지)와 충돌처럼 보이나, `effectiveStepIndex=-1`일 때 RPC 자체를 **전송하지 않으므로** 위반 아님. RPC 전송 시에는 여전히 유효한 stepIndex만 사용.
### [2026-03-25] [Architecture] Discord Signal Drop & Extension Freezes ### [2026-03-25] [Architecture] Discord Signal Drop & Extension Freezes
- **증상**: 장시간 자리비움 후 복귀 시 Discord로 승인 신호가 오지 않거나 VS Code UI가 간헐적/지속적으로 멈춤(Freeze). - **증상**: 장시간 자리비움 후 복귀 시 Discord로 승인 신호가 오지 않거나 VS Code UI가 간헐적/지속적으로 멈춤(Freeze).
- **원인**: - **원인**:

View File

@@ -0,0 +1,5 @@
# Devlog — 2026-03-28
| # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------|
| 001 | 09:12 | guitar_score step-probe UTF-8 무한루프 수정 + approval stepIndex 보정 (v0.5.11) | pending | ✅ |

View File

@@ -313,7 +313,8 @@ async function processResponseFile(filePath: string) {
*/ */
export async function tryApprovalStrategies(approved: boolean, sessionId: string, stepType: string = '', stepIndex: number = -1): Promise<string> { export async function tryApprovalStrategies(approved: boolean, sessionId: string, stepType: string = '', stepIndex: number = -1): Promise<string> {
const action = approved ? 'APPROVE' : 'REJECT'; const action = approved ? 'APPROVE' : 'REJECT';
const effectiveStepIndex = Math.max(0, stepIndex >= 0 ? stepIndex : ctx.lastPendingStepIndex); const effectiveStepIndex = stepIndex >= 0 ? stepIndex
: (ctx.lastPendingStepIndex >= 0 ? ctx.lastPendingStepIndex : -1);
ctx.logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${effectiveStepIndex}`); 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) ── // ── Dynamic Command Discovery (log what's available during WAITING state) ──
@@ -338,7 +339,7 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string
// ══════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════
// STRATEGY 0-PROTO: Correct proto-based RPC (decoded from AG source) // STRATEGY 0-PROTO: Correct proto-based RPC (decoded from AG source)
// ══════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════
if (ctx.sdk && approved) { if (ctx.sdk && approved && effectiveStepIndex >= 0) {
// Build interaction sub-message based on step_type // Build interaction sub-message based on step_type
const typeLower = stepType.toLowerCase().replace('cortex_step_type_', ''); const typeLower = stepType.toLowerCase().replace('cortex_step_type_', '');
let interactionPayload: Record<string, any> = {}; let interactionPayload: Record<string, any> = {};

View File

@@ -601,7 +601,79 @@ function setupMonitor() {
} }
} }
} catch (e: any) { } catch (e: any) {
ctx.logToFile(`[STEP-PROBE] error: ${e.message}`); ctx.logToFile(`[STEP-PROBE] error: ${e.message?.substring(0, 150)}`);
// UTF-8 invalid data in a step causes a permanent 500 error on full fetch.
// Attempt stepOffset to skip that step and fetch only recent steps.
const isUtf8Error = e.message?.includes('invalid UTF-8') || e.message?.includes('proto:');
if (isUtf8Error && ctx.sdk) {
try {
const utf8Offset = Math.max(0, currentCount - 20);
ctx.logToFile(`[STEP-PROBE] UTF-8 fallback: retrying with stepOffset=${utf8Offset}`);
const offsetResp = await ctx.sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
cascadeId: bestSessionId,
stepOffset: utf8Offset,
verbosity: 1,
});
if (offsetResp?.steps?.length > 0) {
const offsetSteps = offsetResp.steps;
ctx.logToFile(`[STEP-PROBE] UTF-8 offset=${utf8Offset} returned ${offsetSteps.length} steps`);
let foundWaitingInOffset = false;
for (let osi = offsetSteps.length - 1; osi >= 0; osi--) {
const oStep = offsetSteps[osi];
if (oStep?.status === 'CORTEX_STEP_STATUS_WAITING') {
foundWaitingInOffset = true;
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 {
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;
ctx.logToFile(`[STEP-PROBE] ★ WAITING (via UTF-8 offset)! step=${actualIndex} type=${oStep.type} cmd='${command}'`);
if (actualIndex !== ctx.lastPendingStepIndex) {
ctx.stallProbed = true;
if (actualIndex > ctx.lastPendingStepIndex) ctx.lastPendingStepIndex = actualIndex;
lastPendingTime = Date.now();
ctx.sawRunningAfterPending = false;
if (ctx.projectName !== 'default') {
writePendingApproval({
conversation_id: ctx.activeSessionId,
command,
description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`,
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_utf8_offset',
});
}
}
// NOTE: no break — process ALL parallel WAITING steps
}
}
if (!foundWaitingInOffset) {
ctx.logToFile(`[STEP-PROBE] UTF-8 offset: no WAITING found — stallProbed=true to prevent loop`);
ctx.stallProbed = true; // prevent retry loop; resets on delta>0
ctx.sessionStalled = false;
}
} else {
ctx.logToFile(`[STEP-PROBE] UTF-8 offset returned empty — stallProbed=true`);
ctx.stallProbed = true;
}
} catch (oe: any) {
ctx.logToFile(`[STEP-PROBE] UTF-8 offset also failed: ${oe.message?.substring(0, 100)}`);
ctx.stallProbed = true; // permanent error — block retry loop; resets on delta>0
}
}
} }
} }