From 13f13ee243ba50768d8389509f77f03d32989d58 Mon Sep 17 00:00:00 2001 From: Variet Worker Date: Wed, 1 Apr 2026 18:21:51 +0900 Subject: [PATCH] fix(extension): resolve 10-item limit truncation & WS zombie disconnection (v0.5.14) --- .agents/references/known-issues.md | 14 ++++++++ docs/devlog/2026-04-01.md | 5 +++ docs/devlog/entries/20260401-001.md | 11 +++++++ extension/package.json | 2 +- extension/src/step-probe.ts | 3 +- extension/src/ws-client.ts | 12 ++++++- hub.py | 3 +- install_vsix.py | 20 ++++++++++++ test_rpc.js | 31 ++++++++++++++++++ test_ws_logic.js | 50 +++++++++++++++++++++++++++++ 10 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 docs/devlog/2026-04-01.md create mode 100644 docs/devlog/entries/20260401-001.md create mode 100644 install_vsix.py create mode 100644 test_rpc.js create mode 100644 test_ws_logic.js diff --git a/.agents/references/known-issues.md b/.agents/references/known-issues.md index c343f77..f993a22 100644 --- a/.agents/references/known-issues.md +++ b/.agents/references/known-issues.md @@ -29,6 +29,20 @@ ## ๐Ÿ”ด Active/Recent Issues +### [2026-03-31] [step-probe] GetAllCascadeTrajectories 10-Item Hard Limit (Signal Drop) +- **์ฆ์ƒ**: `guitar_score` ๋“ฑ์—์„œ ํ™œ์„ฑํ™”๋œ ์„ธ์…˜์˜ ๋””์Šค์ฝ”๋“œ ์Šน์ธ ์‹ ํ˜ธ๋ฅผ "๊ณ„์†ํ•ด์„œ" ์žก์ง€ ๋ชปํ•จ. (WS 60์ดˆ ํƒ€์ž„์•„์›ƒ๋ณด๋‹ค ๋” ์น˜๋ช…์ ์œผ๋กœ ์‹ ํ˜ธ๊ฐ€ ์•„์˜ˆ ๊ฐ€์ง€ ์•Š์Œ) +- **์›์ธ**: Extension์ด ํ™œ์„ฑ ์„ธ์…˜์„ ์ฐพ๊ธฐ ์œ„ํ•ด ํ˜ธ์ถœํ•˜๋Š” `GetAllCascadeTrajectories` LS API๊ฐ€ `{}`(๋นˆ ์ธ์ž)๋กœ ํ˜ธ์ถœ๋  ๋•Œ, ๊ธฐ๋ณธ์ ์œผ๋กœ **10๊ฐœ์˜ ์„ธ์…˜๋งŒ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•˜๋“œ ๋ฆฌ๋ฐ‹(Pagination Limit)**์ด ๊ฑธ๋ ค์žˆ์Œ. ์ด๋กœ ์ธํ•ด ์ž‘์—… ๋‚ด์—ญ์ด ๋ˆ„์ ๋˜๋ฉด ์ˆ˜๋งŽ์€ ์ตœ์‹ /์ง„ํ–‰ ์ค‘ ์„ธ์…˜๋“ค์ด 10๊ฐœ ๋ชฉ๋ก์—์„œ ๋ฐ€๋ ค๋‚˜ ๋ˆ„๋ฝ๋จ. ์ต์Šคํ…์…˜์€ ์„ธ์…˜์ด ์—†๋‹ค๊ณ  ํŒ๋‹จํ•ด ๊ฐ•์ œ๋กœ `IDLE` ๋ชจ๋“œ์— ์ง„์ž…ํ•˜๋ฉฐ, ์Šน์ธ ๋Œ€๊ธฐ์—ด(WAITING) ์ž์ฒด๋ฅผ ๊ฒ€์‚ฌํ•˜์ง€ ์•Š๊ฒŒ ๋จ. +- **ํ•ด๊ฒฐ** (v0.5.14): `v0.5.13`์—์„œ ๋„์ž…ํ–ˆ๋˜ `{ limit: 100 }`์ด LS ๋‹จ์˜ ์ฟผ๋ฆฌ ๊ณผ๋ถ€ํ•˜๋กœ ์ธํ•œ VS Code UI ํ”„๋ฆฌ์ง•(DoS)์„ ์œ ๋ฐœํ•˜์—ฌ ๋กค๋ฐฑํ•˜๋Š” ์ค‘ ํ•„์ˆ˜ ์ •๋ ฌ ํŒŒ๋ผ๋ฏธํ„ฐ(`descending: true`)๊นŒ์ง€ ์†Œ์‹ค๋˜์—ˆ๋˜ ์‹ค์ˆ˜๋ฅผ ๊ต์ •ํ•จ. ์ตœ์ข…์ ์œผ๋กœ `{ limit: 30, descending: true }`๋ฅผ ์ ์šฉํ•˜์—ฌ ํŒŒ์‹ฑ ๋ถ€ํ•˜ ์ตœ์†Œํ™” ๋ฐ ์ตœ์‹  ์„ธ์…˜ ์ตœ์ƒ๋‹จ(Index 0) ์กฐํšŒ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ตฌํ˜„ํ•จ. +- **์ฃผ์˜**: LS์˜ ๊ธฐ๋ณธ SQLite/DB ์‘๋‹ต Limit ๊ทœ์น™์— ์˜์กดํ•˜์—ฌ ์ „์ฒด ๋ฐ์ดํ„ฐ ์Šค์บ”์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋กœ์ง์€ ์–ธ์ œ๋“  Truncation ์ด์Šˆ(Data Loss)๋ฅผ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ์Œ. + +### [2026-03-31] [WS] Browser API Fallback 60s Timeout (Zombie Connection) +- **์ฆ์ƒ**: `guitar_score` ๋“ฑ ๋ชจ๋“  ์ž‘์—… ํ™˜๊ฒฝ์—์„œ ์•ฝ 60์ดˆ๋งˆ๋‹ค WebSocket ์—ฐ๊ฒฐ์ด ๋Š๊ธฐ๊ณ  ์žฌ์—ฐ๊ฒฐ๋˜๋Š” ํ˜„์ƒ์ด ๋ฐ˜๋ณต๋˜๋ฉฐ(extension.log์— `Heartbeat timeout` ๊ณ„์† ์ถœ๋ ฅ), ๊ทธ ์‚ฌ์ด ๋””์Šค์ฝ”๋“œ ์Šน์ธ ์‹ ํ˜ธ๋ฅผ ๋†“์นจ. +- **์›์ธ**: Extension์ด `ws` ๋ชจ๋“ˆ ๋กœ๋“œ ์‹คํŒจ(VS Code ํ™˜๊ฒฝ ๋“ฑ)๋กœ ์ธํ•ด ๋ธŒ๋ผ์šฐ์ € ๋‚ด์žฅ `WebSocket` ๊ฐ์ฒด๋กœ Fallback ๋จ. ๋ธŒ๋ผ์šฐ์ € WS๋Š” ์„œ๋ฒ„์˜ ๋„ค์ดํ‹ฐ๋ธŒ ping์„ ๋ฐ›์•„ pong์„ ์ž๋™ ์‘๋‹ตํ•˜์ง€๋งŒ JS์— ์ด๋ฒคํŠธ๋ฅผ ๋…ธ์ถœํ•˜์ง€ ์•Š์Œ. ์ด๋กœ ์ธํ•ด `lastPongTime` ๊ฐฑ์‹ ์ด ๋ถˆ๊ฐ€๋Šฅํ•ด์ ธ, `Date.now() - lastPongTime > 60000` ์กฐ๊ฑด์ด ๋ฌด์กฐ๊ฑด ํ†ต๊ณผ๋˜์–ด ๋ฉ€์ฉกํ•œ ์—ฐ๊ฒฐ์„ ๊ฐ•์ œ ์ข…๋ฃŒํ•จ (False Positive). +- **ํ•ด๊ฒฐ** (v0.5.12): + 1. `hub.py`: `{"type": "heartbeat"}` JSON ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  ์‹œ ๋ช…์‹œ์ ์œผ๋กœ `{"type": "pong"}` JSON์„ ์‘๋‹ตํ•˜๋„๋ก ์ˆ˜์ •. + 2. `ws-client.ts`: ๋ช…์‹œ์  `pong` ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€. JSON pong ์ง€์› ์„œ๋ฒ„๊ฑฐ๋‚˜ Node.js ws๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋งŒ 60์ดˆ ํƒ€์ž„์•„์›ƒ ๊ฒ€์ฆ์„ ๊ฑฐ์น˜๋„๋ก ์กฐ๊ฑด ๋ณด๊ฐ• (`forceHeartbeatTimeoutIfNoPong`). +- **์ฃผ์˜**: ๋ธŒ๋ผ์šฐ์ € ํ‘œ์ค€ WebSockets(W3C)๋Š” ping/pong ์ œ์–ด ํ”„๋ ˆ์ž„์„ JS๋กœ ๋…ธ์ถœํ•˜์ง€ ์•Š์Œ. ํด๋ฆฌํ•„/ํฌ๋กœ์Šคํ”Œ๋žซํผ WS ๋ž˜ํผ ์‚ฌ์šฉ ์‹œ ํ•˜ํŠธ๋น„ํŠธ๋Š” ๋ฐ˜๋“œ์‹œ JSON ๋ฉ”์„ธ์ง€ ํ˜•ํƒœ์˜ Application Layer Ping/Pong์œผ๋กœ ํ’€์–ด๋‚ด๊ฑฐ๋‚˜, Native WS API ์—ฌ๋ถ€๋ฅผ ํ™•์‹คํžˆ ์ฒดํฌํ•ด์•ผ ํ•จ. + ### [2026-03-28] [step-probe] GetCascadeTrajectorySteps UTF-8 ์—๋Ÿฌ ๋ฌดํ•œ ๋ฃจํ”„ - **์ฆ์ƒ**: `guitar_score` ํ”„๋กœ์ ํŠธ์—์„œ `[STEP-PROBE] error: ...invalid UTF-8` ์—๋Ÿฌ๊ฐ€ 5์ดˆ๋งˆ๋‹ค ๋ฐ˜๋ณต๋˜๋ฉฐ Discord ์Šน์ธ ์‹ ํ˜ธ๊ฐ€ ์ „๋‹ฌ๋˜์ง€ ์•Š์Œ. - **์›์ธ**: AG LS ์„œ๋ฒ„์—์„œ ํŠน์ • step์˜ `CortexStepEphemeralMessage.content`์— ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ(์ด๋ฏธ์ง€ ๋“ฑ) ํฌํ•จ โ†’ proto UTF-8 ์ง๋ ฌํ™” 500 ์—๋Ÿฌ. `catch(e)` ๋ธ”๋ก์—์„œ `stallProbed=true`๋ฅผ ์„ค์ •ํ•˜์ง€ ์•Š์•„ `!ctx.stallProbed` ์กฐ๊ฑด์ด ํ•ญ์ƒ true โ†’ 5์ดˆ๋งˆ๋‹ค ๋™์ผ ์š”์ฒญ ๋ฌดํ•œ ์žฌ์‹œ๋„. diff --git a/docs/devlog/2026-04-01.md b/docs/devlog/2026-04-01.md new file mode 100644 index 0000000..6b086b4 --- /dev/null +++ b/docs/devlog/2026-04-01.md @@ -0,0 +1,5 @@ +# 2026-04-01 Devlog + +| NNN | HH:MM | ์ž‘์—… ์„ค๋ช… | `์ปค๋ฐ‹ํ•ด์‹œ` | โœ… ๋˜๋Š” ๐Ÿ”ง | +|-------|-------|-----------|-------------|--------------| +| 001 | 18:22 | `step-probe` 10-Item Truncation/DoS ์šฐํšŒ (vsix v0.5.14) | `TBD` | โœ… | diff --git a/docs/devlog/entries/20260401-001.md b/docs/devlog/entries/20260401-001.md new file mode 100644 index 0000000..3c6e09e --- /dev/null +++ b/docs/devlog/entries/20260401-001.md @@ -0,0 +1,11 @@ +# step-probe Pagination 10-Item Truncation vs LS DoS ์˜ค๋ฅ˜ ์ˆ˜์ • + +- **์‹œ๊ฐ„**: 2026-04-01 13:00~18:22 +- **Commit**: `TBD` +- **Vikunja**: #N/A (์ž„์‹œ ๋ฒ„๊ทธ ํ”ฝ์Šค) + +## ๊ฒฐ์ • ์‚ฌํ•ญ +- ๊ธฐ์กด `v0.5.13`์—์„œ `limit: 100`์œผ๋กœ Pagination Limit(๊ธฐ๋ณธ 10๊ฐœ)์„ ์šฐํšŒํ•˜๋ ค ํ–ˆ์œผ๋‚˜, LS DB ์Šค์บ” ๋ฐ ๊ฑฐ๋Œ€ํ•œ JSON ํŒŒ์‹ฑ์ด VS Code Event Loop ๋ธ”๋กœํ‚น์„ ์œ ๋ฐœํ•˜์—ฌ UI ๋ฉˆ์ถค(DoS) ๋ฐœ์ƒ. +- ๋กค๋ฐฑ ๊ณผ์ •์—์„œ `{}`(์ธ์ž ์—†์Œ)์œผ๋กœ ์›๋ณตํ•˜๋ฉด์„œ ํ•„์ˆ˜์ ์ธ `descending: true` ํŒŒ๋ผ๋ฏธํ„ฐ๊นŒ์ง€ ๋ˆ„๋ฝ๋จ. +- ์ด๋กœ ์ธํ•ด `guitar_score` ๋“ฑ์˜ ์ตœ์‹  ์ž‘์„ฑ ์„ธ์…˜์ด LS ์กฐํšŒ ๋ฆฌ๋ฐ‹(10)์—์„œ ๋ฐ€๋ ค๋‚˜ ์Šน์ธ ์‹ ํ˜ธ๋ฅผ ์ˆ˜์‹ ํ•˜์ง€ ๋ชปํ•˜๋Š” ์ด์Šˆ ์žฌ๋ฐœ. +- ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด `limit: 30, descending: true`๋กœ ์„ค์ •. ํŒŒ์‹ฑํ•ด์•ผ ํ•  JSON ๊ฐ์ฒด ์ˆ˜๋ฅผ 1/3๋กœ ์ค„์ž„๊ณผ ๋™์‹œ์—, ์ •๋ ฌ ๋ณด์žฅ์„ ํ†ตํ•ด ์ตœ๊ทผ 10์ดˆ ์ด๋‚ด์— ํ™œ์„ฑํ™”๋œ ์„ธ์…˜์€ ์–ธ์ œ๋‚˜ Index 0๋ฒˆ ์ตœ์ƒ๋‹จ์— ๊ณ ์ •๋˜๊ฒŒ๋” ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์ˆ˜์ •ํ•จ. diff --git a/extension/package.json b/extension/package.json index 0145fbb..35fdce5 100644 --- a/extension/package.json +++ b/extension/package.json @@ -2,7 +2,7 @@ "name": "gravity-bridge", "displayName": "Gravity Bridge", "description": "Antigravity โ†” Discord ๋ธŒ๋ฆฌ์ง€ ์—ฐ๋™ ํ™•์žฅ", - "version": "0.5.11", + "version": "0.5.14", "publisher": "variet", "engines": { "vscode": "^1.100.0" diff --git a/extension/src/step-probe.ts b/extension/src/step-probe.ts index 7fa66f5..afc8ed7 100644 --- a/extension/src/step-probe.ts +++ b/extension/src/step-probe.ts @@ -178,7 +178,8 @@ function setupMonitor() { ctx.logToFile(`[POLL#${pollCount}] alive`); } try { - const allTraj = await ctx.sdk.ls.rawRPC('GetAllCascadeTrajectories', {}); + // Fix (v0.5.14): Reverted 100-limit DoS but restored descending: true with a safe limit of 30 + const allTraj = await ctx.sdk.ls.rawRPC('GetAllCascadeTrajectories', { limit: 30, descending: true }); if (!allTraj?.trajectorySummaries) { if (pollCount <= 3) ctx.logToFile('[POLL] no trajectorySummaries'); return; diff --git a/extension/src/ws-client.ts b/extension/src/ws-client.ts index d8f7d96..9907e50 100644 --- a/extension/src/ws-client.ts +++ b/extension/src/ws-client.ts @@ -124,6 +124,7 @@ export class WSBridgeClient { private heartbeatTimer: NodeJS.Timeout | null = null; private authTimer: NodeJS.Timeout | null = null; private lastPongTime: number = 0; + private forceHeartbeatTimeoutIfNoPong = false; // Message queue (survives reconnection) private messageQueue: WSMessage[] = []; @@ -440,6 +441,14 @@ export class WSBridgeClient { break; } + case 'pong': { + // Sent by Hub in response to our 'heartbeat' JSON message + // This is crucial for Browser-style WebSockets that don't expose native ping/pong + this.forceHeartbeatTimeoutIfNoPong = true; + this.lastPongTime = Date.now(); + break; + } + default: this.logFn(`[WS] Unknown message type: ${msg.type}`); } @@ -498,7 +507,8 @@ export class WSBridgeClient { this.heartbeatTimer = setInterval(() => { if (this.ws && this.connected) { // Check for zombie connection (no pong for 60s) - if (Date.now() - this.lastPongTime > 60000) { + const isNodeWs = (typeof this.ws.ping === 'function'); + if ((isNodeWs || this.forceHeartbeatTimeoutIfNoPong) && Date.now() - this.lastPongTime > 60000) { this.logFn('[WS] Heartbeat timeout โ€” no pong received for 60s (zombie connection), terminating'); if (this.ws) { try { this.ws.terminate(); } catch { try { this.ws.close(); } catch { } } diff --git a/hub.py b/hub.py index e95bff2..3cafcc0 100644 --- a/hub.py +++ b/hub.py @@ -590,7 +590,8 @@ class WSHub: await self._on_brain_event(conn.project, payload) elif msg_type == MsgType.HEARTBEAT: - pass # last_heartbeat already updated above + # Echo back a "pong" so clients without native ping/pong can update their timers + await conn.ws.send_json({"type": "pong"}) else: logger.warning(f"[HUB] Unknown message type: {msg_type} from {conn.conn_id}") diff --git a/install_vsix.py b/install_vsix.py new file mode 100644 index 0000000..260ebe4 --- /dev/null +++ b/install_vsix.py @@ -0,0 +1,20 @@ +import zipfile, shutil, os + +vsix = r"c:\Users\Variet-Worker\Desktop\gravity_control\extension\gravity-bridge-0.5.14.vsix" +dest = os.path.expanduser(r"~\.antigravity\extensions\variet.gravity-bridge-0.5.14") +tmp = r"C:\tmp\vsix_extract" + +if os.path.exists(tmp): + shutil.rmtree(tmp) +os.makedirs(tmp, exist_ok=True) + +with zipfile.ZipFile(vsix, 'r') as z: + z.extractall(tmp) + +src = os.path.join(tmp, "extension") +if os.path.exists(dest): + shutil.rmtree(dest) + +shutil.copytree(src, dest) +print(f"Installed to {dest}") +print("Files:", os.listdir(dest)) diff --git a/test_rpc.js b/test_rpc.js new file mode 100644 index 0000000..b10bf73 --- /dev/null +++ b/test_rpc.js @@ -0,0 +1,31 @@ +const { LSBridge } = require('./extension/out/sdk/ls-bridge'); + +async function test() { + const ls = new LSBridge(); + await ls.connect(); + + console.log("Testing { limit: 5, descending: true }..."); + let start = Date.now(); + const res = await ls._rpc('GetAllCascadeTrajectories', { limit: 5, descending: true }); + let duration = Date.now() - start; + + const summaries = res.trajectorySummaries || {}; + const keys = Object.keys(summaries); + console.log(`Execution time: ${duration}ms`); + console.log(`Returned entries: ${keys.length}`); + + keys.slice(0, 5).forEach((k, idx) => { + const modT = summaries[k].lastModifiedTime || summaries[k].lastModifiedTimestamp || 'UNKNOWN'; + console.log(`[${idx}] id=${k.substring(0,8)} mod=${modT} status=${summaries[k].status}`); + }); + + console.log("\nTesting { limit: 100, descending: true }..."); + start = Date.now(); + const res100 = await ls._rpc('GetAllCascadeTrajectories', { limit: 100, descending: true }); + duration = Date.now() - start; + console.log(`Execution time: ${duration}ms`); + console.log(`Returned entries: ${Object.keys(res100.trajectorySummaries || {}).length}`); + + ls.disconnect(); +} +test(); diff --git a/test_ws_logic.js b/test_ws_logic.js new file mode 100644 index 0000000..230c085 --- /dev/null +++ b/test_ws_logic.js @@ -0,0 +1,50 @@ +// test_ws_logic.js +class FakeWS { + constructor() { + this.msgLog = []; + this.terminated = false; + } + send(msg) { + this.msgLog.push(msg); + } + terminate() { + this.terminated = true; + } + close() { + this.terminated = true; + } +} + +// SIMULATE _startHeartbeat() logic from ws-client.ts v0.5.12 +function testLogic(isNodeWs, serverSendsPong) { + let ws = new FakeWS(); + let connected = true; + let lastPongTime = Date.now(); + let forceHeartbeatTimeoutIfNoPong = serverSendsPong; + let checkCounter = 0; + + // Fast forward 61 seconds in time + let timeElapsed = 61000; + let currentNow = Date.now() + timeElapsed; + + // Simulate heartbeat timeout logic + let conditionMet = false; + if ((isNodeWs || forceHeartbeatTimeoutIfNoPong) && currentNow - lastPongTime > 60000) { + conditionMet = true; + ws.terminate(); + } + + return { + conditionMet: conditionMet, + terminated: ws.terminated + }; +} + +console.log("Scenario 1: Node WS (native ping/pong) MUST enforce 60s timeout:"); +console.log(testLogic(true, false)); // expect true, true + +console.log("\nScenario 2: Browser WS (fallback) + NO JSON PONG FROM SERVER MUST NOT enforce 60s timeout:"); +console.log(testLogic(false, false)); // expect false, false (PREVENTS FALSE POSITIVE) + +console.log("\nScenario 3: Browser WS (fallback) + JSON PONG FROM SERVER MUST enforce 60s timeout:"); +console.log(testLogic(false, true)); // expect true, true (DETECTS ZOMBIE)