fix(observer/bridge): v13 _promptOnlySkipped fallback guard + generic button no-context filter (v0.5.46) #task-619
This commit is contained in:
@@ -21,6 +21,12 @@
|
|||||||
> 鍮꾩듂븳 臾몄젣媛 옱諛쒗븯硫 archive뿉꽌 寃깋븯꽭슂.
|
> 鍮꾩듂븳 臾몄젣媛 옱諛쒗븯硫 archive뿉꽌 寃깋븯꽭슂.
|
||||||
|
|
||||||
|
|
||||||
|
### [2026-04-15] [Extension] Observer fallback 컨텍스트가 채팅/UI 텍스트를 명령어로 추출 (v0.5.46)
|
||||||
|
- **증상**: Discord에 `cmd="실 동작검증을 해봐야하는데"`, `cmd="variet.gravity-bridge"` 등 사용자 채팅/AI 응답 텍스트가 명령어로 전송됨
|
||||||
|
- **원인**: v0.5.45에서 `PROMPT_ONLY_RE`가 `code/pre` 요소 스킵 성공했으나, `extractContextFromNearby()`의 fallback(`span/div/p` 수집)이 여전히 작동하여 DOM 트리의 채팅 본문/UI 라벨을 명령어로 추출
|
||||||
|
- **해결 (v0.5.46)**: Observer v13에 `_promptOnlySkipped` 플래그 — code 요소가 모두 prompt-only이면 fallback 비활성화. http-bridge에 generic button + no-context일 때 무조건 필터
|
||||||
|
- **주의**: 프롬프트 스킵과 fallback 비활성화는 항상 연동해야 함. VSIX 설치 누락 방지를 위해 빌드 후 즉시 `code --install-extension` 확인 필수
|
||||||
|
|
||||||
### [2026-04-15] [Extension] PROMPT_ONLY_RE regex fixes — prompt-only terminal text leaking as enriched cmd (v0.5.45)
|
### [2026-04-15] [Extension] PROMPT_ONLY_RE regex fixes — prompt-only terminal text leaking as enriched cmd (v0.5.45)
|
||||||
- **증상**: Discord에 `cmd="…\gravity_control >"` (실제 명령어 없는 빈 터미널 프롬프트)가 전송됨. 명령어가 포함된 경우는 정상 작동
|
- **증상**: Discord에 `cmd="…\gravity_control >"` (실제 명령어 없는 빈 터미널 프롬프트)가 전송됨. 명령어가 포함된 경우는 정상 작동
|
||||||
- **원인 1 (Observer)**: `PROMPT_ONLY_RE` 의 `\\\\s`(4중 backslash)가 template literal 안의 regex 리터럴에서 "literal backslash+s"가 되어 whitespace class가 아닌 문자열 매칭
|
- **원인 1 (Observer)**: `PROMPT_ONLY_RE` 의 `\\\\s`(4중 backslash)가 template literal 안의 regex 리터럴에서 "literal backslash+s"가 되어 whitespace class가 아닌 문자열 매칭
|
||||||
|
|||||||
@@ -3,3 +3,4 @@
|
|||||||
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완료? |
|
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완료? |
|
||||||
|-------|-------|----------|-----------|----------|
|
|-------|-------|----------|-----------|----------|
|
||||||
| 001 | 09:12 | PROMPT_ONLY_RE 근본원인 분석 및 수정 — Observer regex 이스케이핑(4중→2중 backslash) + http-bridge ellipsis prefix 지원, 16개 테스트 전체 통과, VSIX v0.5.45 빌드/배포 | `01539e9` | ✅ |
|
| 001 | 09:12 | PROMPT_ONLY_RE 근본원인 분석 및 수정 — Observer regex 이스케이핑(4중→2중 backslash) + http-bridge ellipsis prefix 지원, 16개 테스트 전체 통과, VSIX v0.5.45 빌드/배포 | `01539e9` | ✅ |
|
||||||
|
| 002 | 10:35 | Observer fallback 컨텍스트 추출 수정 — v0.5.45 VSIX 설치 누락 발견/수정 + v13 `_promptOnlySkipped` 플래그로 채팅/UI 텍스트 추출 차단 + bridge generic button 무조건 필터 (v0.5.46) | `b8cda27` | 🔧 |
|
||||||
|
|||||||
30
docs/devlog/entries/20260415-002.md
Normal file
30
docs/devlog/entries/20260415-002.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Observer fallback 컨텍스트 추출 수정 (v0.5.46)
|
||||||
|
|
||||||
|
- **시간**: 2026-04-15 10:35~11:02
|
||||||
|
- **Commit**: `pending`
|
||||||
|
- **Vikunja**: #619 → 진행중
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
|
||||||
|
### `_promptOnlySkipped` 플래그 설계
|
||||||
|
|
||||||
|
**문제**: v0.5.45에서 PROMPT_ONLY_RE가 code/pre 요소의 프롬프트 텍스트를 정상 스킵했으나, `extractContextFromNearby()`의 **fallback 경로**(span/div/p 텍스트 수집)가 DOM 트리를 올라가면서 채팅 본문, UI 라벨, AI 응답을 명령어로 잘못 추출.
|
||||||
|
|
||||||
|
**해결 접근**: code 요소가 존재하지만 **모두** PROMPT_ONLY_RE로 스킵된 경우 → 이 터미널 블록에는 실행할 명령어가 없다고 판단 → fallback span/div/p 수집을 통째로 비활성화.
|
||||||
|
|
||||||
|
**대안 검토**:
|
||||||
|
- ❌ fallback 텍스트에 CJK/자연어 필터 추가 → false negative 위험 (한국어 명령어 경로명 등)
|
||||||
|
- ❌ fallback 수집 depth 제한 → DOM 구조가 바뀌면 다시 깨짐
|
||||||
|
- ✅ **prompt-only 스킵과 fallback 비활성화 연동** → 가장 간결하고 확실
|
||||||
|
|
||||||
|
### VSIX 설치 누락 발견
|
||||||
|
|
||||||
|
이전 세션(fd78c28e)에서 v0.5.45 VSIX를 빌드했으나 **설치를 하지 않았음**. extensions.json 확인 결과 v0.5.43이 설치되어 있었음. 원인: 이전 세션에서 `code --install-extension` 실행 없이 AG 재시작만 수행.
|
||||||
|
|
||||||
|
→ known-issues에 "빌드 후 즉시 install 확인 필수" 주의사항 추가
|
||||||
|
|
||||||
|
## 미완료
|
||||||
|
|
||||||
|
- AG 재시작 후 v0.5.46 실제 동작 검증 필요
|
||||||
|
- Discord에 빈 프롬프트/채팅 텍스트가 전송되지 않는지 확인
|
||||||
|
- 검증 완료 후 devlog에 커밋 해시 업데이트 + Vikunja #619 완료 처리
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "gravity-bridge",
|
"name": "gravity-bridge",
|
||||||
"displayName": "Gravity Bridge",
|
"displayName": "Gravity Bridge",
|
||||||
"description": "Discord-based unified approval system for Antigravity AI interactions.",
|
"description": "Discord-based unified approval system for Antigravity AI interactions.",
|
||||||
"version": "0.5.45",
|
"version": "0.5.46",
|
||||||
"publisher": "variet",
|
"publisher": "variet",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.100.0"
|
"vscode": "^1.100.0"
|
||||||
|
|||||||
@@ -279,9 +279,20 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
|
|||||||
enrichedDesc = `[${rawCmd}] ${rawDesc}`;
|
enrichedDesc = `[${rawCmd}] ${rawDesc}`;
|
||||||
ctx.logToFile(`[HTTP] command enriched: "${rawCmd}" → "${enrichedCmd.substring(0, 60)}"`);
|
ctx.logToFile(`[HTTP] command enriched: "${rawCmd}" → "${enrichedCmd.substring(0, 60)}"`);
|
||||||
} else {
|
} else {
|
||||||
// Keep original button text as command, but use desc as-is
|
// v13: Enrichment failed — description is prompt-only or empty
|
||||||
|
// If cmd is still generic button text, unconditionally filter it
|
||||||
ctx.logToFile(`[HTTP] enrichment skipped (prompt-only): "${rawCmd}" desc="${rawDesc.substring(0, 60)}"`);
|
ctx.logToFile(`[HTTP] enrichment skipped (prompt-only): "${rawCmd}" desc="${rawDesc.substring(0, 60)}"`);
|
||||||
|
ctx.logToFile(`[HTTP] filtered generic+prompt-only: "${rawCmd}"`);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ ok: false, filtered: true, reason: 'generic_btn_prompt_only' }));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (GENERIC_BTN_RE.test(rawCmd) && (rawDesc.length <= 10 || rawDesc === rawCmd)) {
|
||||||
|
// v13: Generic button with no useful description (observer prompt-only context)
|
||||||
|
ctx.logToFile(`[HTTP] filtered generic button no-context: "${rawCmd}" desc="${rawDesc.substring(0, 30)}"`);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ ok: false, filtered: true, reason: 'generic_btn_no_context' }));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Server-side false positive filter (uses enriched cmd) ──
|
// ── Server-side false positive filter (uses enriched cmd) ──
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export function generateApprovalObserverScript(_port: number): string {
|
export function generateApprovalObserverScript(_port: number): string {
|
||||||
return `
|
return `
|
||||||
// ── Gravity Bridge v12: Prompt-Skip + Multi-CodeEl Context Extraction ──
|
// ── Gravity Bridge v13: Prompt-Skip + Fallback Guard ──
|
||||||
// v12: Skip prompt-only code text, try all codeEls, improved enrichment fallback
|
// v13: When all code els are prompt-only, disable fallback text collection entirely
|
||||||
(function(){
|
(function(){
|
||||||
'use strict';
|
'use strict';
|
||||||
var BASE='',_obs=false,_sent={},_ready=false;
|
var BASE='',_obs=false,_sent={},_ready=false;
|
||||||
@@ -10,7 +10,7 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
var CLEANUP_MS=300000;
|
var CLEANUP_MS=300000;
|
||||||
|
|
||||||
function log(m){console.log('[GB Observer] '+m);}
|
function log(m){console.log('[GB Observer] '+m);}
|
||||||
log('v12 Script loaded — Prompt-Skip Context Extraction');
|
log('v13 Script loaded — Prompt-Skip + Fallback Guard');
|
||||||
|
|
||||||
// DIAGNOSTIC BEACON: immediate POST to confirm script execution in renderer
|
// DIAGNOSTIC BEACON: immediate POST to confirm script execution in renderer
|
||||||
try {
|
try {
|
||||||
@@ -109,30 +109,36 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// v12: Climb DOM tree to find context near the button
|
// v13: Climb DOM tree to find context near the button
|
||||||
// Priority: pre/code (skip prompt-only) > substantial span/div/p text > aria-label > button text
|
// Priority: pre/code (skip prompt-only) > substantial span/div/p text > aria-label > button text
|
||||||
// PROMPT_ONLY: matches terminal prompts like "...\project >" or "PS C:\...>" with no actual command
|
// PROMPT_ONLY: matches terminal prompts like "...\project >" or "PS C:\...">" with no actual command
|
||||||
var PROMPT_ONLY_RE = /^[^\\n]*[\\/>\xbb$#]\\s*$/;
|
var PROMPT_ONLY_RE = /^[^\\n]*[\\/\>\xbb$#]\\s*$/;
|
||||||
function extractContextFromNearby(btn) {
|
function extractContextFromNearby(btn) {
|
||||||
var node = btn;
|
var node = btn;
|
||||||
var _debugTrail = [];
|
var _debugTrail = [];
|
||||||
var _fallbackText = ''; // Best span/div/p text found (used if no pre/code)
|
var _fallbackText = ''; // Best span/div/p text found (used if no pre/code)
|
||||||
var _bestCodeText = ''; // Best code text found across all depths
|
var _bestCodeText = ''; // Best code text found across all depths
|
||||||
var _bestCodeHeader = '';
|
var _bestCodeHeader = '';
|
||||||
|
var _sawCodeEls = false; // v13: did we encounter any code/pre elements?
|
||||||
|
var _promptOnlySkipped = false; // v13: were ALL code elements prompt-only?
|
||||||
for (var depth = 0; depth < 20 && node; depth++) {
|
for (var depth = 0; depth < 20 && node; depth++) {
|
||||||
if (!node.querySelector) { _debugTrail.push('d'+depth+':noQS'); node = node.parentElement; continue; }
|
if (!node.querySelector) { _debugTrail.push('d'+depth+':noQS'); node = node.parentElement; continue; }
|
||||||
// Look for code/pre blocks (actual command text)
|
// Look for code/pre blocks (actual command text)
|
||||||
var codeEls = node.querySelectorAll('pre, code, [class*="terminal"]');
|
var codeEls = node.querySelectorAll('pre, code, [class*="terminal"]');
|
||||||
_debugTrail.push('d'+depth+':tag='+((node.tagName||'?').toLowerCase())+',cls='+(((typeof node.className==='string')?node.className:'').substring(0,60))+',codeEls='+codeEls.length);
|
_debugTrail.push('d'+depth+':tag='+((node.tagName||'?').toLowerCase())+',cls='+(((typeof node.className==='string')?node.className:'').substring(0,60))+',codeEls='+codeEls.length);
|
||||||
|
var _depthHadCode = false;
|
||||||
|
var _depthAllPrompt = true;
|
||||||
for (var ci = 0; ci < codeEls.length; ci++) {
|
for (var ci = 0; ci < codeEls.length; ci++) {
|
||||||
var codeText = cleanLines((codeEls[ci].textContent || '').trim().substring(0, 500));
|
var codeText = cleanLines((codeEls[ci].textContent || '').trim().substring(0, 500));
|
||||||
if (!codeText || codeText.length <= 5) continue;
|
if (!codeText || codeText.length <= 5) continue;
|
||||||
if (/^Running\\s*\\d/i.test(codeText)) continue;
|
if (/^Running\\s*\\d/i.test(codeText)) continue;
|
||||||
|
_depthHadCode = true;
|
||||||
// v12: Skip prompt-only text (e.g. "...\gravity_control >") - no actual command
|
// v12: Skip prompt-only text (e.g. "...\gravity_control >") - no actual command
|
||||||
if (PROMPT_ONLY_RE.test(codeText.trim())) {
|
if (PROMPT_ONLY_RE.test(codeText.trim())) {
|
||||||
_debugTrail.push('skip_prompt_ci='+ci+':'+codeText.substring(0,30));
|
_debugTrail.push('skip_prompt_ci='+ci+':'+codeText.substring(0,30));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
_depthAllPrompt = false;
|
||||||
// This code element has actual content - capture it
|
// This code element has actual content - capture it
|
||||||
if (!_bestCodeText || codeText.length > _bestCodeText.length) {
|
if (!_bestCodeText || codeText.length > _bestCodeText.length) {
|
||||||
_bestCodeText = codeText;
|
_bestCodeText = codeText;
|
||||||
@@ -148,6 +154,11 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// v13: Track whether code elements were seen and all were prompt-only
|
||||||
|
if (_depthHadCode) {
|
||||||
|
_sawCodeEls = true;
|
||||||
|
if (_depthAllPrompt) _promptOnlySkipped = true;
|
||||||
|
}
|
||||||
// v12: If we found a good code text, return it immediately at this depth
|
// v12: If we found a good code text, return it immediately at this depth
|
||||||
if (_bestCodeText) {
|
if (_bestCodeText) {
|
||||||
var parts = [];
|
var parts = [];
|
||||||
@@ -157,8 +168,12 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
_lastContextDebug = _debugTrail.join(' > ');
|
_lastContextDebug = _debugTrail.join(' > ');
|
||||||
return parts.join(' — ');
|
return parts.join(' — ');
|
||||||
}
|
}
|
||||||
// v11: Look for substantial text in span/div/p as fallback context
|
// v13: If we saw code elements but all were prompt-only, do NOT collect fallback text
|
||||||
if (depth <= 8 && !_fallbackText) {
|
// This prevents capturing chat messages / UI text from nearby DOM elements
|
||||||
|
if (_promptOnlySkipped) {
|
||||||
|
_debugTrail.push('fallback_blocked_prompt_only');
|
||||||
|
} else if (depth <= 8 && !_fallbackText) {
|
||||||
|
// v11: Look for substantial text in span/div/p as fallback context
|
||||||
var textEls = node.querySelectorAll('span, div, p');
|
var textEls = node.querySelectorAll('span, div, p');
|
||||||
for (var ti = 0; ti < Math.min(textEls.length, 20); ti++) {
|
for (var ti = 0; ti < Math.min(textEls.length, 20); ti++) {
|
||||||
var tText = (textEls[ti].textContent || '').trim();
|
var tText = (textEls[ti].textContent || '').trim();
|
||||||
@@ -175,6 +190,13 @@ export function generateApprovalObserverScript(_port: number): string {
|
|||||||
}
|
}
|
||||||
node = node.parentElement;
|
node = node.parentElement;
|
||||||
}
|
}
|
||||||
|
// v13: If code elements existed but were all prompt-only, return just button text
|
||||||
|
// This ensures http-bridge's Run/Always run filter can handle it properly
|
||||||
|
if (_promptOnlySkipped) {
|
||||||
|
log('CONTEXT-PROMPT-ONLY trail='+_debugTrail.join(' > '));
|
||||||
|
_lastContextDebug = _debugTrail.join(' > ');
|
||||||
|
return cleanButtonText(btn);
|
||||||
|
}
|
||||||
// v11: Use fallback text from span/div/p if available
|
// v11: Use fallback text from span/div/p if available
|
||||||
if (_fallbackText && _fallbackText.length > 5) {
|
if (_fallbackText && _fallbackText.length > 5) {
|
||||||
log('CONTEXT-OK src=fallback trail='+_debugTrail.join(' > '));
|
log('CONTEXT-OK src=fallback trail='+_debugTrail.join(' > '));
|
||||||
|
|||||||
Reference in New Issue
Block a user