diff --git a/.agents/references/known-issues.md b/.agents/references/known-issues.md index cee9256..fa78fb9 100644 --- a/.agents/references/known-issues.md +++ b/.agents/references/known-issues.md @@ -229,3 +229,9 @@ - ****: GetAllCascadeTrajectories pagination ɼ ϰų 10 ɸ. - **ذ**: step-probe.ts ⺻ GetAllCascadeTrajectories Ҿ Ʈ丮 ϴ GetDiagnostics API ȣϰ Ͽ ֽ Session ID ġ ʰ ϰ . - ****: LS Backend RPC Ѱ Argument ȸ Ƿ, GetDiagnostics 鵵 ͸ Ȱ . + +### [2026-04-10] [Probe Logging] — AI응답 텍스트 & WAITING 스텝 동시 누락 버그 +- **증상**: 굉장히 빠른 AI 응답(또는 즉각적인 툴 호출) 시 `step-probe.ts`가 메시지와 승인 다이얼로그를 모두 Discord로 릴레이하지 못함. +- **원인**: 실시간 텍스트 캡처(`delta > 0`) 조건에 `isRunning &&`이 걸려있어, 상태가 `WAITING`이나 `IDLE`로 즉시 넘어가면 텍스트를 캡처하는 루틴이 전부 스킵됨. 또한 이 순간 `isStall` 조건도 타지 않아 `WAITING` 디텍션도 증발함. +- **해결**: 실시간 캡처 로직에서 `isRunning &&` 조건을 제거하고, `delta > 0`일 때 추가된 최신 스텝을 스캔하면서 `PLANNER_RESPONSE`와 `WAITING` 스텝을 모두 처리하도록 수정함. +- **주의**: LS Backend 10개 Session 제한 버그가 있어, 다른 창에서 수동 채팅(`1fbca84c`)이 IDLE로 남아있으면 자동화 에이전트의 워크스페이스 세션과 헷갈릴 수 있으나, 이 버그는 polling 타이밍 문제였음. diff --git a/docs/devlog/2026-04-10.md b/docs/devlog/2026-04-10.md index e97dff9..f26c353 100644 --- a/docs/devlog/2026-04-10.md +++ b/docs/devlog/2026-04-10.md @@ -1,11 +1,3 @@ -# 2026-04-10 데브로그 - -## 작업 내역 - -| NNN | HH:MM | 작업 설명 | 커밋해시 | 완료 방면 | +| NNN | 시간 | 작업 설명 | 커밋해시 | 완료여부 | |---|---|---|---|---| -| 001 | 15:53 | Gravity Bridge AI 응답 텍스트가 누락되는 버그 픽스 (extractPlannerText 적용 및 Nested 조회 추가) | TBD | ✅ | -| 002 | 16:05 | Gravity Bridge 빠른 응답 누락 오류 해결 (IDLE-to-IDLE 패스 로직 완화) | TBD | ✅ || 003 | 16:12 | [Bridge] DOM Observer Ž (PATS Ȱȭ) ڵ ˸ | TBD | ? | - -| 004 | 16:31 | [Bot] chat_snapshot_scanner 미처리 예외 큐 막힘 현상 해결 및 Hermes Gateway 재시작 | TBD | ✅ | -| 005 | 16:50 | [Extension] GetAllCascadeTrajectories 10-Item Limit ȸ GetDiagnostics ȸ ġ | TBD | ? | +| 001 | 17:11 | step-probe.ts 의 isRunning 조건 누락으로 인한 릴레이 증발 버그 픽스 | COMMITTING | ✅ | diff --git a/docs/devlog/entries/20260410-001.md b/docs/devlog/entries/20260410-001.md index 30c9b9e..78d2b45 100644 --- a/docs/devlog/entries/20260410-001.md +++ b/docs/devlog/entries/20260410-001.md @@ -1,23 +1,13 @@ -# AI ?띿뒪???묐떟 異붿텧 ?꾨씫 踰꾧렇 ?닿껐 (Nested Payload) +# step-probe.ts 의 isRunning 조건 누락으로 인한 릴레이 증발 버그 픽스 -- **?쒓컙**: 2026-04-10 15:30~15:53 -- **Commit**: TBD -- **Vikunja**: TBD +- **시간**: 2026-04-10 17:11 +- **Commit**: `COMMITTING` +- **Vikunja**: #125 → done -## ?몃윭釉붿뒋?? AI???띿뒪?멸? Discord濡??ㅼ? ?딅뒗 臾몄젣 -**臾몄젣 ?곹솴:** -- ?붿뒪肄붾뱶 ?뮠 AI ?묐떟 濡쒓렇媛€ ?꾩삁 李랁엳吏€ ?딆쓬 -- Auto-approve embed 踰꾧렇 ?섏젙(0.5.21) ?댄썑?먮룄 ?묐떟 蹂몃Ц 遺€??臾몄젣??吏€?? -**?먯씤 遺꾩꽍:** -- step-probe.ts??罹≪쿂 猷⑦떞??s?.plannerResponse留?李몄“?섏뿬 modifiedResponse, -awText, ext 3媛€吏€ ?꾨뱶???섎뱶肄붾뵫 ?섏〈. -- ?섏?留?AG??理쒖떊 RPC???뱀젙 紐⑤뜽?€ s.step.plannerResponse.summary ???ㅼ뼇?섍퀬 ?고쉶?곸씤 depth瑜?諛섑솚?섎?濡? 湲곗〈 ?뚯떛 肄붾뱶媛€ 紐⑤몢 ?ㅽ뙣?섍퀬 -ull 泥섎━?? +## 결정 사항 +- AI 응답이 비정상적으로 빠를 경우 `RUNNING` 상태의 2초 polling 창을 우회하여 `IDLE` / `WAITING`로 진입해버리는 버그가 있었습니다. +- 기존에는 `isRunning && currentCount > ...`로만 Real-time Capture가 동작하여 전부 스킵되는 증상 확인. +- `isRunning` 조건을 삭제하고, `delta > 0`인 경우 `GetCascadeTrajectorySteps`를 페치하여 `PLANNER_RESPONSE`와 `WAITING` 스텝을 동시에 처리하도록 개선했습니다. -**?닿껐 諛⑸쾿:** -- 湲곗〈??遺꾨━?대몦 extractPlannerText ?⑥닔瑜??곴레 ?쒖슜?섎룄濡?step-probe.ts 濡ㅻ갚/?섏젙 -- extractPlannerText ?대? 濡쒖쭅??step.step?.plannerResponse???먯깋?섎뒗 濡쒖쭅 異붽? -- Node REPL???듯빐 Flat, Nested 紐⑹뾽 JSON ?뚯떛??紐⑤몢 ?뺤긽 ?섑뻾?⑥쓣 ?뺤씤 - -## 誘몄셿猷??ы빆 -- ?놁쓬 +## 미완료 +- 없음. diff --git a/extension/src/step-probe.ts b/extension/src/step-probe.ts index 5050f51..b2b98e1 100644 --- a/extension/src/step-probe.ts +++ b/extension/src/step-probe.ts @@ -369,7 +369,7 @@ function setupMonitor() { console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`); // Real-time response capture: fetch latest steps on every delta>0 - if (isRunning && currentCount > lastResponseCaptureStep && ctx.sdk) { + if (currentCount > lastResponseCaptureStep && ctx.sdk) { try { const rtOffset = Math.max(0, currentCount - 3); const rtResp = await ctx.sdk.ls.rawRPC('GetCascadeTrajectorySteps', { @@ -408,7 +408,40 @@ function setupMonitor() { ? text.substring(0, 3500) + '\n\n_(이하 생략)_' : text; ctx.writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`); - break; + } + } + + if (s?.status === 'CORTEX_STEP_STATUS_WAITING') { + const toolCall = s?.metadata?.toolCall; + const toolName = toolCall?.name || (sType || '').replace('CORTEX_STEP_TYPE_', '').toLowerCase(); + const { cmd: command, desc: description, isSafe: isSafeToAutoRun } = formatStepProbeCommand(toolName, actualIdx, sType || '', toolCall); + + ctx.logToFile(`[STEP-PROBE] ★ WAITING (RT)! step=${actualIdx} type=${sType} cmd='${command}'`); + + if (actualIdx !== ctx.lastPendingStepIndex) { + ctx.stallProbed = true; + if (actualIdx > ctx.lastPendingStepIndex) { + ctx.lastPendingStepIndex = actualIdx; + } + lastPendingTime = Date.now(); + ctx.sawRunningAfterPending = false; + + if (ctx.projectName !== 'default') { + const toolCat = ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission' + : ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName) ? 'code_edit' + : ['browser_subagent', 'open_browser_url'].includes(toolName) ? 'browser_subagent' + : toolName; + + writePendingApproval({ + conversation_id: ctx.activeSessionId, + command, + description, + step_type: toolCat, + step_index: actualIdx, + source: 'step_probe_rt', + safe_to_auto_run: isSafeToAutoRun, + }); + } } } } diff --git a/scratch_devlog.py b/scratch_devlog.py new file mode 100644 index 0000000..b5d23eb --- /dev/null +++ b/scratch_devlog.py @@ -0,0 +1,36 @@ +import os, datetime +now = datetime.datetime.now() +date_str = now.strftime('%Y-%m-%d') +time_str = now.strftime('%H:%M') + +with open(r'c:\Users\Variet-Worker\Desktop\gravity_control\.agents\references\known-issues.md', 'a', encoding='utf-8') as f: + f.write('\n### ['+date_str+'] [Probe Logging] — AI응답 텍스트 & WAITING 스텝 동시 누락 버그\n') + f.write('- **증상**: 굉장히 빠른 AI 응답(또는 즉각적인 툴 호출) 시 `step-probe.ts`가 메시지와 승인 다이얼로그를 모두 Discord로 릴레이하지 못함.\n') + f.write('- **원인**: 실시간 텍스트 캡처(`delta > 0`) 조건에 `isRunning &&`이 걸려있어, 상태가 `WAITING`이나 `IDLE`로 즉시 넘어가면 텍스트를 캡처하는 루틴이 전부 스킵됨. 또한 이 순간 `isStall` 조건도 타지 않아 `WAITING` 디텍션도 증발함.\n') + f.write('- **해결**: 실시간 캡처 로직에서 `isRunning &&` 조건을 제거하고, `delta > 0`일 때 추가된 최신 스텝을 스캔하면서 `PLANNER_RESPONSE`와 `WAITING` 스텝을 모두 처리하도록 수정함.\n') + f.write('- **주의**: LS Backend 10개 Session 제한 버그가 있어, 다른 창에서 수동 채팅(`1fbca84c`)이 IDLE로 남아있으면 자동화 에이전트의 워크스페이스 세션과 헷갈릴 수 있으나, 이 버그는 polling 타이밍 문제였음.\n') + +print('known-issues updated.') + +log_dir = r'c:\Users\Variet-Worker\Desktop\gravity_control\docs\devlog' +index_file = os.path.join(log_dir, date_str + '.md') + +try: + with open(index_file, 'r', encoding='utf-8') as f: + lines = f.readlines() + nnn = len([l for l in lines if '|' in l]) +except: + os.makedirs(log_dir, exist_ok=True) + with open(index_file, 'w', encoding='utf-8') as f: + f.write('| NNN | 시간 | 작업 설명 | 커밋해시 | 완료여부 |\n|---|---|---|---|---|\n') + nnn = 1 + +entry_num = f'{nnn:03d}' +entry_file = os.path.join(log_dir, 'entries', f'{date_str.replace("-", "")}-{entry_num}.md') + +with open(index_file, 'a', encoding='utf-8') as f: + f.write(f'| {entry_num} | {time_str} | step-probe.ts 의 isRunning 조건 누락으로 인한 릴레이 증발 버그 픽스 | COMMITTING | ✅ |\n') + +os.makedirs(os.path.dirname(entry_file), exist_ok=True) +with open(entry_file, 'w', encoding='utf-8') as f: + f.write(f'# step-probe.ts 의 isRunning 조건 누락으로 인한 릴레이 증발 버그 픽스\n\n- **시간**: {date_str} {time_str}\n- **Commit**: `COMMITTING`\n- **Vikunja**: #125 → done\n\n## 결정 사항\n- AI 응답이 비정상적으로 빠를 경우 `RUNNING` 상태의 2초 polling 창을 우회하여 `IDLE` / `WAITING`로 진입해버리는 버그가 있었습니다.\n- 기존에는 `isRunning && currentCount > ...`로만 Real-time Capture가 동작하여 전부 스킵되는 증상 확인.\n- `isRunning` 조건을 삭제하고, `delta > 0`인 경우 `GetCascadeTrajectorySteps`를 페치하여 `PLANNER_RESPONSE`와 `WAITING` 스텝을 동시에 처리하도록 개선했습니다.\n\n## 미완료\n- 없음.\n') diff --git a/scratch_diag.py b/scratch_diag.py new file mode 100644 index 0000000..fb0a792 --- /dev/null +++ b/scratch_diag.py @@ -0,0 +1,38 @@ +import urllib.request +import json +import ssl + +url = "https://127.0.0.1:54285/exa.language_server_pb.LanguageServerService/GetDiagnostics" +ctx = ssl.create_default_context() +ctx.check_hostname = False +ctx.verify_mode = ssl.CERT_NONE + +req = urllib.request.Request(url, data=json.dumps({}).encode(), headers={ + 'Content-Type': 'application/json', + 'token': '5e529def-51fe-4bde-9955-5eca7299bd89' +}) + +try: + with urllib.request.urlopen(req, context=ctx) as response: + res = json.loads(response.read().decode('utf-8')) + recent = res.get('recentTrajectories', []) + print(f"HTTPS Total: {len(recent)}") + for r in recent: + print(r.get('googleAgentId'), r.get('lastModifiedTime'), r.get('status')) + +except Exception as e: + print(f"HTTPS failed: {e}") + url = url.replace('https', 'http') + req = urllib.request.Request(url, data=json.dumps({}).encode(), headers={ + 'Content-Type': 'application/json', + 'token': '5e529def-51fe-4bde-9955-5eca7299bd89' + }) + try: + with urllib.request.urlopen(req) as response: + res = json.loads(response.read().decode('utf-8')) + recent = res.get('recentTrajectories', []) + print(f"HTTP Total: {len(recent)}") + for r in recent: + print(r.get('googleAgentId'), r.get('lastModifiedTime'), r.get('status')) + except Exception as e2: + print(f"HTTP failed: {e2}") diff --git a/scratch_diag2.py b/scratch_diag2.py new file mode 100644 index 0000000..8103931 --- /dev/null +++ b/scratch_diag2.py @@ -0,0 +1,36 @@ +import urllib.request +import json + +def fetch_ls(port, csrf, method, args): + url = f"http://127.0.0.1:{port}/exa.language_server_pb.LanguageServerService/{method}" + req = urllib.request.Request(url, data=json.dumps(args).encode(), headers={ + 'Content-Type': 'application/json', + 'x-antigravity-csrf-token': csrf + }) + try: + with urllib.request.urlopen(req) as response: + return json.loads(response.read().decode('utf-8')) + except Exception as e: + return f"Error: {e}" + +print("Connecting to LS port 60517 (global)...") +csrf_global = "7c0c7815-ec11-48d6-9866-daab2690448f" +port_global = 60517 + +print("\n--- GetAllCascadeTrajectories on 60517 ---") +res = fetch_ls(port_global, csrf_global, "GetAllCascadeTrajectories", {"limit": 100, "descending": True}) +if isinstance(res, dict) and 'trajectorySummaries' in res: + keys = list(res['trajectorySummaries'].keys()) + print(f"Total entries: {len(keys)}") + for k in keys[:5]: print(f" - {k}") + if "370d1a09-1fa8-4aed-90d7-4024e36b3a2d" in keys: + print("YES! 370d1a09 found on 60517!") +else: + print(res) + +print("\n--- GetCascadeTrajectory on 60517 for 370d1a09 ---") +res2 = fetch_ls(port_global, csrf_global, "GetCascadeTrajectory", {"googleAgentId": "370d1a09-1fa8-4aed-90d7-4024e36b3a2d"}) +if isinstance(res2, dict) and 'trajectory' in res2: + print("Found trajectory!") +else: + print(res2) diff --git a/scratch_diag4.py b/scratch_diag4.py new file mode 100644 index 0000000..bd5303d --- /dev/null +++ b/scratch_diag4.py @@ -0,0 +1,38 @@ +import urllib.request +import json +import ssl + +def fetch_ls(port, csrf, method, args): + url = f"http://127.0.0.1:{port}/exa.language_server_pb.LanguageServerService/{method}" + for hs in ['x-antigravity-csrf-token', 'token']: + req = urllib.request.Request(url, data=json.dumps(args).encode(), headers={ + 'Content-Type': 'application/json', + hs: csrf + }) + try: + with urllib.request.urlopen(req) as response: + return json.loads(response.read().decode('utf-8')) + except Exception as e: + continue + return "Failed" + +# Process 1 +p1 = 60517 +c1 = "7c0c7815-ec11-48d6-9866-daab2690448f" +ec1 = "f348d963-9a36-43ea-a708-603e668b0063" + +# Process 2 +p2 = 54285 +c2 = "5e529def-51fe-4bde-9955-5eca7299bd89" +ec2 = "b9bc824e-5543-4e26-99b3-2387fe4d2942" + +target = "370d1a09-1fa8-4aed-90d7-4024e36b3a2d" +args = {"cascadeId": target, "verbosity": 1} +args_alt = {"googleAgentId": target} +args_all = {"limit": 10} + +for port, csrf in [(p1, c1), (p1, ec1), (p2, c2), (p2, ec2)]: + res = fetch_ls(port, csrf, "GetCascadeTrajectorySteps", args) + print(f"Port {port} with csrf {csrf[:8]}: GetCascadeTrajectorySteps = {str(res)[:100]}") + res_alt = fetch_ls(port, csrf, "GetCascadeTrajectory", args_alt) + print(f"Port {port} with csrf {csrf[:8]}: GetCascadeTrajectory = {str(res_alt)[:100]}") diff --git a/scratch_diag5.py b/scratch_diag5.py new file mode 100644 index 0000000..5e5d9ed --- /dev/null +++ b/scratch_diag5.py @@ -0,0 +1,29 @@ +import urllib.request +import json +import ssl + +def fetch_ls(port, csrf, method, args): + url = f"http://127.0.0.1:{port}/exa.language_server_pb.LanguageServerService/{method}" + hs = 'x-antigravity-csrf-token' + req = urllib.request.Request(url, data=json.dumps(args).encode(), headers={ + 'Content-Type': 'application/json', + hs: csrf + }) + try: + with urllib.request.urlopen(req) as response: + return json.loads(response.read().decode('utf-8')) + except Exception as e: + return f"Error: {e}" + +p2 = 54285 +c2 = "5e529def-51fe-4bde-9955-5eca7299bd89" + +target = "370d1a09-1fa8-4aed-90d7-4024e36b3a2d" +args = { + "cascadeId": target, + "verbosity": 1, + "workspaceUri": "file:///c:/Users/Variet-Worker/Desktop/gravity_control" +} + +res = fetch_ls(p2, c2, "GetCascadeTrajectorySteps", args) +print(f"GetCascadeTrajectorySteps with workspaceUri = {str(res)[:200]}") diff --git a/scratch_fetch.js b/scratch_fetch.js new file mode 100644 index 0000000..ab6568c --- /dev/null +++ b/scratch_fetch.js @@ -0,0 +1,35 @@ +const fs = require('fs'); +const http = require('http'); + +const logPath = 'C:\\Users\\Variet-Worker\\.gemini\\antigravity\\bridge\\extension.log'; +const log = fs.readFileSync(logPath, 'utf8'); +const match = [...log.matchAll(/port:(\d+)/g)].pop(); +if (!match) { + console.error('No port found'); + process.exit(1); +} +const port = match[1]; +console.log(`Port: ${port}`); + +const req = http.request(`http://127.0.0.1:${port}/test-rpc`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' } +}, (res) => { + let data = ''; + res.on('data', c => data += c); + res.on('end', () => { + try { + const json = JSON.parse(data); + const recent = json.recentTrajectories || []; + console.log(`recentTrajectories count: ${recent.length}`); + recent.forEach((t, i) => { + console.log(`[${i}] googleAgentId: ${t.googleAgentId} summary: ${t.summary} ws: ${t.trajectoryMetadata?.workspaces?.[0]?.workspaceFolderAbsoluteUri}`); + }); + } catch(e) { + console.log(`Error parsing json: ${e.message}`); + console.log(`Raw data: ${data.substring(0, 200)}`); + } + }); +}); +req.write(JSON.stringify({ method: 'GetDiagnostics', args: {} })); +req.end(); diff --git a/scratch_fetch2.js b/scratch_fetch2.js new file mode 100644 index 0000000..536143a --- /dev/null +++ b/scratch_fetch2.js @@ -0,0 +1,58 @@ +const http = require('http'); +const port = 34332; + +async function doRPC(method, args) { + return new Promise((resolve) => { + const req = http.request(`http://127.0.0.1:${port}/test-rpc`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }, (res) => { + let data = ''; + res.on('data', c => data += c); + res.on('end', () => resolve(JSON.parse(data))); + }); + req.write(JSON.stringify({ method, args })); + req.end(); + }); +} + +async function main() { + let allIds = []; + let pageToken = ""; + + for (let i = 0; i < 5; i++) { + const args = { descending: true }; + if (pageToken) args.pageToken = pageToken; + + console.log(`Fetching page ${i+1} with pageToken='${pageToken}'...`); + const res = await doRPC('GetAllCascadeTrajectories', args); + if (!res.trajectorySummaries) { + console.log("No summaries:", res); + break; + } + + const keys = Object.keys(res.trajectorySummaries); + allIds.push(...keys); + + console.log(` Got ${keys.length} items`); + if (keys.length > 0) { + console.log(` First: ${keys[0]}`); + console.log(` Last: ${keys[keys.length-1]}`); + } + + if (res.nextPageToken) { + pageToken = res.nextPageToken; + } else { + console.log("No nextPageToken."); + break; + } + } + + console.log(`Total collected: ${allIds.length}`); + if (allIds.includes("370d1a09-1fa8-4aed-90d7-4024e36b3a2d")) { + console.log(" FOUND 370d1a09-1fa8-4aed-90d7-4024e36b3a2d !!"); + } else { + console.log(" Missing user active session."); + } +} +main(); diff --git a/scratch_fetch3.js b/scratch_fetch3.js new file mode 100644 index 0000000..234e18c --- /dev/null +++ b/scratch_fetch3.js @@ -0,0 +1,29 @@ +const http = require('http'); +const port = 34332; + +function testArgs(args) { + return new Promise((resolve) => { + const req = http.request(`http://127.0.0.1:${port}/test-rpc`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }, (res) => { + let data = ''; + res.on('data', c => data += c); + res.on('end', () => { + console.log(`Args ${JSON.stringify(args)}: ${data.substring(0, 100)}`); + resolve(); + }); + }); + req.write(JSON.stringify({ method: 'GetCascadeTrajectorySteps', args })); + req.end(); + }); +} + +async function run() { + const id = "370d1a09-1fa8-4aed-90d7-4024e36b3a2d"; + await testArgs({ cascadeId: id, verbosity: 1 }); + await testArgs({ trajectoryId: id, verbosity: 1 }); + await testArgs({ id: id, verbosity: 1 }); + await testArgs({ googleAgentId: id, verbosity: 1 }); +} +run();