fix(ext): resolve ui freeze and saftoautorun burst send memory leak (v0.5.15)
This commit is contained in:
@@ -29,6 +29,18 @@
|
|||||||
|
|
||||||
## 🔴 Active/Recent Issues
|
## 🔴 Active/Recent Issues
|
||||||
|
|
||||||
|
### [2026-04-08] [VS Code Extension] 동기 프로세스(execSync)로 인한 UI 프리징
|
||||||
|
- **증상**: 익스텐션 활성화 시 `activate()` 내부에서 `detectProjectName()`가 호출되며 VS Code UI 전체가 최대 2초간 완전 멈춤(Freeze).
|
||||||
|
- **원인**: 확장 프로그램 구동 이벤트 루프에서 `cp.execSync('git remote get-url origin', { timeout: 2000 })`라는 무거운 동기 블로킹 연산을 수행.
|
||||||
|
- **해결** (v0.5.15): `.git/config` 설정 파일을 `fs.readFileSync`로 직접 파싱해 1ms 내에 안전하게 URL을 추출하도록 완전히 교체함.
|
||||||
|
- **주의**: VS Code Extension 개발 시 `activate`나 UI 스레드 상에서 `execSync` 등의 동기적 자식 프로세스 생성을 엄격히 금지.
|
||||||
|
|
||||||
|
### [2026-04-08] [Architecture] Discord Burst Rate Limit & 메모리 누수 방어(LRU)
|
||||||
|
- **증상**: `SafeToAutoRun` 구동 시 디스코드 자동 알림을 과거처럼 생명주기 단위 배열 비우기로 관리하면, WS 재연결 시 100여 개의 오래된 스텝이 일시(Burst) 발송되어 Discord Hub 60/10s Rate Limit을 초과, 영구 채널 파괴 위험 발생.
|
||||||
|
- **해결** (v0.5.15): `Set<number>`와 Size 1000 제약, 그리고 `Set.values().next().value`를 활용한 LRU Eviction 캐시로 리팩토링. 수명이 지나도 최근 스텝은 절대 잊지 않아 중복 발송을 원천 차단함.
|
||||||
|
- **주의**: 긴 큐나 상태 데이터를 다룰 때는 `clear()` 방식 대신 크기 제한이 있는 LRU 방식으로 안전망을 구축할 것.
|
||||||
|
|
||||||
|
|
||||||
### [2026-03-31] [step-probe] GetAllCascadeTrajectories 10-Item Hard Limit (Signal Drop)
|
### [2026-03-31] [step-probe] GetAllCascadeTrajectories 10-Item Hard Limit (Signal Drop)
|
||||||
- **증상**: `guitar_score` 등에서 활성화된 세션의 디스코드 승인 신호를 "계속해서" 잡지 못함. (WS 60초 타임아웃보다 더 치명적으로 신호가 아예 가지 않음)
|
- **증상**: `guitar_score` 등에서 활성화된 세션의 디스코드 승인 신호를 "계속해서" 잡지 못함. (WS 60초 타임아웃보다 더 치명적으로 신호가 아예 가지 않음)
|
||||||
- **원인**: Extension이 활성 세션을 찾기 위해 호출하는 `GetAllCascadeTrajectories` LS API가 `{}`(빈 인자)로 호출될 때, 기본적으로 **10개의 세션만 반환하는 하드 리밋(Pagination Limit)**이 걸려있음. 이로 인해 작업 내역이 누적되면 수많은 최신/진행 중 세션들이 10개 목록에서 밀려나 누락됨. 익스텐션은 세션이 없다고 판단해 강제로 `IDLE` 모드에 진입하며, 승인 대기열(WAITING) 자체를 검사하지 않게 됨.
|
- **원인**: Extension이 활성 세션을 찾기 위해 호출하는 `GetAllCascadeTrajectories` LS API가 `{}`(빈 인자)로 호출될 때, 기본적으로 **10개의 세션만 반환하는 하드 리밋(Pagination Limit)**이 걸려있음. 이로 인해 작업 내역이 누적되면 수많은 최신/진행 중 세션들이 10개 목록에서 밀려나 누락됨. 익스텐션은 세션이 없다고 판단해 강제로 `IDLE` 모드에 진입하며, 승인 대기열(WAITING) 자체를 검사하지 않게 됨.
|
||||||
|
|||||||
4
extension/package-lock.json
generated
4
extension/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "gravity-bridge",
|
"name": "gravity-bridge",
|
||||||
"version": "0.5.4",
|
"version": "0.5.15",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "gravity-bridge",
|
"name": "gravity-bridge",
|
||||||
"version": "0.5.4",
|
"version": "0.5.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^8.19.0"
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "gravity-bridge",
|
"name": "gravity-bridge",
|
||||||
"displayName": "Gravity Bridge",
|
"displayName": "Gravity Bridge",
|
||||||
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
||||||
"version": "0.5.14",
|
"version": "0.5.15",
|
||||||
"publisher": "variet",
|
"publisher": "variet",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.100.0"
|
"vscode": "^1.100.0"
|
||||||
@@ -85,4 +85,4 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^8.19.0"
|
"ws": "^8.19.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,15 +70,17 @@ function detectProjectName(): string {
|
|||||||
if (folders && folders.length > 0) {
|
if (folders && folders.length > 0) {
|
||||||
const cwd = folders[0].uri.fsPath;
|
const cwd = folders[0].uri.fsPath;
|
||||||
try {
|
try {
|
||||||
const remoteUrl = cp.execSync('git remote get-url origin', {
|
const gitConfig = path.join(cwd, '.git', 'config');
|
||||||
cwd, encoding: 'utf-8', timeout: 2000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore']
|
if (fs.existsSync(gitConfig)) {
|
||||||
}).toString().trim();
|
const cfg = fs.readFileSync(gitConfig, 'utf-8');
|
||||||
const match = remoteUrl.match(/\/([^\/]+?)(?:\.git)?$/);
|
const match = cfg.match(/url\s*=\s*(.*?)\n/);
|
||||||
if (match && match[1]) {
|
if (match && match[1]) {
|
||||||
return match[1].toLowerCase().replace(/[\s\-]+/g, '_');
|
const repo = match[1].trim().match(/\/([^\/]+?)(?:\.git)?$/);
|
||||||
|
if (repo && repo[1]) return repo[1].toLowerCase().replace(/[\s]+/g, '_');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
return path.basename(cwd).toLowerCase().replace(/[\s\-]+/g, '_');
|
return path.basename(cwd).toLowerCase().replace(/[\s]+/g, '_');
|
||||||
}
|
}
|
||||||
return 'default';
|
return 'default';
|
||||||
}
|
}
|
||||||
@@ -219,7 +221,7 @@ export async function fixLSConnection(): Promise<boolean> {
|
|||||||
// 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(/[^a-z0-9]/gi, '').toLowerCase();
|
||||||
|
|
||||||
if (!hint) { logToFile('[LS-FIX] skipped: empty hint'); return false; }
|
if (!hint) { logToFile('[LS-FIX] skipped: empty hint'); return false; }
|
||||||
|
|
||||||
@@ -255,7 +257,7 @@ export async function fixLSConnection(): Promise<boolean> {
|
|||||||
// Match workspace_id arg against our hint
|
// Match workspace_id arg against our hint
|
||||||
const wsMatch = line.match(/--workspace_id[= ](\S+)/i);
|
const wsMatch = line.match(/--workspace_id[= ](\S+)/i);
|
||||||
if (wsMatch) {
|
if (wsMatch) {
|
||||||
const wsid = wsMatch[1].toLowerCase();
|
const wsid = wsMatch[1].replace(/[^a-z0-9]/gi, '').toLowerCase();
|
||||||
if (wsid.includes(hint)) {
|
if (wsid.includes(hint)) {
|
||||||
matchedLine = line;
|
matchedLine = line;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const PENDING_MEMORY_TTL_MS = 60_000;
|
|||||||
|
|
||||||
// generateApprovalObserverScript → extracted to ./observer-script.ts
|
// generateApprovalObserverScript → extracted to ./observer-script.ts
|
||||||
const lastSnapshotText = new Map<string, string>();
|
const lastSnapshotText = new Map<string, string>();
|
||||||
|
const autoRunSteps = new Set<number>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current approval context for WS response routing.
|
* Get current approval context for WS response routing.
|
||||||
@@ -333,6 +334,19 @@ function setupMonitor() {
|
|||||||
ctx.logToFile(`[DIFF-TRACK] + ${bn} (step ${actualIdx})`);
|
ctx.logToFile(`[DIFF-TRACK] + ${bn} (step ${actualIdx})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const safeToAutoRun = tcArgs.SafeToAutoRun === true || tcArgs.safeToAutoRun === true;
|
||||||
|
if (safeToAutoRun && !autoRunSteps.has(actualIdx)) {
|
||||||
|
if (autoRunSteps.size > 1000) {
|
||||||
|
const oldest = autoRunSteps.values().next().value;
|
||||||
|
if (oldest !== undefined) autoRunSteps.delete(oldest);
|
||||||
|
}
|
||||||
|
autoRunSteps.add(actualIdx);
|
||||||
|
const cmdText = tcArgs.CommandLine || tcArgs.command || tcArgs.Command || JSON.stringify(tcArgs);
|
||||||
|
const truncatedCmd = cmdText.length > 500 ? cmdText.substring(0, 500) + '...' : cmdText;
|
||||||
|
ctx.logToFile(`[AUTO-RUN] step=${actualIdx} captured`);
|
||||||
|
ctx.writeChatSnapshot(`🤖 **[Background Execution]**\n\n\`\`\`bash\n${truncatedCmd}\n\`\`\``);
|
||||||
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
if (sType.includes('PLANNER_RESPONSE') && s?.status?.includes('DONE')) {
|
if (sType.includes('PLANNER_RESPONSE') && s?.status?.includes('DONE')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user