fix(extension): resolve AI response dropping for sub-5s executions by relaxing IDLE capture condition #task-607

This commit is contained in:
Variet Worker
2026-04-10 16:05:30 +09:00
parent 6bbc9ddd00
commit 2ece05fc6f
4 changed files with 27 additions and 42 deletions

View File

@@ -212,5 +212,10 @@
### [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) 모두 모킹하여 직접 파싱 확인.
### [2026-04-10] [Extension] Fast Execution `<5s` Response Capture Missed (IDLE-to-IDLE)
- **증상**: 디스코드로 내용이 아예 전달되지 않음. `[RT-CAPTURE]`, `[RESPONSE-CAPTURE]` 로그 모두 전혀 남지 않음.
- **원인**: AI 응답이나 코딩 작업이 5초(폴링 주기) 미만으로 매우 빠르게 끝나면, 확장이 `IDLE -> IDLE` 상태만 관찰하며 `wasRunning` 플래그가 `false`로 유지됨. 기존 `[RESPONSE-CAPTURE]` 조건식(`wasRunning && !isRunning && currentCount > ...`)이 `wasRunning=false`로 인해 블록되어 캡처 자체를 완전히 건너뛰게 됨.
- **해결**: `wasRunning` 검증을 삭제하고 `!isRunning && currentCount > lastResponseCaptureStep` 조건으로 완화하여 누락된 step이 있을 때 무조건 캡처하도록 변경. 추가로 오래된 `[RESPONSE-CAPTURE]` 내 하드코딩 파서를 `extractPlannerText`로 일원화 적용.
- **주의**: 폴링 방식에서는 상태(RUNNING->IDLE) 전이를 확신할 수 없으므로, Step Count(인덱스 전진)라는 100% 신뢰 가능한 마커를 통해 새 응답 여부를 감지해야 함.

View File

@@ -4,4 +4,5 @@
| NNN | HH:MM | 작업 설명 | 커밋해시 | 완료 방면 |
|---|---|---|---|---|
| 001 | 15:53 | Gravity Bridge AI 응답 텍스트가 누락되는 버그 픽스 (extractPlannerText 적용 및 Nested 조회 추가) | TBD | ✅ |
| 001 | 15:53 | Gravity Bridge AI 응답 텍스트가 누락되는 버그 픽스 (extractPlannerText 적용 및 Nested 조회 추가) | TBD | ✅ |
| 002 | 16:05 | Gravity Bridge 빠른 응답 누락 오류 해결 (IDLE-to-IDLE 패스 로직 완화) | TBD | ✅ |

View File

@@ -0,0 +1,13 @@
# Gravity Bridge 빠른 응답(Fast Execution) 누락 오류 해결
- **시간**: 2026-04-10
- **Commit**: TBD
- **Vikunja**: #607 → done
## 문제 원인
- AI 생성이나 응답 작업이 폴링 간격(5초) 미만으로 끝났을 때, 익스텐션의 폴링 루프는 이전과 동일한 `IDLE` 상태만을 보게 됨.
- `lastResponseCaptureStep` 검사는 마련되어 있었으나, `wasRunning` 플래그 제약(`wasRunning && !isRunning`)으로 인하여 IDLE->IDLE 전이를 거치는 모든 단기응답이 `[RESPONSE-CAPTURE]`를 영구히 건너뛰고 통째로 누락됨.
## 해결 방법
- `wasRunning` 방어 조건을 해제하고, `!isRunning && currentCount > lastResponseCaptureStep` 조건으로 완화 (인덱스 전진 기반 감지로 수정).
- 오래된 하드코딩 파서를 버리고 방벽 파서 역할을 하는 `extractPlannerText`로 갈무리 블록의 AI 응답 추출 로직을 단일화하여 적용.

View File

@@ -859,8 +859,8 @@ function setupMonitor() {
}
}
if (wasRunning && !isRunning && currentCount > lastResponseCaptureStep) {
ctx.logToFile(`[RESPONSE-CAPTURE] RUNNING→IDLE, steps=${currentCount}, capturing response...`);
if (!isRunning && currentCount > lastResponseCaptureStep) {
ctx.logToFile(`[RESPONSE-CAPTURE] IDLE check, steps=${currentCount} > last=${lastResponseCaptureStep}, capturing response...`);
lastResponseCaptureStep = currentCount;
try {
const offset = Math.max(0, currentCount - 5);
@@ -875,41 +875,7 @@ function setupMonitor() {
const s = steps[ri];
const sType = s?.type || '';
if (sType.includes('PLANNER_RESPONSE') && !sType.includes('EPHEMERAL')) {
let textContent = '';
// Extract from plannerResponse field
const pr = s?.plannerResponse;
if (pr) {
// Priority: modifiedResponse (confirmed field from AG)
if (pr.modifiedResponse) textContent = pr.modifiedResponse;
else if (pr.rawText) textContent = pr.rawText;
else if (pr.text) textContent = pr.text;
else if (pr.message) textContent = typeof pr.message === 'string' ? pr.message : '';
else if (pr.content?.parts) {
for (const p of pr.content.parts) {
if (p?.text) textContent += p.text;
}
}
// Log first time to capture actual field names
if (!textContent) {
ctx.logToFile(`[RESPONSE-CAPTURE] plannerResponse keys: [${Object.keys(pr).join(',')}] values(100): ${JSON.stringify(pr).substring(0, 200)}`);
}
}
// Extract from ephemeralMessage field
const em = s?.ephemeralMessage;
if (!textContent && em) {
if (typeof em === 'string') textContent = em;
else if (em.message) textContent = em.message;
else if (em.content) textContent = typeof em.content === 'string' ? em.content : JSON.stringify(em.content);
}
// Fallback: metadata, content, rawOutput
if (!textContent) {
const parts = s?.content?.parts || s?.parts || [];
for (const p of parts) {
if (p?.text) textContent += p.text;
}
}
if (!textContent && s?.metadata?.text) textContent = s.metadata.text;
if (!textContent && s?.rawOutput) textContent = typeof s.rawOutput === 'string' ? s.rawOutput : JSON.stringify(s.rawOutput);
const textContent = extractPlannerText(s) || '';
if (textContent.length > 10) {
ctx.logToFile(`[RESPONSE-CAPTURE] found ${sType} (${textContent.length} chars) at step ${offset + ri}`);
const truncated = textContent.length > 3500
@@ -918,7 +884,7 @@ function setupMonitor() {
ctx.writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
break;
} else {
ctx.logToFile(`[RESPONSE-CAPTURE] ${sType} too short (${textContent.length}), keys=[${Object.keys(s).join(',')}]`);
ctx.logToFile(`[RESPONSE-CAPTURE] ${sType} too short (${textContent.length})`);
}
}
}
@@ -930,8 +896,8 @@ function setupMonitor() {
ctx.writeChatSnapshot(`✅ **Step ${currentCount} 작업 종료**`);
}
// ── Diff review detection: if session just went IDLE and files were modified ──
if (wasRunning && !isRunning && pendingModifiedFiles.length > 0) {
// ── Diff review detection: if session went IDLE and files were modified ──
if (!isRunning && pendingModifiedFiles.length > 0) {
// Phase 3 FIX: Filter out brain/ artifact files (task.md, implementation_plan.md etc.)
// These are AG internal artifacts, NOT code changes needing user review.
const brainPathSegment = '.gemini/antigravity/brain/';