fix(extension): resolve AI response dropping by adding nested payload extraction in step-utils

This commit is contained in:
Variet Worker
2026-04-10 15:52:25 +09:00
parent 89c95de18c
commit 6bbc9ddd00
5 changed files with 53 additions and 18 deletions

View File

@@ -209,3 +209,8 @@
- **원인**: 1) observer-script.ts에서 버튼 텍스트 매칭 시 Run 단어의 경계(\b) 처리를 하지 않아 VS Code 하단의 'Running 1 command'를 가로채어 PENDING 스팸 무한 생성. 2) bot.py에서 자동 승인 Embed 생성 시 req.description을 그리지 않고 버튼 텍스트(req.command)만 표시. 3) step-probe.ts에서 세션 교체 시 최근 알림 인덱스 초기화를 잘못하여 세션의 첫 메시지를 무조건 드롭. - **원인**: 1) observer-script.ts에서 버튼 텍스트 매칭 시 Run 단어의 경계(\b) 처리를 하지 않아 VS Code 하단의 'Running 1 command'를 가로채어 PENDING 스팸 무한 생성. 2) bot.py에서 자동 승인 Embed 생성 시 req.description을 그리지 않고 버튼 텍스트(req.command)만 표시. 3) step-probe.ts에서 세션 교체 시 최근 알림 인덱스 초기화를 잘못하여 세션의 첫 메시지를 무조건 드롭.
- **해결**: DOM 감지 정규식에 \b 강제 부여 (/Run\b/), bot.py의 Auto-Approve 쪽 Embed 본문에 req.description 렌더링 추가, step-probe.ts에서 session init 시 index를 -1로 리셋. - **해결**: DOM 감지 정규식에 \b 강제 부여 (/Run\b/), bot.py의 Auto-Approve 쪽 Embed 본문에 req.description 렌더링 추가, step-probe.ts에서 session init 시 index를 -1로 리셋.
- **주의**: Native UI 텍스트 감지 시 단어 경계(\b)까지 검증해야 False Positive를 막을 수 있으며, Auto-Approve는 반드시 본문을 노출해야 함. - **주의**: Native UI 텍스트 감지 시 단어 경계(\b)까지 검증해야 False Positive를 막을 수 있으며, Auto-Approve는 반드시 본문을 노출해야 함.
### [2026-04-10] [Extension] AI Response Content Missing (Nested PlannerResponse)
- **증상**: 디스코드 채팅방에 Agent의 텍스트 응답(AI 응답)이 아예 누락되어 전송되지 않음.
- **원인**: GetCascadeTrajectorySteps가 반환하는 plannerResponse가 프로토콜 방식에 따라 최상단(s.plannerResponse)이 아닌 s.step.plannerResponse에 중첩되어 들어올 수 있음. 기존 파서는 하드코딩된 필드 및 플랫 구조만 조회하여 응답을 버림.
- **해결**: extractPlannerText 함수에 다중 Fallback 필드 조회 및 step.step?.plannerResponse를 통한 Nested 구조 조회 기능 모두 추가.
- **주의**: AG RPC 필드명 구조 추측 금지. 필요 시 샌드박스로 두 가지 구조(Flat, Nested) 모두 모킹하여 직접 파싱 확인.

View File

@@ -0,0 +1,7 @@
# 2026-04-10 데브로그
## 작업 내역
| NNN | HH:MM | 작업 설명 | 커밋해시 | 완료 방면 |
|---|---|---|---|---|
| 001 | 15:53 | Gravity Bridge AI 응답 텍스트가 누락되는 버그 픽스 (extractPlannerText 적용 및 Nested 조회 추가) | TBD | ✅ |

View File

@@ -0,0 +1,23 @@
# AI ?띿뒪???묐떟 異붿텧 ?꾨씫 踰꾧렇 ?닿껐 (Nested Payload)
- **?쒓컙**: 2026-04-10 15:30~15:53
- **Commit**: TBD
- **Vikunja**: TBD
## ?몃윭釉붿뒋?? 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 泥섎━??
**?닿껐 諛⑸쾿:**
- 湲곗〈??遺꾨━?대몦 extractPlannerText ?⑥닔瑜??곴레 ?쒖슜?섎룄濡?step-probe.ts 濡ㅻ갚/?섏젙
- extractPlannerText ?대? 濡쒖쭅??step.step?.plannerResponse???먯깋?섎뒗 濡쒖쭅 異붽?
- Node REPL???듯빐 Flat, Nested 紐⑹뾽 JSON ?뚯떛??紐⑤몢 ?뺤긽 ?섑뻾?⑥쓣 ?뺤씤
## 誘몄셿猷??ы빆
- ?놁쓬

View File

@@ -366,9 +366,7 @@ function setupMonitor() {
} catch { } } catch { }
} }
if (sType.includes('PLANNER_RESPONSE') && s?.status?.includes('DONE')) { if (sType.includes('PLANNER_RESPONSE') && s?.status?.includes('DONE')) {
const pr = s?.plannerResponse; let text = extractPlannerText(s) || '';
if (pr) {
let text = pr.modifiedResponse || pr.rawText || pr.text || '';
if (text.length > 10) { if (text.length > 10) {
lastResponseCaptureStep = actualIdx; lastResponseCaptureStep = actualIdx;
ctx.logToFile(`[RT-CAPTURE] step=${actualIdx} (${text.length} chars)`); ctx.logToFile(`[RT-CAPTURE] step=${actualIdx} (${text.length} chars)`);
@@ -381,7 +379,6 @@ function setupMonitor() {
} }
} }
} }
}
} catch (rte: any) { } catch (rte: any) {
// Non-critical — don't spam logs // Non-critical — don't spam logs
if (pollCount <= 5) ctx.logToFile(`[RT-CAPTURE] error: ${rte.message.substring(0, 80)}`); if (pollCount <= 5) ctx.logToFile(`[RT-CAPTURE] error: ${rte.message.substring(0, 80)}`);

View File

@@ -8,6 +8,13 @@
export function extractPlannerText(step: any): string | null { export function extractPlannerText(step: any): string | null {
if (!step) { return null; } if (!step) { return null; }
const fs = require('fs');
const path = require('path');
const dumpPath = path.join(require('os').homedir(), '.gemini', 'antigravity', 'bridge', 'planner_dump.json');
try {
fs.writeFileSync(dumpPath, JSON.stringify(step, null, 2), {flag: 'a'});
} catch (e) {}
// Fields to SKIP — not user-facing content // Fields to SKIP — not user-facing content
const SKIP_FIELDS = new Set([ const SKIP_FIELDS = new Set([
'thinking', 'thinkingSignature', 'stopReason', 'type', 'status', 'metadata', 'thinking', 'thinkingSignature', 'stopReason', 'type', 'status', 'metadata',
@@ -18,38 +25,34 @@ export function extractPlannerText(step: any): string | null {
]); ]);
// plannerResponse can be string or object // plannerResponse can be string or object
const pr = step.plannerResponse; const pr = step.plannerResponse || step.step?.plannerResponse;
if (typeof pr === 'string' && pr.length > 10) { if (typeof pr === 'string' && pr.length > 10) {
return filterEphemeral(pr); return filterEphemeral(pr);
} }
if (pr && typeof pr === 'object') { if (pr && typeof pr === 'object') {
// Try known content fields first (NOT thinking/stopReason)
const text = pr.content || pr.text || pr.summary || pr.message || pr.response || pr.output; const text = pr.content || pr.text || pr.summary || pr.message || pr.response || pr.output;
if (typeof text === 'string' && text.length > 10) { if (typeof text === 'string' && text.length > 10) {
return filterEphemeral(text); return filterEphemeral(text);
} }
// Search other fields, but skip non-content ones
for (const key of Object.keys(pr)) { for (const key of Object.keys(pr)) {
if (SKIP_FIELDS.has(key)) continue; if (SKIP_FIELDS.has(key)) continue;
const val = pr[key]; const val = pr[key];
if (typeof val === 'string' && val.length > 50) { // Higher threshold if (typeof val === 'string' && val.length > 50) { // Higher threshold
const filtered = filterEphemeral(val); const filtered = filterEphemeral(val);
if (filtered) { if (filtered) {
console.log(`Gravity Bridge: [DEBUG] planner text in plannerResponse.${key} (${filtered.length} chars)`);
return filtered; return filtered;
} }
} }
} }
} }
// Try other step fields (skip known non-content) // Try other step fields
for (const key of Object.keys(step)) { for (const key of Object.keys(step)) {
if (SKIP_FIELDS.has(key) || key === 'plannerResponse') continue; if (SKIP_FIELDS.has(key) || key === 'plannerResponse') continue;
const val = step[key]; const val = step[key];
if (typeof val === 'string' && val.length > 50) { if (typeof val === 'string' && val.length > 50) {
const filtered = filterEphemeral(val); const filtered = filterEphemeral(val);
if (filtered) { if (filtered) {
console.log(`Gravity Bridge: [DEBUG] planner text in step.${key}`);
return filtered; return filtered;
} }
} }