fix(ext+hub): v0.5.2 Idle→Resume 신호 소실 3중 버그 수정 — auth_fail 재연결 + pending_owners 보존 + step-probe 리셋
This commit is contained in:
@@ -53,6 +53,12 @@
|
||||
- **해결** (v0.5.1): `approval-handler.ts` L384에 `browser_subagent` 추가, `step-probe.ts` L481/L549에 `browser_subagent`/`open_browser_url` step_type 분류 추가 (`549af6d`)
|
||||
- **주의**: 새로운 AG 도구 추가 시 반드시 (1) step-probe step_type 매핑 (2) approval-handler RPC payload 매핑 양쪽 모두 업데이트
|
||||
|
||||
### [2026-03-21] Idle→Resume 신호 소실 — 3중 버그
|
||||
- **증상**: AG 장시간 idle 후 작업 재개 시 Discord 승인 신호가 전달되지 않음
|
||||
- **원인**: (1) `ws-client.ts` `auth_fail` 시 `shouldReconnect=false` — JWT 24h 만료 시 WS 영구 종료. (2) `hub.py` `_disconnect`에서 유일 연결 시 `pending_owners` 삭제 — 재연결 후 Discord 버튼 무효. (3) `step-probe.ts` `stallProbed=true` + `lastPendingStepIndex=N`이 WS 재연결 시 리셋 안 됨 — WAITING step 재전송 영구 차단
|
||||
- **해결** (v0.5.2): (1) `auth_fail` → `registrationCode` 재시도. (2) `pending_owners` orphan 마커로 보존+재할당. (3) `resetPendingStateForReconnect()` + `onConnected`에서 호출
|
||||
- **주의**: WS `onConnected`에서 반드시 step-probe 상태 리셋 필수. `stallProbed`/`lastPendingStepIndex`는 TTL 없는 영구 값
|
||||
|
||||
---
|
||||
|
||||
> [!NOTE]
|
||||
@@ -79,3 +85,4 @@
|
||||
| 10 | **diff_review는 VS Code 커맨드만 유효** — RPC 3개 전략 모두 실패 확정 | diff_review RPC dead-end |
|
||||
| 11 | **HttpBridgeContext에 프리미티브 by-value 복사 금지** — 별도 객체 생성 시 getter 사용 | HttpBridgeContext stale primitive |
|
||||
| 12 | **새 AG 도구 추가 시 step-probe step_type 매핑 + approval-handler RPC payload 매핑 양쪽 필수** | browser_subagent Allow |
|
||||
| 13 | **WS `onConnected`에서 step-probe 상태 리셋 필수** — `stallProbed`/`lastPendingStepIndex`는 TTL 없는 영구 값 | Idle→Resume 신호 소실 |
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
| # | 시간 | 작업 | 커밋 | 상태 |
|
||||
|---|------|------|------|------|
|
||||
| 1 | 07:30 | v0.5.0 이후 이슈 조사 — 외부 네트워크 접근(서버 정상 확인), browser_subagent Allow RPC 매핑 수정, .env 정리 | `549af6d` | ✅ |
|
||||
| 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 리셋 | — | 🔧 |
|
||||
|
||||
@@ -443,6 +443,8 @@ async function activate(context) {
|
||||
logToFile(`[WS] Connected: ${connId} instance=#${instanceNum}`);
|
||||
statusBar.text = '$(check) Bridge WS';
|
||||
statusBar.tooltip = `Gravity Bridge: ${projectName} (WS #${instanceNum})`;
|
||||
// Reset step-probe state so WAITING steps are re-detected after reconnect
|
||||
(0, step_probe_1.resetPendingStateForReconnect)();
|
||||
},
|
||||
onDisconnected: () => {
|
||||
logToFile('[WS] Disconnected — using file fallback');
|
||||
|
||||
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.5.1",
|
||||
"version": "0.5.2",
|
||||
"publisher": "variet",
|
||||
"engines": {
|
||||
"vscode": "^1.100.0"
|
||||
|
||||
@@ -16,7 +16,7 @@ import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as cp from 'child_process';
|
||||
import { WSBridgeClient, WSResponseData, WSCommandData } from './ws-client';
|
||||
import { initStepProbe, BridgeContext, writePendingApproval, tryApprovalStrategies, writeRegistration, getApprovalContext, resetPendingState, handleDiffReviewResponse, getActiveSessionId as getStepProbeSessionId, getStepProbeContext } from './step-probe';
|
||||
import { initStepProbe, BridgeContext, writePendingApproval, tryApprovalStrategies, writeRegistration, getApprovalContext, resetPendingState, resetPendingStateForReconnect, handleDiffReviewResponse, getActiveSessionId as getStepProbeSessionId, getStepProbeContext } from './step-probe';
|
||||
import { startHttpBridge, getDeterministicPort, HttpBridgeContext } from './http-bridge';
|
||||
import { setupApprovalObserver } from './html-patcher';
|
||||
import { watchCommandsDir, handleWSCommand, disposeCommandsWatcher, CommandHandlerContext } from './command-handler';
|
||||
@@ -433,6 +433,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
logToFile(`[WS] Connected: ${connId} instance=#${instanceNum}`);
|
||||
statusBar.text = '$(check) Bridge WS';
|
||||
statusBar.tooltip = `Gravity Bridge: ${projectName} (WS #${instanceNum})`;
|
||||
// Reset step-probe state so WAITING steps are re-detected after reconnect
|
||||
resetPendingStateForReconnect();
|
||||
},
|
||||
onDisconnected: () => {
|
||||
logToFile('[WS] Disconnected — using file fallback');
|
||||
|
||||
@@ -85,6 +85,19 @@ export function resetPendingState(): void {
|
||||
ctx.sawRunningAfterPending = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset step-probe state after WS reconnection.
|
||||
* Without this, stallProbed=true + lastPendingStepIndex=N permanently block
|
||||
* re-detection of WAITING steps whose pending was lost during disconnect.
|
||||
*/
|
||||
export function resetPendingStateForReconnect(): void {
|
||||
ctx.lastPendingStepIndex = -1;
|
||||
ctx.stallProbed = false;
|
||||
ctx.sawRunningAfterPending = false;
|
||||
recentPendingSteps.clear();
|
||||
ctx.logToFile('[STEP-PROBE] Reset pending state for WS reconnect');
|
||||
}
|
||||
|
||||
// handleDiffReviewResponse → moved to ./approval-handler.ts
|
||||
|
||||
/**
|
||||
|
||||
@@ -359,14 +359,21 @@ export class WSBridgeClient {
|
||||
case 'auth_fail': {
|
||||
const reason = (msg as any).reason || 'Unknown';
|
||||
this.logFn(`[WS] Auth failed: ${reason}`);
|
||||
// Clear session token if it was rejected
|
||||
// Clear session token if it was rejected (e.g. expired after 24h)
|
||||
this.sessionToken = '';
|
||||
if (this.registrationCode) {
|
||||
// Token expired → retry with registration code on next reconnect
|
||||
this.logFn('[WS] Retrying with registration code...');
|
||||
this._cleanup();
|
||||
this._scheduleReconnect();
|
||||
} else {
|
||||
// No registration code available → permanent failure
|
||||
// MUST set shouldReconnect=false BEFORE _cleanup(), because _cleanup()
|
||||
// closes the WS → triggers close event → _onDisconnect() → _scheduleReconnect().
|
||||
// Without this, auth failures cause infinite reconnect loops.
|
||||
this.shouldReconnect = false;
|
||||
this._cleanup();
|
||||
this.handlers.onError?.(`Auth failed: ${reason}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
20
hub.py
20
hub.py
@@ -269,11 +269,17 @@ class WSHub:
|
||||
self.project_connections[conn.project] = set()
|
||||
self.project_connections[conn.project].add(conn.conn_id)
|
||||
|
||||
# FIX: Reassign orphaned pending_owners (from dead conn_ids) to this new connection.
|
||||
# When Extension reconnects, old conn_id entries become stale.
|
||||
# FIX: Reassign orphaned pending_owners (from dead conn_ids or orphan markers)
|
||||
# to this new connection. When Extension reconnects, old entries become stale.
|
||||
reassigned = 0
|
||||
for rid, cid in list(self.pending_owners.items()):
|
||||
if cid not in self.connections:
|
||||
if cid.startswith("orphan:"):
|
||||
# Only reassign orphans from the same project
|
||||
if cid != f"orphan:{conn.project}":
|
||||
continue
|
||||
self.pending_owners[rid] = conn.conn_id
|
||||
reassigned += 1
|
||||
elif cid not in self.connections:
|
||||
self.pending_owners[rid] = conn.conn_id
|
||||
reassigned += 1
|
||||
if reassigned:
|
||||
@@ -324,7 +330,13 @@ class WSHub:
|
||||
f"(disconnected {conn_id})"
|
||||
)
|
||||
else:
|
||||
del self.pending_owners[rid]
|
||||
# Preserve as orphan marker instead of deleting —
|
||||
# will be reassigned when Extension reconnects
|
||||
self.pending_owners[rid] = f"orphan:{project}"
|
||||
logger.info(
|
||||
f"[HUB] Orphaned pending {rid[:12]} "
|
||||
f"(no active connections in {project})"
|
||||
)
|
||||
|
||||
# Close WebSocket if still open
|
||||
if not conn.ws.closed:
|
||||
|
||||
Reference in New Issue
Block a user