From 6234301a47a543a0f836bb014c14e2bd95fb7850 Mon Sep 17 00:00:00 2001 From: Variet Worker Date: Sat, 21 Mar 2026 21:15:18 +0900 Subject: [PATCH] =?UTF-8?q?fix(ext):=20v0.5.5=20wrong-LS=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EB=B3=B5=EA=B5=AC=20=E2=80=94=20fixLSConnection=20?= =?UTF-8?q?export=20+=20'input=20not=20registered'=20=EA=B0=90=EC=A7=80=20?= =?UTF-8?q?=EC=8B=9C=20LS=20=EC=9E=AC=EC=97=B0=EA=B2=B0=20+=201=ED=9A=8C?= =?UTF-8?q?=20retry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/references/known-issues.md | 1 + extension/src/approval-handler.ts | 31 +++++++++++++++++++++++++++++- extension/src/extension.ts | 31 ++++++++++++++++++------------ extension/src/step-probe.ts | 1 + 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/.agents/references/known-issues.md b/.agents/references/known-issues.md index c203f4d..f128197 100644 --- a/.agents/references/known-issues.md +++ b/.agents/references/known-issues.md @@ -105,3 +105,4 @@ | 12 | **새 AG 도구 추가 시 step-probe step_type 매핑 + approval-handler RPC payload 매핑 양쪽 필수** | browser_subagent Allow | | 13 | **WS `onConnected`에서 step-probe 상태 리셋 필수** — `stallProbed`/`lastPendingStepIndex`는 TTL 없는 영구 값 | Idle→Resume 신호 소실 | | 14 | **AG proto `uint32` 필드에 음수 전달 금지** — `stepIndex` 등은 `Math.max(0, ...)` 필수 | stepIndex=-1 RPC 400 | +| 15 | **RPC "input not registered" = wrong-LS 연결** — `fixLSConnection()` 자동 재시도 필수, `lines.length<=1` 조기종료 금지 | Deriva wrong-LS (v0.5.5) | diff --git a/extension/src/approval-handler.ts b/extension/src/approval-handler.ts index 264a583..4d2b169 100644 --- a/extension/src/approval-handler.ts +++ b/extension/src/approval-handler.ts @@ -426,6 +426,7 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string }, }, ]; + let lastRpcError = ''; for (let i = 0; i < protoVariants.length; i++) { try { const payload = protoVariants[i]; @@ -434,7 +435,35 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string 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)}`); + lastRpcError = e.message || ''; + ctx.logToFile(`[APPROVAL-PROTO-${i}] ❌ ${lastRpcError.substring(0, 300)}`); + } + } + + // ── Auto-recovery: wrong-LS detection ────────────────────── + // All 3 proto variants failed. If the error is "input not registered", + // SDK is likely connected to wrong LS process. Attempt fixLSConnection + // and retry ONE time to avoid permanent failure. + if (ctx.fixLSConnection && lastRpcError.includes('input not registered')) { + ctx.logToFile('[APPROVAL] ⚠️ wrong-LS detected ("input not registered"), attempting LS fix...'); + try { + const lsChanged = await ctx.fixLSConnection(); + if (lsChanged) { + ctx.logToFile('[APPROVAL] LS reconnected — retrying first proto variant...'); + try { + const retryPayload = protoVariants[0]; + ctx.logToFile(`[APPROVAL-RETRY] HandleCascadeUserInteraction(${JSON.stringify(retryPayload).substring(0, 250)})`); + const retryResult = await ctx.sdk.ls.rawRPC('HandleCascadeUserInteraction', retryPayload); + ctx.logToFile(`[APPROVAL-RETRY] ✅ SUCCESS: ${JSON.stringify(retryResult).substring(0, 200)}`); + return `RPC-RETRY:HandleCascadeUserInteraction(${typeLower})`; + } catch (retryErr: any) { + ctx.logToFile(`[APPROVAL-RETRY] ❌ ${retryErr.message?.substring(0, 200)}`); + } + } else { + ctx.logToFile('[APPROVAL] LS not changed — already on correct port or fix unavailable'); + } + } catch (fixErr: any) { + ctx.logToFile(`[APPROVAL] fixLSConnection error: ${fixErr.message?.substring(0, 200)}`); } } } diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 96dd413..07dfec6 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -210,18 +210,18 @@ async function initSDK(context: vscode.ExtensionContext): Promise { * lowercased (desktop_variet_agent) → no match → falls back to first LS * found (wrong workspace). */ -async function fixLSConnection(): Promise { - if (!sdk?.ls) return; +export async function fixLSConnection(): Promise { + if (!sdk?.ls) { logToFile('[LS-FIX] skipped: sdk.ls not available'); return false; } try { const folders = vscode.workspace.workspaceFolders; - if (!folders || folders.length === 0) return; + if (!folders || folders.length === 0) { logToFile('[LS-FIX] skipped: no workspace folders'); return false; } // Generate the workspace hint the same way SDK does, but we'll match case-insensitively const folder = folders[0].uri.fsPath; const parts = folder.replace(/\\/g, '/').split('/'); const hint = parts.slice(-2).join('_').replace(/[-.\s]/g, '_').toLowerCase(); - if (!hint) return; + if (!hint) { logToFile('[LS-FIX] skipped: empty hint'); return false; } // Find all language_server processes with csrf_token const { exec } = cp; @@ -237,12 +237,16 @@ async function fixLSConnection(): Promise { { encoding: 'utf8', timeout: 15000, windowsHide: true } ); output = result.stdout; - } catch { - return; // Can't discover processes — leave SDK's choice + } catch (psErr: any) { + logToFile(`[LS-FIX] skipped: PowerShell failed — ${psErr.message?.substring(0, 100)}`); + return false; } const lines = output.split('\n').filter((l: string) => l.trim().length > 0); - if (lines.length <= 1) return; // Only one LS — no ambiguity + if (lines.length === 0) { logToFile('[LS-FIX] skipped: no LS processes found'); return false; } + // NOTE: Do NOT skip on single LS — SDK may have fallen back to wrong LS + // due to case-sensitive hint mismatch, even when only one process exists. + logToFile(`[LS-FIX] found ${lines.length} LS process(es), hint="${hint}"`); // Find the line whose workspace_id matches our workspace (case-insensitive) let matchedLine: string | null = null; @@ -261,7 +265,7 @@ async function fixLSConnection(): Promise { if (!matchedLine) { logToFile(`[LS-FIX] No LS process matched hint="${hint}" (${lines.length} processes)`); - return; + return false; } // Extract port and csrf_token from matched line @@ -271,7 +275,7 @@ async function fixLSConnection(): Promise { if (!csrfMatch || !extPortMatch) { logToFile(`[LS-FIX] Matched LS but missing csrf/port args`); - return; + return false; } const csrfToken = csrfMatch[1]; @@ -281,7 +285,7 @@ async function fixLSConnection(): Promise { // Check if SDK already connected to this LS if (sdk.ls.port === extPort) { logToFile(`[LS-FIX] SDK already on correct LS port=${extPort}`); - return; + return false; } // Find ConnectRPC port via netstat (same as SDK logic) @@ -297,7 +301,7 @@ async function fixLSConnection(): Promise { logToFile(`[LS-FIX] netstat failed, using ext_port=${extPort} for PID=${pid}`); sdk.ls.setConnection(extPort, csrfToken, false); logToFile(`[LS-FIX] ✅ Reconnected to correct LS: port=${extPort} hint="${hint}" PID=${pid}`); - return; + return true; } const portMatches = netstatOutput.matchAll(/127\.0\.0\.1:(\d+)/g); @@ -338,7 +342,7 @@ async function fixLSConnection(): Promise { if (ok) { sdk.ls.setConnection(port, csrfToken, useTls); logToFile(`[LS-FIX] ✅ Reconnected to correct LS: port=${port} ${proto} hint="${hint}" PID=${pid}`); - return; + return true; } } catch { /* try next */ } } @@ -347,8 +351,10 @@ async function fixLSConnection(): Promise { // Last resort: use extension_server_port sdk.ls.setConnection(extPort, csrfToken, false); logToFile(`[LS-FIX] ✅ Reconnected via ext_port=${extPort} hint="${hint}" PID=${pid}`); + return true; } catch (err: any) { logToFile(`[LS-FIX] error: ${err.message}`); + return false; } } @@ -510,6 +516,7 @@ export async function activate(context: vscode.ExtensionContext) { recentDiscordSentTexts, writeChatSnapshot, writeChatSnapshotWithFiles, + fixLSConnection, } as BridgeContext); // Start HTTP bridge with live step-probe state (prevents stale primitive bug) const httpBridgeCtx: HttpBridgeContext = { diff --git a/extension/src/step-probe.ts b/extension/src/step-probe.ts index cd8a246..9a054d5 100644 --- a/extension/src/step-probe.ts +++ b/extension/src/step-probe.ts @@ -30,6 +30,7 @@ export interface BridgeContext { recentDiscordSentTexts: Map; writeChatSnapshot: (text: string) => void; writeChatSnapshotWithFiles: (text: string, files: Array<{ name: string, content: string }>) => void; + fixLSConnection?: () => Promise; } let ctx: BridgeContext;