Compare commits

...

3 Commits

Author SHA1 Message Date
Variet Worker
c1e61d8d87 diag(observer): v10-diag — extractContextFromNearby trail logging for context extraction failure analysis #task-619 2026-04-14 07:38:01 +09:00
Variet Worker
a9c64e6716 docs: known-issues — template literal regex escaping bug (v0.5.41) 2026-04-14 06:06:23 +09:00
Variet Worker
8e89209a27 fix(observer): template literal regex escaping — \s/\d had wrong backslash count (v0.5.41)
Root cause: regex literals inside template literal (\...\) need exactly 2
backslashes (\\s, \\d) to produce \s, \d in the output. The v9 code had 4
backslashes (\\\\s) which produced literal \\s in the browser (matching
backslash+s instead of whitespace class). This broke:
- 'Running N commands' skip pattern (never matched)
- NOISE_CODE_RE (declare/import line filters)
- cleanLines() pre-strip word boundary \\b and split/join \\n
- cleanButtonText() whitespace strip \\s

The fix restores all regex-literal patterns to exactly 2 backslashes.
new RegExp() string patterns (like NOISE_RE) already used 4 backslashes
correctly (string→regex has one extra escaping layer).

Verified: node eval of generated code confirms /^Running\s*\d+\s*commands?$/i
correctly matches 'Running2 commands' and 'Running 4 commands'.

#task-619 #task-620
2026-04-14 06:05:43 +09:00
6 changed files with 98 additions and 3 deletions

View File

@@ -14,6 +14,21 @@
> **씠 뙆씪 SSOT(Single Source of Truth)엯땲떎.** > **씠 뙆씪 SSOT(Single Source of Truth)엯땲떎.**
> 뵒踰꾧퉭씠굹 援ы쁽 쟾뿉 **諛섎뱶떆** 씠 뙆뙆씪쓣 솗씤븯꽭슂. > 뵒踰꾧퉭씠굹 援ы쁽 쟾뿉 **諛섎뱶떆** 씠 뙆뙆씪쓣 솗씤븯꽭슂.
> 꽭뀡 醫낅즺 떆 깉濡 諛쒓껄맂 씠뒋瑜 씠 뙆뙆씪뿉 異붽빀땲떎.
> [!TIP]
> 빐寃 셿猷뚮맂 怨쇨굅 씠뒋뒗 [`known-issues-archive.md`](file:///c:/Users/Variet-Worker/Desktop/gravity_control/.agents/references/known-issues-archive.md)뿉 蹂닿릺뼱 엳뒿땲떎.
> 鍮꾩듂븳 臾몄젣媛 옱諛쒗븯硫 archive뿉꽌 寃깋븯꽭슂.
### [2026-04-14] [Extension] Observer template literal 정규식 이스케이핑 오류 — "Running N commands" 스킵 미작동
- **증상**: v9에서 `Running N commands` 그룹 헤더를 스킵하는 정규식 `/^Running\s*\d+\s*commands?$/i`를 추가했으나, 실제로 "Running2 commands" 텍스트를 전혀 매칭하지 못하여 여전히 Discord에 `cmd="Running2 commands"`가 전송됨
- **원인**: `observer-script.ts`의 전체 코드가 TypeScript template literal (`` ` `` ... `` ` ``) 안에 있으므로, 정규식 리터럴의 백슬래시가 2중 해석됨:
- TS 소스 `\\\\s` (4중) → template literal 출력 `\\s`**브라우저에서 regex 리터럴 `\\s` = 리터럴 백슬래시+s**
- TS 소스 `\\s` (2중) → template literal 출력 `\s`**브라우저에서 regex 리터럴 `\s` = whitespace class**
- TS 소스 `\s` (1중) → template literal에서 이스케이프 소멸 → **출력 `s`**
- **영향 범위**: `NOISE_CODE_RE`, `cleanLines()` (word boundary `\b`, newline `\n`), `cleanButtonText()` (whitespace `\s`), port 탐색 regex `\d`, "Running N commands" 스킵 regex 전부 미작동이었음
- 단, `NOISE_RE` (new RegExp() 사용)는 문자열 이스케이핑이므로 4중 백슬래시가 올바름 (string → RegExp는 별도 이스케이핑 레이어)
- **해결**: 모든 정규식 리터럴 (`/pattern/flags`) 안의 `\\\\s``\\s`, `\\\\d``\\d`, `\\\\b``\\b`, `\\\\n``\\n` 으로 수정 (v0.5.41, `8e89209`) - **해결**: 모든 정규식 리터럴 (`/pattern/flags`) 안의 `\\\\s``\\s`, `\\\\d``\\d`, `\\\\b``\\b`, `\\\\n``\\n` 으로 수정 (v0.5.41, `8e89209`)
- **주의**: **template literal 안에서 정규식 특수문자를 쓸 때 반드시 구분:** - **주의**: **template literal 안에서 정규식 특수문자를 쓸 때 반드시 구분:**
- `new RegExp('pattern')` 문자열: `\\\\s` (4중) — string 이스케이핑(1번)+regex 이스케이핑(1번) = 총 2번 - `new RegExp('pattern')` 문자열: `\\\\s` (4중) — string 이스케이핑(1번)+regex 이스케이핑(1번) = 총 2번

View File

@@ -0,0 +1,5 @@
# 2026-04-14
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완료여부 |
|-------|-------|----------|-----------|----------|
| 001 | 07:29 | Observer v9 E2E 검증 — BEACON/ping 동작 확인, pending POST 수신 확인, 컨텍스트 추출 실패 진단 (ctx="Always run"), v10-diag 진단 로깅 추가 (observer + http-bridge), V8 캐시 삭제(523MB) | `pending` | 🔧 |

View File

@@ -0,0 +1,49 @@
# Observer v9 E2E 검증 + v10-diag 진단 추가
- **시간**: 2026-04-14 07:29~07:36
- **Commit**: `pending`
- **Vikunja**: #task-619 → 진행중
## 검증 결과
### ✅ 동작 확인 (정상)
1. **Observer v9 스크립트 실행**: `/ping` 수신 확인 (22:30:13~)
2. **BEACON ping**: `/ping?beacon=1&t=...` 형태로 GET /ping 수신
3. **DOM dump**: `dump_html_5.json` (7858 bytes) 정상 저장
4. **WS Hub 연결**: `wsConnected: true`
5. **세션 감지**: `activeSessionId: 39c51225` (현재 세션)
6. **pending POST**: `/pending``1776119428450_gnpw` 등 다수 수신
### ❌ 실패: 컨텍스트 추출
- 모든 pending에서 `cmd="Always run"`, `btns=1`, `ctx="Always run"`
- `extractContextFromNearby()` 실패: pre/code 요소를 찾지 못함
- `collectSiblingButtons()` 실패: 1개 버튼만 감지 ("Cancel" 미감지)
- `sessionStalled: true` + `lastPendingStepIndex: -1` (trajectory not found)
### 원인 분석 (추정)
- AG Native UI의 "Always run" 버튼 근처에 `<pre>`, `<code>`, `[class*="terminal"]` 요소가 없음
- 실제 명령어 텍스트가 다른 요소 유형(span, div 등)에 담겨있을 가능성
- DOM 구조가 이전 세션에서 확인한 것과 다를 수 있음 (Settings/Launchpad dump vs 대화 dump)
## 수정 사항 (v10-diag)
### observer-script.ts
- `extractContextFromNearby()`: 각 depth에서 tag/class/codeEls 수를 `_debugTrail`에 기록
- depth ≤ 5에서 `span, div, p` 요소의 실제 텍스트도 trail에 포함
- 성공 시 `CONTEXT-OK`, 실패 시 `CONTEXT-FAIL` 로그 출력
- pending payload에 `_debug_trail` 필드 추가
### http-bridge.ts
- `_handlePending()`에서 `_debug_trail` 필드를 `[HTTP-DIAG] trail:` 로그로 출력
## 미완료
1. **AG 재시작 필요** — v10-diag 빌드 완료 + V8 캐시 삭제됨, AG 재시작 후 trail 분석 필요
2. **trail 분석 후 대응**:
- pre/code 대신 실제 명령어가 담긴 요소 유형 파악
- `extractContextFromNearby()`의 셀렉터 확장 (span/div 등)
- `collectSiblingButtons()` 범위 확장 검토
3. **"trajectory not found" 에러** — AG Native 세션이 Cascade trajectory API에 등록되지 않는 근본 문제 (known-issues에 이미 기록)
## 결정 사항
- 진단을 먼저 진행하는 것이 올바른 접근 — blind fix 대신 실제 DOM 구조를 trail로 확인한 후 정확한 셀렉터 수정
- trail 데이터가 extension.log에 기록되므로 AG 재시작 후 즉시 확인 가능

View File

@@ -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.40", "version": "0.5.41",
"publisher": "variet", "publisher": "variet",
"engines": { "engines": {
"vscode": "^1.100.0" "vscode": "^1.100.0"

View File

@@ -328,6 +328,9 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
ctx.logToFile(`[HTTP-WS] pending sent via WS: ${rid}`); ctx.logToFile(`[HTTP-WS] pending sent via WS: ${rid}`);
} }
ctx.logToFile(`[HTTP] pending created: ${rid} cmd="${data.command}" btns=${(data.buttons || []).length} ctx="${(data.description || '').substring(0, 50)}"`); ctx.logToFile(`[HTTP] pending created: ${rid} cmd="${data.command}" btns=${(data.buttons || []).length} ctx="${(data.description || '').substring(0, 50)}"`);
if (data._debug_trail) {
ctx.logToFile(`[HTTP-DIAG] trail: ${data._debug_trail.substring(0, 500)}`);
}
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, request_id: rid })); res.end(JSON.stringify({ ok: true, request_id: rid }));
} catch (e: any) { } catch (e: any) {

View File

@@ -110,12 +110,15 @@ export function generateApprovalObserverScript(_port: number): string {
} }
// v9: Climb DOM tree to find pre/code content near the button (no data-step-index needed) // v9: Climb DOM tree to find pre/code content near the button (no data-step-index needed)
// v10-diag: Added diagnostic trail logging
function extractContextFromNearby(btn) { function extractContextFromNearby(btn) {
var node = btn; var node = btn;
var _debugTrail = [];
for (var depth = 0; depth < 20 && node; depth++) { for (var depth = 0; depth < 20 && node; depth++) {
if (!node.querySelector) { 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);
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 && !/^Running\\s*\\d/i.test(codeText)) { if (codeText && codeText.length > 5 && !/^Running\\s*\\d/i.test(codeText)) {
@@ -133,16 +136,35 @@ export function generateApprovalObserverScript(_port: number): string {
var parts = []; var parts = [];
if (headerText) parts.push(headerText); if (headerText) parts.push(headerText);
parts.push(codeText); parts.push(codeText);
log('CONTEXT-OK d='+depth+' trail='+_debugTrail.join(' > '));
return parts.join(' — '); return parts.join(' — ');
} }
} }
// v10-diag: also look for any text-bearing elements (span, div, p) with substantial text
if (depth <= 5) {
var textEls = node.querySelectorAll('span, div, p');
var foundTexts = [];
for (var ti = 0; ti < Math.min(textEls.length, 10); ti++) {
var tText = (textEls[ti].textContent || '').trim();
if (tText.length > 10 && tText.length < 300 && !isNoiseLine(tText)) {
foundTexts.push(tText.substring(0, 80));
}
}
if (foundTexts.length > 0) {
_debugTrail.push('texts=['+foundTexts.join('|')+']');
}
}
node = node.parentElement; node = node.parentElement;
} }
// Last resort: try aria-label or title on the button // Last resort: try aria-label or title on the button
var ariaLabel = btn.getAttribute('aria-label') || btn.getAttribute('title') || ''; var ariaLabel = btn.getAttribute('aria-label') || btn.getAttribute('title') || '';
log('CONTEXT-FAIL trail='+_debugTrail.join(' > '));
// v10-diag: dump button ancestor chain as diagnostic
_lastContextDebug = _debugTrail.join(' > ');
if (ariaLabel && ariaLabel.length > 5) return ariaLabel; if (ariaLabel && ariaLabel.length > 5) return ariaLabel;
return cleanButtonText(btn); return cleanButtonText(btn);
} }
var _lastContextDebug = '';
function extractStepContext(btn) { function extractStepContext(btn) {
var stepEl = getStepContainer(btn); var stepEl = getStepContainer(btn);
@@ -625,7 +647,8 @@ export function generateApprovalObserverScript(_port: number): string {
command:txt2, command:txt2,
description:desc2, description:desc2,
step_type:type2, step_type:type2,
buttons:buttonsArr2 buttons:buttonsArr2,
_debug_trail:_lastContextDebug||''
}; };
fetch(BASE+'/pending',{ fetch(BASE+'/pending',{
method:'POST', method:'POST',