diff --git a/.agents/references/known-issues.md b/.agents/references/known-issues.md index 1d9a2a8..75ad1a6 100644 --- a/.agents/references/known-issues.md +++ b/.agents/references/known-issues.md @@ -29,6 +29,15 @@ ## πŸ”΄ Active/Recent Issues +### [2026-03-25] [Architecture] Discord Signal Drop & Extension Freezes +- **증상**: μž₯μ‹œκ°„ μžλ¦¬λΉ„μ›€ ν›„ 볡귀 μ‹œ Discord둜 승인 μ‹ ν˜Έκ°€ μ˜€μ§€ μ•Šκ±°λ‚˜ VS Code UIκ°€ 간헐적/μ§€μ†μ μœΌλ‘œ 멈좀(Freeze). +- **원인**: + 1. `ws.onerror` λ°œμƒ ν›„ `onclose` λˆ„λ½ μ‹œ μž¬μ—°κ²° 콜백 호좜이 이루어지지 μ•Šμ•„ λ¬΄ν•œ λŒ€κΈ° (μž₯μ‹œκ°„ λ§ˆλΉ„) + 2. `ws-client` μž¬μ—°κ²° μ‹œ λˆ„μ λœ 200개 큐λ₯Ό 동기식 burst μ „μ†‘ν•˜μ—¬ Hub의 속도 μ œν•œ(60개/10초)에 κ±Έλ € ν™•μ • 영ꡬ μ‚­μ œλ¨ + 3. 둜컬 λΈŒλ¦Ώμ§€ `http-bridge.ts`의 κ³Όκ±° μœ μ‚°μΈ `FALSE_POSITIVE_RE` μ •κ·œμ‹μ΄ AI 고유 λ²„νŠΌ(Allow, Deny, Accept) λ§ˆμ € ν•„ν„°λ§ν•˜μ—¬ Discord 전솑 μ›μ²œ 차단 + 4. `step-probe.ts` 폴링 루프 λ‚΄ 동기식 파일 I/O μ‚¬μš©μœΌλ‘œ μΈν•œ ν”„λ¦¬μ¦ˆ +- **ν•΄κ²°** (v0.5.10): ws-client에 ν•˜λ“œ νƒ€μž„μ•„μ›ƒ 및 50ms Paced-flush 적용, http-bridge의 μ •κ·œμ‹ κΈ°λŠ₯ μ™„ν™”, step-probe 비동기 I/O μ „ν™˜ 체제 적용, observer-script의 ν•„ν„°λœ μ‹ ν˜Έ λ¬΄ν•œ HTTP 폴링 λ°©μ–΄ μ½”λ“œ 반영. +- **주의**: Extension λ‚΄λΆ€ 둜직 λ²„κ·Έμ˜€μœΌλ―€λ‘œ Hub(Python) μ½”λ“œλŠ” κ±΄λ“œλ¦¬μ§€ μ•ŠμŒ. Hub 속도 μ œν•œμ€ 정상 λ°©μ–΄ κΈ°μ œμ΄λ―€λ‘œ ν΄λΌμ΄μ–ΈνŠΈ λ‹¨μ˜ Pacing이 μ˜¬λ°”λ₯Έ λ°©ν–₯μž„. ### [2026-03-24] DOM Observer /trigger-click λ Œλ”λ§ μˆœμ„œ μ˜€μž‘λ™ 및 False Positive 프리징 - **증상**: v0.5.9 패치 이후 μ½”λ”© μ‹œ Agent 화면이 λŠμž„μ—†μ΄ μ„œλͺ… λŒ€κΈ°(Pending) μƒνƒœλ‘œ 멈좀. λ˜λŠ” λ””μŠ€μ½”λ“œμ—μ„œ `Approve` μ‹œ 에디터 λ‚΄μ˜ μ—‰λš±ν•œ `Run Test`(μ½”λ“œ 렌즈)λ₯Ό 클릭함. - **원인**: ν…μŠ€νŠΈμ™€ μ •κ·œμ‹(`/^Run/i` λ“±)μ—λ§Œ μ˜μ‘΄ν•˜μ—¬ `querySelectorAll`을 μˆ˜ν–‰ν•  경우, DOM νŠΈλ¦¬μ— λ Œλ”λ§λœ μˆ˜λ§Žμ€ VS Code λ„€μ΄ν‹°λΈŒ μ½”λ“œ 렌즈 λ²„νŠΌμ„ Agent λ²„νŠΌλ³΄λ‹€ λ¨Όμ € μ°Ύμ•„λ²„λ¦¬λŠ” λ°œμƒ μœ„μΉ˜(Context)의 ν•œκ³„μ . diff --git a/docs/devlog/2026-03-25.md b/docs/devlog/2026-03-25.md new file mode 100644 index 0000000..05e069e --- /dev/null +++ b/docs/devlog/2026-03-25.md @@ -0,0 +1,5 @@ +# 2026-03-25 Devlog + +| NNN | HH:MM | μž‘μ—… μ„€λͺ… | `μ»€λ°‹ν•΄μ‹œ` | βœ… λ˜λŠ” πŸ”§ | +|-----|-------|----------|-----------|-----------| +| 001 | 07:15 | ws-client reconnect pacing 및 http-bridge μ •κ·œμ‹ ν•„ν„° μ™„ν™”λ‘œ Signal Drop ν•΄κ²° (v0.5.10) | `pending` | βœ… | diff --git a/extension/src/http-bridge.ts b/extension/src/http-bridge.ts index 3833686..6d1d6a3 100644 --- a/extension/src/http-bridge.ts +++ b/extension/src/http-bridge.ts @@ -189,7 +189,8 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) { // ── Server-side false positive filter ── const cmd = (data.command || '').trim(); - const FALSE_POSITIVE_RE = /^(Proceed|Continue|Open|Close|OK|Yes|No|Save|Undo|Redo|Back|Next|More|Less|Got it|Deny|Allow Once|Allow This Conversation|Dismiss|Decline|Accept|Reject|Accept all|Reject all)$/i; + // Removed valid AI buttons (Accept, Reject, Allow, Deny) which are now structurally protected by the observer script + const FALSE_POSITIVE_RE = /^(Proceed|Continue|Open|Close|OK|Yes|No|Save|Undo|Redo|Back|Next|More|Less|Got it|Dismiss)$/i; if (FALSE_POSITIVE_RE.test(cmd)) { ctx.logToFile(`[HTTP] filtered false positive: "${cmd}"`); res.writeHead(200, { 'Content-Type': 'application/json' }); diff --git a/extension/src/observer-script.ts b/extension/src/observer-script.ts index bd29ed7..cf81472 100644 --- a/extension/src/observer-script.ts +++ b/extension/src/observer-script.ts @@ -479,6 +479,12 @@ export function generateApprovalObserverScript(_port: number): string { headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }).then(function(r){return r.json();}).then(function(d){ + if (!d.ok || d.filtered) { + log('Pending rejected/filtered for group ['+buttonsArr2.map(function(x){return x.text;}).join(', ')+']'); + delete _sent[groupKey2]; + for(var di=0;di= 0)); if (isMatch) { pd.status = 'auto_resolved'; - fs.writeFileSync(pfPath, JSON.stringify(pd, null, 2), 'utf-8'); + fs.promises.writeFile(pfPath, JSON.stringify(pd, null, 2), 'utf-8').catch(e => { + ctx.logToFile(`[AUTO-RESOLVE] write error: ${e.message}`); + }); resolvedCount++; const cmd = pd.command || ''; if (cmd.length > primaryCommand.length && cmd !== 'Deny' && !cmd.includes('Allow')) { @@ -989,7 +991,9 @@ export function writePendingApproval(data: { conversation_id: string; command: s if (data.step_type) existing.step_type = data.step_type; if (data.step_index !== undefined) existing.step_index = data.step_index; existing.source = 'dom_observer+step_probe'; // mark as merged - fs.writeFileSync(efPath, JSON.stringify(existing, null, 2), 'utf-8'); + fs.promises.writeFile(efPath, JSON.stringify(existing, null, 2), 'utf-8').catch(e => { + ctx.logToFile(`[DEDUP] merge write error: ${e.message}`); + }); ctx.logToFile(`[DEDUP] MERGED step_probe info into DOM pending: ${ef} cmd="${data.command.substring(0, 60)}"`); // Record in memory dedup if (data.step_index !== undefined && data.conversation_id) { @@ -1071,7 +1075,9 @@ export function writePendingApproval(data: { conversation_id: string; command: s return; } // File route (fallback β€” only when WS is NOT connected) - fs.writeFileSync(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8'); + fs.promises.writeFile(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8').catch(e => { + console.error(`Gravity Bridge: failed to write pending: ${e.message}`); + }); console.log(`Gravity Bridge: pending approval written β†’ ${id}.json`); // Cache diff_review metadata in-memory (survives pending file deletion by Collector/Bot) if (data.step_type === 'diff_review' && (data.edit_step_indices?.length || data.modified_files?.length)) { diff --git a/extension/src/ws-client.ts b/extension/src/ws-client.ts index a5cba89..d8f7d96 100644 --- a/extension/src/ws-client.ts +++ b/extension/src/ws-client.ts @@ -213,12 +213,21 @@ export class WSBridgeClient { this.logFn(`[WS] Connecting to ${this.hubUrl}...`); const ws = new WebSocket(this.hubUrl); + let connectTimeout: NodeJS.Timeout | null = null; + const clearConnectTimeout = () => { + if (connectTimeout) { + clearTimeout(connectTimeout); + connectTimeout = null; + } + }; + // Detect API style: Node.js 'ws' module has .on(), browser WebSocket doesn't const isNodeWs = typeof ws.on === 'function'; if (isNodeWs) { // ─── Node.js ws module (EventEmitter API) ─── ws.on('open', () => { + clearConnectTimeout(); this.logFn('[WS] Connection opened, authenticating...'); this.ws = ws; this.connected = true; @@ -235,11 +244,18 @@ export class WSBridgeClient { }); ws.on('close', (code: number, reason: Buffer) => { + clearConnectTimeout(); const reasonStr = reason ? reason.toString('utf-8') : ''; this.logFn(`[WS] Connection closed: code=${code} reason=${reasonStr}`); this._onDisconnect(); }); + ws.on('error', (err: any) => { + clearConnectTimeout(); + this.logFn(`[WS] Connection error: ${err.message || err}`); + this._onDisconnect(); + }); + ws.on('pong', () => { // Server responded to our ping β€” connection is alive this.lastPongTime = Date.now(); @@ -247,6 +263,7 @@ export class WSBridgeClient { } else { // ─── Browser-style WebSocket API (.onopen / .onmessage) ─── ws.onopen = () => { + clearConnectTimeout(); this.logFn('[WS] Connection opened (browser API), authenticating...'); this.ws = ws; this.connected = true; @@ -264,15 +281,29 @@ export class WSBridgeClient { }; ws.onclose = (event: any) => { + clearConnectTimeout(); this.logFn(`[WS] Connection closed: code=${event.code} reason=${event.reason || ''}`); this._onDisconnect(); }; ws.onerror = (event: any) => { + clearConnectTimeout(); this.logFn(`[WS] Error: ${event.message || 'connection error'}`); + this._onDisconnect(); }; } + // Connection timeout to prevent hanging if no close/error fires + connectTimeout = setTimeout(() => { + this.logFn('[WS] Connection timeout (15s) β€” forcing disconnect'); + if (this.ws) { + try { this.ws.terminate(); } catch { try { this.ws.close(); } catch { } } + } else if (ws) { + try { ws.terminate(); } catch { try { ws.close(); } catch { } } + } + this._onDisconnect(); + }, 15000); + } catch (e: any) { this.logFn(`[WS] Connect failed: ${e.message}`); this._scheduleReconnect(); @@ -448,13 +479,15 @@ export class WSBridgeClient { } } - private _flushQueue(): void { + private async _flushQueue(): Promise { if (this.messageQueue.length === 0) return; - this.logFn(`[WS] Flushing ${this.messageQueue.length} queued messages`); + this.logFn(`[WS] Flushing ${this.messageQueue.length} queued messages (paced)`); const queue = [...this.messageQueue]; this.messageQueue = []; for (const msg of queue) { this._sendRaw(msg); + // Pace the burst to avoid hitting the Hub's rate limit (60 msgs / 10s) + await new Promise(r => setTimeout(r, 50)); } }