diff --git a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json index 98f4535..e93e4e0 100644 --- a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json +++ b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json @@ -1,5 +1,5 @@ { "result": "Final report with inconsistencies. This should be long enough to pass validation.", - "createdAt": 1778137049532, + "createdAt": 1778170647495, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json index 1b9ee74..f300b94 100644 --- a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json +++ b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json @@ -1,5 +1,5 @@ { "result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", - "createdAt": 1778137049529, + "createdAt": 1778170647483, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json index fdb7177..70dc148 100644 --- a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json +++ b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json @@ -1,5 +1,5 @@ { "result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", - "createdAt": 1778137049527, + "createdAt": 1778170647479, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json index 1ec2769..c9c2ead 100644 --- a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json +++ b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json @@ -1,5 +1,5 @@ { - "result": "---\nid: stress_conflict_1778137049510\ndate: 2026-05-07T06:57:29.533Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (16ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (2ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (3ms)\n", - "createdAt": 1778137049533, + "result": "---\nid: stress_conflict_1778170647465\ndate: 2026-05-07T16:17:27.498Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (9ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (5ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (12ms)\n", + "createdAt": 1778170647499, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/missions/stress_conflict_1778137049510.json b/.astra/tests/stress/.astra/missions/stress_conflict_1778170647465.json similarity index 78% rename from .astra/tests/stress/.astra/missions/stress_conflict_1778137049510.json rename to .astra/tests/stress/.astra/missions/stress_conflict_1778170647465.json index d1b6c01..a5f11e4 100644 --- a/.astra/tests/stress/.astra/missions/stress_conflict_1778137049510.json +++ b/.astra/tests/stress/.astra/missions/stress_conflict_1778170647465.json @@ -1,8 +1,8 @@ { - "missionId": "stress_conflict_1778137049510", + "missionId": "stress_conflict_1778170647465", "status": "completed", - "startTime": "2026-05-07T06:57:29.510Z", - "totalElapsedMs": 24, + "startTime": "2026-05-07T16:17:27.465Z", + "totalElapsedMs": 36, "results": { "planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", "researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", @@ -16,30 +16,30 @@ { "from": "idle", "to": "planner", - "durationMs": 16, + "durationMs": 9, "message": "전략 수립 중...", - "ts": "2026-05-07T06:57:29.526Z" + "ts": "2026-05-07T16:17:27.474Z" }, { "from": "planner", "to": "researcher", - "durationMs": 2, + "durationMs": 5, "message": "핵심 정보 수집 및 분석 중...", - "ts": "2026-05-07T06:57:29.528Z" + "ts": "2026-05-07T16:17:27.479Z" }, { "from": "researcher", "to": "writer", - "durationMs": 3, + "durationMs": 12, "message": "최종 리포트 작성 및 편집 중...", - "ts": "2026-05-07T06:57:29.531Z" + "ts": "2026-05-07T16:17:27.491Z" }, { "from": "writer", "to": "completed", - "durationMs": 3, + "durationMs": 10, "message": "미션 완료", - "ts": "2026-05-07T06:57:29.534Z" + "ts": "2026-05-07T16:17:27.501Z" } ], "resilienceMetrics": { diff --git a/package-lock.json b/package-lock.json index 7f3aac7..9c8b8ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "astra", - "version": "2.80.5", + "version": "2.80.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "astra", - "version": "2.80.5", + "version": "2.80.18", "license": "MIT", "dependencies": { "marked": "^18.0.2", @@ -58,7 +58,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1943,7 +1942,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2841,7 +2839,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -4398,7 +4395,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index af9de03..2d641fc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "astra", "displayName": "Astra", "description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.", - "version": "2.80.17", + "version": "2.80.18", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/agent.ts b/src/agent.ts index 6a930de..6a8fd7a 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -1942,35 +1942,37 @@ export class AgentExecutor { temperature: number; }): Promise<{ response: Response; engine: 'lmstudio' | 'ollama'; apiUrl: string }> { const { baseUrl, modelName, reqMessages, temperature } = params; - const primaryEngine = resolveEngine(baseUrl); - const engines = primaryEngine === 'lmstudio' ? ['lmstudio', 'ollama'] as const : ['ollama', 'lmstudio'] as const; + const engine = resolveEngine(baseUrl); // 사용자가 설정한 엔진만 사용 + const apiUrl = buildApiUrl(baseUrl, engine, 'chat'); + const messageVariants = this.buildEngineMessageVariants(reqMessages, engine); + const modelCandidates = this.buildModelCandidates(modelName, engine); let lastError: Error | null = null; - for (const engine of engines) { - const apiUrl = buildApiUrl(baseUrl, engine, 'chat'); - const messageVariants = this.buildEngineMessageVariants(reqMessages, engine); - const modelCandidates = this.buildModelCandidates(modelName, engine); - - for (const candidateModel of modelCandidates) { - for (const variant of messageVariants) { - const streamBody = { - model: candidateModel, - messages: variant.messages, - stream: true, - ...(engine === 'lmstudio' - ? { max_tokens: 4096, temperature } - : { options: { num_ctx: 32768, num_predict: 4096, temperature } }), - }; + // 같은 엔진 내에서만 model candidate / message variant retry + for (const candidateModel of modelCandidates) { + for (const variant of messageVariants) { + const streamBody = { + model: candidateModel, + messages: variant.messages, + stream: true, + ...(engine === 'lmstudio' + ? { max_tokens: 4096, temperature } + : { options: { num_ctx: 32768, num_predict: 4096, temperature } }), + }; + // 일시적 네트워크 오류용 retry (최대 2회, 지수 backoff) + const MAX_RETRIES = 2; + for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { try { + if (attempt > 0) { + const delay = 500 * Math.pow(2, attempt - 1); // 500ms, 1000ms + await new Promise(r => setTimeout(r, delay)); + logInfo('AI streaming request retry.', { engine, attempt, model: candidateModel }); + } logInfo('AI streaming request started.', { - engine, - apiUrl, - model: candidateModel, - variant: variant.name, - messageCount: variant.messages.length, - roles: variant.messages.map(message => message.role), - firstUserPreview: summarizeText(String(variant.messages.find(message => message.role === 'user')?.content || ''), 300) + engine, apiUrl, model: candidateModel, + variant: variant.name, messageCount: variant.messages.length, + attempt }); const response = await fetch(apiUrl, { method: 'POST', @@ -1988,7 +1990,12 @@ export class AgentExecutor { if (!response.ok) { const errText = await response.text(); lastError = new Error(`AI Engine error (${engine}/${variant.name}): ${response.status} - ${summarizeText(errText, 300)}`); - logError('AI streaming request returned non-OK status.', { engine, variant: variant.name, apiUrl, status: response.status, body: summarizeText(errText, 500) }); + logError('AI streaming request returned non-OK status.', { + engine, variant: variant.name, apiUrl, + status: response.status, body: summarizeText(errText, 500) + }); + // 4xx는 재시도해도 의미없음. 5xx만 재시도. + if (response.status >= 400 && response.status < 500) break; continue; } @@ -1996,13 +2003,26 @@ export class AgentExecutor { return { response, engine, apiUrl }; } catch (error: any) { lastError = error instanceof Error ? error : new Error(String(error)); - logError('AI streaming request failed.', { engine, variant: variant.name, apiUrl, model: candidateModel, error: lastError.message }); + // AbortError는 사용자가 취소한 것이므로 retry 금지 + if (lastError.name === 'AbortError') { + throw lastError; + } + logError('AI streaming request failed.', { + engine, variant: variant.name, apiUrl, model: candidateModel, + attempt, error: lastError.message + }); } } } } - throw lastError || new Error('Unable to connect to AI engine.'); + // 명확한 에러 메시지: 어느 엔진이 실패했는지 사용자에게 알림 + const engineLabel = engine === 'lmstudio' ? 'LM Studio' : 'Ollama'; + throw new Error( + `${engineLabel} 엔진에 연결할 수 없습니다. ` + + `${engineLabel}가 실행 중이고 모델 '${modelName}'이 로드되어 있는지 확인하세요. ` + + `(원인: ${lastError?.message || 'unknown'})` + ); } private normalizeMessages(messages: ChatMessage[]) { diff --git a/src/extension.ts b/src/extension.ts index 6f44c8e..3df19ba 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -90,6 +90,14 @@ export function deactivate() { } async function runInitialSetup(context: vscode.ExtensionContext) { + // 이미 사용자가 URL을 설정했다면 자동 감지를 스킵 + const existingUrl = vscode.workspace.getConfiguration('g1nation').get('ollamaUrl'); + if (existingUrl && existingUrl.trim()) { + context.globalState.update('setupComplete', true); + logInfo('Initial setup skipped: ollamaUrl already configured.', { existingUrl }); + return; + } + try { let engineName = ''; let modelName = ''; diff --git a/src/sidebarProvider.ts b/src/sidebarProvider.ts index c1d9674..d9332e2 100644 --- a/src/sidebarProvider.ts +++ b/src/sidebarProvider.ts @@ -51,6 +51,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn private _currentSessionBrainId: string | null = null; private _currentNegativePrompt: string = ''; private readonly _chronicle = new ProjectChronicleManager(); + private _modelDiscoveryInFlight = false; constructor( private readonly _extensionUri: vscode.Uri, @@ -75,13 +76,18 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn }; // [State Persistence Fix] 사이드바가 다시 보여질 때 세팅값 자동 복원 + let _lastVisibilityRefresh = 0; webviewView.onDidChangeVisibility(() => { - if (webviewView.visible) { - logInfo('Sidebar became visible, restoring state...'); - void this._sendModels(); - void this._sendBrainProfiles(); - void this._sendAgentsList(); - } + if (!webviewView.visible) return; + const now = Date.now(); + // 5초 이내에 이미 갱신했으면 건너뜀 + if (now - _lastVisibilityRefresh < 5000) return; + _lastVisibilityRefresh = now; + + logInfo('Sidebar became visible, restoring state...'); + void this._sendModels(); + void this._sendBrainProfiles(); + void this._sendAgentsList(); }); webviewView.webview.html = this._getHtml(webviewView.webview); @@ -1950,26 +1956,26 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn private async _sendModels() { if (!this._view) return; + if (this._modelDiscoveryInFlight) { + logInfo('Model discovery already in progress, skipping.'); + return; + } + this._modelDiscoveryInFlight = true; try { const config = getConfig(); const url = config.ollamaUrl; let defaultModel = config.defaultModel; let models: string[] = []; - const primaryEngine = resolveEngine(url); - const engines = primaryEngine === 'lmstudio' ? ['lmstudio', 'ollama'] as const : ['ollama', 'lmstudio'] as const; - - for (const engine of engines) { - const modelsUrl = buildApiUrl(url, engine, 'models'); - try { - logInfo('Model discovery started.', { engine, modelsUrl }); - const res = await fetch(modelsUrl, { signal: AbortSignal.timeout(5000) }); - const rawText = await res.text(); - if (!res.ok) { - logError('Model discovery returned non-OK status.', { engine, modelsUrl, status: res.status, body: summarizeText(rawText, 300) }); - continue; - } - + const engine = resolveEngine(url); // 단일 엔진만 + const modelsUrl = buildApiUrl(url, engine, 'models'); + try { + logInfo('Model discovery started.', { engine, modelsUrl }); + const res = await fetch(modelsUrl, { signal: AbortSignal.timeout(5000) }); + const rawText = await res.text(); + if (!res.ok) { + logError('Model discovery returned non-OK status.', { engine, modelsUrl, status: res.status, body: summarizeText(rawText, 300) }); + } else { const data = rawText ? JSON.parse(rawText) as any : {}; models = engine === 'lmstudio' ? (data.data || []).map((m: any) => m.id) @@ -1977,11 +1983,10 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn if (models.length > 0) { logInfo('Model discovery succeeded.', { engine, count: models.length, modelsPreview: models.slice(0, 5) }); - break; } - } catch (e: any) { - logError('Model discovery failed.', { engine, modelsUrl, error: e?.message || String(e) }); } + } catch (e: any) { + logError('Model discovery failed.', { engine, modelsUrl, error: e?.message || String(e) }); } if (models.length === 0) { @@ -2017,8 +2022,8 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn this._view.webview.postMessage({ type: 'modelsList', value: { models, selected: defaultModel } }); } catch (err) { logError('Model list update failed.', err); - const fallbackModel = getConfig().defaultModel; - this._view.webview.postMessage({ type: 'modelsList', value: { models: fallbackModel ? [fallbackModel] : [], selected: fallbackModel } }); + } finally { + this._modelDiscoveryInFlight = false; } }