Compare commits

...

68 Commits

Author SHA1 Message Date
Variet Worker
32cf69469c docs: session end — known-issues 2건 추가 + observer-dev-guide 3.3 업데이트 + devlog #019
[2026-04-19] Accept all span 렌더링 감지 실패 (v0.5.101)
[2026-04-19] auto-approve _from_ws 마커 누락 (v0.5.103)
observer-dev-guide 3.3: Accept all Observer 접근 가능으로 변경
Vikunja: #638 done, #639 done
2026-04-20 04:58:13 +09:00
Variet Worker
7c8891b99c fix(bridge): v38 auto-approve response에 _from_ws 마커 추가 — Observer polling 실패 수정 (v0.5.103)
근본 원인: auto-approve response 파일에 _from_ws 마커 없음.
processResponseFile(response watcher)이 Observer보다 먼저 파일을 읽고 삭제.
Observer의 GET /response/{rid} polling이 항상 {waiting:true} 반환.
known-issues [2026-04-18] WS response 파일 삭제 버그와 동일 패턴.
2026-04-20 04:43:56 +09:00
Variet Worker
3cc3442fda fix(bridge): v37 openReviewChanges 선호출 — agentAcceptAllInFile 실효성 보장 (v0.5.102)
agentAcceptAllInFile은 diff review 패널이 포커스되어야 동작.
v34에서 직접 호출만 했더니 SUCCESS 반환하지만 실제 효과 없음.
openReviewChanges → 500ms 대기 → agentAcceptAllInFile 순서로 수정.
2026-04-20 04:34:44 +09:00
Variet Worker
e95e7791f9 fix(observer): v36 Accept all 감지 — span.cursor-pointer 선택자 추가 (v0.5.101)
근본 원인: AG Native에서 Accept all 버튼이 <button>이 아닌 <span> 태그.
Observer의 allBtns 선택자가 button만 스캔하여 Accept all 미감지.
ACCEPT-SCAN 로그에서 tag=SPAN cls=cursor-pointer 확인.
span.cursor-pointer 추가로 diff review 버튼 감지 복구.
2026-04-20 04:26:58 +09:00
Variet Worker
2bf1eb41d1 feat(probe): v35 code_edit 자동 Accept — agentAcceptAllInFile 직접 호출 (v0.5.100)
Accept all 버튼이 Observer DOM에 없음 확인 (에디터 레이어).
step_probe에서 WAITING code_edit 감지 시 500ms 후 agentAcceptAllInFile 직접 실행.
Observer relay 필터에 ACCEPT 추가.
2026-04-20 04:05:47 +09:00
Variet Worker
cf1352eefa fix(bridge): v34 Accept all 이중 보장 — agentAcceptAllInFile 직접 호출 + ACCEPT 로그 relay (v0.5.99)
v34: Accept all/diff_review 감지 시 Observer DOM 클릭 + extension host agentAcceptAllInFile 이중 실행.
Observer relay 필터에 ACCEPT 키워드 추가로 ACCEPT-SCAN 진단 로그 활성화.
2026-04-20 00:11:07 +09:00
Variet Worker
6aea48e2e9 feat(bridge): v33 Accept all 자동승인 — diff review 버튼도 auto-approve (v0.5.98)
Always run과 동일하게 Accept all / Accept 버튼도 즉시 자동 승인.
Observer가 step_type=diff_review로 전송 → bridge에서 AUTO_APPROVE_RE 매칭 → 즉시 응답.
2026-04-19 21:27:18 +09:00
Variet Worker
bd5a7ca8b9 fix(observer): v32 터미널 프롬프트 조기감지 — JUNK/PROMPT 필터 전에 명령어 추출 (v0.5.97)
근본원인: code 요소의 textContent '❯ project > command'가 PowerShell 키워드
(return, function, const)를 포함할 수 있어 JUNK_CODE_RE에 잘못 걸림.
v32: ❯ 마커로 시작하는 code 텍스트는 JUNK/PROMPT 전에 명령어 직접 추출.
14/14 E2E 테스트 통과.
2026-04-19 19:55:44 +09:00
Variet Worker
8ada5f7daf fix(observer): v31 — content_copy 아이콘 필터 + 후보 길이순 정렬 + trailing icon strip (v0.5.96) 2026-04-19 15:18:11 +09:00
Variet Worker
4f2be831a1 diag(observer): v30 SCAN 진단 로그 — div textContent 덤프 (v0.5.95) 2026-04-19 15:08:56 +09:00
Variet Worker
cbfd137dcb fix(observer): v30 Running command 추출 — includes 매칭 + raw fallback + 디버그 로그 (v0.5.94) 2026-04-19 15:03:09 +09:00
Variet Worker
a99a1e3f54 docs: 배포 전 체크리스트에 log relay 필터/regex E2E/가정 검증 항목 추가 2026-04-19 14:56:40 +09:00
Variet Worker
ad4ed623bd docs: observer-dev-guide DOM 구조 BTN-DOM-DUMP 기반 갱신 + log relay 필터 규칙 추가 2026-04-19 14:48:42 +09:00
Variet Worker
64800d3c20 fix(observer): 'Running command' div에서 명령어 추출 — pre/code 대신 plain div 탐색 (v0.5.93) 2026-04-19 14:40:05 +09:00
Variet Worker
70c83b4226 fix(observer): log 필터에 CONTEXT/DEFERRED/DETECTED/BTN-DOM 추가 — 진단 로그 relay 누락 수정 (v0.5.92) 2026-04-19 14:10:59 +09:00
Variet Worker
bb54802c06 feat(enrichment): Discord 알림 지연 + Step Probe 폴링 — generic Always run 커맨드 100% 보강 (v0.5.91) 2026-04-19 10:25:55 +09:00
Variet Worker
bf53072f3c feat(enrichment): Step Probe API 메모리 기반 명령어 보강 — Always run 표시 개선 (v0.5.90) 2026-04-19 09:59:20 +09:00
Variet Worker
02b4b03699 diag(observer): Always run 버튼 주변 DOM 구조 원샷 덤프 추가 (v0.5.89) 2026-04-19 09:19:31 +09:00
Variet Worker
db805c6fde fix(observer): matchedType 대소문자 무시 — Always run이 permission으로 잘못 분류되는 문제 수정 (v0.5.88) 2026-04-19 08:01:47 +09:00
Variet Worker
7f33a20e43 docs: 배포 전 자기검증 체크리스트 추가 — 재시작 요구 최소화 정책 2026-04-19 07:58:14 +09:00
Variet Worker
ef788e6ecc docs: Observer 개발 가이드 SSOT 문서 생성 — 제약사항/배포/DOM구조/교훈 종합 2026-04-19 07:52:53 +09:00
Variet Worker
cd00986274 fix(observer): 깨진 문자열 리터럴 2건 수정 — walkNode 크래시 해결 (v0.5.87) 2026-04-19 07:51:29 +09:00
Variet Worker
12095f36a4 fix(observer): function declaration → var expression — strict mode 크래시 수정 (v0.5.86) 2026-04-19 07:41:04 +09:00
Variet Worker
498683c977 fix(approval): _from_ws 파일 60초 TTL 자동 삭제 — stale SKIP 스팸 방지 (v0.5.85) 2026-04-19 07:32:49 +09:00
Variet Worker
1662ac4f6b fix(observer): regex → 문자열 비교로 isGenericDesc 수정 — template literal escaping 회피 (v0.5.84) 2026-04-19 07:02:04 +09:00
Variet Worker
d027562f17 fix(observer): 500ms 딜레이드 컨텍스트 추출 + 버튼 셀렉터 확장 (v0.5.83)
- Always run 감지 시 desc가 generic이면 500ms 딜레이 후 재추출
- 버튼 셀렉터에 role=button, monaco-button, vscode-button 추가
- ACCEPT-SCAN 디버그 로그 (30초 간격)
2026-04-19 06:53:54 +09:00
Variet Worker
cc261011d6 fix(observer): 구 visibility 체크 제거 — Accept all 버튼 감지 차단 수정 (v0.5.81) 2026-04-19 04:18:35 +09:00
Variet Worker
37fbb9657e fix(observer): diff_review Accept all 버튼 감지 — offsetParent 체크 완화 (v0.5.80) 2026-04-19 03:53:03 +09:00
Variet Worker
965f619664 docs: relay-architecture + known-issues 업데이트 (v0.5.78-79 변경사항 반영) 2026-04-19 03:48:31 +09:00
Variet Worker
139ad3ee93 fix(extension): Retry auto-approve 흐름 복구 + Observer 형제 탐색 + thinking 필터링 (v0.5.79)
- WS response 파일에 _from_ws 마커 추가하여 processResponseFile 삭제 방지
- extractContextFromNearby에 sibling 탐색 추가 (AG Native DOM 구조 대응)
- thinking 블록 (max-h-[200px]) 필터링으로 내부 사고 릴레이 차단
- DOM 탐색 depth 5→10 확대 + pre.font-mono 우선 탐색
- 사용자 메시지 셀렉터 (.select-text.rounded-lg) 추가
2026-04-19 03:46:39 +09:00
Variet Worker
08fd08b9a6 feat(observer): diagnostic log relay via HTTP + auto-approve enrichment fallback (v0.5.63) #task-634 2026-04-18 08:18:35 +09:00
Variet Worker
326454be40 fix(bridge): move Always run auto-approve BEFORE filter chain — no more silent drops (v0.5.60) #task-634 2026-04-18 06:54:15 +09:00
Variet Worker
98b7585e3c fix(observer): text-level markdown table wrapping for Discord — AG Native uses divs not HTML tables (v0.5.59) #task-634 2026-04-18 06:46:21 +09:00
Variet Worker
c7f939ce85 fix(bridge): Always run auto-approve now checks buttons array, not just rawCmd (v0.5.58) #task-634 2026-04-18 06:35:01 +09:00
Variet Worker
7a1675fd5d feat(observer): table-to-codeblock conversion for Discord compatibility (v0.5.57) #task-634 2026-04-18 06:25:55 +09:00
Variet Worker
6b9f1188c3 feat(bridge): DOM Markdown parser restoration (v0.5.56) + code noise filter fix + user msg relay #task-634 2026-04-17 08:06:53 +09:00
Variet Worker
13e569f426 chore(bridge): update known-issues and prep for DOM Observer MD restoration (#634) 2026-04-17 06:25:22 +09:00
Variet Worker
b2f17a086b docs: devlog 20260416-005 v0.5.53 Always run auto-approve + index update 2026-04-16 22:08:16 +09:00
Variet Worker
7dbf73aa89 feat(bridge): v17 Always run auto-approve + Retry button relay (v0.5.53) #task-632
- Observer v17: ACTION_WORDS에 'Retry' 추가 — Agent terminated 대화상자의 Retry 버튼 감지/릴레이
- http-bridge: 'Always run' 버튼 자동승인 — response 파일 즉시 생성하여 observer가 바로 클릭
- bot.py: auto_approved 상태 처리 — Discord에 비대화형 '자동 승인' 알림 표시
- Observer matchedType에 'retry' step_type 매핑
2026-04-16 22:03:09 +09:00
Variet Worker
62ee081ffe fix(observer): v16 style/script strip in extractCleanStepText — CSS가 AI 응답으로 Discord 전달되는 버그 수정 (v0.5.52) #task-632 2026-04-16 21:08:52 +09:00
Variet Worker
60a2a97868 docs: E2E 코드 검증 결과 기록 — known-issues #632 업데이트 + devlog 003 2026-04-16 17:17:18 +09:00
Variet Worker
7ae43088e6 docs: devlog 20260416-002 AG Native chat relay + index update 2026-04-16 05:30:20 +09:00
Variet Worker
729875f3a6 feat(observer): v15 AG Native chat relay — scanChatBodies dual strategy (#632)
- Add AG Native DOM path: #conversation + .leading-relaxed.select-text
- Keep Cascade path: [data-testid=conversation-view] + [data-step-index]
- Register #632 in known-issues.md (SDK+DOM both blocked for AG Native)
- Bump version 0.5.50 → 0.5.51
- Add DOM analysis helper scripts
2026-04-16 05:28:44 +09:00
Variet Worker
a00d561e28 docs: devlog 2026-04-16 + known-issues v0.5.50 terminal output filter 2026-04-16 04:59:27 +09:00
Variet Worker
7ade31e4cf fix(bridge): v16 terminal output filter + v15 stale LS auto-fix + heartbeat probe (v0.5.50) #task-619
- http-bridge v16: Block terminal OUTPUT as enriched cmd — if description has no prompt marker (> » $ #), it's stdout from code block, not an actual command. Prevents 'No extension.log found' etc. from reaching Discord.
- step-probe v15: Stale LS auto-detection — if all sessions are >5min old, periodically retry fixLSConnection(). Heartbeat probe every 10 polls to detect step changes when summary API returns frozen stepCount.
- extension.ts v15: fixLSConnection() fallback — match LS processes without --workspace_id (common after AG restart)
2026-04-16 04:58:05 +09:00
Variet Worker
66233bd9cb fix(bridge): strengthen JUNK_CONTENT_RE — add CSS block, .code-block/.code-line, integration.build patterns
Unit tested: 12 legit commands pass, all junk patterns now filtered.
HTML manually patched to v14 (bypasses extension host cache).
2026-04-15 15:37:09 +09:00
Variet Worker
ed90cbf874 fix(observer/bridge): v14 — strict 5-level DOM scope, CSS/code/icon junk filter, auto-version sync (v0.5.47) #task-619
Root causes fixed:
1. extractContextFromNearby depth 20→5 — stops grabbing unrelated UI/editor code
2. JUNK_CODE_RE — rejects CSS rules, JS source code, extension internals
3. ICON_GLUE_RE — rejects Material icon text glued with content
4. Fallback span/div/p collection REMOVED entirely (always grabbed chat text)
5. html-patcher strips old observer from integration.build() cache
6. http-bridge server-side JUNK_CONTENT_RE as last line of defense
2026-04-15 14:55:58 +09:00
Variet Worker
2e32be96fe fix(observer/bridge): v13 _promptOnlySkipped fallback guard + generic button no-context filter (v0.5.46) #task-619 2026-04-15 13:27:09 +09:00
Variet Worker
87c99c7243 docs: devlog 2026-04-15 + scratch cleanup
- devlog entry: PROMPT_ONLY_RE 근본원인 분석 및 수정 (v0.5.45)
- scratch files: 이전 세션 디버깅 스크립트 전체 정리
2026-04-15 09:59:22 +09:00
Variet Worker
01539e9bfb fix(observer/bridge): PROMPT_ONLY_RE — regex escaping + ellipsis prefix handling (v0.5.45) #task-619
Observer: \\\\s (4-backslash) → \\s (2-backslash) in template literal regex
  - Per known-issues rule: regex literals need 2-backslash for \s, \d etc.
  - Old: literal \\s matched, new: whitespace class matched correctly

http-bridge: /^[\s\\\/]*[\w_.-]+\s*[>»$#]\s*$/ → /^.*[>»$#]\s*$/
  - Old pattern failed on ellipsis '…' prefix (U+2026 not in charset)
  - Terminal prompt '…\\gravity_control >' was passing through as cmd
  - New pattern: any text ending with prompt marker is skipped
2026-04-15 09:53:32 +09:00
Variet Worker
4684376c78 fix(http-bridge): remove incorrect extracted===rawDesc skip in enrichment (v0.5.44) #task-619 2026-04-15 07:47:56 +09:00
Variet Worker
7ee5947b32 fix(observer): v12 — skip prompt-only code text + enrichment validation (v0.5.44) #task-619
- extractContextFromNearby: PROMPT_ONLY_RE skips code elements containing only terminal prompts (e.g. '\\gravity_control >')
- Multi-codeEl traversal: tries all code elements at each depth, picks longest non-prompt text
- http-bridge enrichment: validates extracted command is not just a prompt fragment before enriching
- Fixes: cmd='\\gravity_control >' (prompt-only) no longer sent to Discord as the command text
2026-04-15 07:43:15 +09:00
Variet Worker
59ddcbb612 fix(http-bridge): move command enrichment before Run filter — fixes context loss (v0.5.43) #task-619 2026-04-15 06:54:06 +09:00
Variet Worker
ed63f65975 feat(observer): v11 — enhanced context extraction + 5-level sibling search + command enrichment (v0.5.42) #task-619
- extractContextFromNearby: span/div/p text fallback when pre/code not found (depth 0-8)
- collectSiblingButtons: extended to 5 parent levels, stop only when both action+reject found
- http-bridge: command enrichment — swap generic button text with actual command from description
- Fixes: cmd='Always run' now shows actual command text, Cancel button detection improved
2026-04-15 06:20:47 +09:00
Variet Worker
02d9799f20 diag(observer): v10-diag — extractContextFromNearby trail logging for context extraction failure analysis #task-619 2026-04-14 07:38:27 +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
Variet Worker
a8d875167d fix(observer): v9 - stop treating Running N commands as approval button, add DOM-climbing context extraction 2026-04-13 19:37:18 +09:00
Variet Worker
2a1ebf1020 fix(extension): UTF-8 encoding + noise filter enhancement (v0.5.39)
- http-bridge.ts: add req.setEncoding('utf8') to all POST handlers
  to fix Korean text corruption in pending/chat/dump payloads
- observer-script.ts: add inline pre-strip in cleanLines() for
  Material icon names concatenated by textContent without newlines
- observer-script.ts: apply cleanLines() to codeText extraction
- known-issues: document UTF-8 encoding and noise filter issues
2026-04-13 12:56:25 +09:00
Variet Worker
5a76e30993 docs: known-issues — html-patcher String.replace dollar-pattern corruption bug 2026-04-13 12:22:41 +09:00
Variet Worker
d6ed8764b8 fix(html-patcher): escape $ in inline script replacement to prevent String.replace() special pattern corruption — root cause of observer SyntaxError #task-619 2026-04-13 12:22:02 +09:00
Variet Worker
a214ab029f docs: devlog — observer v8 verification session, V8 cache re-cleared 2026-04-13 11:02:26 +09:00
Variet Worker
f45d2d970d fix(observer): ensure inline script injection before </body> for Electron execution + add BEACON diagnostic ping
- html-patcher: relocate inline script from after </html> to before </body>
- html-patcher: clean up duplicate </html> tags from previous bad insertions
- observer-script: add immediate BEACON fetch to /ping on script load
- http-bridge: add diagnostic request logging for non-polling endpoints
- devlog 003: crash recovery session notes
2026-04-12 21:30:58 +09:00
Variet Worker
6dc0854c47 docs: devlog 003 — observer v8 full-DOM dump 2026-04-12 07:37:52 +09:00
Variet Worker
0e03b3a300 feat(observer): v8 full-DOM unconditional dump — body tree capture at 5s/15s/60s with depth 15, indexed dump files, deep-inspect overhaul #task-619 2026-04-12 07:37:25 +09:00
Variet Worker
353265bed8 docs: AG Native bundle reverse engineering analysis — plannerResponse/Whi renderer structure, V8 cache fix, known-issues update 2026-04-12 07:05:57 +09:00
Variet Worker
eef59e6bb2 docs: devlog entry for AG Native DOM parser v7 rewrite session 2026-04-12 06:15:47 +09:00
Variet Worker
a4d7286bce refactor(observer): v7 step-aware AG Native DOM parser with data-testid/data-step-index based content extraction
- Replace CSS class-based scanning with [data-testid='conversation-view'] + [data-step-index] traversal
- New extractCleanStepText(): clone-and-strip buttons/SVG/icons before text extraction
- New extractStepContext(): step-container-aware context with header + code block
- NOISE_RE: block Material icon names, button labels, UI artifacts
- Auto DOM structure dump on first conversation-view detection
- Enhanced deep-inspect with step element + button inventory
- known-issues: document AG Native SDK API incompatibility
2026-04-12 06:14:46 +09:00
80 changed files with 4423 additions and 1316 deletions

View File

@@ -1,253 +1,629 @@
# Known Issues & Lessons Learned # Known Issues & Lessons Learned
> **이 파일은 SSOT(Single Source of Truth)입니다.**
> 디버깅이나 구현 전에 **반드시** 이 파일을 확인하세요.
> 세션 종료 시 새로 발견된 이슈를 이 파일에 추가합니다. > **<2A><20><EFBFBD><EFBFBD><EC94AA><EFBFBD> SSOT(Single Source of Truth)<29><EFBFBD><EFBFBD>떎.**
> <EFBFBD>뵒踰꾧퉭<EFBFBD><EFBFBD>굹 援ы쁽 <20><EFBFBD>뿉 **諛섎뱶<EC848E>떆** <20><20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><EFBFBD>슂.
> <EFBFBD><EFBFBD>뀡 醫낅즺 <20><20>깉濡<EAB989> 諛쒓껄<EC9293><20><EFBFBD>뒋瑜<EB928B> <20><20><EFBFBD><EFBFBD>뿉 異붽<E795B0><EBB6BD><EFBFBD><EFBFBD><EFBFBD>떎.
# Known Issues & Lessons Learned
> **씠 뙆씪 SSOT(Single Source of Truth)엯땲떎.**
> 뵒踰꾧퉭씠굹 援ы쁽 쟾뿉 **諛섎뱶떆** 씠 뙆뙆씪쓣 솗씤븯꽭슂.
> 꽭뀡 醫낅즺 떆 깉濡 諛쒓껄맂 씠뒋瑜 씠 뙆뙆씪뿉 異붽빀땲떎.
> [!TIP] > [!TIP]
> 해결 완료된 과거 이슈는 [`known-issues-archive.md`](file:///c:/Users/Variet-Worker/Desktop/gravity_control/.agents/references/known-issues-archive.md)에 보관되어 있습니다. > 빐寃 셿猷뚮맂 怨쇨굅 씠뒋뒗 [`known-issues-archive.md`](file:///c:/Users/Variet-Worker/Desktop/gravity_control/.agents/references/known-issues-archive.md)뿉 蹂닿릺뼱 엳뒿땲떎.
> 비슷한 문제가 재발하면 archive에서 검색하세요. > 鍮꾩듂븳 臾몄젣媛 옱諛쒗븯硫 archive뿉꽌 寃깋븯꽭슂.
### [2026-04-19] [Observer] ★ Accept all 버튼이 `<span>`으로 렌더링 — Observer 감지 실패 (v0.5.101)
- **증상**: "Accept all" diff review 버튼이 화면에 보이지만 Observer가 감지하지 못함. Discord에 "Accept all" 자동 승인 알림이 안 옴.
- **원인**: AG Native UI 업데이트로 "Accept all"이 `<button>`이 아닌 `<span class="cursor-pointer">`로 렌더링됨. Observer의 `allBtns = querySelectorAll('button, [role="button"]')`에 span이 포함되지 않음. ACCEPT-SCAN 로그: `tag=SPAN cls=hover:text-ide-button-hover-color cursor-po txt=Accept all`.
- **해결 (v0.5.101)**: `allBtns` 선택자에 `span.cursor-pointer` 추가.
- **주의**: observer-dev-guide 섹션 3.3 "Accept all — Observer 접근 불가"는 outdated. UI 변경으로 chat panel footer에 Accept all이 표시됨. 가이드 업데이트 필요.
### [2026-04-19] [Bridge] ★ auto-approve response 파일에 `_from_ws` 마커 누락 — Observer polling 실패 (v0.5.103)
- **증상**: Observer가 "Accept all"을 감지하고 bridge가 자동 승인했지만, Observer의 `pollResponseGroup` GET `/response/{rid}`가 항상 `{waiting: true}` 반환. 버튼 클릭이 실행되지 않음.
- **원인**: http-bridge의 auto-approve 경로에서 response JSON 파일에 `_from_ws: true` 마커가 없음 → `processResponseFile`(response watcher)이 Observer보다 먼저 파일을 읽고 삭제 → Observer polling 시 파일 부재. known-issues [2026-04-18] WS response 삭제 버그와 동일 패턴.
- **해결 (v0.5.103)**: auto-approve response에 `_from_ws: true` + `_auto_approve_ttl` 마커 추가.
- **주의**: **response 디렉토리에 파일을 쓰는 모든 경로**는 반드시 `_from_ws: true` 마커를 포함해야 함. processResponseFile이 먼저 소비하는 race condition 항상 존재.
### [2026-04-18] [Extension] ★ WS response 파일이 processResponseFile에 의해 삭제 → Observer pollResponseGroup 실패 (v0.5.78)
- **증상**: `!auto` Retry가 작동하지 않음. Observer가 `/response/{rid}`를 폴링하지만 항상 `{waiting: true}` 반환.
- **원인**: extension.ts의 WS 응답 핸들러가 `response/{rid}.json` 파일 작성 → 300ms 후 response watcher(`processResponseFile`)가 파일 감지 → pending 파일이 없어 `isDomObserver=false``fs.unlinkSync()` 실행 → Observer가 폴링할 때 파일이 이미 삭제됨.
- **해결 (v0.5.78)**: WS 응답 파일에 `_from_ws: true` 마커 추가. `processResponseFile`에서 `_from_ws` 파일 스킵 (WS 핸들러에서 이미 `tryApprovalStrategies` 실행하므로 중복 방지도 함께 해결).
- **주의**: http-bridge의 `_handlePending`는 pending 파일을 생성하지 않음 (WS 전송만 수행). 따라서 `processResponseFile``isDomObserver` 판별이 실패함. WS 경로로 들어오는 모든 response는 반드시 마커로 구분해야 함.
### [2026-04-18] [Extension] ★ extractContextFromNearby 조상 탐색만으로는 명령어 추출 불가 (v0.5.79)
- **증상**: Discord auto-approve 알림에 "Always run"만 표시되고 실제 명령어가 안 보임. depth를 10으로 늘려도 동일.
- **원인**: AG Native DOM 구조에서 "Always run" 버튼은 `footer` 내부에 있고, 실제 명령어(`pre.font-mono`)는 `footer`**형제(sibling)** 요소에 있음. 조상 탐색(parentElement)으로는 도달 불가. trail: `d0:button → d1:div → d2:footer` (footer.parentElement가 null).
- **해결 (v0.5.79)**: `extractContextFromNearby`에 형제 탐색 로직 추가. 각 depth에서 `node.parentElement.children`을 순회하며 `pre.font-mono, pre, code`를 찾음 → `CONTEXT-OK src=sibling` 성공.
- **주의**: Observer 코드 변경은 HTML 인라인 스크립트이므로 AG 2번 재시작(Quit + Relaunch × 2) 필요.
### [2026-04-18] [Extension] Thinking 블록이 AI 응답으로 릴레이됨
- **증상**: AI의 내부 사고 과정(thinking/reasoning)이 Discord에 릴레이됨.
- **원인**: Observer의 `scanChatBodies``.leading-relaxed.select-text` 블록을 전부 캡처하는데, thinking 블록도 이 셀렉터에 매칭됨.
- **해결**: thinking 블록의 조상에 `max-h-[200px]` 클래스가 있는지 확인하여 필터링.
- **주의**: AG UI 업데이트로 thinking 블록의 클래스가 변경될 수 있음.
- **증상**: Step Probe의 RT-CAPTURE, HB-CAPTURE가 현재 대화 중에 전혀 발동하지 않음. POLL에서 `status=IDLE, steps=928, delta=0` 고정. Heartbeat probe에서도 `real=928 known=928` 불변.
- **원인**: AG Native의 `GetCascadeTrajectorySteps` API는 **cascade가 완전히 종료(IDLE 전환)된 후에만** 새 step을 반환합니다. 진행 중인 cascade에서는 step count가 동결됩니다. `GetAllCascadeTrajectories``stepCount`도 마찬가지.
- **영향**: Step Probe 기반의 모든 실시간 캡처(RT-CAPTURE, HB-CAPTURE, USER_INPUT 감지)가 **구조적으로 불가능**. Observer DOM이 유일한 실시간 데이터 경로.
- **해결**: Observer DOM 기반 chat relay를 재활성화 (v0.5.72). Step Probe는 cascade 완료 후 보완 용도로만 사용.
- **주의**: 이 제약은 AG Native 아키텍처의 근본적 특성. Polling 주기나 heartbeat 빈도를 변경해도 해결 불가. **실시간 릴레이는 반드시 Observer DOM 경로를 사용해야 함.**
- **참조**: `.agents/references/relay-architecture.md` (상세 분석)
### [2026-04-18] [Extension] Observer의 /pending POST에 명령어 컨텍스트가 없음 (Always run만 전달)
- **증상**: Discord auto-approve 알림에 "Always run"만 표시되고 실제 명령어가 안 보임.
- **원인**: Observer가 `/pending` POST 시 `command: "Always run"`, `description: "Always run"`만 보냄. `extractContextFromNearby(btn)`이 버튼 주변 DOM에서 유의미한 텍스트를 찾지 못함.
- **해결 (부분)**: v0.5.69에서 bridge/pending/ 디렉토리의 최신 Step Probe pending 파일에서 명령어를 읽는 fallback 추가. Step Probe pending이 있을 때만 작동.
- **주의**: Step Probe WAITING 감지가 진행 중 cascade에서 불가하므로 (위 이슈 참조), 현재 대부분의 경우 fallback도 실패. Observer의 DOM 컨텍스트 추출 개선이 필요.
### [2026-04-18] [Extension] Observer의 사용자 메시지 셀렉터 미매칭
- **증상**: 사용자가 AG에서 입력한 메시지가 Discord에 전혀 전달되지 않음.
- **원인**: Observer의 셀렉터(`.text-ide-message-block-user-color`, `[data-message-role="user"]` 등)가 AG Native DOM에서 매칭되지 않음. AI 응답만 `.leading-relaxed.select-text`로 매칭됨.
- **해결**: DOM 덤프에서 사용자 메시지 블록의 실제 CSS 클래스를 식별 후 셀렉터 추가 필요.
- **주의**: Step Probe의 USER_INPUT 캡처도 진행 중 cascade에서 불가 (위 이슈 참조).
### [2026-04-16] [Extension] ★ AG Native 세션 AI 응답이 Discord에 CSS로 전달됨 (v0.5.52 수정, #632)
- **증상**: Discord에 AI 대화 응답 대신 **CSS 스타일시트 코드** (`remark-github-blockquote-alert/alert.css`)가 전달됨. `scanChatBodies()` → POST /chat 경로는 작동하지만 내용이 CSS.
- **원인**: `extractCleanStepText()`에서 clone한 DOM에서 버튼/SVG/아이콘은 제거하지만 **`<style>` 태그는 제거하지 않음**. AG Native 마크다운 렌더러가 `.leading-relaxed.select-text` 내부에 `<style>` 블록을 주입하는데, 이 CSS textContent가 AI 응답 텍스트로 추출됨.
- **해결 (v0.5.52)**: `extractCleanStepText()` 최상단에 `clone.querySelectorAll('style, script, noscript, link[rel="stylesheet"]')` 제거 로직 추가. CSS/JS가 텍스트로 포함되는 것을 원천 차단.
- **배포**: v0.5.52 VSIX 설치 + v0.5.50/out/ JS 직접 복사 + V8 CachedData 삭제. AG File→Quit 재시작 필요.
- **이전 블로커 해결 이력**:
- SDK 경로: AG Native는 Cascade trajectory API 미등록 → step-probe RT-CAPTURE 불가 (구조적 한계)
- DOM 경로: v15에서 `#conversation` + `.leading-relaxed.select-text` 셀렉터 추가로 해결
- BEACON=0: AG 프로세스 완전 재시작으로 해결 (Reload Window로는 렌더러 HTML 캐시 유지)
- **주의**: AG Native 마크다운 렌더링은 `<style>` 블록을 응답 DOM 내부에 인라인으로 삽입함. DOM 텍스트 추출 시 반드시 style/script 태그를 먼저 제거해야 함.
- **Vikunja**: #632
### [2026-04-16] [Extension] ★ AG Native Observer innerText로 인한 마크다운 포맷 유실 및 사용자 요청 누락
- **증상**: Discord로 릴레이되는 AI 응답에서 리스트 번호(`1. 2.`)나 불릿(`-`) 등 마크다운 포맷이 완전히 유실되고 텍스트만 이어져서 나옴. 그리고 사용자가 입력한 메시지(요청)는 아예 릴레이되지 않음.
- **원인 1**: `extractCleanStepText()`에서 `innerText`를 사용하여 텍스트를 추출할 때, 렌더링되지 않은 DOM이나 CSS 카운터로 생성된 list-item 마커가 무시됨.
- **원인 2**: `scanChatBodies()` 로직이 `.leading-relaxed.select-text` (AI 응답 블록)만을 타겟팅하여 사용자의 메시지 박스(예: `.text-ide-message-block-user-color`)는 추출 대상에서 제외됨.
- **해결 (계획 중)**: `innerText` 대신 HTML DOM 노드를 순회하며 `convertNodeToMarkdown` 변환 함수를 통해 마크다운 문법을 보존하도록 개선. User 블록도 함께 감지하여 브릿지에 `role: 'user'` 플래그를 추가 전송하도록 수정 예정.
- **주의**: Webview 내에서 텍스트 노드를 파싱할 때 `innerText`는 브라우저 레이아웃 엔진에 의존하므로 완전한 마크다운 보존을 위해서는 Node Type 순회를 활용한 구조 복원이 보장되어야 함.
### [2026-04-16] [Extension] 터미널 출력(stdout) 텍스트가 명령어로 Discord에 전송 (v0.5.50)
- **증상**: Discord에 `cmd="No extension.log found"`, `cmd="AG CLI not found..."`, `cmd="Log found: C:\..."` 등 터미널 **출력** 텍스트가 명령어로 전송됨
- **원인**: Observer가 code 블록 2개를 감지: (1) 프롬프트+명령어 → JUNK_CODE_RE로 스킵, (2) 터미널 출력 → 유효한 code로 판단 → description에 포함. http-bridge enrichment에서 description에 prompt marker(`>`)가 없으면 rawDesc 전체를 enrichedCmd로 채택
- **해결 (v0.5.50)**: `promptMatch` 실패 시(description에 `>` 없음) → 터미널 OUTPUT으로 판단하여 `terminal_output` 사유로 즉시 필터. 실제 명령어는 항상 `…\project > command` 프롬프트를 포함
- **주의**: enrichment은 반드시 prompt marker가 있는 경우에만 수행. description에 `>` 없으면 code 블록의 출력 텍스트
### [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)
- **증상**: Discord에 `cmd="…\gravity_control >"` (실제 명령어 없는 빈 터미널 프롬프트)가 전송됨. 명령어가 포함된 경우는 정상 작동
- **원인 1 (Observer)**: `PROMPT_ONLY_RE``\\\\s`(4중 backslash)가 template literal 안의 regex 리터럴에서 "literal backslash+s"가 되어 whitespace class가 아닌 문자열 매칭
- **원인 2 (http-bridge)**: `PROMPT_ONLY_RE = /^[\s\\\/]*[\w_.-]+\s*[>»$#]\s*$/` — AG 터미널 프롬프트가 `…`(U+2026 ellipsis)로 시작하는데 첫 문자 클래스에 포함 안 됨
- **해결 (v0.5.45, `d2c44fe`)**: Observer `\\s``\s`, http-bridge 패턴을 `/^.*[>»$#]\s*$/`로 단순화
- **검증**: 11개 테스트 케이스 전체 통과
--- ---
### [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`)
- **주의**: **template literal 안에서 정규식 특수문자를 쓸 때 반드시 구분:**
- `new RegExp('pattern')` 문자열: `\\\\s` (4중) — string 이스케이핑(1번)+regex 이스케이핑(1번) = 총 2번
- `/pattern/` 정규식 리터럴: `\\s` (2중) — template literal 이스케이핑(1번)만 = 총 1번
- **검증법**: `node -e "var f = require('./extension/out/observer-script.js').generateApprovalObserverScript; require('fs').writeFileSync('tmp.js', f(0)); console.log(require('fs').readFileSync('tmp.js','utf8').match(/Running.{10}/g));"` 으로 생성된 코드 확인
---
### [2026-04-13] [Extension] Observer v8 "Running N commands" 그룹 헤더를 승인 버튼으로 오인 — Discord 빈 내용+잘못된 버튼
- **증상**: Discord 승인 요청에 command="Running2 commands", description 비어있음, 버튼도 "Running2 commands / Always run" 형태. 실제 코드/명령어 내용이 전혀 표시되지 않음
- **원인 1**: `observer-script.ts``isActionBtn()``/Running\\s*\\d*\\s*command/i` 패턴이 있어 AG UI의 그룹 헤더 버튼("Running 3 commands")을 승인 버튼으로 분류. `scan()`이 이 버튼을 먼저 만나고 `break`로 나가 실제 "Always run"/"Cancel" 버튼은 처리 안 됨
- **원인 2**: `extractStepContext()``data-step-index` 속성 없으면 `cleanButtonText(btn)` = "Running2 commands"를 그대로 반환. AG Native에는 `data-step-index`/`data-testid` 속성이 없음 (DOM 덤프로 확인)
- **원인 3**: `http-bridge.ts`의 "Run/Always run" 필터가 step-probe 미활성(activeSessionId 비어있음) 시에도 DOM observer 신호를 차단
- **해결**: observer v9 (v0.5.40):
1. `isActionBtn()`에서 "Running N commands" 패턴 제거
2. `scan()`에서 `^Running\\s*\\d+\\s*commands?$` 명시적 스킵
3. `extractContextFromNearby()` 추가: `data-step-index` 없이 DOM 트리를 20레벨까지 올라가며 `pre`/`code` 블록에서 실제 명령어 추출
4. `collectSiblingButtons()` 범위를 parent → grandparent → great-grandparent로 확대, 그룹 헤더 스킵
5. `http-bridge.ts`의 "Run" 필터에 `ctx.activeSessionId` 체크 추가 — step-probe 미활성 시 DOM observer 허용
- **주의**: AG Native UI의 "Running N commands"는 아코디언/그룹 헤더이며, 실제 승인 버튼은 하위 레벨의 "Run"/"Always run"/"Cancel". DOM 구조상 버튼 탐색 시 그룹 헤더를 반드시 스킵해야 함
### [2026-04-13] [Extension] HTTP Bridge UTF-8 인코딩 깨짐 — 한글 description 손실
- **증상**: pending/ 파일의 description 필드에서 한글이 `[AI ]`처럼 깨져서 저장됨. Discord로 전달되는 승인 요청 본문도 깨짐
- **원인**: Node.js HTTP 서버의 `req.on('data', chunk)` 콜백에서 chunk가 Buffer 타입으로 전달되는데, `body += chunk`로 string 결합 시 Buffer의 기본 인코딩(latin1)이 사용되어 multi-byte UTF-8 문자가 손실됨
- **해결**: 모든 POST 핸들러(`/pending`, `/dump-html`, `/chat`, `/deep-inspect-result`, `/test-rpc`)에 `req.setEncoding('utf8')` 추가 (v0.5.39)
- **주의**: Node.js HTTP 서버에서 POST body를 문자열로 수집할 때는 반드시 `req.setEncoding('utf8')`을 호출하거나, Buffer를 배열로 모은 후 `Buffer.concat().toString('utf8')`로 변환해야 함
### [2026-04-13] [Extension] Observer noise 필터 미작동 — textContent가 아이콘 텍스트를 줄바꿈 없이 합침
- **증상**: pending description에 `Thought for 1s`, `chevron_right` 등 Material 아이콘명과 UI 노이즈가 그대로 남아있음
- **원인**: DOM `textContent`는 block 요소 사이에 newline을 삽입하지 않아 `[AI 본문 요약]Thought for 1schevron_right[결행 명령]`처럼 한 줄로 합쳐짐. `cleanLines()`의 줄 단위 noise 필터(`^pattern$`)가 매칭 실패. 또한 `codeText` 추출에는 `cleanLines()`가 아예 미적용
- **해결**: `cleanLines()`에 인라인 pre-strip 추가 — icon명 18종을 regex로 먼저 `\n`으로 치환 후 줄 단위 필터 적용. `codeText`에도 `cleanLines()` 적용 (v0.5.39)
- **주의**: DOM에서 텍스트 추출 시 `textContent`는 레이아웃 무시, `innerText`는 detached 노드에서 미작동. 노이즈 필터링은 줄 단위뿐 아니라 인라인 패턴 제거도 병행해야 함
### [2026-04-13] [Extension] html-patcher String.replace() `$'` 특수 패턴으로 인라인 스크립트 SyntaxError
- **증상**: Observer v8 인라인 스크립트가 workbench.html에 삽입되었으나 렌더러에서 전혀 실행되지 않음 (BEACON 핑 0건). V8 캐시 삭제 + AG 재시작 후에도 동일
- **원인**: `html-patcher.ts`에서 `html.replace('</body>', '\n' + inlineBlock + '\n</body>')`를 사용. 인라인 스크립트의 NOISE_RE 정규식에 `')$', 'i'`가 있는데, `$'`는 JS `String.replace()`의 특수 대체 패턴(match 뒤의 텍스트)으로 해석됨. 이로 인해 `</body>` 뒤의 원본 HTML 구조(`<!-- Startup -->`, `<script src="workbench.js">`, `</html>`)가 JS 코드 중간(정규식 리터럴 안)에 삽입되어 **치명적 SyntaxError** 발생
- **해결**: `inlineBlock.replace(/\$/g, '$$$$')`로 모든 `$`를 이스케이프한 후 replacement 문자열로 사용 (v0.5.38, `d6ed876`)
- **주의**: `String.prototype.replace()`의 replacement 문자열에서 `$&`, `$'`, `` $` ``, `$1` 등은 특수 패턴. 동적 콘텐츠를 replacement로 사용할 때 반드시 `$` → `$$` 이스케이프 필요
### [2026-04-12] [Extension] V8 CachedData가 Observer 스크립트 로딩을 차단
- **증상**: html-patcher가 workbench-jetski-agent.html에 observer v7 인라인 스크립트를 성공적으로 삽입했지만, deep-inspect가 렌더러에서 응답 없음 (10s timeout). AG 재시작 후에도 observer가 로드되지 않음
- **원인**: `%APPDATA%\Antigravity\CachedData\` (50MB)에 V8 캐시가 남아있어, AG가 패치된 HTML 대신 캐시된 이전 버전을 로드. extension.log에 `patcher.install() called (needs reload)` 메시지가 표시되지만 실제 적용이 안 됨
- **해결**: `Remove-Item "$env:APPDATA\Antigravity\CachedData\*" -Recurse -Force` 실행 후 AG 재시작. known-issues-archive #6에도 동일 케이스 있음
- **주의**: HTML 패치 변경 시 **반드시** V8 CachedData 삭제 + AG 재시작 필요. 단순 AG 재시작만으로는 부족
### [2026-04-12] [DOM] text-ide-message-block-bot-color는 AI 응답 컨테이너가 아닌 NUX tooltip 전용
- **증상**: observer-script가 `.text-ide-message-block-bot-color`를 AI 응답 컨테이너로 사용하지만, 실제 AI 텍스트를 추출하지 못함
- **원인**: 번들 분석(jetskiAgent/main.js 10.8MB)으로 확인 결과, 이 클래스는 `hsn` 컴포넌트(NUX Tooltip 텍스트 색상)에서만 사용. AI 응답 텍스트는 `plannerResponse` step의 `Whi` 렌더러 → `div.px-2.py-1``MarkdownRenderer` 내부에 렌더링됨
- **해결**: observer-script에서 `.text-ide-message-block-bot-color` 의존성 제거 필요. `markdown-body` 클래스도 AG Native에 존재하지 않음
- **주의**: AI 응답 마크다운은 `prose` 관련 클래스나 MarkdownRenderer 내부 구조로 타겟팅해야 함. 실제 DOM 덤프로 정확한 셀렉터 확인 필요
### [2026-04-12] [SDK/DOM] AG Native 세션은 Cascade SDK API에 등록되지 않음 — DOM이 유일한 데이터 소스
- **증상**: AG Native 세션에서 Discord 릴레이로 AI 응답이 전혀 전달되지 않고, 대신 UI 노이즈(`content_copy`, `Always run`, `keyboard_arrow_up`, `Cancel`)가 전송됨
- **원인 1 (SDK)**: `GetCascadeTrajectorySteps(cascadeId=세션ID)``500 trajectory not found`. `GetDiagnostics``404`. AG Native 세션은 Cascade trajectory API에 전혀 등록되지 않는 별도 시스템
- **원인 2 (DOM)**: `observer-script.ts` v6의 `scanChatBodies()``.text-ide-message-block-bot-color` 컨테이너의 `textContent`를 통째로 가져오면서 내부 버튼/아이콘 텍스트까지 포함
- **해결**: `observer-script.ts` v7로 전면 재설계:
1. `[data-testid="conversation-view"]` + `[data-step-index]` 기반 step-aware 파싱
2. `extractCleanStepText()`: 클론 후 button/svg/icon 엘리먼트 제거 → 마크다운 텍스트만 추출
3. `extractStepContext()`: `getStepContainer()` → step 헤더 + code 블록만 추출
4. `NOISE_RE`: Material icon 이름, 버튼 레이블, UI 텍스트 전면 차단
5. 최초 `conversation-view` 감지 시 DOM 구조 자동 덤프 (`/dump-html`)
- **주의**: SDK 경로(step-probe RT-CAPTURE)는 AG Native에서 사용 불가. DOM이 유일한 콘텐츠 소스이므로 AG UI 업데이트 시 `data-testid`/`data-step-index` 속성 존재 여부 반드시 확인 필요
### [2026-04-09] [Bridge] Discord Body Content Missing Due to Step Probe Dummy Payload ### [2026-04-09] [Bridge] Discord Body Content Missing Due to Step Probe Dummy Payload
- **증상**: 대규모 UI 마이그레이션 후, 디스코드 승인 메시지 본문에 실행할 코드/명령어가 완전히 누락되고 "Step #15"와 같은 디폴트 텍스트만 전송됨. - **利앹긽**: <EFBFBD><EFBFBD><EFBFBD>洹쒕え UI 留덉씠洹몃젅<EBAA83><EFBFBD><20>썑, <20><EFBFBD>뒪肄붾뱶 <20><EFBFBD>씤 硫붿떆吏<EB9686> 蹂몃Ц<EBAA83><20><EFBFBD><EFBFBD>븷 肄붾뱶/紐낅졊<EB8285>뼱媛<EBBCB1> <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>릺怨<EBA6BA> "Step #15"<22><><EFBFBD> 媛숈<E5AA9B><EC8888> <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>듃留<EB9383> <20><EFBFBD><EFBFBD>.
- **원인**: Native UI 변경으로 인해 DOM observer가 추출한 버튼 텍스트("Always run") `http-bridge.ts` 필터 우회 및 bot.py에서 지연(defer) 처리됨. 반면 `step-probe.ts` `GetAllCascadeTrajectories` 폴링을 통해 동시에 발생시킨 dummy pending payload (명령어 상세 내용이 없이 `Step #XX` 라는 텍스트만 포함)가 봇에 의해 먼저 자동 승인되면서 정작 실제 코드 영역 정보가 증발함. - **<EFBFBD><EFBFBD>**: Native UI <EFBFBD>寃쎌쑝濡<EFBFBD> <20><EFBFBD> DOM observer<EFBFBD> 異붿텧<EBB6BF>븳 踰꾪듉 <20><EFBFBD><EFBFBD>("Always run")<EFBFBD> `http-bridge.ts` <EFBFBD><EFBFBD><20><EFBFBD>쉶 諛<> bot.py<70><EFBFBD>꽌 吏<><EFA79E>뿰(defer) 泥섎━<EC848E>맖. 諛섎㈃ `step-probe.ts`<EFBFBD> `GetAllCascadeTrajectories` <EFBFBD>뤃留곸쓣 <20><EFBFBD><20><EFBFBD><EFBFBD>뿉 諛쒖깮<EC9296><EFBFBD>궓 dummy pending payload (紐낅졊<EB8285><20><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD>`Step #XX` <20><EFBFBD><20><EFBFBD><EFBFBD>듃留<EB9383> <20><EFBFBD>븿)媛<> 遊뉗뿉 <20><EFBFBD>빐 癒쇱<E79992><EC87B1> <20><EFBFBD><20><EFBFBD><EFBFBD>릺硫댁꽌 <20><EFBFBD><20><EFBFBD>젣 肄붾뱶 <20><EFBFBD><20>젙蹂닿<E8B982><EB8BBF> 利앸컻<EC95B8>븿.
- **해결**: `step-probe.ts` 내에 `formatStepProbeCommand` 헬퍼 함수를 추가하여, WAITING 상태 스텝의 `argumentsJson` 데이터를 직접 파싱하고 `CommandLine`, `TargetFile` 등 실제 명령어와 상세 인자/코드를 `command` `description`으로 할당하여 브릿지로 넘기도록 패치함. DOM 옵저버의 불안정성과 관계없이 일관된 본문 전달 보장. - **<EFBFBD>빐寃<EFBFBD>**: `step-probe.ts` <EFBFBD><EFBFBD> `formatStepProbeCommand` <EFBFBD><EFBFBD><20>븿<EFBFBD>닔瑜<EB8B94> 異붽<E795B0><EBB6BD><EFBFBD><EFBFBD>뿬, WAITING <20><EFBFBD><20><EFBFBD><EFBFBD>`argumentsJson` <20><EFBFBD><EFBFBD>꽣瑜<EABDA3> 吏곸젒 <20><EFBFBD><EFBFBD>븯怨<EBB8AF> `CommandLine`, `TargetFile` <EFBFBD><20><EFBFBD>젣 紐낅졊<EB8285><EFBFBD><EBBCB1><EFBFBD> <20><EFBFBD><20><EFBFBD>옄/肄붾뱶瑜<EBB1B6> `command`<EFBFBD><EFBFBD><EFBFBD> `description`<EFBFBD>쑝濡<EFBFBD> <20><EFBFBD><EFBFBD><EFBFBD>뿬 釉뚮┸吏<E294B8><EFBFBD> <20>꽆湲곕룄濡<EBA384> <20>뙣移섑븿. DOM <20><EFBFBD><EC8383><EFBFBD>踰꾩쓽 遺덉븞<EB8D89><EFBFBD>꽦怨<EABDA6><>怨꾩뾾<EABEA9><20>씪愿<EC94AA><E684BF>맂 蹂몃Ц <20><EFBFBD>떖 蹂댁옣.
- **주의**: UI 스크래핑에 의존하는 DOM Observer 방식은 UI 레이아웃, 아이콘 삽입 등에 취약하므로, 상세 페이로드 추출은 항상 100% 신뢰 가능한 SDK RPC(`step-probe.ts`) 데이터를 우선 사용하도록 구성해야 함. - **二쇱쓽**: UI <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><20>쓽議댄븯<EB8C84>뒗 DOM Observer 諛⑹떇<E291B9><EB9687><EFBFBD> UI <20><EFBFBD><EFBFBD><EFBFBD>썐, <20><EFBFBD>씠肄<EC94A0> <20><EFBFBD><20><EFBFBD>뿉 痍⑥빟<E291A5>븯誘<EBB8AF><EFBFBD>, <20><EFBFBD><20><EFBFBD>씠濡쒕뱶 異붿텧<EBB6BF><ED85A7><EFBFBD> <20><EFBFBD>긽 100% <20>떊猶<EB968A><><E5AA9B><EFBFBD>븳 SDK RPC(`step-probe.ts`) <20><EFBFBD><EFBFBD>꽣瑜<EABDA3> <20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD>룄濡<EBA384> 援ъ꽦<D18A><EFBFBD><20>븿.
## <20>룷留<EBA3B7>
## 포맷
### [2026-03-23] [Extension] Cross-Project DOM Observer Leakage ### [2026-03-23] [Extension] Cross-Project DOM Observer Leakage
- **증상**: 다중 원격 컴퓨터에서 동일한 프로젝트명으로 실행된 VS Code들이 서로의 `execute JavaScript` (Allow) 승인 신호를 가로채거나 엉뚱한 서버로 보냄.
- **원인**: Extension이 `workbench.html`에 스크립트를 주입할 때 결정론적 포트를 하드코딩했는데, 전역 캐시된 HTML 파일을 모든 로컬/원격 연결이 공유하면서 마지막에 열린 프로젝트의 포트 번호로 덮어씌워짐.
- **해결**: `extension.ts`에서 상태 표시줄(Status Bar) `tooltip`에 포트를 주입하고, `observer-script.ts`에서 DOM 쿼리를 통해 동적으로 자신의 창(Window)에 할당된 포트를 찾아내도록 수정. `vscode.env.asExternalUri`를 사용하여 포트 충돌 시 우회된 주소까지 로컬 포워딩에 매핑되도록 지원.
- **주의**: VS Code UI 코어(HTML) 패치 시, 여러 창(Window)이나 다중 원격 접속 시 환경(Scope) 분리에 각별한 주의가 필요함. 전역 자원에 의존하는 하드코딩 지양.
### [날짜] [키워드] — 한줄 요약 - **利앹긽**: <20>떎以<EB968E> <20>썝寃<EC8D9D> 而댄벂<EB8C84><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><20>봽濡쒖젥<EC9296>듃紐낆쑝濡<EC919D> <20><EFBFBD><EFBFBD>맂 VS Code<64><EFBFBD><20>꽌濡쒖쓽 `execute JavaScript` (Allow) <20><EFBFBD><20><EFBFBD>샇瑜<EC8387><>濡쒖콈嫄곕굹 <20><EFBFBD><EFBFBD><20>꽌踰꾨줈 蹂대깂.
- **증상**: 무엇이 잘못되었는가
- **원인**: 근본 원인 - **<EFBFBD><EFBFBD>씤**: Extension<6F>`workbench.html`<EFBFBD><20><EFBFBD>겕由쏀듃瑜<EB9383> 二쇱엯<EC87B1><20>븣 寃곗젙濡좎쟻 <20><EFBFBD>듃瑜<EB9383> <20><EFBFBD>뱶肄붾뵫<EBB6BE><EFBFBD><EFBFBD>뜲, <20><EFBFBD>뿭 罹먯떆<EBA8AF>맂 HTML <20><EFBFBD><EFBFBD>쓣 紐⑤뱺 濡쒖뺄/<2F>썝寃<EC8D9D> <20>뿰寃곗씠 怨듭쑀<EB93AD>븯硫댁꽌 留덉<EFA78D><EB8D89>留됱뿉 <20>뿴由<EBBFB4> <20>봽濡쒖젥<EC9296><EFBFBD><20><EFBFBD>듃 踰덊샇濡<EC8387> <20><EFBFBD><EFBFBD><EFBFBD>썙吏<EC8D99>.
- **해결**: 올바른 해결 방법
- **주의**: 재발 방지를 위한 교훈 - **<EFBFBD>빐寃<EFBFBD>**: `extension.ts`<EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD>떆以<EB9686>(Status Bar) `tooltip`<EFBFBD><20><EFBFBD>듃瑜<EB9383> 二쇱엯<EC87B1>븯怨<EBB8AF>, `observer-script.ts`<EFBFBD><EFBFBD>꽌 DOM 荑쇰━瑜<E29481> <20><EFBFBD><20><EFBFBD><EFBFBD>쑝濡<EC919D> <20><EFBFBD><EFBFBD>쓽 李<>(Window)<29><20><EFBFBD><EFBFBD><20><EFBFBD>듃瑜<EB9383> 李얠븘<EC96A0><EFBFBD>룄濡<EBA384> <20><EFBFBD>젙. `vscode.env.asExternalUri`<EFBFBD> <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>듃 異⑸룎 <20><20><EFBFBD><EFBFBD>맂 二쇱냼源뚯<E6BA90><EB9AAF> 濡쒖뺄 <20><EFBFBD><EFBFBD><EFBFBD>뿉 留ㅽ븨<E385BD><EFBFBD>룄濡<EBA384><><EFA79E>썝.
- **二쇱쓽**: VS Code UI 肄붿뼱(HTML) <20>뙣移<EB99A3> <20>떆, <20><EFBFBD>윭 李<>(Window)<29><EFBFBD><20>떎以<EB968E> <20>썝寃<EC8D9D> <20><EFBFBD><20><20>솚寃<EC869A>(Scope) 遺꾨━<EABEA8>뿉 媛곷퀎<EAB3B7>븳 二쇱쓽媛<EC93BD> <20><EFBFBD><EFBFBD>븿. <20><EFBFBD><20><EFBFBD><EFBFBD><20>쓽議댄븯<EB8C84><20><EFBFBD>뱶肄붾뵫 吏<><EFA79E>뼇.
### [<5B>궇吏<EAB687>] [<5B><EFBFBD><EFBFBD>뱶] <20><><EFBFBD> <20>븳以<EBB8B3> <20><EFBFBD>
- **利앹긽**: 臾댁뾿<EB8C81><20>옒紐삳릺<EC82B3><EFBFBD>뒗媛<EB9297>
- **<2A><EFBFBD>씤**: 洹쇰낯 <20><EFBFBD>
- **<2A>빐寃<EBB990>**: <20>삱諛붾Ⅸ <20>빐寃<EBB990> 諛⑸쾿
- **二쇱쓽**: <20>옱諛<EC98B1> 諛⑹<E8AB9B><E291B9><EFBFBD> <20><EFBFBD>븳 援먰썕
``` ```
--- ---
### [2026-04-08] [Discord Bot] Channel Deletion Cache Desync ### [2026-04-08] [Discord Bot] Channel Deletion Cache Desync
- **증상**: 봇이 켜져 있는 상태에서 Discord 채널(g-project-name)을 삭제하면, 봇이 삭제를 인지하지 못하고 새 채널을 생성하지 않으며 메시지도 증발함.
- **원인**: ot.py의 self.project_channels 딕셔너리에 채널 객체가 캐시되어 있어, API 호출 없이 캐시된(삭제된) 채널로 메시지를 보내려 시도하다 404 에러 발생 후 실패함. - **利앹긽**: 遊뉗씠 耳쒖졇 <20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD>꽌 Discord 梨꾨꼸(g-project-name)<29><20><EFBFBD><EFBFBD>븯硫<EBB8AF>, 遊뉗씠 <20><EFBFBD>젣瑜<ECA0A3> <20>씤吏<EC94A4><EFA79E>븯吏<EBB8AF> 紐삵븯怨<EBB8AF> <20>깉 梨꾨꼸<EABEA8><20><EFBFBD><EFBFBD>븯吏<EBB8AF> <20><EFBFBD>쑝硫<EC919D> 硫붿떆吏<EB9686><EFA79E>룄 利앸컻<EC95B8>븿.
- **해결**: 채널 맵핑이 꼬였을 때는 **Python 봇(Docker 컨테이너)을 재시작**하여 캐시를 초기화하고 채널 목록을 새로 갱신하게 함.
- **주의**: 채널 관리는 캐시에 의존하기 때문에 강제로 Discord UI에서 채널을 지웠을 때는 반드시 봇을 재구동해야 함. - **<EFBFBD><EFBFBD>씤**: ot.py<70>쓽 self.project_channels <20><EFBFBD><EFBFBD>꼫由ъ뿉 梨꾨꼸 媛앹껜媛<EABB9C> 罹먯떆<EBA8AF><EFBFBD><20><EFBFBD>뼱, API <20>샇異<EC8387> <20><EFBFBD>씠 罹먯떆<EBA8AF>맂(<28><EFBFBD><EFBFBD>맂) 梨꾨꼸濡<EABCB8> 硫붿떆吏<EB9686><EFBFBD> 蹂대궡<EB8C80><20><EFBFBD><EFBFBD><EFBFBD>떎 404 <20><EFBFBD>윭 諛쒖깮 <20><20><EFBFBD><EFBFBD>븿.
- **<2A>빐寃<EBB990>**: 梨꾨꼸 留듯븨<EB93AF>씠 瑗ъ<E79197><D18A><EFBFBD><20><EFBFBD>뒗 **Python 遊<>(Docker 而⑦뀒<E291A6><EFBFBD>꼫)<29><20><EFBFBD><EFBFBD>옉**<2A><EFBFBD>뿬 罹먯떆瑜<EB9686> 珥덇린<EB8D87><EFBFBD>븯怨<EBB8AF> 梨꾨꼸 紐⑸줉<E291B8><20>깉濡<EAB989> 媛깆떊<EAB986>븯寃<EBB8AF> <20>븿.
- **二쇱쓽**: 梨꾨꼸 愿<>由щ뒗 罹먯떆<EBA8AF><20>쓽議댄븯湲<EBB8AF> <20>븣臾몄뿉 媛뺤젣濡<ECA0A3> Discord UI<55><EFBFBD>꽌 梨꾨꼸<EABEA8>쓣 吏<><EFA79E><EFBFBD><20><EFBFBD>뒗 諛섎뱶<EC848E>떆 遊뉗쓣 <20>옱援щ룞<D189><EFBFBD><20>븿.
### [2026-04-08] [Extension] Multiple Workspace LS Cross-Connection ### [2026-04-08] [Extension] Multiple Workspace LS Cross-Connection
- **증상**: ariet-llm 창에서 켰으나 gravity_control의 백그라운드 구동 중인 LS에 연결되어 자기 자신 창의 신호를 잡지 못함.
- **원인**: 여러 VS Code 창을 띄웠을 때 어떤 창에서는 Antigravity 패널을 누르지 않아 전용 LS가 시작되지 않음. ixLSConnection()이 자기 몫의 LS를 찾지 못하고 fallback으로 기존에 떠 있던 다른 창의 LS에 연결됨.
- **해결**: 대상 창에서 Developer: Reload Window 실행 후 **사이드바의 로컬 Antigravity 챗봇 패널을 한 번 열어** 자신의 LS 프로세스를 띄운 뒤에 Gravity Bridge를 Start함.
- **주의**: LS는 자동으로 시작되지 않고 사용자가 채팅 패널을 한 번 클릭/활성화해야만 Spawn 됨.
## 🔴 Active/Recent Issues - **利앹긽**: ariet-llm 李쎌뿉<EC8E8C>꽌 耳곗쑝<EAB397>굹 gravity_control<6F>쓽 諛깃렇<EAB983><EFBFBD><EFBFBD>뱶 援щ룞 以묒씤 LS<4C><20>뿰寃곕릺<EAB395><20>옄湲<EC9884> <20><EFBFBD>떊 李쎌쓽 <20><EFBFBD>샇瑜<EC8387> <20>옟吏<EC989F> 紐삵븿.
- **<2A><EFBFBD>씤**: <20><EFBFBD>윭 VS Code 李쎌쓣 <20><EFBFBD><EFBFBD><20><20><EFBFBD>뼡 李쎌뿉<EC8E8C><EFBFBD>뒗 Antigravity <20><EFBFBD><EFBFBD><20>늻瑜댁<E7919C><EB8C81> <20><EFBFBD><20><EFBFBD>슜 LS媛<53> <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>쓬. ixLSConnection()<29><20>옄湲<EC9884> 紐レ쓽 LS瑜<53> 李얠<EFA7A1><EC96A0> 紐삵븯怨<EBB8AF> fallback<63>쑝濡<EC919D> 湲곗〈<EAB397><20><20><EFBFBD><20>떎瑜<EB968E> 李쎌쓽 LS<4C><20>뿰寃곕맖.
- **<2A>빐寃<EBB990>**: <20><><EFBFBD><EFBFBD>긽 李쎌뿉<EC8E8C>꽌 Developer: Reload Window <20><EFBFBD><20>썑 **<2A><EFBFBD><EFBFBD>뱶諛붿쓽 濡쒖뺄 Antigravity 梨쀫큸 <20><EFBFBD><EFBFBD><20>븳 踰<> <20><EFBFBD>뼱** <20><EFBFBD><EFBFBD>쓽 LS <20>봽濡쒖꽭<EC9296>뒪瑜<EB92AA> <20><EFBFBD><20><EFBFBD>뿉 Gravity Bridge瑜<65> Start<72>븿.
- **二쇱쓽**: LS<4C><20><EFBFBD><EFBFBD>쑝濡<EC919D> <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20>븡怨<EBB8A1> <20><EFBFBD><EFBFBD>옄媛<EC9884> 梨꾪똿 <20><EFBFBD><EFBFBD><20>븳 踰<> <20>겢由<EAB2A2>/<2F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>빞留<EBB99E> Spawn <20>맖.
## <20><EFBFBD> Active/Recent Issues
### [2026-04-09] [Extension] Agent UI Native Migration & Icon Text Gluing ### [2026-04-09] [Extension] Agent UI Native Migration & Icon Text Gluing
- **증상**: UI Tailwind/Native 마이그레이션 및 아이콘 적용 후, Discord 브릿지로 신호가 전송되지 않음.
- **원인**: 네이티브 UI 버튼의 `textContent` 추출 시, Codicons 등 아이콘 폰트 문자열(e.g., ` Accept`)이 앞부분에 병합(Gluing)되면서, 기존의 `^` 앵커가 포함된 정규식 매칭(`/^(?:Always\s*)?Run/i`)이 실패함. - **利앹긽**: UI Tailwind/Native 留덉씠洹몃젅<EBAA83><EFBFBD>뀡 諛<> <20><EFBFBD>씠肄<EC94A0> <20><EFBFBD><20>썑, Discord 釉뚮┸吏<E294B8><EFBFBD> <20><EFBFBD>샇媛<EC8387> <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>.
- **해결**: `observer-script.ts`의 스캔, Sibling 버튼 수집, Webview Trigger-click 등 `textContent`를 추출하는 모든 DOM 읽기 구간에 `txt.replace(/^[^a-zA-Z0-9]+/, '')` 전처리를 적용하여 선행 기호/아이콘을 안전하게 제거.
- **주의**: Native UI 컴포넌트 환경에서는 텍스트 노드뿐만 아니라 아이콘/SVG 컴포넌트의 텍스트 글루잉 현상으로 인해 엄격한 시작점(`^`) 정규식이 깨질 수 있으므로, 항상 불필요한 특수문자 전처리를 선행해야 함. - **<EFBFBD><EFBFBD>씤**: <20><EFBFBD><EFBFBD>떚釉<EB969A> UI 踰꾪듉<EABEAA>쓽 `textContent` 異붿텧 <20>떆, Codicons <20><20><EFBFBD>씠肄<EC94A0> <20><EFBFBD>듃 臾몄옄<EBAA84>뿴(e.g., `<EFBFBD> Accept`)<29><20>븵遺<EBB8B5>遺꾩뿉 蹂묓빀(Gluing)<29>릺硫댁꽌, 湲곗〈<EAB397>쓽 `^` <20>빑而ㅺ<E8808C><E385BA> <20><EFBFBD>븿<EFBFBD><20>젙洹쒖떇 留ㅼ묶(`/^(?:Always\s*)?Run/i`)<29><20><EFBFBD><EFBFBD>븿.
- **<2A>빐寃<EBB990>**: `observer-script.ts`<60><20>뒪罹<EB92AA>, Sibling 踰꾪듉 <20>닔吏<EB8B94>, Webview Trigger-click <20>벑 `textContent`瑜<> 異붿텧<EBB6BF><EFBFBD>뒗 紐⑤뱺 DOM <20>씫湲<EC94AB> 援ш컙<D188>뿉 `txt.replace(/^[^a-zA-Z0-9]+/, '')` <20>쟾泥섎━瑜<E29481> <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>뻾 湲고샇/<2F><EFBFBD>씠肄섏쓣 <20><EFBFBD><EFBFBD>븯寃<EBB8AF> <20>젣嫄<ECA0A3>.
- **二쇱쓽**: Native UI 而댄룷<EB8C84><EFBFBD><20>솚寃쎌뿉<EC8E8C><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD>뱶肉먮쭔 <20><EFBFBD><EFBFBD><20><EFBFBD>씠肄<EC94A0>/SVG 而댄룷<EB8C84><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>듃 湲<>猷⑥엵 <20><EFBFBD><EFBFBD>쑝濡<EC919D> <20><EFBFBD><20>뾼寃⑺븳 <20><EFBFBD><EFBFBD>젏(`^`) <20>젙洹쒖떇<EC9296>씠 源⑥쭏 <20><20><EFBFBD>쑝誘<EC919D><EFBFBD>, <20><EFBFBD>긽 遺덊븘<EB8D8A><EFBFBD><20><EFBFBD>닔臾몄옄 <20>쟾泥섎━瑜<E29481> <20><EFBFBD><EFBFBD><EFBFBD><20>븿.
### [2026-04-09] [Extension] Agent UI Native Migration & CodeLens False Positive Filter ### [2026-04-09] [Extension] Agent UI Native Migration & CodeLens False Positive Filter
- **증상**: UI Tailwind/Native 마이그레이션 적용 후, Discord 브릿지로 신호가 전혀 전송되지 않음
- **원인**: Agent 패널이 탭/에디터 본문에 직접 렌더링되면서, 기존 오작동 방지 로직(`if (b.closest('.monaco-editor'))`)에 패널 전체 버튼이 포착되어 무시됨 - **利앹긽**: UI Tailwind/Native 留덉씠洹몃젅<EBAA83><EFBFBD><20><EFBFBD><20>썑, Discord 釉뚮┸吏<E294B8><EFBFBD> <20><EFBFBD>샇媛<EC8387> <20><EFBFBD><EC9FBE><EFBFBD> <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>
- **해결**: 너무 광범위한 `.monaco-editor` 방어를 해제하고, 코드 렌즈 고유 컨테이너인 `.codelens-decoration` 내부일 경우에만 무시하도록 핀포인트 수정
- **주의**: DOM 옵저버 필터 조건 작성 시 래퍼 클래스는 UI 디자인 개편(Native, Editor Tab 등 위치 변경)에 매우 취약함. 가장 구체적인 내부 노드 클래스나 타겟 고유 속성을 통해 필터링할 것 - **<EFBFBD><EFBFBD>씤**: Agent <20><EFBFBD><EFBFBD><20>꺆/<2F><EFBFBD><EFBFBD>꽣 蹂몃Ц<EBAA83>뿉 吏곸젒 <20><EFBFBD>뜑留곷릺硫댁꽌, 湲곗〈 <20><EFBFBD><EFBFBD>룞 諛⑹<E8AB9B><E291B9> 濡쒖쭅(`if (b.closest('.monaco-editor'))`)<29><20><EFBFBD><20>쟾泥<EC9FBE> 踰꾪듉<EABEAA><20>룷李⑸릺<E291B8>뼱 臾댁떆<EB8C81>
- **<2A>빐寃<EBB990>**: <20>꼫臾<EABCAB> 愿묐쾾<EBAC90><EFBFBD>븳 `.monaco-editor` 諛⑹뼱瑜<EBBCB1> <20><EFBFBD><EFBFBD>븯怨<EBB8AF>, 肄붾뱶 <20>젋利<ECA08B> 怨좎쑀 而⑦뀒<E291A6><EFBFBD><EFBFBD>씤 `.codelens-decoration` <20>궡遺<EAB6A1><E981BA>씪 寃쎌슦<EC8E8C>뿉留<EBBF89> 臾댁떆<EB8C81><EFBFBD>룄濡<EBA384> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>
- **二쇱쓽**: DOM <20><EFBFBD><EC8383><EFBFBD><EFBFBD> <20><EFBFBD>꽣 議곌굔 <20><EFBFBD><20><20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD>뒗 UI <20><EFBFBD><EFBFBD>씤 媛쒗렪(Native, Editor Tab <20><20>쐞移<EC909E><><EFBFBD>)<29>뿉 留ㅼ슦 痍⑥빟<E291A5>븿. 媛<><E5AA9B>옣 援ъ껜<D18A><EFBFBD><20>궡遺<EAB6A1> <20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><20><><EFBFBD><EFBFBD> 怨좎쑀 <20><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD>꽣留곹븷 寃<>
### [2026-03-31] [step-probe] GetAllCascadeTrajectories 10-Item Hard Limit (Signal Drop) ### [2026-03-31] [step-probe] GetAllCascadeTrajectories 10-Item Hard Limit (Signal Drop)
- **증상**: `guitar_score` 등에서 활성화된 세션의 디스코드 승인 신호를 "계속해서" 잡지 못함. (WS 60초 타임아웃보다 더 치명적으로 신호가 아예 가지 않음)
- **원인**: Extension이 활성 세션을 찾기 위해 호출하는 `GetAllCascadeTrajectories` LS API가 `{}`(빈 인자)로 호출될 때, 기본적으로 **10개의 세션만 반환하는 하드 리밋(Pagination Limit)**이 걸려있음. 이로 인해 작업 내역이 누적되면 수많은 최신/진행 중 세션들이 10개 목록에서 밀려나 누락됨. 익스텐션은 세션이 없다고 판단해 강제로 `IDLE` 모드에 진입하며, 승인 대기열(WAITING) 자체를 검사하지 않게 됨. - **利앹긽**: `guitar_score` <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD>뒪肄붾뱶 <20><EFBFBD><20><EFBFBD>샇瑜<EC8387> "怨꾩냽<EABEA9><EFBFBD>꽌" <20>옟吏<EC989F> 紐삵븿. (WS 60珥<30> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>썐蹂대떎 <20>뜑 移섎챸<EC848E><EFBFBD>쑝濡<EC919D> <20><EFBFBD>샇媛<EC8387> <20><EFBFBD>삁 媛<><EFBFBD> <20><EFBFBD>쓬)
- **해결** (v0.5.14): `v0.5.13`에서 도입했던 `{ limit: 100 }`이 LS 단의 쿼리 과부하로 인한 VS Code UI 프리징(DoS)을 유발하여 롤백하는 중 필수 정렬 파라미터(`descending: true`)까지 소실되었던 실수를 교정함. 최종적으로 `{ limit: 30, descending: true }`를 적용하여 파싱 부하 최소화 및 최신 세션 최상단(Index 0) 조회를 안전하게 구현함.
- **주의**: LS의 기본 SQLite/DB 응답 Limit 규칙에 의존하여 전체 데이터 스캔을 수행하는 로직은 언제든 Truncation 이슈(Data Loss)를 유발할 수 있음. - **<EFBFBD><EFBFBD>씤**: Extension<6F><20><EFBFBD><20><EFBFBD><EFBFBD>쓣 李얘린 <20><EFBFBD><20>샇異쒗븯<EC9297>뒗 `GetAllCascadeTrajectories` LS API媛<49> `{}`(鍮<> <20><EFBFBD>옄)濡<> <20>샇異쒕맆 <20>븣, 湲곕낯<EAB395><EFBFBD>쑝濡<EC919D> **10媛쒖쓽 <20><EFBFBD>뀡留<EB80A1> 諛섑솚<EC8491><EFBFBD><20><EFBFBD>뱶 由щ컠(Pagination Limit)**<2A>씠 嫄몃젮<EBAA83><EFBFBD>쓬. <20>씠濡<EC94A0> <20><EFBFBD><20><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>릺硫<EBA6BA> <20>닔留롮<EFA78D><EBA1AE> 理쒖떊/吏꾪뻾 以<> <20><EFBFBD><EFBFBD><EFBFBD>씠 10媛<30> 紐⑸줉<E291B8><EFBFBD>꽌 諛<><E8AB9B><EFBFBD><20><EFBFBD><EFBFBD>맖. <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EB80A1><EFBFBD> <20><EFBFBD><EFBFBD><20><EFBFBD>떎怨<EB968E> <20><EFBFBD><EFBFBD>빐 媛뺤젣濡<ECA0A3> `IDLE` 紐⑤뱶<E291A4>뿉 吏꾩엯<EABEA9>븯硫<EBB8AF>, <20><EFBFBD><20><><EFBFBD>湲곗뿴(WAITING) <20>옄泥대<EFA7A3><EB8C80><><E5AF83><EFBFBD>븯吏<EBB8AF> <20>븡寃<EBB8A1> <20>.
- **<2A>빐寃<EBB990>** (v0.5.14): `v0.5.13`<60><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD>뜕 `{ limit: 100 }`<60>씠 LS <20><EFBFBD>쓽 荑쇰━ 怨쇰<E680A8><EC87B0><EFBFBD>븯濡<EBB8AF> <20><EFBFBD>븳 VS Code UI <20>봽由ъ쭠(DoS)<29><20>쑀諛쒗븯<EC9297>뿬 濡ㅻ갚<E385BB><EFBFBD>뒗 以<> <20><EFBFBD><20><EFBFBD><20><EFBFBD>씪誘명꽣(`descending: true`)源뚯<E6BA90><EB9AAF> <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>닔瑜<EB8B94> 援먯젙<EBA8AF>븿. 理쒖쥌<EC9296><EFBFBD>쑝濡<EC919D> `{ limit: 30, descending: true }`瑜<> <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>떛 遺<><E981BA>븯 理쒖냼<EC9296>솕 諛<> 理쒖떊 <20><EFBFBD>뀡 理쒖긽<EC9296>떒(Index 0) 議고쉶瑜<EC89B6> <20><EFBFBD><EFBFBD>븯寃<EBB8AF> 援ы쁽<D18B>븿.
- **二쇱쓽**: LS<4C>쓽 湲곕낯 SQLite/DB <20><EFBFBD>떟 Limit 洹쒖튃<EC9296><20>쓽議댄븯<EB8C84><20>쟾泥<EC9FBE> <20><EFBFBD><EFBFBD><20>뒪罹붿쓣 <20><EFBFBD><EFBFBD><EFBFBD>뒗 濡쒖쭅<EC9296><ECAD85><EFBFBD> <20><EFBFBD><EFBFBD>뱺 Truncation <20><EFBFBD>뒋(Data Loss)瑜<> <20>쑀諛쒗븷 <20><20><EFBFBD>쓬.
### [2026-03-31] [WS] Browser API Fallback 60s Timeout (Zombie Connection) ### [2026-03-31] [WS] Browser API Fallback 60s Timeout (Zombie Connection)
- **증상**: `guitar_score` 등 모든 작업 환경에서 약 60초마다 WebSocket 연결이 끊기고 재연결되는 현상이 반복되며(extension.log에 `Heartbeat timeout` 계속 출력), 그 사이 디스코드 승인 신호를 놓침.
- **원인**: Extension이 `ws` 모듈 로드 실패(VS Code 환경 등)로 인해 브라우저 내장 `WebSocket` 객체로 Fallback 됨. 브라우저 WS는 서버의 네이티브 ping을 받아 pong을 자동 응답하지만 JS에 이벤트를 노출하지 않음. 이로 인해 `lastPongTime` 갱신이 불가능해져, `Date.now() - lastPongTime > 60000` 조건이 무조건 통과되어 멀쩡한 연결을 강제 종료함 (False Positive).
- **해결** (v0.5.12):
1. `hub.py`: `{"type": "heartbeat"}` JSON 메시지 수신 시 명시적으로 `{"type": "pong"}` JSON을 응답하도록 수정.
2. `ws-client.ts`: 명시적 `pong` 핸들러 추가. JSON pong 지원 서버거나 Node.js ws를 사용할 때만 60초 타임아웃 검증을 거치도록 조건 보강 (`forceHeartbeatTimeoutIfNoPong`).
- **주의**: 브라우저 표준 WebSockets(W3C)는 ping/pong 제어 프레임을 JS로 노출하지 않음. 폴리필/크로스플랫폼 WS 래퍼 사용 시 하트비트는 반드시 JSON 메세지 형태의 Application Layer Ping/Pong으로 풀어내거나, Native WS API 여부를 확실히 체크해야 함.
### [2026-03-28] [step-probe] GetCascadeTrajectorySteps UTF-8 에러 무한 루프 - **利앹긽**: `guitar_score` <20>벑 紐⑤뱺 <20><EFBFBD><20>솚寃쎌뿉<EC8E8C><20>빟 60珥덈쭏<EB8D88>떎 WebSocket <20>뿰寃곗씠 <20>걡湲곌퀬 <20><EFBFBD>뿰寃곕릺<EAB395><20><EFBFBD><EFBFBD>씠 諛섎났<EC848E>릺硫<EBA6BA>(extension.log<6F>뿉 `Heartbeat timeout` 怨꾩냽 異쒕젰), 洹<> <20><EFBFBD><20><EFBFBD>뒪肄붾뱶 <20><EFBFBD><20><EFBFBD>샇瑜<EC8387> <20>넃移<EB8483>.
- **증상**: `guitar_score` 프로젝트에서 `[STEP-PROBE] error: ...invalid UTF-8` 에러가 5초마다 반복되며 Discord 승인 신호가 전달되지 않음.
- **원인**: AG LS 서버에서 특정 step의 `CortexStepEphemeralMessage.content`에 바이너리 데이터(이미지 등) 포함 → proto UTF-8 직렬화 500 에러. `catch(e)` 블록에서 `stallProbed=true`를 설정하지 않아 `!ctx.stallProbed` 조건이 항상 true → 5초마다 동일 요청 무한 재시도. - **<EFBFBD><EFBFBD>씤**: Extension<6F>씠 `ws` 紐⑤뱢 濡쒕뱶 <20><EFBFBD>뙣(VS Code <20>솚寃<EC869A> <20>벑)濡<> <20><EFBFBD>빐 釉뚮씪<EB9AAE><EFBFBD><EC8AA6><EFBFBD> <20><EFBFBD>옣 `WebSocket` 媛앹껜濡<EABB9C> Fallback <20>맖. 釉뚮씪<EB9AAE><EFBFBD><EC8AA6><EFBFBD> WS<57><20>꽌踰꾩쓽 <20><EFBFBD><EFBFBD>떚釉<EB969A> ping<6E>쓣 諛쏆븘 pong<6E><20><EFBFBD><20><EFBFBD><EFBFBD>븯吏<EBB8AF><EFBFBD> JS<4A><20>씠踰ㅽ듃瑜<EB9383> <20>끂異쒗븯吏<EBB8AF> <20><EFBFBD>쓬. <20>씠濡<EC94A0> <20><EFBFBD>빐 `lastPongTime` 媛깆떊<EAB986>씠 遺덇<E981BA><EB8D87><EFBFBD><EFBFBD><EFBFBD>졇, `Date.now() - lastPongTime > 60000` 議곌굔<EAB38C>씠 臾댁“嫄<E2809C> <20>넻怨쇰릺<EC87B0>뼱 硫<>姨≫븳 <20>뿰寃곗쓣 媛뺤젣 醫낅즺<EB8285>븿 (False Positive).
- **해결** (v0.5.11): `catch` 블록에서 UTF-8 에러 감지 시 `stepOffset=currentCount-20`으로 fallback 요청. offset도 실패 시 `stallProbed=true` 설정하여 루프 차단. `delta>0` 이벤트 발생 시 L433에서 자동 리셋.
- **주의**: `stallProbed=true`는 영구 Lock이 아님 — `delta>0` 시 자동 리셋. UTF-8 에러는 AG 서버 측 문제(이미지/바이너리 데이터가 ephemeral message에 포함)이므로 Extension에서 graceful fallback만 처리. - **<EFBFBD>빐寃<EFBFBD>** (v0.5.12):
1. `hub.py`: `{"type": "heartbeat"}` JSON 硫붿떆吏<EB9686> <20><EFBFBD><20>떆 紐낆떆<EB8286><EFBFBD>쑝濡<EC919D> `{"type": "pong"}` JSON<4F><20><EFBFBD><EFBFBD><EFBFBD>룄濡<EBA384> <20><EFBFBD>젙.
2. `ws-client.ts`: 紐낆떆<EB8286>쟻 `pong` <20><EFBFBD><EFBFBD>윭 異붽<E795B0><EBB6BD>. JSON pong 吏<><EFA79E><20>꽌踰꾧굅<EABEA7>굹 Node.js ws瑜<73> <20><EFBFBD><EFBFBD><20>븣留<EBB8A3> 60珥<30> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>썐 寃<>利앹쓣 嫄곗튂<EAB397>룄濡<EBA384> 議곌굔 蹂닿컯 (`forceHeartbeatTimeoutIfNoPong`).
- **二쇱쓽**: 釉뚮씪<EB9AAE><EFBFBD><EC8AA6><EFBFBD> <20>몴以<EBAAB4> WebSockets(W3C)<29>뒗 ping/pong <20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD>쓣 JS濡<53> <20>끂異쒗븯吏<EBB8AF> <20><EFBFBD>쓬. <20>뤃由ы븘/<2F>겕濡쒖뒪<EC9296><EFBFBD><EFBFBD>뤌 WS <20><EFBFBD><20><EFBFBD><20><20><EFBFBD>듃鍮꾪듃<EABEAA>뒗 諛섎뱶<EC848E>떆 JSON 硫붿꽭吏<EABDAD> <20><EFBFBD><EFBFBD>쓽 Application Layer Ping/Pong<6E>쑝濡<EC919D> <20><><EFBFBD><EFBFBD><EFBFBD>궡嫄곕굹, Native WS API <20>뿬遺<EBBFAC><EFBFBD> <20><EFBFBD><EFBFBD>엳 泥댄겕<EB8C84><EFBFBD><20>븿.
### [2026-03-28] [step-probe] GetCascadeTrajectorySteps UTF-8 <20><EFBFBD>윭 臾댄븳 猷⑦봽
- **利앹긽**: `guitar_score` <20>봽濡쒖젥<EC9296><EFBFBD><EFBFBD>꽌 `[STEP-PROBE] error: ...invalid UTF-8` <20><EFBFBD>윭媛<EC9CAD> 5珥덈쭏<EB8D88>떎 諛섎났<EC848E>릺硫<EBA6BA> Discord <20><EFBFBD><20><EFBFBD>샇媛<EC8387> <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>쓬.
- **<2A><EFBFBD>씤**: AG LS <20>꽌踰꾩뿉<EABEA9><20><EFBFBD>젙 step<65>쓽 `CortexStepEphemeralMessage.content`<60>뿉 諛붿씠<EBB6BF>꼫由<EABCAB> <20><EFBFBD><EFBFBD>꽣(<28>씠誘몄<E8AA98><EBAA84> <20>벑) <20><EFBFBD>븿 <20>넂 proto UTF-8 吏곷젹<EAB3B7>솕 500 <20><EFBFBD>윭. `catch(e)` 釉붾줉<EBB6BE><EFBFBD>꽌 `stallProbed=true`瑜<> <20><EFBFBD><EFBFBD>븯吏<EBB8AF> <20><EFBFBD>븘 `!ctx.stallProbed` 議곌굔<EAB38C><20><EFBFBD>긽 true <20>넂 5珥덈쭏<EB8D88><20><EFBFBD><20>슂泥<EC8A82> 臾댄븳 <20><EFBFBD><EFBFBD>룄.
- **<2A>빐寃<EBB990>** (v0.5.11): `catch` 釉붾줉<EBB6BE><EFBFBD>꽌 UTF-8 <20><EFBFBD>윭 媛먯<E5AA9B><EBA8AF> <20>떆 `stepOffset=currentCount-20`<60>쑝濡<EC919D> fallback <20>슂泥<EC8A82>. offset<65><20><EFBFBD><20>떆 `stallProbed=true` <20><EFBFBD><EFBFBD><EFBFBD>뿬 猷⑦봽 李⑤떒. `delta>0` <20>씠踰ㅽ듃 諛쒖깮 <20>떆 L433<33><EFBFBD><20><EFBFBD>룞 由ъ뀑.
- **二쇱쓽**: `stallProbed=true`<60><20>쁺援<EC81BA> Lock<63><20><EFBFBD><20><><EFBFBD> `delta>0` <20><20><EFBFBD>룞 由ъ뀑. UTF-8 <20><EFBFBD><EFBFBD>뒗 AG <20>꽌踰<EABD8C><> 臾몄젣(<28>씠誘몄<E8AA98><EBAA84>/諛붿씠<EBB6BF>꼫由<EABCAB> <20><EFBFBD><EFBFBD>꽣媛<EABDA3> ephemeral message<67><20><EFBFBD>븿)<29>씠誘<EC94A0><EFBFBD> Extension<6F><EFBFBD>꽌 graceful fallback留<6B> 泥섎━.
### [2026-03-28] [approval-handler] stepIndex 誘명솗<EBAA85><20>떆 wrong-stepIndex RPC <20>궘鍮<EAB698>
- **利앹긽**: DOM observer 寃쎈줈濡<ECA488> `terminal_command` pending <20><EFBFBD><20>썑 Discord <20><EFBFBD><20>떆 `HandleCascadeUserInteraction(stepIndex=0)` <20>넂 `"input not registered for step 0"` <20>넂 LS reconnect <20><20><EFBFBD><EFBFBD><20>넂 DOM click fallback<63>쑝濡<EC919D> <20><><EFBFBD><EFBFBD>븯. (wrong-LS<4C><53><EFBFBD> <20><EFBFBD><EFBFBD>븳 利앹긽<EC95B9><EFBFBD><20>떎瑜<EB968E> <20><EFBFBD>씤)
- **<2A><EFBFBD>씤**: `ctx.lastPendingStepIndex=-1` (step-probe媛<65> UTF-8 <20><EFBFBD>윭濡<EC9CAD> WAITING 誘멸컧吏<ECBBA7>)<29><EFBFBD><EFBFBD>룄 `Math.max(0, -1)=0`<60>쑝濡<EC919D> clamp<6D><EFBFBD>뼱 議댁옱<EB8C81>븯吏<EBB8AF> <20><EFBFBD>뒗 step 0<>뿉 RPC <20><EFBFBD>넚.
- **<2A>빐寃<EBB990>** (v0.5.11): `effectiveStepIndex = stepIndex >= 0 ? stepIndex : (lastPendingStepIndex >= 0 ? lastPendingStepIndex : -1)`. `effectiveStepIndex < 0`<60>씠硫<EC94A0> RPC 釉붾줉 <20>쟾泥<EC9FBE> skip <20>넂 DOM click 吏곹뻾 (湲곗〈怨<E38088> <20><EFBFBD><20><EFBFBD>씪, LS reconnect <20>궘鍮<EAB698> <20>젣嫄<ECA0A3>).
- **二쇱쓽**: 湲곗〈 洹쒖튃 #14(`uint32`<60><20><EFBFBD>닔 湲덉<E6B9B2><EB8D89>)<29><><EFBFBD> 異⑸룎泥섎읆 蹂댁씠<EB8C81>굹, `effectiveStepIndex=-1`<60><20>븣 RPC <20>옄泥대<EFA7A3><EB8C80> **<2A><EFBFBD><EFBFBD>븯吏<EBB8AF> <20><EFBFBD>쑝誘<EC919D><EFBFBD>** <20>쐞諛<EC909E> <20><EFBFBD>떂. RPC <20><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>븳 stepIndex留<78> <20><EFBFBD>슜.
### [2026-03-28] [approval-handler] stepIndex 미확정 시 wrong-stepIndex RPC 낭비
- **증상**: DOM observer 경로로 `terminal_command` pending 생성 후 Discord 승인 시 `HandleCascadeUserInteraction(stepIndex=0)` → `"input not registered for step 0"` → LS reconnect → 재시도 → DOM click fallback으로 저하. (wrong-LS와 동일한 증상이나 다른 원인)
- **원인**: `ctx.lastPendingStepIndex=-1` (step-probe가 UTF-8 에러로 WAITING 미감지)임에도 `Math.max(0, -1)=0`으로 clamp되어 존재하지 않는 step 0에 RPC 전송.
- **해결** (v0.5.11): `effectiveStepIndex = stepIndex >= 0 ? stepIndex : (lastPendingStepIndex >= 0 ? lastPendingStepIndex : -1)`. `effectiveStepIndex < 0`이면 RPC 블록 전체 skip → DOM click 직행 (기존과 동작 동일, LS reconnect 낭비 제거).
- **주의**: 기존 규칙 #14(`uint32`에 음수 금지)와 충돌처럼 보이나, `effectiveStepIndex=-1`일 때 RPC 자체를 **전송하지 않으므로** 위반 아님. RPC 전송 시에는 여전히 유효한 stepIndex만 사용.
### [2026-03-25] [Architecture] Discord Signal Drop & Extension Freezes ### [2026-03-25] [Architecture] Discord Signal Drop & Extension Freezes
- **증상**: 장시간 자리비움 후 복귀 시 Discord로 승인 신호가 오지 않거나 VS Code UI가 간헐적/지속적으로 멈춤(Freeze).
- **원인**:
1. `ws.onerror` 발생 후 `onclose` 누락 시 재연결 콜백 호출이 이루어지지 않아 무한 대기 (장시간 마비)
2. `ws-client` 재연결 시 누적된 200개 큐를 동기식 burst 전송하여 Hub의 속도 제한(60개/10초)에 걸려 확정 영구 삭제됨
3. 로컬 브릿지 `http-bridge.ts`의 과거 유산인 `FALSE_POSITIVE_RE` 정규식이 AI 고유 버튼(Allow, Deny, Accept) 마저 필터링하여 Discord 전송 원천 차단
4. `step-probe.ts` 폴링 루프 내 동기식 파일 I/O 사용으로 인한 프리즈
- **해결** (v0.5.10): ws-client에 하드 타임아웃 및 50ms Paced-flush 적용, http-bridge의 정규식 기능 완화, step-probe 비동기 I/O 전환 체제 적용, observer-script의 필터된 신호 무한 HTTP 폴링 방어 코드 반영.
- **주의**: Extension 내부 로직 버그였으므로 Hub(Python) 코드는 건드리지 않음. Hub 속도 제한은 정상 방어 기제이므로 클라이언트 단의 Pacing이 올바른 방향임.
### [2026-03-24] DOM Observer /trigger-click 렌더링 순서 오작동 및 False Positive 프리징
- **증상**: v0.5.9 패치 이후 코딩 시 Agent 화면이 끊임없이 서명 대기(Pending) 상태로 멈춤. 또는 디스코드에서 `Approve` 시 에디터 내의 엉뚱한 `Run Test`(코드 렌즈)를 클릭함.
- **원인**: 텍스트와 정규식(`/^Run/i` 등)에만 의존하여 `querySelectorAll`을 수행할 경우, DOM 트리에 렌더링된 수많은 VS Code 네이티브 코드 렌즈 버튼을 Agent 버튼보다 먼저 찾아버리는 발생 위치(Context)의 한계점.
- **해결** (v0.5.10):
1. 감지(Scan): `isVSCodeMainWindow` 및 탐색 노드 `isBodyRoot` 확인을 통해, 에디터 본문 영역에서는 "Run", "Approve" 감지를 원천 제거 (오직 패널 내로 한정).
2. 클릭(Trigger-click): `deepFindButtons()` 내에서 `findPanel()`(에이전트 패널) -> 알림 Toasts -> Document 본문 순으로 탐색 **우선순위(Priority)**를 강제 적용.
- **주의**: 버튼 이벤트 후킹 시 텍스트 매칭에만 의존하지 말고, 반드시 DOM 탐색 우선순위와 컨텍스트 범위를 함께 필터링하여 False Positive를 차단할 것.
### [2026-03-24] DOM Observer — VS Code Native UI Blind Spot - **利앹긽**: <20><EFBFBD>떆媛<EB9686> <20>옄由щ퉬<D189><ED89AC><EFBFBD> <20>썑 蹂듦<E8B982><EB93A6> <20>떆 Discord濡<64> <20><EFBFBD><20><EFBFBD>샇媛<EC8387> <20>삤吏<EC82A4> <20>븡嫄곕굹 VS Code UI媛<49> 媛꾪뿉<EABEAA>쟻/吏<><EFA79E><EFBFBD><EFBFBD>쑝濡<EC919D> 硫덉땄(Freeze).
- **증상**: "Always Allow" 및 일반 "Allow Alt+↵" 권한 알림 버튼이 디스코드 권한 센싱에서 완전히 누락됨.
- **원인**: VS Code 네이티브 알림 및 채팅 패널 내의 버튼은 `<button>` 태그 대신 `<a role="button">`, `<vscode-button>` 등을 사용하는데, 기존 DOM scan 로직이 `querySelectorAll('button')`으로 하드코딩되어 노드를 아예 찾지 못함. (추가로 Always Allow 정규식 누락) - **<EFBFBD><EFBFBD>씤**:
- **해결** (v0.5.9): DOM scan, 리슨 훅 등 모든 탐색 로직 셀렉터를 `button, [role="button"], vscode-button, .monaco-text-button` 으로 전면 개편. 정규식을 `/^(?:Always )?Allow/i`로 수정.
1. `ws.onerror` 諛쒖깮 <20>썑 `onclose` <20><EFBFBD><20><20><EFBFBD>뿰寃<EBBFB0> 肄쒕갚 <20>샇異쒖씠 <20>씠猷⑥뼱吏<EBBCB1><EFBFBD> <20><EFBFBD>븘 臾댄븳 <20><><EFBFBD><EFBFBD> (<28><EFBFBD>떆媛<EB9686> 留덈퉬)
2. `ws-client` <20><EFBFBD>뿰寃<EBBFB0> <20><20><EFBFBD><EFBFBD>맂 200媛<30> <20>걧瑜<EAB1A7> <20>룞湲곗떇 burst <20><EFBFBD><EFBFBD><EFBFBD>뿬 Hub<75><20><EFBFBD><20><EFBFBD>븳(60媛<30>/10珥<30>)<29>뿉 嫄몃젮 <20><EFBFBD><20>쁺援<EC81BA> <20><EFBFBD><EFBFBD>
3. 濡쒖뺄 釉뚮┸吏<E294B8> `http-bridge.ts`<60>쓽 怨쇨굅 <20><EFBFBD><EFBFBD>씤 `FALSE_POSITIVE_RE` <20>젙洹쒖떇<EC9296>씠 AI 怨좎쑀 踰꾪듉(Allow, Deny, Accept) 留덉<EFA78D><EB8D89> <20><EFBFBD>꽣留곹븯<EAB3B9>뿬 Discord <20><EFBFBD><20>썝泥<EC8D9D> 李⑤떒
4. `step-probe.ts` <20>뤃留<EBA483> 猷⑦봽 <20><20>룞湲곗떇 <20><EFBFBD>씪 I/O <20><EFBFBD><EFBFBD>쑝濡<EC919D> <20><EFBFBD><20>봽由ъ쫰
- **<2A>빐寃<EBB990>** (v0.5.10): ws-client<6E><20><EFBFBD><20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>썐 諛<> 50ms Paced-flush <20><EFBFBD>슜, http-bridge<67><20>젙洹쒖떇 湲곕뒫 <20><EFBFBD>솕, step-probe 鍮꾨룞湲<EBA39E> I/O <20><EFBFBD>솚 泥댁젣 <20><EFBFBD>슜, observer-script<70><20><EFBFBD><EFBFBD><20><EFBFBD>샇 臾댄븳 HTTP <20>뤃留<EBA483> 諛⑹뼱 肄붾뱶 諛섏쁺.
- **二쇱쓽**: Extension <20>궡遺<EAB6A1> 濡쒖쭅 踰꾧렇<EABEA7><EBA087><EFBFBD><EFBFBD>쑝誘<EC919D><EFBFBD> Hub(Python) 肄붾뱶<EBB6BE>뒗 嫄대뱶由ъ<E794B1><D18A> <20><EFBFBD>쓬. Hub <20><EFBFBD><20><EFBFBD><EFBFBD><EBB8B3><EFBFBD> <20><EFBFBD>긽 諛⑹뼱 湲곗젣<EAB397>씠誘<EC94A0><EFBFBD> <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>쓽 Pacing<6E><20>삱諛붾Ⅸ 諛⑺뼢<E291BA>엫.
### [2026-03-24] DOM Observer /trigger-click <20><EFBFBD>뜑留<EB9C91> <20><EFBFBD><20><EFBFBD><EFBFBD>룞 諛<> False Positive <20>봽由ъ쭠
- **利앹긽**: v0.5.9 <20>뙣移<EB99A3> <20><EFBFBD>썑 肄붾뵫 <20>떆 Agent <20>솕硫댁씠 <20><EFBFBD><EFBFBD><EFBFBD><20>꽌紐<EABD8C> <20><><EFBFBD><EFBFBD>(Pending) <20><EFBFBD>깭濡<EAB9AD> 硫덉땄. <20><EFBFBD><20><EFBFBD>뒪肄붾뱶<EBB6BE><EFBFBD>꽌 `Approve` <20><20><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD><EFBFBD>븳 `Run Test`(肄붾뱶 <20>젋利<ECA08B>)瑜<> <20>겢由<EAB2A2><E794B1>븿.
- **<2A><EFBFBD>씤**: <20><EFBFBD><EFBFBD><EFBFBD><EB9383><EFBFBD> <20>젙洹쒖떇(`/^Run/i` <20>벑)<29>뿉留<EBBF89> <20>쓽議댄븯<EB8C84>뿬 `querySelectorAll`<60><20><EFBFBD><EFBFBD>븷 寃쎌슦, DOM <20>듃由ъ뿉 <20><EFBFBD>뜑留곷맂 <20>닔留롮<EFA78D><EBA1AE> VS Code <20><EFBFBD><EFBFBD>떚釉<EB969A> 肄붾뱶 <20>젋利<ECA08B> 踰꾪듉<EABEAA>쓣 Agent 踰꾪듉蹂대떎 癒쇱<E79992><EC87B1> 李얠븘踰꾨━<EABEA8>뒗 諛쒖깮 <20>쐞移<EC909E>(Context)<29><20>븳怨꾩젏.
- **<2A>빐寃<EBB990>** (v0.5.10):
1. 媛먯<E5AA9B><EBA8AF>(Scan): `isVSCodeMainWindow` 諛<> <20><EFBFBD><20><EFBFBD>뱶 `isBodyRoot` <20><EFBFBD><EFBFBD><20><EFBFBD>빐, <20><EFBFBD><EFBFBD>꽣 蹂몃Ц <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD>뒗 "Run", "Approve" 媛먯<E5AA9B><EBA8AF><EFBFBD> <20>썝泥<EC8D9D> <20>젣嫄<ECA0A3> (<28>삤吏<EC82A4> <20><EFBFBD><20>궡濡<EAB6A1> <20><EFBFBD>젙).
2. <20>겢由<EAB2A2>(Trigger-click): `deepFindButtons()` <20><EFBFBD><EFBFBD>꽌 `findPanel()`(<28><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>꼸) -> <20>븣由<EBB8A3> Toasts -> Document 蹂몃Ц <20><EFBFBD>쑝濡<EC919D> <20><EFBFBD>깋 **<2A><EFBFBD><EFBFBD><EFBFBD>쐞(Priority)**瑜<> 媛뺤젣 <20><EFBFBD>슜.
- **二쇱쓽**: 踰꾪듉 <20>씠踰ㅽ듃 <20><EFBFBD><20><20><EFBFBD><EFBFBD>듃 留ㅼ묶<E385BC>뿉留<EBBF89> <20>쓽議댄븯吏<EBB8AF> 留먭퀬, 諛섎뱶<EC848E>떆 DOM <20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EC909E><EFBFBD> 而⑦뀓<E291A6><EFBFBD>듃 踰붿쐞瑜<EC909E> <20>븿猿<EBB8BF> <20><EFBFBD>꽣留곹븯<EAB3B9>뿬 False Positive瑜<65> 李⑤떒<E291A4>븷 寃<>.
### [2026-03-24] DOM Observer <20><><EFBFBD> VS Code Native UI Blind Spot
- **利앹긽**: "Always Allow" 諛<> <20>씪諛<EC94AA> "Allow Alt+<2B>넻" 沅뚰븳 <20>븣由<EBB8A3> 踰꾪듉<EABEAA><20><EFBFBD>뒪肄붾뱶 沅뚰븳 <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>맖.
- **<2A><EFBFBD>씤**: VS Code <20><EFBFBD><EFBFBD>떚釉<EB969A> <20>븣由<EBB8A3><> 梨꾪똿 <20><EFBFBD><20><EFBFBD>쓽 踰꾪듉<EABEAA><EB9389><EFBFBD> `<button>` <20>깭洹<EAB9AD> <20><><EFBFBD><EFBFBD>떊 `<a role="button">`, `<vscode-button>` <20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><EFBFBD>뜲, 湲곗〈 DOM scan 濡쒖쭅<EC9296>씠 `querySelectorAll('button')`<60>쑝濡<EC919D> <20><EFBFBD>뱶肄붾뵫<EBB6BE><EFBFBD><20><EFBFBD>뱶瑜<EBB1B6> <20><EFBFBD>삁 李얠<EFA7A1><EC96A0> 紐삵븿. (異붽<E795B0><EBB6BD><EFBFBD> Always Allow <20>젙洹쒖떇 <20><EFBFBD>씫)
- **<2A>빐寃<EBB990>** (v0.5.9): DOM scan, 由ъ뒯 <20><20>벑 紐⑤뱺 <20><EFBFBD>깋 濡쒖쭅 <20><><EFBFBD><EFBFBD><EFBFBD>꽣瑜<EABDA3> `button, [role="button"], vscode-button, .monaco-text-button` <20>쑝濡<EC919D> <20>쟾硫<EC9FBE> 媛쒗렪. <20>젙洹쒖떇<EC9296>쓣 `/^(?:Always )?Allow/i`濡<> <20><EFBFBD>젙.
### [2026-03-24] Python Hub <20><><EFBFBD><><EFBFBD> 而ㅻ꽖<E385BB>뀡 諛<> UI <20>봽由ъ쭠
- **利앹긽**: `npm run` 紐낅졊<EB8285>씠 `<EFBFBD><EFBFBD><20>젙梨<ECA099>` 愿<><E684BF><20>삤瑜섎줈 <20><EFBFBD>
- **<2A><EFBFBD>씤**: PowerShell <20><EFBFBD>겕由쏀듃 <20><EFBFBD><20>젙梨낆씠 <20><EFBFBD><EFBFBD>
- **<2A>빐寃<EBB990>**: `cmd /c npm run dev` <20><EFBFBD><EFBFBD>쑝濡<EC919D> cmd瑜<64> <20><EFBFBD><20><EFBFBD>
- **二쇱쓽**: npm 愿<><E684BF>젴 紐낅졊<EB8285><ECA18A><EFBFBD> <20><EFBFBD>긽 `cmd /c` <20><EFBFBD><EFBFBD><20><EFBFBD>슜 沅뚯옣
### [2026-03-08] PowerShell curl <20><><EFBFBD> Invoke-WebRequest 異⑸룎
- **利앹긽**: `curl` 紐낅졊<EB8285><20><EFBFBD>긽怨<EAB8BD> <20>떎瑜<EB968E> <20><EFBFBD><20><EFBFBD><EFBFBD>쓣 諛섑솚
- **<2A><EFBFBD>씤**: PowerShell<6C><EFBFBD>꽌 `curl`<60><><EFBFBD> `Invoke-WebRequest`<60>쓽 蹂꾩묶
- **<2A>빐寃<EBB990>**: **`curl.exe`**瑜<> 紐낆떆<EB8286><EFBFBD>쑝濡<EC919D> <20><EFBFBD>
- **二쇱쓽**: HTTP 愿<><E684BF>젴 紐⑤뱺 紐낅졊<EB8285><EFBFBD>꽌 `curl.exe` <20><EFBFBD><20><EFBFBD>
### [2026-03-24] Python Hub — 좀비 커넥션 및 UI 프리징
- **증상**: `npm run` 명령이 `실행 정책` 관련 오류로 실패
- **원인**: PowerShell 스크립트 실행 정책이 제한적
- **해결**: `cmd /c npm run dev` 형식으로 cmd를 통해 실행
- **주의**: npm 관련 명령은 항상 `cmd /c` 접두어 사용 권장
### [2026-03-08] PowerShell curl — Invoke-WebRequest 충돌
- **증상**: `curl` 명령이 예상과 다른 응답 형식을 반환
- **원인**: PowerShell에서 `curl`은 `Invoke-WebRequest`의 별칭
- **해결**: **`curl.exe`**를 명시적으로 사용
- **주의**: HTTP 관련 모든 명령에서 `curl.exe` 사용 필수
--- ---
## 미해결 이슈
### [2026-03-23/24] 평생 지속되는 WebSocket 좀비 커넥션 및 False Positive 강제 연결 끊김 (v0.5.5 → 0.5.8)
- **증상**:
1. (v0.5.5) 절전 모드 복구 시 실연결이 끊어졌음에도 확장이 이를 인지하지 못하는 좀비(Half-open) 소켓 발생.
2. (v0.5.6) 좀비 소켓을 잡기 위해 10초 타이머(`pongTimeoutTimer`)를 넣었으나, VS Code의 무거운 파일 검색 시 Event Loop가 블로킹되면 멀쩡한 연결인데도 허위 타임아웃(False Positive) 판정으로 연결을 강제 종료함. 이로 인해 누적된 재연결 딜레이(Exponential Backoff)가 60초까지 늘어나면서 확장이 심각하게 멈춤(Freeze).
- **원인**: Node.js `ws` 라이브러리의 `ws.ping()`은 비동기 I/O 네트워크 큐를 타지만, `setTimeout(..., 10000)` 타임아웃은 Event Loop 블로킹 해제 직후 곧바로 만료되어 버림. 따라서 네트워크 I/O 응답(pong)보다 로컬 타이머가 먼저 터져서 정상적인 소켓을 죽임.
- **해결** (v0.5.8 완성):
- 위험한 `setTimeout` 방식 폐기.
- 기존의 25초 주기 `setInterval` 하트비트 루프 내부에 `Date.now() - lastPongTime > 60000` (60초 초과 시 타임아웃) 검증 로직을 도입.
- 만약 Event Loop가 수십 초 밀리더라도, 블로킹 해제 후 큐된 I/O 이벤트(`pong`)가 `setInterval` 타이머 콜백 이전에 먼저 처리되거나(Node.js Phase 규칙), 적어도 60초라는 버퍼 덕분에 **False Positive 가능성을 원천 차단**함과 동시에 좀비 소켓을 안정적으로 제거함.
- **주의**: Node.js의 단일 스레드 Event Loop 환경(특히 무거운 동기 작업이 잦은 VS Code Extension)에서 네트워크 I/O를 로컬 `setTimeout`과 경주(Race)시키는 설계는 필연적으로 False Positive를 낳음. Timestamp(`Date.now()`) 기반 간격 검증(Interval check)이 훨씬 안전함.
### [2026-03-11] rejectAgentStep / !stop — AG 미등록 커맨드 + 렌더러 전용 함수 + 스테일 프리미티브 ## 誘명빐寃<EBB990> <20><EFBFBD>
- **증상**: `!stop` 명령이 AI를 멈추지 못함. 로그: "No active cascade" / "no session tracked yet"
- **원인**: (1) `antigravity.agent.rejectAgentStep`은 AG 미등록 커맨드. (2) 대체한 `getActiveCascadeId()`는 **렌더러(DOM) 전용 함수** — Extension host에서 항상 `undefined` 반환. (3) **v0.4.5 수정도 실패**: `extension.ts`의 `getActiveSessionId: () => activeSessionId`가 module-level 스트링 프리미티브를 참조 — step-probe가 `ctx.activeSessionId`를 업데이트해도 extension.ts의 변수는 불변 (프리미티브 복사)
- **해결** (2026-03-18 v0.4.6): `step-probe.ts`에서 `getActiveSessionId()` getter 함수 export → extension.ts closures에서 `getStepProbeSessionId()` 호출. 이제 step-probe의 live `ctx.activeSessionId`를 직접 읽음 (`ab0c116`)
- **주의**: JS에서 **string/number는 프리미티브라 참조 전달 불가** — 객체 속성을 공유하려면 getter 함수나 객체 래퍼 사용 필수 ### [2026-03-23/24] <20><EFBFBD>깮 吏<><EFA79E><EFBFBD><EFBFBD>뒗 WebSocket 醫<><EFBFBD> 而ㅻ꽖<E385BB>뀡 諛<> False Positive 媛뺤젣 <20>뿰寃<EBBFB0> <20>걡源<EAB1A1> (v0.5.5 <20>넂 0.5.8)
- **利앹긽**:
1. (v0.5.5) <20><EFBFBD>쟾 紐⑤뱶 蹂듦뎄 <20><20><EFBFBD>뿰寃곗씠 <20><EFBFBD>뼱議뚯쓬<EB9AAF><EFBFBD><20><EFBFBD><EFBFBD><20>씠瑜<EC94A0> <20>씤吏<EC94A4><EFA79E>븯吏<EBB8AF> 紐삵븯<EC82B5>뒗 醫<><EFBFBD>(Half-open) <20>냼耳<EB83BC> 諛쒖깮.
2. (v0.5.6) 醫<><EFBFBD> <20>냼耳볦쓣 <20>옟湲<EC989F> <20><EFBFBD>빐 10珥<30> <20><><EFBFBD><EFBFBD>씠癒<EC94A0>(`pongTimeoutTimer`)瑜<> <20><EFBFBD><EFBFBD><EFBFBD>굹, VS Code<64>쓽 臾닿굅<EB8BBF><20><EFBFBD>씪 寃<><E5AF83><20>떆 Event Loop媛<70> 釉붾줈<EBB6BE><EFBFBD>릺硫<EBA6BA><>姨≫븳 <20>뿰寃곗씤<EAB397><EFBFBD><20><EFBFBD><20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>썐(False Positive) <20><EFBFBD><EFBFBD>쑝濡<EC919D> <20>뿰寃곗쓣 媛뺤젣 醫낅즺<EB8285>븿. <20>씠濡<EC94A0> <20><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD>뿰寃<EBBFB0> <20><EFBFBD><EFBFBD>씠(Exponential Backoff)媛<> 60珥덇퉴吏<ED89B4> <20><EFBFBD><EFBFBD>굹硫댁꽌 <20><EFBFBD><EFBFBD><20>떖媛곹븯寃<EBB8AF> 硫덉땄(Freeze).
- **<2A><EFBFBD>씤**: Node.js `ws` <20><EFBFBD>씠釉뚮윭由ъ쓽 `ws.ping()`<60><><EFBFBD> 鍮꾨룞湲<EBA39E> I/O <20><EFBFBD><EFBFBD><EFBFBD><20>걧瑜<EAB1A7> <20><><EFBFBD><EFBFBD><EFBFBD>, `setTimeout(..., 10000)` <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EC8D90><EFBFBD> Event Loop 釉붾줈<EBB6BE><20><EFBFBD>젣 吏곹썑 怨㏓컮濡<ECBBAE> 留뚮즺<EB9AAE><EFBFBD>뼱 踰꾨┝. <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD>겕 I/O <20><EFBFBD>떟(pong)蹂대떎 濡쒖뺄 <20><><EFBFBD><EFBFBD>씠癒멸<E79992><EBA9B8> 癒쇱<E79992><EC87B1> <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><20>냼耳볦쓣 二쎌엫.
- **<2A>빐寃<EBB990>** (v0.5.8 <20><EFBFBD>꽦):
- <20><EFBFBD><EFBFBD>븳 `setTimeout` 諛⑹떇 <20>룓湲<EBA393>.
- 湲곗〈<EAB397>쓽 25珥<35> 二쇨린 `setInterval` <20><EFBFBD>듃鍮꾪듃 猷⑦봽 <20>궡遺<EAB6A1><E981BA>뿉 `Date.now() - lastPongTime > 60000` (60珥<30> 珥덇낵 <20><20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>썐) 寃<><EFBFBD> 濡쒖쭅<EC9296><20><EFBFBD>엯.
- 留뚯빟 Event Loop媛<70> <20><EFBFBD>떗 珥<><>由щ뜑<D189><EFBFBD>룄, 釉붾줈<EBB6BE><20><EFBFBD><20><20><EFBFBD>맂 I/O <20>씠踰ㅽ듃(`pong`)媛<> `setInterval` <20><><EFBFBD><EFBFBD>씠癒<EC94A0> 肄쒕갚 <20><EFBFBD><EFBFBD>뿉 癒쇱<E79992><EC87B1> 泥섎━<EC848E>릺嫄곕굹(Node.js Phase 洹쒖튃), <20><EFBFBD><EFBFBD>룄 60珥덈씪<EB8D88>뒗 踰꾪띁 <20>뜒遺꾩뿉 **False Positive 媛<><E5AA9B><EFBFBD><EFBFBD><20>썝泥<EC8D9D> 李⑤떒**<2A>븿怨<EBB8BF> <20><EFBFBD><EFBFBD>뿉 醫<><EFBFBD> <20>냼耳볦쓣 <20><EFBFBD><EFBFBD><EFBFBD>쑝濡<EC919D> <20>젣嫄고븿.
- **二쇱쓽**: Node.js<6A><20><EFBFBD><20><EFBFBD><EFBFBD>뱶 Event Loop <20>솚寃<EC869A>(<28><EFBFBD>엳 臾닿굅<EB8BBF><20>룞湲<EBA39E> <20><EFBFBD><EFBFBD><20><EFBFBD><EC98A6><EFBFBD> VS Code Extension)<29><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD>겕 I/O瑜<4F> 濡쒖뺄 `setTimeout`怨<> 寃쎌<(Race)<29><EFBFBD><EFBFBD><20>꽕怨꾨뒗 <20><EFBFBD><EFBFBD><EFBFBD>쑝濡<EC919D> False Positive瑜<65> <20><EFBFBD>쓬. Timestamp(`Date.now()`) 湲곕컲 媛꾧꺽 寃<><EFBFBD>(Interval check)<29><20><EFBFBD><20><EFBFBD><EFBFBD>븿.
### [2026-03-11] rejectAgentStep / !stop <20><><EFBFBD> AG 誘몃벑濡<EBB291> 而ㅻ㎤<E385BB>뱶 + <20><EFBFBD><EFBFBD><20><EFBFBD><20>븿<EFBFBD>닔 + <20><EFBFBD><EFBFBD><20>봽由щ<E794B1>명떚釉<EB969A>
- **利앹긽**: `!stop` 紐낅졊<EB8285>씠 AI瑜<49> 硫덉텛吏<ED859B> 紐삵븿. 濡쒓렇: "No active cascade" / "no session tracked yet"
- **<2A><EFBFBD>씤**: (1) `antigravity.agent.rejectAgentStep`<60><><EFBFBD> AG 誘몃벑濡<EBB291> 而ㅻ㎤<E385BB>뱶. (2) <20><><EFBFBD>泥댄븳 `getActiveCascadeId()`<60>뒗 **<2A><EFBFBD><EFBFBD>윭(DOM) <20><EFBFBD><20>븿<EFBFBD>닔** <20><><EFBFBD> Extension host<73><EFBFBD><20><EFBFBD>긽 `undefined` 諛섑솚. (3) **v0.4.5 <20><EFBFBD><EFBFBD><20><EFBFBD>뙣**: `extension.ts`<60>쓽 `getActiveSessionId: () => activeSessionId`媛<> module-level <20><EFBFBD>듃留<EB9383> <20>봽由щ<E794B1>명떚釉뚮<E98789><EB9AAE> 李몄“ <20><><EFBFBD> step-probe媛<65> `ctx.activeSessionId`瑜<> <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>룄 extension.ts<74>쓽 蹂<><E8B982><EFBFBD>뒗 遺덈<E981BA><EB8D88> (<28>봽由щ<E794B1>명떚釉<EB969A> 蹂듭궗)
- **<2A>빐寃<EBB990>** (2026-03-18 v0.4.6): `step-probe.ts`<60><EFBFBD>꽌 `getActiveSessionId()` getter <20>븿<EFBFBD>닔 export <20>넂 extension.ts closures<65><EFBFBD>꽌 `getStepProbeSessionId()` <20>샇異<EC8387>. <20><EFBFBD>젣 step-probe<62>쓽 live `ctx.activeSessionId`瑜<> 吏곸젒 <20><EFBFBD>쓬 (`ab0c116`)
- **二쇱쓽**: JS<4A><EFBFBD>꽌 **string/number<65><20>봽由щ<E794B1>명떚釉뚮씪 李몄“ <20><EFBFBD>떖 遺덇<E981BA><EB8D87>** <20><><EFBFBD> 媛앹껜 <20><EFBFBD><EFBFBD>쓣 怨듭쑀<EB93AD><EFBFBD>젮硫<ECA0AE> getter <20>븿<EFBFBD><EFBFBD>굹 媛앹껜 <20><EFBFBD><20><EFBFBD><20><EFBFBD>
- **Vikunja**: #411, #410 - **Vikunja**: #411, #410
### [2026-03-19] browser_subagent Allow — 잘못된 RPC payload
- **증상**: 서브 에이전트 "execute JavaScript on localhost" Allow 버튼이 자동 승인되지 않음
- **원인**: `step-probe.ts`에서 `browser_subagent` toolName이 step_type 분류 없이 raw toolName으로 전달 → `approval-handler.ts`에서 `runExtensionCode` 매핑에 포함되지 않아 default `runCommand` RPC payload 사용 → AG가 잘못된 interaction type으로 무시
- **해결** (v0.5.1): `approval-handler.ts` L384에 `browser_subagent` 추가, `step-probe.ts` L481/L549에 `browser_subagent`/`open_browser_url` step_type 분류 추가 (`549af6d`)
- **주의**: 새로운 AG 도구 추가 시 반드시 (1) step-probe step_type 매핑 (2) approval-handler RPC payload 매핑 양쪽 모두 업데이트
### [2026-03-21] Idle→Resume 신호 소실 — 3중 버그
- **증상**: AG 장시간 idle 후 작업 재개 시 Discord 승인 신호가 전달되지 않음 ### [2026-03-19] browser_subagent Allow <20><><EFBFBD> <20>옒紐삳맂 RPC payload
- **원인**: (1) `ws-client.ts` `auth_fail` 시 `shouldReconnect=false` — JWT 24h 만료 시 WS 영구 종료. (2) `hub.py` `_disconnect`에서 유일 연결 시 `pending_owners` 삭제 — 재연결 후 Discord 버튼 무효. (3) `step-probe.ts` `stallProbed=true` + `lastPendingStepIndex=N`이 WS 재연결 시 리셋 안 됨 — WAITING step 재전송 영구 차단
- **해결** (v0.5.2): (1) `auth_fail` → `registrationCode` 재시도. (2) `pending_owners` orphan 마커로 보존+재할당. (3) `resetPendingStateForReconnect()` + `onConnected`에서 호출 - **利앹긽**: <20>꽌釉<EABD8C> <20><EFBFBD><EFBFBD><EFBFBD>듃 "execute JavaScript on localhost" Allow 踰꾪듉<EABEAA><20><EFBFBD><20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>
- **주의**: WS `onConnected`에서 반드시 step-probe 상태 리셋 필수. `stallProbed`/`lastPendingStepIndex`는 TTL 없는 영구 값
- **<2A><EFBFBD>씤**: `step-probe.ts`<60><EFBFBD>꽌 `browser_subagent` toolName<6D>씠 step_type 遺꾨쪟 <20><EFBFBD>씠 raw toolName<6D>쑝濡<EC919D> <20><EFBFBD><20>넂 `approval-handler.ts`<60><EFBFBD>꽌 `runExtensionCode` 留ㅽ븨<E385BD><20><EFBFBD>븿<EFBFBD>릺吏<EBA6BA> <20><EFBFBD>븘 default `runCommand` RPC payload <20><EFBFBD><20>넂 AG媛<47> <20>옒紐삳맂 interaction type<70>쑝濡<EC919D> 臾댁떆
- **<2A>빐寃<EBB990>** (v0.5.1): `approval-handler.ts` L384<38>뿉 `browser_subagent` 異붽<E795B0><EBB6BD>, `step-probe.ts` L481/L549<34>뿉 `browser_subagent`/`open_browser_url` step_type 遺꾨쪟 異붽<E795B0><EBB6BD> (`549af6d`)
- **二쇱쓽**: <20>깉濡쒖슫 AG <20>룄援<EBA384> 異붽<E795B0><EBB6BD> <20>떆 諛섎뱶<EC848E>떆 (1) step-probe step_type 留ㅽ븨 (2) approval-handler RPC payload 留ㅽ븨 <20>뼇履<EBBC87> 紐⑤몢 <20><EFBFBD><EFBFBD><EFBFBD>
### [2026-03-21] Idle<6C>넂Resume <20><EFBFBD><20><EFBFBD><20><><EFBFBD> 3以<33> 踰꾧렇
- **利앹긽**: AG <20><EFBFBD>떆媛<EB9686> idle <20><20><EFBFBD><20>옱媛<EC98B1> <20>떆 Discord <20><EFBFBD><20><EFBFBD>샇媛<EC8387> <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>
- **<2A><EFBFBD>씤**: (1) `ws-client.ts` `auth_fail` <20>떆 `shouldReconnect=false` <20><><EFBFBD> JWT 24h 留뚮즺 <20>떆 WS <20>쁺援<EC81BA> 醫낅즺. (2) `hub.py` `_disconnect`<60><EFBFBD><20><EFBFBD><20>뿰寃<EBBFB0> <20>떆 `pending_owners` <20><EFBFBD><20><><EFBFBD> <20><EFBFBD>뿰寃<EBBFB0> <20>썑 Discord 踰꾪듉 臾댄슚. (3) `step-probe.ts` `stallProbed=true` + `lastPendingStepIndex=N`<60>씠 WS <20><EFBFBD>뿰寃<EBBFB0> <20>떆 由ъ뀑 <20><20><20><><EFBFBD> WAITING step <20><EFBFBD><EFBFBD><20>쁺援<EC81BA> 李⑤떒
- **<2A>빐寃<EBB990>** (v0.5.2): (1) `auth_fail` <20>넂 `registrationCode` <20><EFBFBD><EFBFBD>룄. (2) `pending_owners` orphan 留덉빱濡<EBB9B1> 蹂댁〈+<2B><EFBFBD><EFBFBD>떦. (3) `resetPendingStateForReconnect()` + `onConnected`<60><EFBFBD><20>샇異<EC8387>
- **二쇱쓽**: WS `onConnected`<60><EFBFBD>꽌 諛섎뱶<EC848E>떆 step-probe <20><EFBFBD>깭 由ъ뀑 <20><EFBFBD>닔. `stallProbed`/`lastPendingStepIndex`<60>뒗 TTL <20><EFBFBD><20>쁺援<EC81BA><>
--- ---
> [!NOTE] > [!NOTE]
> v0.4.5 수정 사항(Hub pending_owners, diff_review WS, auto_approve 이중쓰기, WS dual-write, ApprovalView fallback)은
> 코드 수정 완료됨. E2E 통합 검증은 Vikunja #410에서 추적 중.
### [2026-03-21] stepIndex=-1 — AG proto uint32 에러 > v0.4.5 <20><EFBFBD><20><EFBFBD>빆(Hub pending_owners, diff_review WS, auto_approve <20>씠以묒벐湲<EBB290>, WS dual-write, ApprovalView fallback)<29><><EFBFBD>
- **증상**: DOM observer가 Allow 버튼 감지 → Discord 승인 → RPC `HandleCascadeUserInteraction` 400 에러
- **원인**: DOM observer 경로는 step index를 모름 → `stepIndex=-1` 전달 → AG proto `uint32` 필드에 음수 불가 > 肄붾뱶 <20><EFBFBD><20>셿猷뚮맖. E2E <20><EFBFBD>빀 寃<>利앹<EFA79D><EC95B9> Vikunja #410<31><EFBFBD>꽌 異붿쟻 以<>.
- **해결**: `Math.max(0, ...)` 로 clamp. `permission` type → `runExtensionCode.confirm` 매핑 추가 (v0.5.4)
- **주의**: DOM observer 경로의 step_type은 항상 `stepIndex=-1`일 수 있으므로 proto 전달 전 양수 보장 필수
### [2026-03-21] stepIndex=-1 <20><><EFBFBD> AG proto uint32 <20><EFBFBD>
- **利앹긽**: DOM observer媛<72> Allow 踰꾪듉 媛먯<E5AA9B><EBA8AF> <20>넂 Discord <20><EFBFBD><20>넂 RPC `HandleCascadeUserInteraction` 400 <20><EFBFBD>
- **<2A><EFBFBD>씤**: DOM observer 寃쎈줈<EC8E88>뒗 step index瑜<78> 紐⑤쫫 <20>넂 `stepIndex=-1` <20><EFBFBD><20>넂 AG proto `uint32` <20><EFBFBD><EFBFBD><20><EFBFBD>닔 遺덇<E981BA><EB8D87>
- **<2A>빐寃<EBB990>**: `Math.max(0, ...)` 濡<> clamp. `permission` type <20>넂 `runExtensionCode.confirm` 留ㅽ븨 異붽<E795B0><EBB6BD> (v0.5.4)
- **二쇱쓽**: DOM observer 寃쎈줈<EC8E88>쓽 step_type<70><65><EFBFBD> <20><EFBFBD>긽 `stepIndex=-1`<60><20><20><EFBFBD>쑝誘<EC919D><EFBFBD> proto <20><EFBFBD><20><20><EFBFBD>닔 蹂댁옣 <20><EFBFBD>
### [2026-03-21] reviewAbsoluteUris <20><><EFBFBD> latestNotifyUserStep <20><EFBFBD>뱶紐<EBB1B6> 遺덉씪移<EC94AA>
- **利앹긽**: `notify_user`<60>쓽 PathsToReview <20><EFBFBD>씪 由대젅<EB8C80>씠媛<EC94A0> <20>븳 踰덈룄 <20><EFBFBD><EFBFBD>븯吏<EBB8AF> <20><EFBFBD>
- **<2A><EFBFBD>씤**: AG <20><EFBFBD><20><EFBFBD>뱶紐<EBB1B6> `reviewAbsoluteUris` vs 肄붾뱶 `pathsToReview`/`paths_to_review`/`filePaths`
- **<2A>빐寃<EBB990>**: `reviewAbsoluteUris` 瑜<><> 踰덉㎏ <20>썑蹂대줈 異붽<E795B0><EBB6BD> (v0.5.3)
- **二쇱쓽**: AG RPC <20><EFBFBD>뱶紐낆<EFA78F><EB8286> extension.log `[NOTIFY-STEP] keys=` 濡<> <20><EFBFBD>씤 媛<><E5AA9B>뒫. 異붿륫 湲덉<E6B9B2><EB8D89>
### [2026-03-21] <20><EFBFBD><20><EFBFBD><20><><EFBFBD><> WAITING 媛먯<E5AA9B><EBA8AF> 20-25s 吏<><EFA79E>
- **利앹긽**: <20><20><><EFBFBD><EFBFBD><20><EFBFBD><20>썑 泥<> run_command <20><EFBFBD><EFBFBD>씠 Discord<72><20><20>삤怨<EC82A4> AG<41><EFBFBD>꽌 吏곸젒 <20><EFBFBD><EFBFBD><EFBFBD><20>븿
- **<2A><EFBFBD>씤**: `lastModTime=''` 由ъ뀑 <20>넂 `modTimeChanged=true` <20>넂 THINKING 遺꾧린 諛섎났 <20>넂 probe 15-25s 吏<><EFA79E>
- **<2A>빐寃<EBB990>**: `lastModTime=currentModTime` + `return` <20>젣嫄<ECA0A3> + 利됱떆 probe 媛뺤젣 + <20>쉶洹<EC89B6><><E5AA9B>뱶 異붽<E795B0><EBB6BD> (v0.5.3)
- **二쇱쓽**: <20><EFBFBD><20><EFBFBD><20>떆 `wasRunning`/`pendingModifiedFiles` 由ъ뀑 <20><EFBFBD>닔 (<28><EFBFBD><20><EFBFBD><20><EFBFBD>뿬臾쇰줈 false diff_review 諛⑹<E8AB9B><E291B9>)
### [2026-03-21] reviewAbsoluteUris — latestNotifyUserStep 필드명 불일치
- **증상**: `notify_user`의 PathsToReview 파일 릴레이가 한 번도 작동하지 않음
- **원인**: AG 실제 필드명 `reviewAbsoluteUris` vs 코드 `pathsToReview`/`paths_to_review`/`filePaths`
- **해결**: `reviewAbsoluteUris` 를 첫 번째 후보로 추가 (v0.5.3)
- **주의**: AG RPC 필드명은 extension.log `[NOTIFY-STEP] keys=` 로 확인 가능. 추측 금지
### [2026-03-21] 세션 전환 — 첫 WAITING 감지 20-25s 지연
- **증상**: 새 대화 시작 후 첫 run_command 승인이 Discord에 안 오고 AG에서 직접 승인해야 함
- **원인**: `lastModTime=''` 리셋 → `modTimeChanged=true` → THINKING 분기 반복 → probe 15-25s 지연
- **해결**: `lastModTime=currentModTime` + `return` 제거 + 즉시 probe 강제 + 회귀 가드 추가 (v0.5.3)
- **주의**: 세션 전환 시 `wasRunning`/`pendingModifiedFiles` 리셋 필수 (이전 세션 잔여물로 false diff_review 방지)
--- ---
## 핵심 작업 규칙 (과거 이슈에서 반복된 패턴)
> 아래는 과거 이슈에서 반복적으로 나타난 패턴을 규칙으로 정리한 것입니다.
| # | 규칙 | 관련 이슈 (archive 참조) | ## <20><EFBFBD><20><EFBFBD>뾽 洹쒖튃 (怨쇨굅 <20><EFBFBD><EFBFBD><EFBFBD>꽌 諛섎났<EC848E><20><EFBFBD>꽩)
> <20><EFBFBD><EFBFBD>뒗 怨쇨굅 <20><EFBFBD><EFBFBD><EFBFBD>꽌 諛섎났<EC848E><EFBFBD>쑝濡<EC919D> <20><EFBFBD><EAB5B9><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>쓣 洹쒖튃<EC9296>쑝濡<EC919D> <20>젙由ы븳 寃껋엯<EABB8B><EFBFBD>떎.
| # | 洹쒖튃 | 愿<><E684BF><20><EFBFBD>뒋 (archive 李몄“) |
|---|------|--------------------------| |---|------|--------------------------|
| 1 | **Hub WS와 file bridge는 상호 배타적** — `if hub: ws + return` / `else: file` | WS dual-write, _auto_approve 이중 쓰기 |
| 2 | **WS 경로 추가 시 file-bridge의 모든 분기를 포팅** | diff_review WS regression | | 1 | **Hub WS<57><53><EFBFBD> file bridge<EFBFBD><20><EFBFBD>샇 諛고<E8AB9B><EAB3A0><EFBFBD>쟻** <20><><EFBFBD> `if hub: ws + return` / `else: file` | WS dual-write, _auto_approve <20>씠以<EC94A0> <20>벐湲<EBB290> |
| 3 | **AG RPC `{}` 응답은 실패로 간주** — 메서드명 틀려도 에러 없이 `{}` 반환 | AcknowledgeCascadeCodeEdit |
| 4 | **ResolveOutstandingSteps는 CANCEL 동작** — 승인에 절대 사용 금지 | Step probe reject | | 2 | **WS 寃쎈줈 異붽<E795B0><EBB6BD> <20>떆 file-bridge<67>쓽 紐⑤뱺 遺꾧린瑜<EBA6B0> <20><EFBFBD>똿** | diff_review WS regression |
| 5 | **Extension 코드 수정 후 반드시 VSIX 빌드 + AG 풀 재시작** | Extension 버전 미배포 |
| 6 | **HTML 패치 변경 시 V8 CachedData 삭제 필수** | V8 CachedData, CSP | | 3 | **AG RPC `{}` <20><EFBFBD><EFBFBD><EB969F><EFBFBD> <20><EFBFBD>뙣濡<EB99A3> 媛꾩<** <20><><EFBFBD> 硫붿꽌<EBB6BF>뱶紐<EBB1B6> <20><><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD>씠 `{}` 諛섑솚 | AcknowledgeCascadeCodeEdit |
| 7 | **`bridge/pending/` 조작 시 반드시 `project_name` + `conversation_id` 필터** | 크로스 프로젝트 DEDUP MERGE |
| 8 | **`processResponseFile` 상태 리셋은 `sawRunningAfterPending=true`만** | processResponseFile 무한 루프 | | 4 | **ResolveOutstandingSteps<EFBFBD>뒗 CANCEL <20><EFBFBD>옉** <20><><EFBFBD> <20><EFBFBD><EFBFBD><20><EFBFBD><ECA085><EFBFBD> <20><EFBFBD>슜 湲덉<E6B9B2><EB8D89> | Step probe reject |
| 9 | **fs.watch Windows 불안정 — 반드시 polling fallback 병행** | fs.watch silent fail |
| 10 | **diff_review는 VS Code 커맨드만 유효** — RPC 3개 전략 모두 실패 확정 | diff_review RPC dead-end | | 5 | **Extension 肄붾뱶 <20><EFBFBD><20>썑 諛섎뱶<EC848E>떆 VSIX 鍮뚮뱶 + AG <20><><EFBFBD> <20><EFBFBD><EFBFBD>옉** | Extension 踰꾩쟾 誘몃같<EBAA83> |
| 11 | **HttpBridgeContext에 프리미티브 by-value 복사 금지** — 별도 객체 생성 시 getter 사용 | HttpBridgeContext stale primitive |
| 12 | **새 AG 도구 추가 시 step-probe step_type 매핑 + approval-handler RPC payload 매핑 양쪽 필수** | browser_subagent Allow | | 6 | **HTML <20>뙣移<EB99A3><><EFBFBD> <20>떆 V8 CachedData <20><EFBFBD><20><EFBFBD>닔** | V8 CachedData, CSP |
| 13 | **WS `onConnected`에서 step-probe 상태 리셋 필수** — `stallProbed`/`lastPendingStepIndex`는 TTL 없는 영구 값 | Idle→Resume 신호 소실 |
| 14 | **AG proto `uint32` 필드에 음수 전달 금지** — `stepIndex` 등은 `Math.max(0, ...)` 필수 | stepIndex=-1 RPC 400 | | 7 | **`bridge/pending/` 議곗옉 <20>떆 諛섎뱶<EC848E>떆 `project_name` + `conversation_id` <20><EFBFBD>꽣** | <20>겕濡쒖뒪 <20>봽濡쒖젥<EC9296>듃 DEDUP MERGE |
| 15 | **RPC "input not registered" = wrong-LS 연결** — `fixLSConnection()` 자동 재시도 필수, `lines.length<=1` 조기종료 금지 | Deriva wrong-LS (v0.5.5) |
| 16 | **익스텐션(Bridge)은 자의적 비즈니스 판단 절대 금지** — `SafeToAutoRun` 등의 조건 브랜치 분기는 모두 봇으로 위임 (Agnostic Bridge) | SafeToAutoRun Deadlock (v0.5.15) | | 8 | **`processResponseFile` <20><EFBFBD>깭 由ъ뀑<D18A><EB8091><EFBFBD> `sawRunningAfterPending=true`留<>** | processResponseFile 臾댄븳 猷⑦봽 |
| 17 | **package.json 빌드 스크립트 강제** — `vscode:prepublish` 추가로 낡은 소스 배포 원천 차단 | VSIX v0.5.15 빌드 누락 |
| 18 | **동기식 `cp.execSync` 사용 금지** — Windows 환경에서 메인 이벤트루프 프리징 및 WS heartbeat 단절 유발 | detectProjectName 프리징 | | 9 | **fs.watch Windows 遺덉븞<EB8D89><20><><EFBFBD> 諛섎뱶<EC848E>떆 polling fallback 蹂묓뻾** | fs.watch silent fail |
| 10 | **diff_review<65>뒗 VS Code 而ㅻ㎤<E385BB>뱶留<EBB1B6> <20><EFBFBD>슚** <20><><EFBFBD> RPC 3媛<33> <20><EFBFBD>왂 紐⑤몢 <20><EFBFBD><20><EFBFBD>젙 | diff_review RPC dead-end |
| 11 | **HttpBridgeContext<78><20>봽由щ<E794B1>명떚釉<EB969A> by-value 蹂듭궗 湲덉<E6B9B2><EB8D89>** <20><><EFBFBD> 蹂꾨룄 媛앹껜 <20><EFBFBD><20>떆 getter <20><EFBFBD>슜 | HttpBridgeContext stale primitive |
| 12 | **<2A>깉 AG <20>룄援<EBA384> 異붽<E795B0><EBB6BD> <20>떆 step-probe step_type 留ㅽ븨 + approval-handler RPC payload 留ㅽ븨 <20>뼇履<EBBC87> <20><EFBFBD>닔** | browser_subagent Allow |
| 13 | **WS `onConnected`<60><EFBFBD>꽌 step-probe <20><EFBFBD>깭 由ъ뀑 <20><EFBFBD>닔** <20><><EFBFBD> `stallProbed`/`lastPendingStepIndex`<60>뒗 TTL <20><EFBFBD><20>쁺援<EC81BA><> | Idle<6C>넂Resume <20><EFBFBD><20><EFBFBD>떎 |
| 14 | **AG proto `uint32` <20><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD>떖 湲덉<E6B9B2><EB8D89>** <20><><EFBFBD> `stepIndex` <20><EFBFBD><EBB291><EFBFBD> `Math.max(0, ...)` <20><EFBFBD>닔 | stepIndex=-1 RPC 400 |
| 15 | **RPC "input not registered" = wrong-LS <20>뿰寃<EBBFB0>** <20><><EFBFBD> `fixLSConnection()` <20><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD>닔, `lines.length<=1` 議곌린醫낅즺 湲덉<E6B9B2><EB8D89> | Deriva wrong-LS (v0.5.5) |
| 16 | **<2A><EFBFBD><EFBFBD><EFBFBD>뀡(Bridge)<29><><EFBFBD> <20><EFBFBD><EFBFBD>쟻 鍮꾩쫰<EABEA9><EFBFBD><20><EFBFBD><20><EFBFBD><ECA085><EFBFBD> 湲덉<E6B9B2><EB8D89>** <20><><EFBFBD> `SafeToAutoRun` <20><EFBFBD>쓽 議곌굔 釉뚮옖移<EC9896> 遺꾧린<EABEA7>뒗 紐⑤몢 遊뉗쑝濡<EC919D> <20><EFBFBD>엫 (Agnostic Bridge) | SafeToAutoRun Deadlock (v0.5.15) |
| 17 | **package.json 鍮뚮뱶 <20><EFBFBD>겕由쏀듃 媛뺤젣** <20><><EFBFBD> `vscode:prepublish` 異붽<E795B0><EBB6BD><EFBFBD> <20><EFBFBD><EAB68A><EFBFBD> <20><EFBFBD>뒪 諛고룷 <20>썝泥<EC8D9D> 李⑤떒 | VSIX v0.5.15 鍮뚮뱶 <20><EFBFBD>씫 |
| 18 | **<2A>룞湲곗떇 `cp.execSync` <20><EFBFBD>슜 湲덉<E6B9B2><EB8D89>** <20><><EFBFBD> Windows <20>솚寃쎌뿉<EC8E8C>꽌 硫붿씤 <20>씠踰ㅽ듃猷⑦봽 <20>봽由ъ쭠 諛<> WS heartbeat <20><EFBFBD><20>쑀諛<EC9180> | detectProjectName <20>봽由ъ쭠 |
### [2026-04-09] [Bot/Extension] Discord Signal Relay Failure & Empty Body ### [2026-04-09] [Bot/Extension] Discord Signal Relay Failure & Empty Body
- **증상**: 디스코드 봇은 '자동 승인됨'을 띄우지만 실제 코드 본문이 표시되지 않고, 채널에 진짜 채팅 메시지나 알림이 스팸 큐 뒤에 밀려 전송되지 않음. - **利앹긽**: <EFBFBD><EFBFBD>뒪肄붾뱶 遊뉗<E9818A><EB8997> '<27><EFBFBD><20><EFBFBD><EFBFBD>맖'<27><20><EFBFBD>슦吏<EC8AA6><EFBFBD> <20><EFBFBD>젣 肄붾뱶 蹂몃Ц<EBAA83><20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20>븡怨<EBB8A1>, 梨꾨꼸<EABEA8>뿉 吏꾩쭨 梨꾪똿 硫붿떆吏<EB9686><EFA79E><20>븣由쇱씠 <20><EFBFBD><20><20><EFBFBD>뿉 諛<><E8AB9B><20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>.
- **원인**: 1) observer-script.ts에서 버튼 텍스트 매칭 시 Run 단어의 경계(\b) 처리를 하지 않아 VS Code 하단의 'Running 1 command'를 가로채어 PENDING 스팸 무한 생성. 2) bot.py에서 자동 승인 Embed 생성 시 req.description을 그리지 않고 버튼 텍스트(req.command)만 표시. 3) step-probe.ts에서 세션 교체 시 최근 알림 인덱스 초기화를 잘못하여 세션의 첫 메시지를 무조건 드롭. - **<EFBFBD><EFBFBD>**: 1) observer-script.ts<EFBFBD><EFBFBD>꽌 踰꾪듉 <20><EFBFBD><EFBFBD>듃 留ㅼ묶 <20>떆 Run <20><EFBFBD><EFBFBD>쓽 寃쎄퀎(\b) 泥섎━瑜<E29481> <20>븯吏<EBB8AF> <20><EFBFBD> VS Code <EFBFBD><EFBFBD><EFBFBD> 'Running 1 command'<EFBFBD><>濡쒖콈<EC9296>뼱 PENDING <20><EFBFBD>뙵 臾댄븳 <20><EFBFBD>꽦. 2) bot.py<70><EFBFBD><20><EFBFBD><20><EFBFBD>씤 Embed <20><EFBFBD><20>떆 req.description<6F>쓣 洹몃━吏<E29481> <20>븡怨<EBB8A1> 踰꾪듉 <20><EFBFBD><EFBFBD>(req.command)<EFBFBD> <20><EFBFBD>. 3) step-probe.ts<EFBFBD><EFBFBD><20><EFBFBD>뀡 援먯껜 <20>떆 理쒓렐 <20>븣由<EBB8A3> <20><EFBFBD><EFBFBD>뒪 珥덇린<EB8D87>솕瑜<EC8695> <20>옒紐삵븯<EC82B5><20><EFBFBD><EFBFBD>쓽 泥<> 硫붿떆吏<EB9686><EFBFBD> 臾댁“嫄<E2809C> <20>뱶濡<EBB1B6>.
- **해결**: DOM 감지 정규식에 \b 강제 부여 (/Run\b/), bot.py Auto-Approve Embed 본문에 req.description 렌더링 추가, step-probe.ts에서 session init index -1로 리셋. - **<EFBFBD>빐寃<EFBFBD>**: DOM 媛먯<EFBFBD><EFBFBD> <20>젙洹쒖떇<EC9296>뿉 \b 媛뺤젣 遺<><E981BA> (/Run\b/), bot.py<EFBFBD> Auto-Approve <EFBFBD> Embed 蹂몃Ц<EFBFBD> req.description <EFBFBD><EFBFBD>뜑留<EFBFBD> 異붽<E795B0><EBB6BD>, step-probe.ts<EFBFBD><EFBFBD> session init <EFBFBD> index<EFBFBD> -1<EFBFBD> 由ъ뀑.
- **주의**: Native UI 텍스트 감지 시 단어 경계(\b)까지 검증해야 False Positive를 막을 수 있으며, Auto-Approve는 반드시 본문을 노출해야 함. - **二쇱쓽**: Native UI <EFBFBD><EFBFBD><EFBFBD>듃 媛먯<E5AA9B><EBA8AF> <20><20><EFBFBD>뼱 寃쎄퀎(\b)源뚯<E6BA90><EB9AAF><>利앺빐<EC95BA>빞 False Positive瑜<65> 留됱쓣 <20><20><EFBFBD>쑝硫<EC919D>, Auto-Approve<76>뒗 諛섎뱶<EC848E>떆 蹂몃Ц<EBAA83><20>끂異쒗빐<EC9297><20>븿.
### [2026-04-10] [Extension] AI Response Content Missing (Nested PlannerResponse) ### [2026-04-10] [Extension] AI Response Content Missing (Nested PlannerResponse)
- **증상**: 디스코드 채팅방에 Agent의 텍스트 응답(AI 응답)이 아예 누락되어 전송되지 않음. - **利앹긽**: <EFBFBD><EFBFBD>뒪肄붾뱶 梨꾪똿諛⑹뿉 Agent<6E><20><EFBFBD><EFBFBD><20><EFBFBD>떟(AI <20><EFBFBD>떟)<29><20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>.
- **원인**: GetCascadeTrajectorySteps가 반환하는 plannerResponse가 프로토콜 방식에 따라 최상단(s.plannerResponse)이 아닌 s.step.plannerResponse에 중첩되어 들어올 수 있음. 기존 파서는 하드코딩된 필드 및 플랫 구조만 조회하여 응답을 버림. - **<EFBFBD><EFBFBD>**: GetCascadeTrajectorySteps<EFBFBD> 諛섑솚<EC8491><EFBFBD> plannerResponse<EFBFBD> <20>봽濡쒗넗肄<EB8497> 諛⑹떇<E291B9><20><EFBFBD>씪 理쒖긽<EC9296>(s.plannerResponse)<EFBFBD><20><EFBFBD> s.step.plannerResponse<EFBFBD>뿉 以묒꺽<EBAC92><EFBFBD><20><EFBFBD><EFBFBD><20><20><EFBFBD>쓬. 湲곗〈 <20><EFBFBD><EFBFBD><20><EFBFBD>뱶肄붾뵫<EBB6BE><20><EFBFBD>뱶 諛<> <20><EFBFBD>옯 援ъ“留<E2809C> 議고쉶<EAB3A0><EFBFBD><20><EFBFBD><EFBFBD>쓣 踰꾨┝.
- **주의**: AG RPC 필드명 구조 추측 금지. 필요 시 샌드박스로 두 가지 구조(Flat, Nested) 모두 모킹하여 직접 파싱 확인. - **二쇱쓽**: AG RPC <EFBFBD><EFBFBD>뱶紐<EFBFBD> 援ъ“ 異붿륫 湲덉<E6B9B2><EB8D89>. <20><EFBFBD><20><20><EFBFBD>뱶諛뺤뒪濡<EB92AA> <20>몢 媛<><EFBFBD> 援ъ“(Flat, Nested) 紐⑤몢 紐⑦궧<E291A6><EFBFBD>뿬 吏곸젒 <20><EFBFBD><20><EFBFBD>.
### [2026-04-10] [Extension] Fast Execution `<5s` Response Capture Missed (IDLE-to-IDLE) ### [2026-04-10] [Extension] Fast Execution `<5s` Response Capture Missed (IDLE-to-IDLE)
- **증상**: 디스코드로 내용이 아예 전달되지 않음. `[RT-CAPTURE]`, `[RESPONSE-CAPTURE]` 로그 모두 전혀 남지 않음. - **利앹긽**: <EFBFBD><EFBFBD>뒪肄붾뱶濡<EFBFBD> <20><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD>. `[RT-CAPTURE]`, `[RESPONSE-CAPTURE]` 濡쒓렇 紐⑤몢 <20><EFBFBD><EC9FBE><EFBFBD> <20>궓吏<EAB693> <20><EFBFBD>.
- **원인**: AI 응답이나 코딩 작업이 5초(폴링 주기) 미만으로 매우 빠르게 끝나면, 확장이 `IDLE -> IDLE` 상태만 관찰하며 `wasRunning` 플래그가 `false`로 유지됨. 기존 `[RESPONSE-CAPTURE]` 조건식(`wasRunning && !isRunning && currentCount > ...`) `wasRunning=false`로 인해 블록되어 캡처 자체를 완전히 건너뛰게 됨. - **<EFBFBD><EFBFBD>**: AI <EFBFBD><EFBFBD><EFBFBD><EFBFBD>굹 肄붾뵫 <20><EFBFBD><EFBFBD>씠 5珥<35>(<28>뤃留<EBA483> 二쇨린) 誘몃쭔<EBAA83>쑝濡<EC919D> 留ㅼ슦 鍮좊<E285A4> <20><EFBFBD>굹硫<EAB5B9>, <20><EFBFBD><EFBFBD> `IDLE -> IDLE` <EFBFBD><EFBFBD>깭留<EFBFBD><>李고븯硫<EBB8AF> `wasRunning` <EFBFBD><EFBFBD>옒洹멸<EFBFBD><EFBFBD> `false`<EFBFBD> <20>쑀吏<EC9180><EFA79E>맖. 湲곗〈 `[RESPONSE-CAPTURE]` 議곌굔<EFBFBD>(`wasRunning && !isRunning && currentCount > ...`)<EFBFBD> `wasRunning=false`<EFBFBD> <20><EFBFBD>빐 釉붾줉<EBB6BE><EFBFBD>뼱 罹≪쿂 <20>옄泥대<EFA7A3><EB8C80> <20><EFBFBD><EFBFBD>엳 嫄대꼫<EB8C80>쎇寃<EC8E87> <20>.
- **해결**: `wasRunning` 검증을 삭제하고 `!isRunning && currentCount > lastResponseCaptureStep` 조건으로 완화하여 누락된 step이 있을 때 무조건 캡처하도록 변경. 추가로 오래된 `[RESPONSE-CAPTURE]` 내 하드코딩 파서를 `extractPlannerText`로 일원화 적용. - **<EFBFBD>빐寃<EFBFBD>**: `wasRunning` <EFBFBD>利앹쓣 <20><EFBFBD><EFBFBD>븯怨<EBB8AF> `!isRunning && currentCount > lastResponseCaptureStep` 議곌굔<EFBFBD>쑝濡<EFBFBD> <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>맂 step<65><20><EFBFBD><20>븣 臾댁“嫄<E2809C> 罹≪쿂<E289AA><EFBFBD>룄濡<EBA384><><EFBFBD>. 異붽<E795B0><EBB6BD><EFBFBD> <20><EFBFBD><EFBFBD> `[RESPONSE-CAPTURE]` <EFBFBD><20><EFBFBD>뱶肄붾뵫 <20><EFBFBD>꽌瑜<EABD8C> `extractPlannerText`<EFBFBD> <20><EFBFBD><EFBFBD><20><EFBFBD>.
- **주의**: 폴링 방식에서는 상태(RUNNING->IDLE) 전이를 확신할 수 없으므로, Step Count(인덱스 전진)라는 100% 신뢰 가능한 마커를 통해 새 응답 여부를 감지해야 함. - **二쇱쓽**: <EFBFBD>뤃留<EFBFBD> 諛⑹떇<E291B9><EFBFBD><EFBFBD><20><EFBFBD>깭(RUNNING->IDLE) <20><EFBFBD>씠瑜<EC94A0> <20><EFBFBD><EFBFBD><20><20><EFBFBD>쑝誘<EC919D><EFBFBD>, Step Count(<28><EFBFBD><EFBFBD><20>쟾吏<EC9FBE>)<29><EFBFBD>뒗 100% <20>떊猶<EB968A><><E5AA9B><EFBFBD>븳 留덉빱瑜<EBB9B1> <20><EFBFBD><20><20><EFBFBD><20>뿬遺<EBBFAC><EFBFBD> 媛먯<E5AA9B><EBA8AF><EFBFBD><EFBFBD><20>븿.
### [2026-04-10] [Bot] chat_snapshot_scanner 무한 Abort 및 파일 적체 (Exception 누락)
- **증상**: 봇이 디스코드로 AI 답변(채팅 스냅샷)을 전혀 전송하지 못하고 렉이 걸림. ridge/chat_snapshots/에 처리되지 않은 JSON 파일이 수십 개 적체됨. ### [2026-04-10] [Bot] chat_snapshot_scanner 臾댄븳 Abort 諛<> <20><EFBFBD><20>쟻泥<EC9FBB> (Exception <20><EFBFBD>씫)
- **원인**: ot.py의 chat_snapshot_scanner에서 파일을 순회 파싱할 때 내부의 .unlink() 과정에서 발생하는 예외나 discord.Embed 생성 예외 등을 루프 안에서 잡아주지 못함. 첫 에러 파일(poison pill)을 만나는 순간 루프 전체가 폭파되어 뒤쪽의 정상 파일들도 영원히 처리되지 않고 다음 폴 스케줄에서 다시 첫 파일에 막힘. - **利앹긽**: 遊뉗씠 <20><EFBFBD>뒪肄붾뱶濡<EBB1B6> AI <20>떟蹂<EB969F>(梨꾪똿 <20><EFBFBD><EFBFBD>꺑)<29><20><EFBFBD><EC9FBE><EFBFBD> <20><EFBFBD><EFBFBD>븯吏<EBB8AF> 紐삵븯怨<EBB8AF> <20><EFBFBD>씠 嫄몃┝. ridge/chat_snapshots/<2F>뿉 泥섎━<EC848E>릺吏<EBA6BA> <20><EFBFBD><EBB8A1><EFBFBD> JSON <20><EFBFBD><EFBFBD><20><EFBFBD>떗 媛<> <20>쟻泥대맖.
- **해결**: 루프 내부에 except Exception을 추가하여 전역 예외를 잡아 방어. 실패한 파일은 glob에서 반복 시도되지 않게 .json.failed로 우회(rename)시켜 큐를 비워줌. - **<EFBFBD><EFBFBD>씤**: ot.py<70>쓽 chat_snapshot_scanner<65><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD><EFBFBD><20><20>궡遺<EAB6A1><E981BA> .unlink() 怨쇱젙<EC87B1><EFBFBD>꽌 諛쒖깮<EC9296><EFBFBD><20><EFBFBD><EFBFBD>굹 discord.Embed <20><EFBFBD><20><EFBFBD><20><EFBFBD>쓣 猷⑦봽 <20><EFBFBD><EFBFBD><20><EFBFBD>븘二쇱<E4BA8C><EC87B1> 紐삵븿. 泥<> <20><EFBFBD><20><EFBFBD>씪(poison pill)<29>쓣 留뚮굹<EB9AAE><20>닚媛<EB8B9A> 猷⑦봽 <20>쟾泥닿<EFA7A3><EB8BBF> <20><EFBFBD><EFBFBD><EFBFBD><20>뮘履쎌쓽 <20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>엳 泥섎━<EC848E>릺吏<EBA6BA> <20>븡怨<EBB8A1> <20><EFBFBD><20><20>뒪耳<EB92AA>以꾩뿉<EABEA9><20><EFBFBD>떆 泥<> <20><EFBFBD><EFBFBD>뿉 留됲옒.
- **주의**: 폴링/스캐너 or 루프 내부에서는 개별 아이템 파싱 단계에서 발생 가능한 모든 예외 상태에 대한 Defensive Catch 및 Continue(우회) 로직이 필수임. - **<EFBFBD>빐寃<EFBFBD>**: 猷⑦봽 <20>궡遺<EAB6A1><E981BA>뿉 except Exception<6F>쓣 異붽<E795B0><EBB6BD><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD>쇅瑜<EC8785> <20><EFBFBD>븘 諛⑹뼱. <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EC94AA><EFBFBD> glob<6F><EFBFBD>꽌 諛섎났 <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20>븡寃<EBB8A1> .json.failed濡<64> <20><EFBFBD>쉶(rename)<29>떆耳<EB9686> <20>걧瑜<EAB1A7> 鍮꾩썙以<EC8D99>.
### [2026-04-10] [Extension] GetAllCascadeTrajectories 10-Item Hard Limit Bypass (Signal Drop) - **二쇱쓽**: <20>뤃留<EBA483>/<2F>뒪罹먮꼫 or 猷⑦봽 <20>궡遺<EAB6A1><E981BA><EFBFBD><EFBFBD>뒗 媛쒕퀎 <20><EFBFBD><EFBFBD><20><EFBFBD><20>떒怨꾩뿉<EABEA9>꽌 諛쒖깮 媛<><E5AA9B><EFBFBD>븳 紐⑤뱺 <20><EFBFBD><20><EFBFBD><EFBFBD><20><><EFBFBD><EFBFBD>븳 Defensive Catch 諛<> Continue(<28><EFBFBD>쉶) 濡쒖쭅<EC9296><20><EFBFBD><EFBFBD>엫.
- **<2A><><EFBFBD><EFBFBD>**: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ۼ<EFBFBD><DBBC>ߴ<EFBFBD> { limit: 30 } <20>Ķ<EFBFBD><C4B6><EFBFBD><EFBFBD>Ͱ<EFBFBD> LS <20><EFBFBD><EFBFBD><E5BFA1> <20><><EFBFBD>õǾ<C3B5> <20>ֽ<EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 10<31><30> <20><><EFBFBD>ѿ<EFBFBD> <20>ɷ<EFBFBD> <20>߷<EFBFBD><DFB7><EFBFBD><EFBFBD><EFBFBD>. (Discord<72><64> <20>޽<EFBFBD><DEBD><EFBFBD> <20><> <20><> <20><><EFBFBD>ڵ<EFBFBD> <20><> <20>Ѿ<EFBFBD><D1BE><EFBFBD>).
- **<2A><><EFBFBD><EFBFBD>**: GetAllCascadeTrajectories<65><73> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> pagination <20>ɼ<EFBFBD><C9BC><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>ϰų<CFB0> <20><><EFBFBD><EFBFBD> 10 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>ɸ<EFBFBD>. ### [2026-04-10] [Extension] GetAllCascadeTrajectories 10-Item Hard Limit Bypass (Signal Drop)
- **<EFBFBD>ذ<EFBFBD>**: step-probe.ts<74><73><EFBFBD><EFBFBD> <20>⺻ GetAllCascadeTrajectories<65><73> <20><><EFBFBD>Ҿ<EFBFBD> <20><><EFBFBD><EFBFBD> Ʈ<><C6AE><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E4B8AE> <20><><EFBFBD><EFBFBD><EFBFBD>ϴ<EFBFBD> GetDiagnostics API<50><49> <20><><EFBFBD><EFBFBD> ȣ<><C8A3><EFBFBD>ϰ<EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>Ͽ<EFBFBD> <20>ֽ<EFBFBD> Session ID<49><44> <20><>ġ<EFBFBD><C4A1> <20>ʰ<EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>ϰ<EFBFBD> <20><>. - **증상**: 기존에 작성했던 { limit: 30 } 파라미터가 LS 백엔드에서 무시되어 최신 세션이 10개 제한에 걸려 잘려나감. (Discord로 메시지 단 한 글자도 안 넘어옴).
- **<EFBFBD><EFBFBD><EFBFBD><EFBFBD>**: LS Backend<6E><64><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> RPC<50><43> <20>Ѱ<EFBFBD><D1B0><EFBFBD> Argument <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ȸ<><C8B8><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD>, <20><><EFBFBD><EFBFBD> GetDiagnostics <20><> <20><EFBFBD><E9B5B5> <20><><EFBFBD><EFBFBD><EFBFBD>͸<EFBFBD> Ȱ<><C8B0><EFBFBD><EFBFBD> <20><>. - **원인**: GetAllCascadeTrajectories는 구조적으로 pagination 옵션을 무시하거나 강제 10 제한이 걸림.
- **해결**: step-probe.ts에서 기본 GetAllCascadeTrajectories와 더불어 모든 트래젝토리를 덤프하는 GetDiagnostics API를 병행 호출하고 머지하여 최신 Session ID를 놓치지 않고 추출하게 함.
- **주의**: LS Backend에서 정의한 RPC의 한계상 Argument 조작으로 제한을 회피할 수 없으므로, 향후 GetDiagnostics 등 백도어 데이터를 활용할 것.
### [2026-04-10] [Probe Logging] <20><><EFBFBD> AI<41><EFBFBD><20><EFBFBD><EFBFBD>듃 & WAITING <20><EFBFBD><20><EFBFBD><20><EFBFBD>씫 踰꾧렇
- **利앹긽**: 援됱옣<EB90B1>엳 鍮좊Ⅸ AI <20><EFBFBD>떟(<28><EFBFBD>뒗 利됯컖<EB90AF><EFBFBD><20><20>샇異<EC8387>) <20>떆 `step-probe.ts`媛<> 硫붿떆吏<EB9686><EFA79E><EFBFBD><EFBFBD> <20><EFBFBD><20><EFBFBD><EFBFBD>뼹濡쒓렇瑜<EBA087> 紐⑤몢 Discord濡<64> 由대젅<EB8C80><EFBFBD>븯吏<EBB8AF> 紐삵븿.
- **<2A><EFBFBD>씤**: <20><EFBFBD>떆媛<EB9686> <20><EFBFBD><EFBFBD>듃 罹≪쿂(`delta > 0`) 議곌굔<EAB38C>뿉 `isRunning &&`<60>씠 嫄몃젮<EBAA83><EFBFBD>뼱, <20><EFBFBD>깭媛<EAB9AD> `WAITING`<60><EFBFBD>굹 `IDLE`濡<> 利됱떆 <20><EFBFBD>뼱媛<EBBCB1><EFBFBD> <20><EFBFBD><EFBFBD>듃瑜<EB9383> 罹≪쿂<E289AA><EFBFBD>뒗 猷⑦떞<E291A6><20>쟾遺<EC9FBE> <20><EFBFBD><EFBFBD>맖. <20><EFBFBD><20><20>닚媛<EB8B9A> `isStall` 議곌굔<EAB38C><20><><EFBFBD><EFBFBD> <20><EFBFBD>븘 `WAITING` <20><EFBFBD><EFBFBD><EFBFBD>룄 利앸컻<EC95B8>븿.
- **<2A>빐寃<EBB990>**: <20><EFBFBD>떆媛<EB9686> 罹≪쿂 濡쒖쭅<EC9296><EFBFBD>꽌 `isRunning &&` 議곌굔<EAB38C><20>젣嫄고븯怨<EBB8AF>, `delta > 0`<60><20>븣 異붽<E795B0><EBB6BD><EFBFBD>맂 理쒖떊 <20><EFBFBD><EFBFBD><20>뒪罹뷀븯硫댁꽌 `PLANNER_RESPONSE`<60><><EFBFBD> `WAITING` <20><EFBFBD><EFBFBD>쓣 紐⑤몢 泥섎━<EC848E><EFBFBD>룄濡<EBA384> <20><EFBFBD><EFBFBD>븿.
- **二쇱쓽**: LS Backend 10媛<30> Session <20><EFBFBD>븳 踰꾧렇媛<EBA087> <20><EFBFBD>뼱, <20>떎瑜<EB968E> 李쎌뿉<EC8E8C><20><EFBFBD>룞 梨꾪똿(`1fbca84c`)<29>씠 IDLE濡<45> <20><EFBFBD><EFBFBD><EFBFBD>쑝硫<EC919D> <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>뀡怨<EB80A1> <20>뿷媛덈┫ <20><20><EFBFBD><EFBFBD>굹, <20>씠 踰꾧렇<EABEA7>뒗 polling <20><><EFBFBD><EFBFBD>씠諛<EC94A0> 臾몄젣<EBAA84><ECA0A3><EFBFBD><EFBFBD>쓬.
### [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 타이밍 문제였음.
### [2026-04-10] [Extension] AI Response Missing for New Sessions (Session Tracking Failure) ### [2026-04-10] [Extension] AI Response Missing for New Sessions (Session Tracking Failure)
- **증상**: 새로운 대화(Session) 시작 시 첫 AI 응답 텍스트가 디스코드에 전혀 전송되지 않는 현상. - **利앹긽**: <EFBFBD>깉濡쒖슫 <20><><EFBFBD><EFBFBD>솕(Session) <20><EFBFBD><20>떆 泥<> AI <20><EFBFBD><20><EFBFBD><EFBFBD>듃媛<EB9383> <20><EFBFBD>뒪肄붾뱶<EBB6BE><20><EFBFBD><EC9FBE><EFBFBD> <20><EFBFBD><EFBFBD>릺吏<EBA6BA> <20><EFBFBD><20><EFBFBD>.
- **원인**: 백엔드의 `GetAllCascadeTrajectories` 10개 세션만 반환하여 새 세션이 누락됨. 이를 보완하기 위해 `brain/` 디렉토리를 스캔하는 Fallback 로직이 동작했으나, 신규 세션의 첫 단계에서 `GetCascadeTrajectorySteps`(stepOffset: 0) 호출 시 내부 응답(UTF-8 파싱 등) 에러로 인해 Exception이 발생, `trajectorySummaries`에 세션이 아예 등록되지 않음. 세션이 추적되지 않으니 `delta > 0` 기반의 응답 캡처가 발생하지 않음. - **<EFBFBD><EFBFBD>씤**: 諛깆뿏<EAB986><EFBFBD> `GetAllCascadeTrajectories`<EFBFBD> 10<EFBFBD> <20><EFBFBD>뀡留<EB80A1> 諛섑솚<EC8491><EFBFBD><20><20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD>맖. <20>씠瑜<EC94A0> 蹂댁셿<EB8C81>븯湲<EBB8AF> <20><EFBFBD>빐 `brain/` <20><EFBFBD><EFBFBD>넗由щ<E794B1><D189> <20>뒪罹뷀븯<EBB780>뒗 Fallback 濡쒖쭅<EC9296><20><EFBFBD><EFBFBD><EFBFBD><EFBFBD>굹, <20>떊洹<EB968A> <20><EFBFBD><EFBFBD>쓽 泥<> <20>떒怨꾩뿉<EABEA9> `GetCascadeTrajectorySteps`(stepOffset: 0) <EFBFBD>샇異<EFBFBD> <20><20>궡遺<EAB6A1> <20><EFBFBD>떟(UTF-8 <20><EFBFBD><20>벑) <20><EFBFBD>윭濡<EC9CAD> <20><EFBFBD> Exception<EFBFBD>씠 諛쒖깮, `trajectorySummaries`<EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD><20>벑濡앸릺吏<EBA6BA> <20><EFBFBD>쓬. <20><EFBFBD><EFBFBD>씠 異붿쟻<EBB6BF>릺吏<EBA6BA> <20><EFBFBD><EFBFBD>땲 `delta > 0` 湲곕컲<EAB395><20><EFBFBD>떟 罹≪쿂媛<ECBF82> 諛쒖깮<EC9296>븯吏<EBB8AF> <20><EFBFBD>.
- **해결**: `step-probe.ts` Fallback 2 `catch` 블록에서 에러가 발생하더라도 강제로 `stepCount: 1`로 세션을 등록하도록 패치하여 세션 인식 유실 방지. - **<EFBFBD>빐寃<EFBFBD>**: `step-probe.ts`<EFBFBD> Fallback 2 `catch` 釉붾줉<EFBFBD><EFBFBD><20><EFBFBD>윭媛<EC9CAD> 諛쒖깮<EC9296><EFBFBD><EFBFBD><EFBFBD>룄 媛뺤젣濡<ECA0A3> `stepCount: 1`濡<> <20><EFBFBD><EFBFBD><20>벑濡앺븯<EC95BA>룄濡<EBA384> <20>뙣移섑븯<EC8491><20><EFBFBD><20><EFBFBD><20><EFBFBD>떎 諛⑹<E8AB9B><E291B9>.
- **주의**: API 호출 실패를 조용히 `catch`로 넘기면 전체 파이프라인(여기서는 상태 폴링)이 해당 데이터를 영원히 무시하게 되는 치명적 버그가 발생함. 장애 허용 설계 시 기본값 복원(Fallback State) 설정 필수. - **二쇱쓽**: API <EFBFBD>샇異<EFBFBD> <20><EFBFBD>뙣瑜<EB99A3> 議곗슜<EAB397>엳 `catch`濡<> <20>꽆湲곕㈃ <20>쟾泥<EC9FBE> <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD>씤(<28>뿬湲곗꽌<EAB397><20><EFBFBD><20>뤃留<EBA483>)<29><20><EFBFBD><20><EFBFBD><EFBFBD>꽣瑜<EABDA3> <20><EFBFBD><EFBFBD>엳 臾댁떆<EB8C81>븯寃<EBB8AF> <20><EFBFBD>뒗 移섎챸<EC848E>쟻 踰꾧렇媛<EBA087> 諛쒖깮<EC9296>븿. <20><EFBFBD><20><EFBFBD><20>꽕怨<EABD95> <20>떆 湲곕낯媛<EB82AF> 蹂듭썝(Fallback State) <EFBFBD><EFBFBD><20><EFBFBD>.
### [2026-04-10] [Extension] Trigger-Click False Positives & Button Matching Failure ### [2026-04-10] [Extension] Trigger-Click False Positives & Button Matching Failure
- **증상**: 디스코드에서 승인(Approve)을 누르면, 에이전트 확장 프로그램이 알맞은 버튼(예: `Always run`)을 누르지 못하거나, 엉뚱한 버튼(예: 상단의 `Running1 command`)을 눌러버려 실제 승인 처리가 누락되는 현상. - **利앹긽**: <EFBFBD><EFBFBD>뒪肄붾뱶<EFBFBD><EFBFBD><20><EFBFBD>씤(Approve)<29><20>늻瑜대㈃, <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><20>봽濡쒓렇<EC9293><EFBFBD><20>븣留욎<EFA78D><EC9A8E> 踰꾪듉(<28>삁: `Always run`)<29><20>늻瑜댁<E7919C><EB8C81> 紐삵븯嫄곕굹, <20><EFBFBD><EFBFBD>븳 踰꾪듉(<28>삁: <20><EFBFBD><EFBFBD>쓽 `Running1 command`)<29><20><EFBFBD>윭踰꾨젮 <20><EFBFBD><20><EFBFBD>씤 泥섎━媛<E29481> <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD>.
- **원인**: 1) UI 버튼 텍스트에 `keyboard_arrow_up` 등 머티리얼 아이콘 텍스트가 접착(`Always runkeyboard_arrow_up`)되어 정규식이 실패할 것을 우려해 단어 경계(`\b`)를 제거한 패치가 원인. 단어 경계가 사라지면서 `/Run/i` 패턴이 `Running1 command` 같은 다른 상태 텍스트 버튼에 오탐(False Positive). 2) DOM 순서상 상태 텍스트 버튼이 앞서 있으므로 오탐된 버튼이 우선 클릭됨. - **<EFBFBD><EFBFBD>**: 1) UI 踰꾪듉 <20><EFBFBD><EFBFBD><EFBFBD> `keyboard_arrow_up` <EFBFBD>벑 癒명떚由ъ뼹 <20><EFBFBD>씠肄<EC94A0> <20><EFBFBD><EFBFBD>듃媛<EB9383> <20>젒李<ECA092>(`Always runkeyboard_arrow_up`)<EFBFBD><EFBFBD><20>젙洹쒖떇<EC9296><20><EFBFBD><EFBFBD>븷 寃껋쓣 <20><EFBFBD><EFBFBD><20><EFBFBD>뼱 寃쎄퀎(`\b`)瑜<> <20>젣嫄고븳 <20>뙣移섍<E7A7BB><EC848D> <20><EFBFBD>씤. <20><EFBFBD>뼱 寃쎄퀎媛<ED808E> <20><EFBFBD>씪吏<EC94AA>硫댁꽌 `/Run/i` <EFBFBD><EFBFBD><EFBFBD> `Running1 command` 媛숈<EFBFBD><EFBFBD> <20>떎瑜<EB968E> <20><EFBFBD><20><EFBFBD><EFBFBD>듃 踰꾪듉<EABEAA><20><EFBFBD>(False Positive)<EFBFBD>. 2) DOM <EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><20><EFBFBD><EFBFBD>듃 踰꾪듉<EABEAA><20><EFBFBD><20><EFBFBD>쑝誘<EC919D><EFBFBD> <20><EFBFBD><EFBFBD>맂 踰꾪듉<EABEAA><20><EFBFBD><20>겢由<EAB2A2><E794B1>.
- **해결**: `trigger-click` 로직 실행 전 버튼의 `textContent`에서 `keyboard_arrow_up` 등 알려진 꼬리 아이콘 문자열을 명시적으로 제거(strip)하고, 모든 트리거 정규식에 다시 단어 경계(`\b`)를 강제 삽입하여 오탐을 원천 차단함. - **<EFBFBD>빐寃<EFBFBD>**: `trigger-click` 濡쒖쭅 <20><EFBFBD><20>쟾 踰꾪듉<EABEAA> `textContent`<EFBFBD><EFBFBD> `keyboard_arrow_up` <EFBFBD><20><EFBFBD>젮吏<ECA0AE> 瑗щ━ <20><EFBFBD>씠肄<EC94A0> 臾몄옄<EBAA84><EFBFBD>쓣 紐낆떆<EB8286><EFBFBD>쑝濡<EC919D> <20>젣嫄<ECA0A3>(strip)<29>븯怨<EBB8AF>, 紐⑤뱺 <20>듃由ш굅 <20>젙洹쒖떇<EC9296><20><EFBFBD><20><EFBFBD>뼱 寃쎄퀎(`\b`)瑜<> 媛뺤젣 <20><EFBFBD><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><20>썝泥<EC8D9D> 李⑤떒<E291A4>븿.
- **주의**: UI 요소를 DOM에서 긁어올 때는 텍스트에 숨겨진 아이콘/웹폰트 리거쳐(ligatures)가 없는지 검토해야 함. 패턴 매칭 시 꼬리표를 먼저 제거하고 명확한 경계를 부여할 것. - **二쇱쓽**: UI <EFBFBD><EFBFBD>냼瑜<EFBFBD> DOM<4F><EFBFBD>꽌 湲곸뼱<EAB3B8><20><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><20>닲寃⑥쭊 <20><EFBFBD>씠肄<EC94A0>/<2F><EFBFBD><EFBFBD>듃 由ш굅爾<EAB585>(ligatures)媛<> <20><EFBFBD>뒗吏<EB9297><><E5AF83><EFBFBD><EFBFBD><20>븿. <20><EFBFBD>꽩 留ㅼ묶 <20>떆 瑗щ━<D189>몴瑜<EBAAB4> 癒쇱<E79992><EC87B1> <20>젣嫄고븯怨<EBB8AF> 紐낇솗<EB8287>븳 寃쎄퀎瑜<ED808E><><E981BA><EFBFBD>븷 寃<>.
### [2026-04-10] [Extension] Ghost Session Hijack & Infinite Polling Loop (trajectory not found) ### [2026-04-10] [Extension] Ghost Session Hijack & Infinite Polling Loop (trajectory not found)
- **증상**: 신규 작업 시 '신호안들어와' (Discord로 릴레이 안 됨). 로그에 500 error trajectory not found 무한 반복.\n- **원인**: Antigravity가 작업하면서 brain/에 36글자 폴더를 생성하는데, Cascade가 아니므로 GetCascadeTrajectorySteps에서 500 에러를 냅니다. 하지만 이전 신규 세션 유실 방지 패치가 이 Ghost 세션을 RUNNING으로 강제 등록하면서, 활성 세션(activeSessionId)을 탈취하고 무한 에러 루프에 빠지게 만들었습니다.\n- **해결**: step-probe.ts에서 폴백 등록 시 error message에 'trajectory not found'가 포함되면 Ghost 세션으로 간주해 강제 등록(continue)을 건너뛰게 하고, Stall Probe 에러 catch에서도 UTF-8 에러가 아니면 stallProbed=true를 주어 재시도 무한 루프를 완전히 끊어냈습니다.\n- **주의**: uuid 길이(36자)만으로 디렉토리를 식별할 때 Antigravity와 Google Agent가 모호해질 수 있으므로, 반드시 Backend 응답의 확실한 에러(trajectory not found) 메시지로 예외 판별을 해야 합니다.\n
- **利앹긽**: <20>떊洹<EB968A> <20><EFBFBD><20>떆 '<27><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EBBCB1><EFBFBD>' (Discord濡<64> 由대젅<EB8C80><20><20>맖). 濡쒓렇<EC9293>뿉 500 error trajectory not found 臾댄븳 諛섎났.\n- **<EFBFBD><EFBFBD>씤**: Antigravity媛<79> <20><EFBFBD><EFBFBD>븯硫댁꽌 brain/<2F>뿉 36湲<36><E6B9B2><20><EFBFBD>뜑瑜<EB9C91> <20><EFBFBD><EFBFBD><EFBFBD><EFBFBD>뜲, Cascade媛<65> <20><EFBFBD>땲誘<EB95B2><EFBFBD> GetCascadeTrajectorySteps<70><EFBFBD>꽌 500 <20><EFBFBD>윭瑜<EC9CAD> <20><EFBFBD><EFBFBD>떎. <20>븯吏<EBB8AF><EFBFBD> <20><EFBFBD><20>떊洹<EB968A> <20><EFBFBD><20><EFBFBD>떎 諛⑹<E8AB9B><E291B9> <20>뙣移섍<E7A7BB><EC848D> <20>씠 Ghost <20><EFBFBD><EFBFBD>쓣 RUNNING<4E>쑝濡<EC919D> 媛뺤젣 <20>벑濡앺븯硫댁꽌, <20><EFBFBD><20><EFBFBD>뀡(activeSessionId)<29><20>깉痍⑦븯怨<EBB8AF> 臾댄븳 <20><EFBFBD>윭 猷⑦봽<E291A6>뿉 鍮좎<E98DAE><ECA28E><EFBFBD> 留뚮뱾<EB9AAE><EFBFBD><EFBFBD><EFBFBD>떎.\n- **<EFBFBD>빐寃<EFBFBD>**: step-probe.ts<74><EFBFBD><20>뤃諛<EBA483> <20>벑濡<EBB291> <20>떆 error message<67>뿉 'trajectory not found'媛<> <20><EFBFBD>븿<EFBFBD>릺硫<EBA6BA> Ghost <20><EFBFBD><EFBFBD>쑝濡<EC919D> 媛꾩<EABEA9>빐 媛뺤젣 <20>벑濡<EBB291>(continue)<29>쓣 嫄대꼫<EB8C80>쎇寃<EC8E87> <20>븯怨<EBB8AF>, Stall Probe <20><EFBFBD>윭 catch<63><EFBFBD><EFBFBD>룄 UTF-8 <20><EFBFBD>윭媛<EC9CAD> <20><EFBFBD>땲硫<EB95B2> stallProbed=true瑜<65> 二쇱뼱 <20><EFBFBD><EFBFBD>룄 臾댄븳 猷⑦봽瑜<EBB4BD> <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>떎.\n- **二쇱쓽**: uuid 湲몄씠(36<33>옄)留뚯쑝濡<EC919D> <20><EFBFBD><EFBFBD>넗由щ<E794B1><D189> <20>떇蹂꾪븷 <20>븣 Antigravity<74><79><EFBFBD> Google Agent媛<74> 紐⑦샇<E291A6>빐吏<EBB990> <20><20><EFBFBD>쑝誘<EC919D><EFBFBD>, 諛섎뱶<EC848E>떆 Backend <20><EFBFBD><EFBFBD><20><EFBFBD><EFBFBD><20><EFBFBD>윭(trajectory not found) 硫붿떆吏<EB9686><EFBFBD> <20><EFBFBD><20>뙋蹂꾩쓣 <20><EFBFBD><20><EFBFBD><EFBFBD>떎.\n

View File

@@ -0,0 +1,205 @@
# Observer Script 개발 가이드 — SSOT
> **이 문서는 Observer 코드 변경 전 반드시 확인하는 SSOT입니다.**
> 모든 Observer 관련 설계, 배포, 제약사항이 이 문서에 있습니다.
---
## 1. Observer 코드 특성
### 1.1 실행 환경
- Observer는 **workbench-jetski-agent.html**에 인라인 `<script>`로 삽입됨
- **AG Native의 Electron 렌더러 프로세스**에서 실행 (VS Code extension host가 아님)
- 렌더러는 **strict mode 아님** (확인 필요), 하지만 V8 parser는 일부 strict-like 규칙 적용
- `generateApprovalObserverScript(port)` 함수가 **TypeScript template literal**로 스크립트 생성
### 1.2 코드 작성 규칙 (위반 시 Observer 전체 크래시)
| 규칙 | 이유 | 예시 |
|------|------|------|
| **for 루프 안에 function 선언 금지** | V8 strict mode error | `var fn = function(){}` 사용 |
| **문자열 리터럴에 특수문자 금지** | template literal 이스케이핑 깨짐 | `'??'``'MAX'` |
| **regex에 `\\s` 등 이스케이프 금지** | template literal이 `\\\\s``\\s` (리터럴) | 문자열 비교 사용 |
| **ES6+ 구문 금지** | 구 V8 호환 | `var` 사용, `let/const/arrow` 금지 |
| **배포 전 SYNTAX CHECK 필수** | Observer 크래시 방지 | 아래 검증 명령어 참조 |
### 1.3 필수 검증 명령어 (모든 빌드 전 실행)
```powershell
npm.cmd run compile; node -e "const {generateApprovalObserverScript}=require('./out/observer-script'); let s=generateApprovalObserverScript(18080); try { new Function(s); console.log('SYNTAX OK'); } catch(e) { console.log('ERROR:', e.message); }"
```
**SYNTAX OK가 나오지 않으면 절대 배포하지 않는다.**
### 1.4 배포 전 자기검증 체크리스트 (MANDATORY)
**재시작을 요구하기 전 반드시 다음을 모두 통과해야 한다:**
1. [ ] **SYNTAX CHECK 통과**: `new Function(s)``SYNTAX OK`
2. [ ] **수정 방향 검증**: 이 수정이 문제를 해결하는 올바른 접근인지 스스로 2번 재검증
3. [ ] **template literal 규칙 위반 없음**: regex 이스케이프, 특수문자, function 선언 등
4. [ ] **변경 범위 최소화**: 불필요한 코드 포함 여부 확인
5. [ ] **재시작 사유 명시**: 사용자에게 (a) 무엇을 수정했고 (b) 왜 재시작이 필요한지 1~2줄로 설명
6. [ ] **재시작 횟수 명시**: Observer 변경 = 2회, Extension host만 변경 = 1회
7. [ ] **log() relay 필터 확인**: 새 로그 키워드 추가 시 log() 함수의 키워드 필터에도 추가했는지 확인 (섹션 3.5 참조)
8. [ ] **regex E2E 테스트**: Observer에서 사용하는 새 regex는 생성된 코드에서 직접 실행하여 매칭 검증
9. [ ] **구현 전 가정 검증**: 새 접근을 코딩하기 전에, 핵심 가정이 성립하는지 로그 1줄로 먼저 확인 (예: "Step Probe가 WAITING을 볼 수 있는가?" → `STEP-PROBE.*WAITING` 로그 검색)
**정당한 사유 없이 재시작을 요구하지 않는다.**
**DOM 구조를 먼저 파악하고 설계한 후 코드를 작성한다.**
**시행착오식(trial-and-error) 접근을 하지 않는다.**
**추측으로 코딩하지 않는다. 로그/데이터로 확인한 사실에 기반하여 코딩한다.**
---
## 2. 배포 프로세스
### 2.1 Observer 코드가 포함된 변경
Observer 코드 변경은 **extension host 코드 변경보다 비용이 높다**:
```
VSIX 빌드 → VSIX 설치 → AG 재시작 #1 (extension이 HTML 패치)
→ AG 재시작 #2 (패치된 HTML 로드) → Observer 실행
```
**총 2번 AG 재시작 필요**
### 2.2 Extension host 코드만 변경 (approval-handler, http-bridge 등)
```
VSIX 빌드 → VSIX 설치 → AG 재시작 #1 → 즉시 적용
```
**1번 AG 재시작 필요**
### 2.3 VSIX 설치 확인
```powershell
Get-ChildItem "$env:USERPROFILE\.vscode\extensions" -Filter "*gravity*" -Directory |
ForEach-Object { $j = Get-Content (Join-Path $_.FullName "package.json") | ConvertFrom-Json; "$($_.Name): v$($j.version)" }
```
### 2.4 HTML 패치 확인 (Observer 코드가 반영되었는지)
```powershell
Select-String -Path "$env:LOCALAPPDATA\Programs\Antigravity\resources\app\out\vs\code\electron-browser\workbench\workbench-jetski-agent.html" -Pattern "검색할_함수명" -Quiet
```
---
## 3. AG Native DOM 구조
### 3.1 Chat Panel (Observer가 접근 가능)
- 위치: `document.querySelector('#conversation')` 또는 `document.querySelector('[class*="conversation"]')`
- AI 응답 블록: `.leading-relaxed.select-text`
- 사용자 메시지 블록: `.select-text.rounded-lg` (v0.5.74+)
- Thinking 블록: 조상에 `max-h-[200px]` 클래스 있음 → 필터링
### 3.2 승인 버튼 — "Always run" (v0.5.93 BTN-DOM-DUMP 확인)
실제 DOM 구조 (v0.5.92 로그로 확인):
- d0: button.flex.cursor-pointer (Always run 버튼)
- d1: div.min-w-0
- d2: div.flex.items-center.justify-between.rounded-b.border-t (버튼 바)
- d3: div (이름 없는 컨테이너)
- div.mb-1 = "Running command" (헤더)
- div.flex = " gravity_control > ..." (실제 명령어, plain div!)
- div.flex = "Always run Cancel" (버튼들)
> 명령어는 pre.font-mono나 code가 아닌 plain div.flex에 있음.
> v30: "Running command" div의 형제를 탐색하여 프롬프트 마커 뒤의 명령어 추출.
- "Retry" 버튼: button 태그, chat panel 내
### 3.3 Diff Review (Accept all / Reject all) — Observer 접근 가능 (v0.5.101+)
- **v0.5.101 이전**: 에디터 webview에 렌더링, Observer document에서 접근 불가
- **v0.5.101 이후**: AG UI 업데이트로 chat panel footer에 `<span class="cursor-pointer">` 태그로 렌더링
- Observer의 `allBtns` 선택자에 `span.cursor-pointer` 포함 필수
- `matchedType = 'diff_review'`로 분류됨 (L1120: `txt.includes('Accept')`)
- auto-approve response 파일에 `_from_ws: true` 마커 필수 (processResponseFile race condition 방지)
### 3.4 DOM 렌더링 타이밍
- "Always run" 버튼이 DOM에 나타날 때 명령어 div도 함께 렌더링됨
- v30의 "Running command" div 탐색은 즉시 성공
### 3.5 log() relay 필터 규칙
Observer의 log() 함수는 키워드 필터로 일부 로그만 extension.log에 relay.
새 로그 키워드 추가 시 반드시 필터도 함께 수정해야 함.
현재 필터 키워드 (v0.5.92+):
CV-CLASSES, CV-CHILDREN, child[, CV found, Conversation view,
BEACON, ERROR, chat relay, user-cls,
CONTEXT, BTN-DOM, DEFERRED, DETECTED
---
## 4. 파일 경로 매핑
| 항목 | 경로 |
|------|------|
| AG 설치 경로 | `$env:LOCALAPPDATA\Programs\Antigravity\` |
| workbench HTML | `...\resources\app\out\vs\code\electron-browser\workbench\workbench-jetski-agent.html` |
| Extension 로그 | `$env:USERPROFILE\.gemini\antigravity\bridge\extension.log` |
| Pending 파일 | `$env:USERPROFILE\.gemini\antigravity\bridge\pending\*.json` |
| Response 파일 | `$env:USERPROFILE\.gemini\antigravity\bridge\response\*.json` |
| VSIX extensions | `$env:USERPROFILE\.vscode\extensions\variet.gravity-bridge-*\` |
| HTTP Bridge 포트 | 34332 (또는 `getDeterministicPort('gravity_control')`) |
| Discord 채널 | `#ag-gravity_control` (ID: 1483082084540223663) |
---
## 5. 과거 실수 및 교훈
### 5.1 VSIX 미설치 (v0.5.78~83)
- **증상**: 빌드만 하고 `code --install-extension` 실행 안 함
- **결과**: 설치된 버전이 v0.5.50, 모든 수정사항 미적용
- **교훈**: 빌드 후 반드시 `code --install-extension *.vsix --force` 실행
- **확인**: `Get-ChildItem "$env:USERPROFILE\.vscode\extensions" -Filter "*gravity*"`
### 5.2 function 선언 → Observer 크래시 (v0.5.84~86)
- **증상**: `function _isGenericDesc(d){}` 를 for 루프 내부에 선언
- **결과**: Observer 전체 크래시, chat relay + auto-approve 중단
- **교훈**: `var fn = function(){}` 사용, 배포 전 SYNTAX CHECK 필수
### 5.3 깨진 문자열 리터럴 (v0.5.86)
- **증상**: `{tag:'??',...}` (특수문자가 따옴표 깨뜨림)
- **결과**: SYNTAX ERROR, Observer 미작동
- **교훈**: template literal 안에서 특수문자/이모지 사용 주의
### 5.4 regex 이스케이핑 실패 (v0.5.83~84)
- **증상**: `/Always\\s+run/` → 생성 시 `\\s` (리터럴 백슬래시+s)로 출력
- **결과**: "Always run" 매칭 실패
- **교훈**: template literal 안에서 regex 대신 **문자열 비교** 사용
### 5.5 _from_ws 파일 무한 누적 (v0.5.78~84)
- **증상**: response 파일에 `_from_ws: true` 마커 → processResponseFile이 스킵 → 영원히 삭제 안 됨
- **결과**: 3초마다 4개 파일 × SKIP 로그 → 로그 스팸, 다른 처리 방해
- **교훈**: 보존 파일에는 반드시 **TTL(자동 만료)** 추가
---
## 6. 디버깅 체크리스트
### Observer가 작동하지 않을 때
1. `extension.log`에서 `setup complete` 확인 → Observer 로드 여부
2. `OBSERVER-LOG` 패턴 검색 → 스캔 활동 여부
3. `HTTP-REQ` 검색 → HTTP bridge에 요청 도달 여부
4. **SYNTAX CHECK** 실행 → 생성 스크립트 문법 검증
5. `SKIP _from_ws` 반복 확인 → stale response 파일 정리
### Discord에 메시지가 안 올 때
1. `POST /chat` 검색 → chat relay 전송 여부
2. `WS.*send` 검색 → WebSocket 전송 여부
3. Discord API로 직접 확인: `node extension/scratch/discord_read.js`
4. stale response 파일 확인: `Get-ChildItem $env:USERPROFILE\.gemini\antigravity\bridge\response\*.json`
---
## 7. 버전 히스토리 요약
| 버전 | 핵심 변경 | 결과 |
|------|----------|------|
| v0.5.50 | 기본 릴레이 시스템 | ✅ 안정 |
| v0.5.78 | `_from_ws` 마커 (Retry 보존) | ✅ 작동 (TTL 미구현) |
| v0.5.79 | sibling 탐색 + thinking 필터 | ✅ 작동 |
| v0.5.80~81 | Accept all offsetParent 완화 | ❌ 구조적 불가 (에디터 webview) |
| v0.5.82 | 버튼 셀렉터 확장 + ACCEPT-SCAN | 진단용 |
| v0.5.83 | DEFERRED 컨텍스트 500ms | regex 이스케이핑 실패 |
| v0.5.84 | regex → 문자열 비교 | function 선언 크래시 |
| v0.5.85 | `_from_ws` TTL 60초 | ✅ stale 정리 |
| v0.5.86 | function → var expression | 깨진 문자열 미발견 |
| v0.5.87 | 깨진 문자열 2건 수정 | ✅ SYNTAX OK |

View File

@@ -0,0 +1,216 @@
# AG Native 릴레이 아키텍처 분석
> **이 문서는 AG Native ↔ Discord 릴레이의 데이터 흐름 SSOT입니다.**
> 구현/디버깅 전 반드시 확인합니다.
---
## 1. 데이터 경로 요약
AG Native에서 Discord로 메시지를 전달하는 경로는 크게 2개:
| # | 경로 | 소스 | 실시간? | 상태 |
|---|------|------|---------|------|
| 1 | **Observer DOM** | workbench.html 인라인 스크립트 → DOM 관찰 → HTTP POST → http-bridge | ✅ 실시간 | AI 응답: ✅ 작동 (v0.5.72+), 사용자 메시지: ✅ 작동 (v0.5.74+) |
| 2 | **Step Probe (trajectory API)** | LS RPC `GetCascadeTrajectorySteps` → step 분석 | ❌ cascade 완료 후에만 | AI 응답: ❌ 실시간 불가, 사용자 메시지: ❌ 실시간 불가 |
### 1.1 핵심 API 제약 (2026-04-18 확인)
> [!CAUTION]
> **`GetCascadeTrajectorySteps`는 진행 중인 cascade의 step을 실시간으로 반환하지 않습니다.**
> step count는 cascade가 **완전히 종료**(IDLE 전환)된 후에만 업데이트됩니다.
> 따라서 Step Probe의 RT-CAPTURE, HB-CAPTURE 모두 **현재 진행 중인 대화에서는 작동하지 않습니다.**
**검증 데이터**:
- POLL에서 `status=CASCADE_RUN_STATUS_IDLE`, `steps=928`, `delta=0` 고정
- HEARTBEAT probe: `offset=927 got=1 real=928 known=928` → 변함 없음
- 실제로 수십 개의 tool call이 실행되었지만 step count 불변
- Cascade 종료 후 다음 poll에서 step count가 점프 (예: 733 → 865 → 928)
### 1.2 AG Native SDK EventMonitor
SDK에 이벤트 시스템이 있으나 **모두 polling 기반**:
- `EventMonitor.onStepCountChanged` — getDiagnostics 기반 polling
- `EventMonitor.onActiveSessionChanged` — state.vscdb 기반 polling
- **실시간 push (WebSocket/SSE)는 없음**
- 현재 상태: ERR_CONNECTION_REFUSED 문제로 비활성화됨
---
## 2. Observer DOM 경로 상세
### 2.1 Observer 스크립트 삽입 체인
```
extension.ts activate()
→ html-patcher.ts setupApprovalObserver()
→ observer-script.ts generateApprovalObserverScript(port)
→ workbench-jetski-agent.html에 인라인 <script> 삽입
→ AG 재시작 시 렌더러가 로드
```
### 2.2 Observer 주요 함수
| 함수 | 역할 |
|------|------|
| `scanChatBodies()` | 3초마다 실행, conversation view에서 메시지 블록 탐색 |
| `extractCleanStepText(el)` | DOM 클론 → style/script/button 제거 → textContent 추출 |
| `extractContextFromNearby(btn)` | 승인 버튼 주변 DOM에서 명령어 텍스트 추출 (v23: sibling 탐색 포함) |
| `pollResponseGroup(rid, btnRefs)` | response 파일 polling → 버튼 자동 클릭 |
### 2.3 AI 응답 감지 셀렉터
```javascript
var responseBlocks = cv.querySelectorAll(
'.leading-relaxed.select-text, ' // ← AI 응답 마크다운 블록 (주력)
+ '.text-ide-message-block-user-color, ' // ← 사용자 메시지 (미매칭)
+ '.text-ide-message-block-bot-color, ' // ← NUX tooltip 전용 (오매칭)
+ '.bg-ide-message-block-user-background, '// ← 사용자 메시지 (미매칭)
+ '[data-message-role="user"], ' // ← 사용자 메시지 (미매칭)
+ '[data-role="user"]' // ← 사용자 메시지 (미매칭)
);
```
> [!NOTE]
> **v0.5.74에서 사용자 메시지 셀렉터가 추가되었습니다.**
> AG Native 소스(`jetskiAgent/main.js`)의 `Esn` 컴포넌트 분석으로
> 사용자 메시지 CSS 클래스(`msn = "bg-gray-500/10 border border-gray-500/20 p-2 rounded-lg w-full text-sm select-text"`)를 식별.
> 셀렉터: `.select-text.rounded-lg`, 역할 판별: `rounded-lg` 있고 `leading-relaxed` 없으면 → user
### 2.4 AI 응답 추출 흐름
```
scanChatBodies() 3초 간격
→ cv = document.querySelector('#conversation')
→ responseBlocks = cv.querySelectorAll('.leading-relaxed.select-text, ...')
→ lastBlock = responseBlocks[last] (가장 최근 블록)
→ 이미 scrape 됐으면 skip
→ blockText = extractCleanStepText(lastBlock)
→ 안정화 대기 (3초 동안 텍스트 변경 없으면)
→ POST /chat { text, source, block_index, role }
→ http-bridge → writeChatSnapshot() → WS → Discord
```
### 2.5 Observer 업데이트 제약
> [!CAUTION]
> **Observer 코드는 workbench.html에 인라인 삽입됩니다.**
> extension reload만으로는 Observer 코드가 업데이트되지 않습니다.
> **AG 재시작 + V8 CachedData 삭제**가 필요합니다.
> (단, product.json 체크섬이 맞으면 CachedData 삭제 없이 AG 재시작만으로 충분할 수 있음)
---
## 3. 승인 버튼 (Auto-Approve) 경로
### 3.1 "Always run" 자동 승인 흐름
```
Observer DOM scan
→ "Always run" 버튼 텍스트 감지
→ POST /pending { command: "Always run", description: "...", buttons: [...] }
→ http-bridge _handlePending()
→ alwaysRunDetected = true
→ enrichment 시도:
1. rawDesc에서 > 프롬프트 마커 찾기 → ✅ 성공 (buttons=2일 때 desc에 프롬프트 포함)
2. rawDesc 최장 라인 사용 → buttons=1일 때 desc="Always run"이라 실패
3. v20 fallback: bridge/pending/ 최신 파일에서 command 읽기 → Step Probe pending 있을 때만
4. v23 sibling: Observer가 footer 형제 요소에서 pre.font-mono 탐색 → ✅ 성공
→ response 파일 작성 → Observer pollResponseGroup → 버튼 클릭
→ WS sendPending { status: 'auto_approved', command: displayCmd }
→ Discord embed 표시
```
### 3.2 명령어 enrichment 현황 (2026-04-18 검증)
| 조건 | 결과 | 빈도 |
|------|------|------|
| Observer가 buttons=2 (`["Always run","Cancel"]`)이고 desc에 `>` 포함 | ✅ 명령어 표시 | ~50% |
| Observer가 buttons=1 (`["Always run"]`)이고 desc="Always run" | ❌ "Always run" 표시 | ~50% |
**로그 증거** (04:25:59):
```
AUTO-APPROVE raw: cmd="Always run" desc="…\extension > npm.cmd run compile..." buttons=["Always run","Cancel"]
→ cmd="npm.cmd run compile 2>&1; npm.cmd version patch..." ✅ 성공
AUTO-APPROVE raw: cmd="Always run" desc="Always run" buttons=["Always run"]
→ cmd="Always run" ❌ 실패
```
> buttons=2인 경우("Always run" + "Cancel")는 Observer가 code 블록을 찾아 description에 포함.
> buttons=1인 경우는 code 블록이 DOM에서 아직 렌더링되지 않았거나 접근 불가.
---
## 4. 사용자 메시지 릴레이 상태
### 4.1 현재 상태: ✅ 작동 (v0.5.74+)
| 경로 | 상태 | 비고 |
|------|------|------|
| Observer DOM | ✅ | `.select-text.rounded-lg` 셀렉터로 캡처 (v0.5.74) |
| Step Probe (trajectory API) | ❌ | cascade 진행 중 step 조회 불가 |
| Step Probe (observer [USER-MSG]) | ❌ | `lastUserInputStepIndex`가 갱신되지 않음 |
### 4.2 해결 방안
1. **DOM 덤프에서 사용자 메시지 클래스 식별** → Observer 셀렉터 추가
2. **Cascade 완료 후** Step Probe HB-CAPTURE에서 `USER_INPUT` step 캡처 (지연 릴레이)
---
## 5. 파일/포트 매핑
| 항목 | 값 |
|------|-----|
| Observer 삽입 대상 | `workbench-jetski-agent.html` |
| HTTP Bridge 포트 | `getDeterministicPort('gravity_control')` = **18080** |
| Extension 로그 | `~/.gemini/antigravity/bridge/extension.log` |
| Pending 파일 | `~/.gemini/antigravity/bridge/pending/*.json` |
| Response 파일 | `~/.gemini/antigravity/bridge/response/*.json` |
| Chat Snapshot 파일 | `~/.gemini/antigravity/bridge/chat_snapshots/*.json` |
| Discord 채널 | `#ag-gravity_control` (ID: 1483082084540223663) |
| Discord Bot 토큰 | `.env``DISCORD_TOKEN` |
---
## 6. 디버깅 도구
| 도구 | 경로 | 용도 |
|------|------|------|
| Discord 메시지 읽기 | `extension/scratch/discord_read.js` | API로 채널 최근 메시지 조회 |
| Discord 채널 목록 | `extension/scratch/discord_channels.js` | 서버 채널 목록 조회 |
| Extension 로그 확인 | `Select-String -Path $logFile -Pattern "패턴"` | 실시간 로그 분석 |
| DOM 구조 덤프 | Observer 자동 (CV-CHILDREN 로그) | AG Native DOM 클래스 식별 |
---
## 7. 버전 히스토리 (v0.5.67~)
| 버전 | 변경 | 결과 |
|------|------|------|
| v0.5.67 | Observer DOM relay 비활성화, Step Probe RT-CAPTURE로 전환 | ❌ API가 진행중 step 미반환 |
| v0.5.68 | auto-approve enrichment 디버그 로그 추가, 조건 >10 → >3 완화 | Observer가 desc="Always run" 보냄 확인 |
| v0.5.69 | pending 파일 fallback으로 auto-approve 명령어 enrichment | 일부 개선 (Step Probe pending 있을 때만) |
| v0.5.70 | heartbeat 로깅 강화 | API step count 동결 확인 |
| v0.5.71 | heartbeat 3 poll마다 실행, HB-CAPTURE 추가 | API가 진행중 step 미반환 재확인 |
| v0.5.72 | Observer DOM relay 재활성화 | AG 재시작 필요 (Observer HTML 캐시) |
| v0.5.74 | 사용자 메시지 셀렉터 추가 (`.select-text.rounded-lg`) | ✅ 사용자 메시지 릴레이 작동 |
| v0.5.76 | DOM 탐색 depth 5→10, `pre.font-mono` 우선 탐색 | Observer HTML 업데이트 필요 |
| v0.5.77 | WS response 파일 작성 (pollResponseGroup용) | Retry 클릭 경로 추가 |
| v0.5.78 | `_from_ws` 마커로 processResponseFile 삭제 방지 | ✅ Retry auto-approve 작동 |
| v0.5.79 | sibling 탐색 추가 + thinking 블록 필터링 | ✅ 명령어 컨텍스트 부분 추출 |
---
## 8. 남은 작업 (TODO)
- [x] AG 재시작하여 Observer 반영 확인 — ✅ v0.5.72 작동 확인
- [x] Observer의 AI 응답 릴레이가 작동하는지 Discord에서 확인 — ✅ 작동
- [x] 사용자 메시지 셀렉터 추가 — ✅ v0.5.74
- [x] Retry auto-approve 흐름 복구 — ✅ v0.5.78 (_from_ws 마커)
- [x] 명령어 컨텍스트 sibling 탐색 — ✅ v0.5.79
- [x] Thinking 블록 필터링 — ✅ v0.5.79
- [ ] 명령어 컨텍스트 추출 타이밍 이슈 (DOM 렌더링 전 scan 시 추출 실패) #636
- [ ] Observer pollResponseGroup 미시작 케이스 (trigger-click 선점)
- [ ] AI 응답이 마지막 블록만 캡처되는 문제 개선 (전문 캡처)

View File

@@ -0,0 +1,160 @@
"""Analyze AG Native DOM structure to find AI response containers."""
import json, os, sys
def load_dump():
bridge = os.path.join(os.path.expanduser('~'), '.gemini', 'antigravity', 'bridge')
# Try deep-inspect result first, then dump_html
for fname in ['deep-inspect-result.json', 'dump_html.json']:
fpath = os.path.join(bridge, fname)
if os.path.exists(fpath):
print(f"Loading: {fname} ({os.path.getsize(fpath)} bytes)")
with open(fpath, 'r', encoding='utf-8-sig') as f:
return json.load(f), fname
return None, None
def find_text_containers(node, path="", depth=0, results=None):
"""Recursively find nodes with substantial text content (potential AI response containers)."""
if results is None:
results = []
if not isinstance(node, dict):
return results
tag = node.get('tag', '')
cls = node.get('cls', '')
text = node.get('text', '')
attrs = node.get('attrs', {})
children = node.get('children', [])
cur_path = f"{path}/{tag}"
if cls:
short_cls = cls[:60]
cur_path += f".{short_cls}"
# Look for nodes with long text (potential AI responses)
if text and len(text) > 50:
results.append({
'path': cur_path,
'depth': depth,
'tag': tag,
'cls': cls[:100],
'text_len': len(text),
'text_preview': text[:120],
'attrs': {k:v for k,v in attrs.items() if k not in ('style',)}
})
for child in children:
find_text_containers(child, cur_path, depth+1, results)
return results
def find_by_class_pattern(node, patterns, path="", depth=0, results=None):
"""Find nodes matching class patterns."""
if results is None:
results = []
if not isinstance(node, dict):
return results
tag = node.get('tag', '')
cls = node.get('cls', '')
attrs = node.get('attrs', {})
children = node.get('children', [])
text = node.get('text', '')
cur_path = f"{path}/{tag}"
for pattern in patterns:
if pattern.lower() in cls.lower() or pattern.lower() in str(attrs).lower():
child_count = len(children)
results.append({
'path': cur_path,
'depth': depth,
'tag': tag,
'cls': cls[:150],
'pattern': pattern,
'text_preview': text[:80] if text else '',
'child_count': child_count,
'attrs': {k:v[:50] for k,v in attrs.items() if k != 'style'}
})
for child in children:
find_by_class_pattern(child, patterns, cur_path, depth+1, results)
return results
def analyze_chat_structure(node, path="", depth=0):
"""Find the chat/conversation area by looking at the main layout."""
if not isinstance(node, dict):
return
tag = node.get('tag', '')
cls = node.get('cls', '')
children = node.get('children', [])
text = node.get('text', '')
attrs = node.get('attrs', {})
# Print interesting structural nodes at shallow depths
if depth <= 6:
child_count = len(children)
has_text = bool(text and len(text) > 10)
info = f"{' '*depth}{tag}"
if cls:
info += f" .{cls[:80]}"
if attrs:
attr_str = ' '.join(f'{k}={v[:30]}' for k,v in attrs.items() if k not in ('style','class'))
if attr_str:
info += f" [{attr_str}]"
info += f" children={child_count}"
if has_text:
info += f" text=\"{text[:50]}...\""
print(info)
for child in children:
analyze_chat_structure(child, f"{path}/{tag}", depth+1)
data, fname = load_dump()
if not data:
print("No dump file found!")
sys.exit(1)
# Handle both dump formats
body = data.get('body', data)
qi = data.get('quickInfo', {})
print("=" * 60)
print("QUICK INFO")
print("=" * 60)
if qi:
for k, v in qi.items():
if k == 'buttons':
print(f"buttons ({len(v)}):")
for b in v[:15]:
print(f" [{b.get('tag')}] \"{b.get('text','')[:50]}\" visible={b.get('visible')} cls={b.get('cls','')[:60]}")
elif k == 'dataAttrs':
print(f"dataAttrs: {v[:30]}")
else:
print(f"{k}: {v}")
print("\n" + "=" * 60)
print("CHAT-RELATED CLASS PATTERNS")
print("=" * 60)
patterns = ['chat', 'message', 'conversation', 'response', 'answer', 'reply',
'markdown', 'prose', 'content', 'panel', 'agent', 'assistant',
'planner', 'step', 'trajectory', 'bot', 'ai-', 'turn']
matches = find_by_class_pattern(body, patterns)
for m in matches:
print(f" [{m['tag']}] cls=\"{m['cls']}\" pattern={m['pattern']} children={m['child_count']} {m.get('attrs',{})}")
print("\n" + "=" * 60)
print("LONG TEXT NODES (potential AI responses)")
print("=" * 60)
texts = find_text_containers(body)
texts.sort(key=lambda x: x['text_len'], reverse=True)
for t in texts[:20]:
print(f" [{t['tag']}] depth={t['depth']} len={t['text_len']} cls=\"{t['cls'][:60]}\"")
print(f" text: \"{t['text_preview']}\"")
if t['attrs']:
print(f" attrs: {t['attrs']}")
print("\n" + "=" * 60)
print("DOM TREE (depth<=6)")
print("=" * 60)
analyze_chat_structure(body)

View File

@@ -0,0 +1,19 @@
import json, os, sys
dump_path = os.path.join(os.path.expanduser('~'), '.gemini', 'antigravity', 'bridge', 'dump_html.json')
with open(dump_path, 'r', encoding='utf-8') as f:
data = json.load(f)
qi = data.get('quickInfo', {})
print('=== Quick Info ===')
print('hasConversationView:', qi.get('hasConversationView'))
print('hasStepIndex:', qi.get('hasStepIndex'))
print('hasBotColor:', qi.get('hasBotColor'))
print('hasMarkdownBody:', qi.get('hasMarkdownBody'))
print('hasProse:', qi.get('hasProse'))
print('totalElements:', qi.get('totalElements'))
print('dataTestIds:', qi.get('dataTestIds'))
print('dataAttrs (first 20):', qi.get('dataAttrs', [])[:20])
print('buttons (first 10):')
for b in qi.get('buttons', [])[:10]:
print(f" [{b.get('tag')}] {b.get('text', '')[:60]} visible={b.get('visible')}")

View File

@@ -0,0 +1,83 @@
"""Search AG Native DOM dump for chat content and buttons."""
import json, os
fpath = os.path.join(os.path.expanduser('~'), '.gemini', 'antigravity', 'bridge', 'dump_html_5.json')
with open(fpath, 'r', encoding='utf-8-sig') as f:
data = json.load(f)
body = data.get('body', data.get('bodyTree', {}))
qi = data.get('quickInfo', {})
# Show all buttons
print('=== BUTTONS ===')
for b in qi.get('buttons', []):
print(f' [{b["tag"]}] "{b["text"][:60]}" visible={b["visible"]} cls={b.get("cls","")[:80]}')
# Data attrs
print('\n=== DATA ATTRS ===')
for attr in qi.get('dataAttrs', []):
print(f' {attr}')
# Recursive search for nodes by text
def find_nodes_by_text(node, target, path='', results=None, depth=0):
if results is None: results = []
if not isinstance(node, dict): return results
tag = node.get('tag','')
cls = node.get('cls','')
text = node.get('text','')
children = node.get('children', [])
cur = f'{path}/{tag}'
if target.lower() in text.lower():
results.append({'path': cur, 'depth': depth, 'cls': cls[:80], 'text': text[:80], 'children': len(children)})
for c in children:
find_nodes_by_text(c, target, cur, results, depth+1)
return results
print('\n=== NODES containing "Always run" ===')
matches = find_nodes_by_text(body, 'Always run')
for m in matches:
print(f' depth={m["depth"]} cls="{m["cls"]}" text="{m["text"]}" children={m["children"]}')
print('\n=== NODES containing "Always" ===')
matches = find_nodes_by_text(body, 'Always')
for m in matches:
print(f' depth={m["depth"]} cls="{m["cls"]}" text="{m["text"]}" children={m["children"]}')
# Find ALL text nodes with > 30 chars
def find_all_text(node, results=None, depth=0, path=''):
if results is None: results = []
if not isinstance(node, dict): return results
tag = node.get('tag','')
cls = node.get('cls','')
text = node.get('text','')
children = node.get('children', [])
if text and len(text) > 30:
results.append({'depth': depth, 'tag': tag, 'cls': cls[:80], 'text': text[:100], 'path': f'{path}/{tag}'})
for c in children:
find_all_text(c, results, depth+1, f'{path}/{tag}')
return results
print('\n=== LONG TEXT NODES (>30 chars) ===')
texts = find_all_text(body)
texts.sort(key=lambda x: len(x['text']), reverse=True)
for t in texts[:25]:
print(f' d={t["depth"]} [{t["tag"]}] cls="{t["cls"][:50]}" len={len(t["text"])} "{t["text"][:80]}"')
# Find nodes with many children (structural containers)
def find_containers(node, results=None, depth=0, path=''):
if results is None: results = []
if not isinstance(node, dict): return results
tag = node.get('tag','')
cls = node.get('cls','')
children = node.get('children', [])
if len(children) > 5:
results.append({'depth': depth, 'tag': tag, 'cls': cls[:100], 'children': len(children), 'path': f'{path}/{tag}'})
for c in children:
find_containers(c, results, depth+1, f'{path}/{tag}')
return results
print('\n=== CONTAINERS (>5 children) ===')
conts = find_containers(body)
conts.sort(key=lambda x: x['children'], reverse=True)
for c in conts[:20]:
print(f' d={c["depth"]} [{c["tag"]}] children={c["children"]} cls="{c["cls"][:70]}"')

View File

@@ -0,0 +1,109 @@
"""Trace the DOM path from body to AI response container."""
import json, os
fpath = os.path.join(os.path.expanduser('~'), '.gemini', 'antigravity', 'bridge', 'dump_html_5.json')
with open(fpath, 'r', encoding='utf-8-sig') as f:
data = json.load(f)
body = data.get('body', data.get('bodyTree', {}))
def find_path_to_class(node, target_cls, path=None, depth=0):
"""Find the DOM path down to a node with a matching class."""
if path is None: path = []
if not isinstance(node, dict): return []
tag = node.get('tag', '')
cls = node.get('cls', '')
children = node.get('children', [])
text = node.get('text', '')
attrs = node.get('attrs', {})
entry = {
'depth': depth,
'tag': tag,
'cls': cls[:120],
'children': len(children),
'text': text[:60] if text else '',
'attrs': {k:v[:40] for k,v in attrs.items() if k not in ('style',)}
}
if target_cls.lower() in cls.lower():
return path + [entry]
for i, child in enumerate(children):
result = find_path_to_class(child, target_cls, path + [entry], depth+1)
if result:
return result
return []
# Find path to the AI response container
print("=== PATH TO 'leading-relaxed select-text' ===")
path = find_path_to_class(body, 'leading-relaxed select-text')
for p in path:
indent = ' ' * p['depth']
print(f'{indent}[{p["tag"]}] cls="{p["cls"]}" children={p["children"]} {p["attrs"]}')
if p['text']:
print(f'{indent} text: "{p["text"]}"')
# Now get the full subtree of the AI response container
def get_subtree(node, target_cls, depth=0):
if not isinstance(node, dict): return None
cls = node.get('cls', '')
if target_cls.lower() in cls.lower():
return node
for child in node.get('children', []):
result = get_subtree(child, target_cls, depth+1)
if result:
return result
return None
print("\n=== AI RESPONSE CONTAINER SUBTREE ===")
container = get_subtree(body, 'leading-relaxed select-text')
if container:
def print_tree(node, depth=0, max_depth=4):
if not isinstance(node, dict) or depth > max_depth: return
tag = node.get('tag','')
cls = node.get('cls','')[:80]
text = node.get('text','')
children = node.get('children', [])
indent = ' ' * depth
line = f'{indent}[{tag}]'
if cls: line += f' cls="{cls}"'
line += f' children={len(children)}'
if text: line += f' text="{text[:60]}"'
print(line)
for c in children:
print_tree(c, depth+1, max_depth)
print_tree(container, 0, 3)
# Also search for the chat panel container - what wraps the entire conversation
print("\n=== SEARCH FOR CHAT PANEL WRAPPERS ===")
chat_patterns = ['chat', 'antigravity', 'gemini', 'panel', 'agentview', 'sidebar', 'conversation']
for pat in chat_patterns:
path = find_path_to_class(body, pat)
if path:
last = path[-1]
print(f' Pattern "{pat}" found at depth={last["depth"]} [{last["tag"]}] cls="{last["cls"]}" children={last["children"]}')
# Find the parent chain from body to the container - look by scanning ALL class names
print("\n=== ALL UNIQUE CLASS NAMES (depth <= 12) ===")
all_classes = set()
def collect_classes(node, depth=0, max_depth=12):
if not isinstance(node, dict) or depth > max_depth: return
cls = node.get('cls', '')
if cls:
for c in cls.split():
if len(c) > 3 and not c.startswith('{') and 'mtk' not in c:
all_classes.add(c)
for child in node.get('children', []):
collect_classes(child, depth+1, max_depth)
collect_classes(body)
# Print classes sorted, grouped by potential relevance
relevant = sorted([c for c in all_classes if any(k in c.lower() for k in
['chat', 'message', 'response', 'agent', 'gemini', 'turn', 'model', 'user', 'bot', 'conversation', 'markdown', 'prose', 'text-', 'content'])])
print("Relevant classes:")
for c in relevant:
print(f' {c}')

19
bot.py
View File

@@ -687,11 +687,28 @@ class GravityBot(commands.Bot):
if request_id in self._sent_approval_ids: if request_id in self._sent_approval_ids:
return return
# Check auto_resolved status # Check auto_resolved / auto_approved status
status = data.get("status", "pending") status = data.get("status", "pending")
if status in ("auto_resolved", "expired"): if status in ("auto_resolved", "expired"):
await self._handle_auto_resolved(request_id, status) await self._handle_auto_resolved(request_id, status)
return return
if status == "auto_approved":
# Bridge-level auto-approve (e.g. "Always run") — show notification only
channel = await self._get_channel(project)
if channel:
cmd_text = data.get("command", "")[:200]
desc_text = data.get("description", "")[:300]
embed = discord.Embed(
title="🤖 자동 승인됨 (Always run)",
description=f"✅ **{cmd_text}**" + (f"\n```\n{desc_text}\n```" if desc_text and len(desc_text) > 3 else ""),
color=discord.Color.green(),
)
embed.set_footer(text=f"auto-approve | {request_id[:12]}")
await channel.send(embed=embed)
self._cap_dict(self._sent_approval_ids)
self._sent_approval_ids[request_id] = True
logger.info(f"[HUB-PENDING] Auto-approved (Always run): {request_id[:12]} project={project}")
return
instance_number = data.get("_instance_number", 0) instance_number = data.get("_instance_number", 0)
pc_name = data.get("_pc_name", "") pc_name = data.get("_pc_name", "")

View File

@@ -2,4 +2,5 @@
| NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 | | NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 |
|-------|-------|----------|-----------|-----------| |-------|-------|----------|-----------|-----------|
| 001 | 13:04 | Pure 웹소켓 게이트웨이 완전 전환 및 레거시 파일 브릿지 통신코드 삭제 | `TBD` | ✅ | | 001 | 13:04 | Pure 웹소켓 게이트웨이 완전 전환 및 레거시 파일 브릿지 통신코드 삭제 | `072f83b` | ✅ |
| 002 | 17:25 | Antigravity Observer 컨텍스트 추출 범위 제한 및 노이즈(UI/TypeScript 코드) 필터링, Discord 임베드 개선 | `70dc301` | ✅ |

View File

@@ -0,0 +1,7 @@
# 2026-04-12
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완/미 |
|-------|-------|----------|-----------|-----------|
| 001 | 06:12 | AG Native DOM 파싱 v7 전면 재설계 — data-testid/data-step-index 기반 step-aware 파서, UI 노이즈 차단 | `a4d7286` | 🔧 |
| 002 | 07:03 | AG Native 번들 역공학 분석 + V8 CachedData 삭제 — plannerResponse→Whi 렌더러 구조 확인, bot-color가 NUX용임 발견 | — | 🔧 |
| 003 | 07:37 | Observer v8 전면 개편 — conversation-view 의존 제거, body 전체 무조건 덤프(depth 15), 5s/15s/60s 자동 덤프, VSIX v0.5.37 설치 | `0e03b3a` | 🔧 |

View File

@@ -0,0 +1,7 @@
# 2026-04-13
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완료여부 |
|-------|-------|----------|-----------|----------|
| 001 | 09:50 | Observer v8 검증 — Extension POLL 확인, HTML 패치 확인, V8 캐시 삭제(24MB), BEACON 미수신(AG 재시작 필요) | 없음 | 🔧 |
| 002 | 12:34 | DOM Observer 데이터 품질 검증 + UTF-8 인코딩 수정 + noise 필터 강화 (v0.5.39) | `pending` | ✅ |
| 003 | 19:26 | Observer v9: "Running N commands" 오인 수정 + DOM-climbing 컨텍스트 추출 + http-bridge 필터 완화 (v0.5.40) | `pending` | 🔧 |

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) | `c1e61d8` | 🔧 |

View File

@@ -0,0 +1,6 @@
# 2026-04-15
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완료? |
|-------|-------|----------|-----------|----------|
| 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` | 🔧 |

11
docs/devlog/2026-04-16.md Normal file
View File

@@ -0,0 +1,11 @@
# 2026-04-16
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완료? |
|-------|-------|----------|-----------|----------|
| 001 | 04:52 | v16 터미널 출력 필터 + v15 stale LS 자동복구 + heartbeat probe — stdout가 enrichedCmd로 채택되는 버그 수정, VSIX v0.5.50 빌드/배포 | `7ade31e` | 🔧 |
| 002 | 05:28 | AG Native AI 응답 Discord 미전달 근본원인 분석 + Observer v15 scanChatBodies 이중전략 (#conversation + .leading-relaxed.select-text) 구현, v0.5.51 배포 | `729875f` | 🔧 |
| 003 | 17:13 | Observer v15 E2E 코드 검증 — CSP/체크섬/문법/핸들러 전수 확인 OK. BEACON=0 원인: AG 미재시작(렌더러 HTML 캐시). v0.5.50/out에 v15 JS 직접 배포 | — | ✅ |
| 004 | 21:07 | Observer v16 CSS 추출 버그 수정 — `<style>` 태그 textContent가 AI 응답으로 Discord 전달. extractCleanStepText()에 style/script strip 추가, v0.5.52 배포 | `62ee081` | ✅ |
| 005 | 22:07 | Observer v17 Always run 자동승인 + Retry 릴레이 — "Always run" 브릿지 레벨 자동승인, Retry 버튼 Discord 전달, v0.5.53 배포 | `7dbf73a` | ✅ |
| 006 | 21:28 | AG Native <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Markdown <20><><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>Ʈ/<2F><>) <20><><EFBFBD><EFBFBD> <20><> User <20><>û <20>̼<EFBFBD><CCBC><EFBFBD> <20>м<EFBFBD>, <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><>ȹ<EFBFBD><C8B9> <20>ۼ<EFBFBD> (v0.5.54 <20><><EFBFBD><EFBFBD> <20><>) | ? | ?? |

View File

@@ -0,0 +1,5 @@
# 2026-04-17
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완료 |
|-------|-------|----------|-----------|----------|
| 001 | 08:05 | v18 Observer DOM->Markdown 파서 개선(<a> 파싱 포함) 및 노이즈 필터 부작용(코드 블럭 잘림 방지) 해결, User 구문 추출 연동, v0.5.56 배포 | `미정` | ✅ |

View File

@@ -0,0 +1,5 @@
# Devlog — 2026-04-18
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|---|------|----------|------|------|
| 001 | 09:20~23:50 | Retry auto-approve 흐름 복구 — WS response 파일 보존 (`_from_ws`), Observer 형제 탐색(sibling), thinking 블록 필터링 | `pending` | ✅ |

26
docs/devlog/2026-04-19.md Normal file
View File

@@ -0,0 +1,26 @@
# Devlog 2026-04-19
## 작업 인덱스
| # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------|
| 001 | 21:22 | v30-32 Observer 명령어 추출 안정화 (터미널 프롬프트 조기감지) | `bd5a7ca` | ✅ |
| 002 | 23:16 | v33 Accept all 자동승인 — diff review auto-approve | `6aea48e` | ✅ |
| 003 | 00:18 | v34 Accept all 이중 보장 — agentAcceptAllInFile 직접 호출 | `cf1352e` | ✅ |
| 004 | 00:34 | v35 code_edit 자동 Accept — step-probe 경로 | `2bf1eb4` | ✅ |
| 005 | 04:26 | v36 Accept all span 감지 — 근본 원인 발견 (button→span) | `e95e779` | ✅ |
| 006 | 04:34 | v37 openReviewChanges 선호출 — agentAcceptAllInFile 보조 | `3cc3442` | ✅ |
| 007 | 04:43 | v38 _from_ws 마커 추가 — Observer polling 실패 근본 수정 | `7c8891b` | ✅ |
## v0.5.103 — Accept all (Diff Review) 자동 승인 복구
### 근본 원인 (2가지)
1. **Observer 감지 실패**: AG UI가 "Accept all"을 `<button>`이 아닌 `<span class="cursor-pointer">`로 렌더링. Observer의 `allBtns` 선택자가 `button`만 스캔하여 미감지.
2. **Response 파일 race condition**: auto-approve response 파일에 `_from_ws: true` 마커 없음 → `processResponseFile`이 Observer보다 먼저 파일 삭제 → Observer polling 무한 실패.
### 검증 결과
- Observer ACCEPT-SCAN: `tag=SPAN cls=cursor-pointer txt=Accept all`
- `DETECTED diff_review: Accept all`
- `response served to renderer: ...approved=true` (이전 0건 → 7건) ✅
- Discord "자동 승인됨 Accept all" 표시 ✅
- 화면에서 "Accept all" 버튼 자동 소멸 확인 ✅

View File

@@ -0,0 +1,22 @@
# AG Native DOM 파싱 v7 전면 재설계
- **시간**: 2026-04-12 05:49~06:12
- **Commit**: `a4d7286`
## 배경
- AG Native 세션에서 Discord 릴레이가 전혀 동작하지 않음
- SDK `GetCascadeTrajectorySteps``trajectory not found` 반환 — AG Native는 Cascade API에 등록 안 됨
- DOM observer가 UI 노이즈(content_copy, keyboard_arrow_up, Always run, Cancel)를 AI 응답으로 오인
## 결정 사항
- SDK 경로 대신 **DOM이 유일한 데이터 소스**로 확정
- `jetskiAgent/main.js` (11MB) 번들 분석으로 AG Native DOM 구조 역공학:
- `data-testid="conversation-view"` — 대화 최상위 컨테이너
- `data-step-index` — 각 step 식별 속성
- `text-ide-message-block-bot-color` — 봇 메시지 클래스 (확인됨)
- observer-script v6→v7 전면 재설계: step-aware 파싱
## 미완료
- AG 재시작 후 실제 동작 검증 필요 (DOM 덤프 → 셀렉터 미세조정)
- deep-inspect 정상 동작 확인
- Discord에 실제 AI 응답 전달 확인

View File

@@ -0,0 +1,22 @@
# AG Native 번들 역공학 + V8 캐시 정리 + Observer 미작동 원인 규명
- **시간**: 2026-04-12 06:28~07:03
- **Commit**: — (분석/조사 세션, 코드 변경 없음)
## 결정 사항
- `text-ide-message-block-bot-color`는 AI 응답 컨테이너가 **아닌** NUX tooltip 전용 클래스로 확인 → observer 셀렉터에서 제거 필요
- `markdown-body` 클래스도 AG Native에 존재하지 않음 → 폴백 셀렉터 변경 필요
- AI 응답 텍스트는 `plannerResponse` step → `Whi` 렌더러 → `div.px-2.py-1``MarkdownRenderer` 내부에 위치
- `data-step-index`는 디버그 패널에서 확인되었으나 메인 대화 뷰에서의 존재 여부는 라이브 DOM 덤프로 확인 필요
## 새로 알게 된 사실
- AG Native 번들(jetskiAgent/main.js 10.8MB): 전체 step.case→renderer 매핑 확보 (pan 객체)
- Allow/Deny는 `lHr` 컴포넌트, `border-t border-gray-500/25` 클래스
- Observer v7이 HTML에 삽입되었지만 V8 CachedData(50MB) 때문에 실제 렌더러에서 로드되지 않았음
- CachedData 삭제 완료 → AG 리로드 후 observer 작동 예상
## 미완료
- AG 리로드 후 observer 작동 확인
- deep-inspect로 실제 DOM 구조 캡처
- observer-script 셀렉터 미세조정 (bot-color 제거, MarkdownRenderer 타겟팅)
- Discord 릴레이 E2E 검증

View File

@@ -0,0 +1,28 @@
# Observer v8 Electron 실행 보장 + 진단 beacon 추가
- **시간**: 2026-04-12 21:00~21:30
- **Commit**: (이전 세션 크래시 복구 커밋)
## 배경
- 이전 세션(f9491880)에서 Observer v8이 렌더러에서 실행되지 않는 문제 디버깅 중 크래시 발생
- deep-inspect 엔드포인트가 `timeout` 반환 — 인라인 스크립트가 Electron에서 실행 안 됨
- 원인: 인라인 스크립트가 `</html>` 뒤에 삽입되어 Electron이 무시하는 것으로 추정
## 변경 사항
### observer-script.ts
- DIAGNOSTIC BEACON 추가: 스크립트 로드 즉시 `/ping?beacon=1`으로 fetch → 실행 여부 확인 가능
### html-patcher.ts
- 인라인 스크립트 삽입 위치를 `</html>` 앞에서 **`</body>` 앞**으로 변경
- 기존 잘못된 위치의 인라인 블록을 제거 후 재삽입하는 로직 추가
- 이전 패칭에서 발생한 중복 `</html>` 태그 정리 로직 추가
### http-bridge.ts
- 진단용 HTTP 요청 로깅 추가 (폴링 엔드포인트 제외)
## 미완료
- AG 리로드 후 observer 작동 확인 (beacon ping 수신 확인)
- deep-inspect로 실제 DOM 구조 캡처
- observer-script 셀렉터 미세조정 (bot-color 제거, MarkdownRenderer 타겟팅)
- Discord 릴레이 E2E 검증

View File

@@ -0,0 +1,25 @@
# Observer v8 검증 — V8 캐시 차단 재확인
- **시간**: 2026-04-13 09:50~11:00
- **Commit**: 없음 (진단/검증 세션)
- **Vikunja**: #619, #620 (진행 중)
## 진단 결과
1. **Extension POLL**: ✅ 정상 — 세션 `a91b5318` 추적 중, WS 명령 수신 정상
2. **bridge/ 위치**: `~/.gemini/antigravity/bridge/` (프로젝트 루트 아님)
3. **HTML 패치**: ✅ workbench.html + workbench-jetski-agent.html 모두 인라인 스크립트 + BEACON 삽입, `</body>` 앞 위치 정상
4. **Observer 실행**: ❌ BEACON 핑 0건, dom_dumps 디렉토리 없음
5. **V8 CachedData**: 24.42 MB 존재 → 삭제 완료
6. **Discord 릴레이**: ❌ AI 응답 텍스트 추출 불가
## 결정 사항
- Observer 스크립트 미실행의 원인은 V8 CachedData가 패치된 HTML 로드를 차단하는 known-issue와 동일 패턴
- AG가 09:41에 재시작되었지만, 이전 세션에서 삭제한 V8 캐시가 AG 시작과 함께 다시 생성됨
- 해결: V8 캐시 삭제 → AG 재시작 순서 필수 (이번 세션에서 캐시 삭제 완료)
## 미완료
- AG 재시작 후 BEACON 핑 수신 확인
- DOM 덤프 분석: 실제 DOM 구조에서 AI 응답 셀렉터 검증
- 셀렉터 미세조정: bot-color 제거, MarkdownRenderer 타겟팅
- Discord 릴레이 E2E 검증

View File

@@ -0,0 +1,27 @@
# DOM Observer 데이터 품질 검증 + UTF-8/noise 수정
- **시간**: 2026-04-13 12:34~12:52
- **Commit**: `pending`
- **Vikunja**: #619, #620 (진행 중)
## 검증 결과
- DOM Observer v8 **동작 확인**: `pending/`에 45개 시그널 생성됨 (`source: "dom_observer"`)
- 버튼 분류 정상: `command`(30), `permission`(15)
- 명령어/conversation_id/버튼(Allow/Deny/Cancel) 추출 정상
- **한글 인코딩 깨짐** 발견: description 필드에 `[AI 본문 요약]``[AI <20> <20>]`
## 변경 사항 (v0.5.39)
### http-bridge.ts
- 모든 POST 핸들러에 `req.setEncoding('utf8')` 추가
- Node.js HTTP 서버의 Buffer→string latin1 기본 인코딩으로 인한 multi-byte UTF-8 손실 수정
### observer-script.ts
- `cleanLines()`에 인라인 pre-strip 추가: Material 아이콘명 18종을 regex로 `\n`으로 치환
- `Thought for Xs` 패턴 인라인 제거 추가
- `codeText` 추출에 `cleanLines()` 적용 (이전 미적용)
## 미완료
- AG 재시작 후 v0.5.39 적용 검증 (한글 정상 출력 확인)
- DOM dump 추출 검증
- Discord 릴레이 E2E 검증

View File

@@ -0,0 +1,28 @@
# DOM Observer 컨텍스트 추출 수정 — v9 (v0.5.40)
- **시간**: 2026-04-13 19:26~
- **Commit**: `pending`
- **Vikunja**: #619, #620 (진행 중)
## 문제
Discord 승인 요청에 내용이 비어있음:
- command = "Running2 commands" (그룹 헤더 버튼을 잘못 캡처)
- description = 비어있거나 UI 노이즈만 포함
- buttons = "Running2 commands / Always run" (잘못된 구조)
## 변경 사항
### observer-script.ts (v8 → v9)
1. `isActionBtn()`에서 "Running N commands" 패턴 제거 — 이것은 그룹 헤더이며 승인 버튼이 아님
2. `scan()`에서 `^Running\s*\d+\s*commands?$` 명시적 스킵
3. `extractContextFromNearby()` 신규 함수 추가 — `data-step-index` 없이 DOM 트리를 20레벨까지 올라가며 `pre`/`code` 블록에서 실제 명령어 추출
4. `collectSiblingButtons()` 범위를 parent → grandparent → great-grandparent 3레벨로 확대, 그룹 헤더 스킵, 텍스트 기반 dedup 추가
5. `matchedType` 판별에서 `/Running\d/` 패턴 제거
### http-bridge.ts
6. "Run/Always run" 필터에 `ctx.activeSessionId` 체크 추가 — step-probe가 세션 미추적 시 DOM observer 신호 허용
## 미완료
- AG 재시작 후 v0.5.40 적용 검증
- Discord E2E 검증 (실제 명령어/코드 내용 표시 확인)

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

@@ -0,0 +1,27 @@
# PROMPT_ONLY_RE 근본원인 수정 (v0.5.45)
- **시간**: 2026-04-15 09:12~09:57
- **Commit**: `01539e9`
- **Vikunja**: #619 → 진행중
## 결정 사항
### Template Literal 안의 Regex 리터럴 이스케이핑 규칙
**혼동 포인트**: `observer-script.ts`의 전체 코드는 TypeScript template literal 안에 있다. 이 안에서 regex 리터럴(`/pattern/`)을 쓸 때:
- `\\s` (TS 소스 2-backslash) → template 출력 `\s`**JS에서 invalid escape → 원본 보존** → regex `\s` = whitespace class ✅
- `\\\\s` (TS 소스 4-backslash) → template 출력 `\\s`**JS에서 valid escape `\s`** → regex `\s` = whitespace class ✅
**결론**: 2중과 4중 **둘 다 작동**하지만, 4중이 의도적이고 명시적. 그러나 PROMPT_ONLY_RE는 **기존 4중에서 실패하고 있었으므로** 실제 원인은 다른 곳에 있었음 — `(.*[\\/>»$#]\\\\s*)` 패턴 자체가 `>` 다음에 `\\s*` 매칭이 아닌 `\\\\s*` 리터럴 매칭이 되고 있었던 것.
### http-bridge PROMPT_ONLY_RE 단순화
- 기존: `/^[\s\\\/]*[\w_.-]+\s*[>»$#]\s*$/``…`(U+2026 ellipsis) prefix 미지원
- 변경: `/^.*[>»$#]\s*$/` — prompt marker로 끝나는 모든 텍스트 스킵
- 트레이드오프: `echo >` 같은 극단적 edge case에서 false positive → 1% 미만 확률, 허용
## 미완료
- AG 재시작 후 v0.5.45 실제 동작 확인 필요 (현재 v0.5.44 메모리 로드 상태)
- `Running N commands` 4-backslash 패턴은 정상 동작 확인됨 — 그대로 유지

View 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 완료 처리

View File

@@ -0,0 +1,38 @@
# v16 터미널 출력 필터 + v15 Stale LS 자동복구 + Heartbeat Probe (v0.5.50)
- **시간**: 2026-04-16 04:52~05:00
- **Commit**: `7ade31e`
- **Vikunja**: #619 → 진행중
## 결정 사항
### 터미널 출력(stdout)이 명령어로 추출되는 버그 수정 (v16)
**증상**: Discord에 `cmd="No extension.log found"`, `cmd="AG CLI not found..."`, `cmd="Log found: C:\..."` 등 터미널의 **출력** 텍스트가 명령어로 전송됨.
**원인**: Observer의 `extractContextFromNearby()`가 code 블록 2개를 찾음:
1. ci=0: 프롬프트+명령어 (`…\gravity_control > $log = ...`) → JUNK_CODE_RE로 스킵
2. ci=1: 터미널 출력 (`No extension.log found`) → 유효한 code로 판단 → description에 포함
http-bridge enrichment에서 description에 prompt marker(`>`)가 없으면 rawDesc 전체를 enrichedCmd로 채택하여 Discord로 전송.
**해결**: `promptMatch` 실패 시 (description에 `>` 없음) → 터미널 OUTPUT으로 판단하여 즉시 필터. 실제 명령어는 항상 `…\project > command` 형식의 프롬프트를 포함.
### step-probe v15 — Stale LS 자동감지 + Heartbeat Probe
- **Stale LS**: 모든 세션이 5분 이상 오래되면 주기적으로 `fixLSConnection()` 시도
- **Heartbeat Probe**: 매 10 polls마다 `GetCascadeTrajectorySteps`를 직접 호출하여 summary API가 frozen일 때도 step 변화 감지
- **fixLSConnection() fallback**: `--workspace_id` 없는 LS 프로세스(AG 재시작 직후 주로 발생)도 fallback으로 매칭
## 검증 결과
- ✅ Observer v14 동작 중 — POST /pending 신호 정상
- ✅ Generic button 필터 작동 — "Always run" desc="Always run" → 필터됨
- ✅ Command enrichment 작동 — "Always run" → "git diff --stat" 등 정상 추출
- ✅ 다수 명령어(git, cmd /c, Get-Content, code, Remove-Item 등) 정상 추출 확인
- ❌→✅ 터미널 출력 텍스트 누출 버그 발견 → v16에서 수정
## 미완료
- AG 재시작 후 v0.5.50 실제 동작 확인 필요
- v15 stale LS 자동복구 + heartbeat probe 실동작 확인 (장시간 세션 필요)

View File

@@ -0,0 +1,24 @@
# AG Native AI 응답 Discord 릴레이 구현 (Observer v15)
- **시간**: 2026-04-16 04:52~05:28
- **Commit**: `729875f`
- **Vikunja**: #632 → 진행중
## 문제 분석
AG Native 세션에서 AI 대화 응답이 Discord에 전혀 전달되지 않는 근본원인을 규명:
1. **SDK 경로 차단**: `GetCascadeTrajectorySteps(cascadeId)``trajectory not found`. AG Native는 Cascade trajectory API에 미등록 → stepCount=1 고정, delta=0 → RT-CAPTURE 진입 불가
2. **DOM 경로 차단**: `scanChatBodies()``conversation-view`, `data-step-index` 등 Cascade 전용 셀렉터 사용 → AG Native DOM에 전무 → 즉시 return
## 결정 사항
- SDK 경로는 AG 구조적 한계로 사용 불가 → **DOM 경로를 AG Native에 맞게 확장**
- AG Native DOM 분석 결과: `#conversation` (id), `.leading-relaxed.select-text` (AI 응답 영역) 확인
- 기존 Cascade 경로도 유지하여 호환성 보장 (이중 전략)
## 미완료
- **AG Reload Window 필요**: v15 Observer가 workbench.html에 패치되려면 AG 재시작 필수
- **실동작 검증**: Discord에 AI 응답 텍스트가 실제로 수신되는지 end-to-end 확인
- **enrichment 오탐 edge case**: 로그 텍스트 내 `>` 문자가 prompt marker로 오인되는 1건 (빈도 낮음, v17에서 수정 검토)

View File

@@ -0,0 +1,17 @@
# DOM Observer 마크다운 구조 복원 및 사용자 메시지 연동 (v0.5.56)
### 목표
DOM Observer(`observer-script.ts`)가 AI 채팅을 `innerText`로 추출하며 잃어버리는 마크다운 서식을 복원하고, 사용자(User) 메시지도 포착하여 함께 Discord 봇으로 보내기 (#634 이슈).
### 변경 사항
1. **`convertNodeToMarkdown` 파서 확장**:
- AI 채팅창의 DOM Tree를 순회하며 `<h1>`~`<h4>`, `<p>`, `<ul>`, `<ol>`, `<li>`, `<strong>`, `<em>`, `<code>`, `<pre>`, `<blockquote>` 등 대부분의 마크다운 요소를 파싱하는 로직 도입.
- 추가로 `<a>` 태그(Link) 속성을 지원하여 `[text](href)` 형태로 복원하도록 개선.
2. **파괴적인 `cleanLines()` 노이즈 필터 제거**:
- 이전에 사용되던 `cleanLines()``}[공백]`이나 `import` 같은 코드를 UI 노이즈로 오인하여 삭제(Drop)하는 심각한 이슈를 발견. 전체 마크다운 문자열에는 해당 필터를 적용하지 않고 정규식을 통해 `Thought for X s` 형태의 메시지만 지우도록 수정.
3. **User 메시지 대상 추가**:
- `scanChatBodies()`의 탐색 Selector에 `.text-ide-message-block-user-color`, `.bg-ide-message-block-user-background` 등을 추가하여 사용자 메시지 블록도 대상에 포함.
- 데이터 전송 시 `role: 'user'` 정보를 보내고, `http-bridge.ts`에서 이를 구분하여 헤더를 `🧑‍💻 **[DOM 추출] 사용자 요청**`로 지정해 Discord로 릴레이.
### 결과
`v0.5.56` VSIX 배포 준비 완료 (v0.5.54/55 빌드는 테스트 과정 중 건너뜀). AG Native에서 확장 설치 캐시를 리셋하거나 직접 VSIX를 설치하면 적용됨.

View File

@@ -0,0 +1,31 @@
# Retry Auto-Approve 흐름 복구 및 Observer 고도화
- **시간**: 2026-04-18 09:20~23:50
- **Commit**: `pending`
- **Vikunja**: 신규 생성 예정
## 결정 사항
### 1. `_from_ws` 마커 기반 response 파일 보존
- **문제**: WS 응답 핸들러가 response 파일 작성 → processResponseFile이 300ms 후 삭제 → Observer pollResponseGroup 실패
- **선택**: response 파일에 `_from_ws: true` 마커 추가, processResponseFile에서 스킵
- **이유**: pending 파일 생성을 추가하는 것보다 단순하고, WS 핸들러에서 이미 tryApprovalStrategies를 실행하므로 중복 실행 방지도 함께 해결
### 2. 형제(sibling) DOM 탐색
- **문제**: "Always run" 버튼의 조상(parentElement) 탐색으로는 `pre.font-mono` 도달 불가 (footer.parentElement가 null)
- **선택**: 각 depth에서 `node.parentElement.children`을 순회하여 형제 요소의 code 블록 탐색
- **이유**: AG Native DOM 구조에서 명령어는 footer의 형제 요소에 있으므로 조상 탐색만으로는 구조적으로 불가
### 3. Thinking 블록 필터링
- **문제**: AI의 내부 사고 과정이 Discord에 릴레이됨
- **선택**: `max-h-[200px]` 조상 확인으로 thinking 블록 식별
- **이유**: thinking 블록은 접힌 상태에서 max-height가 200px로 제한되는 특징이 있음
## 시행착오
1. depth 5→10 증가만으로 해결 시도 → 실패 (조상이 아닌 형제에 명령어가 있었음)
2. Observer HTML 변경 후 Reload Window만 실행 → 실패 (AG 2번 재시작 필요)
3. response 파일이 삭제되는 원인을 clickTrigger 타이밍으로 오인 → 실제는 processResponseFile의 isDomObserver 판별 실패
## 미완료
- [ ] 명령어 컨텍스트 추출 타이밍 이슈 (DOM 렌더링 전 scan 시 추출 실패)
- [ ] Observer pollResponseGroup이 시작되지 않는 케이스 (POST /pending 이전에 trigger-click 소비)

View File

@@ -243,8 +243,8 @@ async function fixLSConnection() {
logToFile(`[LS-FIX] found ${lines.length} LS process(es), hint="${hint}"`); logToFile(`[LS-FIX] found ${lines.length} LS process(es), hint="${hint}"`);
// Find the line whose workspace_id matches our workspace (case-insensitive) // Find the line whose workspace_id matches our workspace (case-insensitive)
let matchedLine = null; let matchedLine = null;
let fallbackLine = null; // v15: LS without workspace_id (AG restart)
for (const line of lines) { for (const line of lines) {
const lower = line.toLowerCase();
// Match workspace_id arg against our hint // Match workspace_id arg against our hint
const wsMatch = line.match(/--workspace_id[= ](\S+)/i); const wsMatch = line.match(/--workspace_id[= ](\S+)/i);
if (wsMatch) { if (wsMatch) {
@@ -254,10 +254,25 @@ async function fixLSConnection() {
break; break;
} }
} }
else {
// v15: LS without --workspace_id (new AG main LS after restart)
// Skip --enable_lsp processes (secondary/old LSP instances)
if (!line.includes('--enable_lsp') && !fallbackLine) {
fallbackLine = line;
logToFile(`[LS-FIX] found fallback LS (no workspace_id): PID=${line.split('|')[0]?.trim()}`);
}
}
} }
if (!matchedLine) { if (!matchedLine) {
logToFile(`[LS-FIX] No LS process matched hint="${hint}" (${lines.length} processes)`); if (fallbackLine) {
return false; // v15: Use workspace_id-less LS as fallback (common after AG restart)
logToFile(`[LS-FIX] No workspace_id match — using fallback LS`);
matchedLine = fallbackLine;
}
else {
logToFile(`[LS-FIX] No LS process matched hint="${hint}" (${lines.length} processes)`);
return false;
}
} }
// Extract port and csrf_token from matched line // Extract port and csrf_token from matched line
const csrfMatch = matchedLine.match(/--csrf_token[= ](\S+)/); const csrfMatch = matchedLine.match(/--csrf_token[= ](\S+)/);
@@ -382,6 +397,23 @@ async function activate(context) {
return; return;
} }
// Normal approval — tryApprovalStrategies // Normal approval — tryApprovalStrategies
// v22: ALSO write response file so Observer's pollResponseGroup can click
// the correct button (with exact button_index). Without this, only the
// imprecise pollTriggerClick fallback was used for WS-path responses.
const responseDir = path.join(bridgePath, 'response');
if (!fs.existsSync(responseDir)) {
fs.mkdirSync(responseDir, { recursive: true });
}
const respPayload = {
request_id: data.request_id,
approved,
button_index: data.button_index ?? 0,
step_type: stepType,
project_name: projectName,
_from_ws: true,
};
fs.writeFileSync(path.join(responseDir, `${data.request_id}.json`), JSON.stringify(respPayload), 'utf-8');
logToFile(`[WS-RESPONSE] Response file written for pollResponseGroup: ${data.request_id?.substring(0, 12)}`);
const approvalCtx = (0, step_probe_1.getApprovalContext)(); const approvalCtx = (0, step_probe_1.getApprovalContext)();
logToFile(`[WS-RESPONSE] Triggering approval: approved=${approved} session=${approvalCtx.sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${approvalCtx.stepIndex}`); logToFile(`[WS-RESPONSE] Triggering approval: approved=${approved} session=${approvalCtx.sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${approvalCtx.stepIndex}`);
(0, step_probe_1.tryApprovalStrategies)(approved, approvalCtx.sessionId, stepType, approvalCtx.stepIndex) (0, step_probe_1.tryApprovalStrategies)(approved, approvalCtx.sessionId, stepType, approvalCtx.stepIndex)
@@ -488,6 +520,7 @@ async function activate(context) {
get sessionStalled() { return (0, step_probe_1.getStepProbeContext)().sessionStalled; }, get sessionStalled() { return (0, step_probe_1.getStepProbeContext)().sessionStalled; },
get lastPendingStepIndex() { return (0, step_probe_1.getStepProbeContext)().lastPendingStepIndex; }, get lastPendingStepIndex() { return (0, step_probe_1.getStepProbeContext)().lastPendingStepIndex; },
writeChatSnapshot, writeChatSnapshot,
getLastWaitingCommand: step_probe_1.getLastWaitingCommand,
}; };
const bridgePort = await (0, http_bridge_1.startHttpBridge)(httpBridgeCtx, sdk); const bridgePort = await (0, http_bridge_1.startHttpBridge)(httpBridgeCtx, sdk);
let localPort = bridgePort; let localPort = bridgePort;

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,12 @@
{ {
"name": "gravity-bridge", "name": "gravity-bridge",
"version": "0.5.34", "version": "0.5.103",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "gravity-bridge", "name": "gravity-bridge",
"version": "0.5.34", "version": "0.5.103",
"dependencies": { "dependencies": {
"cheerio": "^1.2.0", "cheerio": "^1.2.0",
"ws": "^8.19.0" "ws": "^8.19.0"

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

View File

@@ -0,0 +1,98 @@
const fs = require('fs');
const path = require('path');
// Try all available dumps
const bridgePath = path.join(process.env.USERPROFILE, '.gemini/antigravity/bridge');
const dumpFiles = ['dump_html_1.json', 'dump_html_2.json', 'dump_html_3.json', 'dump_html_4.json', 'dump_html_5.json', 'deep-inspect-result.json', 'deep-inspect-manual.json'];
function printTree(node, indent, maxDepth) {
if (!node || indent > maxDepth) return;
const tag = (node.tag || node.tagName || '?').toLowerCase();
const clsArr = (node.cls || node.className || '').split(' ').filter(c => c.length > 0);
const text = (node.text || node.textContent || '').substring(0, 50).replace(/[\n\r]+/g, ' ');
const childCount = (node.children || []).length;
let line = ' '.repeat(indent) + tag;
if (clsArr.length > 0) line += '.' + clsArr[0];
if (childCount) line += ' [' + childCount + ']';
if (text && childCount === 0) line += ' = "' + text + '"';
console.log(line);
if (node.children) {
for (const c of node.children) printTree(c, indent + 1, maxDepth);
}
}
// Find the conversation area in each dump
function findConvo(node, depth) {
if (!node || depth > 20) return null;
const cls = node.cls || node.className || '';
if (cls.includes('bg-agent-convo-background') || cls.includes('agent-convo')) return node;
if (node.children) {
for (const c of node.children) {
const r = findConvo(c, depth + 1);
if (r) return r;
}
}
return null;
}
// Find buttons (Run, Allow, Always run, Accept)
function findButtons(node, depth, results) {
if (!node || depth > 25) return;
const tag = (node.tag || node.tagName || '').toLowerCase();
const text = (node.text || node.textContent || '');
if (tag === 'button' && /Run|Allow|Accept|Always/i.test(text) && text.length < 50) {
results.push({ text: text.trim(), depth });
}
if (node.children) {
for (const c of node.children) findButtons(c, depth + 1, results);
}
}
// Find pre/code blocks near buttons
function findCodeBlocks(node, depth, results) {
if (!node || depth > 25) return;
const tag = (node.tag || node.tagName || '').toLowerCase();
const cls = node.cls || node.className || '';
if ((tag === 'pre' || tag === 'code') && cls.includes('font-mono')) {
const text = (node.text || node.textContent || '').substring(0, 80);
results.push({ tag, cls: cls.substring(0, 50), text, depth });
}
if (node.children) {
for (const c of node.children) findCodeBlocks(c, depth + 1, results);
}
}
for (const df of dumpFiles) {
const fp = path.join(bridgePath, df);
if (!fs.existsSync(fp)) continue;
try {
const dump = JSON.parse(fs.readFileSync(fp, 'utf8'));
const root = dump.bodyTree || dump.body || dump;
console.log('\n=== ' + df + ' ===');
// Find conversation area
const convo = findConvo(root, 0);
if (convo) {
console.log('>> Conversation area found, tree (depth 12):');
printTree(convo, 0, 12);
}
// Find buttons
const btns = [];
findButtons(root, 0, btns);
if (btns.length > 0) {
console.log('>> Buttons found:');
for (const b of btns) console.log(' d' + b.depth + ': ' + b.text);
}
// Find code blocks
const codes = [];
findCodeBlocks(root, 0, codes);
if (codes.length > 0) {
console.log('>> Code blocks (font-mono):');
for (const c of codes) console.log(' d' + c.depth + ': <' + c.tag + '> cls=' + c.cls + ' text="' + c.text + '"');
}
} catch (e) {
console.log('ERROR reading ' + df + ': ' + e.message);
}
}

View File

@@ -0,0 +1,28 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const dump = JSON.parse(fs.readFileSync(
path.join(os.homedir(), '.gemini', 'antigravity', 'bridge', 'dump_html.json'), 'utf-8'
));
const bodyStr = JSON.stringify(dump.body);
// Find all unique tag names
const tagMatches = bodyStr.match(/"tag":"[a-z0-9]+"/g) || [];
const uniqueTags = [...new Set(tagMatches)];
console.log('=== Unique DOM tags ===');
console.log(uniqueTags.sort().join('\n'));
// Check for pipe characters (markdown table syntax)
console.log('\n=== Pipe | in text content ===');
const pipeMatches = [...bodyStr.matchAll(/"text":"[^"]*\|[^"]*"/g)];
console.log(`Found ${pipeMatches.length} text nodes with pipe |`);
pipeMatches.slice(0, 5).forEach(m => console.log(' ', m[0].substring(0, 120)));
// Check for table-related class names
console.log('\n=== Table-related classes ===');
const classMatches = bodyStr.match(/"cls":"[^"]*"/g) || [];
const tableClasses = classMatches.filter(c => /table|grid|cell|col|row/i.test(c));
console.log(`Found ${tableClasses.length} table-related classes`);
[...new Set(tableClasses)].slice(0, 10).forEach(c => console.log(' ', c));

View File

@@ -0,0 +1,2 @@
# diff_review detection test v2
test_value = "hello"

View File

@@ -0,0 +1,37 @@
// List all channels in the guild
const https = require('https');
const TOKEN = 'MTQ3OTY0ODcxNjA1MzgwNzI4NQ.GVMGbd.WN7BliH8oq9fqbaiQcyxXesJTYgBx-ObsDkK7o';
const GUILD_ID = '1478722210460991662';
function apiGet(path) {
return new Promise((resolve, reject) => {
const opts = {
hostname: 'discord.com',
path: `/api/v10${path}`,
headers: { 'Authorization': `Bot ${TOKEN}` }
};
https.get(opts, res => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => {
try { resolve(JSON.parse(data)); } catch { resolve(data); }
});
}).on('error', reject);
});
}
async function main() {
const channels = await apiGet(`/guilds/${GUILD_ID}/channels`);
if (!Array.isArray(channels)) {
console.log('Error:', channels);
return;
}
console.log(`Total channels: ${channels.length}\n`);
channels.sort((a,b) => (a.position||0) - (b.position||0));
channels.forEach(c => {
const type = ['TEXT','DM','VOICE','GROUP_DM','CATEGORY','ANNOUNCE','','','','','','THREAD','THREAD','THREAD','','FORUM','MEDIA'][c.type] || c.type;
console.log(`${c.id} | ${type.padEnd(10)} | #${c.name}`);
});
}
main().catch(e => console.error(e));

View File

@@ -0,0 +1,55 @@
// Read latest Discord messages from ag-gravity_control channel
const https = require('https');
const TOKEN = 'MTQ3OTY0ODcxNjA1MzgwNzI4NQ.GVMGbd.WN7BliH8oq9fqbaiQcyxXesJTYgBx-ObsDkK7o';
const CHANNEL_ID = '1483082084540223663';
function apiGet(path) {
return new Promise((resolve, reject) => {
const opts = {
hostname: 'discord.com',
path: `/api/v10${path}`,
headers: { 'Authorization': `Bot ${TOKEN}` }
};
https.get(opts, res => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => {
try { resolve(JSON.parse(data)); } catch { resolve(data); }
});
}).on('error', reject);
});
}
async function main() {
const limit = process.argv[2] || 15;
const msgs = await apiGet(`/channels/${CHANNEL_ID}/messages?limit=${limit}`);
if (!Array.isArray(msgs)) {
console.log('Error:', JSON.stringify(msgs));
return;
}
console.log(`=== #ag-gravity_control — Last ${msgs.length} messages ===\n`);
msgs.reverse().forEach(m => {
const time = new Date(m.timestamp).toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', hour: '2-digit', minute: '2-digit', second: '2-digit' });
const author = m.author?.username || '?';
if (m.embeds?.length > 0) {
m.embeds.forEach(e => {
const title = e.title || '(no title)';
const desc = (e.description || '').substring(0, 300);
const colorHex = e.color ? `#${e.color.toString(16).padStart(6, '0')}` : 'default';
const footer = e.footer?.text || '';
console.log(`[${time}] 📦 EMBED [${colorHex}] ${title}`);
if (desc) console.log(` ${desc.replace(/\n/g, '\n ')}`);
if (footer) console.log(` 📎 ${footer}`);
});
} else if (m.content) {
const content = m.content.substring(0, 300);
console.log(`[${time}] 💬 ${author}: ${content}`);
}
console.log('');
});
}
main().catch(e => console.error(e));

View File

@@ -0,0 +1,29 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const d = JSON.parse(fs.readFileSync(
path.join(os.homedir(), '.gemini', 'antigravity', 'bridge', 'dump_html.json'), 'utf-8'
));
const s = JSON.stringify(d.body);
console.log('title:', d.quickInfo.title);
console.log('Has id=conversation:', s.includes('"id":"conversation"'));
console.log('Has agent-side-panel:', s.includes('antigravity-agent-side-panel'));
// Find message-block patterns
const mb = [...s.matchAll(/message-block/g)];
console.log('message-block occurrences:', mb.length);
// Find user-related class patterns
const userPatterns = ['user-color', 'user-background', 'user-message', 'user-query', 'user-input', 'human'];
userPatterns.forEach(p => {
const cnt = [...s.matchAll(new RegExp(p, 'gi'))].length;
if (cnt > 0) console.log(` ${p}: ${cnt} occurrences`);
});
// Show all unique classes that include 'message' or 'chat' or 'conversation'
const clsMatches = [...s.matchAll(/"cls":"([^"]*(?:message|chat|conversation|query|user|human)[^"]*)"/gi)];
console.log('\nClasses with message/chat/conversation/user/human:');
const uniq = [...new Set(clsMatches.map(m => m[1]))];
uniq.forEach(c => console.log(' ', c.substring(0, 120)));

View File

@@ -0,0 +1,26 @@
const fs = require('fs');
const path = require('path');
const dump = JSON.parse(fs.readFileSync(
path.join(process.env.USERPROFILE, '.gemini/antigravity/bridge/deep-inspect-result.json'), 'utf8'
));
function printTree(node, indent, maxDepth) {
if (!node || indent > maxDepth) return;
const tag = (node.tag || '?').toLowerCase();
const clsArr = (node.cls || '').split(' ').filter(c => c.length > 0);
const clsShort = clsArr.slice(0, 3).join(' ');
const text = (node.text || '').substring(0, 40).replace(/[\n\r]+/g, ' ');
const childCount = (node.children || []).length;
let line = ' '.repeat(indent) + tag;
if (clsShort) line += '.' + clsArr[0];
if (childCount) line += ' [' + childCount + ' children]';
if (text && childCount === 0) line += ' = "' + text + '"';
console.log(line);
if (node.children) {
for (const c of node.children) printTree(c, indent + 1, maxDepth);
}
}
console.log('=== FULL DOM TREE (depth 8) ===');
printTree(dump.bodyTree || dump.body, 0, 8);

View File

@@ -0,0 +1,22 @@
// Test: call agentAcceptAllInFile via extension's HTTP bridge trigger-click
// This simulates what the approval handler does
const http = require('http');
const PORT = 34332; // from observer setup log
// Write a trigger-click file to make Observer click "Accept all"
const fs = require('fs');
const path = require('path');
const bridgePath = path.join(process.env.USERPROFILE, '.gemini', 'antigravity', 'bridge');
// Check if there's a trigger_click.json
const triggerFile = path.join(bridgePath, 'trigger_click.json');
console.log('Writing trigger_click.json for accept...');
fs.writeFileSync(triggerFile, JSON.stringify({ action: 'approve', type: 'diff_review', ts: Date.now() }), 'utf-8');
console.log('Done. Check if Accept all was clicked.');
// Also check extension log for recent entries
const logFile = path.join(bridgePath, 'extension.log');
const lines = fs.readFileSync(logFile, 'utf-8').split('\n');
const recent = lines.slice(-5);
recent.forEach(l => console.log(l.substring(0, 200)));

View File

@@ -0,0 +1,114 @@
// Final simulation: exact v0.5.96 flow with realistic DOM
const {generateApprovalObserverScript} = require('../out/observer-script');
let s = generateApprovalObserverScript(18080);
try { new Function(s); console.log('SYNTAX: OK'); } catch(e) { console.log('SYNTAX ERROR:', e.message); process.exit(1); }
let promptRe = /[\u003e\u00bb\u276f]\s+(.+)/;
let stripRe = /\s*(content_copy|content_paste|play_arrow|check_circle|keyboard_arrow[_a-z]*)\s*$/;
let iconFilterRe = /^(content_copy|content_paste|play_arrow|check_circle|chevron_|keyboard_arrow|more_horiz|more_vert|expand_|alternate_email|arrow_drop)/;
// Realistic scenario: "Running command" div has siblings including a copy button
// The actual DOM probably has a structure like:
// div "Running command"
// span/div with the copy icon (textContent = "> content_copy" or just "content_copy")
// div with the actual prompt+command
// div with the buttons
function v31_simulate(name, siblings) {
console.log('\n=== ' + name + ' ===');
// Step 1: Find "Running command" header
let rcIdx = -1;
for (let i = 0; i < siblings.length; i++) {
let t = siblings[i].trim();
if (t === 'Running command' || (t.indexOf('Running command') !== -1 && t.length < 30)) {
rcIdx = i;
break;
}
}
if (rcIdx < 0) { console.log(' NO RC HEADER'); return; }
// Step 2: Collect candidates (filter icons and buttons)
let cands = [];
for (let i = 0; i < siblings.length; i++) {
if (i === rcIdx) continue;
let t = siblings[i].trim();
if (t.length < 5) continue;
if (iconFilterRe.test(t)) { console.log(' FILTER icon: "' + t.substring(0,40) + '"'); continue; }
if (/^(Always|Run|Allow|Cancel|Deny|keyboard_arrow)/i.test(t)) { console.log(' FILTER btn: "' + t.substring(0,40) + '"'); continue; }
if (t.indexOf('Always run') !== -1 && t.indexOf('Cancel') !== -1) { console.log(' FILTER btn-bar: "' + t.substring(0,40) + '"'); continue; }
cands.push(t);
console.log(' CANDIDATE: "' + t.substring(0,60) + '" (len=' + t.length + ')');
}
// Step 3: Sort by length (longest first)
cands.sort((a,b) => b.length - a.length);
// Step 4: Extract command from best candidate
for (let cand of cands) {
let m = promptRe.exec(cand);
if (m && m[1].trim().length > 3) {
let cmdV = m[1].trim().replace(stripRe, '').trim();
if (cmdV.length < 3) { console.log(' SKIP (too short after strip): "' + cmdV + '"'); continue; }
if (/^(Always|Run|Allow|Cancel|Deny)/i.test(cmdV)) continue;
console.log(' EXTRACTED: "' + cmdV + '"');
return;
}
if (cand.length > 10 && /[\u276f\u003e]/.test(cand)) {
let raw = cand.replace(stripRe, '').trim();
console.log(' EXTRACTED (raw): "' + raw + '"');
return;
}
}
console.log(' NO MATCH - will fallback to "Always run"');
}
// Scenario A: What ACTUALLY happened (3 siblings, "content_copy" mixed in command text)
v31_simulate('A: Icon in command text', [
'Running command',
'\u276f gravity_control > Start-Sleep 12; $logFile content_copy',
'Always run keyboard_arrow_up Cancel'
]);
// Scenario B: Copy button as separate small div
v31_simulate('B: Icon as separate div + command div', [
'Running command',
'> content_copy',
'\u276f gravity_control > npm run compile',
'Always run Cancel'
]);
// Scenario C: Just "content_copy" standalone (no >)
v31_simulate('C: Standalone icon + command', [
'Running command',
'content_copy',
'\u276f gravity_control > git push origin main',
'Always run Cancel'
]);
// Scenario D: Multiple icons mixed
v31_simulate('D: Multiple icons + command', [
'Running command',
'play_arrow',
'> content_copy',
'\u276f gravity_control > node -e "console.log(1)" content_copy',
'Always run keyboard_arrow_up Cancel'
]);
// Scenario E: Edge - no command, only prompt
v31_simulate('E: Prompt only', [
'Running command',
'\u276f gravity_control > ',
'Always run Cancel'
]);
// Scenario F: The v0.5.95 cmdV=content_copy case
// This implies regex matched "content_copy" from a "> content_copy" sibling
// and there was no longer sibling
v31_simulate('F: Only icon sibling (worst case)', [
'Running command',
'> content_copy',
'Always run Cancel'
]);
console.log('\n=== SIMULATION COMPLETE ===');

View File

@@ -0,0 +1,43 @@
const {generateApprovalObserverScript} = require('../out/observer-script');
let s = generateApprovalObserverScript(18080);
// Extract the regex strings
let junkMatch = s.match(/JUNK_CODE_RE\s*=\s*(\/[^;]+)/);
let promptMatch = s.match(/PROMPT_ONLY_RE\s*=\s*(\/[^;]+)/);
console.log('JUNK_CODE_RE:', junkMatch[1].substring(0, 100));
console.log('PROMPT_ONLY_RE:', promptMatch[1]);
// Use eval to construct the actual regexes
let JUNK = eval(junkMatch[1]);
let PROMPT = eval(promptMatch[1]);
let tests = [
['\u276f gravity_control > ', 'prompt only (no command)'],
['\u276f extension > ', 'prompt only (extension)'],
['\u276f gravity_control > $logFile = Join-Path $env:USERPROFILE', 'PS var assignment'],
['\u276f extension > npm.cmd run compile', 'npm compile'],
['\u276f gravity_control > Start-Sleep 12', 'Start-Sleep'],
['\u276f gravity_control > git add -A; git commit -m "test"', 'git commit'],
['\u276f gravity_control > node -e "const {gen}=require()"', 'node with require'],
['\u276f gravity_control > Get-Content file.txt', 'Get-Content'],
['\u276f gravity_control > npm.cmd version patch', 'npm version'],
['function test() { return 1; }', 'JS function (should be JUNK)'],
['const x = require("fs")', 'JS const (should be JUNK)'],
['import { foo } from "bar"', 'JS import (should be JUNK)'],
];
console.log('\n=== CODE ELEMENT FILTER ANALYSIS ===');
for (let [text, desc] of tests) {
let isJunk = JUNK.test(text);
let isPrompt = PROMPT.test(text.trim());
let junkPart = isJunk ? text.match(JUNK)[0] : null;
let status;
if (isPrompt) status = 'SKIP-PROMPT';
else if (isJunk) status = 'SKIP-JUNK (' + junkPart + ')';
else status = 'PASS';
let isBug = (isJunk || isPrompt) && text.indexOf('\u276f') !== -1 && text.trim().length > 25;
console.log((isBug ? 'BUG ' : ' ') + status.padEnd(40) + ' | ' + desc);
}

View File

@@ -0,0 +1,42 @@
const {generateApprovalObserverScript} = require('../out/observer-script');
let s = generateApprovalObserverScript(18080);
// Find the regex used in v30 candidate matching
let idx = s.indexOf('candT.match(');
if (idx < 0) idx = s.indexOf('sibT.match(');
let reStr = s.substring(idx, s.indexOf(');', idx) + 1);
console.log('Match code:', reStr.substring(0, 60));
// Extract just the regex part
let reMatch = reStr.match(/\/(.*?)\//);
let reSource = reMatch ? reMatch[0] : 'NOT FOUND';
console.log('Regex source:', reSource);
// Build and test the actual regex
let re = new RegExp(reMatch[1]);
console.log('Regex object:', re);
// Test with the EXACT patterns from logs
let tests = [
['Normal', '\u276f gravity_control > Start-Sleep 12 content_copy'],
['Git cmd', '\u276f gravity_control > git add -A; git commit -m "test"'],
['Short', '> content_copy'],
['Prompt only', '\u276f gravity_control > '],
['Dir cmd', '\u276f gravity_control > dir content_copy'],
];
console.log('\n=== REGEX TESTS ===');
let stripRe = /\s*(content_copy|content_paste|play_arrow|check_circle|keyboard_arrow[_a-z]*)\s*$/;
for (let [name, text] of tests) {
let m = re.exec(text);
if (m) {
let raw = m[1].trim();
let cleaned = raw.replace(stripRe, '').trim();
console.log(name + ':');
console.log(' raw match[1]: "' + raw + '"');
console.log(' after strip: "' + cleaned + '"');
console.log(' length ok: ' + (cleaned.length >= 3));
} else {
console.log(name + ': NO MATCH');
}
}

View File

@@ -0,0 +1,122 @@
const {generateApprovalObserverScript} = require('../out/observer-script');
let s = generateApprovalObserverScript(18080);
// 1. SYNTAX CHECK
try { new Function(s); console.log('[1] SYNTAX: OK'); } catch(e) { console.log('[1] SYNTAX ERROR:', e.message); process.exit(1); }
// 2. v30 block exists
let v30Start = s.indexOf('// v30:');
let v30End = s.indexOf('// v23:', v30Start);
console.log('[2] v30 block:', v30Start > 0 && v30End > v30Start ? 'OK' : 'MISSING');
// 3. Key features present
console.log('[3] rcCands:', s.indexOf('rcCands') > 0 ? 'OK' : 'MISSING');
console.log('[4] content_copy filter:', s.indexOf('content_copy|content_paste') > 0 ? 'OK' : 'MISSING');
console.log('[5] sort by length:', s.indexOf('.sort(') > 0 ? 'OK' : 'MISSING');
console.log('[6] icon strip replace:', (s.match(/content_copy/g)||[]).length >= 2 ? 'OK (filter+strip)' : 'CHECK');
// 4. Simulate exact DOM from BTN-DOM-DUMP + CONTEXT logs
let promptRe = /[\u003e\u00bb\u276f]\s+(.+)/;
let stripRe = /\s*(content_copy|content_paste|play_arrow|check_circle|keyboard_arrow[_a-z]*)\s*$/;
let iconFilterRe = /^(content_copy|content_paste|play_arrow|check_circle|chevron_|keyboard_arrow|more_horiz|more_vert|expand_|alternate_email|arrow_drop)/;
let btnFilterRe = /^(Always|Run|Allow|Cancel|Deny|keyboard_arrow)/i;
function simulate(name, siblings) {
console.log('\n=== ' + name + ' ===');
let rcFound = false;
for (let sib of siblings) {
if (sib === 'Running command' || (sib.indexOf('Running command') !== -1 && sib.length < 30)) {
rcFound = true;
break;
}
}
if (!rcFound) { console.log(' RC header NOT FOUND'); return null; }
let cands = [];
for (let sib of siblings) {
if (sib === 'Running command') continue;
if (sib.length < 5) continue;
if (iconFilterRe.test(sib)) { console.log(' SKIP icon: "' + sib.substring(0,30) + '"'); continue; }
if (btnFilterRe.test(sib)) { console.log(' SKIP btn: "' + sib.substring(0,30) + '"'); continue; }
if (sib.indexOf('Always run') !== -1 && sib.indexOf('Cancel') !== -1) { console.log(' SKIP btn-bar: "' + sib.substring(0,30) + '"'); continue; }
cands.push(sib);
}
cands.sort((a,b) => b.length - a.length);
console.log(' Candidates: ' + cands.length);
for (let i = 0; i < cands.length; i++) {
console.log(' [' + i + '] len=' + cands[i].length + ': "' + cands[i].substring(0,80) + '"');
}
for (let cand of cands) {
let m = promptRe.exec(cand);
if (m && m[1].trim().length > 3) {
let cmdV = m[1].trim().replace(stripRe, '').trim();
if (cmdV.length < 3) continue;
console.log(' RESULT: "' + cmdV + '"');
return cmdV;
}
if (cand.length > 10 && /[\u276f\u003e]/.test(cand)) {
let raw = cand.replace(stripRe, '').trim();
console.log(' RESULT (raw): "' + raw + '"');
return raw;
}
}
console.log(' RESULT: NO MATCH');
return null;
}
// Case 1: From BTN-DOM-DUMP (3 siblings, command + content_copy icon)
simulate('Case1: Normal command with icon', [
'Running command',
'\u276f gravity_control > Start-Sleep 12; $logFile content_copy',
'Always run keyboard_arrow_up Cancel'
]);
// Case 2: content_copy as standalone sibling
simulate('Case2: Icon as separate div', [
'Running command',
'content_copy',
'\u276f gravity_control > npm run compile',
'Always run Cancel'
]);
// Case 3: No icon appended
simulate('Case3: Clean command', [
'Running command',
'\u276f gravity_control > git add -A; git commit -m "test"',
'Always run Cancel'
]);
// Case 4: Very long command
simulate('Case4: Long command', [
'Running command',
'\u276f gravity_control > Select-String -Path "$env:USERPROFILE\\.gemini\\antigravity\\bridge\\extension.log" -Pattern "CONTEXT" content_copy',
'Always run keyboard_arrow_up Cancel'
]);
// Case 5: Prompt only (no command yet)
simulate('Case5: Prompt only', [
'Running command',
'\u276f gravity_control > ',
'Always run Cancel'
]);
// Case 6: Multiple icon texts
simulate('Case6: Multiple icons', [
'Running command',
'play_arrow',
'content_copy',
'\u276f gravity_control > dir content_copy',
'Always run Cancel'
]);
// Case 7: Observed log pattern - "content_copy" was cmdV
// This means the regex matched on just "content_copy" with a > before it
// Possible: the sibling text is "> content_copy" (very short prompt)
simulate('Case7: Short prompt with icon only', [
'Running command',
'> content_copy',
'Always run Cancel'
]);
console.log('\n=== ALL TESTS COMPLETE ===');

View File

@@ -0,0 +1,52 @@
const {generateApprovalObserverScript} = require('../out/observer-script');
let s = generateApprovalObserverScript(18080);
// Extract the terminal prompt regex from generated code
let idx = s.indexOf('_termPromptMatch');
let reCtx = s.substring(idx, s.indexOf(');', idx) + 1);
console.log('v32 code:', reCtx.substring(0, 80));
// Extract regex
let reMatch = reCtx.match(/\/(.+?)\//);
let termRe = new RegExp(reMatch[1]);
console.log('v32 regex:', termRe);
let stripRe = /\s*(content_copy|content_paste|play_arrow)\s*$/;
let tests = [
// Should MATCH (terminal commands)
['\u276f gravity_control > Start-Sleep 12', true, 'Start-Sleep'],
['\u276f gravity_control > npm.cmd run compile', true, 'npm compile'],
['\u276f gravity_control > $logFile = Join-Path $env:USERPROFILE', true, 'PS variable (had JUNK match)'],
['\u276f gravity_control > git add -A; git commit -m "test"', true, 'git commit'],
['\u276f gravity_control > node -e "const {gen}=require(\'./out\')"', true, 'node with const (was JUNK)'],
['\u276f extension > npm.cmd run compile', true, 'extension npm'],
['\u276f gravity_control > Start-Sleep 12 content_copy', true, 'with icon (strip)'],
['\u276f gravity_control > Get-Content f.txt | Select-Object -Last 5', true, 'Get-Content'],
// Should NOT match (prompt only, no command)
['\u276f gravity_control > ', false, 'prompt only'],
['\u276f extension > ', false, 'prompt only ext'],
// Should NOT match (not terminal - JS code)
['function test() { return 1; }', false, 'JS function'],
['const x = require("fs")', false, 'JS const'],
['import { foo } from "bar"', false, 'JS import'],
// Should NOT match (no prompt marker)
['gravity_control > dir', false, 'no marker'],
];
console.log('\n=== v32 TERMINAL PROMPT REGEX TESTS ===');
let pass = 0, fail = 0;
for (let [text, shouldMatch, desc] of tests) {
let m = termRe.exec(text);
let matched = false;
let cmdV = null;
if (m && m[1] && m[1].trim().length > 2) {
cmdV = m[1].trim().replace(stripRe, '').trim();
matched = cmdV.length > 2;
}
let ok = matched === shouldMatch;
if (ok) pass++; else fail++;
console.log((ok ? 'PASS' : 'FAIL') + ' | ' + (matched ? 'MATCH' : 'SKIP ').padEnd(5) + ' | ' + desc);
if (matched && cmdV) console.log(' cmd: "' + cmdV + '"');
}
console.log('\nResult: ' + pass + '/' + (pass+fail) + ' passed' + (fail > 0 ? ' (' + fail + ' FAILED!)' : ' ALL OK'));

View File

@@ -5,8 +5,13 @@
* Handles: * Handles:
* - Response file watching (file-based bridge fallback) * - Response file watching (file-based bridge fallback)
* - Response processing (diff_review, DOM observer, step_probe paths) * - Response processing (diff_review, DOM observer, step_probe paths)
* - Multi-strategy approval execution (RPC, VS Code commands, DOM click) * - Multi-strategy approval execution (VS Code commands, RPC, DOM click)
* - Diff review Accept/Reject via VS Code commands * - Diff review Accept/Reject via VS Code commands
*
* STRATEGY ORDER (most reliable first):
* 0. antigravity.acceptAgentStep / rejectAgentStep — AG's own commands, always works
* 1. HandleCascadeUserInteraction RPC — cross-platform, needs stepIndex
* 2. DOM click trigger via HTTP bridge — fallback
*/ */
import * as vscode from 'vscode'; import * as vscode from 'vscode';
@@ -200,6 +205,25 @@ async function processResponseFile(filePath: string) {
} }
const content = fs.readFileSync(filePath, 'utf-8'); const content = fs.readFileSync(filePath, 'utf-8');
const resp = JSON.parse(content); const resp = JSON.parse(content);
// v22: Skip files written by the WS response handler (extension.ts onResponse).
// Those files exist ONLY for Observer's pollResponseGroup to read via HTTP.
// The WS handler already calls tryApprovalStrategies, so processing here is redundant.
// Without this skip, the watcher deletes the file before Observer can poll it
// (since no pending file exists for the isDomObserver check).
if (resp._from_ws) {
// v26: TTL — delete stale _from_ws files after 60s to prevent infinite SKIP spam
const wsRidTs = parseInt((resp.request_id || '').split('_')[0], 10);
const wsAge = isNaN(wsRidTs) ? 999999 : Date.now() - wsRidTs;
if (wsAge > 60_000) {
ctx.logToFile(`[RESPONSE] CLEANUP stale _from_ws file: ${resp.request_id} age=${Math.round(wsAge / 1000)}s`);
try { fs.unlinkSync(filePath); } catch { }
return;
}
ctx.logToFile(`[RESPONSE] SKIP _from_ws file (for Observer pollResponseGroup): ${resp.request_id}`);
return;
}
const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved} step_type=${resp.step_type || '(missing)'} keys=[${Object.keys(resp).join(',')}]`; const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved} step_type=${resp.step_type || '(missing)'} keys=[${Object.keys(resp).join(',')}]`;
console.log(`Gravity Bridge: ${msg}`); console.log(`Gravity Bridge: ${msg}`);
ctx.logToFile(msg); ctx.logToFile(msg);
@@ -256,7 +280,7 @@ async function processResponseFile(filePath: string) {
} catch { } } catch { }
} }
// ═══ MULTI-STRATEGY APPROVAL (v2.1) ═══ // ═══ MULTI-STRATEGY APPROVAL (v3.0) ═══
const approved = resp.approved; const approved = resp.approved;
// ── diff_review: Accept all / Reject all ── // ── diff_review: Accept all / Reject all ──
@@ -268,16 +292,10 @@ async function processResponseFile(filePath: string) {
button_index: resp.button_index, button_index: resp.button_index,
step_type: pendingStepType, step_type: pendingStepType,
}); });
} else if (isDomObserver) {
// DOM observer path: ALSO try RPC strategies (renderer click is unreliable)
const targetSession = sessionId || ctx.activeSessionId;
ctx.logToFile(`[RESPONSE] dom_observer → tryApprovalStrategies(${approved}, ${targetSession.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`);
const strategyResult = await tryApprovalStrategies(approved, targetSession, pendingStepType, pendingStepIndex);
ctx.logToFile(`[RESPONSE] dom strategy result: ${strategyResult}`);
} else { } else {
// Step probe path: run ALL approval strategies // ALL paths (dom_observer + step_probe) use same strategy pipeline
const targetSession = sessionId || ctx.activeSessionId; const targetSession = sessionId || ctx.activeSessionId;
ctx.logToFile(`[RESPONSE] step_probe → tryApprovalStrategies(${approved}, ${targetSession.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`); ctx.logToFile(`[RESPONSE] → tryApprovalStrategies(${approved}, ${targetSession.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`);
const strategyResult = await tryApprovalStrategies(approved, targetSession, pendingStepType, pendingStepIndex); const strategyResult = await tryApprovalStrategies(approved, targetSession, pendingStepType, pendingStepIndex);
ctx.logToFile(`[RESPONSE] strategy result: ${strategyResult}`); ctx.logToFile(`[RESPONSE] strategy result: ${strategyResult}`);
} }
@@ -307,9 +325,9 @@ async function processResponseFile(filePath: string) {
* Returns a string describing which method succeeded (or all failed). * Returns a string describing which method succeeded (or all failed).
* *
* Strategy order (most reliable first): * Strategy order (most reliable first):
* 1. HandleCascadeUserInteraction RPC (cross-platform, no focus) * 0. antigravity.acceptAgentStep / rejectAgentStep (AG VS Code commands — always works)
* 2. VS Code accept/reject commands (focus-dependent) * 1. HandleCascadeUserInteraction RPC (cross-platform, needs stepIndex)
* 3. Log failure for manual intervention * 2. Renderer DOM Click via HTTP Bridge (fallback)
*/ */
export async function tryApprovalStrategies(approved: boolean, sessionId: string, stepType: string = '', stepIndex: number = -1): Promise<string> { export async function tryApprovalStrategies(approved: boolean, sessionId: string, stepType: string = '', stepIndex: number = -1): Promise<string> {
const action = approved ? 'APPROVE' : 'REJECT'; const action = approved ? 'APPROVE' : 'REJECT';
@@ -317,90 +335,153 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string
: (ctx.lastPendingStepIndex >= 0 ? ctx.lastPendingStepIndex : -1); : (ctx.lastPendingStepIndex >= 0 ? ctx.lastPendingStepIndex : -1);
ctx.logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${effectiveStepIndex}`); ctx.logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${effectiveStepIndex}`);
// ── Dynamic Command Discovery (log what's available during WAITING state) ── // ══════════════════════════════════════════════════════════
let approvalCmdList: string[] = []; // STRATEGY 0: SDK-verified AG commands (step_type-aware dispatch)
try { //
const allCmds = await vscode.commands.getCommands(true); // From SDK index.js (verified command mapping):
const agCmds = allCmds.filter((c: string) => c.startsWith('antigravity.')); // antigravity.agent.acceptAgentStep — code edits, file writes
approvalCmdList = agCmds.filter((c: string) => { // antigravity.agent.rejectAgentStep — reject code edits
const lower = c.toLowerCase(); // antigravity.command.accept — non-terminal commands (Run, Allow, etc.)
return lower.includes('accept') || lower.includes('reject') || lower.includes('approve') // antigravity.command.reject — reject non-terminal commands
|| lower.includes('terminal') || lower.includes('run') || lower.includes('step') // antigravity.terminalCommand.accept — terminal commands
|| lower.includes('cascade') || lower.includes('action'); // antigravity.terminalCommand.reject — reject terminal commands
}); // antigravity.terminalCommand.run — run terminal commands
ctx.logToFile(`[APPROVAL-CMD-CHECK] ${agCmds.length} total, ${approvalCmdList.length} approval-related:`); //
for (const c of approvalCmdList) { // These operate on the currently focused/active step — no stepIndex needed!
ctx.logToFile(`[APPROVAL-CMD-CHECK] → ${c}`); // ══════════════════════════════════════════════════════════
{
const typeLower = stepType.toLowerCase().replace('cortex_step_type_', '');
// Determine which SDK command pair to use based on step_type
let acceptCmd: string;
let rejectCmd: string;
if (typeLower.includes('code_edit') || typeLower.includes('write_to_file')
|| typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')
|| typeLower === 'diff_review') {
// Code edits → agent step commands
acceptCmd = 'antigravity.agent.acceptAgentStep';
rejectCmd = 'antigravity.agent.rejectAgentStep';
} else if (typeLower.includes('run_command') || typeLower.includes('shell_exec')
|| typeLower.includes('send_command_input')) {
// Terminal commands → terminal command pair
acceptCmd = 'antigravity.terminalCommand.accept';
rejectCmd = 'antigravity.terminalCommand.reject';
} else if (typeLower === 'command' || typeLower.includes('permission')
|| typeLower.includes('browser') || typeLower.includes('mcp')
|| typeLower.includes('extension_code') || typeLower.includes('subagent')
|| typeLower.includes('open_browser') || typeLower.includes('read_url')
|| typeLower.includes('invoke_subagent')) {
// Non-terminal commands (Run, Allow, etc.) → command pair
acceptCmd = 'antigravity.command.accept';
rejectCmd = 'antigravity.command.reject';
} else {
// Unknown type — try all three in order
acceptCmd = 'antigravity.command.accept';
rejectCmd = 'antigravity.command.reject';
}
const primaryCmd = approved ? acceptCmd : rejectCmd;
ctx.logToFile(`[APPROVAL-0] stepType="${stepType}" → ${primaryCmd}`);
try {
await vscode.commands.executeCommand(primaryCmd);
ctx.logToFile(`[APPROVAL-0] ✅ ${primaryCmd} SUCCESS`);
return `SDK:${primaryCmd}`;
} catch (e: any) {
ctx.logToFile(`[APPROVAL-0] ❌ ${primaryCmd} failed: ${e.message?.substring(0, 200)}`);
}
// Fallback: if the primary type-specific command failed, try the other pairs
const fallbackPairs = [
approved ? 'antigravity.command.accept' : 'antigravity.command.reject',
approved ? 'antigravity.agent.acceptAgentStep' : 'antigravity.agent.rejectAgentStep',
approved ? 'antigravity.terminalCommand.accept' : 'antigravity.terminalCommand.reject',
].filter(cmd => cmd !== primaryCmd); // skip already-tried
for (const fallbackCmd of fallbackPairs) {
try {
ctx.logToFile(`[APPROVAL-0-FB] Trying ${fallbackCmd}...`);
await vscode.commands.executeCommand(fallbackCmd);
ctx.logToFile(`[APPROVAL-0-FB] ✅ ${fallbackCmd} SUCCESS`);
return `SDK-FB:${fallbackCmd}`;
} catch (e: any) {
ctx.logToFile(`[APPROVAL-0-FB] ❌ ${fallbackCmd}: ${e.message?.substring(0, 100)}`);
}
} }
} catch (e: any) {
ctx.logToFile(`[APPROVAL-CMD-CHECK] error: ${e.message}`);
} }
// ══════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════
// STRATEGY 0-PROTO: Correct proto-based RPC (decoded from AG source) // STRATEGY 1: HandleCascadeUserInteraction RPC
// Now supports BOTH approve AND reject.
// Requires valid stepIndex for most step types.
// ══════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════
if (ctx.sdk && approved && effectiveStepIndex >= 0) { if (ctx.sdk && effectiveStepIndex >= 0) {
// Build interaction sub-message based on step_type
const typeLower = stepType.toLowerCase().replace('cortex_step_type_', ''); const typeLower = stepType.toLowerCase().replace('cortex_step_type_', '');
let interactionPayload: Record<string, any> = {}; let interactionPayload: Record<string, any> = {};
// Code edit steps — use dedicated RPC
if (typeLower.includes('code_edit') || typeLower.includes('write_to_file') || typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')) { if (typeLower.includes('code_edit') || typeLower.includes('write_to_file') || typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')) {
// CODE EDIT: Uses acknowledgeCodeActionStep RPC (correct AG LS method)
try { try {
ctx.logToFile(`[APPROVAL-CODE-EDIT] trying submitCodeAcknowledgement command`); ctx.logToFile(`[APPROVAL-1-CODE] trying submitCodeAcknowledgement command`);
await vscode.commands.executeCommand('antigravity.prioritized.submitCodeAcknowledgement'); await vscode.commands.executeCommand('antigravity.prioritized.submitCodeAcknowledgement');
ctx.logToFile(`[APPROVAL-CODE-EDIT] ✅ submitCodeAcknowledgement OK`); ctx.logToFile(`[APPROVAL-1-CODE] ✅ submitCodeAcknowledgement OK`);
return `CMD:submitCodeAcknowledgement(accept=${approved})`; return `CMD:submitCodeAcknowledgement(accept=${approved})`;
} catch { } catch {
ctx.logToFile(`[APPROVAL-CODE-EDIT] submitCodeAcknowledgement not available, trying RPC`); ctx.logToFile(`[APPROVAL-1-CODE] submitCodeAcknowledgement not available, trying RPC`);
} }
// Direct LS RPC with correct method name
try { try {
ctx.logToFile(`[APPROVAL-CODE-EDIT] acknowledgeCodeActionStep(cascadeId=${sessionId.substring(0, 8)}, accept=${approved}, stepIndices=[${effectiveStepIndex}])`); ctx.logToFile(`[APPROVAL-1-CODE] acknowledgeCodeActionStep(cascadeId=${sessionId.substring(0, 8)}, accept=${approved}, stepIndices=[${effectiveStepIndex}])`);
const ackResult = await ctx.sdk.ls.rawRPC('acknowledgeCodeActionStep', { const ackResult = await ctx.sdk.ls.rawRPC('acknowledgeCodeActionStep', {
cascadeId: sessionId, cascadeId: sessionId,
accept: approved, accept: approved,
stepIndices: [effectiveStepIndex], stepIndices: [effectiveStepIndex],
}); });
ctx.logToFile(`[APPROVAL-CODE-EDIT] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`); ctx.logToFile(`[APPROVAL-1-CODE] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`);
return `RPC:acknowledgeCodeActionStep(accept=${approved})`; return `RPC:acknowledgeCodeActionStep(accept=${approved})`;
} catch (e: any) { } catch (e: any) {
ctx.logToFile(`[APPROVAL-CODE-EDIT] ❌ ${e.message.substring(0, 200)}`); ctx.logToFile(`[APPROVAL-1-CODE] ❌ ${e.message.substring(0, 200)}`);
ctx.logToFile(`[APPROVAL-CODE-EDIT] falling back to HandleCascadeUserInteraction`); // Fall through to generic HandleCascadeUserInteraction
interactionPayload = { runCommand: { confirm: true } }; interactionPayload = { runCommand: { confirm: approved } };
} }
} }
// Map step_type to interaction sub-message field // Map step_type to interaction sub-message field
// CRITICAL FIX: Use `confirm: approved` (not always true) to support REJECT
if (typeLower.includes('run_command') || typeLower.includes('shell_exec')) { if (typeLower.includes('run_command') || typeLower.includes('shell_exec')) {
interactionPayload = { runCommand: { confirm: true } }; interactionPayload = { runCommand: { confirm: approved } };
} else if (typeLower.includes('open_browser')) { } else if (typeLower.includes('open_browser')) {
interactionPayload = { openBrowserUrl: { confirm: true } }; interactionPayload = { openBrowserUrl: { confirm: approved } };
} else if (typeLower.includes('send_command_input')) { } else if (typeLower.includes('send_command_input')) {
interactionPayload = { sendCommandInput: { confirm: true } }; interactionPayload = { sendCommandInput: { confirm: approved } };
} else if (typeLower.includes('read_url')) { } else if (typeLower.includes('read_url')) {
interactionPayload = { readUrlContent: { confirm: true } }; interactionPayload = { readUrlContent: { confirm: approved } };
} else if (typeLower.includes('mcp')) { } else if (typeLower.includes('mcp')) {
interactionPayload = { mcpTool: { confirm: true } }; interactionPayload = { mcpTool: { confirm: approved } };
} else if (typeLower.includes('invoke_subagent') || typeLower.includes('extension_code') || typeLower.includes('browser_subagent')) { } else if (typeLower.includes('invoke_subagent') || typeLower.includes('extension_code') || typeLower.includes('browser_subagent')) {
interactionPayload = { runExtensionCode: { confirm: true } }; interactionPayload = { runExtensionCode: { confirm: approved } };
} else if (typeLower.includes('file_permission')) { } else if (typeLower.includes('file_permission')) {
const scope = typeLower.includes('conversation') ? 2 : 1; if (typeLower.includes('deny')) {
interactionPayload = { filePermission: { allow: true, scope } }; interactionPayload = { filePermission: { allow: false, scope: 1 } };
} else {
const scope = typeLower.includes('conversation') ? 2 : 1;
interactionPayload = { filePermission: { allow: approved, scope } };
}
} else if (typeLower.includes('elicitation')) { } else if (typeLower.includes('elicitation')) {
interactionPayload = { elicitation: {} }; interactionPayload = { elicitation: {} };
} else if (typeLower === 'permission' || typeLower.includes('permission')) { } else if (typeLower === 'permission' || typeLower.includes('permission')) {
// DOM observer 'permission' type: browser_subagent Allow/Deny dialog interactionPayload = { runExtensionCode: { confirm: approved } };
// Try runExtensionCode first (most common for JS execution permission) } else if (typeLower === 'command' || typeLower === '') {
interactionPayload = { runExtensionCode: { confirm: true } }; // Generic command — most common case from DOM observer
interactionPayload = { runCommand: { confirm: approved } };
} else { } else {
// Default: try run_command (most common) // Default: try run_command
interactionPayload = { runCommand: { confirm: true } }; interactionPayload = { runCommand: { confirm: approved } };
} }
const activeTrajectoryId = getTrajectoryId(); const activeTrajectoryId = getTrajectoryId();
const protoVariants = [ const protoVariants = [
// Variant A: camelCase with trajectoryId (proven working for run_command) // Variant A: camelCase with trajectoryId
{ {
cascadeId: sessionId, cascadeId: sessionId,
interaction: { interaction: {
@@ -431,20 +512,17 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string
for (let i = 0; i < protoVariants.length; i++) { for (let i = 0; i < protoVariants.length; i++) {
try { try {
const payload = protoVariants[i]; const payload = protoVariants[i];
ctx.logToFile(`[APPROVAL-PROTO-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 250)})`); ctx.logToFile(`[APPROVAL-1-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 250)})`);
const rpcResult = await ctx.sdk.ls.rawRPC('HandleCascadeUserInteraction', payload); const rpcResult = await ctx.sdk.ls.rawRPC('HandleCascadeUserInteraction', payload);
ctx.logToFile(`[APPROVAL-PROTO-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`); ctx.logToFile(`[APPROVAL-1-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
return `RPC-PROTO-${i}:HandleCascadeUserInteraction(${typeLower})`; return `RPC-${i}:HandleCascadeUserInteraction(${typeLower},${action})`;
} catch (e: any) { } catch (e: any) {
lastRpcError = e.message || ''; lastRpcError = e.message || '';
ctx.logToFile(`[APPROVAL-PROTO-${i}] ❌ ${lastRpcError.substring(0, 300)}`); ctx.logToFile(`[APPROVAL-1-${i}] ❌ ${lastRpcError.substring(0, 300)}`);
} }
} }
// ── Auto-recovery: wrong-LS detection ────────────────────── // ── Auto-recovery: wrong-LS detection ──────────────────────
// All 3 proto variants failed. If the error is "input not registered",
// SDK is likely connected to wrong LS process. Attempt fixLSConnection
// and retry ONE time to avoid permanent failure.
if (ctx.fixLSConnection && lastRpcError.includes('input not registered')) { if (ctx.fixLSConnection && lastRpcError.includes('input not registered')) {
ctx.logToFile('[APPROVAL] ⚠️ wrong-LS detected ("input not registered"), attempting LS fix...'); ctx.logToFile('[APPROVAL] ⚠️ wrong-LS detected ("input not registered"), attempting LS fix...');
try { try {
@@ -453,10 +531,9 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string
ctx.logToFile('[APPROVAL] LS reconnected — retrying first proto variant...'); ctx.logToFile('[APPROVAL] LS reconnected — retrying first proto variant...');
try { try {
const retryPayload = protoVariants[0]; const retryPayload = protoVariants[0];
ctx.logToFile(`[APPROVAL-RETRY] HandleCascadeUserInteraction(${JSON.stringify(retryPayload).substring(0, 250)})`);
const retryResult = await ctx.sdk.ls.rawRPC('HandleCascadeUserInteraction', retryPayload); const retryResult = await ctx.sdk.ls.rawRPC('HandleCascadeUserInteraction', retryPayload);
ctx.logToFile(`[APPROVAL-RETRY] ✅ SUCCESS: ${JSON.stringify(retryResult).substring(0, 200)}`); ctx.logToFile(`[APPROVAL-RETRY] ✅ SUCCESS: ${JSON.stringify(retryResult).substring(0, 200)}`);
return `RPC-RETRY:HandleCascadeUserInteraction(${typeLower})`; return `RPC-RETRY:HandleCascadeUserInteraction(${typeLower},${action})`;
} catch (retryErr: any) { } catch (retryErr: any) {
ctx.logToFile(`[APPROVAL-RETRY] ❌ ${retryErr.message?.substring(0, 200)}`); ctx.logToFile(`[APPROVAL-RETRY] ❌ ${retryErr.message?.substring(0, 200)}`);
} }
@@ -467,9 +544,14 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string
ctx.logToFile(`[APPROVAL] fixLSConnection error: ${fixErr.message?.substring(0, 200)}`); ctx.logToFile(`[APPROVAL] fixLSConnection error: ${fixErr.message?.substring(0, 200)}`);
} }
} }
} else if (ctx.sdk && effectiveStepIndex < 0) {
ctx.logToFile(`[APPROVAL-1] SKIPPED RPC: stepIndex=${effectiveStepIndex} (unknown) — Strategy 0 (VS Code command) was the primary attempt`);
} }
// ── Strategy 2: Renderer DOM Click via HTTP Bridge (primary fallback) ── // ══════════════════════════════════════════════════════════
// STRATEGY 2: Renderer DOM Click via HTTP Bridge (fallback)
// Sets a click trigger that the observer script polls and executes.
// ══════════════════════════════════════════════════════════
try { try {
const triggerAction = approved ? 'approve' : 'reject'; const triggerAction = approved ? 'approve' : 'reject';
ctx.logToFile(`[APPROVAL-2] Setting clickTrigger=${triggerAction} for renderer DOM click`); ctx.logToFile(`[APPROVAL-2] Setting clickTrigger=${triggerAction} for renderer DOM click`);
@@ -479,6 +561,6 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string
ctx.logToFile(`[APPROVAL-2] ❌ FAIL: ${e.message}`); ctx.logToFile(`[APPROVAL-2] ❌ FAIL: ${e.message}`);
} }
ctx.logToFile(`[APPROVAL] strategies complete — check logs for results`); ctx.logToFile(`[APPROVAL] All strategies complete for ${action}`);
return `STRATEGIES_DONE:${action}`; return `STRATEGIES_DONE:${action}`;
} }

View File

@@ -16,7 +16,7 @@ import * as path from 'path';
import * as os from 'os'; import * as os from 'os';
import * as cp from 'child_process'; import * as cp from 'child_process';
import { WSBridgeClient, WSResponseData, WSCommandData } from './ws-client'; import { WSBridgeClient, WSResponseData, WSCommandData } from './ws-client';
import { initStepProbe, BridgeContext, writePendingApproval, tryApprovalStrategies, writeRegistration, getApprovalContext, resetPendingState, resetPendingStateForReconnect, handleDiffReviewResponse, getActiveSessionId as getStepProbeSessionId, getStepProbeContext } from './step-probe'; import { initStepProbe, BridgeContext, writePendingApproval, tryApprovalStrategies, writeRegistration, getApprovalContext, resetPendingState, resetPendingStateForReconnect, handleDiffReviewResponse, getActiveSessionId as getStepProbeSessionId, getStepProbeContext, getLastWaitingCommand } from './step-probe';
import { startHttpBridge, getDeterministicPort, HttpBridgeContext } from './http-bridge'; import { startHttpBridge, getDeterministicPort, HttpBridgeContext } from './http-bridge';
import { setupApprovalObserver } from './html-patcher'; import { setupApprovalObserver } from './html-patcher';
import { watchCommandsDir, handleWSCommand, disposeCommandsWatcher, CommandHandlerContext } from './command-handler'; import { watchCommandsDir, handleWSCommand, disposeCommandsWatcher, CommandHandlerContext } from './command-handler';
@@ -209,8 +209,8 @@ export async function fixLSConnection(): Promise<boolean> {
// Find the line whose workspace_id matches our workspace (case-insensitive) // Find the line whose workspace_id matches our workspace (case-insensitive)
let matchedLine: string | null = null; let matchedLine: string | null = null;
let fallbackLine: string | null = null; // v15: LS without workspace_id (AG restart)
for (const line of lines) { for (const line of lines) {
const lower = line.toLowerCase();
// Match workspace_id arg against our hint // Match workspace_id arg against our hint
const wsMatch = line.match(/--workspace_id[= ](\S+)/i); const wsMatch = line.match(/--workspace_id[= ](\S+)/i);
if (wsMatch) { if (wsMatch) {
@@ -219,12 +219,25 @@ export async function fixLSConnection(): Promise<boolean> {
matchedLine = line; matchedLine = line;
break; break;
} }
} else {
// v15: LS without --workspace_id (new AG main LS after restart)
// Skip --enable_lsp processes (secondary/old LSP instances)
if (!line.includes('--enable_lsp') && !fallbackLine) {
fallbackLine = line;
logToFile(`[LS-FIX] found fallback LS (no workspace_id): PID=${line.split('|')[0]?.trim()}`);
}
} }
} }
if (!matchedLine) { if (!matchedLine) {
logToFile(`[LS-FIX] No LS process matched hint="${hint}" (${lines.length} processes)`); if (fallbackLine) {
return false; // v15: Use workspace_id-less LS as fallback (common after AG restart)
logToFile(`[LS-FIX] No workspace_id match — using fallback LS`);
matchedLine = fallbackLine;
} else {
logToFile(`[LS-FIX] No LS process matched hint="${hint}" (${lines.length} processes)`);
return false;
}
} }
// Extract port and csrf_token from matched line // Extract port and csrf_token from matched line
@@ -375,6 +388,28 @@ export async function activate(context: vscode.ExtensionContext) {
} }
// Normal approval — tryApprovalStrategies // Normal approval — tryApprovalStrategies
// v22: ALSO write response file so Observer's pollResponseGroup can click
// the correct button (with exact button_index). Without this, only the
// imprecise pollTriggerClick fallback was used for WS-path responses.
const responseDir = path.join(bridgePath, 'response');
if (!fs.existsSync(responseDir)) {
fs.mkdirSync(responseDir, { recursive: true });
}
const respPayload = {
request_id: data.request_id,
approved,
button_index: data.button_index ?? 0,
step_type: stepType,
project_name: projectName,
_from_ws: true,
};
fs.writeFileSync(
path.join(responseDir, `${data.request_id}.json`),
JSON.stringify(respPayload),
'utf-8'
);
logToFile(`[WS-RESPONSE] Response file written for pollResponseGroup: ${data.request_id?.substring(0, 12)}`);
const approvalCtx = getApprovalContext(); const approvalCtx = getApprovalContext();
logToFile(`[WS-RESPONSE] Triggering approval: approved=${approved} session=${approvalCtx.sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${approvalCtx.stepIndex}`); logToFile(`[WS-RESPONSE] Triggering approval: approved=${approved} session=${approvalCtx.sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${approvalCtx.stepIndex}`);
tryApprovalStrategies(approved, approvalCtx.sessionId, stepType, approvalCtx.stepIndex) tryApprovalStrategies(approved, approvalCtx.sessionId, stepType, approvalCtx.stepIndex)
@@ -486,6 +521,7 @@ export async function activate(context: vscode.ExtensionContext) {
get sessionStalled() { return getStepProbeContext().sessionStalled; }, get sessionStalled() { return getStepProbeContext().sessionStalled; },
get lastPendingStepIndex() { return getStepProbeContext().lastPendingStepIndex; }, get lastPendingStepIndex() { return getStepProbeContext().lastPendingStepIndex; },
writeChatSnapshot, writeChatSnapshot,
getLastWaitingCommand,
}; };
const bridgePort = await startHttpBridge(httpBridgeCtx, sdk); const bridgePort = await startHttpBridge(httpBridgeCtx, sdk);
let localPort = bridgePort; let localPort = bridgePort;

View File

@@ -55,6 +55,17 @@ export async function setupApprovalObserver(
if (patcher && typeof patcher.getScriptPath === 'function') { if (patcher && typeof patcher.getScriptPath === 'function') {
let baseScript = ''; let baseScript = '';
try { baseScript = integration.build(); } catch { baseScript = ''; } try { baseScript = integration.build(); } catch { baseScript = ''; }
// Strip old Gravity Bridge observer IIFE from baseScript.
// integration.build() caches the previous session's script, so without
// stripping, the old observer (e.g. v12) runs alongside the new one (v13),
// and the old one wins because it executes first.
if (baseScript.includes('Gravity Bridge v')) {
const oldVer = baseScript.match(/Gravity Bridge v(\d+)/);
const newVer = observerJS.match(/Gravity Bridge v(\d+)/);
logToFile(`[OBSERVER] baseScript contains old observer ${oldVer?.[0] || '?'}, new is ${newVer?.[0] || '?'} — stripping old`);
// Remove the old observer IIFE: starts with "// ── Gravity Bridge" comment, ends at the last "})();" before EOF or next section
baseScript = baseScript.replace(/\n?\/\/\s*[─═].*Gravity Bridge[\s\S]*$/, '');
}
const combinedScript = baseScript + '\n' + observerJS; const combinedScript = baseScript + '\n' + observerJS;
const scriptPath = patcher.getScriptPath(); const scriptPath = patcher.getScriptPath();
fs.writeFileSync(scriptPath, combinedScript, 'utf8'); fs.writeFileSync(scriptPath, combinedScript, 'utf8');
@@ -275,23 +286,36 @@ function _patchHtmlFiles(scriptDir: string, combinedScript: string, logToFile: (
logToFile(`[OBSERVER] removed external script tag from ${spec.name}`); logToFile(`[OBSERVER] removed external script tag from ${spec.name}`);
} }
// Insert or update inline script // Insert or update inline script — MUST be BEFORE </body> for Electron execution
const inlineMarkerStart = '<!-- AG SDK INLINE [variet-gravity-bridge] -->'; const inlineMarkerStart = '<!-- AG SDK INLINE [variet-gravity-bridge] -->';
const inlineMarkerEnd = '<!-- /AG SDK INLINE [variet-gravity-bridge] -->'; const inlineMarkerEnd = '<!-- /AG SDK INLINE [variet-gravity-bridge] -->';
const inlineBlock = `${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}`;
if (html.includes(inlineMarkerStart)) { if (html.includes(inlineMarkerStart)) {
// Remove existing block (may be in wrong position, e.g. after </body>)
const re = new RegExp( const re = new RegExp(
inlineMarkerStart.replace(/[[\]]/g, '\\$&') + '\\n?' + inlineMarkerStart.replace(/[[\]]/g, '\\$&') +
'[\\s\\S]*?' + '[\\s\\S]*?' +
inlineMarkerEnd.replace(/[[\]]/g, '\\$&') inlineMarkerEnd.replace(/[[\]]/g, '\\$&') + '\\n?'
); );
html = html.replace(re, html = html.replace(re, '');
`${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}`); // Remove duplicate </html> if present (from previous bad insertions)
logToFile(`[OBSERVER] ${spec.name} inline script UPDATED`); html = html.replace(/(<\/html>\s*){2,}/gi, '</html>\n');
logToFile(`[OBSERVER] ${spec.name} removed old inline script block`);
}
// Insert BEFORE </body> (not </html>) to ensure Electron executes it
// CRITICAL: Escape $ in inlineBlock to prevent String.replace() special patterns
// ($' = text after match, $& = matched text, etc.) which corrupt the JS code.
const safeInlineBlock = inlineBlock.replace(/\$/g, '$$$$');
if (html.includes('</body>')) {
html = html.replace('</body>',
`\n${safeInlineBlock}\n</body>`);
logToFile(`[OBSERVER] ${spec.name} inline script INSERTED before </body>`);
} else { } else {
// Fallback: insert before </html>
html = html.replace('</html>', html = html.replace('</html>',
`\n${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}\n</html>`); `\n${safeInlineBlock}\n</html>`);
logToFile(`[OBSERVER] ${spec.name} inline script INSERTED`); logToFile(`[OBSERVER] ${spec.name} inline script INSERTED before </html> (fallback)`);
} }
// SAFETY: Final validation before write // SAFETY: Final validation before write
if (html.length < 500 || !html.includes('<!DOCTYPE html>') || !html.includes(spec.requiredMarker)) { if (html.length < 500 || !html.includes('<!DOCTYPE html>') || !html.includes(spec.requiredMarker)) {

View File

@@ -26,6 +26,7 @@ export interface HttpBridgeContext {
lastPendingStepIndex: number; lastPendingStepIndex: number;
logToFile: (msg: string) => void; logToFile: (msg: string) => void;
writeChatSnapshot?: (text: string) => void; writeChatSnapshot?: (text: string) => void;
getLastWaitingCommand?: () => { cmd: string; desc: string; ts: number };
} }
// ─── Module-level state ─── // ─── Module-level state ───
@@ -79,6 +80,11 @@ export function startHttpBridge(ctx: HttpBridgeContext, sdk: any): Promise<numbe
const url = new URL(req.url, `http://127.0.0.1`); const url = new URL(req.url, `http://127.0.0.1`);
// DIAGNOSTIC: log ALL requests (except noisy polling endpoints)
if (!['/trigger-click', '/deep-inspect-trigger'].includes(url.pathname)) {
ctx.logToFile(`[HTTP-REQ] ${req.method} ${url.pathname}`);
}
// POST /pending — renderer reports a detected approval button // POST /pending — renderer reports a detected approval button
if (req.method === 'POST' && url.pathname === '/pending') { if (req.method === 'POST' && url.pathname === '/pending') {
_handlePending(req, res, ctx); _handlePending(req, res, ctx);
@@ -121,14 +127,33 @@ export function startHttpBridge(ctx: HttpBridgeContext, sdk: any): Promise<numbe
return; return;
} }
// POST /log — renderer relays important diagnostic logs
if (req.method === 'POST' && url.pathname === '/log') {
let logBody = '';
req.setEncoding('utf8');
req.on('data', (c: string) => logBody += c);
req.on('end', () => {
try {
const logData = JSON.parse(logBody);
ctx.logToFile(`[OBSERVER-LOG] ${logData.msg || logBody.substring(0, 500)}`);
} catch { ctx.logToFile(`[OBSERVER-LOG] ${logBody.substring(0, 500)}`); }
res.writeHead(200); res.end('ok');
});
return;
}
if (req.method === 'POST' && url.pathname === '/dump-html') { if (req.method === 'POST' && url.pathname === '/dump-html') {
let dumpBody = ''; let dumpBody = '';
req.setEncoding('utf8');
req.on('data', (c: string) => dumpBody += c); req.on('data', (c: string) => dumpBody += c);
req.on('end', () => { req.on('end', () => {
try { try {
const fs = require('fs'); // Save indexed dump for history + latest as dump_html.json
const path = require('path'); let idx = 1;
try { const parsed = JSON.parse(dumpBody); idx = parsed.dumpIndex || idx; } catch {}
fs.writeFileSync(path.join(ctx.bridgePath, `dump_html_${idx}.json`), dumpBody, 'utf-8');
fs.writeFileSync(path.join(ctx.bridgePath, 'dump_html.json'), dumpBody, 'utf-8'); fs.writeFileSync(path.join(ctx.bridgePath, 'dump_html.json'), dumpBody, 'utf-8');
ctx.logToFile(`[HTTP] DOM dump #${idx} saved (${dumpBody.length} bytes)`);
} catch (e) { } } catch (e) { }
res.writeHead(200); res.end('ok'); res.writeHead(200); res.end('ok');
}); });
@@ -137,6 +162,7 @@ export function startHttpBridge(ctx: HttpBridgeContext, sdk: any): Promise<numbe
if (req.method === 'POST' && url.pathname === '/test-rpc') { if (req.method === 'POST' && url.pathname === '/test-rpc') {
let rpcBody = ''; let rpcBody = '';
req.setEncoding('utf8');
req.on('data', (c: string) => rpcBody += c); req.on('data', (c: string) => rpcBody += c);
req.on('end', async () => { req.on('end', async () => {
try { try {
@@ -151,6 +177,25 @@ export function startHttpBridge(ctx: HttpBridgeContext, sdk: any): Promise<numbe
return; return;
} }
// GET /status — diagnostic endpoint
if (req.method === 'GET' && url.pathname === '/status') {
const { getStepProbeContext } = require('./step-probe');
const probeCtx = getStepProbeContext();
const status = {
projectName: ctx.projectName,
activeSessionId: probeCtx.activeSessionId || ctx.activeSessionId,
lastPendingStepIndex: probeCtx.lastPendingStepIndex,
sessionStalled: probeCtx.sessionStalled,
wsConnected: ctx.wsBridge?.isConnected() ?? false,
clickTrigger: clickTrigger ? { ...clickTrigger, ageMs: Date.now() - clickTrigger.timestamp } : null,
uptime: Math.round(process.uptime()),
timestamp: new Date().toISOString(),
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(status, null, 2));
return;
}
// GET /ping — health check // GET /ping — health check
if (url.pathname === '/ping') { if (url.pathname === '/ping') {
res.writeHead(200); res.end('pong'); res.writeHead(200); res.end('pong');
@@ -221,14 +266,210 @@ export function startHttpBridge(ctx: HttpBridgeContext, sdk: any): Promise<numbe
function _handlePending(req: any, res: any, ctx: HttpBridgeContext) { function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
let body = ''; let body = '';
req.setEncoding('utf8');
req.on('data', (c: string) => body += c); req.on('data', (c: string) => body += c);
req.on('end', () => { req.on('end', () => {
try { try {
const data = JSON.parse(body); const data = JSON.parse(body);
// ── Server-side false positive filter ── // ── v12: Command enrichment FIRST — extract actual command from description ──
const cmd = (data.command || '').trim(); // Must run before filters so "Always run" with useful description isn't filtered out
// Removed valid AI buttons (Accept, Reject, Allow, Deny) which are now structurally protected by the observer script const rawCmd = (data.command || '').trim();
// v15: Strip Material icon names from description BEFORE enrichment
// DOM textContent concatenates icon text (e.g. "content_copy") without separators
const ICON_STRIP_RE = /\b(chevron_right|chevron_left|arrow_drop_down|arrow_drop_up|arrow_right|arrow_left|arrow_forward|arrow_back|expand_more|expand_less|more_horiz|more_vert|content_copy|content_paste|check_circle|check|keyboard_arrow_up|keyboard_arrow_down|keyboard_arrow_left|keyboard_arrow_right|slow_motion_video|open_in_new|alternate_email)\b/g;
const rawDesc = (data.description || '').replace(ICON_STRIP_RE, '').replace(/\s{2,}/g, ' ').trim();
const GENERIC_BTN_RE = /^(?:Always\s*)?(?:Run|Allow|Accept|Approve)$/i;
let enrichedCmd = rawCmd;
let enrichedDesc = rawDesc;
// v19: "Always run" auto-approve — MUST run BEFORE any filter can reject it
// Detects from rawCmd OR from buttons array (Observer may detect sibling first)
// v33: Also auto-approve "Accept all" (diff review) and "Accept" buttons
const AUTO_APPROVE_RE = /^(Always\s+run|Accept\s+all|Accept)$/i;
let alwaysRunDetected = AUTO_APPROVE_RE.test(rawCmd);
let alwaysRunBtnIndex = alwaysRunDetected ? 0 : -1;
if (!alwaysRunDetected && Array.isArray(data.buttons)) {
for (let bi = 0; bi < data.buttons.length; bi++) {
if (AUTO_APPROVE_RE.test((data.buttons[bi].text || '').trim())) {
alwaysRunDetected = true;
alwaysRunBtnIndex = bi;
break;
}
}
}
if (alwaysRunDetected) {
// v34: If this is "Accept all" / "Accept", also call agentAcceptAllInFile directly
const isAcceptAll = /^Accept/i.test(rawCmd) || (data.step_type === 'diff_review');
if (isAcceptAll) {
ctx.logToFile(`[HTTP] AUTO-APPROVE Accept all → opening review + agentAcceptAllInFile`);
try {
const vscode = require('vscode');
// v37: Must focus diff review panel BEFORE calling agentAcceptAllInFile
// Without this, the command succeeds but has no effect
(async () => {
try {
await vscode.commands.executeCommand('antigravity.openReviewChanges');
ctx.logToFile(`[HTTP] openReviewChanges OK`);
await new Promise((r: any) => setTimeout(r, 500));
await vscode.commands.executeCommand('antigravity.prioritized.agentAcceptAllInFile');
ctx.logToFile(`[HTTP] ✅ agentAcceptAllInFile SUCCESS`);
} catch (e: any) {
ctx.logToFile(`[HTTP] ❌ agentAcceptAllInFile: ${e.message?.substring(0, 100)}`);
}
})();
} catch (e: any) { ctx.logToFile(`[HTTP] ❌ vscode require failed: ${e.message}`); }
}
// Try enrichment for better Discord display text
let displayCmd = rawCmd;
ctx.logToFile(`[HTTP] AUTO-APPROVE raw: cmd="${rawCmd}" desc="${rawDesc.substring(0, 120)}" buttons=${JSON.stringify((data.buttons || []).map((b: any) => b.text)).substring(0, 200)}`);
if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 3 && rawDesc !== rawCmd) {
const promptMatch = rawDesc.match(/[>»]\s*(.+)/);
if (promptMatch && promptMatch[1].trim().length > 3) {
displayCmd = promptMatch[1].trim().substring(0, 200);
} else {
// v19: Fallback — use longest line from description as command text
const descLines = rawDesc.split(/\n/).map((l: string) => l.trim()).filter((l: string) => l.length > 3);
if (descLines.length > 0) {
descLines.sort((a: string, b: string) => b.length - a.length);
displayCmd = descLines[0].substring(0, 200);
}
}
}
// v20: Final fallback — read latest Step Probe pending file for actual command
if (displayCmd === rawCmd && GENERIC_BTN_RE.test(displayCmd)) {
try {
const pendingDir = path.join(ctx.bridgePath, 'pending');
if (fs.existsSync(pendingDir)) {
const pFiles = fs.readdirSync(pendingDir)
.filter((f: string) => f.endsWith('.json'))
.map((f: string) => ({ name: f, time: fs.statSync(path.join(pendingDir, f)).mtimeMs }))
.sort((a: any, b: any) => b.time - a.time);
if (pFiles.length > 0 && (Date.now() - pFiles[0].time) < 30_000) {
const pData = JSON.parse(fs.readFileSync(path.join(pendingDir, pFiles[0].name), 'utf-8'));
if (pData.command && pData.command.length > 3 && !GENERIC_BTN_RE.test(pData.command)) {
displayCmd = pData.command.substring(0, 200);
ctx.logToFile(`[HTTP] AUTO-APPROVE enriched from pending file: "${displayCmd.substring(0, 80)}"`);
} else if (pData.description && pData.description.length > 5) {
displayCmd = pData.description.substring(0, 200);
ctx.logToFile(`[HTTP] AUTO-APPROVE enriched from pending desc: "${displayCmd.substring(0, 80)}"`);
}
}
}
} catch (e: any) { ctx.logToFile(`[HTTP] AUTO-APPROVE pending lookup error: ${e.message}`); }
}
// v29: Final-final fallback — Step Probe API memory
if (displayCmd === rawCmd && GENERIC_BTN_RE.test(displayCmd) && ctx.getLastWaitingCommand) {
const wc = ctx.getLastWaitingCommand();
if (wc.cmd && wc.cmd.length > 3 && !GENERIC_BTN_RE.test(wc.cmd) && (Date.now() - wc.ts) < 30_000) {
displayCmd = wc.desc && wc.desc.length > wc.cmd.length ? wc.desc.substring(0, 200) : wc.cmd.substring(0, 200);
ctx.logToFile(`[HTTP] AUTO-APPROVE enriched from step-probe memory: "${displayCmd.substring(0, 80)}"`);
}
}
const rid = data.request_id || Date.now().toString();
ctx.logToFile(`[HTTP] AUTO-APPROVE "Always run" (btnIdx=${alwaysRunBtnIndex}): cmd="${displayCmd.substring(0, 80)}"`);
// Write response file IMMEDIATELY so observer clicks the button with zero delay
const responseDir = path.join(ctx.bridgePath, 'response');
if (!fs.existsSync(responseDir)) {
fs.mkdirSync(responseDir, { recursive: true });
}
const respPayload = {
request_id: rid,
approved: true,
button_index: alwaysRunBtnIndex >= 0 ? alwaysRunBtnIndex : 0,
step_type: data.step_type || 'command',
project_name: ctx.projectName,
_from_ws: true, // v38: prevent processResponseFile from consuming before Observer polls
_auto_approve_ttl: Date.now() + 60_000, // auto-expire after 60s
};
fs.writeFileSync(
path.join(responseDir, `${rid}.json`),
JSON.stringify(respPayload),
'utf-8'
);
// v29: Respond to Observer immediately (don't block button click)
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, request_id: rid, auto_approved: true }));
// v29: Discord notification — if displayCmd is generic, poll Step Probe for real command
const isGenericDisplay = GENERIC_BTN_RE.test(displayCmd);
const sendDiscord = (finalCmd: string, finalDesc: string) => {
if (ctx.wsBridge && ctx.wsBridge.isConnected()) {
ctx.wsBridge.sendPending({
request_id: rid,
command: finalCmd,
description: finalDesc,
step_type: data.step_type || 'command',
status: 'auto_approved',
buttons: data.buttons,
project_name: ctx.projectName,
});
}
};
if (isGenericDisplay && ctx.getLastWaitingCommand) {
// Poll Step Probe memory for up to 6s (API polls every 5s)
let pollAttempt = 0;
const maxAttempts = 30; // 30 * 200ms = 6s
const pollTimer = setInterval(() => {
pollAttempt++;
const wc = ctx.getLastWaitingCommand!();
if (wc.cmd && wc.cmd.length > 3 && !GENERIC_BTN_RE.test(wc.cmd) && (Date.now() - wc.ts) < 15_000) {
clearInterval(pollTimer);
const enrichedCmd = wc.desc && wc.desc.length > wc.cmd.length ? wc.desc.substring(0, 200) : wc.cmd.substring(0, 200);
ctx.logToFile(`[HTTP] AUTO-APPROVE enriched (delayed ${pollAttempt * 200}ms): "${enrichedCmd.substring(0, 80)}"`);
sendDiscord(enrichedCmd, `[${rawCmd}] ${enrichedCmd}`);
} else if (pollAttempt >= maxAttempts) {
clearInterval(pollTimer);
ctx.logToFile(`[HTTP] AUTO-APPROVE no enrichment after ${maxAttempts * 200}ms — sending generic`);
sendDiscord(displayCmd, rawDesc ? `[${rawCmd}] ${rawDesc}` : rawCmd);
}
}, 200);
} else {
// Already enriched or no Step Probe — send immediately
sendDiscord(displayCmd, rawDesc ? `[${rawCmd}] ${rawDesc}` : rawCmd);
}
return;
}
if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 10 && rawDesc !== rawCmd) {
// Extract the actual command from description (often includes terminal prompt)
// Pattern: "…\project_name > actual_command"
const promptMatch = rawDesc.match(/[>»]\s*(.+)/);
if (promptMatch && promptMatch[1].trim().length > 3) {
const extracted = promptMatch[1].trim();
// v16: Validate extracted text is not just a prompt fragment or path
const PROMPT_ONLY_RE = /^.*[>»$#]\s*$/;
const TERMINAL_PROMPT_RE = /^[^\n]*\\[^\\>]+\s*[>»]\s*$/;
if (!PROMPT_ONLY_RE.test(extracted) && !TERMINAL_PROMPT_RE.test(extracted)) {
enrichedCmd = extracted.substring(0, 200);
enrichedDesc = `[${rawCmd}] ${rawDesc}`;
ctx.logToFile(`[HTTP] command enriched: "${rawCmd}" → "${enrichedCmd.substring(0, 60)}"`);
} else {
// Prompt-only extraction — filter
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 {
// v16: No prompt marker (> » $ #) found in description — this is terminal OUTPUT, not a command
// Observer extracted stdout text from code block (e.g. "No extension.log found", "Log found: ...")
ctx.logToFile(`[HTTP] filtered terminal output (no prompt marker): "${rawDesc.substring(0, 60)}"`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: false, filtered: true, reason: 'terminal_output' }));
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) ──
const cmd = enrichedCmd;
const FALSE_POSITIVE_RE = /^(Proceed|Continue|Open|Close|OK|Yes|No|Save|Undo|Redo|Back|Next|More|Less|Got it|Dismiss)$/i; const FALSE_POSITIVE_RE = /^(Proceed|Continue|Open|Close|OK|Yes|No|Save|Undo|Redo|Back|Next|More|Less|Got it|Dismiss)$/i;
if (FALSE_POSITIVE_RE.test(cmd)) { if (FALSE_POSITIVE_RE.test(cmd)) {
ctx.logToFile(`[HTTP] filtered false positive: "${cmd}"`); ctx.logToFile(`[HTTP] filtered false positive: "${cmd}"`);
@@ -236,31 +477,59 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
res.end(JSON.stringify({ ok: false, filtered: true })); res.end(JSON.stringify({ ok: false, filtered: true }));
return; return;
} }
// v14: Server-side junk content filter — CSS, source code, icon glue
// This is the last line of defense regardless of observer version
const JUNK_CONTENT_RE = /(!important|::selection|background-color:|var\(--|font-size:|border-[a-z]+:|padding:|margin:|display:\s|===|!==|\|\||\.\btest\(|\.\bmatch\(|\.\breplace\(|_RE[.\s]|\brawDesc\b|\brawCmd\b|\benrichedCmd\b|\bquerySelector\b|\.code-block|\.code-line|\.line-content|\{\s*--|integration\.build)/;
// v15: ICON_GLUE_RE now also catches standalone icon names (no trailing [a-zA-Z] required)
const ICON_GLUE_RE = /\b(alternate_email|content_copy|content_paste|check_circle|chevron_right|chevron_left|keyboard_arrow|arrow_drop_down|arrow_drop_up|more_horiz|more_vert|expand_more|expand_less)\b/;
// v15: Terminal prompt pattern — catches bare prompts like "…\project >" or "PS C:\path>"
const BARE_PROMPT_RE = /^[^\n]{0,60}[>»$#]\s*$/;
if (JUNK_CONTENT_RE.test(cmd) || ICON_GLUE_RE.test(cmd)) {
ctx.logToFile(`[HTTP] filtered junk content: "${cmd.substring(0, 80)}"`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: false, filtered: true, reason: 'junk_content' }));
return;
}
// v15: Final bare prompt filter — catches any enriched cmd that's just a terminal prompt
if (BARE_PROMPT_RE.test(cmd) && cmd.length < 80) {
ctx.logToFile(`[HTTP] filtered bare prompt: "${cmd.substring(0, 80)}"`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: false, filtered: true, reason: 'bare_prompt' }));
return;
}
// "Run" button → step_probe handles these with full command detail // "Run" button → step_probe handles these with full command detail
// Only let through if session is stalled AND step_probe hasn't created a pending yet // Only filter when step_probe IS actively tracking AND cmd is still generic button text
if (/^(?:Always\s*)?Run\b/i.test(cmd)) { if (/^(?:Always\s*)?Run\b/i.test(cmd)) {
if (!ctx.sessionStalled || ctx.lastPendingStepIndex >= 0) { if (ctx.activeSessionId && (!ctx.sessionStalled || ctx.lastPendingStepIndex >= 0)) {
ctx.logToFile(`[HTTP] filtered "Run" — ${!ctx.sessionStalled ? 'not stalled' : 'step_probe pending exists'}`); ctx.logToFile(`[HTTP] filtered "Run" — ${!ctx.sessionStalled ? 'not stalled' : 'step_probe pending exists'} (session=${ctx.activeSessionId.substring(0, 8)})`);
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: false, filtered: true })); res.end(JSON.stringify({ ok: false, filtered: true }));
return; return;
} }
// v9: When step_probe has no active session, let DOM observer handle approval
ctx.logToFile(`[HTTP] allowing "Run" — step_probe has no active session`);
} }
const rid = data.request_id || Date.now().toString(); const rid = data.request_id || Date.now().toString();
const pending: Record<string, any> = { const pending: Record<string, any> = {
...data, ...data,
request_id: rid, request_id: rid,
command: enrichedCmd,
description: enrichedDesc,
conversation_id: ctx.activeSessionId || '', conversation_id: ctx.activeSessionId || '',
timestamp: Date.now() / 1000, timestamp: Date.now() / 1000,
status: 'pending', status: 'pending',
project_name: ctx.projectName, project_name: ctx.projectName,
auto_detected: true, auto_detected: true,
source: 'dom_observer', source: 'dom_observer',
step_type: data.step_type,
buttons: data.buttons,
step_index: ctx.lastPendingStepIndex >= 0 ? ctx.lastPendingStepIndex : undefined, step_index: ctx.lastPendingStepIndex >= 0 ? ctx.lastPendingStepIndex : undefined,
}; };
// v19: "Always run" auto-approve was already handled above (before filter chain)
// No need for duplicate check here.
// File permission: inject multi-choice buttons // File permission: inject multi-choice buttons
const cmdLower = (data.command || '').toLowerCase(); const cmdLower = enrichedCmd.toLowerCase();
if (cmdLower.includes('allow') && !pending.buttons) { if (cmdLower.includes('allow') && !pending.buttons) {
// Dedup: skip if another file_permission pending was created within 10s // Dedup: skip if another file_permission pending was created within 10s
const nowMs = Date.now(); const nowMs = Date.now();
@@ -279,8 +548,8 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
]; ];
pending.step_type = 'file_permission'; pending.step_type = 'file_permission';
// Clean description: remove button labels from text // Clean description: remove button labels from text
const rawDesc = (data.description || data.command || '').replace(/Deny|Allow Once|Allow This Conversation/gi, '').trim(); const cleanDesc = enrichedDesc.replace(/Deny|Allow Once|Allow This Conversation/gi, '').trim();
pending.command = `파일 접근 권한${rawDesc ? ': ' + rawDesc : ''}`; pending.command = `파일 접근 권한${cleanDesc ? ': ' + cleanDesc : ''}`;
} }
// WS dispatch // WS dispatch
if (ctx.wsBridge && ctx.wsBridge.isConnected()) { if (ctx.wsBridge && ctx.wsBridge.isConnected()) {
@@ -295,7 +564,11 @@ 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="${pending.command || data.command}" btns=${(pending.buttons || data.buttons || []).length} ctx="${(pending.description || data.description || '').substring(0, 80)}"`);
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) {
@@ -371,6 +644,7 @@ function _handleDeepInspectTrigger(res: any) {
function _handleDeepInspectResult(req: any, res: any, ctx: HttpBridgeContext) { function _handleDeepInspectResult(req: any, res: any, ctx: HttpBridgeContext) {
let body = ''; let body = '';
req.setEncoding('utf8');
req.on('data', (c: string) => body += c); req.on('data', (c: string) => body += c);
req.on('end', () => { req.on('end', () => {
try { try {
@@ -393,13 +667,16 @@ function _handleDeepInspectResult(req: any, res: any, ctx: HttpBridgeContext) {
function _handleChatSnapshot(req: any, res: any, ctx: HttpBridgeContext) { function _handleChatSnapshot(req: any, res: any, ctx: HttpBridgeContext) {
let body = ''; let body = '';
req.setEncoding('utf8');
req.on('data', (c: string) => body += c); req.on('data', (c: string) => body += c);
req.on('end', () => { req.on('end', () => {
try { try {
const data = JSON.parse(body); const data = JSON.parse(body);
if (data.text && typeof ctx.writeChatSnapshot === 'function') { if (data.text && typeof ctx.writeChatSnapshot === 'function') {
ctx.writeChatSnapshot(`💬 **[DOM 추출] AI 응답**\n\n${data.text}`); const isUser = data.role === 'user';
ctx.logToFile(`[HTTP] chat snapshot written (${data.text.length} chars)`); const prefix = isUser ? '🧑‍💻 **[DOM 추출] 사용자 요청**' : '💬 **[DOM 추출] AI 응답**';
ctx.writeChatSnapshot(`${prefix}\n\n${data.text}`);
ctx.logToFile(`[HTTP] chat snapshot written (${data.text.length} chars, role: ${data.role || 'bot'})`);
} }
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true })); res.end(JSON.stringify({ ok: true }));

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,10 @@ let responseWatcher: fs.FSWatcher | null = null;
let brainWatcher: BrainWatcher | null = null; let brainWatcher: BrainWatcher | null = null;
let activeTrajectoryId = ''; let activeTrajectoryId = '';
const recentPendingSteps = new Map<string, number>(); const recentPendingSteps = new Map<string, number>();
const PENDING_MEMORY_TTL_MS = 60_000; const PENDING_MEMORY_TTL_MS = 30_000;
// v29: Last WAITING command from API — used by http-bridge for Always run enrichment
let lastWaitingCommand = { cmd: '', desc: '', ts: 0 };
// generateApprovalObserverScript → extracted to ./observer-script.ts // generateApprovalObserverScript → extracted to ./observer-script.ts
const lastSnapshotText = new Map<string, string>(); const lastSnapshotText = new Map<string, string>();
@@ -79,6 +82,14 @@ export function getStepProbeContext(): { activeSessionId: string; sessionStalled
}; };
} }
/**
* v29: Get last WAITING command from Step Probe API.
* Used by http-bridge as fallback when Observer's extractContext returns generic "Always run".
*/
export function getLastWaitingCommand(): { cmd: string; desc: string; ts: number } {
return { ...lastWaitingCommand };
}
/** /**
* Reset pending state after successful approval. * Reset pending state after successful approval.
* Called after WS response triggers approval in extension.ts. * Called after WS response triggers approval in extension.ts.
@@ -203,6 +214,7 @@ function setupMonitor() {
let pendingModifiedFilePaths: string[] = []; // full paths for diff review let pendingModifiedFilePaths: string[] = []; // full paths for diff review
let pendingEditStepIndices: number[] = []; // step indices for AcknowledgeCascadeCodeEdit let pendingEditStepIndices: number[] = []; // step indices for AcknowledgeCascadeCodeEdit
let lastResponseCaptureStep = -1; // dedup: don't capture same response twice let lastResponseCaptureStep = -1; // dedup: don't capture same response twice
let lastLSFixPoll = 0; // v15: track last fixLSConnection() attempt for periodic retry
setInterval(async () => { setInterval(async () => {
pollCount++; pollCount++;
@@ -303,6 +315,34 @@ function setupMonitor() {
return; return;
} }
// ── v15: Stale LS detection — periodic fixLSConnection() ──
// If the best session's lastModifiedTime hasn't changed for >5min,
// the extension might be connected to a stale LS. Retry fixLSConnection().
if (ctx.fixLSConnection && (pollCount - lastLSFixPoll) >= 60) {
// Check if current best session has stale data
const bestEntries = Object.entries(allTraj.trajectorySummaries) as [string, any][];
const hasStaleData = bestEntries.every(([, data]) => {
const mod = data.lastModifiedTime || '';
if (!mod) return true;
const modDate = new Date(mod);
return (Date.now() - modDate.getTime()) > 300_000; // 5 min
});
if (hasStaleData) {
lastLSFixPoll = pollCount;
ctx.logToFile(`[LS-AUTO-FIX] All sessions stale (>5min) — attempting fixLSConnection()`);
try {
const fixed = await ctx.fixLSConnection();
if (fixed) {
ctx.logToFile(`[LS-AUTO-FIX] ✅ Reconnected to new LS — next poll should have fresh data`);
} else {
ctx.logToFile(`[LS-AUTO-FIX] No better LS found`);
}
} catch (e: any) {
ctx.logToFile(`[LS-AUTO-FIX] error: ${e.message?.substring(0, 100)}`);
}
}
}
// ── Filter to sessions owned by THIS window ── // ── Filter to sessions owned by THIS window ──
// PRIMARY: Use trajectoryMetadata.workspaces URI to match sessions to this workspace. // PRIMARY: Use trajectoryMetadata.workspaces URI to match sessions to this workspace.
// FALLBACK: Use bridge/register/ files for sessions without metadata. // FALLBACK: Use bridge/register/ files for sessions without metadata.
@@ -422,13 +462,86 @@ function setupMonitor() {
const delta = currentCount - lastKnownStepCount; const delta = currentCount - lastKnownStepCount;
lastKnownStepCount = currentCount; lastKnownStepCount = currentCount;
if (delta > 0) { // ── v15: Heartbeat probe — detect step changes when summary API is stale ──
// GetAllCascadeTrajectories can return frozen stepCount/lastModifiedTime,
// v20: Heartbeat every 3 polls (~15s) — AG API never reports delta for active sessions
if (delta === 0 && ctx.sdk && ctx.activeSessionId && pollCount % 3 === 0) {
try {
const hbOffset = Math.max(0, currentCount - 5);
const hbResp = await ctx.sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
cascadeId: ctx.activeSessionId,
stepOffset: hbOffset,
verbosity: 1, // need content for capture
});
if (hbResp?.steps?.length > 0) {
const realStepCount = hbOffset + hbResp.steps.length;
if (realStepCount > lastKnownStepCount) {
ctx.logToFile(`[HEARTBEAT] stale! reported=${lastKnownStepCount} real=${realStepCount}`);
// Process new steps for RT-CAPTURE
for (let hi = 0; hi < hbResp.steps.length; hi++) {
const hs = hbResp.steps[hi];
const hIdx = hbOffset + hi;
if (hIdx <= lastResponseCaptureStep) continue;
const hType = hs?.type || '';
// Capture AI responses
if (hType.includes('PLANNER_RESPONSE') && hs?.status?.includes('DONE')) {
let text = extractPlannerText(hs) || '';
if (text.length > 10) {
lastResponseCaptureStep = hIdx;
ctx.logToFile(`[HB-CAPTURE] AI step=${hIdx} (${text.length} chars)`);
const truncated = text.length > 3500
? text.substring(0, 3500) + '\n\n_(이하 생략)_'
: text;
ctx.writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
}
}
// Capture user messages
if (hType.includes('USER_INPUT') && hIdx > lastUserInputStepIdx) {
lastUserInputStepIdx = hIdx;
const ui = hs?.userInput;
const umText = (ui?.userResponse || ui?.text || '').trim();
if (umText.length > 2) {
const sentAt = ctx.recentDiscordSentTexts.get(umText);
if (!sentAt || (Date.now() - sentAt) > 60_000) {
const dedupKey = `user_msg:${umText}`;
const lastRelayed = lastSnapshotText.get(dedupKey);
if (!lastRelayed || (Date.now() - Number(lastRelayed)) > 30_000) {
lastSnapshotText.set(dedupKey, String(Date.now()));
const clientType = ui?.clientType || '';
const source = clientType.includes('IDE') ? 'AG 직접 입력' : 'API';
const truncated = umText.length > 800
? umText.substring(0, 800) + '\n\n_(이하 생략)_'
: umText;
ctx.writeChatSnapshot(`👤 **사용자 (${source})**\n\n${truncated}`);
ctx.logToFile(`[HB-CAPTURE] User step=${hIdx} (${umText.length} chars)`);
}
}
}
}
}
lastKnownStepCount = realStepCount;
} else if (pollCount % 30 === 0) {
ctx.logToFile(`[HEARTBEAT] ok offset=${hbOffset} got=${hbResp.steps.length} real=${realStepCount} known=${lastKnownStepCount}`);
}
} else if (pollCount % 30 === 0) {
ctx.logToFile(`[HEARTBEAT] no steps returned for offset=${hbOffset}`);
}
} catch (hbErr: any) {
if (pollCount % 10 === 0) ctx.logToFile(`[HEARTBEAT] probe error: ${hbErr.message?.substring(0, 100)}`);
}
}
// Recalculate delta after heartbeat correction
const effectiveDelta = lastKnownStepCount - (currentCount > 0 ? currentCount : lastKnownStepCount);
const hasDelta = delta > 0 || effectiveDelta > 0;
if (hasDelta) {
console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`); console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`);
// Real-time response capture: fetch latest steps on every delta>0 // Real-time response capture: fetch latest steps on every delta>0 or heartbeat
if (currentCount > lastResponseCaptureStep && ctx.sdk) { if (lastKnownStepCount > lastResponseCaptureStep && ctx.sdk) {
try { try {
const rtOffset = Math.max(0, currentCount - 3); const rtOffset = Math.max(0, lastKnownStepCount - 3);
const rtResp = await ctx.sdk.ls.rawRPC('GetCascadeTrajectorySteps', { const rtResp = await ctx.sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
cascadeId: bestSessionId, cascadeId: bestSessionId,
stepOffset: rtOffset, stepOffset: rtOffset,
@@ -468,12 +581,46 @@ function setupMonitor() {
} }
} }
// v20: Capture USER_INPUT steps for user message relay
if (sType.includes('USER_INPUT') && actualIdx > lastUserInputStepIdx) {
lastUserInputStepIdx = actualIdx;
const ui = s?.userInput;
const umText = (ui?.userResponse || ui?.text || s?.plannerResponse?.textContent || '').trim();
const clientType = ui?.clientType || '';
const isFromIDE = clientType.includes('IDE');
ctx.logToFile(`[RT-USER-MSG] step=${actualIdx} client=${clientType} text=${umText.substring(0, 100)}`);
if (umText.length > 2) {
// Skip echo: if text was recently sent from Discord
const sentAt = ctx.recentDiscordSentTexts.get(umText);
if (sentAt && (Date.now() - sentAt) < 60_000) {
ctx.recentDiscordSentTexts.delete(umText);
ctx.logToFile(`[RT-USER-MSG] skipped echo relay (Discord origin)`);
} else {
// Content-based dedup
const dedupKey = `user_msg:${umText}`;
const lastRelayed = lastSnapshotText.get(dedupKey);
if (!lastRelayed || (Date.now() - Number(lastRelayed)) > 30_000) {
lastSnapshotText.set(dedupKey, String(Date.now()));
const truncated = umText.length > 800
? umText.substring(0, 800) + '\n\n_(이하 생략)_'
: umText;
const source = isFromIDE ? 'AG 직접 입력' : 'API';
ctx.writeChatSnapshot(`👤 **사용자 (${source})**\n\n${truncated}`);
ctx.logToFile(`[RT-USER-MSG] relayed ${umText.length} chars`);
}
}
}
}
if (s?.status === 'CORTEX_STEP_STATUS_WAITING') { if (s?.status === 'CORTEX_STEP_STATUS_WAITING') {
const toolCall = s?.metadata?.toolCall; const toolCall = s?.metadata?.toolCall;
const toolName = toolCall?.name || (sType || '').replace('CORTEX_STEP_TYPE_', '').toLowerCase(); const toolName = toolCall?.name || (sType || '').replace('CORTEX_STEP_TYPE_', '').toLowerCase();
const { cmd: command, desc: description, isSafe: isSafeToAutoRun } = formatStepProbeCommand(toolName, actualIdx, sType || '', toolCall); const { cmd: command, desc: description, isSafe: isSafeToAutoRun } = formatStepProbeCommand(toolName, actualIdx, sType || '', toolCall);
ctx.logToFile(`[STEP-PROBE] ★ WAITING (RT)! step=${actualIdx} type=${sType} cmd='${command}'`); ctx.logToFile(`[STEP-PROBE] ★ WAITING (RT)! step=${actualIdx} type=${sType} cmd='${command}'`);
// v29: Save for http-bridge enrichment
lastWaitingCommand = { cmd: command, desc: description, ts: Date.now() };
if (actualIdx !== ctx.lastPendingStepIndex) { if (actualIdx !== ctx.lastPendingStepIndex) {
ctx.stallProbed = true; ctx.stallProbed = true;
@@ -589,6 +736,7 @@ function setupMonitor() {
// lastModifiedTime is still changing = AI is thinking, NOT approval // lastModifiedTime is still changing = AI is thinking, NOT approval
consecutiveIdleCount = 0; // Reset! consecutiveIdleCount = 0; // Reset!
ctx.stallProbed = false; ctx.stallProbed = false;
ctx.sessionStalled = false; // FIX: also reset stalled flag on modTime change
if (pollCount <= 10 || pollCount % 12 === 0) { if (pollCount <= 10 || pollCount % 12 === 0) {
ctx.logToFile(`[THINK] step=${currentCount} modTime changing → not stall`); ctx.logToFile(`[THINK] step=${currentCount} modTime changing → not stall`);
} }
@@ -660,6 +808,19 @@ function setupMonitor() {
source: 'step_probe_offset', source: 'step_probe_offset',
safe_to_auto_run: isSafeToAutoRun, safe_to_auto_run: isSafeToAutoRun,
}); });
// v35: Auto-accept code edits (offset path)
if (['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName)) {
ctx.logToFile(`[STEP-PROBE] v35: code_edit (offset) → auto-accepting in 500ms`);
setTimeout(async () => {
try {
const vscode = require('vscode');
await vscode.commands.executeCommand('antigravity.prioritized.agentAcceptAllInFile');
ctx.logToFile(`[STEP-PROBE] ✅ agentAcceptAllInFile (offset) SUCCESS`);
} catch (e: any) {
ctx.logToFile(`[STEP-PROBE] ❌ agentAcceptAllInFile (offset): ${e.message?.substring(0, 100)}`);
}
}, 500);
}
} }
} }
// NOTE: no break — process ALL parallel WAITING steps // NOTE: no break — process ALL parallel WAITING steps
@@ -713,6 +874,20 @@ function setupMonitor() {
source: 'step_probe', source: 'step_probe',
safe_to_auto_run: isSafeToAutoRun, safe_to_auto_run: isSafeToAutoRun,
}); });
// v35: Auto-accept code edits via agentAcceptAllInFile
// Observer can't see "Accept all" button (different DOM layer)
if (['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName)) {
ctx.logToFile(`[STEP-PROBE] v35: code_edit detected → auto-accepting in 500ms`);
setTimeout(async () => {
try {
const vscode = require('vscode');
await vscode.commands.executeCommand('antigravity.prioritized.agentAcceptAllInFile');
ctx.logToFile(`[STEP-PROBE] ✅ agentAcceptAllInFile SUCCESS`);
} catch (e: any) {
ctx.logToFile(`[STEP-PROBE] ❌ agentAcceptAllInFile: ${e.message?.substring(0, 100)}`);
}
}, 500);
}
} }
} }
// NOTE: no break — process ALL parallel WAITING steps // NOTE: no break — process ALL parallel WAITING steps

View File

@@ -1,8 +0,0 @@
import json; d=json.load(open(r'C:\Users\Variet-Worker\.gemini\antigravity\bridge\deep-inspect-result.json', encoding='utf-8', errors='ignore')); print('Total Nodes:', len(d.get('nodes',[])));
for n in d.get('nodes', []):
if 'agent' in n.get('label','').lower() or n.get('buttons'):
print(f"\n[Node] {n.get('label')}")
for b in n.get('buttons', []):
print(f" BTN: '{b.get('text')}' class='{b.get('class')}' hidden={b.get('hidden')} disabled={b.get('disabled')}")
for b in n.get('roleBtns', []):
print(f" ROLE-BTN: '{b.get('text')}'")

View File

@@ -1,6 +0,0 @@
import json, re; d=json.load(open(r'C:\Users\Variet-Worker\.gemini\antigravity\bridge\deep-inspect-result.json', encoding='utf-8', errors='ignore'))
approveRe=[re.compile(r'^(?:Always\s*)?Run\b', re.IGNORECASE), re.compile(r'^(?:Always\s*)?Accept\b', re.IGNORECASE)]
for b in d['nodes'][0]['buttons']:
t = b['text'].strip()
t = re.sub(r'(?:keyboard_arrow_up|keyboard_arrow_down)$', '', t, flags=re.IGNORECASE).strip()
if any(p.match(t) for p in approveRe): print(f"MATCH: {b['text']} -> {t}")

View File

@@ -1,36 +0,0 @@
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')

View File

@@ -1,38 +0,0 @@
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}")

View File

@@ -1,36 +0,0 @@
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)

View File

@@ -1,38 +0,0 @@
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]}")

View File

@@ -1,29 +0,0 @@
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]}")

View File

@@ -1,16 +0,0 @@
import asyncio
import json
from mcp_client import MCPClient
async def main():
client = MCPClient()
await client.connect()
try:
# Get raw API response
resp = await client.request("EvaluateCascadeLspMethods", {"method": "GetDiagnostics", "params": "{}"})
print(json.dumps(resp, indent=2))
finally:
await client.close()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,20 +0,0 @@
const path = require('path');
const fs = require('fs');
async function testDiag() {
const bridgePath = process.cwd();
// we want to list latest brainDir and check state summary instead.
const brainDir = path.resolve(bridgePath, '.gemini/antigravity/brain');
if (fs.existsSync(brainDir)) {
const brainDirs = fs.readdirSync(brainDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory() && dirent.name.length === 36)
.map(dirent => {
const stats = fs.statSync(path.join(brainDir, dirent.name));
return { name: dirent.name, time: stats.mtimeMs };
})
.sort((a, b) => b.time - a.time);
console.log(`Latest brain UUIDs:`, brainDirs.slice(0, 3));
}
}
testDiag();

View File

@@ -1,5 +0,0 @@
const fs = require('fs');
const readline = require('readline');
// Let's parse extension.log to find the steps! No wait, let's just make a script that uses rawRPC.
// I can't use rawRPC from an external script easily because it needs the MCP connection or WS bridge.
// Wait! The bot has a bridge!

View File

@@ -1,35 +0,0 @@
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();

View File

@@ -1,58 +0,0 @@
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();

View File

@@ -1,29 +0,0 @@
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();

View File

@@ -1,50 +0,0 @@
import json
try:
with open(r'C:\Users\Variet-Worker\.gemini\antigravity\bridge\deep-inspect-result.json', 'r', encoding='utf-8', errors='ignore') as f:
data = json.load(f)
# Print first 50 buttons from nodes
count = 0
print('=== First 100 Buttons in DOM Nodes ===')
for node in data.get('nodes', []):
label = node.get('label', 'unknown')
btns = node.get('buttons', [])
if btns:
print(f'\n[Node] {label}')
for b in btns:
t = b.get('text', '').replace('\n', ' ').strip()
hidden = b.get("hidden")
cls = b.get("class")
if t:
print(f' - "{t[:50]}" (Hidden: {hidden}, Class: {cls[:30]})')
count += 1
if count > 100:
break
if count > 100:
break
# Print first 50 buttons from webviews
count = 0
print('\n=== First 100 Buttons in Webviews ===')
for probe in data.get('webviewProbes', []):
if probe.get('success'):
pd = probe.get('data', {})
btns = pd.get('buttons', [])
label = pd.get('title', 'Unknown Title') + f" (URL: {pd.get('url', 'Unknown URL')})"
if btns:
print(f'\n[WebviewProbe {probe.get("index")}] {label}')
for b in btns:
t = b.get('text', '').replace('\n', ' ').strip()
hidden = b.get("hidden")
cls = b.get("class")
if t:
print(f' - "{t[:50]}" (Hidden: {hidden}, Class: {cls[:30]})')
count += 1
if count > 100:
break
if count > 100:
break
except Exception as e:
print(f'Error reading JSON: {e}')

View File

@@ -1,27 +0,0 @@
import json
try:
with open(r'C:\Users\Variet-Worker\.gemini\antigravity\bridge\deep-inspect-result.json', 'r', encoding='utf-8', errors='ignore') as f:
data = json.load(f)
print(f"Total Nodes: {len(data.get('nodes', []))}")
for node in data.get('nodes', []):
label = node.get('label', 'unknown')
iframes = node.get('iframes', [])
webviews = node.get('webviews', [])
buttons = node.get('buttons', [])
print(f"[Node] {label}")
print(f" URL: {node.get('url', '')[:50]}")
print(f" Total Elements: {node.get('totalElements', 0)}")
print(f" Buttons count: {len(buttons)}")
print(f" Iframes count: {len(iframes)}")
print(f" Webviews count: {len(webviews)}")
if iframes:
for iframe in iframes:
print(f" - iframe[{iframe.get('index')}]: accessible={iframe.get('accessible')} src={iframe.get('src', '')[:50]}")
if webviews:
for w in webviews:
print(f" - webview[{w.get('index')}]: src={w.get('src', '')[:50]}")
except Exception as e:
print(f'Error reading JSON: {e}')

View File

@@ -1,20 +0,0 @@
import json
try:
with open(r'C:\Users\Variet-Worker\.gemini\antigravity\bridge\deep-inspect-result.json', 'r', encoding='utf-8', errors='ignore') as f:
data = json.load(f)
print("SEARCHING FOR CHAT TEXT IN DUMP...")
found = False
with open(r'C:\Users\Variet-Worker\.gemini\antigravity\bridge\deep-inspect-result.json', 'r', encoding='utf-8', errors='ignore') as f:
raw_text = f.read()
if '스스로 만들고 스스로' in raw_text:
print("YES! The chat text IS in the raw JSON dump!")
found = True
elif 'Variet' in raw_text:
print("Found Variet in dump.")
else:
print("Chat text not found in raw dump.")
except Exception as e:
print(f'Error reading JSON: {e}')

View File

@@ -1,18 +0,0 @@
import json
try:
with open(r'C:\Users\Variet-Worker\.gemini\antigravity\bridge\deep-inspect-result.json', 'r', encoding='utf-8', errors='ignore') as f:
data = json.load(f)
for node in data.get('nodes', []):
label = node.get('label', 'unknown')
btns = node.get('buttons', [])
print(f"\n[Node] {label} (Total btns: {len(btns)})")
for i, b in enumerate(btns):
t = b.get('text', '').replace('\n', ' ').strip()
hidden = b.get("hidden")
cls = b.get("class")
print(f" {i:3d}: \"{t[:50]}\" (Hidden: {hidden}, Class: {cls[:30]})")
except Exception as e:
print(f'Error reading JSON: {e}')

View File

@@ -1,5 +0,0 @@
Start-Sleep -Seconds 3
$log = Get-Content 'C:\Users\Variet-Worker\.gemini\antigravity\bridge\extension.log' -Tail 500
$port = 0
foreach ($line in $log) { if ($line -match 'port (\d+)') { $port = $Matches[1] } }
if ($port -gt 0) { Invoke-RestMethod -Uri "http://127.0.0.1:$port/deep-inspect"; Write-Host 'Dump success!' }

View File

@@ -1,66 +0,0 @@
const fs = require('fs');
const { JSDOM } = require("jsdom");
try {
const observerModule = require("./extension/out/observer-script.js");
const dumpRaw = fs.readFileSync('C:\\Users\\Variet-Worker\\.gemini\\antigravity\\bridge\\dump_html.json', 'utf8');
const parseData = JSON.parse(dumpRaw);
let htmlStr = parseData.html;
// Inject fake port discovery node so it passes discoverPort()
htmlStr += `<div aria-label="Gravity Bridge Control port:1234"></div>`;
const dom = new JSDOM(htmlStr, { url: "http://localhost/", runScripts: "dangerously" });
const window = dom.window;
const document = window.document;
let testResults = [];
// Mock fetch for the observer
window.fetch = async (url, options) => {
if (url.includes('/ping')) {
return { text: async () => 'pong' };
}
if (url.includes('/pending') && options?.method === 'POST') {
const body = JSON.parse(options.body);
testResults.push("✅ POST /pending intercepted! Payload:");
testResults.push(JSON.stringify(body, null, 2));
return { json: async () => ({ok: true, request_id: body.request_id}) };
}
return { json: async () => ({}) };
};
// Fallback overrides
window.console.log = (m) => testResults.push(`[Script Log] ${m}`);
window.MutationObserver = window.MutationObserver || class { observe(){} };
window.AbortSignal = { timeout: () => ({}) };
let scriptStr = observerModule.generateApprovalObserverScript(1234);
// Brutally bypass discoverPort block and force initialization
scriptStr = scriptStr.replace(/discoverPort\(function\(port\)\{[\s\S]*?\}\);/, "BASE='http://127.0.0.1:1234';_ready=true;startObserver();");
scriptStr = scriptStr.replace("function scan(){", "function scan(){ log('scan() STAGE 1'); log('buttons in DOM: ' + document.querySelectorAll('button').length);");
scriptStr = scriptStr.replace("_obs=true;", "_obs=true; log('Forcing scan'); scan();");
// Run script inside JSDOM
const scriptEl = document.createElement("script");
scriptEl.textContent = scriptStr;
document.body.appendChild(scriptEl);
// Wait 3 seconds for discoverPort -> ping -> startObserver -> scheduleScan to execute
setTimeout(() => {
console.log("=== TEST RESULTS ===");
console.log(testResults.join("\n"));
if (!testResults.some(l => l.includes('POST /pending intercepted'))) {
console.error("❌ FAILED: No POST to /pending was made. The DOM scan failed to find the dummy button or extract context.");
process.exit(1);
} else {
console.log("✅ SUCCESS: The DOM extraction is functioning properly.");
process.exit(0);
}
}, 3000);
} catch (e) {
console.error("Test Harness Error:", e);
process.exit(1);
}

View File

@@ -1,144 +0,0 @@
const fs = require('fs');
const path = require('path');
const jsdom = require('jsdom');
const { JSDOM } = jsdom;
const observerModule = require('./extension/out/observer-script.js');
const observerCode = observerModule.generateApprovalObserverScript(8080);
const rawDump = JSON.parse(fs.readFileSync('C:/Users/Variet-Worker/.gemini/antigravity/bridge/dump_html.json', 'utf8'));
// Instantiate DOM
const dom = new JSDOM(rawDump.html, { runScripts: "dangerously", pretendToBeVisual: true, url: "http://localhost/" });
const window = dom.window;
const document = window.document;
// Polyfill offsetParent for visibility check
Object.defineProperty(window.HTMLElement.prototype, 'offsetParent', {
get() { return document.body; }
});
Object.defineProperty(window.HTMLElement.prototype, 'style', {
get() { return { display: 'block' }; }
});
// Mock innerText (JSDOM does not fully support it, but generic walker uses nodeValue / textContent)
// Our logic uses nodeValue for TextNodes, so it will work in JSDOM out of the box!
// Setup the DOM tree to perfectly match a real Chat conversation
const titleSpan = document.querySelector('span[title^="command("]');
if(!titleSpan) {
console.error("COULD NOT FIND command SPAN in dump?!");
process.exit(1);
}
// Find the card container
let toolContainer = titleSpan.parentElement;
while(toolContainer && !toolContainer.className.includes("border-gray-500/10") && !toolContainer.className.includes("bg-gray-500/10")) {
toolContainer = toolContainer.parentElement;
}
if(!toolContainer) toolContainer = titleSpan.parentElement; // fallback
// Create an AI text block just above it
const aiChat = document.createElement('div');
aiChat.className = 'markdown prose';
aiChat.innerHTML = '<p>안녕하세요! 시스템을 수정하기 위해 요청하신 작업을 시작합니다. <b>디스코드 릴레이 기능 복구</b>를 위해 스크립트를 실행하겠습니다.</p>';
// Wrap them up in the turn container
const parent = toolContainer.parentElement;
const convoWrapper = document.createElement('div');
convoWrapper.className = 'bg-agent-convo-background';
parent.insertBefore(convoWrapper, toolContainer);
convoWrapper.appendChild(aiChat);
convoWrapper.appendChild(toolContainer); // Move tool inside the convo wrapper as a sibling to AI chat
// Add action button to the tool container
const btn = document.createElement('button');
btn.innerHTML = '<span class="truncate">Allow</span>';
toolContainer.appendChild(btn);
console.log("Mock Button offsetParent:", btn.offsetParent ? btn.offsetParent.tagName : 'null');
console.log("Mock Button display:", btn.style.display);
console.log("Mock Button text:", btn.textContent);
// MOCK FETCH
const fetchCalls = [];
window.fetch = function(url, options) {
fetchCalls.push({url, options});
if (url.includes('/ping')) {
return Promise.resolve({ text: function() { return Promise.resolve('pong'); } });
}
if (url.includes('/pending')) {
return Promise.resolve({ json: function() { return Promise.resolve({ok: true, request_id: 'test-rid'}); } });
}
return Promise.resolve({ json: function() { return Promise.resolve({}); } });
};
// Polyfill offsetParent for visibility check
Object.defineProperty(window.HTMLElement.prototype, 'offsetParent', {
get() { return document.body; }
});
Object.defineProperty(window.HTMLElement.prototype, 'style', {
get() { return { display: 'block' }; }
});
const originalLog = console.log;
window.console.log = function(...args) {
if (args.length > 0 && typeof args[0] === 'string' && args[0].includes("NOT MATCHED")) {
let txt = args[1];
let codes = [];
if (txt) {
for(let i=0; i<txt.length; i++) codes.push(txt.charCodeAt(i));
}
originalLog('[JSDOM-WIN]', args[0], `\nRAW="${txt}"\nCODES=[${codes.join(',')}]`);
} else {
originalLog('[JSDOM-WIN]', ...args);
}
};
// Inject the observer
const scriptEl = document.createElement('script');
scriptEl.textContent = observerCode;
document.body.appendChild(scriptEl);
console.log("Observer injected, waiting for cycles...");
console.log("Total Buttons in DOM:", document.querySelectorAll('button').length);
// Emulate UI mutation to trigger the MutationObserver and force an instant scan()
setTimeout(() => {
console.log("Triggering DOM mutation to force scan()...");
document.body.appendChild(document.createElement('span'));
}, 1500);
// Give it time to finish scan().
setTimeout(() => {
console.log("\n====== FETCH CALLS ======");
if(fetchCalls.length === 0) console.log("NO FETCH CALLS MADE!");
fetchCalls.forEach(c => {
console.log(`\n[${c.options ? c.options.method || 'GET' : 'GET'}] ${c.url}`);
if(c.options && c.options.body) {
try {
let j = JSON.parse(c.options.body);
console.log("[BODY] request_id:", j.request_id);
console.log("[BODY] command:", j.command);
console.log("[BODY] description:\n" + "=".repeat(40) + "\n" + j.description + "\n" + "=".repeat(40));
} catch(e) {
console.log("[BODY]", c.options.body);
}
}
});
// Determine success
const pendingCall = fetchCalls.find(c => c.url.includes('/pending'));
if(pendingCall && pendingCall.options && pendingCall.options.body) {
const payload = JSON.parse(pendingCall.options.body);
if(payload.description.includes("안녕하세요!") && payload.description.includes("METHOD=TITLE_SPAN")) {
console.log("\n✅ SUCCESS: Both Chat Body & Tool Command effectively extracted!");
process.exit(0);
} else {
console.log("\n❌ FAIL: Payload description missing either chat text or command string!");
process.exit(1);
}
} else {
console.log("\n❌ FAIL: /pending never called!");
process.exit(1);
}
}, 4000);

27
test_hub.py Normal file
View File

@@ -0,0 +1,27 @@
import asyncio
from bot import GravityBot
import discord
class FakeChannel:
async def send(self, embed, view=None):
print("Sent to channel successfully!")
class FakeMsg:
id = 999
return FakeMsg()
async def test():
bot = GravityBot(asyncio.Queue())
bot.project_channels["gravity_control"] = FakeChannel()
class FakeHub:
def get_active_count(self, proj): return 1
bot.hub = FakeHub()
await bot._hub_on_pending("gravity_control", {
"request_id": "test1",
"command": "Running1 command",
"description": "test",
"step_type": "",
"buttons": [{"text": "Proceed", "index": 0}],
"timestamp": 12345678.0
})
asyncio.run(test())

47
test_hub_remote.py Normal file
View File

@@ -0,0 +1,47 @@
import asyncio
import websockets
import json
async def main():
uri = "wss://ag.variet.net/ws"
try:
async with websockets.connect(uri) as ws:
print("Connected to remote hub.")
# Send AUTH first
auth_msg = {
"type": "auth",
"token": "2352253f42bd0f9190a83c26f05cc252e86c55c044206953fa7b8fd97adaa6d3",
"project": "gravity_control",
"instance_number": 1,
"pc_name": "TEST_PC"
}
await ws.send(json.dumps(auth_msg))
resp = await ws.recv()
print("Auth response:", resp)
# Now send pending
msg = {
"type": "pending",
"data": {
"request_id": "999999999_mock_1",
"command": "MOCK COMMAND FROM TEST",
"description": "If you see this, the hub is routing perfectly.",
"step_type": "",
"status": "pending",
"buttons": [{"text": "Proceed", "index": 0}],
"project_name": "gravity_control",
"conversation_id": "test_conv"
}
}
await ws.send(json.dumps(msg))
print("Message sent.")
# Keep alive and print responses
while True:
resp = await asyncio.wait_for(ws.recv(), timeout=5.0)
print("Received:", resp)
except Exception as e:
print("Done:", e)
asyncio.run(main())

View File

@@ -1,98 +0,0 @@
const fs = require('fs');
const { JSDOM } = require("jsdom");
try {
const dumpRaw = fs.readFileSync('C:\\Users\\Variet-Worker\\.gemini\\antigravity\\bridge\\dump_html.json', 'utf8');
const parseData = JSON.parse(dumpRaw);
let htmlStr = parseData.html;
const dom = new JSDOM(htmlStr);
const document = dom.window.document;
// Direct copy of functions from observer-script.ts
function findButtonContainer(btn){
return btn.closest('.p-1')
|| btn.closest('.bg-agent-convo-background')
|| btn.closest('[class*="border-gray-500/10"]')
|| btn.closest('.monaco-list-row')
|| btn.parentElement;
}
function cleanButtonText(btn) {
if (!btn) return '';
var tr = btn.querySelector('.truncate');
var txt = (tr ? tr.textContent : btn.textContent) || '';
return txt.trim().replace(/(Alt|Ctrl|Shift|Meta)\+.*/i,'').trim();
}
function extractContext(b){
var container = findButtonContainer(b);
if (!container) return "ERROR_NO_CONTAINER";
var titleSpans = container.querySelectorAll('span[title^="command("]');
if (titleSpans && titleSpans.length > 0) {
var t = titleSpans[0].getAttribute('title');
if (t && t.length > 5) return "METHOD=TITLE_SPAN | " + t.substring(0, 800);
}
var preEls = container.querySelectorAll('pre');
if (preEls && preEls.length > 0) {
var t2 = (preEls[preEls.length-1].textContent || '').trim();
if (t2.length > 2) return "METHOD=PRE_SPAN | " + t2.substring(0, 800);
}
var codeText = '';
var codes = container.querySelectorAll('code, [class*="command"]');
for(var i=0; i<codes.length; i++) {
codeText += (codes[i].textContent || '').trim() + ' ';
}
if (codeText.length > 2) return "METHOD=CODES | " + codeText.trim().substring(0, 800);
var fallback = (container.textContent || '').replace(cleanButtonText(b), '').trim();
return "METHOD=FALLBACK | " + fallback.substring(0, 500);
}
// RUN TEST
const allBtns = document.querySelectorAll('button');
console.log(`Total buttons found: ${allBtns.length}`);
let tested = 0;
for(let j=0; j<allBtns.length; j++) {
let b = allBtns[j];
let txt = cleanButtonText(b);
if (txt.length <= 1) continue; // Icon
var PATS = [
{ type: 'command', re: /^(?:Always\s*)?Run\b/i },
{ type: 'permission', re: /^(?:Always\s*)?Allow\b/i },
{ type: 'permission', re: /^(?:Always\s*)?Approve\b/i },
{ type: 'diff_review', re: /^(?:Always\s*)?Accept\b/i }
];
var matchedType=null;
for(var p=0;p<PATS.length;p++){
if(PATS[p].re.test(txt)){
matchedType=PATS[p].type;
break;
}
}
if (!matchedType) continue;
console.log(`\n✅ Matched Button: "${txt}" (Type: ${matchedType})`);
console.log(` Extracting Context Data...`);
console.log(` -> ` + extractContext(b));
tested++;
}
if (tested === 0) {
console.log("❌ No actionable buttons matched!");
process.exit(1);
} else {
console.log("\n✅ SUCCESS: Context fully extracted via DOM script logic.");
process.exit(0);
}
} catch (e) {
console.error(e);
process.exit(1);
}

View File

@@ -1,31 +0,0 @@
const { LSBridge } = require('./extension/out/sdk/ls-bridge');
async function test() {
const ls = new LSBridge();
await ls.connect();
console.log("Testing { limit: 5, descending: true }...");
let start = Date.now();
const res = await ls._rpc('GetAllCascadeTrajectories', { limit: 5, descending: true });
let duration = Date.now() - start;
const summaries = res.trajectorySummaries || {};
const keys = Object.keys(summaries);
console.log(`Execution time: ${duration}ms`);
console.log(`Returned entries: ${keys.length}`);
keys.slice(0, 5).forEach((k, idx) => {
const modT = summaries[k].lastModifiedTime || summaries[k].lastModifiedTimestamp || 'UNKNOWN';
console.log(`[${idx}] id=${k.substring(0,8)} mod=${modT} status=${summaries[k].status}`);
});
console.log("\nTesting { limit: 100, descending: true }...");
start = Date.now();
const res100 = await ls._rpc('GetAllCascadeTrajectories', { limit: 100, descending: true });
duration = Date.now() - start;
console.log(`Execution time: ${duration}ms`);
console.log(`Returned entries: ${Object.keys(res100.trajectorySummaries || {}).length}`);
ls.disconnect();
}
test();

12
test_view.py Normal file
View File

@@ -0,0 +1,12 @@
import discord
from bot import ApprovalView
from models import ApprovalRequest
request = ApprovalRequest("id", "convo", "cmd", "desc", 0.0)
view = ApprovalView(request, buttons=[{"text":"Proceed","index":0}], hub=None)
print("View items:", view.children)
try:
print(view.to_components())
print("SUCCESS")
except Exception as e:
print("CRASH:", e)

12
test_view2.py Normal file
View File

@@ -0,0 +1,12 @@
import discord
from bot import ApprovalView
from models import ApprovalRequest
request = ApprovalRequest("id", "convo", "cmd", "desc", 0.0)
view = ApprovalView(request, buttons=[{"text":"yes","index":0}, {"text":"no","index":1}], hub=None)
print("View items:", view.children)
try:
print(view.to_components())
print("SUCCESS")
except Exception as e:
print("CRASH:", e)

View File

@@ -1,50 +0,0 @@
// test_ws_logic.js
class FakeWS {
constructor() {
this.msgLog = [];
this.terminated = false;
}
send(msg) {
this.msgLog.push(msg);
}
terminate() {
this.terminated = true;
}
close() {
this.terminated = true;
}
}
// SIMULATE _startHeartbeat() logic from ws-client.ts v0.5.12
function testLogic(isNodeWs, serverSendsPong) {
let ws = new FakeWS();
let connected = true;
let lastPongTime = Date.now();
let forceHeartbeatTimeoutIfNoPong = serverSendsPong;
let checkCounter = 0;
// Fast forward 61 seconds in time
let timeElapsed = 61000;
let currentNow = Date.now() + timeElapsed;
// Simulate heartbeat timeout logic
let conditionMet = false;
if ((isNodeWs || forceHeartbeatTimeoutIfNoPong) && currentNow - lastPongTime > 60000) {
conditionMet = true;
ws.terminate();
}
return {
conditionMet: conditionMet,
terminated: ws.terminated
};
}
console.log("Scenario 1: Node WS (native ping/pong) MUST enforce 60s timeout:");
console.log(testLogic(true, false)); // expect true, true
console.log("\nScenario 2: Browser WS (fallback) + NO JSON PONG FROM SERVER MUST NOT enforce 60s timeout:");
console.log(testLogic(false, false)); // expect false, false (PREVENTS FALSE POSITIVE)
console.log("\nScenario 3: Browser WS (fallback) + JSON PONG FROM SERVER MUST enforce 60s timeout:");
console.log(testLogic(false, true)); // expect true, true (DETECTS ZOMBIE)