fix(ext): v0.5.5 wrong-LS 자동 복구 — fixLSConnection export + 'input not registered' 감지 시 LS 재연결 + 1회 retry
This commit is contained in:
@@ -105,3 +105,4 @@
|
|||||||
| 12 | **새 AG 도구 추가 시 step-probe step_type 매핑 + approval-handler RPC payload 매핑 양쪽 필수** | browser_subagent Allow |
|
| 12 | **새 AG 도구 추가 시 step-probe step_type 매핑 + approval-handler RPC payload 매핑 양쪽 필수** | browser_subagent Allow |
|
||||||
| 13 | **WS `onConnected`에서 step-probe 상태 리셋 필수** — `stallProbed`/`lastPendingStepIndex`는 TTL 없는 영구 값 | Idle→Resume 신호 소실 |
|
| 13 | **WS `onConnected`에서 step-probe 상태 리셋 필수** — `stallProbed`/`lastPendingStepIndex`는 TTL 없는 영구 값 | Idle→Resume 신호 소실 |
|
||||||
| 14 | **AG proto `uint32` 필드에 음수 전달 금지** — `stepIndex` 등은 `Math.max(0, ...)` 필수 | stepIndex=-1 RPC 400 |
|
| 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) |
|
||||||
|
|||||||
@@ -426,6 +426,7 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
let lastRpcError = '';
|
||||||
for (let i = 0; i < protoVariants.length; i++) {
|
for (let i = 0; i < protoVariants.length; i++) {
|
||||||
try {
|
try {
|
||||||
const payload = protoVariants[i];
|
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)}`);
|
ctx.logToFile(`[APPROVAL-PROTO-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||||
return `RPC-PROTO-${i}:HandleCascadeUserInteraction(${typeLower})`;
|
return `RPC-PROTO-${i}:HandleCascadeUserInteraction(${typeLower})`;
|
||||||
} catch (e: any) {
|
} 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)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,18 +210,18 @@ async function initSDK(context: vscode.ExtensionContext): Promise<boolean> {
|
|||||||
* lowercased (desktop_variet_agent) → no match → falls back to first LS
|
* lowercased (desktop_variet_agent) → no match → falls back to first LS
|
||||||
* found (wrong workspace).
|
* found (wrong workspace).
|
||||||
*/
|
*/
|
||||||
async function fixLSConnection(): Promise<void> {
|
export async function fixLSConnection(): Promise<boolean> {
|
||||||
if (!sdk?.ls) return;
|
if (!sdk?.ls) { logToFile('[LS-FIX] skipped: sdk.ls not available'); return false; }
|
||||||
try {
|
try {
|
||||||
const folders = vscode.workspace.workspaceFolders;
|
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
|
// Generate the workspace hint the same way SDK does, but we'll match case-insensitively
|
||||||
const folder = folders[0].uri.fsPath;
|
const folder = folders[0].uri.fsPath;
|
||||||
const parts = folder.replace(/\\/g, '/').split('/');
|
const parts = folder.replace(/\\/g, '/').split('/');
|
||||||
const hint = parts.slice(-2).join('_').replace(/[-.\s]/g, '_').toLowerCase();
|
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
|
// Find all language_server processes with csrf_token
|
||||||
const { exec } = cp;
|
const { exec } = cp;
|
||||||
@@ -237,12 +237,16 @@ async function fixLSConnection(): Promise<void> {
|
|||||||
{ encoding: 'utf8', timeout: 15000, windowsHide: true }
|
{ encoding: 'utf8', timeout: 15000, windowsHide: true }
|
||||||
);
|
);
|
||||||
output = result.stdout;
|
output = result.stdout;
|
||||||
} catch {
|
} catch (psErr: any) {
|
||||||
return; // Can't discover processes — leave SDK's choice
|
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);
|
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)
|
// Find the line whose workspace_id matches our workspace (case-insensitive)
|
||||||
let matchedLine: string | null = null;
|
let matchedLine: string | null = null;
|
||||||
@@ -261,7 +265,7 @@ async function fixLSConnection(): Promise<void> {
|
|||||||
|
|
||||||
if (!matchedLine) {
|
if (!matchedLine) {
|
||||||
logToFile(`[LS-FIX] No LS process matched hint="${hint}" (${lines.length} processes)`);
|
logToFile(`[LS-FIX] No LS process matched hint="${hint}" (${lines.length} processes)`);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract port and csrf_token from matched line
|
// Extract port and csrf_token from matched line
|
||||||
@@ -271,7 +275,7 @@ async function fixLSConnection(): Promise<void> {
|
|||||||
|
|
||||||
if (!csrfMatch || !extPortMatch) {
|
if (!csrfMatch || !extPortMatch) {
|
||||||
logToFile(`[LS-FIX] Matched LS but missing csrf/port args`);
|
logToFile(`[LS-FIX] Matched LS but missing csrf/port args`);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const csrfToken = csrfMatch[1];
|
const csrfToken = csrfMatch[1];
|
||||||
@@ -281,7 +285,7 @@ async function fixLSConnection(): Promise<void> {
|
|||||||
// Check if SDK already connected to this LS
|
// Check if SDK already connected to this LS
|
||||||
if (sdk.ls.port === extPort) {
|
if (sdk.ls.port === extPort) {
|
||||||
logToFile(`[LS-FIX] SDK already on correct 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)
|
// Find ConnectRPC port via netstat (same as SDK logic)
|
||||||
@@ -297,7 +301,7 @@ async function fixLSConnection(): Promise<void> {
|
|||||||
logToFile(`[LS-FIX] netstat failed, using ext_port=${extPort} for PID=${pid}`);
|
logToFile(`[LS-FIX] netstat failed, using ext_port=${extPort} for PID=${pid}`);
|
||||||
sdk.ls.setConnection(extPort, csrfToken, false);
|
sdk.ls.setConnection(extPort, csrfToken, false);
|
||||||
logToFile(`[LS-FIX] ✅ Reconnected to correct LS: port=${extPort} hint="${hint}" PID=${pid}`);
|
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);
|
const portMatches = netstatOutput.matchAll(/127\.0\.0\.1:(\d+)/g);
|
||||||
@@ -338,7 +342,7 @@ async function fixLSConnection(): Promise<void> {
|
|||||||
if (ok) {
|
if (ok) {
|
||||||
sdk.ls.setConnection(port, csrfToken, useTls);
|
sdk.ls.setConnection(port, csrfToken, useTls);
|
||||||
logToFile(`[LS-FIX] ✅ Reconnected to correct LS: port=${port} ${proto} hint="${hint}" PID=${pid}`);
|
logToFile(`[LS-FIX] ✅ Reconnected to correct LS: port=${port} ${proto} hint="${hint}" PID=${pid}`);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
} catch { /* try next */ }
|
} catch { /* try next */ }
|
||||||
}
|
}
|
||||||
@@ -347,8 +351,10 @@ async function fixLSConnection(): Promise<void> {
|
|||||||
// Last resort: use extension_server_port
|
// Last resort: use extension_server_port
|
||||||
sdk.ls.setConnection(extPort, csrfToken, false);
|
sdk.ls.setConnection(extPort, csrfToken, false);
|
||||||
logToFile(`[LS-FIX] ✅ Reconnected via ext_port=${extPort} hint="${hint}" PID=${pid}`);
|
logToFile(`[LS-FIX] ✅ Reconnected via ext_port=${extPort} hint="${hint}" PID=${pid}`);
|
||||||
|
return true;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logToFile(`[LS-FIX] error: ${err.message}`);
|
logToFile(`[LS-FIX] error: ${err.message}`);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,6 +516,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
recentDiscordSentTexts,
|
recentDiscordSentTexts,
|
||||||
writeChatSnapshot,
|
writeChatSnapshot,
|
||||||
writeChatSnapshotWithFiles,
|
writeChatSnapshotWithFiles,
|
||||||
|
fixLSConnection,
|
||||||
} as BridgeContext);
|
} as BridgeContext);
|
||||||
// Start HTTP bridge with live step-probe state (prevents stale primitive bug)
|
// Start HTTP bridge with live step-probe state (prevents stale primitive bug)
|
||||||
const httpBridgeCtx: HttpBridgeContext = {
|
const httpBridgeCtx: HttpBridgeContext = {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export interface BridgeContext {
|
|||||||
recentDiscordSentTexts: Map<string, number>;
|
recentDiscordSentTexts: Map<string, number>;
|
||||||
writeChatSnapshot: (text: string) => void;
|
writeChatSnapshot: (text: string) => void;
|
||||||
writeChatSnapshotWithFiles: (text: string, files: Array<{ name: string, content: string }>) => void;
|
writeChatSnapshotWithFiles: (text: string, files: Array<{ name: string, content: string }>) => void;
|
||||||
|
fixLSConnection?: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ctx: BridgeContext;
|
let ctx: BridgeContext;
|
||||||
|
|||||||
Reference in New Issue
Block a user