fix(bridge): v0.3.12 approval state management — sawRunningAfterPending gate + approval-flow.md system doc
- processResponseFile: set sawRunningAfterPending=true instead of removing resets (prevents infinite pending loop AND known-issues L479 auto_resolve regression) - Hoist sawRunningAfterPending to module level for cross-function access - Add recentPendingSteps memory dedup Map (60s TTL) for file-deletion resilience - Create docs/approval-flow.md: complete system flow guide with state diagram - Update known-issues.md: 2 new entries (state reset fix, memory dedup)
This commit is contained in:
@@ -526,3 +526,17 @@
|
||||
- **해결**: 나노단위 전체 Flow 추적으로 교차 검증 후 진짜 결함만 추림 (P2 3건, P3 2건)
|
||||
- **주의**: **코드 감사 시 반드시 producer→transport→consumer→side effects 전체 경로를 추적. 단편적 로컬 분석으로 위험도를 과장하지 말 것**
|
||||
|
||||
### [2026-03-16] processResponseFile 상태 리셋 — 무한 루프 vs auto_resolve 회귀
|
||||
- **증상**: (v0.3.11) Discord 승인 후 같은 step에 대해 pending이 반복 생성 → 무한 auto-approve 루프
|
||||
- **원인**: processResponseFile이 `lastPendingStepIndex = -1` + `stallProbed = false`로 무조건 리셋 → step_probe가 같은 WAITING step을 새 step으로 착각 → pending 재생성 → 루프
|
||||
- **1차 시도 실패**: 리셋 2줄을 완전 제거 → 무한 루프 해소, BUT **L479 회귀** (sawRunningAfterPending이 false 유지 → Discord 승인 후 AG 진행 시 auto_resolve 중복 알림 발생)
|
||||
- **올바른 해결 (v0.3.12)**: `sawRunningAfterPending = true`만 설정. lastPendingStepIndex와 stallProbed는 유지(dedup 보호), sawRunningAfterPending=true로 auto_resolve gate 닫기. 3개 변수 모두 delta>0에서 자연 리셋
|
||||
- **주의**: processResponseFile의 상태 리셋은 **sawRunningAfterPending = true만 설정**. lastPendingStepIndex와 stallProbed를 건드리려면 반드시 `docs/approval-flow.md`의 상태 전이 다이어그램을 확인하고, known-issues L479 회귀 및 무한 루프 시나리오를 검증할 것. 자세한 Flow는 `docs/approval-flow.md` 참조
|
||||
|
||||
### [2026-03-16] recentPendingSteps 메모리 dedup — pending 파일 삭제 후 재생성 방지
|
||||
- **증상**: Bot/Collector가 pending 파일을 삭제 → 파일 기반 dedup 통과 → 같은 step_index로 새 pending 생성
|
||||
- **원인**: writePendingApproval()의 dedup이 파일 존재 여부에만 의존. write_response()가 pending 삭제 → dedup 무효화
|
||||
- **해결**: `recentPendingSteps` Map (TTL 60초) 추가. `${conversationId}:${stepIndex}`를 키로 사용. 파일 삭제 후에도 메모리에서 차단. delta>0에서 해당 세션 항목 클리어
|
||||
- **주의**: DOM observer HTTP `/pending` 경로(L738-812)는 `writePendingApproval()`을 우회하므로 이 메모리 dedup 미적용. "Run" 필터(L757)와 file_permission dedup(L786-800)이 대신 방어
|
||||
|
||||
|
||||
|
||||
185
docs/approval-flow.md
Normal file
185
docs/approval-flow.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Gravity Bridge — 승인 시스템 완전 Flow 가이드
|
||||
|
||||
> **Last Updated**: 2026-03-16 (v0.3.12)
|
||||
> **SSOT**: 이 문서는 승인 시스템의 전체 데이터 플로우와 상태 관리를 설명합니다.
|
||||
> **수정 시**: known-issues.md와 동기화 필수
|
||||
|
||||
---
|
||||
|
||||
## 1. 시스템 아키텍처 개요
|
||||
|
||||
```
|
||||
AG IDE (Antigravity)
|
||||
├── Extension (extension.ts) ← Bridge 핵심
|
||||
│ ├── setupMonitor() ← 5초 폴링 (GetAllCascadeTrajectories)
|
||||
│ ├── step_probe ← WAITING step 감지 (GetCascadeTrajectorySteps)
|
||||
│ ├── DOM Observer ← 렌더러 스크립트 (버튼 감지)
|
||||
│ ├── processResponseFile() ← Discord 응답 처리
|
||||
│ ├── writePendingApproval() ← pending 파일 생성 (dedup 포함)
|
||||
│ └── tryApprovalStrategies() ← RPC 실행
|
||||
├── bridge/ (파일 시스템)
|
||||
│ ├── pending/*.json ← 승인 대기 목록
|
||||
│ ├── response/*.json ← Discord 응답
|
||||
│ ├── snapshot/*.json ← 채팅 릴레이
|
||||
│ └── register/*.json ← 세션-프로젝트 매핑
|
||||
└── Bot (bot.py) ← Discord 통신
|
||||
├── pending_approval_scanner ← 3초 폴링
|
||||
├── auto_approve_scanner ← !auto 토글
|
||||
└── snapshot_scanner ← 채팅 릴레이
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 데이터 플로우: 승인 요청 → 응답
|
||||
|
||||
### 2.1 Pending 생성 경로 (2개)
|
||||
|
||||
#### 경로 A: Step Probe → `writePendingApproval()`
|
||||
```
|
||||
1. setupMonitor() 5초 폴링 → GetAllCascadeTrajectories
|
||||
2. RUNNING + delta=0 + modTime 미변경 → stall 감지
|
||||
3. consecutiveIdleCount >= 1 && !stallProbed
|
||||
4. GetCascadeTrajectorySteps → WAITING step 발견
|
||||
5. si !== lastPendingStepIndex 확인 (dedup)
|
||||
6. writePendingApproval() 호출
|
||||
├── recentPendingSteps 메모리 dedup 체크 (60초 TTL)
|
||||
├── 기존 pending 파일 dedup 체크 (15초 윈도우)
|
||||
└── pending 파일 생성 + recentPendingSteps에 기록
|
||||
7. stallProbed = true, lastPendingStepIndex = si
|
||||
```
|
||||
|
||||
#### 경로 B: DOM Observer → HTTP POST `/pending`
|
||||
```
|
||||
1. 렌더러 MutationObserver → 버튼 감지 (Run, Accept, Allow 등)
|
||||
2. FALSE_POSITIVE_RE 필터 (Proceed, Continue, Deny 등 차단)
|
||||
3. "Run"은 sessionStalled=true && lastPendingStepIndex < 0 일 때만 통과
|
||||
4. HTTP POST /pending → Extension HTTP 핸들러 (L738-812)
|
||||
5. 파일 직접 생성 (writePendingApproval() 우회!)
|
||||
⚠️ recentPendingSteps 메모리 dedup 미적용
|
||||
```
|
||||
|
||||
> **주의**: 경로 B는 `writePendingApproval()`의 메모리 dedup을 우회합니다. 하지만 `lastPendingStepIndex >= 0`일 때 "Run" 필터(L757)와 15초 파일 기반 dedup이 방어합니다.
|
||||
|
||||
### 2.2 Response 처리 경로
|
||||
|
||||
```
|
||||
1. Bot pending_approval_scanner → pending 파일 발견
|
||||
2. auto-approve OR Discord 버튼 → write_response() 호출
|
||||
├── response/*.json 생성
|
||||
└── pending/*.json 삭제 (!)
|
||||
3. Extension response watcher (fs.watch + 3초 폴링 fallback)
|
||||
→ processResponseFile() (300ms 딜레이)
|
||||
4. processResponseFile():
|
||||
├── 파일 존재 확인 (HTTP handler가 먼저 삭제했을 수 있음)
|
||||
├── stale timeout 필터 (2분)
|
||||
├── auto_resolved/expired 상태 skip
|
||||
├── project_name 필터
|
||||
└── tryApprovalStrategies() → RPC 실행
|
||||
5. sawRunningAfterPending = true (v0.3.12 핵심 수정)
|
||||
6. response 파일 삭제 (DOM observer 경로는 HTTP handler에 위임)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 상태 변수 완전 참조
|
||||
|
||||
### 3.1 모듈 레벨 변수 (extension.ts)
|
||||
|
||||
| 변수 | 위치 | 역할 | 설정 | 리셋 |
|
||||
|------|------|------|------|------|
|
||||
| `lastPendingStepIndex` | L707 | 마지막으로 pending을 생성한 step index | step_probe(L2047,2108), error_probe(L2178) | delta>0(L1972), session change(L1841) |
|
||||
| `stallProbed` | L708 | 현재 stall에서 probe 완료 여부 | step_probe(L2046,2107,2177) | delta>0(L1980), modTime changed(L1986), session change(L1842) |
|
||||
| `sawRunningAfterPending` | L709 | pending 후 delta>0 발생 여부 (auto_resolve gate) | delta>0(L1979), **processResponseFile(L2622)** | step_probe(L2049,2110) |
|
||||
| `sessionStalled` | L706 | AG가 stall 상태인지 | idle count≥1(L1993) | delta>0(L1937), not WAITING(L2135) |
|
||||
| `recentPendingSteps` | L54 | 메모리 기반 pending dedup Map | writePendingApproval(L2787,2837) | delta>0(L1974), TTL 60초 |
|
||||
|
||||
### 3.2 setupMonitor() 로컬 변수
|
||||
|
||||
| 변수 | 역할 |
|
||||
|------|------|
|
||||
| `consecutiveIdleCount` | 연속 idle poll 수 (stall 감지 debounce) |
|
||||
| `lastPendingTime` | 마지막 pending 생성 시간 |
|
||||
| `lastModTime` | 마지막 modifiedTime (thinking vs approval 구분) |
|
||||
| `wasRunning` | RUNNING→IDLE 전이 추적 |
|
||||
| `lastResponseCaptureStep` | 응답 캡처 dedup |
|
||||
|
||||
---
|
||||
|
||||
## 4. 핵심 상태 전이 다이어그램
|
||||
|
||||
```
|
||||
[IDLE] ──step진행(delta>0)──→ [RUNNING]
|
||||
│
|
||||
delta=0 + modTime 변동 → [THINKING] (stall 카운터 리셋)
|
||||
delta=0 + modTime 고정 → [STALLED]
|
||||
│
|
||||
!stallProbed → step_probe 실행
|
||||
│
|
||||
WAITING 발견 → [PENDING_CREATED]
|
||||
(stallProbed=true, lastPendingStepIndex=si)
|
||||
│
|
||||
┌──────────────────────────────────┤
|
||||
▼ ▼
|
||||
[DISCORD_APPROVED] [AG_LOCAL_APPROVED]
|
||||
processResponseFile() delta > 0 + !sawRunningAfterPending
|
||||
sawRunningAfterPending=true → auto_resolve → Discord 알림
|
||||
│ │
|
||||
└──────────────────────────────────┘
|
||||
│
|
||||
[STEP_PROGRESSED]
|
||||
delta > 0 → 전체 리셋
|
||||
lastPendingStepIndex = -1
|
||||
stallProbed = false
|
||||
sawRunningAfterPending = true
|
||||
recentPendingSteps 클리어
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. v0.3.12 수정 — 왜 `sawRunningAfterPending = true`인가
|
||||
|
||||
### 5.1 이전 문제: 무한 루프 (v0.3.11 이전)
|
||||
|
||||
processResponseFile이 `lastPendingStepIndex = -1`로 리셋 → step_probe가 같은 WAITING step 재감지 → 새 pending → auto-approve → response → 다시 리셋 → **무한 루프**
|
||||
|
||||
### 5.2 v0.3.11 시도: 모든 리셋 제거
|
||||
|
||||
`lastPendingStepIndex`와 `stallProbed` 리셋을 완전 제거 → **무한 루프 해소**, 하지만:
|
||||
- known-issues L479 회귀: Discord 승인 후 AG 진행 시 `sawRunningAfterPending=false` + `lastPendingStepIndex>=0` → auto_resolve 중복 알림
|
||||
- `stallProbed` 영구 잠금 우려 (실제로는 delta>0에서 자연 리셋)
|
||||
|
||||
### 5.3 v0.3.12 해결: `sawRunningAfterPending = true`
|
||||
|
||||
Discord 승인 response 처리 후 `sawRunningAfterPending = true`만 설정:
|
||||
1. ✅ 무한 루프 방지: `lastPendingStepIndex` 유지 → dedup 작동
|
||||
2. ✅ auto_resolve 중복 방지: `sawRunningAfterPending = true` → L1939 조건 FALSE
|
||||
3. ✅ stallProbed 자연 리셋: delta>0에서 L1980
|
||||
4. ✅ 신호 수집 무영향: step_probe, GetAllCascadeTrajectories 코드 미변경
|
||||
|
||||
---
|
||||
|
||||
## 6. 위험 지점 목록 (수정 시 반드시 확인)
|
||||
|
||||
| 코드 위치 | 위험 | 확인 사항 |
|
||||
|-----------|------|----------|
|
||||
| processResponseFile 리셋 (L2607+) | 무한 루프 or auto_resolve 중복 | `sawRunningAfterPending = true`만 설정. `lastPendingStepIndex`와 `stallProbed`는 건드리지 말 것 |
|
||||
| HTTP POST /pending (L738-812) | DOM observer 경로가 writePendingApproval() 우회 | "Run" 필터(L757)와 파일 기반 dedup이 방어 |
|
||||
| bridge.py write_response (L460-461) | pending 파일 삭제 | 메모리 dedup(recentPendingSteps)이 재생성 방지 |
|
||||
| auto_resolve (L1939-1977) | 중복 알림 | `sawRunningAfterPending` gate 확인 |
|
||||
| step_probe offset (L2025-2070) | 775-step 리밋 | stepOffset으로 최신 step 조회 |
|
||||
| session change (L1832-1854) | 모든 상태 초기화 | lastPendingStepIndex, stallProbed, sawRunningAfterPending 모두 리셋 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 과거 이슈 교차 참조
|
||||
|
||||
| 이슈 (known-issues.md) | 방어 코드 | 상태 |
|
||||
|------------------------|----------|------|
|
||||
| L252: 중복 승인 요청 | writePendingApproval dedup (15초 윈도우 + 메모리 dedup) | ✅ 해결 |
|
||||
| L264: pending 무한 누적 | write_response()에서 삭제 + 5분 age filter | ✅ 해결 |
|
||||
| L288: DOM observer ENOENT | isDomObserver 분기 삭제 (L2619) | ✅ 해결 |
|
||||
| L384: 크로스 프로젝트 MERGE | project_name 가드 (L2774) | ✅ 해결 |
|
||||
| L444: DEDUP 크로스 세션 | conversation_id 가드 (L2794) | ✅ 해결 |
|
||||
| L474-479: auto_resolve 중복 | `sawRunningAfterPending = true` (v0.3.12) | ✅ 해결 |
|
||||
| L493: Double-Fire auto-approve | Extension auto-approve 경로 제거, Bot 단일 경로 | ✅ 해결 |
|
||||
| L499: Deny false positive | FALSE_POSITIVE_RE + Bot reject guard | ✅ 해결 |
|
||||
5
docs/devlog/2026-03-16.md
Normal file
5
docs/devlog/2026-03-16.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 2026-03-16 Devlog
|
||||
|
||||
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||
|---|------|------|------|------|
|
||||
| 001 | 07:30~11:10 | 승인 상태 관리 근본 원인 분석 + v0.3.12 수정 (sawRunningAfterPending gate) + approval-flow.md 시스템 Flow 문서 + known-issues 2건 추가 | `pending` | ✅ |
|
||||
@@ -82,6 +82,11 @@ let deterministicPort = 0; // derived from projectName, consistent across restar
|
||||
let watcher = null;
|
||||
let commandsWatcher = null;
|
||||
const sentPendingIds = new Set();
|
||||
// Memory-based dedup: tracks recently created pending step_indexes to prevent
|
||||
// regeneration after pending file deletion (by Collector/Bot response cycle).
|
||||
// Map<string, number> = `${conversationId}:${stepIndex}` → creation timestamp
|
||||
const recentPendingSteps = new Map();
|
||||
const PENDING_MEMORY_TTL_MS = 60_000; // 60 seconds memory retention
|
||||
// ─── Project Detection ───
|
||||
function detectProjectName() {
|
||||
const config = vscode.workspace.getConfiguration('gravityBridge');
|
||||
@@ -706,6 +711,7 @@ let clickTrigger = null;
|
||||
let sessionStalled = false; // true when session is stalled waiting for approval
|
||||
let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step (module-level for tryApprovalStrategies)
|
||||
let stallProbed = false; // prevent repeated step probes during same stall (module-level for processResponseFile reset)
|
||||
let sawRunningAfterPending = true; // gate: must see delta>0 before next pending (module-level for processResponseFile)
|
||||
// Deep inspect trigger: curl sets this, renderer picks it up and POSTs results back
|
||||
let deepInspectRequested = false;
|
||||
let deepInspectResult = null;
|
||||
@@ -1733,7 +1739,7 @@ function setupMonitor() {
|
||||
// lastPendingStepIndex is module-level (above sessionStalled)
|
||||
let consecutiveIdleCount = 0; // debounce: require N consecutive stall polls
|
||||
let lastPendingTime = 0; // cooldown: minimum gap between pendings
|
||||
let sawRunningAfterPending = true; // gate: must see delta>0 before next pending
|
||||
// sawRunningAfterPending is module-level (used by processResponseFile to close auto_resolve gate)
|
||||
let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval
|
||||
// stallProbed is module-level (used by processResponseFile to reset after approval)
|
||||
let lastRelayedTaskText = ''; // dedup TASK_BOUNDARY relay
|
||||
@@ -1969,6 +1975,11 @@ function setupMonitor() {
|
||||
writeChatSnapshot(`✅ **AG에서 직접 진행됨** (step ${lastPendingStepIndex})\n\n\`${primaryCommand.substring(0, 200)}\``);
|
||||
}
|
||||
lastPendingStepIndex = -1;
|
||||
// Clear memory dedup for this session (step progressed, new WAITING steps are allowed)
|
||||
for (const k of recentPendingSteps.keys()) {
|
||||
if (k.startsWith(activeSessionId + ':'))
|
||||
recentPendingSteps.delete(k);
|
||||
}
|
||||
}
|
||||
consecutiveIdleCount = 0;
|
||||
sawRunningAfterPending = true;
|
||||
@@ -2628,9 +2639,21 @@ async function processResponseFile(filePath) {
|
||||
logToFile(`[RESPONSE] strategy result: ${strategyResult}`);
|
||||
}
|
||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
||||
// Reset stall probe gate so next WAITING step is detected immediately
|
||||
stallProbed = false;
|
||||
lastPendingStepIndex = -1;
|
||||
// FIX v2 (2026-03-16): Correct state management after response processing.
|
||||
//
|
||||
// HISTORY: processResponseFile originally reset lastPendingStepIndex=-1 and stallProbed=false.
|
||||
// v0.3.11 removed both resets entirely → caused infinite pending loop (step_probe re-detects
|
||||
// same WAITING step because lastPendingStepIndex=-1 makes si!=lastPendingStepIndex true).
|
||||
// v0.3.12 attempt removed resets entirely → conflicts with known-issues.md L479
|
||||
// (auto_resolve duplicate notification on delta>0 because sawRunningAfterPending is false).
|
||||
//
|
||||
// CORRECT FIX: Set sawRunningAfterPending=true to close the auto_resolve gate.
|
||||
// - lastPendingStepIndex: KEEP (prevents step_probe from re-creating pending for same step)
|
||||
// - stallProbed: KEEP (prevents re-probe during same stall)
|
||||
// - sawRunningAfterPending = true: CRITICAL — tells auto_resolve "Discord handled this,
|
||||
// don't generate duplicate 직접 진행됨 notification" (prevents known-issues L479 regression)
|
||||
// All three reset naturally at delta > 0 (L1972-1980) when step actually progresses.
|
||||
sawRunningAfterPending = true;
|
||||
// Cleanup response file
|
||||
// CRITICAL: DOM observer responses must NOT be deleted here!
|
||||
// The renderer polls GET /response/:rid to discover the approval.
|
||||
@@ -2780,6 +2803,22 @@ function writePendingApproval(data) {
|
||||
// ── Dedup: if DOM observer already created a "Run"-only pending, MERGE detailed info into it ──
|
||||
const nowMs = Date.now();
|
||||
const DEDUP_WINDOW_MS = 15_000; // 15 second dedup window
|
||||
// ── FIX: Memory-based dedup (survives pending file deletion by Collector/Bot) ──
|
||||
// Pending files are deleted when Bot writes a response (bridge.py L461, collector.py L259).
|
||||
// File-based dedup alone fails after deletion → same step_index creates new pending → loop.
|
||||
if (data.step_index !== undefined && data.conversation_id) {
|
||||
const memKey = `${data.conversation_id}:${data.step_index}`;
|
||||
const prevTs = recentPendingSteps.get(memKey);
|
||||
if (prevTs && (nowMs - prevTs) < PENDING_MEMORY_TTL_MS) {
|
||||
logToFile(`[DEDUP-MEM] skip: step_index ${data.step_index} already created ${Math.round((nowMs - prevTs) / 1000)}s ago`);
|
||||
return;
|
||||
}
|
||||
// Cleanup stale entries (keep map small)
|
||||
for (const [k, ts] of recentPendingSteps) {
|
||||
if (nowMs - ts > PENDING_MEMORY_TTL_MS)
|
||||
recentPendingSteps.delete(k);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const existingFiles = fs.readdirSync(pendingDir).filter((f) => f.endsWith('.json'));
|
||||
for (const ef of existingFiles) {
|
||||
@@ -2799,6 +2838,10 @@ function writePendingApproval(data) {
|
||||
existing.source = 'dom_observer+step_probe'; // mark as merged
|
||||
fs.writeFileSync(efPath, JSON.stringify(existing, null, 2), 'utf-8');
|
||||
logToFile(`[DEDUP] MERGED step_probe info into DOM pending: ${ef} cmd="${data.command.substring(0, 60)}"`);
|
||||
// Record in memory dedup
|
||||
if (data.step_index !== undefined && data.conversation_id) {
|
||||
recentPendingSteps.set(`${data.conversation_id}:${data.step_index}`, nowMs);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -2845,6 +2888,10 @@ function writePendingApproval(data) {
|
||||
};
|
||||
fs.writeFileSync(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8');
|
||||
console.log(`Gravity Bridge: pending approval written → ${id}.json`);
|
||||
// Record in memory dedup cache (survives file deletion by Collector/Bot)
|
||||
if (data.step_index !== undefined && data.conversation_id) {
|
||||
recentPendingSteps.set(`${data.conversation_id}:${data.step_index}`, nowMs);
|
||||
}
|
||||
// Register session → project mapping (correct because projectName is per-window)
|
||||
if (data.conversation_id) {
|
||||
writeRegistration(data.conversation_id);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
"name": "gravity-bridge",
|
||||
"displayName": "Gravity Bridge",
|
||||
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
||||
"version": "0.3.11",
|
||||
"version": "0.3.12",
|
||||
"publisher": "variet",
|
||||
"engines": {
|
||||
"vscode": "^1.100.0"
|
||||
|
||||
@@ -48,6 +48,11 @@ let watcher: fs.FSWatcher | null = null;
|
||||
let commandsWatcher: fs.FSWatcher | null = null;
|
||||
|
||||
const sentPendingIds = new Set<string>();
|
||||
// Memory-based dedup: tracks recently created pending step_indexes to prevent
|
||||
// regeneration after pending file deletion (by Collector/Bot response cycle).
|
||||
// Map<string, number> = `${conversationId}:${stepIndex}` → creation timestamp
|
||||
const recentPendingSteps = new Map<string, number>();
|
||||
const PENDING_MEMORY_TTL_MS = 60_000; // 60 seconds memory retention
|
||||
|
||||
// ─── Project Detection ───
|
||||
|
||||
@@ -701,6 +706,7 @@ let clickTrigger: { action: 'approve' | 'reject'; timestamp: number } | null = n
|
||||
let sessionStalled = false; // true when session is stalled waiting for approval
|
||||
let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step (module-level for tryApprovalStrategies)
|
||||
let stallProbed = false; // prevent repeated step probes during same stall (module-level for processResponseFile reset)
|
||||
let sawRunningAfterPending = true; // gate: must see delta>0 before next pending (module-level for processResponseFile)
|
||||
|
||||
// Deep inspect trigger: curl sets this, renderer picks it up and POSTs results back
|
||||
let deepInspectRequested = false;
|
||||
@@ -1726,7 +1732,7 @@ function setupMonitor() {
|
||||
// lastPendingStepIndex is module-level (above sessionStalled)
|
||||
let consecutiveIdleCount = 0; // debounce: require N consecutive stall polls
|
||||
let lastPendingTime = 0; // cooldown: minimum gap between pendings
|
||||
let sawRunningAfterPending = true; // gate: must see delta>0 before next pending
|
||||
// sawRunningAfterPending is module-level (used by processResponseFile to close auto_resolve gate)
|
||||
let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval
|
||||
// stallProbed is module-level (used by processResponseFile to reset after approval)
|
||||
let lastRelayedTaskText = ''; // dedup TASK_BOUNDARY relay
|
||||
@@ -1965,6 +1971,10 @@ function setupMonitor() {
|
||||
writeChatSnapshot(`✅ **AG에서 직접 진행됨** (step ${lastPendingStepIndex})\n\n\`${primaryCommand.substring(0, 200)}\``);
|
||||
}
|
||||
lastPendingStepIndex = -1;
|
||||
// Clear memory dedup for this session (step progressed, new WAITING steps are allowed)
|
||||
for (const k of recentPendingSteps.keys()) {
|
||||
if (k.startsWith(activeSessionId + ':')) recentPendingSteps.delete(k);
|
||||
}
|
||||
}
|
||||
consecutiveIdleCount = 0;
|
||||
sawRunningAfterPending = true;
|
||||
@@ -2595,9 +2605,21 @@ async function processResponseFile(filePath: string) {
|
||||
|
||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
||||
|
||||
// Reset stall probe gate so next WAITING step is detected immediately
|
||||
stallProbed = false;
|
||||
lastPendingStepIndex = -1;
|
||||
// FIX v2 (2026-03-16): Correct state management after response processing.
|
||||
//
|
||||
// HISTORY: processResponseFile originally reset lastPendingStepIndex=-1 and stallProbed=false.
|
||||
// v0.3.11 removed both resets entirely → caused infinite pending loop (step_probe re-detects
|
||||
// same WAITING step because lastPendingStepIndex=-1 makes si!=lastPendingStepIndex true).
|
||||
// v0.3.12 attempt removed resets entirely → conflicts with known-issues.md L479
|
||||
// (auto_resolve duplicate notification on delta>0 because sawRunningAfterPending is false).
|
||||
//
|
||||
// CORRECT FIX: Set sawRunningAfterPending=true to close the auto_resolve gate.
|
||||
// - lastPendingStepIndex: KEEP (prevents step_probe from re-creating pending for same step)
|
||||
// - stallProbed: KEEP (prevents re-probe during same stall)
|
||||
// - sawRunningAfterPending = true: CRITICAL — tells auto_resolve "Discord handled this,
|
||||
// don't generate duplicate 직접 진행됨 notification" (prevents known-issues L479 regression)
|
||||
// All three reset naturally at delta > 0 (L1972-1980) when step actually progresses.
|
||||
sawRunningAfterPending = true;
|
||||
|
||||
// Cleanup response file
|
||||
// CRITICAL: DOM observer responses must NOT be deleted here!
|
||||
@@ -2736,6 +2758,23 @@ function writePendingApproval(data: { conversation_id: string; command: string;
|
||||
// ── Dedup: if DOM observer already created a "Run"-only pending, MERGE detailed info into it ──
|
||||
const nowMs = Date.now();
|
||||
const DEDUP_WINDOW_MS = 15_000; // 15 second dedup window
|
||||
|
||||
// ── FIX: Memory-based dedup (survives pending file deletion by Collector/Bot) ──
|
||||
// Pending files are deleted when Bot writes a response (bridge.py L461, collector.py L259).
|
||||
// File-based dedup alone fails after deletion → same step_index creates new pending → loop.
|
||||
if (data.step_index !== undefined && data.conversation_id) {
|
||||
const memKey = `${data.conversation_id}:${data.step_index}`;
|
||||
const prevTs = recentPendingSteps.get(memKey);
|
||||
if (prevTs && (nowMs - prevTs) < PENDING_MEMORY_TTL_MS) {
|
||||
logToFile(`[DEDUP-MEM] skip: step_index ${data.step_index} already created ${Math.round((nowMs - prevTs) / 1000)}s ago`);
|
||||
return;
|
||||
}
|
||||
// Cleanup stale entries (keep map small)
|
||||
for (const [k, ts] of recentPendingSteps) {
|
||||
if (nowMs - ts > PENDING_MEMORY_TTL_MS) recentPendingSteps.delete(k);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const existingFiles = fs.readdirSync(pendingDir).filter((f: string) => f.endsWith('.json'));
|
||||
for (const ef of existingFiles) {
|
||||
@@ -2753,6 +2792,10 @@ function writePendingApproval(data: { conversation_id: string; command: string;
|
||||
existing.source = 'dom_observer+step_probe'; // mark as merged
|
||||
fs.writeFileSync(efPath, JSON.stringify(existing, null, 2), 'utf-8');
|
||||
logToFile(`[DEDUP] MERGED step_probe info into DOM pending: ${ef} cmd="${data.command.substring(0, 60)}"`);
|
||||
// Record in memory dedup
|
||||
if (data.step_index !== undefined && data.conversation_id) {
|
||||
recentPendingSteps.set(`${data.conversation_id}:${data.step_index}`, nowMs);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -2799,6 +2842,10 @@ function writePendingApproval(data: { conversation_id: string; command: string;
|
||||
};
|
||||
fs.writeFileSync(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8');
|
||||
console.log(`Gravity Bridge: pending approval written → ${id}.json`);
|
||||
// Record in memory dedup cache (survives file deletion by Collector/Bot)
|
||||
if (data.step_index !== undefined && data.conversation_id) {
|
||||
recentPendingSteps.set(`${data.conversation_id}:${data.step_index}`, nowMs);
|
||||
}
|
||||
// Register session → project mapping (correct because projectName is per-window)
|
||||
if (data.conversation_id) { writeRegistration(data.conversation_id); }
|
||||
} catch (e: any) {
|
||||
|
||||
Reference in New Issue
Block a user