Compare commits
163 Commits
5a1d4f0b0c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32cf69469c | ||
|
|
7c8891b99c | ||
|
|
3cc3442fda | ||
|
|
e95e7791f9 | ||
|
|
2bf1eb41d1 | ||
|
|
cf1352eefa | ||
|
|
6aea48e2e9 | ||
|
|
bd5a7ca8b9 | ||
|
|
8ada5f7daf | ||
|
|
4f2be831a1 | ||
|
|
cbfd137dcb | ||
|
|
a99a1e3f54 | ||
|
|
ad4ed623bd | ||
|
|
64800d3c20 | ||
|
|
70c83b4226 | ||
|
|
bb54802c06 | ||
|
|
bf53072f3c | ||
|
|
02b4b03699 | ||
|
|
db805c6fde | ||
|
|
7f33a20e43 | ||
|
|
ef788e6ecc | ||
|
|
cd00986274 | ||
|
|
12095f36a4 | ||
|
|
498683c977 | ||
|
|
1662ac4f6b | ||
|
|
d027562f17 | ||
|
|
cc261011d6 | ||
|
|
37fbb9657e | ||
|
|
965f619664 | ||
|
|
139ad3ee93 | ||
|
|
08fd08b9a6 | ||
|
|
326454be40 | ||
|
|
98b7585e3c | ||
|
|
c7f939ce85 | ||
|
|
7a1675fd5d | ||
|
|
6b9f1188c3 | ||
|
|
13e569f426 | ||
|
|
b2f17a086b | ||
|
|
7dbf73aa89 | ||
|
|
62ee081ffe | ||
|
|
60a2a97868 | ||
|
|
7ae43088e6 | ||
|
|
729875f3a6 | ||
|
|
a00d561e28 | ||
|
|
7ade31e4cf | ||
|
|
66233bd9cb | ||
|
|
ed90cbf874 | ||
|
|
2e32be96fe | ||
|
|
87c99c7243 | ||
|
|
01539e9bfb | ||
|
|
4684376c78 | ||
|
|
7ee5947b32 | ||
|
|
59ddcbb612 | ||
|
|
ed63f65975 | ||
|
|
02d9799f20 | ||
|
|
a9c64e6716 | ||
|
|
8e89209a27 | ||
|
|
a8d875167d | ||
|
|
2a1ebf1020 | ||
|
|
5a76e30993 | ||
|
|
d6ed8764b8 | ||
|
|
a214ab029f | ||
|
|
f45d2d970d | ||
|
|
6dc0854c47 | ||
|
|
0e03b3a300 | ||
|
|
353265bed8 | ||
|
|
eef59e6bb2 | ||
|
|
a4d7286bce | ||
|
|
70dc301dca | ||
|
|
7630bf1f8c | ||
|
|
ec7883755a | ||
|
|
072f83bf25 | ||
|
|
5e697cd919 | ||
|
|
b3825e1c8a | ||
|
|
a99c283656 | ||
|
|
58887f6933 | ||
|
|
488b36f192 | ||
|
|
300338d5d3 | ||
|
|
e745744636 | ||
|
|
b88e75b075 | ||
|
|
2ece05fc6f | ||
|
|
6bbc9ddd00 | ||
|
|
89c95de18c | ||
|
|
fadd39424b | ||
|
|
22e1799d66 | ||
|
|
e4f674ec9f | ||
|
|
47c0602427 | ||
|
|
75762964e3 | ||
|
|
d2023321bd | ||
|
|
2eb1fbb6b7 | ||
|
|
13f13ee243 | ||
|
|
2d5059d2d5 | ||
|
|
7bbd8749d7 | ||
|
|
d5fdc41f35 | ||
|
|
3ec45ac6b7 | ||
|
|
101ec20b21 | ||
|
|
86e5a24a75 | ||
|
|
7b6cd59801 | ||
|
|
f13bcc871c | ||
|
|
ecebec3906 | ||
|
|
e21f71baf8 | ||
|
|
b81135d855 | ||
|
|
a6aa643be9 | ||
|
|
6234301a47 | ||
|
|
a72c522ab5 | ||
|
|
f4ded343c7 | ||
|
|
5aad82c727 | ||
|
|
94cbda6f3d | ||
|
|
549af6dae2 | ||
|
|
e306fae130 | ||
|
|
bc9d0f2fbb | ||
|
|
17978a750c | ||
|
|
0f057c0c95 | ||
|
|
a41062b6ff | ||
|
|
029a246658 | ||
|
|
e7631177f8 | ||
|
|
4a5521dcc3 | ||
|
|
ab0c116c9e | ||
|
|
07bbb626a6 | ||
|
|
d55b6b97ad | ||
|
|
d8eac80b2f | ||
|
|
759dab55b6 | ||
|
|
bbfafdc5e4 | ||
|
|
ac803d436f | ||
|
|
ebf2228aa8 | ||
|
|
881a424b23 | ||
|
|
d06b1ea0db | ||
|
|
48ae19b3e1 | ||
|
|
9ccfa83439 | ||
|
|
0fae7e32aa | ||
|
|
47cc838d9d | ||
|
|
4e8ac8d6b7 | ||
|
|
0da6291d98 | ||
|
|
4bb400820c | ||
|
|
302d21d35c | ||
|
|
6640d42449 | ||
|
|
1ce8b7c707 | ||
|
|
2eea5fa638 | ||
|
|
adbed69237 | ||
|
|
442221e6a3 | ||
|
|
50efd52f41 | ||
|
|
f6181e552d | ||
|
|
1bb54eb820 | ||
|
|
9523d1328e | ||
|
|
96e9b8adce | ||
|
|
edd4943e2e | ||
|
|
6ea3211a58 | ||
|
|
b9b240de0b | ||
|
|
36b70505d7 | ||
|
|
5bdaba01bd | ||
|
|
28d399ba91 | ||
|
|
fadfd88f51 | ||
|
|
61bd4b1ffb | ||
|
|
5f795b9a91 | ||
|
|
a372bd8b2d | ||
|
|
e3f8fb93f7 | ||
|
|
7ca0bc0f1f | ||
|
|
7f079a56a0 | ||
|
|
fdc0084813 | ||
|
|
f309518e78 | ||
|
|
412c212c6e | ||
|
|
0035394b9c | ||
|
|
0fdf668abc |
@@ -1,35 +1,322 @@
|
|||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
> 이 프로젝트의 아키텍처를 설명하는 문서입니다.
|
|
||||||
> AI 에이전트는 구현 전 이 문서를 반드시 확인합니다.
|
> AI 에이전트는 구현 전 이 문서를 반드시 확인합니다.
|
||||||
|
|
||||||
## 프로젝트 개요
|
## 1. 프로젝트 개요
|
||||||
|
|
||||||
<!-- 프로젝트의 목적과 핵심 기능을 간략히 서술 -->
|
**Gravity Control**은 Antigravity AI 코딩 에이전트와 Discord를 실시간으로 연결하는 브릿지 시스템이다.
|
||||||
|
|
||||||
(프로젝트 설명을 여기에 작성하세요)
|
### 핵심 목적
|
||||||
|
- AI 에이전트의 **승인 요청**(코드 실행, 파일 수정 등)을 Discord로 전달하고 사용자 응답을 반환
|
||||||
|
- AI 에이전트의 **작업 스냅샷**(대화 요약, 진행 상황)을 Discord에 실시간 표시
|
||||||
|
- **코드 리뷰**(diff review) accept/reject을 Discord에서 처리
|
||||||
|
- 사용자의 Discord **명령어**(!approve, !reject, !auto 등)를 AG Extension으로 전달
|
||||||
|
- **Auto-approve 모드**로 무인 작업 지원
|
||||||
|
|
||||||
## 디렉토리 구조
|
### 시스템 구성
|
||||||
|
|
||||||
```
|
```
|
||||||
project-root/
|
┌────────────────┐ WebSocket ┌──────────────┐ Discord API ┌─────────┐
|
||||||
├── src/ # 소스 코드
|
│ VS Code │◄──────────────────►│ Hub Server │◄───────────────────►│ Discord │
|
||||||
├── tests/ # 테스트
|
│ AG Extension │ type:auth/chat │ (hub.py + │ discord.py bot │ 서버 │
|
||||||
├── docs/ # 문서
|
│ (TypeScript) │ /pending/resp │ gateway.py)│ │ │
|
||||||
├── .agents/ # AI 에이전트 설정
|
└────────────────┘ └──────────────┘ └─────────┘
|
||||||
└── ...
|
↕ AG SDK (RPC) ↕
|
||||||
|
┌────────────────┐ ┌──────────────┐
|
||||||
|
│ Antigravity │ │ 파일 bridge │ ← 레거시 fallback
|
||||||
|
│ AI Engine │ │ (bridge.py) │ (WS 미사용 시)
|
||||||
|
└────────────────┘ └──────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
## 핵심 모듈
|
---
|
||||||
|
|
||||||
<!-- 각 모듈의 역할과 의존 관계를 설명 -->
|
## 2. 디렉토리 구조
|
||||||
|
|
||||||
| 모듈 | 역할 | 의존성 |
|
```
|
||||||
|------|------|--------|
|
gravity_control/
|
||||||
| (모듈명) | (역할 설명) | (의존하는 모듈) |
|
├── main.py # 진입점: Bot + Hub + Watcher 통합 시작
|
||||||
|
├── config.py # 환경변수 + .env 로드 (66줄)
|
||||||
|
│
|
||||||
|
├── ── 서버 측 (Python) ──
|
||||||
|
├── bot.py # Discord 봇: 승인 UI, 채널 관리, Hub 핸들러 (1,286줄)
|
||||||
|
├── hub.py # WebSocket Hub: 연결 관리, 메시지 라우팅 (580줄)
|
||||||
|
├── auth.py # JWT 토큰 + registration code 인증 (127줄)
|
||||||
|
├── gateway.py # HTTP REST API + /ws endpoint (168줄)
|
||||||
|
├── bridge.py # 파일 기반 IPC (레거시 fallback) (270줄)
|
||||||
|
├── watcher.py # Brain 디렉토리 변경 감시 (290줄)
|
||||||
|
├── parser.py # Markdown → Discord 변환 (245줄)
|
||||||
|
│
|
||||||
|
├── ── Extension 측 (TypeScript) ──
|
||||||
|
├── extension/src/
|
||||||
|
│ ├── extension.ts # 메인: SDK init, activate, 오케스트레이션 (650줄)
|
||||||
|
│ ├── step-probe.ts # 폴링 + 응답 처리 + 승인 전략 (1,479줄)
|
||||||
|
│ │ # setupMonitor(), processResponseFile(),
|
||||||
|
│ │ # writePendingApproval(), tryApprovalStrategies()
|
||||||
|
│ ├── http-bridge.ts # HTTP 서버 (Renderer↔Extension Host 통신) (280줄)
|
||||||
|
│ │ # startHttpBridge(), getDeterministicPort()
|
||||||
|
│ ├── html-patcher.ts # AG HTML 패치 + product.json 체크섬 (280줄)
|
||||||
|
│ │ # setupApprovalObserver(), updateProductChecksums()
|
||||||
|
│ ├── command-handler.ts # Discord→AG 명령어 처리 (175줄)
|
||||||
|
│ │ # watchCommandsDir(), handleWSCommand()
|
||||||
|
│ ├── observer-script.ts # DOM Observer 스크립트 생성 (698줄)
|
||||||
|
│ │ # generateApprovalObserverScript()
|
||||||
|
│ ├── ws-client.ts # WSBridgeClient: Hub 연결, 재연결, 큐 (505줄)
|
||||||
|
│ ├── step-utils.ts # Step 파싱 순수 함수 4개 (114줄)
|
||||||
|
│ │ # extractPlannerText, filterEphemeral,
|
||||||
|
│ │ # extractToolCommand, extractToolDescription
|
||||||
|
│ └── sdk/ # Antigravity SDK 로컬 임베드
|
||||||
|
│ ├── index.js # SDK 런타임 (4,014줄)
|
||||||
|
│ └── index.d.ts # SDK 타입 정의 (2,297줄)
|
||||||
|
│
|
||||||
|
├── ── 테스트 ──
|
||||||
|
├── tests/
|
||||||
|
│ ├── test_ws_hub.py # Hub WS 연결 테스트
|
||||||
|
│ └── test_syntax.py # Python 구문 검증
|
||||||
|
│
|
||||||
|
├── ── 문서 / 설정 ──
|
||||||
|
├── .env # 환경변수 (git 제외)
|
||||||
|
├── .agents/references/ # AI 에이전트 레퍼런스
|
||||||
|
├── docs/devlog/ # 작업 로그
|
||||||
|
└── start_bot.bat # 윈도우용 봇 시작 스크립트
|
||||||
|
```
|
||||||
|
|
||||||
## 데이터 흐름
|
---
|
||||||
|
|
||||||
<!-- 주요 데이터 흐름을 Mermaid 다이어그램이나 텍스트로 설명 -->
|
## 3. 핵심 모듈 상세
|
||||||
|
|
||||||
(데이터 흐름을 여기에 작성하세요)
|
### 3.1 Hub (hub.py) — WebSocket 메시지 허브
|
||||||
|
|
||||||
|
**역할**: Extension ↔ Bot 간 실시간 양방향 통신 중계
|
||||||
|
|
||||||
|
| 기능 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| 연결 관리 | 프로젝트별 다중 인스턴스, 인스턴스 번호 자동 부여 |
|
||||||
|
| JWT 인증 | registration_code → JWT 발급 → 이후 토큰 재인증 |
|
||||||
|
| 메시지 라우팅 | pending, chat, register, auto_resolve, brain_event |
|
||||||
|
| 응답 역라우팅 | request_id → pending_owners → 원본 Extension으로 전달 |
|
||||||
|
| Rate limiting | per-connection 100msg/10s |
|
||||||
|
| Dedup | msg_id 기반 60s TTL 중복 제거 |
|
||||||
|
| Heartbeat | 30s 간격 ping/pong |
|
||||||
|
|
||||||
|
**프로토콜**:
|
||||||
|
```
|
||||||
|
1. Client → Server: {type:"auth", registration_code/token, project, pc}
|
||||||
|
2. Server → Client: {type:"auth_ok", conn_id, instance_number, session_token}
|
||||||
|
3. 양방향 메시지 교환:
|
||||||
|
- Extension→Hub: pending, chat, register, auto_resolve, brain_event
|
||||||
|
- Hub→Extension: response, command, instance_update, error
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Auth (auth.py) — 인증 관리
|
||||||
|
|
||||||
|
| 기능 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| Registration Code | 사전 공유 코드로 최초 인증 |
|
||||||
|
| JWT 발급 | HMAC-SHA256, 24시간 유효 |
|
||||||
|
| 토큰 검증 | 만료/위조 감지, 프로젝트+PC 메타데이터 포함 |
|
||||||
|
|
||||||
|
### 3.3 Bot (bot.py) — Discord 인터페이스
|
||||||
|
|
||||||
|
| 기능 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| 승인 UI | Approve/Reject 버튼, diff_review Accept/Reject |
|
||||||
|
| Auto-approve | `!auto` 토글, 세션 간 초기화 |
|
||||||
|
| 채널 관리 | `#ag-{project}` 자동 채널 매칭 |
|
||||||
|
| 스냅샷 전달 | 2000자 초과 시 파일 첨부 |
|
||||||
|
| 명령어 | !approve, !reject, !auto, !status, !send |
|
||||||
|
| Hub 연동 | on_hub_pending, on_hub_chat, on_hub_register 핸들러 |
|
||||||
|
| IDLE 알림 | AI step 종료 시 Discord 알림 |
|
||||||
|
|
||||||
|
### 3.4 Extension (extension.ts) — VS Code 확장 (오케스트레이터)
|
||||||
|
|
||||||
|
| 기능 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| AG SDK 연동 | getCascadeStatus, getAllTrajectories RPC |
|
||||||
|
| 세션 감지 | activeSessionId 자동 추적 |
|
||||||
|
| 프로젝트 자동 감지 | git remote URL 기반 |
|
||||||
|
| 모듈 초기화 | HTTP bridge, observer, command handler 시작 |
|
||||||
|
| WS bridge | WSBridgeClient 통한 Hub 연결 (우선) |
|
||||||
|
| Status bar | SDK 상태 + 연결 상태 표시 |
|
||||||
|
|
||||||
|
### 3.4a HTTP Bridge (http-bridge.ts)
|
||||||
|
|
||||||
|
`HttpBridgeContext` 인터페이스로 extension.ts의 공유 상태 참조:
|
||||||
|
|
||||||
|
| 기능 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| POST /pending | Renderer가 발견한 승인 버튼 보고 |
|
||||||
|
| GET /response/:rid | Renderer가 Discord 응답 폴링 |
|
||||||
|
| GET /trigger-click | Extension→Renderer 클릭 트리거 |
|
||||||
|
| GET/POST /deep-inspect* | DOM 심층 검사 |
|
||||||
|
| getDeterministicPort | 프로젝트명 기반 결정적 포트 |
|
||||||
|
|
||||||
|
### 3.4b HTML Patcher (html-patcher.ts)
|
||||||
|
|
||||||
|
| 기능 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| setupApprovalObserver | AG Workbench HTML 파일에 observer 스크립트 인라인 삽입 |
|
||||||
|
| updateProductChecksums | product.json SHA256 체크섬 업데이트 (vscode-file:// 프로토콜용) |
|
||||||
|
| CSP 패치 | script-src에 'unsafe-inline' 추가 |
|
||||||
|
| .orig 백업 | 최초 패치 전 원본 백업, 손상 시 자동 복구 |
|
||||||
|
|
||||||
|
### 3.4c Command Handler (command-handler.ts)
|
||||||
|
|
||||||
|
`CommandHandlerContext` 인터페이스로 extension.ts 상태 참조:
|
||||||
|
|
||||||
|
| 기능 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| watchCommandsDir | commands/ 디렉토리 fs.watch + 3s 폴링 |
|
||||||
|
| handleWSCommand | WS Hub 경유 명령어 처리 |
|
||||||
|
| !stop, !auto | AG 에이전트 제어 명령어 |
|
||||||
|
| 텍스트 전달 | Discord → AG `sendPromptToAgentPanel` |
|
||||||
|
|
||||||
|
### 3.5 Step Probe (step-probe.ts) — 상태 폴링
|
||||||
|
|
||||||
|
`BridgeContext` 인터페이스로 extension.ts와 상태 공유:
|
||||||
|
|
||||||
|
| 기능 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| setupMonitor | 3초 간격 SDK 폴링, 세션/step 변화 감지 |
|
||||||
|
| processResponseFile | Discord 응답 → AG RPC 실행 |
|
||||||
|
| writePendingApproval | 승인 요청 파일/WS 전송 |
|
||||||
|
| tryApprovalStrategies | 다단계 승인: DOM click → VS Code command → RPC |
|
||||||
|
| setupResponseWatcher | response/ 디렉토리 파일 감시 |
|
||||||
|
|
||||||
|
**BridgeContext 필드** (14개):
|
||||||
|
`bridgePath`, `projectName`, `sdk`, `wsBridge`, `logToFile`, `autoApproveEnabled`, `activeSessionId`, `setClickTrigger`, `recentDiscordSentTexts`, `writeChatSnapshot`, `writeChatSnapshotWithFiles`, `workspaceUri`, `diffReviewMetadata`, `sessionStalled`, `lastPendingStepIndex`, `stallProbed`, `sawRunningAfterPending`
|
||||||
|
|
||||||
|
### 3.6 WS Client (ws-client.ts) — Hub 클라이언트
|
||||||
|
|
||||||
|
| 기능 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| 연결 관리 | WebSocket + 자동 재연결 |
|
||||||
|
| Backoff | 1s→60s 지수 백오프 + ±30% jitter |
|
||||||
|
| 메시지 큐 | 200개 버퍼, 재연결 시 자동 flush |
|
||||||
|
| Heartbeat | 25s 간격 ping |
|
||||||
|
| 인증 | registration_code 또는 session_token |
|
||||||
|
| API | sendPending, sendChat, sendRegister, sendAutoResolve |
|
||||||
|
|
||||||
|
### 3.7 Observer Script (observer-script.ts)
|
||||||
|
|
||||||
|
AG Webview의 DOM을 관찰하여 승인 버튼을 자동 감지/클릭:
|
||||||
|
- MutationObserver로 `.actions-container` 감시
|
||||||
|
- 버튼 텍스트 매칭으로 Approve/Reject 자동 실행
|
||||||
|
- `postMessage`로 Extension과 통신
|
||||||
|
|
||||||
|
### 3.8 Step Utils (step-utils.ts)
|
||||||
|
|
||||||
|
순수 함수 4개:
|
||||||
|
- `extractPlannerText(content)` — AI 응답 텍스트 추출
|
||||||
|
- `filterEphemeral(text)` — 시스템 메시지 필터링
|
||||||
|
- `extractToolCommand(content)` — 도구 명령어 추출
|
||||||
|
- `extractToolDescription(content)` — 도구 설명 추출
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 데이터 흐름
|
||||||
|
|
||||||
|
### 4.1 승인 요청 플로우
|
||||||
|
|
||||||
|
```
|
||||||
|
AG Engine → SDK RPC → Extension(step-probe.ts)
|
||||||
|
→ setupMonitor: WAITING step 감지
|
||||||
|
→ writePendingApproval: pending 데이터 생성
|
||||||
|
→ [WS] wsBridge.sendPending() → Hub → Bot → Discord (버튼 UI)
|
||||||
|
→ [파일] bridge/pending/{id}.json (fallback)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 승인 응답 플로우
|
||||||
|
|
||||||
|
```
|
||||||
|
Discord (사용자 버튼 클릭) → Bot
|
||||||
|
→ [Hub connected] Hub.route_response() → WS → Extension
|
||||||
|
→ [File fallback] bridge/response/{id}.json → setupResponseWatcher
|
||||||
|
→ processResponseFile → tryApprovalStrategies
|
||||||
|
1차: DOM observer script (webview inject)
|
||||||
|
2차: VS Code command (cascade.approveCurrentStep)
|
||||||
|
3차: Direct RPC (acknowledgeCodeActionStep)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 채팅 스냅샷 플로우
|
||||||
|
|
||||||
|
```
|
||||||
|
Extension(step-probe.ts) → 새 step 텍스트 감지
|
||||||
|
→ writeChatSnapshot(text) → truncation + dedup
|
||||||
|
→ [WS] wsBridge.sendChat() → Hub → Bot → Discord (#ag-{project})
|
||||||
|
→ [파일] bridge/pending/ snapshot 파일 (fallback)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Diff Review 플로우
|
||||||
|
|
||||||
|
```
|
||||||
|
AG Engine → 파일 수정 → Extension(step-probe.ts)
|
||||||
|
→ edit_step_indices + modified_files 메타데이터 수집
|
||||||
|
→ writePendingApproval (step_type="diff_review", 8초 지연)
|
||||||
|
→ Discord (Accept all / Reject all 버튼)
|
||||||
|
→ 응답 → handleDiffReviewResponse()
|
||||||
|
→ openReviewChanges → 파일별 포커스 → agentAcceptAllInFile VS Code 커맨드
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> diff_review는 **VS Code 커맨드만 유효**합니다. RPC 방식(AcknowledgeCascadeCodeEdit 등)은 모두 실패 확정.
|
||||||
|
> 상세 경위는 known-issues-archive.md의 "Diff Review 관련" 섹션을 참조하세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 통신 프로토콜
|
||||||
|
|
||||||
|
### 5.1 WebSocket 메시지 타입
|
||||||
|
|
||||||
|
**Extension → Hub (upstream)**:
|
||||||
|
|
||||||
|
| type | data 필드 | 설명 |
|
||||||
|
|------|-----------|------|
|
||||||
|
| `auth` | registration_code/token, project, pc | 최초 인증 |
|
||||||
|
| `pending` | request_id, command, description, buttons | 승인 요청 |
|
||||||
|
| `chat` | content, attached_files, conversation_id | 채팅 스냅샷 |
|
||||||
|
| `register` | conversation_id, project_name | 세션 등록 |
|
||||||
|
| `auto_resolve` | request_id | 자동 해결 알림 |
|
||||||
|
| `brain_event` | (payload) | 브레인 이벤트 |
|
||||||
|
| `heartbeat` | - | 연결 유지 |
|
||||||
|
|
||||||
|
**Hub → Extension (downstream)**:
|
||||||
|
|
||||||
|
| type | data 필드 | 설명 |
|
||||||
|
|------|-----------|------|
|
||||||
|
| `auth_ok` | conn_id, instance_number, session_token | 인증 성공 |
|
||||||
|
| `auth_fail` | reason | 인증 실패 |
|
||||||
|
| `response` | request_id, approved, button_index | 승인 응답 |
|
||||||
|
| `command` | text, action | Discord 명령어 |
|
||||||
|
| `instance_update` | active_count, instances[] | 인스턴스 변경 |
|
||||||
|
| `error` | error | 에러 |
|
||||||
|
|
||||||
|
### 5.2 BOT_MODE 동작 차이
|
||||||
|
|
||||||
|
| 모드 | Watcher | Hub/Gateway | 용도 |
|
||||||
|
|------|---------|-------------|------|
|
||||||
|
| `local` | ✅ (brain 감시) | ❌ | 로컬 개발 (Extension과 같은 PC) |
|
||||||
|
| `gateway` | ❌ | ✅ (port 8585) | 서버 배포 (WS Hub + Gateway) |
|
||||||
|
| `remote` | ✅ | ❌ | ⚠️ **DEPRECATED** — 레거시 Collector (WS Hub 사용 권장) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 보안
|
||||||
|
|
||||||
|
| 항목 | 구현 |
|
||||||
|
|------|------|
|
||||||
|
| WS 인증 | Registration Code → JWT (HMAC-SHA256, 24h) |
|
||||||
|
| Gateway API | API Key 헤더 (`X-API-Key`) |
|
||||||
|
| Rate limit | per-connection 100msg/10s |
|
||||||
|
| 메시지 dedup | msg_id 기반 60s TTL |
|
||||||
|
| Discord | Bot 토큰 + Guild ID 제한 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Extension 설정 (VS Code)
|
||||||
|
|
||||||
|
| 설정 키 | 설명 | 기본값 |
|
||||||
|
|---------|------|--------|
|
||||||
|
| `gravityBridge.bridgePath` | Bridge 디렉토리 경로 | `~/.gemini/antigravity/bridge` |
|
||||||
|
| `gravityBridge.projectName` | 프로젝트 이름 | git remote 자동 감지 |
|
||||||
|
| `gravityBridge.hubUrl` | WebSocket Hub URL | (비어있으면 WS 비활성) |
|
||||||
|
| `gravityBridge.registrationCode` | Hub 등록 코드 | (서버에서 발급) |
|
||||||
|
|||||||
@@ -4,20 +4,37 @@
|
|||||||
|
|
||||||
## 네이밍
|
## 네이밍
|
||||||
|
|
||||||
|
### Python (서버)
|
||||||
|
|
||||||
| 대상 | 규칙 | 예시 |
|
| 대상 | 규칙 | 예시 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 변수/함수 | camelCase | `getUserData()` |
|
| 변수/함수 | snake_case | `write_pending_approval()` |
|
||||||
| 클래스 | PascalCase | `UserService` |
|
| 클래스 | PascalCase | `GravityBot`, `WSHub`, `TokenManager` |
|
||||||
| 상수 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
| 상수 | UPPER_SNAKE_CASE | `MAX_MSG_SIZE`, `HEARTBEAT_INTERVAL` |
|
||||||
| 파일명 | kebab-case | `user-service.js` |
|
| 파일명 | snake_case | `hub.py`, `ws_client.py` |
|
||||||
| CSS 클래스 | kebab-case | `.nav-header` |
|
| 로거명 | 모듈명 | `logging.getLogger("hub")` |
|
||||||
|
|
||||||
|
### TypeScript (Extension)
|
||||||
|
|
||||||
|
| 대상 | 규칙 | 예시 |
|
||||||
|
|------|------|------|
|
||||||
|
| 변수/함수 | camelCase | `writePendingApproval()`, `setupMonitor()` |
|
||||||
|
| 클래스 | PascalCase | `WSBridgeClient` |
|
||||||
|
| 인터페이스 | PascalCase | `BridgeContext`, `WSPendingData` |
|
||||||
|
| 상수 | UPPER_SNAKE_CASE | `MAX_QUEUE_SIZE`, `AUTH_TIMEOUT` |
|
||||||
|
| 파일명 | kebab-case | `ws-client.ts`, `step-probe.ts` |
|
||||||
|
| export 함수 | camelCase | `initStepProbe()`, `generateApprovalObserverScript()` |
|
||||||
|
|
||||||
## 코드 스타일
|
## 코드 스타일
|
||||||
|
|
||||||
- 들여쓰기: (2 spaces / 4 spaces / tab)
|
| 항목 | Python | TypeScript |
|
||||||
- 세미콜론: (사용 / 미사용)
|
|------|--------|-----------|
|
||||||
- 따옴표: (single / double)
|
| 들여쓰기 | 4 spaces | 4 spaces |
|
||||||
- 줄바꿈: LF (Unix style)
|
| 따옴표 | 쌍따옴표 `"` (f-string 포함) | 작은따옴표 `'` |
|
||||||
|
| 세미콜론 | N/A | 사용 |
|
||||||
|
| 줄바꿈 | LF (Unix) | CRLF (Windows, git 자동 변환) |
|
||||||
|
| 최대 줄 길이 | 120자 권장 | 120자 권장 |
|
||||||
|
| 타입 힌트 | 적극 사용 (`-> list[str]`) | strict (`BridgeContext` 인터페이스) |
|
||||||
|
|
||||||
## 커밋 메시지
|
## 커밋 메시지
|
||||||
|
|
||||||
@@ -25,21 +42,84 @@
|
|||||||
<type>(<scope>): <description>
|
<type>(<scope>): <description>
|
||||||
|
|
||||||
type: feat|fix|refactor|test|docs|chore|ci|infra
|
type: feat|fix|refactor|test|docs|chore|ci|infra
|
||||||
scope: (선택)
|
scope: server|extension|hub|bot|gateway|bridge (선택)
|
||||||
```
|
```
|
||||||
|
|
||||||
**예시:**
|
**예시:**
|
||||||
- `feat(server): add WebSocket reconnection logic`
|
- `feat(hub): WebSocket Hub 구현 + JWT 인증`
|
||||||
- `fix(frontend): resolve button overlap on mobile`
|
- `refactor(extension): 모듈 분리 (step-probe, observer-script)`
|
||||||
- `docs: update API documentation`
|
- `fix(bot): auto-approve 세션 간 초기화`
|
||||||
|
- `docs: architecture.md 전면 재작성`
|
||||||
|
|
||||||
|
관련 Vikunja 태스크가 있으면: `feat(hub): WS Hub 구현 #task-395`
|
||||||
|
|
||||||
## 주석
|
## 주석
|
||||||
|
|
||||||
- 한국어/영어 혼용 가능
|
- 한국어/영어 혼용 가능
|
||||||
- TODO 주석: `// TODO: 설명` 형식
|
- TODO 주석: `// TODO: 설명` 형식
|
||||||
|
- 섹션 구분: `// ─── Section Name ───` (TypeScript), `# ─── Section ───` (Python)
|
||||||
- 복잡한 로직에는 반드시 WHY(왜) 주석 추가
|
- 복잡한 로직에는 반드시 WHY(왜) 주석 추가
|
||||||
|
- 함수 docstring: Python은 `"""..."""`, TypeScript는 `/** ... */`
|
||||||
|
|
||||||
|
## 모듈 분리 패턴
|
||||||
|
|
||||||
|
Extension 모듈 분리 시 사용하는 패턴:
|
||||||
|
|
||||||
|
| 패턴 | 용도 | 예시 |
|
||||||
|
|------|------|------|
|
||||||
|
| **순수 함수 추출** | 외부 상태 참조 없는 함수 | `step-utils.ts` |
|
||||||
|
| **독립 스크립트** | 문자열 반환 함수 | `observer-script.ts` |
|
||||||
|
| **Context 패턴** | 공유 상태가 많은 함수 그룹 | `step-probe.ts` (BridgeContext) |
|
||||||
|
| **클래스 추출** | 자체 상태 + 메서드 | `ws-client.ts` (WSBridgeClient) |
|
||||||
|
|
||||||
## 테스트
|
## 테스트
|
||||||
|
|
||||||
- 테스트 파일 위치: (예: `__tests__/` 또는 `*.test.js`)
|
| 항목 | 위치 | 도구 |
|
||||||
- 테스트 네이밍: `should [expected behavior] when [condition]`
|
|------|------|------|
|
||||||
|
| Python 구문 검사 | `tests/test_syntax.py` | `ast.parse` |
|
||||||
|
| WS Hub 연결 | `tests/test_ws_hub.py` | `websockets` |
|
||||||
|
| TypeScript 컴파일 | `npx tsc --noEmit` | TypeScript compiler |
|
||||||
|
| E2E | 수동 (Discord 버튼 클릭) | — |
|
||||||
|
|
||||||
|
## 로깅
|
||||||
|
|
||||||
|
| 측 | 방식 | 포맷 |
|
||||||
|
|----|------|------|
|
||||||
|
| Python | `logging.getLogger(name)` | `YYYY-MM-DD HH:MM:SS [name] LEVEL: message` |
|
||||||
|
| Extension | `logToFile(msg)` → bridge/log/ | `[HH:MM:SS] message` + `[WS]` prefix |
|
||||||
|
| Hub | `[HUB]` prefix | `[HUB] Auth OK: {conn_id} project={project}` |
|
||||||
|
| Gateway | `[GATEWAY]` prefix | `[GATEWAY] HTTP API started on {host}:{port}` |
|
||||||
|
|
||||||
|
## WS / File Bridge 상호 배타 패턴
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> WS Hub과 파일 bridge는 **항상 상호 배타적**이어야 합니다.
|
||||||
|
> 양쪽에 동시에 쓰면 이중 전달 버그가 발생합니다. (known-issues-archive 참조)
|
||||||
|
|
||||||
|
**Extension (TypeScript):**
|
||||||
|
```typescript
|
||||||
|
// ✅ 올바른 패턴
|
||||||
|
if (ctx.wsBridge?.isConnected()) {
|
||||||
|
ctx.wsBridge.sendPending(data);
|
||||||
|
return; // ← 반드시 return으로 파일 쓰기 건너뛰기
|
||||||
|
}
|
||||||
|
// File fallback
|
||||||
|
fs.writeFileSync(pendingPath, JSON.stringify(data));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Bot (Python):**
|
||||||
|
```python
|
||||||
|
# ✅ 올바른 패턴
|
||||||
|
if self.hub:
|
||||||
|
await self.hub.send_response(...)
|
||||||
|
else:
|
||||||
|
bridge.write_response(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
**금지 패턴:**
|
||||||
|
```python
|
||||||
|
# ❌ 이중 쓰기 — 절대 금지
|
||||||
|
if self.hub:
|
||||||
|
await self.hub.send_response(...)
|
||||||
|
bridge.write_response(...) # ← Hub 성공해도 파일에도 씀 → 이중 처리
|
||||||
|
```
|
||||||
|
|||||||
581
.agents/references/known-issues-archive.md
Normal file
581
.agents/references/known-issues-archive.md
Normal file
@@ -0,0 +1,581 @@
|
|||||||
|
# Known Issues — Archive
|
||||||
|
|
||||||
|
> **해결 완료된 이슈 아카이브입니다.**
|
||||||
|
> 현재 활성 이슈는 [`known-issues.md`](file:///c:/Users/Variet-Worker/Desktop/gravity_control/.agents/references/known-issues.md)를 참조하세요.
|
||||||
|
> 비슷한 문제가 재발할 때 이 문서에서 과거 해결 방법을 검색하세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 포맷
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### [날짜] [키워드] — 한줄 요약
|
||||||
|
- **증상**: 무엇이 잘못되었는가
|
||||||
|
- **원인**: 근본 원인
|
||||||
|
- **해결**: 올바른 해결 방법
|
||||||
|
- **주의**: 재발 방지를 위한 교훈
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 승인 전략 결정 체인 (2026-03-08~09 전체 요약)
|
||||||
|
|
||||||
|
> **이 섹션은 2026-03-08~09 전체 세션의 시행착오를 요약합니다.**
|
||||||
|
> **이미 거부된 접근을 다시 시도하지 마세요.**
|
||||||
|
|
||||||
|
### ❌ 시도 후 거부된 접근 (재시도 금지)
|
||||||
|
|
||||||
|
| # | 접근 | 결과 | 거부 사유 |
|
||||||
|
|---|------|------|-----------|
|
||||||
|
| 1 | LS RPC `HandleCascadeUserInteraction` | `socket hang up` | AG 빌드에서 핸들러 미구현 |
|
||||||
|
| 2 | LS RPC `ResolveOutstandingSteps` | CANCEL 동작 (승인이 아님) | 명칭과 달리 step을 취소함 |
|
||||||
|
| 3 | VS Code 7개 승인 명령 (`terminalCommand.run` 등) | `command not found` | AG 런타임에 미등록 |
|
||||||
|
| 4 | 키보드 시뮬레이션 (`type {Enter}`) | 빈 메시지 전송 | Chat input에 캡처됨 |
|
||||||
|
| 5 | `sendChatActionMessage` / `executeCascadeAction` | 미등록 | 119개 명령에 없음 |
|
||||||
|
| 6 | pywinauto (OS 레벨) | 사용자 결정 폐기 | 크로스플랫폼 불가, 창 겹침 문제 |
|
||||||
|
| 7 | CDP (Chrome DevTools Protocol) | **사용자 명시적 거부** | 비표준, `--remote-debugging-port` 필요 |
|
||||||
|
| 8 | Renderer v1 DOM Click (flat scan) | Run 버튼 미발견 | webview iframe 격리 |
|
||||||
|
|
||||||
|
### 🔑 핵심 전제
|
||||||
|
- Run/Accept 버튼은 **`vscode-webview://` origin의 격리된 iframe** 안에 있음
|
||||||
|
- 외부 workbench DOM에서 `querySelector`로 접근 **불가** (cross-origin)
|
||||||
|
- `<webview>.executeJavaScript()`가 **유일한 표준 관통 경로** (Electron API)
|
||||||
|
- AG 패치 후 **반드시 풀 프로세스 재시작** 필요 (Reload Window 불충분)
|
||||||
|
- **양쪽 HTML (workbench.html + workbench-jetski-agent.html) 모두 inline 패치** 필수
|
||||||
|
- HTML 패치 변경 시 **`%APPDATA%\Antigravity\CachedData` 삭제 필수** (V8 바이트코드 캐시 무효화)
|
||||||
|
- **CSP `script-src`에 `'unsafe-inline'` 패치 필수** (없으면 인라인 스크립트 무조건 차단)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Renderer / HTML 패치 관련 (2026-03-08~09)
|
||||||
|
|
||||||
|
### [2026-03-08] Antigravity Renderer Injection — Electron 캐시 차단
|
||||||
|
- **증상**: workbench.html, workbench-jetski-agent.html에 `<script>` 태그 추가 후 리로드해도 실행되지 않음
|
||||||
|
- **원인**: Electron의 V8 코드 캐시가 수정된 HTML을 무시하고 캐시된 버전을 서빙
|
||||||
|
- **해결**: 렌더러 인젝션 방식 **포기**. Extension Host에서 RPC 폴링 방식으로 전환
|
||||||
|
- **주의**: Antigravity는 `workbench-jetski-agent.html`을 사용 (Jetski = 내부 코드네임)
|
||||||
|
|
||||||
|
### [2026-03-08] Antigravity 승인 대기 = RUNNING (NOT IDLE)
|
||||||
|
- **증상**: IDLE 기반 승인 감지가 실제 승인 대기를 놓침
|
||||||
|
- **원인**: 승인 대기 시 세션 상태가 `CASCADE_RUN_STATUS_RUNNING` (IDLE 아님), `IDLE`은 대화 대기(notify_user 후)
|
||||||
|
- **해결**: `RUNNING + delta=0` (stall) 기반 감지로 전환. 6 polls (30초) 이상 FROZEN 시 pending 생성
|
||||||
|
- **주의**: Thinking/생성 중에도 `RUNNING + delta=0`이 발생 → `lastModifiedTime`으로 구분 시도했으나 불완전
|
||||||
|
|
||||||
|
### [2026-03-08] ResolveOutstandingSteps RPC — 승인이 아닌 취소!
|
||||||
|
- **증상**: Discord 승인 → `ResolveOutstandingSteps` 호출 → step이 취소됨
|
||||||
|
- **원인**: `ResolveOutstandingSteps`는 blocking steps를 "resolve" = **REJECT/CANCEL**, approve가 아님
|
||||||
|
- **해결**: `ResolveOutstandingSteps` 제거. `HandleCascadeUserInteraction`은 `socket hang up`
|
||||||
|
- **주의**: KI에 "more reliable"로 기록되어 있으나 실제 동작은 cancel임. KI 업데이트 필요
|
||||||
|
|
||||||
|
### [2026-03-08] VS Code Accept Commands — Silent Success 문제
|
||||||
|
- **증상**: 4개 accept command 모두 OK(undefined) 반환하나 실제 승인 안 됨
|
||||||
|
- **원인**: webview에 활성 포커스가 필요. `panel.focus()`로는 충분하지 않음
|
||||||
|
- **해결**: **미해결**. Windows UI Automation 등 OS 레벨 접근 필요
|
||||||
|
- **주의**: reject commands는 동작함. accept만 focus 의존성 있음
|
||||||
|
|
||||||
|
### [2026-03-08] Multi-Window 세션 등록 경쟁 조건
|
||||||
|
- **증상**: 이 창(gravity_control)의 대화가 `#ag-variet_agent` 채널로 메시지 전달
|
||||||
|
- **원인**: `writeRegistration()`이 폴링 루프에서 호출 → 먼저 폴링한 확장이 세션을 자기 프로젝트로 등록
|
||||||
|
- **해결**: `writeRegistration`을 폴링에서 제거, `writeChatSnapshot`/`writePendingApproval`에서만 지연 호출
|
||||||
|
- **주의**: `GetAllCascadeTrajectories`는 모든 창의 세션을 반환하므로 세션→창 매핑은 불가능. 활동 기반 등록만 신뢰 가능
|
||||||
|
|
||||||
|
### [2026-03-08] 공유 렌더러 스크립트 파일 덮어쓰기 문제
|
||||||
|
- **증상**: DOM Observer 렌더러 스크립트가 잘못된 HTTP bridge 포트에 연결
|
||||||
|
- **원인**: 두 확장이 동일한 `ag-sdk-variet-gravity-bridge.js` 파일에 각자 포트를 씀 → 마지막 확장 것만 남음
|
||||||
|
- **해결**: `ag-bridge-ports.json`에 모든 확장의 port를 JSON으로 기록, 렌더러가 all ports를 순회하며 ping
|
||||||
|
- **주의**: 렌더러 스크립트 파일 경로는 SDK patcher namespace에 의해 고정 — 변경 불가
|
||||||
|
|
||||||
|
### [2026-03-08] workbench.html vs workbench-jetski-agent.html
|
||||||
|
- **증상**: 렌더러에서 `[GB Observer]` 로그가 전혀 안 나옴
|
||||||
|
- **원인**: DevTools가 `workbench.html`을 로드 — 스크립트 태그는 `workbench-jetski-agent.html`에만 패치됨
|
||||||
|
- **해결**: `workbench.html`에도 스크립트 태그 필요. Antigravity 재설치 후 SDK patcher가 올바르게 패치하도록 함
|
||||||
|
- **주의**: SDK patcher는 `both` HTML 파일을 패치하지만, 수동 수정은 Antigravity integrity check에 의해 되돌려질 수 있음
|
||||||
|
|
||||||
|
### [2026-03-08] product.json 체크섬 불일치 → 렌더러 스크립트 미로딩
|
||||||
|
- **증상**: `<script>` 태그가 HTML에 존재하고 .js 파일도 디스크에 있으나, 렌더러 콘솔에 스크립트 로그가 전혀 없음
|
||||||
|
- **원인**: Antigravity 재설치 시 `product.json`의 SHA256 체크섬이 원본으로 리셋됨. Extension이 HTML을 패치하지만 `IntegrityManager.suppressCheck()`를 호출하지 않아 체크섬 불일치. `vscode-file://` 프로토콜이 체크섬 불일치 파일을 무시하고 **원본 캐시 HTML**을 서빙
|
||||||
|
- **해결**: `product.json`의 `checksums` 항목에서 수정된 파일(workbench.html, workbench-jetski-agent.html)의 SHA256 해시를 실제 파일 기준으로 업데이트. SDK `IntegrityManager.suppressCheck()` 호출 또는 수동 스크립트로 해결
|
||||||
|
- **주의**: Extension `setupApprovalObserver()`에 `suppressCheck()` 호출을 영구 추가해야 재설치마다 반복 안 됨. 해시 = `base64(sha256(file)).replace(/=+$/, '')`
|
||||||
|
|
||||||
|
### [2026-03-08] vscode-file:// 프로토콜 — 커스텀 .js 파일 서빙 불가
|
||||||
|
- **증상**: `<script src="./ag-sdk-variet-gravity-bridge.js">` 태그가 HTML에 있으나 `net::ERR_FILE_NOT_FOUND` 발생, GB Observer 로그 전혀 없음
|
||||||
|
- **원인**: `vscode-file://` 프로토콜은 원본 배포에 포함된 파일만 서빙. Extension이 디스크에 쓴 커스텀 `.js` 파일은 프로토콜 레벨에서 차단됨
|
||||||
|
- **해결**: 외부 `<script src>` 참조 대신 **인라인 `<script>...코드...</script>`** 방식으로 HTML에 직접 삽입
|
||||||
|
- **주의**: `ag-bridge-ports.json`도 같은 이유로 XHR 로딩 불가. 모든 렌더러 스크립트/데이터는 HTML 인라인으로 전달해야 함
|
||||||
|
|
||||||
|
### [2026-03-08] Renderer 포트 디스커버리 — ag-bridge-ports.json XHR 실패
|
||||||
|
- **증상**: `[GB Observer] Port discovery timeout after 2min` — 렌더러가 bridge 포트를 찾지 못함
|
||||||
|
- **원인**: 렌더러 스크립트가 `./ag-bridge-ports.json`을 동기 XHR로 읽으려 하나, `vscode-file://` 프로토콜이 `.json` 파일 서빙 거부
|
||||||
|
- **해결**: (1) 프로젝트명 해시 기반 **결정론적 포트** 사용 (`gravity_control→34332`), (2) 스크립트 생성 시 포트를 `HARDCODED_PORT=${port}`로 직접 삽입
|
||||||
|
- **주의**: `server.listen(0)` 랜덤 포트 → 매 재시작마다 변경되어 렌더러와 불일치. 결정론적 포트는 `EADDRINUSE` 시 랜덤 폴백 필요
|
||||||
|
|
||||||
|
### [2026-03-08] GetCascadeTrajectorySteps — cascadeId 파라미터 발견
|
||||||
|
- **증상**: `GetCascadeTrajectorySteps`에 `trajectoryId` 파라미터 → 500 "trajectory not found"
|
||||||
|
- **원인**: 파라미터명이 `trajectoryId`가 아니라 **`cascadeId`**. 값은 `GetAllCascadeTrajectories.trajectorySummaries`의 맵 키(세션 ID)
|
||||||
|
- **해결**: `{ cascadeId: sessionId }`로 호출 → 전체 step 배열 반환 성공
|
||||||
|
- **주의**: `latestToolCallStep` 필드는 `GetAllCascadeTrajectories` 응답에 **존재하지 않음** (KI 오류)
|
||||||
|
|
||||||
|
### [2026-03-08] Step 구조 — CORTEX_STEP_STATUS_WAITING 즉시 감지
|
||||||
|
- **증상**: stall-based 감지(100초)가 너무 느림
|
||||||
|
- **원인**: 이제 `GetCascadeTrajectorySteps`로 최신 step의 status를 직접 확인 가능
|
||||||
|
- **해결**: stall 5초 후 step probe → `CORTEX_STEP_STATUS_WAITING` 확인 → 즉시 pending 생성
|
||||||
|
- **Step 구조**: `{type: "CORTEX_STEP_TYPE_RUN_COMMAND", status: "CORTEX_STEP_STATUS_WAITING", metadata: {toolCall: {name, argumentsJson}}, runCommand, requestedInteraction}`
|
||||||
|
- **주의**: 775-step 하드 리밋은 여전히 존재. 긴 세션에서는 fallback(40초) 사용
|
||||||
|
|
||||||
|
### [2026-03-08] Extension 재설치 안전성 — 자동 패치 메커니즘
|
||||||
|
- **증상**: Antigravity 삭제 후 재설치 시 렌더러 스크립트가 동작 안 함
|
||||||
|
- **원인**: 재설치 시 HTML/product.json이 원본으로 리셋됨
|
||||||
|
- **해결**: Extension의 `setupApprovalObserver()`가 **자동으로** 모든 패치를 수행
|
||||||
|
- **주의**: 패치 후 반드시 **Antigravity 풀 재시작** 필요 (Reload Window 불가)
|
||||||
|
|
||||||
|
### [2026-03-08] Response 파일 Race Condition — DOM Observer 승인 실패
|
||||||
|
- **증상**: Discord에서 승인 → `[RESPONSE] renderer-handled approval` 로그 출력 → 실제 버튼 클릭 안 됨
|
||||||
|
- **원인**: `processResponseFile` (파일 감시자)이 response 파일을 즉시 삭제 → renderer의 `pollResponse`가 HTTP `GET /response/:rid`로 조회 시 파일 이미 없음
|
||||||
|
- **해결**: DOM observer 소스일 때는 response 파일을 삭제하지 않도록 수정. HTTP endpoint가 renderer에게 서빙한 후 삭제
|
||||||
|
- **주의**: non-DOM (stall/step_probe relay)는 watcher에서 삭제해도 됨
|
||||||
|
|
||||||
|
### [2026-03-08] Renderer 스크립트 소스 혼동 — 3곳의 코드
|
||||||
|
- **증상**: `extension.ts`에 BTN-DUMP 추가 → Reload 2번 → 콘솔에 안 나옴
|
||||||
|
- **원인**: renderer 코드가 **3곳**에 존재: (1) `extension.ts`의 `generateApprovalObserverScript()` (소스), (2) `ag-sdk-variet-gravity-bridge.js` (배포됨, Reload시 소스에서 재생성), (3) `workbench-jetski-agent.html` inline (HTML, JS파일과 중복로드 방지됨)
|
||||||
|
- **해결**: 항상 `extension.ts`의 `generateApprovalObserverScript()` 함수를 수정 → 컴파일 → 배포 → Reload
|
||||||
|
- **주의**: HTML inline은 JS파일이 먼저 로드되어 `window.__agSDK` 가드에 의해 실행 안 됨. 실제 실행되는 것은 JS파일 경로의 스크립트
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 승인 / RPC 관련 (2026-03-09)
|
||||||
|
|
||||||
|
### [2026-03-09] VS Code Accept — SDK 승인 명령이 AG에 미등록
|
||||||
|
- **증상**: Discord 승인 → `antigravity.terminalCommand.run` 등 7개 명령 → 모두 `command not found`
|
||||||
|
- **원인**: SDK(command-bridge.ts)에 정의된 7개 승인 명령이 현재 AG 빌드에 **등록되어 있지 않음**
|
||||||
|
- **해결**: Renderer DOM Click 구현 → v3 `deepFindButtons()` 업그레이드
|
||||||
|
- **주의**: `agentPanel.focus`도 미등록, `agentSidePanel.focus`만 존재
|
||||||
|
|
||||||
|
### [2026-03-09] Renderer DOM — webview iframe 격리 확인 + v3 deep traversal
|
||||||
|
- **증상**: Renderer trigger-click이 `document.querySelectorAll('button')`으로 버튼 검색 → Run 버튼 미발견
|
||||||
|
- **원인**: Run/Accept 버튼은 AG 채팅 webview iframe (`vscode-webview://` origin) 안에 렌더링
|
||||||
|
- **해결**: Renderer v3 `deepFindButtons()` 구현 (iframe contentDocument + webview.executeJavaScript + shadow DOM)
|
||||||
|
- **주의**: CDP(Chrome DevTools Protocol)는 **사용자 결정에 의해 명시적으로 거부됨**
|
||||||
|
|
||||||
|
### [2026-03-09] Deep Inspect HTTP Endpoint — curl로 DOM 분석 트리거
|
||||||
|
- **증상**: AG DevTools 콘솔에 붙여넣기 불가 (Chromium 보안 정책)
|
||||||
|
- **원인**: Electron 렌더러 DevTools에서 `allow pasting`이 SyntaxError 발생
|
||||||
|
- **해결**: Extension HTTP bridge에 `/deep-inspect` 엔드포인트 추가
|
||||||
|
- **주의**: 시작 3초 후 자동 실행 + curl로 재트리거 가능
|
||||||
|
|
||||||
|
### [2026-03-09] workbench.html inline 스크립트 미삽입 — jetski만 패치한 버그
|
||||||
|
- **증상**: AG 재시작 후 `/deep-inspect` timeout — renderer v3 스크립트 미로딩
|
||||||
|
- **원인**: `setupApprovalObserver()`가 `workbench-jetski-agent.html`에만 inline 삽입
|
||||||
|
- **해결**: HTML 패치 로직을 **양쪽 모두 inline** 삽입으로 변경
|
||||||
|
- **주의**: **항상 양쪽 HTML을 동일하게 패치**
|
||||||
|
|
||||||
|
### [2026-03-09] V8 CachedData — 체크섬 정상이어도 스크립트 미실행
|
||||||
|
- **증상**: HTML 패치 + product.json 체크섬 일치 → 그런데도 renderer 스크립트 미실행
|
||||||
|
- **원인**: V8 바이트코드 캐시가 `vscode-file://` 프로토콜에도 적용
|
||||||
|
- **해결**: `%APPDATA%\Antigravity\CachedData\*` 전체 삭제 후 AG 풀 재시작
|
||||||
|
- **주의**: **HTML 패치 변경 시마다 CachedData 삭제 필수**
|
||||||
|
|
||||||
|
### [2026-03-09] CSP script-src — 인라인 스크립트 무조건 차단
|
||||||
|
- **증상**: HTML 패치 ✅, 체크섬 ✅, CachedData 삭제 ✅ — 그런데도 renderer 미실행
|
||||||
|
- **원인**: CSP `script-src`에 `'unsafe-inline'` 없음
|
||||||
|
- **해결**: CSP `script-src`에 `'unsafe-inline'` 추가
|
||||||
|
- **주의**: `style-src`에는 `'unsafe-inline'`이 있어 스타일은 동작 → 스크립트만 차단되는 것이 함정
|
||||||
|
|
||||||
|
### [2026-03-09] 중복 승인 요청 — DOM scan + Step probe 동시 발동
|
||||||
|
- **증상**: Discord에 같은 명령에 대해 승인 요청이 2개 도착
|
||||||
|
- **원인**: DOM Observer + Step probe가 동일 step에 대해 2개 파일 생성
|
||||||
|
- **해결**: `writePendingApproval()`에 15초 dedup 윈도우 추가
|
||||||
|
- **주의**: DOM scan은 제거 불가 — step probe가 감지 못하는 UI 버튼 존재
|
||||||
|
|
||||||
|
### [2026-03-09] Step probe reject → ResolveOutstandingSteps가 AI 작업 취소
|
||||||
|
- **증상**: Discord에서 거부 클릭 → AI의 현재 step뿐 아니라 진행 중인 작업 전체가 중단
|
||||||
|
- **원인**: step probe 경로의 `tryApprovalStrategies(approved=false)` → `ResolveOutstandingSteps` RPC 호출
|
||||||
|
- **해결**: step probe 경로에서 reject 시 `tryApprovalStrategies` 호출 제거
|
||||||
|
- **주의**: `ResolveOutstandingSteps`는 이름과 달리 "해결"이 아닌 "취소". 승인에 **절대 사용 금지**
|
||||||
|
|
||||||
|
### [2026-03-09] Pending 파일 무한 누적 — write_response 후 미삭제
|
||||||
|
- **증상**: `bridge/pending/` 디렉토리에 79개 이상의 .json 파일 누적
|
||||||
|
- **원인**: `write_response()`가 pending 파일을 삭제하지 않음
|
||||||
|
- **해결**: pending 파일 삭제 + 5분 age filter + 시작 시 cleanup
|
||||||
|
- **주의**: 봇 재시작 시 자동 정리
|
||||||
|
|
||||||
|
### [2026-03-09] Discord 승인 "Run" 표시 — DOM/step_probe 타이밍 불일치
|
||||||
|
- **증상**: Discord에 상세 명령어 대신 "Run"만 표시
|
||||||
|
- **원인**: DOM observer가 "Run" pending 생성 → 봇이 3초 후 전송 → step_probe MERGE가 10초 후 완료
|
||||||
|
- **해결**: step_probe가 기존 DOM pending에 MERGE + 봇에서 짧은 명령어 대기
|
||||||
|
- **주의**: MERGE 타이밍은 최소 10초
|
||||||
|
|
||||||
|
### [2026-03-09] DOM observer false positive — Proceed/Continue/Open 버튼 오감지
|
||||||
|
- **증상**: 작업 전환 시 승인 요청 없는데도 Discord에 승인 요청 도착
|
||||||
|
- **원인**: DOM observer가 AG UI의 PathsToReview 버튼을 승인 버튼으로 오인
|
||||||
|
- **해결**: FALSE_POSITIVE_RE 필터 추가 + sessionStalled 조건
|
||||||
|
- **주의**: 렌더러 인라인 스크립트는 VSIX 빌드 필요
|
||||||
|
|
||||||
|
### [2026-03-09] Discord ApprovalView timeout — 5분 후 버튼 무응답
|
||||||
|
- **증상**: 시간이 지난 후 Discord 승인 버튼 클릭해도 반응 없음
|
||||||
|
- **원인**: Discord.py View의 기본 timeout이 300초
|
||||||
|
- **해결**: timeout을 1800초로 증가
|
||||||
|
- **주의**: Discord View timeout은 서버 재시작 후만 적용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 멀티 프로젝트 / Pending 관련 (2026-03-10)
|
||||||
|
|
||||||
|
### [2026-03-10] DOM Observer 승인 ENOENT — response 파일 Race Condition
|
||||||
|
- **증상**: Discord에서 ✅승인 → `ENOENT: response/xxx.json` 에러
|
||||||
|
- **원인**: `processResponseFile()`이 response 파일 즉시 삭제 → renderer 조회 실패
|
||||||
|
- **해결**: DOM observer 경로에서 response 파일을 삭제하지 않고 HTTP handler가 서빙 후 삭제
|
||||||
|
- **주의**: DOM observer와 step_probe 두 경로가 독립적
|
||||||
|
|
||||||
|
### [2026-03-10] Allow Once + Allow This Conversation — 별개 pending으로 분리되는 문제
|
||||||
|
- **증상**: 파일 접근 시 Discord에 2개 별도 메시지로 도착
|
||||||
|
- **원인**: renderer `scan()`이 한 사이클에 한 버튼만 처리
|
||||||
|
- **해결**: `findButtonContainer()` + `collectSiblingButtons()`로 그룹화, `buttons` 배열 전송
|
||||||
|
- **주의**: `buttons` 배열이 없는 legacy pending은 기존 2버튼(✅승인/❌거부)으로 표시
|
||||||
|
|
||||||
|
### [2026-03-10] step_probe verbosity — argumentsJson 미포함
|
||||||
|
- **증상**: Discord 승인 메시지에 파라미터 이름만 표시, 값 없음
|
||||||
|
- **원인**: `GetCascadeTrajectorySteps` 기본 verbosity에서 `argumentsJson` 빈 문자열
|
||||||
|
- **해결**: `verbosity: 1` (DEBUG) 추가
|
||||||
|
- **주의**: verbosity 0=NORMAL (키만), 1=DEBUG (값 포함)
|
||||||
|
|
||||||
|
### [2026-03-10] 파일 권한 응답 — "unexpected user interaction type: not file permission"
|
||||||
|
- **증상**: `.agents` 디렉토리 접근 시 에러
|
||||||
|
- **원인**: 봇이 file_permission pending에 잘못된 interaction type 전송
|
||||||
|
- **해결**: step_type별 올바른 RPC 라우팅
|
||||||
|
- **주의**: file_permission과 run_command가 동시에 대기 시 올바른 RPC 라우팅 필수
|
||||||
|
|
||||||
|
### [2026-03-10] active_project.lock — 멀티 프로젝트 동시 사용 차단
|
||||||
|
- **증상**: 여러 AG 프로젝트 실행 시 첫 번째만 bridge 연결
|
||||||
|
- **원인**: `active_project.lock` 파일이 단일 프로젝트만 허용
|
||||||
|
- **해결**: lock 메커니즘 완전 제거, `project_name` 필터 기반으로 전환
|
||||||
|
- **주의**: bridge 격리는 `project_name` 필드 기반 filtering으로 충분
|
||||||
|
|
||||||
|
### [2026-03-10] step_probe file_permission — 3-button 미주입
|
||||||
|
- **증상**: Discord에 3개 선택지 대신 2개만 표시
|
||||||
|
- **원인**: `writePendingApproval()`이 buttons 배열 미주입
|
||||||
|
- **해결**: `step_type === 'file_permission'`일 때 자동 3-button 배열 주입
|
||||||
|
- **주의**: DOM observer 경로는 기존 command 텍스트 기반 감지 유지
|
||||||
|
|
||||||
|
### [2026-03-10] GetAllCascadeTrajectories — 크로스 윈도우 세션 가로채기
|
||||||
|
- **증상**: Deriva AG에서 대화 시작 → gravity_control 채널에 Deriva 내용이 릴레이
|
||||||
|
- **원인**: `GetAllCascadeTrajectories`가 모든 인스턴스의 세션 반환
|
||||||
|
- **해결**: `workspaces[0].workspaceFolderAbsoluteUri` 비교하여 자기 workspace 세션만 처리
|
||||||
|
- **주의**: workspace URI normalize 필수 (protocol strip, %3A decode, 슬래시 통일, lowercase)
|
||||||
|
|
||||||
|
### [2026-03-10] 크로스 프로젝트 Response Watcher 우회
|
||||||
|
- **증상**: Deriva 세션 승인 시도가 gravity_control에서 실패
|
||||||
|
- **원인**: pending 파일 삭제 후 response watcher가 project_name 체크 건너뜀
|
||||||
|
- **해결**: response JSON의 project_name으로 fallback 필터
|
||||||
|
- **주의**: response 데이터 자체에 project_name 필수
|
||||||
|
|
||||||
|
### [2026-03-10] file_permission — write 도구 3-button 미주입
|
||||||
|
- **증상**: `replace_file_content` 등 파일 수정 시 Discord에 2개만 표시
|
||||||
|
- **원인**: step_probe의 file_permission 도구 리스트에 write 도구 누락
|
||||||
|
- **해결**: write 도구를 file_permission 리스트에 추가
|
||||||
|
- **주의**: AG가 파일 접근 권한을 요청하는 모든 도구는 이 리스트에 포함필요
|
||||||
|
|
||||||
|
### [2026-03-10] bestSession IDLE 고착 — RUNNING 세션 못 잡는 버그
|
||||||
|
- **증상**: 새 대화 시작 → bridge가 구 IDLE 세션만 추적
|
||||||
|
- **원인**: `bestSession` 선택이 `lastModifiedTime`만 비교
|
||||||
|
- **해결**: RUNNING 세션이 IDLE보다 항상 우선
|
||||||
|
- **주의**: Reload Window로도 해결되지만 근본적으로는 RUNNING 우선 로직 필요
|
||||||
|
|
||||||
|
### [2026-03-10] Bot IDLE 채널 자동 생성 — 불필요한 Discord 채널 증식
|
||||||
|
- **증상**: 봇 시작 시 모든 등록된 프로젝트의 채널을 자동 생성
|
||||||
|
- **원인**: `pending_approval_scanner`가 매 사이클마다 채널 생성
|
||||||
|
- **해결**: 자동 채널 생성 루프 제거, on-demand 생성
|
||||||
|
- **주의**: `_get_channel()`은 이미 on-demand 생성 로직 포함
|
||||||
|
|
||||||
|
### [2026-03-10] Reload Window 후 세션 stale
|
||||||
|
- **증상**: Reload Window 후 세션이 IDLE/구 stepCount로 고정
|
||||||
|
- **원인**: LS 프로세스는 유지되어 trajectory tracker 캐시 미갱신
|
||||||
|
- **해결**: AG 완전 종료 → 재실행 (Full restart)
|
||||||
|
- **주의**: Extension 코드 변경 후 배포 시 Full restart 권장
|
||||||
|
|
||||||
|
### [2026-03-10] start_bot.bat — Windows Store Python 스텁 우선 실행
|
||||||
|
- **증상**: `start_bot.bat` 실행 시 스텁이 먼저 실행
|
||||||
|
- **원인**: `where python`이 Windows Store의 Python 스텁을 먼저 찾음
|
||||||
|
- **해결**: conda 경로를 우선 확인
|
||||||
|
- **주의**: Windows 10/11에서 App Aliases의 python.exe가 PATH에 기본 포함
|
||||||
|
|
||||||
|
### [2026-03-10] VSIX 빌드 — SDK JS 파일 미포함 (require 실패)
|
||||||
|
- **증상**: Extension 활성화 후 `SDK not initialized`
|
||||||
|
- **원인**: TypeScript 컴파일러가 `.js` 파일을 `out/`에 복사하지 않음
|
||||||
|
- **해결**: `compile` 스크립트에 복사 단계 추가 (`src/sdk/` → `out/sdk/`)
|
||||||
|
- **주의**: VSIX 패키징은 `out/sdk/`를 포함함. 문제는 빌드 단계 복사 누락
|
||||||
|
|
||||||
|
### [2026-03-10] SDK _findLSProcess — 대소문자 구분 workspace hint 매칭 실패
|
||||||
|
- **증상**: variet-agent AG에서 Discord에 신호 미도달
|
||||||
|
- **원인**: SDK가 workspace hint를 대소문자 구분으로 비교
|
||||||
|
- **해결**: `fixLSConnection()` 함수로 대소문자 무시 비교 + 재연결
|
||||||
|
- **주의**: 각 AG 창마다 별도 LS 프로세스 존재 (workspace_id로 구분)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 환경변수 / 설정 관련 (2026-03-11)
|
||||||
|
|
||||||
|
### [2026-03-11] config.py BRAIN_PATH — `.env` 빈 문자열 → CWD 해석 버그
|
||||||
|
- **증상**: 봇이 Extension의 snapshot/pending을 전혀 읽지 못함
|
||||||
|
- **원인**: `.env`에 `BRAIN_PATH=` (빈 값)이면 빈 문자열 반환
|
||||||
|
- **해결**: `os.getenv("BRAIN_PATH") or default` 패턴
|
||||||
|
- **주의**: `os.getenv(key, default)`는 빈 값이라도 default 미사용
|
||||||
|
|
||||||
|
### [2026-03-11] Extension DEDUP MERGE — 크로스 프로젝트 pending 오염
|
||||||
|
- **증상**: `#ag-lifetimepd` 채널에 variet_agent의 승인 요청 표시
|
||||||
|
- **원인**: DEDUP 로직이 `project_name`을 체크하지 않음
|
||||||
|
- **해결**: 3곳 dedup 조건에 `project_name` 가드 추가
|
||||||
|
- **주의**: 모든 Extension 인스턴스가 동일한 `bridge/pending/` 디렉토리 공유
|
||||||
|
|
||||||
|
### [2026-03-11] Collector 동기 HTTP — aiohttp 전환
|
||||||
|
- **증상**: Collector가 이벤트 루프 전체 블로킹
|
||||||
|
- **원인**: `urllib.request.urlopen()` 사용 (blocking I/O)
|
||||||
|
- **해결**: `aiohttp.ClientSession` 기반 비동기 전환
|
||||||
|
- **주의**: `import aiohttp`는 lazy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rate Limit / 무한 루프 관련 (2026-03-12)
|
||||||
|
|
||||||
|
### [2026-03-12] RemoteTransport 429 무한 루프 — Extension 크래시 + AG 먹통
|
||||||
|
- **증상**: `429 Rate limited` 로그가 초당 수십 건 무한 반복
|
||||||
|
- **원인**: 3가지 복합 (백오프 없음 + 개별 HTTP 요청 + 공격적 rate limit)
|
||||||
|
- **해결**: 지수 백오프 + `Retry-After` 지원 + rate limit 완화
|
||||||
|
- **주의**: AG 먹통은 봇 자체가 유발한 문제
|
||||||
|
|
||||||
|
### [2026-03-12] workbench.html 0-byte 파괴 — AG 새 창 먹통
|
||||||
|
- **증상**: AG 새 창 열면 화면 먹통
|
||||||
|
- **원인**: 3개 Extension 인스턴스가 동시에 workbench.html 읽기/쓰기 → 0 bytes로 덮어쓰기
|
||||||
|
- **해결**: pre-patch backup(.orig) + 구조 검증 + 자동 복원
|
||||||
|
- **주의**: 멀티 윈도우 환경에서 HTML 패치 race condition은 파일 잠금 없이 완전 해결 불가
|
||||||
|
|
||||||
|
### [2026-03-12] workbench.html 크로스 복원 — CSS 미로딩으로 레이아웃 깨짐
|
||||||
|
- **증상**: 아이콘은 보이지만 레이아웃 완전 깨짐
|
||||||
|
- **원인**: workbench.html을 jetski HTML에서 복원할 때 CSS 교체 누락
|
||||||
|
- **해결**: 파일별 `requiredMarker` 검증 + `.orig` 백업 + 자동 복원
|
||||||
|
- **주의**: **workbench.html과 workbench-jetski-agent.html은 교환 불가능**
|
||||||
|
|
||||||
|
### [2026-03-12] Collector 단일 프로젝트 폴링 — 멀티 프로젝트 command 전달 불가
|
||||||
|
- **증상**: Deriva AG IDE에 명령 전달되지 않음
|
||||||
|
- **원인**: Collector가 단일 프로젝트만 폴링
|
||||||
|
- **해결**: `_discover_local_projects()`로 모든 프로젝트 폴링
|
||||||
|
- **주의**: `/api/commands/all` 엔드포인트는 크로스 PC 명령 오염을 유발
|
||||||
|
|
||||||
|
### [2026-03-12] RemoteTransport backing off 무한 반복
|
||||||
|
- **증상**: IDLE 시 `backing off 1s` 경고가 영구 반복
|
||||||
|
- **원인**: 3가지 구조적 결함 (즉시 리셋 + 불필요 요청 + asyncio burst)
|
||||||
|
- **해결**: 연속 5회 성공 후 절반 감소 + adaptive 간격 + 루프 stagger
|
||||||
|
- **주의**: `_reset_backoff()` 즉시 리셋 패턴은 다중 소비자 환경에서 **절대 사용 금지**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DEDUP / 크로스 세션 관련 (2026-03-15)
|
||||||
|
|
||||||
|
### [2026-03-15] DEDUP step_index 크로스 세션 충돌 — 승인 신호 누락
|
||||||
|
- **증상**: WAITING step 감지 → pending 미생성 → 10분+ 대기
|
||||||
|
- **원인**: DEDUP 로직이 `conversation_id`를 비교하지 않음
|
||||||
|
- **해결**: DEDUP 조건에 `conversation_id` 가드 추가
|
||||||
|
- **주의**: `project_name` 가드만으로는 불충분 — 같은 Extension이 여러 세션을 볼 수 있음
|
||||||
|
|
||||||
|
### [2026-03-15] Discord Gateway MESSAGE_CREATE 중복 — embed 이중 전송
|
||||||
|
- **증상**: Discord 명령 시 동일 embed가 2개 전송
|
||||||
|
- **원인**: Discord Gateway가 WebSocket 불안정 시 이벤트 중복 전달
|
||||||
|
- **해결**: `on_message`에 `_processed_message_ids` dedup 추가
|
||||||
|
- **주의**: Gateway reconnection, RESUME 실패 시 발생 빈도 증가
|
||||||
|
|
||||||
|
### [2026-03-15] HTML 패치 멀티 인스턴스 race condition — 화면 파괴
|
||||||
|
- **증상**: Extension 패치 후 AG 재시작 시 전체 화면 날아감
|
||||||
|
- **원인**: 2+ Extension 인스턴스가 동시에 같은 HTML에 readFileSync/writeFileSync
|
||||||
|
- **해결**: `.patch-lock` 파일 기반 cross-instance lock 추가
|
||||||
|
- **주의**: Lock은 "방지", .orig 백업은 "복구". 둘 다 유지
|
||||||
|
|
||||||
|
### [2026-03-15] 로컬 승인 ↔ Discord 승인 교차 race condition
|
||||||
|
- **증상**: AG에서 직접 Run 클릭 후 Discord 승인 요청이 "완료됨" 표시 안 됨
|
||||||
|
- **원인**: auto_resolve가 Discord에 알림 없음 + processResponseFile 상태 미체크
|
||||||
|
- **해결**: writeChatSnapshot 추가 + 상태 확인 후 skip + _approval_messages dict
|
||||||
|
- **주의**: processResponseFile L2534의 리셋이 핵심 gate
|
||||||
|
|
||||||
|
### [2026-03-15] 크로스 프로젝트 DEDUP MERGE — Deriva→gravity_control 오염
|
||||||
|
- **증상**: Deriva의 데이터가 gravity_control pending에 MERGE됨
|
||||||
|
- **원인**: MERGE 조건에 `project_name` 가드 없음
|
||||||
|
- **해결**: MERGE 조건에 `project_name` 추가
|
||||||
|
- **주의**: `bridge/pending/` 디렉토리는 모든 Extension 인스턴스가 공유
|
||||||
|
|
||||||
|
### [2026-03-15] Double-Fire Auto-Approve — AI 세션 중단
|
||||||
|
- **증상**: auto-approve ON 시 AI 세션이 간헐적으로 중단
|
||||||
|
- **원인**: Extension auto-approve 경로 + Bot auto-approve 경로 동시 실행 → 2번 RPC
|
||||||
|
- **해결**: Extension auto-approve 경로 제거. Bot만 담당
|
||||||
|
- **주의**: 단일 경로 원칙 유지
|
||||||
|
|
||||||
|
### [2026-03-15] DOM Observer "Deny" False Positive — Auto-approve 세션 크래시
|
||||||
|
- **증상**: auto-approve ON 시 "Deny" command가 자동 승인됨 → 세션 크래시
|
||||||
|
- **원인**: DOM observer가 Deny를 독립 pending으로 생성. default 분기로 잘못된 RPC 전송
|
||||||
|
- **해결**: FALSE_POSITIVE_RE에 Deny/Allow Once 등 추가 + reject-word 차단 가드
|
||||||
|
- **주의**: VSIX 빌드 → AG 풀 재시작 필요
|
||||||
|
|
||||||
|
### [2026-03-15] PATS 배열 Deny 트리거 — 근본 수정
|
||||||
|
- **증상**: Deny가 주 트리거로 사용됨
|
||||||
|
- **원인**: PATS 배열에 Deny 패턴 포함
|
||||||
|
- **해결**: PATS에서 거절/보조 버튼 제거. 긍정 버튼만 그룹 트리거
|
||||||
|
- **주의**: PATS = "그룹 생성 트리거", ALL_ACTION_RE = "형제 수집 패턴"
|
||||||
|
|
||||||
|
### [2026-03-15] Auto-Resolved 채팅 폭주 — 루프 내 writeChatSnapshot
|
||||||
|
- **증상**: "✅ AG에서 직접 승인됨" 메시지가 반복 전송
|
||||||
|
- **원인**: 루프 내부에서 매 파일마다 writeChatSnapshot 호출
|
||||||
|
- **해결**: 루프 바깥에서 1회 + conversation_id 조건 추가
|
||||||
|
- **주의**: 외부 시스템에 메시지 보낼 때는 반드시 루프 바깥에서 집계 후 1회 발송
|
||||||
|
|
||||||
|
### [2026-03-15] projectName=default 승인 오발
|
||||||
|
- **증상**: workspace 없는 AG 창이 다른 프로젝트의 WAITING을 감지
|
||||||
|
- **원인**: `detectProjectName()`이 workspace 없으면 "default" 반환
|
||||||
|
- **해결**: `projectName === 'default'`이면 pending 생성/auto-approve 억제
|
||||||
|
- **주의**: Empty Window에서는 bridge 기능을 최소화
|
||||||
|
|
||||||
|
### [2026-03-15] 이전 분석 오판(False Positive) — 교훈
|
||||||
|
- **증상**: P0/P1으로 보고한 문제들이 이미 방어되고 있었음
|
||||||
|
- **원인**: 로컬 코드 스니펫만 보고 판단
|
||||||
|
- **해결**: 전체 Flow 추적으로 교차 검증
|
||||||
|
- **주의**: **코드 감사 시 반드시 producer→transport→consumer→side effects 전체 경로를 추적**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## processResponseFile 상태 관리 (2026-03-16)
|
||||||
|
|
||||||
|
### [2026-03-16] processResponseFile 상태 리셋 — 무한 루프 vs auto_resolve 회귀
|
||||||
|
- **증상**: Discord 승인 후 같은 step에 대해 pending이 반복 생성 → 무한 auto-approve 루프
|
||||||
|
- **원인**: processResponseFile이 무조건 리셋 → step_probe가 같은 WAITING step을 새 step으로 착각
|
||||||
|
- **해결**: `sawRunningAfterPending = true`만 설정. lastPendingStepIndex와 stallProbed 유지
|
||||||
|
- **주의**: **processResponseFile의 상태 리셋은 sawRunningAfterPending = true만 설정**. `docs/approval-flow.md` 참조
|
||||||
|
|
||||||
|
### [2026-03-16] recentPendingSteps 메모리 dedup
|
||||||
|
- **증상**: pending 파일 삭제 → 같은 step_index로 새 pending 생성
|
||||||
|
- **원인**: writePendingApproval()의 dedup이 파일 존재 여부에만 의존
|
||||||
|
- **해결**: `recentPendingSteps` Map (TTL 60초) 추가
|
||||||
|
- **주의**: DOM observer HTTP 경로는 이 메모리 dedup 미적용
|
||||||
|
|
||||||
|
### [2026-03-16] 멀티 프로젝트 동시 신호 정지 — Scanner O(N) Discord API 병목
|
||||||
|
- **증상**: 여러 프로젝트 동시 pending → 모든 프로젝트 신호 전달 정지
|
||||||
|
- **원인**: scanner가 1 tick에 모든 pending 순차 처리 → Discord 429 rate limit
|
||||||
|
- **해결**: `discord.utils.get(guild.channels)` 캐시 + per-tick cap (5건)
|
||||||
|
- **주의**: `guild.channels`는 discord.py 내부 캐시
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Diff Review 관련 (2026-03-16)
|
||||||
|
|
||||||
|
### [2026-03-16] step_type 매핑 버그 — write_to_file이 file_permission으로 잘못 매핑
|
||||||
|
- **증상**: 코드 편집 승인 시 잘못된 RPC 전송
|
||||||
|
- **원인**: 쓰기 도구가 읽기 도구와 함께 `file_permission`으로 매핑
|
||||||
|
- **해결**: 읽기/쓰기 도구 분리, 쓰기는 `code_edit` step_type 사용
|
||||||
|
- **주의**: AG는 대부분 파일 쓰기에 WAITING 안 만듦
|
||||||
|
|
||||||
|
### [2026-03-16] diff_review isDirty 실패 — AG diff는 VS Code dirty 아님
|
||||||
|
- **증상**: Accept 클릭 → `isDirty` 문서 0개 → 효과 없음
|
||||||
|
- **원인**: AG stacked code review는 VS Code `isDirty`와 무관
|
||||||
|
- **해결**: `AcknowledgeCascadeCodeEdit` RPC → fallback으로 VS Code 커맨드
|
||||||
|
- **주의**: diff_review pending에 `modified_files`와 `edit_step_indices` 필수
|
||||||
|
|
||||||
|
### [2026-03-16] diff_review pending 순서 — AI 응답보다 먼저 Discord 도착
|
||||||
|
- **증상**: diff_review 버튼이 먼저, AI 응답 텍스트가 나중
|
||||||
|
- **원인**: pending_approval_scanner가 chat_snapshot_scanner보다 먼저 fire
|
||||||
|
- **해결**: diff_review pending 생성을 `setTimeout(8000)`으로 지연
|
||||||
|
- **주의**: 8초는 전체 전파 경로 고려
|
||||||
|
|
||||||
|
### [2026-03-16] diff_review AcknowledgeCascadeCodeEdit steps=[] — Collector pending 삭제 race
|
||||||
|
- **증상**: Accept all 클릭 → RPC SUCCESS → diff review 바 안 사라짐
|
||||||
|
- **원인**: Collector가 pending 파일 즉시 삭제 → Extension이 메타데이터 못 읽음
|
||||||
|
- **해결**: `diffReviewMetadata` 인메모리 Map 추가
|
||||||
|
- **주의**: Extension Reload 시 소실되지만 새 diff_review는 정상 동작
|
||||||
|
|
||||||
|
### [2026-03-16] AcknowledgeCascadeCodeEdit SUCCESS → diff review bar 미해제 — 잘못된 RPC 메서드명
|
||||||
|
- **증상**: RPC SUCCESS 반환 → diff review bar 여전히 표시
|
||||||
|
- **원인**: RPC 메서드명 자체가 틀렸음 (`AcknowledgeCascadeCodeEdit` → 실제는 `acknowledgeCodeActionStep`)
|
||||||
|
- **해결**: `agentAcceptAllInFile` / `agentRejectAllInFile` VS Code 커맨드 사용
|
||||||
|
- **주의**: AG의 RPC는 잘못된 메서드명도 에러 없이 `{}` 반환. **RPC `{}`는 실패로 간주해야 함**
|
||||||
|
|
||||||
|
### [2026-03-16] diff_review RPC 3개 전략 모두 dead-end
|
||||||
|
- **증상**: 3개 RPC 전략 모두 실패
|
||||||
|
- **원인**: (1) submitCodeAcknowledgement 미등록, (2) acknowledgeCodeActionStep 404, (3) AcknowledgeCascadeCodeEdit no-op
|
||||||
|
- **해결**: VS Code 커맨드 기반 (`agentAcceptAllInFile` / `agentRejectAllInFile`)
|
||||||
|
- **주의**: **diff_review를 RPC로 해결하려는 시도 모두 실패 확정. VS Code 커맨드 기반만 유효**
|
||||||
|
|
||||||
|
### [2026-03-16] AG 소스 역분석 — diff review 내부 동작 체인
|
||||||
|
- **증상**: Accept all / Reject all 내부 동작을 재현해야 함
|
||||||
|
- **원인**: AG 공식 API/문서 없음
|
||||||
|
- **해결**: AG 설치 경로의 JS 소스에서 역분석
|
||||||
|
- **주의**: minified JS에서 변수명은 버전마다 변경됨
|
||||||
|
|
||||||
|
### [2026-03-16] diff_review 이중 승인 요청 — DOM observer가 Accept/Reject 버튼 캡처
|
||||||
|
- **증상**: diff_review 외에 별도 "Accept"/"Reject" pending 도착
|
||||||
|
- **원인**: `openReviewChanges` 커맨드가 diff UI 패널 → DOM observer 감지
|
||||||
|
- **해결**: FALSE_POSITIVE_RE에 Accept/Reject all 추가
|
||||||
|
- **주의**: diff review bar 버튼은 전용 시스템에서 처리
|
||||||
|
|
||||||
|
### [2026-03-16] diff_review가 brain/ artifact에도 트리거
|
||||||
|
- **증상**: task.md만 수정해도 "코드 리뷰" pending 생성
|
||||||
|
- **원인**: diff_review 감지 로직이 모든 수정 파일 추적
|
||||||
|
- **해결**: `.gemini/antigravity/brain/` 경로 파일 필터링하여 제외
|
||||||
|
- **주의**: 코드 파일 + brain artifact 혼합 시 코드 파일만 diff_review
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WS Hub 전환 관련 (2026-03-16~17)
|
||||||
|
|
||||||
|
### [2026-03-16] !auto 이중 메시지 — Extension echo + Bot embed
|
||||||
|
- **증상**: `!auto` 토글 시 메시지 2개 표시
|
||||||
|
- **원인**: Bot embed + Extension writeChatSnapshot echo
|
||||||
|
- **해결**: Extension의 `!auto` handler에서 writeChatSnapshot echo 제거
|
||||||
|
- **주의**: Bot 재시작 시 auto_approve_projects 초기화 → 수동 모드 복귀
|
||||||
|
|
||||||
|
### [2026-03-16] 병렬 WAITING step 누락 — step_probe break문
|
||||||
|
- **증상**: 병렬 tool call 시 1개만 승인 요청 도착
|
||||||
|
- **원인**: step_probe 루프에서 `break`문이 첫 번째 WAITING 후 종료
|
||||||
|
- **해결**: `break` 제거, 모든 WAITING step에 pending 생성
|
||||||
|
- **주의**: 중복 방지는 `recentPendingSteps` Map이 처리
|
||||||
|
|
||||||
|
### [2026-03-16] Bot chat_snapshot 전송 로깅 부재
|
||||||
|
- **증상**: 전송 성공/실패를 Bot 로그에서 확인 불가
|
||||||
|
- **원인**: `channel.send()` 성공 후 INFO 로그 없음
|
||||||
|
- **해결**: 전송 성공/실패 로그 추가
|
||||||
|
- **주의**: `_get_channel()` 실패 시 WARNING은 이전에도 있었음
|
||||||
|
|
||||||
|
### [2026-03-16] 크로스 프로젝트 이벤트 폭주 — Watcher/Collector 무필터
|
||||||
|
- **증상**: /start 시 타 프로젝트 알림 유입
|
||||||
|
- **원인**: watcher.py가 brain/ 전체를 감시
|
||||||
|
- **해결**: `_is_my_session()` 필터 + 이벤트 전달 필터 추가
|
||||||
|
- **주의**: 미등록 세션은 allow-through 방식
|
||||||
|
|
||||||
|
### [2026-03-16] pending 파일 139개 누적 — 정리 로직 부재
|
||||||
|
- **증상**: bridge/pending/에 139개 파일 누적
|
||||||
|
- **원인**: auto_resolved/expired 파일을 아무도 삭제 안 함
|
||||||
|
- **해결**: Collector에서 auto_resolved/expired 전달 후 삭제 + 10분 자동 삭제
|
||||||
|
- **주의**: startup_pending은 정리 대상에서 제외
|
||||||
|
|
||||||
|
### [2026-03-17] NPM WebSocket 프록시 — Upgrade 헤더 미전달
|
||||||
|
- **증상**: `wss://ag.variet.net/ws` 연결 시 HTTP 400
|
||||||
|
- **원인**: Nginx Proxy Manager에 WebSocket Support 미활성화
|
||||||
|
- **해결**: NPM 대시보드에서 Websockets Support 체크
|
||||||
|
- **주의**: 새 프록시 호스트 생성 시 반드시 확인
|
||||||
|
|
||||||
|
### [2026-03-17] WS auth_fail 무한 재연결 — _cleanup() close 이벤트
|
||||||
|
- **증상**: Auth failed 후 60초마다 재연결 반복
|
||||||
|
- **원인**: `_cleanup()`이 ws.close() → close 이벤트 → _scheduleReconnect() 체인
|
||||||
|
- **해결**: `this.shouldReconnect = false`를 `_cleanup()` 이전에 설정
|
||||||
|
- **주의**: `_cleanup()`은 이벤트 핸들러를 트리거하므로 상태 변경은 반드시 호출 전에
|
||||||
|
|
||||||
|
### [2026-03-17] initStepProbe workspaceUri 누락 — 세션 감지 완전 불능
|
||||||
|
- **증상**: POLL alive만 출력, SESSION-FILTER/SNAPSHOT 없음
|
||||||
|
- **원인**: `initStepProbe()` 호출 시 필수 필드 미전달 + `as` 캐스트가 런타임 검증 없음
|
||||||
|
- **해결**: `workspaceUri`, `diffReviewMetadata: new Map()` 추가
|
||||||
|
- **주의**: TypeScript `as` 캐스트는 런타임 검증 없음
|
||||||
|
|
||||||
|
### [2026-03-17] WS 명령어 에코 릴레이 — Discord 메시지 2번 표시
|
||||||
|
- **증상**: Discord 메시지 입력 → 같은 메시지가 다시 표시
|
||||||
|
- **원인**: handleWSCommand에서 `recentDiscordSentTexts`에 마킹 안 함
|
||||||
|
- **해결**: `recentDiscordSentTexts.set()` 추가
|
||||||
|
- **주의**: 파일 기반 경로에는 이미 마킹 있었음
|
||||||
|
|
||||||
|
### [2026-03-17] writeRegistration 이중 쓰기 — WS 전송 후 파일도 작성
|
||||||
|
- **증상**: WS 상태에서도 register/ 파일 생성
|
||||||
|
- **원인**: WS 전송 후 `return` 없이 파일 쓰기 실행
|
||||||
|
- **해결**: WS 전송 후 `return` 추가
|
||||||
|
- **주의**: 새 WS 전송 함수 추가 시 file fallback과 상호 배타적 `return` 확인
|
||||||
File diff suppressed because it is too large
Load Diff
205
.agents/references/observer-dev-guide.md
Normal file
205
.agents/references/observer-dev-guide.md
Normal 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 |
|
||||||
216
.agents/references/relay-architecture.md
Normal file
216
.agents/references/relay-architecture.md
Normal 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 응답이 마지막 블록만 캡처되는 문제 개선 (전문 캡처)
|
||||||
@@ -5,42 +5,95 @@
|
|||||||
## 언어 & 런타임
|
## 언어 & 런타임
|
||||||
|
|
||||||
| 항목 | 버전 | 경로/비고 |
|
| 항목 | 버전 | 경로/비고 |
|
||||||
|------|------|-----------|
|
|------|------|-----------|
|
||||||
| Python | 3.x (miniforge3) | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe` |
|
| Python | 3.12 (miniforge3) | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe` |
|
||||||
| Node.js | 시스템 설치 | `node`, `npm` (PowerShell에서 `cmd /c npm` 권장) |
|
| Node.js | 시스템 설치 | `node`, `npm` (PowerShell에서 `cmd /c npm` 권장) |
|
||||||
| TypeScript | (Extension) | `extension/src/extension.ts` → `tsc` 빌드 |
|
| TypeScript | 5.3+ | `extension/src/*.ts` → `tsc` → `extension/out/*.js` |
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Python은 **반드시** 위 miniforge3 경로를 사용. WindowsApps의 python stub은 동작하지 않음.
|
> Python은 **반드시** 위 miniforge3 경로를 사용. WindowsApps의 python stub은 동작하지 않음.
|
||||||
|
|
||||||
## 프레임워크
|
## 프레임워크 & 라이브러리
|
||||||
|
|
||||||
| 항목 | 버전 | 용도 |
|
### Python (서버)
|
||||||
|------|------|------|
|
|
||||||
| discord.py | 2.x | Discord 봇 |
|
| 패키지 | 버전 | 용도 |
|
||||||
| watchdog | - | 파일시스템 감시 |
|
|--------|------|------|
|
||||||
| antigravity-sdk | 로컬 | VS Code Extension SDK 연동 |
|
| discord.py | 2.x | Discord 봇 (슬래시 명령, 버튼 UI, 이벤트) |
|
||||||
|
| aiohttp | 3.x | Gateway HTTP 서버 + WebSocket endpoint |
|
||||||
|
| watchdog | - | Brain 디렉토리 파일시스템 감시 |
|
||||||
|
| python-dotenv | - | .env 파일 로드 |
|
||||||
|
| PyJWT | - | ❌ 미사용 (자체 HMAC-SHA256 구현) |
|
||||||
|
|
||||||
|
### TypeScript (Extension)
|
||||||
|
|
||||||
|
| 패키지 | 용도 |
|
||||||
|
|--------|------|
|
||||||
|
| @types/vscode | VS Code Extension API 타입 |
|
||||||
|
| @types/node | Node.js 타입 |
|
||||||
|
| typescript | 컴파일러 |
|
||||||
|
| ws | WebSocket Hub 연결 (`.vscodeignore`에 `!node_modules/ws/**` 필수) |
|
||||||
|
| antigravity-sdk | AG RPC 호출 (로컬 임베드 `sdk/`) |
|
||||||
|
|
||||||
## 패키지 관리
|
## 패키지 관리
|
||||||
|
|
||||||
- **Python**: pip (`requirements.txt`)
|
| 측 | 도구 | 파일 |
|
||||||
- **Extension**: npm (`extension/package.json`)
|
|----|------|------|
|
||||||
|
| Python | pip | `requirements.txt` |
|
||||||
|
| Extension | npm | `extension/package.json` |
|
||||||
|
|
||||||
## 개발 도구
|
## 개발 도구 & 명령어
|
||||||
|
|
||||||
| 도구 | 명령어 |
|
| 작업 | 명령어 |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| **봇 실행** | `start_bot.bat` 또는 `C:\ProgramData\miniforge3\envs\gravity_control\python.exe main.py` |
|
| **봇 실행** | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe main.py` |
|
||||||
| **Extension 빌드** | `cd extension && cmd /c npm run compile` |
|
| **봇 실행 (gateway)** | `.env`에서 `BOT_MODE=gateway` 설정 후 위 명령 |
|
||||||
| **Extension VSIX** | `cd extension && cmd /c npx vsce package` |
|
| **Extension 구문 검사** | `cd extension && npx tsc --noEmit` |
|
||||||
| **봇 구문 검사** | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe -c "import bot, bridge, config, main"` |
|
| **Extension 컴파일** | `cd extension && cmd /c npm run compile` |
|
||||||
|
| **Extension VSIX** | `cd extension && npx @vscode/vsce package --no-dependencies` |
|
||||||
|
| **Python 구문 검사** | `python -c "import ast; [ast.parse(open(f).read()) for f in ['bot.py','hub.py',...]]"` |
|
||||||
|
| **Hub WS 테스트** | `python tests/test_ws_hub.py` (서버 기동 상태에서) |
|
||||||
|
|
||||||
## 환경 변수
|
## 환경 변수 (.env)
|
||||||
|
|
||||||
|
### 필수
|
||||||
|
|
||||||
| 변수명 | 용도 | 기본값 |
|
| 변수명 | 용도 | 기본값 |
|
||||||
|--------|------|--------|
|
|--------|------|--------|
|
||||||
| DISCORD_TOKEN | Discord 봇 토큰 | (필수) |
|
| DISCORD_TOKEN | Discord 봇 토큰 | (필수) |
|
||||||
| DISCORD_GUILD_ID | Discord 서버 ID | (필수) |
|
| DISCORD_GUILD_ID | Discord 서버 ID | (필수) |
|
||||||
|
|
||||||
|
### 선택
|
||||||
|
|
||||||
|
| 변수명 | 용도 | 기본값 |
|
||||||
|
|--------|------|--------|
|
||||||
| BRAIN_PATH | AG 브레인 경로 | `~/.gemini/antigravity/brain` |
|
| BRAIN_PATH | AG 브레인 경로 | `~/.gemini/antigravity/brain` |
|
||||||
| BOT_MODE | 봇 모드 (local/remote) | `local` |
|
| BOT_MODE | `local` / `gateway` | `local` |
|
||||||
| REMOTE_BRIDGE_URL | 원격 브릿지 URL | (remote 모드 전용) |
|
| DEBOUNCE_SECONDS | Watcher 디바운스 간격 | `5` |
|
||||||
|
| PROJECT_NAME | 프로젝트 이름 | `gravity_control` |
|
||||||
|
|
||||||
|
### Gateway 모드 전용
|
||||||
|
|
||||||
|
| 변수명 | 용도 | 기본값 |
|
||||||
|
|--------|------|--------|
|
||||||
|
| GATEWAY_PORT | Gateway HTTP/WS 포트 | `8585` |
|
||||||
|
| GATEWAY_API_KEY | REST API 인증 키 | (미설정 시 인증 미사용) |
|
||||||
|
| GRAVITY_HUB_SECRET | WS Hub JWT 서명 시크릿 (64char hex) | (미설정 시 인증 생략) |
|
||||||
|
| GRAVITY_REGISTRATION_CODE | Extension 등록 코드 (32char hex) | (미설정 시 인증 생략) |
|
||||||
|
|
||||||
|
## Extension VS Code 설정
|
||||||
|
|
||||||
|
| 설정 키 | 용도 |
|
||||||
|
|---------|------|
|
||||||
|
| `gravityBridge.bridgePath` | Bridge 디렉토리 경로 |
|
||||||
|
| `gravityBridge.projectName` | 프로젝트 이름 (기본: git remote) |
|
||||||
|
| `gravityBridge.hubUrl` | Hub WS URL (예: `ws://localhost:8585/ws`) |
|
||||||
|
| `gravityBridge.registrationCode` | Hub 등록 코드 |
|
||||||
|
|
||||||
|
## 빌드 산출물
|
||||||
|
|
||||||
|
| 항목 | 경로 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| VSIX | `extension/gravity-bridge-{ver}.vsix` | VS Code 확장 패키지 |
|
||||||
|
| JS 출력 | `extension/out/*.js` | TypeScript 컴파일 결과물 |
|
||||||
|
| SDK 복사 | `extension/out/sdk/` | compile 시 자동 복사 |
|
||||||
|
|||||||
160
.agents/workflows/helpers/analyze_dom.py
Normal file
160
.agents/workflows/helpers/analyze_dom.py
Normal 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)
|
||||||
19
.agents/workflows/helpers/parse_dump.py
Normal file
19
.agents/workflows/helpers/parse_dump.py
Normal 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')}")
|
||||||
83
.agents/workflows/helpers/search_dom.py
Normal file
83
.agents/workflows/helpers/search_dom.py
Normal 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]}"')
|
||||||
109
.agents/workflows/helpers/trace_dom.py
Normal file
109
.agents/workflows/helpers/trace_dom.py
Normal 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}')
|
||||||
@@ -17,12 +17,15 @@ ACTIVE_TIMEOUT_SECONDS=300
|
|||||||
# Watcher Settings
|
# Watcher Settings
|
||||||
DEBOUNCE_SECONDS=2
|
DEBOUNCE_SECONDS=2
|
||||||
|
|
||||||
# Bot mode: 'local' (default, file-based) or 'gateway' (서버 Docker)
|
# Bot mode: 'local' (default, file-based) or 'gateway' (서버 Docker + WS Hub)
|
||||||
BOT_MODE=local
|
BOT_MODE=local
|
||||||
# Remote bridge URL (only used when BOT_MODE=remote)
|
|
||||||
REMOTE_BRIDGE_URL=
|
|
||||||
|
|
||||||
# Gateway API Key (보안)
|
# Gateway API Key (보안)
|
||||||
# 서버와 Collector에 동일한 키를 설정하세요
|
# 서버와 Collector에 동일한 키를 설정하세요
|
||||||
# 생성: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
# 생성: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||||
GATEWAY_API_KEY=
|
GATEWAY_API_KEY=
|
||||||
|
|
||||||
|
# Hub WebSocket 인증 (선택 — 미설정 시 인증 생략)
|
||||||
|
# 생성: python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
GRAVITY_HUB_SECRET=
|
||||||
|
GRAVITY_REGISTRATION_CODE=
|
||||||
|
|||||||
BIN
.gitlog.txt
Normal file
BIN
.gitlog.txt
Normal file
Binary file not shown.
@@ -1,9 +0,0 @@
|
|||||||
# Gravity Gateway — Caddy Reverse Proxy
|
|
||||||
# Automatic HTTPS via Let's Encrypt
|
|
||||||
#
|
|
||||||
# 도메인을 실제 도메인으로 변경하세요 (예: gateway.variet.net)
|
|
||||||
# Caddy가 자동으로 Let's Encrypt 인증서를 발급합니다.
|
|
||||||
|
|
||||||
gateway.variet.net {
|
|
||||||
reverse_proxy gateway:8585
|
|
||||||
}
|
|
||||||
127
auth.py
Normal file
127
auth.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
"""Authentication module — JWT token management for WebSocket Hub.
|
||||||
|
|
||||||
|
Two-stage auth:
|
||||||
|
1. Extension connects with a registration code (built into .vsix at build time)
|
||||||
|
2. Hub validates the code and issues a short-lived JWT session token
|
||||||
|
3. Subsequent reconnections use the JWT token directly
|
||||||
|
|
||||||
|
The master secret is stored server-side only (GRAVITY_HUB_SECRET env var).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
DEFAULT_TOKEN_TTL = 86400 # 24 hours
|
||||||
|
DEFAULT_REGISTRATION_CODE = "" # Set via GRAVITY_REGISTRATION_CODE env var
|
||||||
|
|
||||||
|
|
||||||
|
class TokenManager:
|
||||||
|
"""Manages JWT-like token creation and verification.
|
||||||
|
|
||||||
|
Uses HMAC-SHA256 for signing. Tokens contain:
|
||||||
|
- project: project name scope
|
||||||
|
- pc: PC identifier (hostname or custom name)
|
||||||
|
- iat: issued at (unix timestamp)
|
||||||
|
- exp: expiration (unix timestamp)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, secret: str = "", registration_code: str = ""):
|
||||||
|
self.secret = secret or os.getenv("GRAVITY_HUB_SECRET", "")
|
||||||
|
self.registration_code = registration_code or os.getenv(
|
||||||
|
"GRAVITY_REGISTRATION_CODE", DEFAULT_REGISTRATION_CODE
|
||||||
|
)
|
||||||
|
if not self.secret:
|
||||||
|
# Auto-generate a secret if not set (ephemeral — tokens invalid after restart)
|
||||||
|
self.secret = hashlib.sha256(os.urandom(32)).hexdigest()
|
||||||
|
logger.warning(
|
||||||
|
"[AUTH] No GRAVITY_HUB_SECRET set — generated ephemeral secret. "
|
||||||
|
"Tokens will be invalid after server restart."
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_registration_code(self, code: str) -> bool:
|
||||||
|
"""Check if the provided registration code matches."""
|
||||||
|
if not self.registration_code:
|
||||||
|
# No registration code configured → allow all connections
|
||||||
|
logger.warning("[AUTH] No registration code configured — accepting all")
|
||||||
|
return True
|
||||||
|
return hmac.compare_digest(code, self.registration_code)
|
||||||
|
|
||||||
|
def create_token(
|
||||||
|
self, project: str, pc_name: str, ttl: int = DEFAULT_TOKEN_TTL
|
||||||
|
) -> str:
|
||||||
|
"""Create a signed token for a specific project and PC.
|
||||||
|
|
||||||
|
Returns a base64-encoded string: {header}.{payload}.{signature}
|
||||||
|
"""
|
||||||
|
now = int(time.time())
|
||||||
|
payload = {
|
||||||
|
"project": project,
|
||||||
|
"pc": pc_name,
|
||||||
|
"iat": now,
|
||||||
|
"exp": now + ttl,
|
||||||
|
}
|
||||||
|
payload_b64 = _b64_encode(json.dumps(payload))
|
||||||
|
signature = self._sign(payload_b64)
|
||||||
|
return f"{payload_b64}.{signature}"
|
||||||
|
|
||||||
|
def verify_token(self, token: str) -> dict | None:
|
||||||
|
"""Verify and decode a token.
|
||||||
|
|
||||||
|
Returns the payload dict if valid, None if invalid or expired.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parts = token.split(".")
|
||||||
|
if len(parts) != 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
payload_b64, signature = parts
|
||||||
|
expected_sig = self._sign(payload_b64)
|
||||||
|
|
||||||
|
if not hmac.compare_digest(signature, expected_sig):
|
||||||
|
logger.warning("[AUTH] Invalid token signature")
|
||||||
|
return None
|
||||||
|
|
||||||
|
payload = json.loads(_b64_decode(payload_b64))
|
||||||
|
|
||||||
|
# Check expiration
|
||||||
|
if payload.get("exp", 0) < time.time():
|
||||||
|
logger.info(f"[AUTH] Token expired for {payload.get('pc', '?')}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return payload
|
||||||
|
except (json.JSONDecodeError, ValueError, KeyError) as e:
|
||||||
|
logger.warning(f"[AUTH] Token decode error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _sign(self, data: str) -> str:
|
||||||
|
"""HMAC-SHA256 sign and return base64."""
|
||||||
|
sig = hmac.new(
|
||||||
|
self.secret.encode("utf-8"),
|
||||||
|
data.encode("utf-8"),
|
||||||
|
hashlib.sha256,
|
||||||
|
).digest()
|
||||||
|
return _b64_encode_bytes(sig)
|
||||||
|
|
||||||
|
|
||||||
|
def _b64_encode(data: str) -> str:
|
||||||
|
"""URL-safe base64 encode a string, no padding."""
|
||||||
|
return urlsafe_b64encode(data.encode("utf-8")).rstrip(b"=").decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def _b64_encode_bytes(data: bytes) -> str:
|
||||||
|
"""URL-safe base64 encode bytes, no padding."""
|
||||||
|
return urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def _b64_decode(data: str) -> str:
|
||||||
|
"""URL-safe base64 decode, handles missing padding."""
|
||||||
|
padded = data + "=" * (4 - len(data) % 4)
|
||||||
|
return urlsafe_b64decode(padded).decode("utf-8")
|
||||||
479
bridge.py
479
bridge.py
@@ -1,479 +0,0 @@
|
|||||||
"""Bridge protocol — communication between Discord bot and Antigravity.
|
|
||||||
|
|
||||||
Bridge directory: ~/.gemini/antigravity/bridge/
|
|
||||||
Structure:
|
|
||||||
bridge/
|
|
||||||
pending/ ← Bot writes approval requests for Discord
|
|
||||||
response/ ← Bot writes user responses from Discord
|
|
||||||
commands/ ← Bot writes user text input from Discord
|
|
||||||
|
|
||||||
Protocol:
|
|
||||||
1. VS Code Extension detects pending approval → writes JSON to pending/
|
|
||||||
2. Bot reads pending/ → sends Discord message with ✅/❌ buttons
|
|
||||||
3. User clicks button → Bot writes JSON to response/
|
|
||||||
4. VS Code Extension reads response/ → executes action
|
|
||||||
|
|
||||||
Transport layer:
|
|
||||||
LocalTransport — file-based (default, single-PC)
|
|
||||||
RemoteTransport — HTTP-based (future: multi-PC collector mode)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
import uuid
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from pathlib import Path
|
|
||||||
from dataclasses import dataclass, asdict
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from config import Config
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ApprovalStatus(Enum):
|
|
||||||
PENDING = "pending"
|
|
||||||
APPROVED = "approved"
|
|
||||||
REJECTED = "rejected"
|
|
||||||
TIMEOUT = "timeout"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ApprovalRequest:
|
|
||||||
"""An approval request from Antigravity."""
|
|
||||||
request_id: str
|
|
||||||
conversation_id: str
|
|
||||||
command: str # The command/action needing approval
|
|
||||||
description: str # Human-readable description
|
|
||||||
timestamp: float
|
|
||||||
status: str = "pending"
|
|
||||||
discord_message_id: int = 0
|
|
||||||
project_name: str = "" # Project routing key
|
|
||||||
step_type: str = "" # e.g. 'diff_review', passed through to response
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class UserResponse:
|
|
||||||
"""A user response from Discord."""
|
|
||||||
request_id: str
|
|
||||||
approved: bool
|
|
||||||
user_input: str = ""
|
|
||||||
timestamp: float = 0
|
|
||||||
button_index: int = -1 # -1 = legacy (approve/reject), 0+ = specific button index
|
|
||||||
step_type: str = "" # pass through from pending for extension routing
|
|
||||||
project_name: str = "" # for multi-project: extension uses this when pending file is missing
|
|
||||||
|
|
||||||
|
|
||||||
# ─── Transport Abstraction ───
|
|
||||||
|
|
||||||
class BridgeTransport(ABC):
|
|
||||||
"""Abstract transport for bridge I/O.
|
|
||||||
|
|
||||||
Implementations handle reading/writing JSON files for the bridge protocol,
|
|
||||||
regardless of whether the storage is local filesystem or remote HTTP.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def list_json_files(self, subdir: str) -> list[str]:
|
|
||||||
"""List JSON filenames in a subdirectory (e.g. 'pending', 'response')."""
|
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def read_json(self, subdir: str, filename: str) -> dict | None:
|
|
||||||
"""Read and parse a JSON file. Returns None if not found or corrupt."""
|
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def write_json(self, subdir: str, filename: str, data: dict) -> None:
|
|
||||||
"""Write data as JSON to a file in the given subdirectory."""
|
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def delete_file(self, subdir: str, filename: str) -> bool:
|
|
||||||
"""Delete a file. Returns True if deleted, False if not found."""
|
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def ensure_dirs(self) -> None:
|
|
||||||
"""Ensure all required subdirectories exist."""
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class LocalTransport(BridgeTransport):
|
|
||||||
"""File-system based transport (default, single-PC mode).
|
|
||||||
|
|
||||||
Reads/writes directly to the bridge directory on local disk.
|
|
||||||
This is the existing behavior, extracted into a transport class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bridge_dir: Path):
|
|
||||||
self.bridge_dir = bridge_dir
|
|
||||||
|
|
||||||
def list_json_files(self, subdir: str) -> list[str]:
|
|
||||||
d = self.bridge_dir / subdir
|
|
||||||
if not d.exists():
|
|
||||||
return []
|
|
||||||
return [f.name for f in d.glob("*.json")]
|
|
||||||
|
|
||||||
def read_json(self, subdir: str, filename: str) -> dict | None:
|
|
||||||
fp = self.bridge_dir / subdir / filename
|
|
||||||
if not fp.exists():
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
return json.loads(fp.read_text(encoding="utf-8-sig"))
|
|
||||||
except (json.JSONDecodeError, OSError) as e:
|
|
||||||
logger.warning(f"LocalTransport: bad file {subdir}/{filename}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def write_json(self, subdir: str, filename: str, data: dict) -> None:
|
|
||||||
d = self.bridge_dir / subdir
|
|
||||||
d.mkdir(parents=True, exist_ok=True)
|
|
||||||
fp = d / filename
|
|
||||||
fp.write_text(
|
|
||||||
json.dumps(data, ensure_ascii=False, indent=2),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete_file(self, subdir: str, filename: str) -> bool:
|
|
||||||
fp = self.bridge_dir / subdir / filename
|
|
||||||
if fp.exists():
|
|
||||||
try:
|
|
||||||
fp.unlink()
|
|
||||||
return True
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
return False
|
|
||||||
|
|
||||||
def ensure_dirs(self) -> None:
|
|
||||||
for sub in ("pending", "response", "commands"):
|
|
||||||
(self.bridge_dir / sub).mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteTransport(BridgeTransport):
|
|
||||||
"""HTTP-based transport for Collector → Gateway communication.
|
|
||||||
|
|
||||||
Maps BridgeTransport methods to Gateway API endpoints:
|
|
||||||
list_json_files("pending") → GET /api/pending (returns list)
|
|
||||||
write_json("pending", ...) → POST /api/pending
|
|
||||||
read_json("response", ...) → GET /api/response/{rid}
|
|
||||||
write_json("commands", ...) → (not used by Collector, Gateway pushes commands)
|
|
||||||
etc.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, base_url: str, api_key: str = ""):
|
|
||||||
self.base_url = base_url.rstrip("/")
|
|
||||||
self.api_key = api_key
|
|
||||||
self._headers = {"Content-Type": "application/json"}
|
|
||||||
if api_key:
|
|
||||||
self._headers["Authorization"] = f"Bearer {api_key}"
|
|
||||||
self._session = None # aiohttp.ClientSession — lazy created
|
|
||||||
|
|
||||||
# Connection health
|
|
||||||
self.connected = False
|
|
||||||
self._consecutive_failures = 0
|
|
||||||
self._max_failures_before_warning = 3
|
|
||||||
|
|
||||||
# Rate limit backoff
|
|
||||||
self._rate_limited_until = 0.0 # timestamp until which we should not send requests
|
|
||||||
self._backoff_seconds = 0.0 # current backoff duration (exponential)
|
|
||||||
self._BACKOFF_BASE = 2.0
|
|
||||||
self._BACKOFF_MAX = 60.0
|
|
||||||
self._success_streak = 0 # consecutive successes for gradual backoff reduction
|
|
||||||
|
|
||||||
# Retry queue: list of (method, path, data) tuples
|
|
||||||
self._retry_queue: list[tuple[str, str, dict | None]] = []
|
|
||||||
self._retry_queue_max = 100
|
|
||||||
|
|
||||||
logger.info(f"RemoteTransport: {self.base_url} (auth={'yes' if api_key else 'no'})")
|
|
||||||
|
|
||||||
async def _get_session(self):
|
|
||||||
"""Lazy-create aiohttp session."""
|
|
||||||
if self._session is None or self._session.closed:
|
|
||||||
import aiohttp
|
|
||||||
timeout = aiohttp.ClientTimeout(total=10)
|
|
||||||
self._session = aiohttp.ClientSession(
|
|
||||||
headers=self._headers, timeout=timeout
|
|
||||||
)
|
|
||||||
return self._session
|
|
||||||
|
|
||||||
async def close(self):
|
|
||||||
"""Close the HTTP session."""
|
|
||||||
if self._session and not self._session.closed:
|
|
||||||
await self._session.close()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_rate_limited(self) -> bool:
|
|
||||||
"""Check if we are currently in a rate-limit backoff period."""
|
|
||||||
return time.time() < self._rate_limited_until
|
|
||||||
|
|
||||||
def _apply_backoff(self, retry_after: float = 0):
|
|
||||||
"""Apply exponential backoff for rate limiting."""
|
|
||||||
self._success_streak = 0 # Reset success streak on any failure
|
|
||||||
if retry_after > 0:
|
|
||||||
self._backoff_seconds = min(retry_after, self._BACKOFF_MAX)
|
|
||||||
else:
|
|
||||||
if self._backoff_seconds == 0:
|
|
||||||
self._backoff_seconds = self._BACKOFF_BASE
|
|
||||||
else:
|
|
||||||
self._backoff_seconds = min(self._backoff_seconds * 2, self._BACKOFF_MAX)
|
|
||||||
self._rate_limited_until = time.time() + self._backoff_seconds
|
|
||||||
logger.warning(f"RemoteTransport: backing off {self._backoff_seconds:.0f}s (until +{self._backoff_seconds:.0f}s)")
|
|
||||||
|
|
||||||
def _on_request_success(self):
|
|
||||||
"""Gradually reduce backoff after consecutive successes.
|
|
||||||
|
|
||||||
Instead of instantly resetting to 0 (which causes the 1s oscillation loop
|
|
||||||
when 7 loops share one transport), require sustained success before reducing.
|
|
||||||
"""
|
|
||||||
if self._backoff_seconds <= 0:
|
|
||||||
return # Already at zero, nothing to do
|
|
||||||
self._success_streak += 1
|
|
||||||
if self._success_streak >= 5:
|
|
||||||
# Halve the backoff (gradual cooldown)
|
|
||||||
self._backoff_seconds = self._backoff_seconds / 2
|
|
||||||
if self._backoff_seconds < 0.5:
|
|
||||||
self._backoff_seconds = 0
|
|
||||||
self._rate_limited_until = 0
|
|
||||||
self._success_streak = 0
|
|
||||||
|
|
||||||
async def _arequest(self, method: str, path: str, data: dict | None = None) -> dict | None:
|
|
||||||
"""Async non-blocking HTTP request to Gateway API."""
|
|
||||||
# Skip if in backoff period (except health checks)
|
|
||||||
if self.is_rate_limited and path != "/health":
|
|
||||||
return None
|
|
||||||
|
|
||||||
session = await self._get_session()
|
|
||||||
url = f"{self.base_url}{path}"
|
|
||||||
try:
|
|
||||||
kwargs = {}
|
|
||||||
if data is not None:
|
|
||||||
kwargs["json"] = data
|
|
||||||
async with session.request(method, url, **kwargs) as resp:
|
|
||||||
if resp.status >= 400:
|
|
||||||
if resp.status == 401:
|
|
||||||
logger.error("RemoteTransport: 401 Unauthorized — check GATEWAY_API_KEY")
|
|
||||||
elif resp.status == 429:
|
|
||||||
retry_after = float(resp.headers.get("Retry-After", 0))
|
|
||||||
self._apply_backoff(retry_after)
|
|
||||||
else:
|
|
||||||
logger.warning(f"RemoteTransport: {method} {path} → {resp.status}")
|
|
||||||
return None
|
|
||||||
result = await resp.json()
|
|
||||||
if not self.connected:
|
|
||||||
logger.info("RemoteTransport: ✅ Gateway connected")
|
|
||||||
self.connected = True
|
|
||||||
self._consecutive_failures = 0
|
|
||||||
self._on_request_success()
|
|
||||||
return result
|
|
||||||
except Exception as e:
|
|
||||||
self._consecutive_failures += 1
|
|
||||||
if self._consecutive_failures == self._max_failures_before_warning:
|
|
||||||
logger.error(f"RemoteTransport: ❌ Gateway unreachable ({self._consecutive_failures} failures): {e}")
|
|
||||||
elif self._consecutive_failures < self._max_failures_before_warning:
|
|
||||||
logger.warning(f"RemoteTransport: {method} {path} → {e}")
|
|
||||||
self.connected = False
|
|
||||||
# Apply backoff on connection failures too
|
|
||||||
if self._consecutive_failures >= self._max_failures_before_warning:
|
|
||||||
self._apply_backoff()
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def _arequest_retry(self, method: str, path: str, data: dict | None = None) -> dict | None:
|
|
||||||
"""Request with retry queue — failed POSTs are queued for later."""
|
|
||||||
result = await self._arequest(method, path, data)
|
|
||||||
if result is None and method == "POST" and data is not None:
|
|
||||||
if len(self._retry_queue) < self._retry_queue_max:
|
|
||||||
self._retry_queue.append((method, path, data))
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def flush_retry_queue(self):
|
|
||||||
"""Retry queued failed requests."""
|
|
||||||
if not self._retry_queue or not self.connected:
|
|
||||||
return
|
|
||||||
queue = self._retry_queue[:]
|
|
||||||
self._retry_queue.clear()
|
|
||||||
succeeded = 0
|
|
||||||
for method, path, data in queue:
|
|
||||||
result = await self._arequest(method, path, data)
|
|
||||||
if result is None:
|
|
||||||
if len(self._retry_queue) < self._retry_queue_max:
|
|
||||||
self._retry_queue.append((method, path, data))
|
|
||||||
break
|
|
||||||
succeeded += 1
|
|
||||||
if succeeded:
|
|
||||||
logger.info(f"[RETRY] flushed {succeeded}/{len(queue)} queued requests")
|
|
||||||
|
|
||||||
async def health_check(self) -> bool:
|
|
||||||
"""Check if Gateway is reachable."""
|
|
||||||
result = await self._arequest("GET", "/health")
|
|
||||||
return result is not None and result.get("status") == "ok"
|
|
||||||
|
|
||||||
# ─── Async methods (used by Collector) ───
|
|
||||||
|
|
||||||
async def awrite_json(self, subdir: str, filename: str, data: dict) -> None:
|
|
||||||
if subdir == "pending":
|
|
||||||
await self._arequest_retry("POST", "/api/pending", data)
|
|
||||||
elif subdir == "response":
|
|
||||||
rid = data.get("request_id", filename.replace(".json", ""))
|
|
||||||
await self._arequest_retry("POST", f"/api/response/{rid}", data)
|
|
||||||
|
|
||||||
async def aread_json(self, subdir: str, filename: str) -> dict | None:
|
|
||||||
rid = filename.replace(".json", "")
|
|
||||||
if subdir == "response":
|
|
||||||
return await self._arequest("GET", f"/api/response/{rid}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def apoll_commands(self, project: str) -> list[dict]:
|
|
||||||
result = await self._arequest("GET", f"/api/commands/{project}")
|
|
||||||
if result and isinstance(result, dict):
|
|
||||||
return result.get("commands", [])
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def aregister_session(self, conv_id: str, project: str) -> None:
|
|
||||||
await self._arequest_retry("POST", "/api/register", {
|
|
||||||
"conversation_id": conv_id, "project_name": project,
|
|
||||||
})
|
|
||||||
|
|
||||||
async def asend_chat(self, project: str, content: str, *, attached_files: list[dict] | None = None) -> None:
|
|
||||||
payload: dict = {"project_name": project, "content": content}
|
|
||||||
if attached_files:
|
|
||||||
payload["attached_files"] = attached_files
|
|
||||||
await self._arequest_retry("POST", "/api/chat", payload)
|
|
||||||
|
|
||||||
async def asend_event(self, event_data: dict) -> None:
|
|
||||||
await self._arequest_retry("POST", "/api/event", event_data)
|
|
||||||
|
|
||||||
# ─── Sync stubs (ABC compliance, not used in Collector) ───
|
|
||||||
|
|
||||||
def list_json_files(self, subdir: str) -> list[str]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
def read_json(self, subdir: str, filename: str) -> dict | None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def write_json(self, subdir: str, filename: str, data: dict) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def delete_file(self, subdir: str, filename: str) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def ensure_dirs(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# ─── Bridge Protocol (uses Transport) ───
|
|
||||||
|
|
||||||
class BridgeProtocol:
|
|
||||||
"""Manages the bridge protocol via a pluggable transport."""
|
|
||||||
|
|
||||||
def __init__(self, transport: BridgeTransport | None = None):
|
|
||||||
if transport is None:
|
|
||||||
bridge_dir = Config.BRAIN_PATH.parent / "bridge"
|
|
||||||
transport = LocalTransport(bridge_dir)
|
|
||||||
self.transport = transport
|
|
||||||
|
|
||||||
# Legacy attributes for backward compatibility
|
|
||||||
# (bot.py uses self.bridge.pending_dir etc. in some places)
|
|
||||||
if isinstance(transport, LocalTransport):
|
|
||||||
self.bridge_dir = transport.bridge_dir
|
|
||||||
self.pending_dir = transport.bridge_dir / "pending"
|
|
||||||
self.response_dir = transport.bridge_dir / "response"
|
|
||||||
self.commands_dir = transport.bridge_dir / "commands"
|
|
||||||
|
|
||||||
# Ensure directories exist
|
|
||||||
self.transport.ensure_dirs()
|
|
||||||
|
|
||||||
# Startup cleanup: purge stale pending files (> 5 min old)
|
|
||||||
self._cleanup_stale_pending()
|
|
||||||
|
|
||||||
logger.info(f"Bridge protocol initialized: transport={type(transport).__name__}")
|
|
||||||
|
|
||||||
def _cleanup_stale_pending(self, max_age_seconds: int = 300):
|
|
||||||
"""Remove pending files older than max_age_seconds on startup."""
|
|
||||||
now = time.time()
|
|
||||||
cleaned = 0
|
|
||||||
for fname in self.transport.list_json_files("pending"):
|
|
||||||
data = self.transport.read_json("pending", fname)
|
|
||||||
if data is None:
|
|
||||||
self.transport.delete_file("pending", fname)
|
|
||||||
cleaned += 1
|
|
||||||
continue
|
|
||||||
ts = data.get("timestamp", 0)
|
|
||||||
if now - ts > max_age_seconds:
|
|
||||||
self.transport.delete_file("pending", fname)
|
|
||||||
cleaned += 1
|
|
||||||
if cleaned:
|
|
||||||
logger.info(f"Startup cleanup: removed {cleaned} stale pending files")
|
|
||||||
|
|
||||||
def get_pending_requests(self) -> list[ApprovalRequest]:
|
|
||||||
"""Read all pending approval requests. Skips files older than 30 minutes."""
|
|
||||||
requests = []
|
|
||||||
fields = {f.name for f in ApprovalRequest.__dataclass_fields__.values()}
|
|
||||||
now = time.time()
|
|
||||||
MAX_AGE = 1800 # 30 minutes (matches Discord button timeout)
|
|
||||||
CLEANUP_AGE = 86400 # 1 day
|
|
||||||
for fname in self.transport.list_json_files("pending"):
|
|
||||||
data = self.transport.read_json("pending", fname)
|
|
||||||
if data is None:
|
|
||||||
continue
|
|
||||||
ts = data.get("timestamp", 0)
|
|
||||||
if now - ts > CLEANUP_AGE:
|
|
||||||
# Too old even to keep as expired — delete to prevent accumulation
|
|
||||||
self.transport.delete_file("pending", fname)
|
|
||||||
continue
|
|
||||||
if now - ts > MAX_AGE:
|
|
||||||
# Too old — mark expired and skip
|
|
||||||
if data.get("status") != "expired":
|
|
||||||
data["status"] = "expired"
|
|
||||||
self.transport.write_json("pending", fname, data)
|
|
||||||
continue
|
|
||||||
if data.get("status") == "pending":
|
|
||||||
# Filter to known fields only
|
|
||||||
filtered = {k: v for k, v in data.items() if k in fields}
|
|
||||||
try:
|
|
||||||
requests.append(ApprovalRequest(**filtered))
|
|
||||||
except TypeError as e:
|
|
||||||
logger.warning(f"Bad pending request {fname}: {e}")
|
|
||||||
return requests
|
|
||||||
|
|
||||||
def read_pending_request(self, request_id: str) -> ApprovalRequest | None:
|
|
||||||
"""Re-read a specific pending request (to get merged data)."""
|
|
||||||
fname = f"{request_id}.json"
|
|
||||||
data = self.transport.read_json("pending", fname)
|
|
||||||
if data is None:
|
|
||||||
return None
|
|
||||||
fields = {fn.name for fn in ApprovalRequest.__dataclass_fields__.values()}
|
|
||||||
filtered = {k: v for k, v in data.items() if k in fields}
|
|
||||||
try:
|
|
||||||
return ApprovalRequest(**filtered)
|
|
||||||
except TypeError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def write_response(self, response: UserResponse):
|
|
||||||
"""Write a user response to the response directory."""
|
|
||||||
response.timestamp = time.time()
|
|
||||||
fname = f"{response.request_id}.json"
|
|
||||||
|
|
||||||
self.transport.write_json("response", fname, asdict(response))
|
|
||||||
logger.info(f"Response written: {fname} (approved={response.approved})")
|
|
||||||
|
|
||||||
# Delete pending file after processing (prevents re-processing and accumulation)
|
|
||||||
self.transport.delete_file("pending", fname)
|
|
||||||
|
|
||||||
def write_command(self, conversation_id: str, text: str, *, project_name: str = ""):
|
|
||||||
"""Write a user text command for Antigravity to consume."""
|
|
||||||
cmd_id = f"{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}"
|
|
||||||
fname = f"{cmd_id}.json"
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"id": cmd_id,
|
|
||||||
"conversation_id": conversation_id,
|
|
||||||
"project_name": project_name,
|
|
||||||
"text": text,
|
|
||||||
"timestamp": time.time(),
|
|
||||||
"consumed": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.transport.write_json("commands", fname, data)
|
|
||||||
logger.info(f"Command written: {cmd_id} → project={project_name}")
|
|
||||||
return cmd_id
|
|
||||||
461
collector.py
461
collector.py
@@ -1,461 +0,0 @@
|
|||||||
"""Collector — local relay between Extension (file-based) and Gateway (HTTP).
|
|
||||||
|
|
||||||
The Collector runs on the local PC alongside the AG IDE.
|
|
||||||
It bridges the gap between the Extension (which writes to local bridge/ files)
|
|
||||||
and the remote Gateway (which manages Discord).
|
|
||||||
|
|
||||||
Flow:
|
|
||||||
Extension → bridge/pending/ → Collector → POST Gateway /api/pending
|
|
||||||
Gateway /api/response/{rid} → Collector → bridge/response/ → Extension
|
|
||||||
Gateway /api/commands/{project} → Collector → bridge/commands/ → Extension
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
import uuid
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from bridge import LocalTransport, RemoteTransport
|
|
||||||
from config import Config
|
|
||||||
from watcher import BrainEvent, EventType
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CollectorBridge:
|
|
||||||
"""Bridges local file-based bridge with remote Gateway API.
|
|
||||||
|
|
||||||
Periodically:
|
|
||||||
1. Scans local pending/ → forwards new ones to Gateway
|
|
||||||
2. Polls Gateway for responses → writes to local response/
|
|
||||||
3. Polls Gateway for commands → writes to local commands/
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, local: LocalTransport, remote: RemoteTransport,
|
|
||||||
project_name: str, event_queue: asyncio.Queue | None = None):
|
|
||||||
self.local = local
|
|
||||||
self.remote = remote
|
|
||||||
self.project_name = project_name
|
|
||||||
self.event_queue = event_queue
|
|
||||||
self._poll_interval = 5 # seconds (was 3 — reduced I/O frequency)
|
|
||||||
self._running = False
|
|
||||||
|
|
||||||
# Pre-populate with existing pending files → skip on startup (prevents 만료됨 spam)
|
|
||||||
self._startup_pending: set[str] = set()
|
|
||||||
self._forwarded_pending: set[str] = set()
|
|
||||||
self._forwarded_timestamps: dict[str, float] = {} # rid → when forwarded
|
|
||||||
self._pending_hashes: dict[str, str] = {} # rid → content hash (for MERGE/status detection)
|
|
||||||
self._pending_mtimes: dict[str, float] = {} # rid → last known file mtime
|
|
||||||
self._RESPONSE_POLL_TTL = 300 # 5 min — stop polling responses for old pending
|
|
||||||
|
|
||||||
# Project discovery cache (avoid re-reading register/ every cycle)
|
|
||||||
self._cached_projects: set[str] | None = None
|
|
||||||
self._projects_cache_ts: float = 0
|
|
||||||
self._PROJECTS_CACHE_TTL = 60.0 # seconds
|
|
||||||
for fname in self.local.list_json_files("pending"):
|
|
||||||
rid = fname.replace(".json", "")
|
|
||||||
self._startup_pending.add(rid)
|
|
||||||
self._forwarded_pending.add(rid)
|
|
||||||
# Pre-hash existing files
|
|
||||||
data = self.local.read_json("pending", fname)
|
|
||||||
if data:
|
|
||||||
self._pending_hashes[rid] = hashlib.md5(
|
|
||||||
json.dumps(data, sort_keys=True).encode()
|
|
||||||
).hexdigest()
|
|
||||||
# Pre-cache mtime
|
|
||||||
try:
|
|
||||||
fpath = self.local.bridge_dir / "pending" / fname
|
|
||||||
self._pending_mtimes[rid] = fpath.stat().st_mtime
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
if self._startup_pending:
|
|
||||||
logger.info(f"[COLLECTOR] skipping {len(self._startup_pending)} existing pending files")
|
|
||||||
|
|
||||||
# Startup cleanup: remove stale response files (> 5 min)
|
|
||||||
self._cleanup_stale_responses()
|
|
||||||
|
|
||||||
def _cleanup_stale_responses(self, max_age: int = 300):
|
|
||||||
"""Remove stale response files (> max_age seconds) on startup."""
|
|
||||||
now = time.time()
|
|
||||||
cleaned = 0
|
|
||||||
for fname in self.local.list_json_files("response"):
|
|
||||||
try:
|
|
||||||
fpath = self.local.bridge_dir / "response" / fname
|
|
||||||
if now - fpath.stat().st_mtime > max_age:
|
|
||||||
self.local.delete_file("response", fname)
|
|
||||||
cleaned += 1
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
if cleaned:
|
|
||||||
logger.info(f"[COLLECTOR] startup cleanup: removed {cleaned} stale response files")
|
|
||||||
|
|
||||||
async def start(self):
|
|
||||||
"""Start the Collector polling loops with staggered offsets.
|
|
||||||
|
|
||||||
Each loop starts with a different delay to prevent all loops from waking
|
|
||||||
up at the same time and causing burst requests to Gateway.
|
|
||||||
"""
|
|
||||||
self._running = True
|
|
||||||
logger.info(f"[COLLECTOR] started for project={self.project_name}")
|
|
||||||
|
|
||||||
async def _staggered(coro, offset: float):
|
|
||||||
await asyncio.sleep(offset)
|
|
||||||
await coro()
|
|
||||||
|
|
||||||
tasks = [
|
|
||||||
_staggered(self._forward_pending_loop, 0.0),
|
|
||||||
_staggered(self._poll_responses_loop, 0.5),
|
|
||||||
_staggered(self._poll_commands_loop, 1.0),
|
|
||||||
_staggered(self._forward_chat_snapshots_loop, 1.5),
|
|
||||||
_staggered(self._forward_registrations_loop, 2.0),
|
|
||||||
_staggered(self._health_check_loop, 2.5),
|
|
||||||
_staggered(self._retry_flush_loop, 3.0),
|
|
||||||
]
|
|
||||||
if self.event_queue:
|
|
||||||
tasks.append(_staggered(self._forward_events_loop, 3.5))
|
|
||||||
await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
async def stop(self):
|
|
||||||
"""Stop the Collector and close HTTP session."""
|
|
||||||
self._running = False
|
|
||||||
await self.remote.close()
|
|
||||||
logger.info("[COLLECTOR] stopped")
|
|
||||||
|
|
||||||
# ─── Forward local pending → Gateway ───
|
|
||||||
|
|
||||||
async def _forward_pending_loop(self):
|
|
||||||
"""Scan local pending/ and forward new + updated requests to Gateway.
|
|
||||||
|
|
||||||
Tracks content hashes to detect:
|
|
||||||
- New pending files → forward immediately
|
|
||||||
- MERGE updates (step_probe updates command text) → re-forward
|
|
||||||
- Status changes (auto_resolved, expired) → re-forward
|
|
||||||
"""
|
|
||||||
while self._running:
|
|
||||||
try:
|
|
||||||
# Skip cycle if rate-limited
|
|
||||||
if self.remote.is_rate_limited:
|
|
||||||
await asyncio.sleep(self._poll_interval)
|
|
||||||
continue
|
|
||||||
|
|
||||||
current_files = set()
|
|
||||||
for fname in self.local.list_json_files("pending"):
|
|
||||||
rid = fname.replace(".json", "")
|
|
||||||
current_files.add(rid)
|
|
||||||
|
|
||||||
# mtime pre-check: skip read+hash if file hasn't been modified
|
|
||||||
try:
|
|
||||||
fpath = self.local.bridge_dir / "pending" / fname
|
|
||||||
current_mtime = fpath.stat().st_mtime
|
|
||||||
except OSError:
|
|
||||||
continue
|
|
||||||
prev_mtime = self._pending_mtimes.get(rid)
|
|
||||||
if prev_mtime is not None and current_mtime == prev_mtime:
|
|
||||||
continue # File untouched since last check — skip read+hash
|
|
||||||
self._pending_mtimes[rid] = current_mtime
|
|
||||||
|
|
||||||
data = self.local.read_json("pending", fname)
|
|
||||||
if data is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Compute content hash to detect changes
|
|
||||||
content_hash = hashlib.md5(
|
|
||||||
json.dumps(data, sort_keys=True).encode()
|
|
||||||
).hexdigest()
|
|
||||||
|
|
||||||
prev_hash = self._pending_hashes.get(rid)
|
|
||||||
if prev_hash == content_hash:
|
|
||||||
continue # No change
|
|
||||||
|
|
||||||
is_new = rid not in self._forwarded_pending
|
|
||||||
if rid in self._startup_pending:
|
|
||||||
# Startup files: only forward status CHANGES (not re-forward as new pending)
|
|
||||||
status = data.get("status", "pending")
|
|
||||||
if status == "pending":
|
|
||||||
continue # Still pending from before startup — skip
|
|
||||||
# Status changed (auto_resolved/expired) — forward the update
|
|
||||||
|
|
||||||
# Forward to Gateway (new or updated)
|
|
||||||
await self.remote.awrite_json("pending", fname, data)
|
|
||||||
self._forwarded_pending.add(rid)
|
|
||||||
self._forwarded_timestamps[rid] = time.time()
|
|
||||||
self._pending_hashes[rid] = content_hash
|
|
||||||
|
|
||||||
if is_new:
|
|
||||||
logger.info(f"[COLLECTOR] → Gateway: pending {rid[:12]}")
|
|
||||||
else:
|
|
||||||
status = data.get("status", "?")
|
|
||||||
logger.info(f"[COLLECTOR] → Gateway: pending UPDATE {rid[:12]} status={status}")
|
|
||||||
|
|
||||||
# Clean up tracking for deleted files
|
|
||||||
for rid in list(self._forwarded_pending):
|
|
||||||
if rid not in current_files and rid not in self._startup_pending:
|
|
||||||
self._forwarded_pending.discard(rid)
|
|
||||||
self._pending_hashes.pop(rid, None)
|
|
||||||
self._pending_mtimes.pop(rid, None)
|
|
||||||
# Also clean up orphaned hashes/mtimes for files no longer on disk
|
|
||||||
for rid in list(self._pending_hashes):
|
|
||||||
if rid not in current_files:
|
|
||||||
self._pending_hashes.pop(rid, None)
|
|
||||||
self._pending_mtimes.pop(rid, None)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[COLLECTOR] forward_pending error: {e}")
|
|
||||||
|
|
||||||
await asyncio.sleep(self._poll_interval)
|
|
||||||
|
|
||||||
# ─── Poll Gateway responses → local ───
|
|
||||||
|
|
||||||
async def _poll_responses_loop(self):
|
|
||||||
"""Poll Gateway for responses and write them locally for Extension.
|
|
||||||
|
|
||||||
Only polls responses for recently-forwarded pending (within _RESPONSE_POLL_TTL).
|
|
||||||
Expired entries are removed from tracking to prevent request accumulation.
|
|
||||||
"""
|
|
||||||
while self._running:
|
|
||||||
try:
|
|
||||||
# Skip cycle if rate-limited
|
|
||||||
if self.remote.is_rate_limited:
|
|
||||||
await asyncio.sleep(self._poll_interval)
|
|
||||||
continue
|
|
||||||
|
|
||||||
now = time.time()
|
|
||||||
# Clean up expired forwarded pending (stop polling responses for old ones)
|
|
||||||
expired = [
|
|
||||||
rid for rid, ts in self._forwarded_timestamps.items()
|
|
||||||
if now - ts > self._RESPONSE_POLL_TTL
|
|
||||||
]
|
|
||||||
for rid in expired:
|
|
||||||
self._forwarded_pending.discard(rid)
|
|
||||||
self._forwarded_timestamps.pop(rid, None)
|
|
||||||
# NOTE: intentionally keep _pending_hashes[rid] to prevent
|
|
||||||
# re-forward cycle (expired pending would be re-detected as
|
|
||||||
# "new" if hash is cleared). Hash is cleaned up when file
|
|
||||||
# is actually deleted from disk (see _forward_pending_loop).
|
|
||||||
if expired:
|
|
||||||
logger.info(f"[COLLECTOR] expired {len(expired)} stale forwarded pending (>{self._RESPONSE_POLL_TTL}s)")
|
|
||||||
|
|
||||||
# Check each active forwarded pending for a response
|
|
||||||
active_rids = [
|
|
||||||
rid for rid in self._forwarded_pending
|
|
||||||
if rid not in self._startup_pending
|
|
||||||
]
|
|
||||||
for rid in active_rids:
|
|
||||||
# Rate-limit guard: stop polling if we got rate-limited mid-cycle
|
|
||||||
if self.remote.is_rate_limited:
|
|
||||||
break
|
|
||||||
data = await self.remote.aread_json("response", f"{rid}.json")
|
|
||||||
if data is None or data.get("waiting"):
|
|
||||||
await asyncio.sleep(0.3) # Throttle between individual response polls
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Write response locally for Extension to pick up
|
|
||||||
self.local.write_json("response", f"{rid}.json", data)
|
|
||||||
# Also delete local pending file (Extension expects this)
|
|
||||||
self.local.delete_file("pending", f"{rid}.json")
|
|
||||||
self._forwarded_pending.discard(rid)
|
|
||||||
self._forwarded_timestamps.pop(rid, None)
|
|
||||||
approved = data.get("approved", "?")
|
|
||||||
logger.info(f"[COLLECTOR] ← Gateway: response {rid[:12]} approved={approved}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[COLLECTOR] poll_responses error: {e}")
|
|
||||||
|
|
||||||
await asyncio.sleep(self._poll_interval)
|
|
||||||
|
|
||||||
# ─── Poll Gateway commands → local ───
|
|
||||||
|
|
||||||
def _discover_local_projects(self) -> set[str]:
|
|
||||||
"""Discover all project names registered by local Extension instances.
|
|
||||||
|
|
||||||
Reads bridge/register/*.json files, which are written by each AG window's
|
|
||||||
Extension with {conversation_id, project_name}. Returns unique project names
|
|
||||||
found, always including self.project_name as a fallback.
|
|
||||||
|
|
||||||
Results are cached for _PROJECTS_CACHE_TTL seconds to avoid re-reading
|
|
||||||
22+ register files every polling cycle.
|
|
||||||
"""
|
|
||||||
now = time.time()
|
|
||||||
if self._cached_projects is not None and now - self._projects_cache_ts < self._PROJECTS_CACHE_TTL:
|
|
||||||
return self._cached_projects
|
|
||||||
|
|
||||||
projects = {self.project_name}
|
|
||||||
register_dir = self.local.bridge_dir / "register"
|
|
||||||
if not register_dir.exists():
|
|
||||||
self._cached_projects = projects
|
|
||||||
self._projects_cache_ts = now
|
|
||||||
return projects
|
|
||||||
for f in register_dir.glob("*.json"):
|
|
||||||
try:
|
|
||||||
data = json.loads(f.read_text(encoding="utf-8-sig"))
|
|
||||||
p = data.get("project_name", "")
|
|
||||||
if p:
|
|
||||||
projects.add(p)
|
|
||||||
except (json.JSONDecodeError, OSError):
|
|
||||||
pass
|
|
||||||
self._cached_projects = projects
|
|
||||||
self._projects_cache_ts = now
|
|
||||||
return projects
|
|
||||||
|
|
||||||
async def _poll_commands_loop(self):
|
|
||||||
"""Poll Gateway for commands with adaptive per-project intervals.
|
|
||||||
|
|
||||||
When a project returns empty commands repeatedly, its poll interval
|
|
||||||
increases (3s → 10s → 30s → 60s). On receiving a command, interval
|
|
||||||
resets to base. This prevents idle projects from wasting requests.
|
|
||||||
"""
|
|
||||||
# Per-project adaptive state
|
|
||||||
project_intervals: dict[str, float] = {} # project → current interval
|
|
||||||
project_last_poll: dict[str, float] = {} # project → last poll timestamp
|
|
||||||
_BASE_INTERVAL = 3.0
|
|
||||||
_IDLE_STEPS = [10.0, 30.0, 60.0] # progressive idle intervals
|
|
||||||
project_empty_streak: dict[str, int] = {} # project → consecutive empty polls
|
|
||||||
|
|
||||||
while self._running:
|
|
||||||
try:
|
|
||||||
# Skip cycle if rate-limited
|
|
||||||
if not self.remote.is_rate_limited:
|
|
||||||
projects = self._discover_local_projects()
|
|
||||||
now = time.time()
|
|
||||||
for project in projects:
|
|
||||||
if self.remote.is_rate_limited:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Check if this project's interval has elapsed
|
|
||||||
interval = project_intervals.get(project, _BASE_INTERVAL)
|
|
||||||
last = project_last_poll.get(project, 0)
|
|
||||||
if now - last < interval:
|
|
||||||
continue # Not time yet for this project
|
|
||||||
|
|
||||||
project_last_poll[project] = now
|
|
||||||
commands = await self.remote.apoll_commands(project)
|
|
||||||
|
|
||||||
if commands:
|
|
||||||
# Got commands → reset to base interval
|
|
||||||
project_intervals[project] = _BASE_INTERVAL
|
|
||||||
project_empty_streak[project] = 0
|
|
||||||
for cmd in commands:
|
|
||||||
cmd_id = cmd.get("id", f"{int(time.time() * 1000)}_{uuid.uuid4().hex[:8]}")
|
|
||||||
fname = f"{cmd_id}.json"
|
|
||||||
self.local.write_json("commands", fname, cmd)
|
|
||||||
logger.info(f"[COLLECTOR] ← Gateway: command [{project}] {cmd.get('text', '?')[:30]}")
|
|
||||||
else:
|
|
||||||
# Empty → increase interval progressively
|
|
||||||
streak = project_empty_streak.get(project, 0) + 1
|
|
||||||
project_empty_streak[project] = streak
|
|
||||||
if streak <= len(_IDLE_STEPS):
|
|
||||||
project_intervals[project] = _IDLE_STEPS[streak - 1]
|
|
||||||
# else stays at max (60s)
|
|
||||||
|
|
||||||
await asyncio.sleep(0.3) # Throttle between projects
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[COLLECTOR] poll_commands error: {e}")
|
|
||||||
|
|
||||||
await asyncio.sleep(self._poll_interval)
|
|
||||||
|
|
||||||
# ─── Forward chat snapshots → Gateway ───
|
|
||||||
|
|
||||||
async def _forward_chat_snapshots_loop(self):
|
|
||||||
"""Forward chat_snapshots/ from Extension to Gateway."""
|
|
||||||
while self._running:
|
|
||||||
try:
|
|
||||||
snap_dir = self.local.bridge_dir / "chat_snapshots"
|
|
||||||
if snap_dir.exists():
|
|
||||||
for f in snap_dir.glob("*.json"):
|
|
||||||
try:
|
|
||||||
data = json.loads(f.read_text(encoding="utf-8-sig"))
|
|
||||||
project = data.get("project_name", self.project_name)
|
|
||||||
content = data.get("content", "")
|
|
||||||
attached_files = data.get("attached_files", [])
|
|
||||||
if content or attached_files:
|
|
||||||
await self.remote.asend_chat(project, content, attached_files=attached_files)
|
|
||||||
af_info = f" +{len(attached_files)} files" if attached_files else ""
|
|
||||||
logger.info(f"[COLLECTOR] → Gateway: chat snapshot len={len(content)}{af_info}")
|
|
||||||
f.unlink() # Cleanup after forwarding
|
|
||||||
except (json.JSONDecodeError, OSError) as e:
|
|
||||||
logger.warning(f"[COLLECTOR] bad chat snapshot {f.name}: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[COLLECTOR] forward_chat_snapshots error: {e}")
|
|
||||||
|
|
||||||
await asyncio.sleep(10) # Chat snapshots: less urgent, 10s interval
|
|
||||||
|
|
||||||
# ─── Forward session registrations → Gateway ───
|
|
||||||
|
|
||||||
async def _forward_registrations_loop(self):
|
|
||||||
"""Forward register/ files from Extension to Gateway."""
|
|
||||||
forwarded_regs: set[str] = set()
|
|
||||||
while self._running:
|
|
||||||
try:
|
|
||||||
register_dir = self.local.bridge_dir / "register"
|
|
||||||
if register_dir.exists():
|
|
||||||
for f in register_dir.glob("*.json"):
|
|
||||||
if f.name in forwarded_regs:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
data = json.loads(f.read_text(encoding="utf-8-sig"))
|
|
||||||
conv_id = data.get("conversation_id", "")
|
|
||||||
project = data.get("project_name", "")
|
|
||||||
if conv_id and project:
|
|
||||||
await self.remote.aregister_session(conv_id, project)
|
|
||||||
forwarded_regs.add(f.name)
|
|
||||||
logger.info(f"[COLLECTOR] → Gateway: register {conv_id[:8]} → {project}")
|
|
||||||
await asyncio.sleep(0.3) # Spread startup burst
|
|
||||||
except (json.JSONDecodeError, OSError) as e:
|
|
||||||
logger.warning(f"[COLLECTOR] bad register {f.name}: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[COLLECTOR] forward_registrations error: {e}")
|
|
||||||
|
|
||||||
await asyncio.sleep(30) # Registration changes rarely — 30s interval
|
|
||||||
# ─── Forward brain events → Gateway ───
|
|
||||||
|
|
||||||
async def _forward_events_loop(self):
|
|
||||||
"""Read BrainEvents from Watcher queue and POST to Gateway."""
|
|
||||||
while self._running:
|
|
||||||
try:
|
|
||||||
event: BrainEvent = await asyncio.wait_for(
|
|
||||||
self.event_queue.get(), timeout=5.0
|
|
||||||
)
|
|
||||||
# Serialize event to JSON
|
|
||||||
event_data = {
|
|
||||||
"event_type": event.event_type.value,
|
|
||||||
"conversation_id": event.conversation_id,
|
|
||||||
"file_name": event.file_name,
|
|
||||||
"file_path": str(event.file_path) if event.file_path else "",
|
|
||||||
"content": event.content,
|
|
||||||
"timestamp": event.timestamp,
|
|
||||||
}
|
|
||||||
await self.remote.asend_event(event_data)
|
|
||||||
logger.info(f"[COLLECTOR] → Gateway: event {event.event_type.value} {event.file_name}")
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
continue
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[COLLECTOR] forward_event error: {e}")
|
|
||||||
|
|
||||||
# ─── Health check ───
|
|
||||||
|
|
||||||
async def _health_check_loop(self):
|
|
||||||
"""Periodically check Gateway connectivity."""
|
|
||||||
while self._running:
|
|
||||||
try:
|
|
||||||
ok = await self.remote.health_check()
|
|
||||||
if not ok and self.remote.connected:
|
|
||||||
logger.warning("[COLLECTOR] ❌ Gateway health check failed")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
await asyncio.sleep(30)
|
|
||||||
|
|
||||||
# ─── Retry flush ───
|
|
||||||
|
|
||||||
async def _retry_flush_loop(self):
|
|
||||||
"""Periodically flush failed request retry queue."""
|
|
||||||
while self._running:
|
|
||||||
try:
|
|
||||||
await self.remote.flush_retry_queue()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[COLLECTOR] retry flush error: {e}")
|
|
||||||
await asyncio.sleep(30) # Retry flush: 30s interval (was 10s)
|
|
||||||
@@ -43,11 +43,14 @@ class Config:
|
|||||||
CHANNEL_PREFIX: str = "AG"
|
CHANNEL_PREFIX: str = "AG"
|
||||||
PROJECT_NAME: str = os.getenv("PROJECT_NAME", "gravity_control")
|
PROJECT_NAME: str = os.getenv("PROJECT_NAME", "gravity_control")
|
||||||
|
|
||||||
# Bot mode: 'local' (file-based bridge) or 'remote' (HTTP polling — future)
|
# Bot mode: 'local' (file-based bridge) or 'gateway' (WS Hub + HTTP API)
|
||||||
BOT_MODE: str = os.getenv("BOT_MODE", "local")
|
BOT_MODE: str = os.getenv("BOT_MODE", "local")
|
||||||
REMOTE_BRIDGE_URL: str = os.getenv("REMOTE_BRIDGE_URL", "")
|
|
||||||
GATEWAY_API_KEY: str = os.getenv("GATEWAY_API_KEY", "")
|
GATEWAY_API_KEY: str = os.getenv("GATEWAY_API_KEY", "")
|
||||||
|
|
||||||
|
# WebSocket Hub
|
||||||
|
GRAVITY_HUB_SECRET: str = os.getenv("GRAVITY_HUB_SECRET", "") # JWT signing secret
|
||||||
|
GRAVITY_REGISTRATION_CODE: str = os.getenv("GRAVITY_REGISTRATION_CODE", "") # Extension auth
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls) -> list[str]:
|
def validate(cls) -> list[str]:
|
||||||
"""Return list of configuration errors."""
|
"""Return list of configuration errors."""
|
||||||
|
|||||||
0
diag_output.txt
Normal file
0
diag_output.txt
Normal file
@@ -3,39 +3,28 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
container_name: gravity-gateway
|
container_name: gravity-gateway
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# Port NOT exposed directly — Caddy handles external access
|
ports:
|
||||||
expose:
|
- "127.0.0.1:8585:8585"
|
||||||
- "8585"
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- DISCORD_TOKEN=${DISCORD_TOKEN}
|
|
||||||
- DISCORD_GUILD_ID=${DISCORD_GUILD_ID}
|
|
||||||
- BOT_MODE=gateway
|
- BOT_MODE=gateway
|
||||||
- GATEWAY_PORT=8585
|
- GATEWAY_PORT=8585
|
||||||
- GATEWAY_API_KEY=${GATEWAY_API_KEY}
|
|
||||||
- BRAIN_PATH=/app/data/brain
|
- BRAIN_PATH=/app/data/brain
|
||||||
volumes:
|
volumes:
|
||||||
- gateway-data:/app/data
|
- gateway-data:/app/data
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- proxy-net
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options:
|
options:
|
||||||
max-size: "10m"
|
max-size: 10m
|
||||||
max-file: "3"
|
max-file: 3
|
||||||
|
|
||||||
caddy:
|
|
||||||
image: caddy:2-alpine
|
|
||||||
container_name: gravity-caddy
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "443:443"
|
|
||||||
- "80:80"
|
|
||||||
volumes:
|
|
||||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
||||||
- caddy-data:/data
|
|
||||||
- caddy-config:/config
|
|
||||||
depends_on:
|
|
||||||
- gateway
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
gateway-data:
|
gateway-data:
|
||||||
caddy-data:
|
|
||||||
caddy-config:
|
networks:
|
||||||
|
proxy-net:
|
||||||
|
external: true
|
||||||
|
|||||||
30
docker-compose_server.yml
Normal file
30
docker-compose_server.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
services:
|
||||||
|
gateway:
|
||||||
|
build: .
|
||||||
|
container_name: gravity-gateway
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8585:8585"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- BOT_MODE=gateway
|
||||||
|
- GATEWAY_PORT=8585
|
||||||
|
- BRAIN_PATH=/app/data/brain
|
||||||
|
volumes:
|
||||||
|
- gateway-data:/app/data
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- proxy-net
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: 10m
|
||||||
|
max-file: 3
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
gateway-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy-net:
|
||||||
|
external: true
|
||||||
@@ -4,5 +4,9 @@
|
|||||||
|---|------|------|------|------|
|
|---|------|------|------|------|
|
||||||
| 001 | 07:30~11:10 | 승인 상태 관리 근본 원인 분석 + v0.3.12 수정 (sawRunningAfterPending gate) + approval-flow.md 시스템 Flow 문서 + known-issues 2건 추가 | `2d9fe96` | ✅ |
|
| 001 | 07:30~11:10 | 승인 상태 관리 근본 원인 분석 + v0.3.12 수정 (sawRunningAfterPending gate) + approval-flow.md 시스템 Flow 문서 + known-issues 2건 추가 | `2d9fe96` | ✅ |
|
||||||
| 002 | 13:25~14:20 | diff_review 핸들러 2-strategy 리팩토링 + 배포 불일치 발견/수정 + pending 순서 8초 지연 + 1차 테스트 (버튼 OK, RPC 미배포→재배포) + known-issues 2건 | `f302984` | ✅ |
|
| 002 | 13:25~14:20 | diff_review 핸들러 2-strategy 리팩토링 + 배포 불일치 발견/수정 + pending 순서 8초 지연 + 1차 테스트 (버튼 OK, RPC 미배포→재배포) + known-issues 2건 | `f302984` | ✅ |
|
||||||
| 003 | 15:18~16:55 | diff_review steps=[] 근본 원인 분석 + 인메모리 캐시 (v0.3.13) + 3차 E2E (RPC SUCCESS but no-op) + 4가지 파라미터 실험 배포 | `00b9491` | 🔧 |
|
| 003 | 15:18~16:55 | diff_review steps=[] 근본 원인 분석 + 인메모리 캐시 (v0.3.13) + 3차 E2E (RPC SUCCESS but no-op) + 4가지 파라미터 실험 배포 | `00b9491` | ✅ |
|
||||||
| 004 | 17:05~18:00 | AG 소스 역분석 — `AcknowledgeCascadeCodeEdit`→`acknowledgeCodeActionStep` 메서드명 오류 발견 + v0.3.14 3단계 전략 배포 + known-issues 2건 업데이트 | `08c2c86` | 🔧 |
|
| 004 | 17:05~18:00 | AG 소스 역분석 — `AcknowledgeCascadeCodeEdit`→`acknowledgeCodeActionStep` 메서드명 오류 발견 + v0.3.14 3단계 전략 배포 + known-issues 2건 업데이트 | `5a1d4f0` | ✅ |
|
||||||
|
| 005 | 18:13~18:43 | v0.3.14 E2E 테스트 → RPC 3개 전략 모두 실패 확인 + v0.3.15 agentAcceptAllInFile 전환 배포 + known-issues 업데이트 | `0fdf668` | ✅ |
|
||||||
|
| 006 | 18:47~19:09 | v0.3.15 diff_review E2E 2회 성공 + 이중 승인 수정 + IDLE 종료 알림 + !auto 이중 메시지 수정 (v0.3.16) + known-issues 2건 | `3cd7122` | ✅ |
|
||||||
|
| 007 | 19:17~20:38 | Discord 알림 누락 디버깅 — Bot snapshot 로깅 추가 + 병렬 WAITING step break 제거 + 서버 Docker 재배포 3회 + known-issues 2건 | `7f079a5` | ✅ |
|
||||||
|
| 008 | 20:50~23:06 | 크로스 프로젝트 알림 폭주 + pending 139개 누적 + diff_review brain/ 거짓양성 — 근본 원인 6건 분석 + Watcher 프로젝트 필터 + Collector stale 정리 + Extension brain/ 제외 + known-issues 3건 | `e3f8fb9` | ✅ |
|
||||||
|
|||||||
28
docs/devlog/2026-03-17.md
Normal file
28
docs/devlog/2026-03-17.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Devlog — 2026-03-17
|
||||||
|
|
||||||
|
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||||
|
|---|------|------|------|------|
|
||||||
|
| 009 | 00:00~06:38 | Extension 모듈 분리 + Hub 통합 테스트 + VSIX v0.4.0 빌드 | `5f795b9` | ✅ |
|
||||||
|
| 010 | 06:50~07:39 | 문서 전면 재작성 + 서버 배포 + WS 호환 수정 | `6ea3211` | ✅ |
|
||||||
|
| 011 | 07:44~08:18 | VSIX v0.4.0 E2E 사전 검증 + WS 프록시 수정 | — | 🔧 |
|
||||||
|
| 012 | 09:00~17:44 | VSIX E2E: workspaceUri, 이중발송, ApprovalRequest, ApprovalView WS, 응답 라우팅 | `2eea5fa` | ✅ |
|
||||||
|
| 013 | 18:05~18:45 | Extension 모듈 분리 #398: http-bridge, html-patcher, command-handler 추출 (1296→650줄) | `6640d42` | ✅ |
|
||||||
|
| 014 | 18:45~20:35 | WS+File dual-delivery 수정 + 에코 릴레이 수정 + VSIX v0.4.4 빌드 | `0da6291` | ✅ |
|
||||||
|
| 015 | 20:45~21:00 | Accept All WS regression 수정 + auto_approve 이중쓰기 수정 + VSIX v0.4.5 | `47cc838` | ✅ |
|
||||||
|
| 016 | 21:00~21:27 | 통신 아키텍처 나노단위 감사: writeRegistration 이중쓰기 + ApprovalView fallback + scanner 최적화 | — | ✅ |
|
||||||
|
| 017 | 21:35~21:53 | Hub pending_owners 생명주기 수정: WS 재연결 시 승인 응답 소실 방지 (reconnect reassign + fallback routing) | `9ccfa83` | ✅ |
|
||||||
|
|
||||||
|
### #010 상세
|
||||||
|
- **문서**: architecture.md(250줄), tech-stack.md(100줄), conventions.md(100줄) 전면 재작성 + Wiki 동기화
|
||||||
|
- **태스크 정리**: #296 폐기, #396~#400 신규 5건 등록
|
||||||
|
- **서버 배포**: docker-compose.yml 서버 실제 구성 반영, Caddyfile 제거, ag.variet.net 도메인 확인
|
||||||
|
- **WS 호환**: ws-client.ts 브라우저 WebSocket API 호환 (.onopen/.onmessage) 수정
|
||||||
|
- **Known issue**: VS Code 캐시로 Extension 코드 반영 지연 — 완전 재시작 필요
|
||||||
|
|
||||||
|
### #011 상세
|
||||||
|
- **WS 프록시 수정**: NPM(openresty)에서 WebSocket Support 활성화 → 101 Switching Protocols 확인
|
||||||
|
- **WS 인증 검증**: `wss://ag.variet.net/ws` → auth_ok, conn_id 발급, instance=#1 확인
|
||||||
|
- **VSIX 설치**: v0.4.0 설치 확인, v0.3.16 제거, ws 모듈 수동 복사
|
||||||
|
- **AG 설정**: `settings.json`에 hubUrl + registrationCode 설정
|
||||||
|
- **ws 번들**: `.vscodeignore`에 `!node_modules/ws/**` 추가, `package.json`에 ws dependency
|
||||||
|
- **미완료**: AG 재시작 후 Extension→Hub→Bot→Discord 실제 E2E 검증 필요
|
||||||
8
docs/devlog/2026-03-18.md
Normal file
8
docs/devlog/2026-03-18.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 2026-03-18 Devlog
|
||||||
|
|
||||||
|
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||||
|
|---|------|------|------|------|
|
||||||
|
| 1 | 11:00 | v0.5.0 Collector 제거 + dead code 정리 + HttpBridgeContext 버그 수정 | `e763117` | ✅ |
|
||||||
|
| 2 | 14:00 | bot.py unit tests 27건 — _write_command, _hub_on_pending, ApprovalView | `a41062b` | ✅ |
|
||||||
|
| 3 | 14:30 | step-probe.ts 모듈 분리 → approval-handler.ts (1597→1017+411줄) + dead code 제거 | `17978a7` | ✅ |
|
||||||
|
| 4 | 15:30 | 코드베이스 건강도 분석 + 통신 레이어 전수 감사 (8파일/7메시지타입) → 수정 필요 0건 | — | ✅ |
|
||||||
6
docs/devlog/2026-03-19.md
Normal file
6
docs/devlog/2026-03-19.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 2026-03-19 Devlog
|
||||||
|
|
||||||
|
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||||
|
|---|------|------|------|------|
|
||||||
|
| 1 | 07:30 | v0.5.1 browser_subagent Allow RPC 매핑 수정 + .env 정리 | `549af6d` | ✅ |
|
||||||
|
| 2 | 10:35 | v0.5.2 Idle→Resume 신호 소실 3중 버그 수정: auth_fail 재연결, pending_owners 보존, step-probe 리셋 | `5aad82c` | ✅ |
|
||||||
6
docs/devlog/2026-03-21.md
Normal file
6
docs/devlog/2026-03-21.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 2026-03-21 Devlog
|
||||||
|
|
||||||
|
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||||
|
|---|------|------|------|------|
|
||||||
|
| 1 | 17:48 | v0.5.3~v0.5.4 신호 감지 3중 버그 수정: 세션 전환 즉시 probe (20-25s→5s), reviewAbsoluteUris 필드 수정, stepIndex=-1 uint32 에러 수정 + permission 매핑 | `0fb33a9` | ✅ |
|
||||||
|
| 2 | 21:14 | v0.5.5 wrong-LS 자동 복구: Deriva RPC "input not registered" 근본 원인 분석 → fixLSConnection export + single-LS 조기종료 제거 + approval-handler 자동 LS 재연결 + 1회 retry | `6234301` | ✅ |
|
||||||
5
docs/devlog/2026-03-22.md
Normal file
5
docs/devlog/2026-03-22.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 2026-03-22 Devlog
|
||||||
|
|
||||||
|
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||||
|
|---|------|------|------|------|
|
||||||
|
| 1 | 01:22 | VSIX v0.5.5 빌드 — package.json 버전 범프 + vsce package | `b81135d` | ✅ |
|
||||||
6
docs/devlog/2026-03-23.md
Normal file
6
docs/devlog/2026-03-23.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 2026-03-23 Devlog
|
||||||
|
|
||||||
|
| NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 |
|
||||||
|
|-----|-------|----------|-----------|-----------|
|
||||||
|
| 001 | 21:09 | WebSocket 좀비 커넥션 해결 및 통신망 메모리 누수 패치 | `ecebec3` | ✅ |
|
||||||
|
| 002 | 22:45 | Cross-Project DOM Observer Leakage 패치 및 포트 동적 디스커버리 적용 | `TBD` | ✅ |
|
||||||
7
docs/devlog/2026-03-24.md
Normal file
7
docs/devlog/2026-03-24.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# 2026-03-24 Devlog
|
||||||
|
|
||||||
|
| NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 |
|
||||||
|
|-----|-------|----------|-----------|-----------|
|
||||||
|
| 001 | 07:05 | v0.5.6 좀비 커넥션 패치 회귀 오류 해결 (False Positive 끊김 방지를 위한 타임스탬프 검증 도입 v0.5.8) | `f13bcc8` | ✅ |
|
||||||
|
| 002 | 13:00 | DOM Observer VS Code 네이티브 알림 UI 캡처 블라인드 스팟 해결 (v0.5.9) | `7b6cd59` | ✅ |
|
||||||
|
| 003 | 18:14 | DOM Observer /trigger-click 렌더링 순서 오작동 및 False Positive 프리징 해결 (v0.5.10) | `101ec20` | ✅ |
|
||||||
5
docs/devlog/2026-03-25.md
Normal file
5
docs/devlog/2026-03-25.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 2026-03-25 Devlog
|
||||||
|
|
||||||
|
| NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 |
|
||||||
|
|-----|-------|----------|-----------|-----------|
|
||||||
|
| 001 | 07:15 | ws-client reconnect pacing 및 http-bridge 정규식 필터 완화로 Signal Drop 해결 (v0.5.10) | `pending` | ✅ |
|
||||||
5
docs/devlog/2026-03-28.md
Normal file
5
docs/devlog/2026-03-28.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Devlog — 2026-03-28
|
||||||
|
|
||||||
|
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||||
|
|---|------|------|------|------|
|
||||||
|
| 001 | 09:12 | guitar_score step-probe UTF-8 무한루프 수정 + approval stepIndex 보정 (v0.5.11) | `7bbd874` | ✅ #539 |
|
||||||
5
docs/devlog/2026-04-01.md
Normal file
5
docs/devlog/2026-04-01.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 2026-04-01 Devlog
|
||||||
|
|
||||||
|
| NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 |
|
||||||
|
|-------|-------|-----------|-------------|--------------|
|
||||||
|
| 001 | 18:22 | `step-probe` 10-Item Truncation/DoS 우회 (vsix v0.5.14) | `TBD` | ✅ |
|
||||||
8
docs/devlog/2026-04-08.md
Normal file
8
docs/devlog/2026-04-08.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 2026-04-08
|
||||||
|
|
||||||
|
| NNN | HH:MM | 작업 설명 | `커밋해시` | 상태 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 004 | 14:00 | SafeToAutoRun 알림 누락 복구 (v0.5.18) | `8f2a1b3` | ✅ |
|
||||||
|
| 005 | 16:30 | SafeToAutoRun pending skip으로 인한 데드락 원인 파악 및 롤백 | `13f13ee` | ✅ |
|
||||||
|
| 006 | 07:30 | SafeToAutoRun 데드락 완전 해결을 위한 Agnostic Bridge 도입 및 프리징 방어 (v0.5.20) | `임시해시` | ✅ |
|
||||||
|
| 007 | 17:57 | Gravity Bridge 안정화: 중복 알림(SafeToAutoRun) 제거 설계 확정 및 Discord 봇 캐시/로컬 LS 크로스매칭 증상 디버깅 완료 | \-\ | ✅ |
|
||||||
9
docs/devlog/2026-04-09.md
Normal file
9
docs/devlog/2026-04-09.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# 2026-04-09
|
||||||
|
|
||||||
|
| NNN | HH:MM | 작업 설명 | `커밋해시` | 상태 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 001 | 21:55 | Agent UI Tailwind/Native 마이그레이션 대응 (DOM 옵저버 구조 개편) | `HEAD` | ✅ |
|
||||||
|
| 002 | 22:30 | Agent UI 버튼 무시 버그 긴급수정 (CodeLens 필터교정) | `HEAD` | ✅ |
|
||||||
|
| 003 | 23:15 | Native UI 아이콘 글루잉 대응 스캐너 픽스 (DOM Regex 매칭 강화) | `HEAD` | ✅ |
|
||||||
|
| 004 | 00:10 | Discord Signal Relay & Auto-Approve Body Null 버그 수정 (False Positive 차단) | "HEAD" | ✅ |
|
||||||
|
| 005 | 23:00 | fix: Resolve empty Discord embed body by populating detailed step-probe payload | \\ | ? |
|
||||||
4
docs/devlog/2026-04-10.md
Normal file
4
docs/devlog/2026-04-10.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
| NNN | 시간 | 작업 설명 | 커밋해시 | 완료여부 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 001 | 17:11 | step-probe.ts 의 isRunning 조건 누락으로 인한 릴레이 증발 버그 픽스 | COMMITTING | ✅ |
|
||||||
|
| #613 | 21:10 | Bridge Relay AI Chat Body DOM extraction and template literal Regex fix | TBD | ✅ |
|
||||||
6
docs/devlog/2026-04-11.md
Normal file
6
docs/devlog/2026-04-11.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 2026-04-11
|
||||||
|
|
||||||
|
| NNN | HH:MM | 작업 설명 | `커밋해시` | ✅ 또는 🔧 |
|
||||||
|
|-------|-------|----------|-----------|-----------|
|
||||||
|
| 001 | 13:04 | Pure 웹소켓 게이트웨이 완전 전환 및 레거시 파일 브릿지 통신코드 삭제 | `072f83b` | ✅ |
|
||||||
|
| 002 | 17:25 | Antigravity Observer 컨텍스트 추출 범위 제한 및 노이즈(UI/TypeScript 코드) 필터링, Discord 임베드 개선 | `70dc301` | ✅ |
|
||||||
7
docs/devlog/2026-04-12.md
Normal file
7
docs/devlog/2026-04-12.md
Normal 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` | 🔧 |
|
||||||
7
docs/devlog/2026-04-13.md
Normal file
7
docs/devlog/2026-04-13.md
Normal 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` | 🔧 |
|
||||||
5
docs/devlog/2026-04-14.md
Normal file
5
docs/devlog/2026-04-14.md
Normal 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` | 🔧 |
|
||||||
6
docs/devlog/2026-04-15.md
Normal file
6
docs/devlog/2026-04-15.md
Normal 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
11
docs/devlog/2026-04-16.md
Normal 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><>) | ? | ?? |
|
||||||
5
docs/devlog/2026-04-17.md
Normal file
5
docs/devlog/2026-04-17.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 2026-04-17
|
||||||
|
|
||||||
|
| NNN | HH:MM | 작업 설명 | `커밋해시` | 완료 |
|
||||||
|
|-------|-------|----------|-----------|----------|
|
||||||
|
| 001 | 08:05 | v18 Observer DOM->Markdown 파서 개선(<a> 파싱 포함) 및 노이즈 필터 부작용(코드 블럭 잘림 방지) 해결, User 구문 추출 연동, v0.5.56 배포 | `미정` | ✅ |
|
||||||
5
docs/devlog/2026-04-18.md
Normal file
5
docs/devlog/2026-04-18.md
Normal 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
26
docs/devlog/2026-04-19.md
Normal 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" 버튼 자동 소멸 확인 ✅
|
||||||
12
docs/devlog/entries/20260323-001.md
Normal file
12
docs/devlog/entries/20260323-001.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# WebSocket 좀비 커넥션 해결 및 통신망 메모리 누수 구조 패치
|
||||||
|
|
||||||
|
- **시간**: 2026-03-23 21:09~21:20
|
||||||
|
- **Commit**: `ecebec3`
|
||||||
|
- **Vikunja**: #510 → done
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
- **ws-client.ts 핑퐁 와치독(Ping-Pong Watchdog)**: 단순 에러 캐치가 아니라 `ws.terminate()`를 통해 무반응 소켓을 강제 종료하여 자체 재연결 로직(`_onDisconnect`)을 활성화하도록 설계.
|
||||||
|
- **통신망 추적 변수 캡핑(Bounded Cap)**: `hub.py`의 `pending_owners` 및 `bot.py`의 `_sent_approval_ids` 등 무한히 쌓일 수 있는 파이썬 딕셔너리에 LRU(오래된 순 삭제) 로직을 추가. 비록 당장 OOM을 유발하진 않지만 이 구조적 메모리 누수(Leak)를 원천적으로 차단하여 시스템 안정성을 극대화함.
|
||||||
|
|
||||||
|
## 미완료
|
||||||
|
- 없음
|
||||||
16
docs/devlog/entries/20260323-002.md
Normal file
16
docs/devlog/entries/20260323-002.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Cross-Project DOM Observer Leakage 해결
|
||||||
|
|
||||||
|
- **시간**: 2026-03-23 22:00~22:45
|
||||||
|
- **Commit**: `TBD`
|
||||||
|
- **Vikunja**: #TBD → done
|
||||||
|
|
||||||
|
## 확인된 사실
|
||||||
|
- Discord 신호 누락이 아닌, 다중 원격 환경에서의 포트 덮어쓰기 문제로 인한 **교차 프로젝트 신호 오염(Leakage)**이었음.
|
||||||
|
|
||||||
|
## 삽질 / 트러블슈팅
|
||||||
|
- 처음에는 디스코드 봇(`bot.py`)이나 익스텐션의 `step_type` 매핑 로직 누락인 줄 알고 코드를 탐색했으나, 실제 DOM observer 스크립트에 하드코딩된 Port 변수가 문제의 원인임을 파악함.
|
||||||
|
- 다중 원격 컴퓨터 환경 중 포트 포워딩(`12345` 충돌 우회)으로 인한 이슈를 해결하기 위해 `vscode.env.asExternalUri`를 도입. 로컬에 매핑된 최종 확정 포트를 알아냄.
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
- DOM Status Bar(`tooltip`)를 일종의 단방향 IPC(Inter-Process Communication) 대용으로 사용하기로 결정함.
|
||||||
|
- Extension Host가 렌더러(DOM Observer)에게 안전하고 해당 창에만 격리(Window-isolated)된 방식으로 포트 번호를 전달할 수 있음. 전역 HTML 파일 패치의 한계를 우아하게 극복함.
|
||||||
12
docs/devlog/entries/20260324-001.md
Normal file
12
docs/devlog/entries/20260324-001.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# v0.5.6 좀비 커넥션 패치 회귀 오류 해결 (v0.5.8 반영)
|
||||||
|
|
||||||
|
- **시간**: 2026-03-23 23:10 ~ 2026-03-24 07:05
|
||||||
|
- **Commit**: `TBD`
|
||||||
|
- **Vikunja**: 신규 추가 예정
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
- **False Positive 멈춤 현상 원인 규명**: v0.5.6에서 추가된 `pongTimeoutTimer` (10초 타임아웃)가 VS Code 확장 내부의 일시적인 Event Loop 블로킹 발생 시 네트워크 I/O(`pong` 응답)보다 먼저 소켓을 강제 종료하고 있었습니다. 이 때문에 멀쩡한 연결이 끊어지고 재연결 지연 페널티가 누적되어 최대 60초까지 응답 불가(멈춤) 상태에 빠지는 현상이 발견되었습니다.
|
||||||
|
- **해결 방안 선택 (타임스탬프 검증)**: 타이머 동시성 경합을 유발하는 `setTimeout` 방식을 전면 폐기하고, 기존의 `setInterval` (25초 주기) 하트비트 루프 내부에서 `ws.on('pong')`이 갱신하는 `lastPongTime`을 대조(`Date.now() - lastPongTime > 60000`)하는 방식으로 변경했습니다. 이를 통해 Event Loop가 지연되더라도 I/O 이벤트를 먼저 수확한 후에 안전하게 판독할 수 있어 오진단(False Positive)을 원천 차단하면서도 좀비 커넥션을 방지했습니다.
|
||||||
|
|
||||||
|
## 미완료
|
||||||
|
- 없음 (v0.5.8 VSIX 컴파일 성공 및 배포 완료)
|
||||||
18
docs/devlog/entries/20260324-002.md
Normal file
18
docs/devlog/entries/20260324-002.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# DOM Observer VS Code 네이티브 알림 UI 캡처 블라인드 스팟 해결 (v0.5.9)
|
||||||
|
|
||||||
|
- **시간**: 2026-03-24 12:00~13:00
|
||||||
|
- **Commit**: `7b6cd59`
|
||||||
|
- **Vikunja**: #514
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
- **문제**: "Always Allow" 및 "Allow Alt+↵" (단축키 포함) 권한 알람이 Discord로 전송되지 않는 문제가 발생했습니다. (v0.5.8)
|
||||||
|
- **근본 원인 확인**:
|
||||||
|
- Regex 실패: `Always Allow`는 `^Allow` 정규식을 통과하지 못합니다.
|
||||||
|
- CSS Selector 실패: `observer-script.ts`의 스캔 엔진이 오직 `document.querySelectorAll('button')`에만 의존하여 렌더링 노드를 찾고 있었습니다. VS Code 네이티브 권한 프롬프트(토스트 알림 및 채팅 패널)는 `<a role="button" class="monaco-text-button">` 또는 `<vscode-button>`을 활용하므로 애초에 찾지도 못하고 스킵되었습니다.
|
||||||
|
- **해결책**:
|
||||||
|
1. `observer-script.ts` 내의 모든 DOM 쿼리를 `button, [role="button"], vscode-button, .monaco-text-button` 으로 확장.
|
||||||
|
2. 허용 권한 토큰 관련 정규식을 `/^(?:Always )?Allow/i` 로 상향 패치.
|
||||||
|
3. `v0.5.9` 로 빌드 및 VSIX 설치 완료 후 정상 동작 검증 완료.
|
||||||
|
|
||||||
|
## 미완료
|
||||||
|
- 없음.
|
||||||
18
docs/devlog/entries/20260324-003.md
Normal file
18
docs/devlog/entries/20260324-003.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# DOM Observer /trigger-click 렌더링 순서 오작동 및 False Positive 프리징 해결 (v0.5.10)
|
||||||
|
|
||||||
|
- **시간**: 2026-03-24 17:50~18:20
|
||||||
|
- **Commit**: `HEAD` (예정)
|
||||||
|
- **Vikunja**: #514 관련 디버깅/핫픽스
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
- **문제**: "0.5.9 패치한 이후 화면이 펜딩되서 움직여지지않아" 라는 증상 확인.
|
||||||
|
- v0.5.9에서 DOM 쿼리를 `[role="button"]` 등으로 확장했으나, 정규식이 `/^Run/i` 등으로 풀어진 상태여서 에디터 뷰의 "Run Test" 등 수많은 CodeLens 버튼들을 Agent의 트리거로 오인함.
|
||||||
|
- 결과적으로 아무 조작도 하지 않았는데 계속 터미널 실행 대기상태(Pending)로 무한 진입하여 UI 화면이 프리징(Freeze)됨.
|
||||||
|
- 특히 디스코드에서 `Approve` 명령을 내렸을 때도, DOM 트리상 상단에 우연히 "Run" CodeLens가 있으면 먼저 캡처되어 진짜 Agent 패널의 버튼을 클릭하지 못하고 엉뚱한 요소를 클릭하는 위험한 순위 불일치 버그까지 있었음.
|
||||||
|
- **해결책 (Structural Context Filtering)**:
|
||||||
|
1. 감지(Scan): 단순 정규식을 빡빡하게 변경하면 동적인 버튼 이름("Run script" 등)이 안 먹히는 부작용이 있으므로 느슨함을 유지하되, **발생 영역(DOM Context)**에 강제 필터를 부여.
|
||||||
|
- `isVSCodeMainWindow` 및 노드 루트가 `document.body`인지를 체크하여, 에디터 본문 영역 안에서는 "Run", "Approve", "Accept" 캡처를 전부 무시.
|
||||||
|
2. 제어(Trigger-click 우선순위): `observer-script.ts`의 `deepFindButtons()` 내부 스캔 트리를 변경하여 `findPanel()`로 안티그래비티 패널을 1순위로 조회, 알림 Toast를 2순위, 본문 Document를 3순위로 탐색하게 강제하여 엉뚱한 버튼 클릭 사고를 100% 방지함.
|
||||||
|
|
||||||
|
## 미완료
|
||||||
|
- 없음 (빌드 및 검증 완료)
|
||||||
11
docs/devlog/entries/20260401-001.md
Normal file
11
docs/devlog/entries/20260401-001.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# step-probe Pagination 10-Item Truncation vs LS DoS 오류 수정
|
||||||
|
|
||||||
|
- **시간**: 2026-04-01 13:00~18:22
|
||||||
|
- **Commit**: `TBD`
|
||||||
|
- **Vikunja**: #N/A (임시 버그 픽스)
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
- 기존 `v0.5.13`에서 `limit: 100`으로 Pagination Limit(기본 10개)을 우회하려 했으나, LS DB 스캔 및 거대한 JSON 파싱이 VS Code Event Loop 블로킹을 유발하여 UI 멈춤(DoS) 발생.
|
||||||
|
- 롤백 과정에서 `{}`(인자 없음)으로 원복하면서 필수적인 `descending: true` 파라미터까지 누락됨.
|
||||||
|
- 이로 인해 `guitar_score` 등의 최신 작성 세션이 LS 조회 리밋(10)에서 밀려나 승인 신호를 수신하지 못하는 이슈 재발.
|
||||||
|
- 이를 해결하기 위해 `limit: 30, descending: true`로 설정. 파싱해야 할 JSON 객체 수를 1/3로 줄임과 동시에, 정렬 보장을 통해 최근 10초 이내에 활성화된 세션은 언제나 Index 0번 최상단에 고정되게끔 메커니즘을 수정함.
|
||||||
20
docs/devlog/entries/20260408-004.md
Normal file
20
docs/devlog/entries/20260408-004.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 2026-04-08 (004) - SafeToAutoRun 명령어의 디스코드 알림 누락 복구
|
||||||
|
|
||||||
|
## 1. 이슈 개요
|
||||||
|
- 사용자가 `/start` 등 백그라운드 명령어(SafeToAutoRun)를 포함한 워크플로우를 실행하였으나, 디스코드로 아무런 메시지도 전송되지 않는 버그가 보고됨.
|
||||||
|
|
||||||
|
## 2. 원인 분석
|
||||||
|
- v0.5.16 배포 당시 Discord 중복 알림(Pending 파일) 이슈를 방지하는 과정에서, `step-probe.ts`에 있던 "⚡ 자동 실행됨" 원본 알림 코드(snapshot 생성 로직)까지 실수로 함께 삭제됨.
|
||||||
|
- `SafeToAutoRun` 구문에서 `writePendingApproval` 스킵 로직은 잘 동작하고 있었으나, 정작 사용자에게 알려야 할 기본적인 '자동 실행됨' 정보마저 소실되어 결과적으로 아무 알림도 가지 않는 침묵 상태가 됨.
|
||||||
|
|
||||||
|
## 3. 해결 및 적용 사항
|
||||||
|
1. `step-probe.ts` 복구
|
||||||
|
- `SafeToAutoRun` 판단 시 `autoRunSteps`를 마킹한 직후 `ctx.writeChatSnapshot()`을 호출하도록 코드를 추가 복원함.
|
||||||
|
- 출력 구조: `💬 **자동 실행됨** (step N)\n\n\`명령어내용\``
|
||||||
|
2. **v0.5.18 배포**
|
||||||
|
- 익스텐션의 `package.json` 버전을 `0.5.18`로 펌핑.
|
||||||
|
- 사전 스크립트가 적용된 `vsce package`를 통해 새로운 `gravity-bridge-0.5.18.vsix` 패키징을 완료함.
|
||||||
|
|
||||||
|
## 4. Next Step
|
||||||
|
- `extension/gravity-bridge-0.5.18.vsix` 파일을 VS Code에 수동 설치할 것 (Install from VSIX...).
|
||||||
|
- 설치 후 반드시 **Reload Window**하여 테스트 수행 요망.
|
||||||
20
docs/devlog/entries/20260408-005.md
Normal file
20
docs/devlog/entries/20260408-005.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 2026-04-08 (005) - SafeToAutoRun 로컬 자동 승인 누락 데드락(Freeze) 해결
|
||||||
|
|
||||||
|
## 1. 이슈 개요
|
||||||
|
- 사용자가 확인 결과 v0.5.15 이후 백그라운드 터미널 명령어 등 모든 AI 에이전트 작업이 '자동 실행됨' 스냅샷만 보내고 VS Code 내부적으로는 여전히 승인(Allow)을 대기하며 완전히 멈춰버림(Freeze).
|
||||||
|
- 신호가 전달조차 안되고 다음 단계로 진행하지 못하는 심각한 블로커 이슈가 발생함.
|
||||||
|
|
||||||
|
## 2. 원인 분석
|
||||||
|
- v0.5.16 버그 픽스("Discord 중복 알림 방지") 당시 `SafeToAutoRun` 상태일 때 `writePendingApproval()`을 수행하지 않도록 코드(`skip pending`)를 변경했음.
|
||||||
|
- 그러나 과거에는 이 Pending 파일이 생성되면 파이썬 백엔드(Bot)가 디스코드에 알림을 띄운 직후, 자동으로 `approve`(허용) 신호를 익스텐션 쪽에 보내어 다음 단계가 허가되었음.
|
||||||
|
- 즉, 익스텐션에서 Pending 파일 생성을 중단(skip)하자 봇으로부터 수락 신호가 아예 오지 않게 되었고, VS Code의 보안 시스템에 의해 명령어는 영원히 "Run(Auto)" 클릭 승인을 대기하는 상태의 데드락에 빠져버림.
|
||||||
|
|
||||||
|
## 3. 해결 및 적용 사항
|
||||||
|
1. `step-probe.ts` 로컬 자동 승인 복구
|
||||||
|
- `safeToAutoRun` 판단으로 Pending 파일 생성을 건너뛸 때, 익스텐션 스스로 백그라운드 승인을 트리거하도록 `tryApprovalStrategies(true, ...)` 함수 호출 코드를 명시적으로 추가함.
|
||||||
|
- 이를 통해 봇의 승인 신호를 기다릴 필요 없이 즉각적으로 승인(Accept)을 단행하여 막힘없이 스텝이 연속 진행되도록 고침.
|
||||||
|
2. **v0.5.19 배포**
|
||||||
|
- VSIX 버전을 `0.5.19`로 펌핑 후 `npx vsce package` 명령으로 익스텐션을 재빌드함.
|
||||||
|
|
||||||
|
## 4. Next Step
|
||||||
|
- `extension/gravity-bridge-0.5.19.vsix` 파일을 수동 재설치하고 VS Code Window를 Reload 한 뒤, `/start` 같은 자동 워크플로우를 재실행하여 신호 블로킹(Freeze) 버그가 해결되었는지 최종 확인.
|
||||||
17
docs/devlog/entries/20260408-006.md
Normal file
17
docs/devlog/entries/20260408-006.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# SafeToAutoRun 데드락 및 익스텐션 프리징 완벽 해결 (v0.5.20)
|
||||||
|
|
||||||
|
- **시간**: 2026-04-08 07:15~07:30
|
||||||
|
- **Commit**: `임시해시`
|
||||||
|
- **Vikunja**: #589 → done
|
||||||
|
|
||||||
|
## 발생 문제
|
||||||
|
1. **Deadlock**: 이전 버전(v0.5.15)에서 디스코드 알림을 줄이려고 익스텐션의 `step-probe.ts`가 `SafeToAutoRun` 발생 시 `pending` 파일 생성 자체를 건너뛰도록 구현함. 하지만 AG 엔진은 CORTEX_STEP_STATUS_WAITING 상태에서 누군가가 해결해주기를 영원히 기다리게 되어, 파이프라인 전체가 데드락(UI 멈춤)에 빠지는 치명적인 부작용 발생.
|
||||||
|
2. **이벤트루프 Freeze**: `extension.ts`의 `detectProjectName` 내부에서 동기식 `cp.execSync('git remote get-url origin')`를 실행하여 윈도우 환경에서 VS Code 이벤트루프가 막히고 WebSocket 통신이 유실되는 현상 발생.
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
- **Agnostic Bridge 철학 준수 (단일 경로 원칙 복구)**
|
||||||
|
- 익스텐션(`step-probe.ts`)은 절대 자의적으로 승인 처리를 하거나 `pending` 파일 생성을 스킵해서는 안 됨. 오직 브릿지 중계자 역할에 충실하도록 롤백하고, 대신 메타데이터에 `safe_to_auto_run: true` 속성을 실어 보냄.
|
||||||
|
- 파이썬 서버(`bot.py`) 관제탑이 이를 확인하면 디스코드에 알림(`Embed`)을 보내는 단계만 슬쩍 생략하고 그 즉시 허가증(`response/`)을 발급. 이를 통해 데드락 해제와 무소음 승인을 동시에 만족함.
|
||||||
|
- **비동기화 및 빌드 파이프라인 강제**
|
||||||
|
- 동기식 git 명령어 대신 비동기식 `.git/config` 파일 읽기로 교체.
|
||||||
|
- `package.json`에 `vscode:prepublish` 스크립트를 부활시켜 낡은 소스코드가 VSIX에 패키징되는 문제 원천 차단.
|
||||||
18
docs/devlog/entries/20260408-007.md
Normal file
18
docs/devlog/entries/20260408-007.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Gravity Bridge 알림 최적화 및 연동 디버깅 완료
|
||||||
|
|
||||||
|
- **시간**: 2026-04-08 17:00~17:55
|
||||||
|
- **Commit**: `pending`
|
||||||
|
- **Vikunja**: 대상 작업 맵핑 예정
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
- `SafeToAutoRun` 시 `step-probe.ts`에서 날리던 **"⚡ 자동 실행됨"** 알림은 과감하게 완전히 제거하였습니다. 파이썬 봇이 이미 "🤖 자동 승인됨" Embed를 송출하고 있으므로 디자인 철학(익스텐션은 중립적인 릴레이 역할만 수행하고, 비즈니스 판정 알림은 중앙 봇이 담당)에 부합합니다.
|
||||||
|
- `extension.ts`의 `writeChatSnapshot` 의존성을 줄여 트래픽 낭비와 중복 노이즈를 해소했습니다.
|
||||||
|
|
||||||
|
## 핵심 디버깅 (Troubleshooting)
|
||||||
|
- **`variet-llm` 프로젝트 연동 실패 이슈:**
|
||||||
|
1. `variet-llm` 창을 열었으나 채팅 패널을 열지 않아 안티그래비티 전용 언어 서버(LS)가 띄워져 있지 않은 상태에서 브릿지를 켬. 브릿지가 엉뚱하게 `gravity_control` LS에 바인딩 됨.
|
||||||
|
2. 사용자가 Discord에서 `variet-llm` 채널을 삭제해 버렸는데, 파이썬 봇(`bot.py`)은 캐시를 가지고 있어서 자신이 파괴된 채널을 대상으로 계속 통신을 시도하며 새 채널을 파지 않음 (HTTP 404).
|
||||||
|
3. 로컬 윈도우 재시작 및 도커(`docker-compose restart`) 컨테이너 재가동을 통해 **봇 프로세스 캐시 초기화** → 채널 자동 재생성, 완벽 디버깅에 성공했습니다.
|
||||||
|
|
||||||
|
## 미완료
|
||||||
|
- 없음. 모두 성공적으로 동작 중.
|
||||||
18
docs/devlog/entries/20260409-001.md
Normal file
18
docs/devlog/entries/20260409-001.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Agent UI Tailwind/Native 마이그레이션 대응 (DOM 옵저버 구조 개편)
|
||||||
|
|
||||||
|
- **시간**: 2026-04-09 19:40~21:55
|
||||||
|
- **Commit**: `[임시해시]`
|
||||||
|
- **Vikunja**: 신규 생성 후 완료 처리
|
||||||
|
|
||||||
|
## 트러블슈팅 및 결정 사항
|
||||||
|
최근 UI 업데이트 후 Discord 릴레이 신호(Run, Accept) 단절.
|
||||||
|
deep-inspect 덤프 분석 결과 Webview/Iframe 환경이 사라지고 Native DOM(VS Code 본문)에 напрямую 그려짐, 기존 시맨틱 클래스가 Tailwind로 변경.
|
||||||
|
1. 기존 `findPanel`이 패널을 못 찾자 `isBodyRoot` 모드로 스캔
|
||||||
|
2. 과거에 추가된 CodeLens 방어 로직(`if (isVSCodeMainWindow && isBodyRoot && PATS[p].type !== 'diff_review') continue;`)에 의해 모든 버튼 스캔이 **버려지고 있었음**.
|
||||||
|
|
||||||
|
**결정**:
|
||||||
|
엄격한 Panel Class Whitelist 기반 방어를 해제하고, 버튼이 `.monaco-editor` 내부에 있는 경우만 무시하도록 Blacklist 기반 방어로 선회.
|
||||||
|
UI 텍스트 글루잉(아이콘 통합) 대응 위해 패터닝 정규식을 `/^(?:Always\s*)?Run/i` 등으로 완화.
|
||||||
|
|
||||||
|
## 미완료
|
||||||
|
- 없음
|
||||||
15
docs/devlog/entries/20260409-002.md
Normal file
15
docs/devlog/entries/20260409-002.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Agent UI 버튼 무시(Discard) 버그 핫픽스
|
||||||
|
|
||||||
|
- **시간**: 2026-04-09 22:10~22:35
|
||||||
|
- **Commit**: `pending`
|
||||||
|
- **Vikunja**: 새로 생성 후 완료 예정
|
||||||
|
|
||||||
|
## 트러블슈팅 및 결정 사항
|
||||||
|
- **이슈**: Native UI 마이그레이션 직후 버튼을 눌러도 브릿지로 신호가 전혀 가지 않는 버그 접수
|
||||||
|
- **원인 분석**:
|
||||||
|
1. `extension.log` 확인 결과 `[HTTP] pending` 자체가 생성되지 않음 (브릿지 자체에 도달하지 않음).
|
||||||
|
2. DOM observer가 수집한 버튼이 `b.closest('.monaco-editor')` 필터 조건에 무조건 걸려서 버려지는 것이었음. Native 전환 후 채팅창이 에디터 탭 내부에 렌더링되면서 `.monaco-editor` 내부 자식이 됨.
|
||||||
|
- **결정**: 기존의 `b.closest('.monaco-editor')` 방어 로직을 폐기하고 실제 CodeLens 버튼 고유의 클래스 `.codelens-decoration`를 명시하도록 변경하여 구조 변화에 강건해지도록 개선 완료. `0.5.22` VSIX 재배포.
|
||||||
|
|
||||||
|
## 미완료
|
||||||
|
- 없음 (검증은 유저 몫으로 인계)
|
||||||
17
docs/devlog/entries/20260409-003.md
Normal file
17
docs/devlog/entries/20260409-003.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Agent UI Native 버튼 아이콘 글루잉 무시 현상 수정
|
||||||
|
|
||||||
|
- **시간**: 2026-04-09 23:00~23:15
|
||||||
|
- **Commit**: `TBD`
|
||||||
|
- **Vikunja**: 신규 생성 (UI 텍스트 글루잉 버튼 버그) → done
|
||||||
|
|
||||||
|
## 문제 상황
|
||||||
|
- 0.5.22 패치(CodeLens 필터) 이후에도 `Run`, `Accept` 버튼 클릭 시 디스코드 브릿지로 아무런 펜딩 요청(POST /pending)이 전송되지 않는 현상 발생.
|
||||||
|
- 원인 규명: Native UI 마이그레이션 적용 후, Agent 패널 버튼들의 아이콘(``, `▶` 등)이 리액트/Tailwind 컴포넌트 렌더링을 거쳐 `element.textContent` 상단에 문자열로 직접 병합(Gluing)됨.
|
||||||
|
- 옵저버 스크립트 내부 정규식(`/^(?:Always\s*)?Run/i`)이 문자열의 맨 첫(^) 시작을 강제하기 때문에, 아이콘으로 시작하는 버튼들의 명령어를 전부 오탐으로 간주함.
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
- 버튼의 텍스트를 읽는 즉시, `txt.replace(/^[^a-zA-Z0-9]+/, '')`를 적용하여 첫 글자가 영어/숫자가 될 때까지, 선행하는 모든 특수문자, 아이콘, 폰트 공백 등을 강제 삭제하도록 스크립트 내부의 3가지 탐색 루프 (본문 스캔, Sibling 버튼 수집, Webview trigger-click 인젝션)에 일괄 업데이트.
|
||||||
|
- 기존 `.monaco-editor`나 `.chat-body` 등 부모 컨테이너에 지나치게 의존하던 `findButtonContainer`에 `chat`, `prose`, `markdown`를 추가 화이트리스팅 하되 Tailwind UI 구조 특성상 시맨틱 래퍼를 찾지 못할 경우 3단계 위 부모를 반환하여 안전하게 컨텍스트를 확보하도록 고도화. -> **구조 변경 시에도 유연하게(Graceful) 기능 동작 지원 보장.**
|
||||||
|
|
||||||
|
## 결과
|
||||||
|
- `v0.5.23` (코드상 0.5.22 유지) VSIX 빌드 및 테스트 준비.
|
||||||
15
docs/devlog/entries/20260409-004.md
Normal file
15
docs/devlog/entries/20260409-004.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Discord Signal Relay & Auto-Approve Body Null 버그 수정 (False Positive 차단)
|
||||||
|
|
||||||
|
- **시간**: 2026-04-10 00:00~00:10
|
||||||
|
- **Commit**: `HEAD`
|
||||||
|
- **Vikunja**: #태스크번호 → done
|
||||||
|
|
||||||
|
## 트러블슈팅 & 삽질
|
||||||
|
- **DOM 정규식의 반란**: Native UI 패치 이후 버튼의 text-gluing 제거 때문에 정규식을 광범위하게 바꿨으나, 하필 `Run\s*` 조건에 단어 경계(`\b`)를 누락하는 바람에 VS Code 하단의 시스템 상태 버튼인 `Running 1 command`까지 AI의 `Run` 버튼으로 인식해버림. 무한 PENDING 스팸을 만들어 브릿지 큐를 폭파시킨 주범.
|
||||||
|
- **Auto-Approve 본문 누락**: 봇에서 자동 승인 Embed 생성 시 `req.description` (실행될 본문 코드)을 아예 그리지 않고 `req.command` (단순 버튼 라벨)만 출력하도록 코딩되어 있었음. 사용자는 '자동 승인' 알림을 받지만 정작 무엇이 승인되었는지는 전혀 알 수 없어 '본문 표시 자체가 안 된다'고 오해할 수밖에 없었음.
|
||||||
|
- **첫 알림 메시지 무시**: `step-probe.ts`에서 세션 전환 시 `lastNotifyStepIndex`를 초기화할 때 `-1` 로 리셋하지 않아 새 세션의 첫 안내 메시지가 매번 씹히는 증상 발견.
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
- `bot.py`의 Auto-Approve Embed 구문에 수동 승인처럼 본문을 표출하도록 렌더링 로직 통일.
|
||||||
|
- `observer-script.ts`의 `TerminalCommand` 정규식에 `\b`를 추가하여 시스템 버튼과의 혼선을 원천 차단함.
|
||||||
|
- `step-probe.ts` 의 index reset 초기값을 `-1` 로 명시화.
|
||||||
13
docs/devlog/entries/20260410-001.md
Normal file
13
docs/devlog/entries/20260410-001.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# step-probe.ts 의 isRunning 조건 누락으로 인한 릴레이 증발 버그 픽스
|
||||||
|
|
||||||
|
- **시간**: 2026-04-10 17:11
|
||||||
|
- **Commit**: `COMMITTING`
|
||||||
|
- **Vikunja**: #125 → done
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
- AI 응답이 비정상적으로 빠를 경우 `RUNNING` 상태의 2초 polling 창을 우회하여 `IDLE` / `WAITING`로 진입해버리는 버그가 있었습니다.
|
||||||
|
- 기존에는 `isRunning && currentCount > ...`로만 Real-time Capture가 동작하여 전부 스킵되는 증상 확인.
|
||||||
|
- `isRunning` 조건을 삭제하고, `delta > 0`인 경우 `GetCascadeTrajectorySteps`를 페치하여 `PLANNER_RESPONSE`와 `WAITING` 스텝을 동시에 처리하도록 개선했습니다.
|
||||||
|
|
||||||
|
## 미완료
|
||||||
|
- 없음.
|
||||||
13
docs/devlog/entries/20260410-002.md
Normal file
13
docs/devlog/entries/20260410-002.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Gravity Bridge 빠른 응답(Fast Execution) 누락 오류 해결
|
||||||
|
|
||||||
|
- **시간**: 2026-04-10
|
||||||
|
- **Commit**: TBD
|
||||||
|
- **Vikunja**: #607 → done
|
||||||
|
|
||||||
|
## 문제 원인
|
||||||
|
- AI 생성이나 응답 작업이 폴링 간격(5초) 미만으로 끝났을 때, 익스텐션의 폴링 루프는 이전과 동일한 `IDLE` 상태만을 보게 됨.
|
||||||
|
- `lastResponseCaptureStep` 검사는 마련되어 있었으나, `wasRunning` 플래그 제약(`wasRunning && !isRunning`)으로 인하여 IDLE->IDLE 전이를 거치는 모든 단기응답이 `[RESPONSE-CAPTURE]`를 영구히 건너뛰고 통째로 누락됨.
|
||||||
|
|
||||||
|
## 해결 방법
|
||||||
|
- `wasRunning` 방어 조건을 해제하고, `!isRunning && currentCount > lastResponseCaptureStep` 조건으로 완화 (인덱스 전진 기반 감지로 수정).
|
||||||
|
- 오래된 하드코딩 파서를 버리고 방벽 파서 역할을 하는 `extractPlannerText`로 갈무리 블록의 AI 응답 추출 로직을 단일화하여 적용.
|
||||||
15
docs/devlog/entries/20260410-003.md
Normal file
15
docs/devlog/entries/20260410-003.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# [Bridge] Disable DOM Observer Proactive Pending to Fix Empty Bots
|
||||||
|
|
||||||
|
- **시간**: 2026-04-10 16:12
|
||||||
|
- **Commit**: TBD
|
||||||
|
- **Vikunja**: TBD
|
||||||
|
|
||||||
|
## 문제 원인
|
||||||
|
- 디스코드에 Running 1 comman d 나 내용 없는 Allow 가 반복적으로 전송되는 문제.
|
||||||
|
- 원인은 v3 DOM Observer 스크립트가 Native UI에서 발생시키는 빈 껍데기 알림(POST /pending)들이, 정상적으로 본문 정보를 모두 추출해 기다리고 있는 step-probe.ts의 완벽한 Pending을 덮어씌우거나 먼저 처리되어버렸기 때문임.
|
||||||
|
- 단어 경계(\b) 정규식 필터조차 VS Code 렌더링 시 노드 줄바꿈 이슈 등으로 인해 완벽한 방어가 불가능했음.
|
||||||
|
|
||||||
|
## 해결 방법
|
||||||
|
- observer-script.ts에서 버튼 텍스트를 감지해 능동적으로 Pending을 생성하는 기능(PATS 배열)을 **전면 비활성화(배열 비움)**.
|
||||||
|
- 이로써, 오직 100% 신뢰 가능한 SDK RPC(step-probe.ts)만이 대기 상태(WAITING)와 명령어 상세 정보를 포착해 Pending을 생성함.
|
||||||
|
- DOM Observer는 브릿지가 보내는 /trigger-click 폴링 명령어를 받아 실제 물리 클릭만 수행하는 '수동적 렌더러' 역할로 격하됨.
|
||||||
14
docs/devlog/entries/20260410-005.md
Normal file
14
docs/devlog/entries/20260410-005.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# GetAllCascadeTrajectories 10-Item Hard Limit Bypass
|
||||||
|
|
||||||
|
- **시간**: 2026-04-10
|
||||||
|
- **Commit**: TBD
|
||||||
|
- **Vikunja**: TBD
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
- `GetAllCascadeTrajectories` LS API의 `limit` 등 페이지네이션 파라미터가 백엔드에서 무시되어 최신 세션이 10개 제한에 잘려나가는 문제를 확인.
|
||||||
|
- `DOM observer`가 더 이상 작동하지 않는 상태(Empty 보디 이슈로 비활성화됨)에서, `step-probe.ts`마저 이 10개 한도 밖으로 밀려난 현재 세션(`activeSessionId`)을 발견하지 못해, 발생한 모든 채팅 이벤트 파일이 작성되지 않는 문제("단 한글자도 안 날아옴")의 근본 원인을 특정함.
|
||||||
|
- `GetDiagnostics` API를 사용하여 내부적으로 저장된 `recentTrajectories` 덤프 전체를 불러와, 기존 `GetAllCascadeTrajectories`의 결과를 병합/보완하도록 변경.
|
||||||
|
- 이를 통해 아무리 많은 수의 세션이 열려 있어도 현재 사용 중인 세션 ID를 식별 가능.
|
||||||
|
|
||||||
|
## 미완료
|
||||||
|
- 없음.
|
||||||
15
docs/devlog/entries/20260410-613.md
Normal file
15
docs/devlog/entries/20260410-613.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Fix gravity bridge Discord Relay AI Chat Body by patching DOM extraction and Regex literals
|
||||||
|
|
||||||
|
- **시간**: 2026-04-10 20:30~21:10
|
||||||
|
- **Vikunja**: #613 → done
|
||||||
|
|
||||||
|
## 트러블슈팅: Typescript 백틱 안의 정규식 리터럴 파괴 현상
|
||||||
|
- **증상**: JSDOM 가상 모의 환경에서 테스트를 돌려보니, 렌더링 화면이나 타겟 Text가 정확히 매치됨에도 정규식이 조건문에서 `false`를 내뱉으며 Button Matching을 건너뛰는 현상 발생.
|
||||||
|
- **원인**: `observer-script.ts`를 `.js`로 변환할 때, Typescript 컴파일러가 `return \`...\`` 템플릿 리터럴 내부의 `/^(?:Always\s*)?Allow\b/i` 구문을 해석하면서, `\s`를 일반 문자 `s`로, `\b`를 아스키 특수문자 `Backspace(0x08)`로 직렬화하여 클라이언트에 꽂아버리는 문제가 있었음. 이로 인해 정규식 자체가 오염되어 어떠한 버튼도 매칭하지 못하고 있었음.
|
||||||
|
- **해결**: `observer-script` 내부의 정규식 리터럴 내부의 이스케이프 문자(`\s`, `\b` 등)를 전부 이중 백슬래시(`\\s`, `\\b`)로 패치하여 브라우저에서 스크립트가 실행될 때 올바른 정규식 파서가 열리도록 수정 보완함.
|
||||||
|
|
||||||
|
## 결정 사항: 웹뷰 내 로컬 fetch CSP 패치 통과
|
||||||
|
- `html-patcher.ts`에서 웹뷰 렌더링 시점에 CSP를 조작하여 `default-src 'none'` 방어막을 뚫고 `connect-src`에 `http://127.0.0.1:* wss://127.0.0.1:*`를 주입하도록 강제 적용함. 이를 통해 Bridge 서버로의 로컬 HTTP 통신이 활성화됨.
|
||||||
|
|
||||||
|
## 완료 상태
|
||||||
|
VSCode VSIX (0.5.27) 빌드 완료 및 릴리스 커밋 패키징 수행.
|
||||||
13
docs/devlog/entries/20260411-001.md
Normal file
13
docs/devlog/entries/20260411-001.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Pure 웹소켓 게이트웨이 전환 (Legacy 파일 브릿지 통신 완전히 제거)
|
||||||
|
|
||||||
|
- **시간**: 2026-04-11 11:00~13:00
|
||||||
|
- **Commit**: `(To be updated)`
|
||||||
|
- **Vikunja**: #N/A
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
- 기존 VS Code 익스텐션과 로컬 Discord Bot 간에 이루어지던 `.gemini/antigravity/bridge/` 기반 파일 공유 통신 체계를 100% 제거하였습니다.
|
||||||
|
- 파이썬 봇 서버(`bot.py`) 내부에서 동작하던 물리적인 폴링 디렉토리 스캐너(`pending_approval_scanner` 및 `chat_snapshot_scanner`) 파일 디펜던시 루프를 완전히 삭제하고 `Hub` WS 핸들러로 대체했습니다. 봇 패키지에 남아있던 `bridge.py`와 `watcher.py` 또한 사용할 이유가 없어져 레포지토리에서 영구적으로 폐기 구별을 내렸습니다.
|
||||||
|
|
||||||
|
## 새로 알게된 사실 혹은 트러블슈팅
|
||||||
|
- 익스텐션에서 `activeSessionId` 변경 시 `watcher.py` 대신 Node.js 네이티브 `fs.watch` 기반으로 자체적인 `BrainWatcher`를 인하우스로 구현해 `step-probe.ts`에 주입함으로써 파이썬 의존도를 완전히 분리할 수 있었습니다.
|
||||||
|
- 권한 팝업 중복 처리 역시 폴더 스캔 대신 단순히 인메모리 `lastFilePermissionTime` 단일 변수로 최적화되었습니다.
|
||||||
22
docs/devlog/entries/20260412-001.md
Normal file
22
docs/devlog/entries/20260412-001.md
Normal 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 응답 전달 확인
|
||||||
22
docs/devlog/entries/20260412-002.md
Normal file
22
docs/devlog/entries/20260412-002.md
Normal 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 검증
|
||||||
28
docs/devlog/entries/20260412-003.md
Normal file
28
docs/devlog/entries/20260412-003.md
Normal 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 검증
|
||||||
25
docs/devlog/entries/20260413-001.md
Normal file
25
docs/devlog/entries/20260413-001.md
Normal 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 검증
|
||||||
27
docs/devlog/entries/20260413-002.md
Normal file
27
docs/devlog/entries/20260413-002.md
Normal 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 검증
|
||||||
28
docs/devlog/entries/20260413-003.md
Normal file
28
docs/devlog/entries/20260413-003.md
Normal 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 검증 (실제 명령어/코드 내용 표시 확인)
|
||||||
49
docs/devlog/entries/20260414-001.md
Normal file
49
docs/devlog/entries/20260414-001.md
Normal 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 재시작 후 즉시 확인 가능
|
||||||
27
docs/devlog/entries/20260415-001.md
Normal file
27
docs/devlog/entries/20260415-001.md
Normal 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 패턴은 정상 동작 확인됨 — 그대로 유지
|
||||||
30
docs/devlog/entries/20260415-002.md
Normal file
30
docs/devlog/entries/20260415-002.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Observer fallback 컨텍스트 추출 수정 (v0.5.46)
|
||||||
|
|
||||||
|
- **시간**: 2026-04-15 10:35~11:02
|
||||||
|
- **Commit**: `pending`
|
||||||
|
- **Vikunja**: #619 → 진행중
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
|
||||||
|
### `_promptOnlySkipped` 플래그 설계
|
||||||
|
|
||||||
|
**문제**: v0.5.45에서 PROMPT_ONLY_RE가 code/pre 요소의 프롬프트 텍스트를 정상 스킵했으나, `extractContextFromNearby()`의 **fallback 경로**(span/div/p 텍스트 수집)가 DOM 트리를 올라가면서 채팅 본문, UI 라벨, AI 응답을 명령어로 잘못 추출.
|
||||||
|
|
||||||
|
**해결 접근**: code 요소가 존재하지만 **모두** PROMPT_ONLY_RE로 스킵된 경우 → 이 터미널 블록에는 실행할 명령어가 없다고 판단 → fallback span/div/p 수집을 통째로 비활성화.
|
||||||
|
|
||||||
|
**대안 검토**:
|
||||||
|
- ❌ fallback 텍스트에 CJK/자연어 필터 추가 → false negative 위험 (한국어 명령어 경로명 등)
|
||||||
|
- ❌ fallback 수집 depth 제한 → DOM 구조가 바뀌면 다시 깨짐
|
||||||
|
- ✅ **prompt-only 스킵과 fallback 비활성화 연동** → 가장 간결하고 확실
|
||||||
|
|
||||||
|
### VSIX 설치 누락 발견
|
||||||
|
|
||||||
|
이전 세션(fd78c28e)에서 v0.5.45 VSIX를 빌드했으나 **설치를 하지 않았음**. extensions.json 확인 결과 v0.5.43이 설치되어 있었음. 원인: 이전 세션에서 `code --install-extension` 실행 없이 AG 재시작만 수행.
|
||||||
|
|
||||||
|
→ known-issues에 "빌드 후 즉시 install 확인 필수" 주의사항 추가
|
||||||
|
|
||||||
|
## 미완료
|
||||||
|
|
||||||
|
- AG 재시작 후 v0.5.46 실제 동작 검증 필요
|
||||||
|
- Discord에 빈 프롬프트/채팅 텍스트가 전송되지 않는지 확인
|
||||||
|
- 검증 완료 후 devlog에 커밋 해시 업데이트 + Vikunja #619 완료 처리
|
||||||
38
docs/devlog/entries/20260416-001.md
Normal file
38
docs/devlog/entries/20260416-001.md
Normal 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 실동작 확인 (장시간 세션 필요)
|
||||||
24
docs/devlog/entries/20260416-002.md
Normal file
24
docs/devlog/entries/20260416-002.md
Normal 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에서 수정 검토)
|
||||||
17
docs/devlog/entries/20260417-001.md
Normal file
17
docs/devlog/entries/20260417-001.md
Normal 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를 설치하면 적용됨.
|
||||||
31
docs/devlog/entries/20260418-001.md
Normal file
31
docs/devlog/entries/20260418-001.md
Normal 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 소비)
|
||||||
@@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
## 시작하기
|
## 시작하기
|
||||||
|
|
||||||
### 1. 봇 실행
|
### 1. 서버 (Docker Gateway)
|
||||||
|
|
||||||
```batch
|
서버에서 Bot + Hub + Gateway를 Docker로 실행:
|
||||||
start_bot.bat
|
|
||||||
```
|
```bash
|
||||||
또는:
|
git clone https://git.variet.net/Variet/gravity_control.git
|
||||||
```powershell
|
cd gravity_control
|
||||||
C:\ProgramData\miniforge3\envs\gravity_control\python.exe main.py
|
cp .env.example .env # DISCORD_TOKEN, DISCORD_GUILD_ID 등 설정
|
||||||
|
docker compose up -d
|
||||||
|
docker compose logs -f # 로그 확인
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Extension 설치
|
### 2. Extension 설치
|
||||||
@@ -21,6 +23,34 @@ cmd /c npx vsce package
|
|||||||
# 생성된 .vsix 파일을 VS Code에서 설치
|
# 생성된 .vsix 파일을 VS Code에서 설치
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 3. Extension 설정
|
||||||
|
|
||||||
|
VS Code `settings.json`에 Hub 연결 정보 추가:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"gravityBridge.hubUrl": "wss://ag.variet.net/ws",
|
||||||
|
"gravityBridge.registrationCode": "<GRAVITY_REGISTRATION_CODE 값>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **참고**: 환경변수 `GRAVITY_HUB_URL`, `GRAVITY_REGISTRATION_CODE`로도 설정 가능하나, VS Code 시작 전에 설정되어야 합니다. `settings.json` 사용을 권장합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 아키텍처
|
||||||
|
|
||||||
|
```
|
||||||
|
[AG IDE] ← Extension ──WS──→ Hub (서버) ←→ Bot ←→ Discord
|
||||||
|
│ │
|
||||||
|
└── step_probe └── pending_owners
|
||||||
|
(WAITING 감지) (응답 라우팅)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Extension → Hub**: WebSocket으로 pending, chat_snapshot, register 전송
|
||||||
|
- **Hub → Extension**: WebSocket으로 command, response 실시간 전송
|
||||||
|
- **File bridge**: WS 미연결 시 폴백 (로컬 `~/.gemini/antigravity/bridge/`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Discord 명령어
|
## Discord 명령어
|
||||||
@@ -31,6 +61,7 @@ cmd /c npx vsce package
|
|||||||
|--------|------|
|
|--------|------|
|
||||||
| `!auto` | 자동 승인 토글 (on/off 반복) |
|
| `!auto` | 자동 승인 토글 (on/off 반복) |
|
||||||
| `!stop` | AG 에이전트 중단 |
|
| `!stop` | AG 에이전트 중단 |
|
||||||
|
| `!N 텍스트` | N번 PC 인스턴스에만 전달 (예: `!2 진행해`) |
|
||||||
| 그 외 텍스트 | AG에 직접 메시지 전달 |
|
| 그 외 텍스트 | AG에 직접 메시지 전달 |
|
||||||
|
|
||||||
### 슬래시 명령어
|
### 슬래시 명령어
|
||||||
@@ -51,103 +82,45 @@ Discord에서 `!auto` 를 입력할 때마다 on↔off 토글됩니다.
|
|||||||
- **OFF (기본)**: 승인 요청마다 Discord에 ✅/❌ 버튼 표시 → 클릭하여 수동 승인
|
- **OFF (기본)**: 승인 요청마다 Discord에 ✅/❌ 버튼 표시 → 클릭하여 수동 승인
|
||||||
- **ON**: 승인 요청 시 자동으로 승인 → Discord에 `🤖 자동 승인됨` 표시
|
- **ON**: 승인 요청 시 자동으로 승인 → Discord에 `🤖 자동 승인됨` 표시
|
||||||
|
|
||||||
```
|
|
||||||
사용자: !auto
|
|
||||||
봇: 🟢 자동 승인 모드
|
|
||||||
프로젝트: gravity_control
|
|
||||||
모든 승인 요청이 자동으로 승인됩니다
|
|
||||||
|
|
||||||
사용자: !auto
|
|
||||||
봇: 🔴 수동 승인 모드
|
|
||||||
프로젝트: gravity_control
|
|
||||||
모든 승인 요청이 수동 확인이 필요합니다
|
|
||||||
```
|
|
||||||
|
|
||||||
### 자동 승인 시 Discord 표시
|
|
||||||
|
|
||||||
```
|
|
||||||
🤖 자동 승인됨
|
|
||||||
┌─────────────────────────────┐
|
|
||||||
│ run_command: npm run build │
|
|
||||||
└─────────────────────────────┘
|
|
||||||
auto-approve | 1741678...
|
|
||||||
```
|
|
||||||
|
|
||||||
> **주의**: 봇 재시작 시 auto-approve 상태는 초기화됩니다 (기본 OFF).
|
> **주의**: 봇 재시작 시 auto-approve 상태는 초기화됩니다 (기본 OFF).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 아키텍처
|
## Bot Mode
|
||||||
|
|
||||||
```
|
|
||||||
[AG IDE] ← Extension → bridge/ ← Bot → Discord
|
|
||||||
│ │
|
|
||||||
└── step_probe └── pending 스캔
|
|
||||||
(WAITING 감지) (자동 승인 처리)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Bridge 프로토콜
|
|
||||||
|
|
||||||
```
|
|
||||||
~/.gemini/antigravity/bridge/
|
|
||||||
├── pending/ Extension → Bot (승인 요청)
|
|
||||||
├── response/ Bot → Extension (승인 결과)
|
|
||||||
├── commands/ Bot → Extension (사용자 명령)
|
|
||||||
└── register/ Extension → Bot (세션 매핑)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Bot Mode
|
|
||||||
|
|
||||||
| 모드 | 설정 | 설명 |
|
| 모드 | 설정 | 설명 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `local` (기본) | `BOT_MODE=local` | 로컬 파일시스템 bridge 사용 |
|
| `gateway` | `BOT_MODE=gateway` | 서버: Bot + Hub WS + Gateway HTTP API (Docker) |
|
||||||
| `remote` (미래) | `BOT_MODE=remote` | HTTP로 원격 bridge 폴링 (Collector 모드) |
|
| `local` | `BOT_MODE=local` | 로컬: 파일 bridge 전용 (Hub 없이 단독 실행) |
|
||||||
| `gateway` | `BOT_MODE=gateway` | 서버에서 Discord 통신 + HTTP API (Docker용) |
|
| `remote` | `BOT_MODE=remote` | ~~Collector 모드~~ **(deprecated — WS Hub로 대체됨)** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 설정 (.env)
|
## 설정 (.env)
|
||||||
|
|
||||||
```env
|
```env
|
||||||
DISCORD_TOKEN=xxx # Discord 봇 토큰 (필수)
|
DISCORD_TOKEN=xxx # Discord 봇 토큰 (필수)
|
||||||
DISCORD_GUILD_ID=xxx # Discord 서버 ID (필수)
|
DISCORD_GUILD_ID=xxx # Discord 서버 ID (필수)
|
||||||
BRAIN_PATH= # AG 브레인 경로 (기본: ~/.gemini/antigravity/brain)
|
BRAIN_PATH= # AG 브레인 경로 (기본: ~/.gemini/antigravity/brain)
|
||||||
BOT_MODE=local # 봇 모드 (local/remote)
|
BOT_MODE=gateway # 봇 모드 (gateway/local)
|
||||||
REMOTE_BRIDGE_URL= # 원격 브릿지 URL (remote 모드 전용)
|
GATEWAY_PORT=8585 # Gateway 포트
|
||||||
DEBOUNCE_SECONDS=2 # 이벤트 디바운스 (초)
|
GATEWAY_API_KEY=xxx # Gateway API 인증 키
|
||||||
|
GRAVITY_REGISTRATION_CODE=xxx # Extension WS 인증 코드
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Docker 배포 (Gateway)
|
## Gateway API
|
||||||
|
|
||||||
서버에서 Gateway 봇을 Docker로 실행:
|
|
||||||
|
|
||||||
```
|
|
||||||
[로컬 PC] [서버 Docker]
|
|
||||||
Extension → bridge/ ← 로컬 Bot ──HTTP──→ Gateway :8585 ←→ Discord
|
|
||||||
(Collector)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 서버에서 실행
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://git.variet.net/Variet/gravity_control.git
|
|
||||||
cd gravity_control
|
|
||||||
cp .env.example .env # DISCORD_TOKEN, DISCORD_GUILD_ID 입력
|
|
||||||
docker compose up -d
|
|
||||||
docker compose logs -f # 로그 확인
|
|
||||||
```
|
|
||||||
|
|
||||||
### Gateway API
|
|
||||||
|
|
||||||
| 메서드 | 경로 | 설명 |
|
| 메서드 | 경로 | 설명 |
|
||||||
|--------|------|------|
|
|--------|------|------|
|
||||||
| GET | `/health` | 헬스체크 |
|
| GET | `/health` | 헬스체크 |
|
||||||
| POST | `/api/pending` | Collector → 승인 요청 |
|
| GET | `/ws` | Extension WebSocket 연결 |
|
||||||
| GET | `/api/response/{rid}` | Collector ← 승인 응답 |
|
| GET | `/hub/status` | Hub 연결 상태 |
|
||||||
| POST | `/api/chat` | Collector → 채팅 스냅샷 |
|
| POST | `/api/pending` | 승인 요청 (API/Collector) |
|
||||||
| GET | `/api/commands/{project}` | Collector ← 명령 폴링 |
|
| GET | `/api/response/{rid}` | 승인 응답 조회 |
|
||||||
|
| POST | `/api/chat` | 채팅 스냅샷 전송 |
|
||||||
|
| GET | `/api/commands/{project}` | 명령 폴링 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -155,6 +128,8 @@ docker compose logs -f # 로그 확인
|
|||||||
|
|
||||||
| 증상 | 해결 |
|
| 증상 | 해결 |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
| 승인 클릭해도 AG 반응 없음 | Hub WS 연결 확인: `/hub/status` → Extension이 connected인지 |
|
||||||
| `!auto` 했는데 자동 승인 안 됨 | Extension VSIX 재빌드 + 재설치 필요 |
|
| `!auto` 했는데 자동 승인 안 됨 | Extension VSIX 재빌드 + 재설치 필요 |
|
||||||
| 봇 재시작 후 auto가 꺼져있음 | 정상 — `!auto`로 다시 켜기 |
|
| 봇 재시작 후 auto가 꺼져있음 | 정상 — `!auto`로 다시 켜기 |
|
||||||
| Python 못 찾음 | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe` 사용 |
|
| Python 못 찾음 | `C:\ProgramData\miniforge3\envs\gravity_control\python.exe` 사용 |
|
||||||
|
| Discord 메시지 이중 전달 | 로컬 Collector (`python main.py` BOT_MODE=remote) 실행 여부 확인 — 종료 필요 |
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
src/**
|
src/**
|
||||||
node_modules/**
|
node_modules/**
|
||||||
|
!node_modules/ws/**
|
||||||
*.ts
|
*.ts
|
||||||
!out/**
|
!out/**
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
430
extension/package-lock.json
generated
430
extension/package-lock.json
generated
@@ -1,58 +1,380 @@
|
|||||||
{
|
{
|
||||||
"name": "gravity-bridge",
|
"name": "gravity-bridge",
|
||||||
"version": "0.3.8",
|
"version": "0.5.103",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "gravity-bridge",
|
"name": "gravity-bridge",
|
||||||
"version": "0.3.8",
|
"version": "0.5.103",
|
||||||
"devDependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^20.0.0",
|
"cheerio": "^1.2.0",
|
||||||
"@types/vscode": "^1.100.0",
|
"ws": "^8.19.0"
|
||||||
"typescript": "^5.3.0"
|
},
|
||||||
},
|
"devDependencies": {
|
||||||
"engines": {
|
"@types/node": "^20.0.0",
|
||||||
"vscode": "^1.100.0"
|
"@types/vscode": "^1.100.0",
|
||||||
}
|
"typescript": "^5.3.0"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"engines": {
|
||||||
"version": "20.19.37",
|
"vscode": "^1.100.0"
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
|
}
|
||||||
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
|
},
|
||||||
"dev": true,
|
"node_modules/@types/node": {
|
||||||
"license": "MIT",
|
"version": "20.19.37",
|
||||||
"dependencies": {
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
|
||||||
"undici-types": "~6.21.0"
|
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
|
||||||
}
|
"dev": true,
|
||||||
},
|
"license": "MIT",
|
||||||
"node_modules/@types/vscode": {
|
"dependencies": {
|
||||||
"version": "1.100.0",
|
"undici-types": "~6.21.0"
|
||||||
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz",
|
}
|
||||||
"integrity": "sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==",
|
},
|
||||||
"dev": true,
|
"node_modules/@types/vscode": {
|
||||||
"license": "MIT"
|
"version": "1.100.0",
|
||||||
},
|
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz",
|
||||||
"node_modules/typescript": {
|
"integrity": "sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==",
|
||||||
"version": "5.9.3",
|
"dev": true,
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"license": "MIT"
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
},
|
||||||
"dev": true,
|
"node_modules/boolbase": {
|
||||||
"license": "Apache-2.0",
|
"version": "1.0.0",
|
||||||
"bin": {
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
"tsc": "bin/tsc",
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||||
"tsserver": "bin/tsserver"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"engines": {
|
"node_modules/cheerio": {
|
||||||
"node": ">=14.17"
|
"version": "1.2.0",
|
||||||
}
|
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz",
|
||||||
},
|
"integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==",
|
||||||
"node_modules/undici-types": {
|
"license": "MIT",
|
||||||
"version": "6.21.0",
|
"dependencies": {
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"cheerio-select": "^2.1.0",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"dom-serializer": "^2.0.0",
|
||||||
"dev": true,
|
"domhandler": "^5.0.3",
|
||||||
"license": "MIT"
|
"domutils": "^3.2.2",
|
||||||
|
"encoding-sniffer": "^0.2.1",
|
||||||
|
"htmlparser2": "^10.1.0",
|
||||||
|
"parse5": "^7.3.0",
|
||||||
|
"parse5-htmlparser2-tree-adapter": "^7.1.0",
|
||||||
|
"parse5-parser-stream": "^7.1.2",
|
||||||
|
"undici": "^7.19.0",
|
||||||
|
"whatwg-mimetype": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cheerio-select": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0",
|
||||||
|
"css-select": "^5.1.0",
|
||||||
|
"css-what": "^6.1.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-select": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0",
|
||||||
|
"css-what": "^6.1.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"nth-check": "^2.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-what": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domelementtype": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domutils": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/encoding-sniffer": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
|
"whatwg-encoding": "^3.1.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.2.2",
|
||||||
|
"entities": "^7.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmlparser2/node_modules/entities": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nth-check": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse5": {
|
||||||
|
"version": "7.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||||
|
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse5-htmlparser2-tree-adapter": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"parse5": "^7.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse5-parser-stream": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"parse5": "^7.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse5/node_modules/entities": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "7.24.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz",
|
||||||
|
"integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-encoding": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||||
|
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-mimetype": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +1,90 @@
|
|||||||
{
|
{
|
||||||
"name": "gravity-bridge",
|
"name": "gravity-bridge",
|
||||||
"displayName": "Gravity Bridge",
|
"displayName": "Gravity Bridge",
|
||||||
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
"description": "Discord-based unified approval system for Antigravity AI interactions.",
|
||||||
"version": "0.3.14",
|
"version": "0.5.103",
|
||||||
"publisher": "variet",
|
"publisher": "variet",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.100.0"
|
"vscode": "^1.100.0"
|
||||||
},
|
},
|
||||||
"categories": [
|
"categories": [
|
||||||
"Other",
|
"Other",
|
||||||
"Chat"
|
"Chat"
|
||||||
|
],
|
||||||
|
"activationEvents": [
|
||||||
|
"onStartupFinished"
|
||||||
|
],
|
||||||
|
"main": "./out/extension.js",
|
||||||
|
"scripts": {
|
||||||
|
"vscode:prepublish": "npm run compile",
|
||||||
|
"compile": "tsc -p ./ && node -e \"const fs=require('fs'),p=require('path');const s=p.join('src','sdk'),d=p.join('out','sdk');if(fs.existsSync(s)){fs.mkdirSync(d,{recursive:true});fs.readdirSync(s).forEach(f=>fs.copyFileSync(p.join(s,f),p.join(d,f)));console.log('SDK copied to out/sdk')};\"",
|
||||||
|
"watch": "tsc -watch -p ./"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/vscode": "^1.100.0",
|
||||||
|
"typescript": "^5.3.0"
|
||||||
|
},
|
||||||
|
"contributes": {
|
||||||
|
"chatParticipants": [
|
||||||
|
{
|
||||||
|
"id": "gravity-bridge.gravity",
|
||||||
|
"name": "gravity",
|
||||||
|
"fullName": "Gravity Bridge",
|
||||||
|
"description": "?<3F>???<3F>스?<3F>리<EFBFBD>?Discord<72>??<3F>송 + AI ?<3F>어",
|
||||||
|
"isSticky": false
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"activationEvents": [
|
"commands": [
|
||||||
"onStartupFinished"
|
{
|
||||||
|
"command": "gravityBridge.start",
|
||||||
|
"title": "Gravity Bridge: Start"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gravityBridge.stop",
|
||||||
|
"title": "Gravity Bridge: Stop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gravityBridge.approve",
|
||||||
|
"title": "Gravity Bridge: Approve Pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gravityBridge.reject",
|
||||||
|
"title": "Gravity Bridge: Reject Pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gravityBridge.connect",
|
||||||
|
"title": "Gravity Bridge: Connect Session"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"main": "./out/extension.js",
|
"configuration": {
|
||||||
"scripts": {
|
"title": "Gravity Bridge",
|
||||||
"compile": "tsc -p ./ && node -e \"const fs=require('fs'),p=require('path');const s=p.join('src','sdk'),d=p.join('out','sdk');if(fs.existsSync(s)){fs.mkdirSync(d,{recursive:true});fs.readdirSync(s).forEach(f=>fs.copyFileSync(p.join(s,f),p.join(d,f)));console.log('SDK copied to out/sdk')};\"",
|
"properties": {
|
||||||
"watch": "tsc -watch -p ./"
|
"gravityBridge.bridgePath": {
|
||||||
},
|
"type": "string",
|
||||||
"devDependencies": {
|
"default": "",
|
||||||
"@types/node": "^20.0.0",
|
"description": "Bridge ?<3F>렉?<3F>리 경로 (기본: ~/.gemini/antigravity/bridge)"
|
||||||
"@types/vscode": "^1.100.0",
|
},
|
||||||
"typescript": "^5.3.0"
|
"gravityBridge.projectName": {
|
||||||
},
|
"type": "string",
|
||||||
"contributes": {
|
"default": "",
|
||||||
"chatParticipants": [
|
"description": "?<3F>로?<3F>트 ?<3F>름 (기본: git remote ?<3F>포<EFBFBD>?"
|
||||||
{
|
},
|
||||||
"id": "gravity-bridge.gravity",
|
"gravityBridge.hubUrl": {
|
||||||
"name": "gravity",
|
"type": "string",
|
||||||
"fullName": "Gravity Bridge",
|
"default": "",
|
||||||
"description": "대화 히스토리를 Discord로 전송 + AI 제어",
|
"description": "WebSocket Hub URL (?? wss://your-server.com/ws)"
|
||||||
"isSticky": false
|
},
|
||||||
}
|
"gravityBridge.registrationCode": {
|
||||||
],
|
"type": "string",
|
||||||
"commands": [
|
"default": "",
|
||||||
{
|
"description": "Hub ?<3F>록 코드 (?<3F>버?<3F>서 발급)"
|
||||||
"command": "gravityBridge.start",
|
|
||||||
"title": "Gravity Bridge: Start"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"command": "gravityBridge.stop",
|
|
||||||
"title": "Gravity Bridge: Stop"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"command": "gravityBridge.approve",
|
|
||||||
"title": "Gravity Bridge: Approve Pending"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"command": "gravityBridge.reject",
|
|
||||||
"title": "Gravity Bridge: Reject Pending"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"command": "gravityBridge.connect",
|
|
||||||
"title": "Gravity Bridge: Connect Session"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configuration": {
|
|
||||||
"title": "Gravity Bridge",
|
|
||||||
"properties": {
|
|
||||||
"gravityBridge.bridgePath": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "",
|
|
||||||
"description": "Bridge 디렉토리 경로 (기본: ~/.gemini/antigravity/bridge)"
|
|
||||||
},
|
|
||||||
"gravityBridge.projectName": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "",
|
|
||||||
"description": "프로젝트 이름 (기본: git remote 레포명)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cheerio": "^1.2.0",
|
||||||
|
"ws": "^8.19.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
98
extension/scratch/analyze_all_dumps.js
Normal file
98
extension/scratch/analyze_all_dumps.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
extension/scratch/analyze_dump.js
Normal file
28
extension/scratch/analyze_dump.js
Normal 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));
|
||||||
2
extension/scratch/diff_test.py
Normal file
2
extension/scratch/diff_test.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# diff_review detection test v2
|
||||||
|
test_value = "hello"
|
||||||
37
extension/scratch/discord_channels.js
Normal file
37
extension/scratch/discord_channels.js
Normal 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));
|
||||||
55
extension/scratch/discord_read.js
Normal file
55
extension/scratch/discord_read.js
Normal 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));
|
||||||
29
extension/scratch/find_user_msg.js
Normal file
29
extension/scratch/find_user_msg.js
Normal 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)));
|
||||||
26
extension/scratch/print_dom_tree.js
Normal file
26
extension/scratch/print_dom_tree.js
Normal 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);
|
||||||
22
extension/scratch/test_accept.js
Normal file
22
extension/scratch/test_accept.js
Normal 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)));
|
||||||
114
extension/scratch/verify_final.js
Normal file
114
extension/scratch/verify_final.js
Normal 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 ===');
|
||||||
43
extension/scratch/verify_junk.js
Normal file
43
extension/scratch/verify_junk.js
Normal 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);
|
||||||
|
}
|
||||||
42
extension/scratch/verify_regex.js
Normal file
42
extension/scratch/verify_regex.js
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
122
extension/scratch/verify_v096.js
Normal file
122
extension/scratch/verify_v096.js
Normal 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 ===');
|
||||||
52
extension/scratch/verify_v32.js
Normal file
52
extension/scratch/verify_v32.js
Normal 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'));
|
||||||
566
extension/src/approval-handler.ts
Normal file
566
extension/src/approval-handler.ts
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
/**
|
||||||
|
* Approval Handler — response processing + approval execution pipeline.
|
||||||
|
*
|
||||||
|
* Extracted from step-probe.ts to reduce file size.
|
||||||
|
* Handles:
|
||||||
|
* - Response file watching (file-based bridge fallback)
|
||||||
|
* - Response processing (diff_review, DOM observer, step_probe paths)
|
||||||
|
* - Multi-strategy approval execution (VS Code commands, RPC, DOM click)
|
||||||
|
* - 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 fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import type { BridgeContext } from './step-probe';
|
||||||
|
|
||||||
|
// ─── Module-level state (injected via initApprovalHandler) ───
|
||||||
|
|
||||||
|
let ctx: BridgeContext;
|
||||||
|
let responseWatcher: fs.FSWatcher | null = null;
|
||||||
|
let getTrajectoryId: () => string = () => '';
|
||||||
|
|
||||||
|
// ─── Public API ───
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the approval handler with shared context.
|
||||||
|
* Called from initStepProbe() in step-probe.ts.
|
||||||
|
*/
|
||||||
|
export function initApprovalHandler(
|
||||||
|
context: BridgeContext,
|
||||||
|
trajectoryIdGetter: () => string,
|
||||||
|
) {
|
||||||
|
ctx = context;
|
||||||
|
getTrajectoryId = trajectoryIdGetter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle diff_review Accept all / Reject all response.
|
||||||
|
* Called from both WS onResponse (extension.ts) and processResponseFile.
|
||||||
|
*
|
||||||
|
* This was previously only in processResponseFile (file-bridge path).
|
||||||
|
* When WS was added (v0.4.x), the onResponse handler skipped this logic entirely,
|
||||||
|
* causing Accept All to stop working — a regression.
|
||||||
|
*/
|
||||||
|
export async function handleDiffReviewResponse(data: {
|
||||||
|
request_id: string;
|
||||||
|
approved: boolean;
|
||||||
|
button_index?: number;
|
||||||
|
step_type?: string;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
const btnIdx = data.button_index ?? -1;
|
||||||
|
const isAccept = btnIdx === 0 || (btnIdx === -1 && data.approved);
|
||||||
|
const cmd = isAccept
|
||||||
|
? 'antigravity.prioritized.agentAcceptAllInFile'
|
||||||
|
: 'antigravity.prioritized.agentRejectAllInFile';
|
||||||
|
ctx.logToFile(`[DIFF-REVIEW-WS] → ${isAccept ? 'ACCEPT' : 'REJECT'} (btnIdx=${btnIdx}, rid=${data.request_id?.substring(0, 12)})`);
|
||||||
|
|
||||||
|
let diffReviewDone = false;
|
||||||
|
let modifiedFiles: string[] = [];
|
||||||
|
|
||||||
|
// Load tracked step indices and modified files from memory cache or pending file
|
||||||
|
const trackedSteps: number[] = [];
|
||||||
|
const memMeta = ctx.diffReviewMetadata.get(data.request_id);
|
||||||
|
if (memMeta) {
|
||||||
|
trackedSteps.push(...memMeta.edit_step_indices);
|
||||||
|
modifiedFiles = memMeta.modified_files;
|
||||||
|
ctx.diffReviewMetadata.delete(data.request_id);
|
||||||
|
ctx.logToFile(`[DIFF-REVIEW-WS] loaded from memory: steps=[${trackedSteps.join(',')}] files=${modifiedFiles.length}`);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const pf = path.join(ctx.bridgePath, 'pending', `${data.request_id}.json`);
|
||||||
|
if (fs.existsSync(pf)) {
|
||||||
|
const pd = JSON.parse(fs.readFileSync(pf, 'utf-8'));
|
||||||
|
if (pd.edit_step_indices) trackedSteps.push(...pd.edit_step_indices);
|
||||||
|
if (pd.modified_files) modifiedFiles = pd.modified_files;
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 1: VS Code command — open review panel + focus each file + accept/reject
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
await vscode.commands.executeCommand('antigravity.openReviewChanges');
|
||||||
|
ctx.logToFile(`[DIFF-REVIEW-WS] openReviewChanges OK`);
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
if (modifiedFiles.length > 0) {
|
||||||
|
for (const fp of modifiedFiles) {
|
||||||
|
try {
|
||||||
|
const uri = vscode.Uri.file(fp);
|
||||||
|
const doc = await vscode.workspace.openTextDocument(uri);
|
||||||
|
await vscode.window.showTextDocument(doc, { preview: false });
|
||||||
|
await new Promise(r => setTimeout(r, 300));
|
||||||
|
await vscode.commands.executeCommand(cmd);
|
||||||
|
ctx.logToFile(`[DIFF-REVIEW-WS] ✅ ${cmd} on ${fp.split(/[\\/]/).pop()} OK`);
|
||||||
|
diffReviewDone = true;
|
||||||
|
} catch (e: any) {
|
||||||
|
ctx.logToFile(`[DIFF-REVIEW-WS] per-file error on ${fp}: ${e.message?.substring(0, 80)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await vscode.commands.executeCommand(cmd);
|
||||||
|
ctx.logToFile(`[DIFF-REVIEW-WS] ✅ ${cmd} executed (no file list)`);
|
||||||
|
diffReviewDone = true;
|
||||||
|
}
|
||||||
|
} catch (cmdErr: any) {
|
||||||
|
ctx.logToFile(`[DIFF-REVIEW-WS] Strategy 1 command error: ${cmdErr.message?.substring(0, 200)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: individual hunk accept/reject
|
||||||
|
if (!diffReviewDone) {
|
||||||
|
try {
|
||||||
|
const hunkCmd = isAccept
|
||||||
|
? 'antigravity.prioritized.agentAcceptFocusedHunk'
|
||||||
|
: 'antigravity.prioritized.agentRejectFocusedHunk';
|
||||||
|
await vscode.commands.executeCommand(hunkCmd);
|
||||||
|
ctx.logToFile(`[DIFF-REVIEW-WS] ✅ ${hunkCmd} fallback OK`);
|
||||||
|
diffReviewDone = true;
|
||||||
|
} catch (hunkErr: any) {
|
||||||
|
ctx.logToFile(`[DIFF-REVIEW-WS] hunk fallback error: ${hunkErr.message?.substring(0, 100)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!diffReviewDone) {
|
||||||
|
ctx.logToFile(`[DIFF-REVIEW-WS] ❌ ALL strategies failed for rid=${data.request_id}`);
|
||||||
|
}
|
||||||
|
return diffReviewDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Response Watcher ───
|
||||||
|
|
||||||
|
export function setupResponseWatcher() {
|
||||||
|
const responseDir = path.join(ctx.bridgePath, 'response');
|
||||||
|
if (!fs.existsSync(responseDir)) {
|
||||||
|
fs.mkdirSync(responseDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const processAnyResponse = (filename: string) => {
|
||||||
|
const fp = path.join(responseDir, filename);
|
||||||
|
if (fs.existsSync(fp)) {
|
||||||
|
// Check if this response belongs to our project
|
||||||
|
const rid = filename.replace('.json', '');
|
||||||
|
const pendingFile = path.join(ctx.bridgePath, 'pending', `${rid}.json`);
|
||||||
|
if (fs.existsSync(pendingFile)) {
|
||||||
|
try {
|
||||||
|
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
|
||||||
|
if (pending.project_name && pending.project_name !== ctx.projectName) {
|
||||||
|
return; // Not our project
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
} else {
|
||||||
|
// Pending file missing (deleted or auto_resolved) — check response data itself
|
||||||
|
try {
|
||||||
|
const respData = JSON.parse(fs.readFileSync(fp, 'utf-8'));
|
||||||
|
if (respData.project_name && respData.project_name !== ctx.projectName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
setTimeout(() => processResponseFile(fp), 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pollAllResponses = () => {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(responseDir)) return;
|
||||||
|
for (const f of fs.readdirSync(responseDir)) {
|
||||||
|
if (f.endsWith('.json')) {
|
||||||
|
processAnyResponse(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
};
|
||||||
|
|
||||||
|
pollAllResponses(); // Process any existing responses on startup
|
||||||
|
|
||||||
|
try {
|
||||||
|
responseWatcher = fs.watch(responseDir, (event, filename) => {
|
||||||
|
if (filename && filename.endsWith('.json') && event === 'rename') {
|
||||||
|
processAnyResponse(filename);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('Gravity Bridge: response watcher started');
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log(`Gravity Bridge: response watcher failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polling fallback: fs.watch on Windows can silently fail
|
||||||
|
setInterval(pollAllResponses, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Response File Processing ───
|
||||||
|
|
||||||
|
async function processResponseFile(filePath: string) {
|
||||||
|
try {
|
||||||
|
// Gracefully handle files already consumed by HTTP handler
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
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(',')}]`;
|
||||||
|
console.log(`Gravity Bridge: ${msg}`);
|
||||||
|
ctx.logToFile(msg);
|
||||||
|
|
||||||
|
// Skip stale timeout responses: if pending is old and this is a reject, it's likely a bot timeout
|
||||||
|
const ridTimestamp = parseInt((resp.request_id || '').split('_')[0], 10);
|
||||||
|
if (!isNaN(ridTimestamp)) {
|
||||||
|
const ageMs = Date.now() - ridTimestamp;
|
||||||
|
const STALE_THRESHOLD_MS = 120_000; // 2 minutes
|
||||||
|
if (ageMs > STALE_THRESHOLD_MS && !resp.approved) {
|
||||||
|
ctx.logToFile(`[RESPONSE] SKIPPED stale timeout: rid=${resp.request_id} age=${Math.round(ageMs / 1000)}s (>${STALE_THRESHOLD_MS / 1000}s, reject)`);
|
||||||
|
try { fs.unlinkSync(filePath); } catch { }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find matching pending request
|
||||||
|
const pendingDir = path.join(ctx.bridgePath, 'pending');
|
||||||
|
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
|
||||||
|
let sessionId = '';
|
||||||
|
let isDomObserver = false;
|
||||||
|
let pendingStepType = resp.step_type || ''; // from bot's response (new)
|
||||||
|
let pendingStepIndex = -1;
|
||||||
|
if (fs.existsSync(pendingFile)) {
|
||||||
|
try {
|
||||||
|
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
|
||||||
|
|
||||||
|
// FIX #2: Skip if pending was already resolved locally (auto_resolve or expired)
|
||||||
|
if (pending.status === 'auto_resolved' || pending.status === 'expired') {
|
||||||
|
ctx.logToFile(`[RESPONSE] SKIP — pending already ${pending.status} (rid=${resp.request_id})`);
|
||||||
|
try { fs.unlinkSync(filePath); } catch { }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionId = pending.conversation_id || '';
|
||||||
|
isDomObserver = pending.auto_detected === true
|
||||||
|
|| pending.source === 'dom_observer';
|
||||||
|
pendingStepType = pending.step_type || '';
|
||||||
|
pendingStepIndex = pending.step_index ?? ctx.lastPendingStepIndex;
|
||||||
|
// File permission detection: check command content or explicit step_type
|
||||||
|
const cmd = (pending.command || '').toLowerCase();
|
||||||
|
if (pendingStepType === 'file_permission' || cmd.includes('allow') || cmd.includes('파일 접근')) {
|
||||||
|
// Map button_index → scope: 0=Once, 1=Conversation, 2=Deny
|
||||||
|
const btnIdx = resp.button_index ?? -1;
|
||||||
|
if (btnIdx === 1) {
|
||||||
|
pendingStepType = 'file_permission_conversation';
|
||||||
|
} else if (btnIdx === 2) {
|
||||||
|
pendingStepType = 'file_permission_deny';
|
||||||
|
} else {
|
||||||
|
pendingStepType = 'file_permission_once';
|
||||||
|
}
|
||||||
|
ctx.logToFile(`[RESPONSE] file_permission detected from pending cmd, mapped to ${pendingStepType}`);
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ MULTI-STRATEGY APPROVAL (v3.0) ═══
|
||||||
|
const approved = resp.approved;
|
||||||
|
|
||||||
|
// ── diff_review: Accept all / Reject all ──
|
||||||
|
if (pendingStepType === 'diff_review') {
|
||||||
|
// Delegate to shared handler (also used by WS onResponse path in extension.ts)
|
||||||
|
await handleDiffReviewResponse({
|
||||||
|
request_id: resp.request_id,
|
||||||
|
approved,
|
||||||
|
button_index: resp.button_index,
|
||||||
|
step_type: pendingStepType,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// ALL paths (dom_observer + step_probe) use same strategy pipeline
|
||||||
|
const targetSession = sessionId || ctx.activeSessionId;
|
||||||
|
ctx.logToFile(`[RESPONSE] → tryApprovalStrategies(${approved}, ${targetSession.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`);
|
||||||
|
const strategyResult = await tryApprovalStrategies(approved, targetSession, pendingStepType, pendingStepIndex);
|
||||||
|
ctx.logToFile(`[RESPONSE] strategy result: ${strategyResult}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
||||||
|
|
||||||
|
// FIX v2 (2026-03-16): Correct state management after response processing.
|
||||||
|
// Set ctx.sawRunningAfterPending=true to close the auto_resolve gate.
|
||||||
|
ctx.sawRunningAfterPending = true;
|
||||||
|
|
||||||
|
// Cleanup response file
|
||||||
|
// CRITICAL: DOM observer responses must NOT be deleted here!
|
||||||
|
if (!isDomObserver) {
|
||||||
|
try { fs.unlinkSync(filePath); } catch { }
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
const log = `[RESPONSE] error: ${e.message}`;
|
||||||
|
console.log(`Gravity Bridge: ${log}`);
|
||||||
|
ctx.logToFile(log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Approval Strategies ───
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try multiple approval methods sequentially.
|
||||||
|
* Returns a string describing which method succeeded (or all failed).
|
||||||
|
*
|
||||||
|
* Strategy order (most reliable first):
|
||||||
|
* 0. antigravity.acceptAgentStep / rejectAgentStep (AG VS Code commands — always works)
|
||||||
|
* 1. HandleCascadeUserInteraction RPC (cross-platform, needs stepIndex)
|
||||||
|
* 2. Renderer DOM Click via HTTP Bridge (fallback)
|
||||||
|
*/
|
||||||
|
export async function tryApprovalStrategies(approved: boolean, sessionId: string, stepType: string = '', stepIndex: number = -1): Promise<string> {
|
||||||
|
const action = approved ? 'APPROVE' : 'REJECT';
|
||||||
|
const effectiveStepIndex = stepIndex >= 0 ? stepIndex
|
||||||
|
: (ctx.lastPendingStepIndex >= 0 ? ctx.lastPendingStepIndex : -1);
|
||||||
|
ctx.logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${effectiveStepIndex}`);
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// STRATEGY 0: SDK-verified AG commands (step_type-aware dispatch)
|
||||||
|
//
|
||||||
|
// From SDK index.js (verified command mapping):
|
||||||
|
// antigravity.agent.acceptAgentStep — code edits, file writes
|
||||||
|
// antigravity.agent.rejectAgentStep — reject code edits
|
||||||
|
// antigravity.command.accept — non-terminal commands (Run, Allow, etc.)
|
||||||
|
// antigravity.command.reject — reject non-terminal commands
|
||||||
|
// antigravity.terminalCommand.accept — terminal commands
|
||||||
|
// antigravity.terminalCommand.reject — reject terminal commands
|
||||||
|
// antigravity.terminalCommand.run — run terminal commands
|
||||||
|
//
|
||||||
|
// These operate on the currently focused/active step — no stepIndex needed!
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
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)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// STRATEGY 1: HandleCascadeUserInteraction RPC
|
||||||
|
// Now supports BOTH approve AND reject.
|
||||||
|
// Requires valid stepIndex for most step types.
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
if (ctx.sdk && effectiveStepIndex >= 0) {
|
||||||
|
const typeLower = stepType.toLowerCase().replace('cortex_step_type_', '');
|
||||||
|
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')) {
|
||||||
|
try {
|
||||||
|
ctx.logToFile(`[APPROVAL-1-CODE] trying submitCodeAcknowledgement command`);
|
||||||
|
await vscode.commands.executeCommand('antigravity.prioritized.submitCodeAcknowledgement');
|
||||||
|
ctx.logToFile(`[APPROVAL-1-CODE] ✅ submitCodeAcknowledgement OK`);
|
||||||
|
return `CMD:submitCodeAcknowledgement(accept=${approved})`;
|
||||||
|
} catch {
|
||||||
|
ctx.logToFile(`[APPROVAL-1-CODE] submitCodeAcknowledgement not available, trying RPC`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ctx.logToFile(`[APPROVAL-1-CODE] acknowledgeCodeActionStep(cascadeId=${sessionId.substring(0, 8)}, accept=${approved}, stepIndices=[${effectiveStepIndex}])`);
|
||||||
|
const ackResult = await ctx.sdk.ls.rawRPC('acknowledgeCodeActionStep', {
|
||||||
|
cascadeId: sessionId,
|
||||||
|
accept: approved,
|
||||||
|
stepIndices: [effectiveStepIndex],
|
||||||
|
});
|
||||||
|
ctx.logToFile(`[APPROVAL-1-CODE] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`);
|
||||||
|
return `RPC:acknowledgeCodeActionStep(accept=${approved})`;
|
||||||
|
} catch (e: any) {
|
||||||
|
ctx.logToFile(`[APPROVAL-1-CODE] ❌ ${e.message.substring(0, 200)}`);
|
||||||
|
// Fall through to generic HandleCascadeUserInteraction
|
||||||
|
interactionPayload = { runCommand: { confirm: approved } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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')) {
|
||||||
|
interactionPayload = { runCommand: { confirm: approved } };
|
||||||
|
} else if (typeLower.includes('open_browser')) {
|
||||||
|
interactionPayload = { openBrowserUrl: { confirm: approved } };
|
||||||
|
} else if (typeLower.includes('send_command_input')) {
|
||||||
|
interactionPayload = { sendCommandInput: { confirm: approved } };
|
||||||
|
} else if (typeLower.includes('read_url')) {
|
||||||
|
interactionPayload = { readUrlContent: { confirm: approved } };
|
||||||
|
} else if (typeLower.includes('mcp')) {
|
||||||
|
interactionPayload = { mcpTool: { confirm: approved } };
|
||||||
|
} else if (typeLower.includes('invoke_subagent') || typeLower.includes('extension_code') || typeLower.includes('browser_subagent')) {
|
||||||
|
interactionPayload = { runExtensionCode: { confirm: approved } };
|
||||||
|
} else if (typeLower.includes('file_permission')) {
|
||||||
|
if (typeLower.includes('deny')) {
|
||||||
|
interactionPayload = { filePermission: { allow: false, scope: 1 } };
|
||||||
|
} else {
|
||||||
|
const scope = typeLower.includes('conversation') ? 2 : 1;
|
||||||
|
interactionPayload = { filePermission: { allow: approved, scope } };
|
||||||
|
}
|
||||||
|
} else if (typeLower.includes('elicitation')) {
|
||||||
|
interactionPayload = { elicitation: {} };
|
||||||
|
} else if (typeLower === 'permission' || typeLower.includes('permission')) {
|
||||||
|
interactionPayload = { runExtensionCode: { confirm: approved } };
|
||||||
|
} else if (typeLower === 'command' || typeLower === '') {
|
||||||
|
// Generic command — most common case from DOM observer
|
||||||
|
interactionPayload = { runCommand: { confirm: approved } };
|
||||||
|
} else {
|
||||||
|
// Default: try run_command
|
||||||
|
interactionPayload = { runCommand: { confirm: approved } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeTrajectoryId = getTrajectoryId();
|
||||||
|
const protoVariants = [
|
||||||
|
// Variant A: camelCase with trajectoryId
|
||||||
|
{
|
||||||
|
cascadeId: sessionId,
|
||||||
|
interaction: {
|
||||||
|
trajectoryId: activeTrajectoryId || sessionId,
|
||||||
|
stepIndex: effectiveStepIndex,
|
||||||
|
...interactionPayload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Variant B: snake_case
|
||||||
|
{
|
||||||
|
cascade_id: sessionId,
|
||||||
|
interaction: {
|
||||||
|
trajectory_id: activeTrajectoryId || sessionId,
|
||||||
|
step_index: effectiveStepIndex,
|
||||||
|
...interactionPayload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Variant C: minimal (no trajectoryId)
|
||||||
|
{
|
||||||
|
cascadeId: sessionId,
|
||||||
|
interaction: {
|
||||||
|
stepIndex: effectiveStepIndex,
|
||||||
|
...interactionPayload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let lastRpcError = '';
|
||||||
|
for (let i = 0; i < protoVariants.length; i++) {
|
||||||
|
try {
|
||||||
|
const payload = protoVariants[i];
|
||||||
|
ctx.logToFile(`[APPROVAL-1-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 250)})`);
|
||||||
|
const rpcResult = await ctx.sdk.ls.rawRPC('HandleCascadeUserInteraction', payload);
|
||||||
|
ctx.logToFile(`[APPROVAL-1-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||||
|
return `RPC-${i}:HandleCascadeUserInteraction(${typeLower},${action})`;
|
||||||
|
} catch (e: any) {
|
||||||
|
lastRpcError = e.message || '';
|
||||||
|
ctx.logToFile(`[APPROVAL-1-${i}] ❌ ${lastRpcError.substring(0, 300)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Auto-recovery: wrong-LS detection ──────────────────────
|
||||||
|
if (ctx.fixLSConnection && lastRpcError.includes('input not registered')) {
|
||||||
|
ctx.logToFile('[APPROVAL] ⚠️ wrong-LS detected ("input not registered"), attempting LS fix...');
|
||||||
|
try {
|
||||||
|
const lsChanged = await ctx.fixLSConnection();
|
||||||
|
if (lsChanged) {
|
||||||
|
ctx.logToFile('[APPROVAL] LS reconnected — retrying first proto variant...');
|
||||||
|
try {
|
||||||
|
const retryPayload = protoVariants[0];
|
||||||
|
const retryResult = await ctx.sdk.ls.rawRPC('HandleCascadeUserInteraction', retryPayload);
|
||||||
|
ctx.logToFile(`[APPROVAL-RETRY] ✅ SUCCESS: ${JSON.stringify(retryResult).substring(0, 200)}`);
|
||||||
|
return `RPC-RETRY:HandleCascadeUserInteraction(${typeLower},${action})`;
|
||||||
|
} catch (retryErr: any) {
|
||||||
|
ctx.logToFile(`[APPROVAL-RETRY] ❌ ${retryErr.message?.substring(0, 200)}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.logToFile('[APPROVAL] LS not changed — already on correct port or fix unavailable');
|
||||||
|
}
|
||||||
|
} catch (fixErr: any) {
|
||||||
|
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 (fallback)
|
||||||
|
// Sets a click trigger that the observer script polls and executes.
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
try {
|
||||||
|
const triggerAction = approved ? 'approve' : 'reject';
|
||||||
|
ctx.logToFile(`[APPROVAL-2] Setting clickTrigger=${triggerAction} for renderer DOM click`);
|
||||||
|
ctx.setClickTrigger(triggerAction as 'approve' | 'reject');
|
||||||
|
ctx.logToFile(`[APPROVAL-2] ✅ clickTrigger set — renderer will poll and click within 2s`);
|
||||||
|
} catch (e: any) {
|
||||||
|
ctx.logToFile(`[APPROVAL-2] ❌ FAIL: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.logToFile(`[APPROVAL] All strategies complete for ${action}`);
|
||||||
|
return `STRATEGIES_DONE:${action}`;
|
||||||
|
}
|
||||||
96
extension/src/brain-watcher.ts
Normal file
96
extension/src/brain-watcher.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import { WSBridgeClient } from './ws-client';
|
||||||
|
|
||||||
|
export interface BrainWatcherContext {
|
||||||
|
logToFile: (msg: string) => void;
|
||||||
|
wsBridge: WSBridgeClient;
|
||||||
|
projectName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BrainWatcher {
|
||||||
|
private brainDir: string;
|
||||||
|
private ctx: BrainWatcherContext;
|
||||||
|
private currentSessionId: string = '';
|
||||||
|
private watcher: fs.FSWatcher | null = null;
|
||||||
|
private lastEventTimes: Map<string, number> = new Map();
|
||||||
|
|
||||||
|
constructor(ctx: BrainWatcherContext) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
// The bridgePath is ~/.gemini/antigravity/bridge, so brain is sibling
|
||||||
|
this.brainDir = path.join(os.homedir(), '.gemini', 'antigravity', 'brain');
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateSession(sessionId: string) {
|
||||||
|
if (!sessionId || this.currentSessionId === sessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.currentSessionId = sessionId;
|
||||||
|
this.startWatching(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private startWatching(sessionId: string) {
|
||||||
|
this.stop();
|
||||||
|
|
||||||
|
const sessionDir = path.join(this.brainDir, sessionId);
|
||||||
|
if (!fs.existsSync(sessionDir)) {
|
||||||
|
// It might not be created yet, poll gently
|
||||||
|
setTimeout(() => this.startWatching(sessionId), 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.watcher = fs.watch(sessionDir, { persistent: false }, (eventType, filename) => {
|
||||||
|
if (!filename || !filename.endsWith('.md')) return;
|
||||||
|
|
||||||
|
// Dedup rapid events
|
||||||
|
const now = Date.now();
|
||||||
|
const last = this.lastEventTimes.get(filename) || 0;
|
||||||
|
if (now - last < 500) return; // 500ms debounce
|
||||||
|
this.lastEventTimes.set(filename, now);
|
||||||
|
|
||||||
|
this.handleFileChange(sessionDir, filename, eventType);
|
||||||
|
});
|
||||||
|
this.ctx.logToFile(`[BRAIN-WATCHER] Started watching session: ${sessionId.substring(0, 8)}`);
|
||||||
|
} catch (e: any) {
|
||||||
|
this.ctx.logToFile(`[BRAIN-WATCHER] Failed to watch ${sessionId}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFileChange(dir: string, filename: string, rawEventType: string) {
|
||||||
|
const filePath = path.join(dir, filename);
|
||||||
|
let content = '';
|
||||||
|
let eventType = 'file_changed';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
} else {
|
||||||
|
eventType = 'file_deleted';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// File might be locked or deleted during read
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.ctx.wsBridge && this.ctx.wsBridge.isConnected()) {
|
||||||
|
this.ctx.wsBridge.sendBrainEvent({
|
||||||
|
event_type: eventType,
|
||||||
|
conversation_id: this.currentSessionId,
|
||||||
|
file_name: filename,
|
||||||
|
content: content,
|
||||||
|
timestamp: Date.now() / 1000,
|
||||||
|
project_name: this.ctx.projectName,
|
||||||
|
});
|
||||||
|
this.ctx.logToFile(`[BRAIN-WATCHER] Sent ${eventType} for ${filename}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop() {
|
||||||
|
if (this.watcher) {
|
||||||
|
this.watcher.close();
|
||||||
|
this.watcher = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user