fix(extension): v0.3.7 — file_permission 3-button 주입 + active_project.lock 제거
- writePendingApproval()에서 step_type=file_permission일 때 자동 3-button 주입 - active_project.lock 메커니즘 제거 (멀티 프로젝트 동시 사용 지원) - step_probe auto-resolve에 project_name 필터 추가 - known-issues 2건 추가
This commit is contained in:
@@ -308,3 +308,16 @@
|
|||||||
- **원인**: 봇이 file_permission pending에 대해 `runCommand.confirm` RPC를 전송 (file_permission이 아닌 잘못된 interaction type). 또는 오래된 pending에 대해 봇이 자동 처리
|
- **원인**: 봇이 file_permission pending에 대해 `runCommand.confirm` RPC를 전송 (file_permission이 아닌 잘못된 interaction type). 또는 오래된 pending에 대해 봇이 자동 처리
|
||||||
- **해결**: (1) pending의 `step_type`이 `file_permission`일 때 extension이 `filePermission` RPC 사용 (이미 구현됨), (2) DOM Observer의 file_permission 감지 → `buttons` 배열 + `step_type: 'file_permission'` 명시
|
- **해결**: (1) pending의 `step_type`이 `file_permission`일 때 extension이 `filePermission` RPC 사용 (이미 구현됨), (2) DOM Observer의 file_permission 감지 → `buttons` 배열 + `step_type: 'file_permission'` 명시
|
||||||
- **주의**: 봇이 오래된 pending을 먼저 처리하면 타이밍 불일치 발생 가능. file_permission과 run_command가 동시에 대기 시 올바른 RPC 라우팅 필수
|
- **주의**: 봇이 오래된 pending을 먼저 처리하면 타이밍 불일치 발생 가능. file_permission과 run_command가 동시에 대기 시 올바른 RPC 라우팅 필수
|
||||||
|
|
||||||
|
### [2026-03-10] active_project.lock — 멀티 프로젝트 동시 사용 차단
|
||||||
|
- **증상**: 동일 PC에서 여러 AG 프로젝트 실행 시 첫 번째 프로젝트만 bridge 연결, 나머지는 경고 후 비활성화 시도
|
||||||
|
- **원인**: `active_project.lock` 파일이 단일 프로젝트만 bridge 사용을 허용하는 설계. 그런데 `bridgeDisabled` 플래그 설정 후 실제로 아무것도 차단하지 않아 → 모든 인스턴스가 같은 pending/response/commands 디렉토리를 동시에 읽고 씀
|
||||||
|
- **해결**: (1) `active_project.lock` 메커니즘 완전 제거, (2) step_probe auto-resolve scan에 `project_name` 필터 추가, (3) response watcher의 기존 project_name 필터 유지
|
||||||
|
- **주의**: bridge 격리는 `project_name` 필드 기반 filtering으로 충분. 파일시스템 레벨 분리(옵션 B)는 불필요하게 복잡
|
||||||
|
|
||||||
|
### [2026-03-10] step_probe file_permission — 3-button 미주입
|
||||||
|
- **증상**: AG에서 파일 접근 권한 요청 시 Discord에 3개 선택지(Allow Once/Allow This Conversation/Deny) 대신 2개(승인/거부)만 표시
|
||||||
|
- **원인**: `writePendingApproval()`이 `step_type: 'file_permission'`을 설정하지만 `buttons` 배열을 주입하지 않음. DOM observer의 `/pending` 핸들러는 `cmdLower.includes('allow')` 조건에서만 buttons 주입 → step_probe의 `view_file: /tmp/...` 형태 command에서는 trigger 안 됨
|
||||||
|
- **해결**: `writePendingApproval()`에서 `step_type === 'file_permission'` && `!buttons`일 때 자동으로 3-button 배열 주입
|
||||||
|
- **주의**: DOM observer 경로는 기존 command 텍스트 기반 감지 유지. step_probe 경로만 수정
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,4 @@
|
|||||||
| 007 | 15:00~15:55 | step_type 패스스루 체인 수정 + file_permission 자동감지 | `7982263`~`d1586c5` | ✅ |
|
| 007 | 15:00~15:55 | step_type 패스스루 체인 수정 + file_permission 자동감지 | `7982263`~`d1586c5` | ✅ |
|
||||||
| 008 | 16:45~17:20 | Single active project lock + stale REJECT 필터 + Vikunja 태스크 정리 | `186875a`~`95d4f85` | ✅ |
|
| 008 | 16:45~17:20 | Single active project lock + stale REJECT 필터 + Vikunja 태스크 정리 | `186875a`~`95d4f85` | ✅ |
|
||||||
| 009 | 17:20~17:47 | v0.3.6 릴리스 — VSIX 빌드 + start_bot.bat 런처 | `bd46bea` | ✅ |
|
| 009 | 17:20~17:47 | v0.3.6 릴리스 — VSIX 빌드 + start_bot.bat 런처 | `bd46bea` | ✅ |
|
||||||
|
| 010 | 18:00~18:30 | v0.3.7 — file_permission 3-button 주입 + active_project.lock 제거 (멀티프로젝트) | `27deb2a` | ✅ |
|
||||||
|
|||||||
@@ -1617,6 +1617,9 @@ function setupMonitor() {
|
|||||||
const pd = JSON.parse(fs.readFileSync(pfPath, 'utf-8'));
|
const pd = JSON.parse(fs.readFileSync(pfPath, 'utf-8'));
|
||||||
if (pd.status !== 'pending')
|
if (pd.status !== 'pending')
|
||||||
continue;
|
continue;
|
||||||
|
// Skip other projects' pendings
|
||||||
|
if (pd.project_name && pd.project_name !== projectName)
|
||||||
|
continue;
|
||||||
// Match by step_index OR by recency (< 60s, any source)
|
// Match by step_index OR by recency (< 60s, any source)
|
||||||
const ageMs = nowMs - (pd.timestamp * 1000);
|
const ageMs = nowMs - (pd.timestamp * 1000);
|
||||||
const isMatch = pd.step_index === lastPendingStepIndex
|
const isMatch = pd.step_index === lastPendingStepIndex
|
||||||
@@ -2388,6 +2391,17 @@ function writePendingApproval(data) {
|
|||||||
logToFile(`[DEDUP] check error (non-fatal): ${dedupErr.message}`);
|
logToFile(`[DEDUP] check error (non-fatal): ${dedupErr.message}`);
|
||||||
}
|
}
|
||||||
const id = nowMs.toString();
|
const id = nowMs.toString();
|
||||||
|
// Auto-inject 3-button array for file_permission steps
|
||||||
|
// (step_probe sets step_type but not buttons; DOM observer /pending handler
|
||||||
|
// only injects buttons when command contains 'allow' which misses step_probe paths)
|
||||||
|
let buttons = data.buttons;
|
||||||
|
if (!buttons && data.step_type === 'file_permission') {
|
||||||
|
buttons = [
|
||||||
|
{ text: 'Allow Once', index: 0 },
|
||||||
|
{ text: 'Allow This Conversation', index: 1 },
|
||||||
|
{ text: 'Deny', index: 2 },
|
||||||
|
];
|
||||||
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
request_id: id,
|
request_id: id,
|
||||||
conversation_id: data.conversation_id,
|
conversation_id: data.conversation_id,
|
||||||
@@ -2400,6 +2414,7 @@ function writePendingApproval(data) {
|
|||||||
...(data.step_type ? { step_type: data.step_type } : {}),
|
...(data.step_type ? { step_type: data.step_type } : {}),
|
||||||
...(data.step_index !== undefined ? { step_index: data.step_index } : {}),
|
...(data.step_index !== undefined ? { step_index: data.step_index } : {}),
|
||||||
...(data.source ? { source: data.source } : {}),
|
...(data.source ? { source: data.source } : {}),
|
||||||
|
...(buttons ? { buttons } : {}),
|
||||||
};
|
};
|
||||||
fs.writeFileSync(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8');
|
fs.writeFileSync(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8');
|
||||||
console.log(`Gravity Bridge: pending approval written → ${id}.json`);
|
console.log(`Gravity Bridge: pending approval written → ${id}.json`);
|
||||||
@@ -2729,57 +2744,9 @@ async function activate(context) {
|
|||||||
bridgePath = configPath || path.join(os.homedir(), '.gemini', 'antigravity', 'bridge');
|
bridgePath = configPath || path.join(os.homedir(), '.gemini', 'antigravity', 'bridge');
|
||||||
ensureBridgeDir();
|
ensureBridgeDir();
|
||||||
console.log(`Gravity Bridge: bridge path: ${bridgePath}`);
|
console.log(`Gravity Bridge: bridge path: ${bridgePath}`);
|
||||||
// ── Single Active Project Lock ──
|
// ── Multi-project: no lock file, each project uses project_name-based filtering ──
|
||||||
const lockFile = path.join(bridgePath, 'active_project.lock');
|
// (active_project.lock removed — was blocking concurrent multi-project usage)
|
||||||
let bridgeDisabled = false;
|
logToFile(`[INIT] project="${projectName}" pid=${process.pid} — multi-project mode (no lock)`);
|
||||||
try {
|
|
||||||
if (fs.existsSync(lockFile)) {
|
|
||||||
const lockData = JSON.parse(fs.readFileSync(lockFile, 'utf-8'));
|
|
||||||
// Check if lock holder is still alive
|
|
||||||
let lockAlive = false;
|
|
||||||
if (lockData.pid) {
|
|
||||||
try {
|
|
||||||
process.kill(lockData.pid, 0);
|
|
||||||
lockAlive = true;
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
lockAlive = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (lockAlive && lockData.project !== projectName) {
|
|
||||||
vscode.window.showWarningMessage(`⚠️ Discord Bridge: "${lockData.project}"가 이미 활성 중 (PID ${lockData.pid}). 이 프로젝트(${projectName})는 Discord에 연결되지 않습니다.`, '무시하고 연결').then(choice => {
|
|
||||||
if (choice === '무시하고 연결') {
|
|
||||||
// Overwrite lock
|
|
||||||
fs.writeFileSync(lockFile, JSON.stringify({
|
|
||||||
project: projectName, pid: process.pid,
|
|
||||||
since: new Date().toISOString(), hostname: os.hostname()
|
|
||||||
}), 'utf-8');
|
|
||||||
logToFile(`[LOCK] Force-acquired lock (was: ${lockData.project})`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
bridgeDisabled = true;
|
|
||||||
logToFile(`[LOCK] Another project active: ${lockData.project} (PID ${lockData.pid}) — bridge disabled`);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Stale lock or same project — overwrite
|
|
||||||
fs.writeFileSync(lockFile, JSON.stringify({
|
|
||||||
project: projectName, pid: process.pid,
|
|
||||||
since: new Date().toISOString(), hostname: os.hostname()
|
|
||||||
}), 'utf-8');
|
|
||||||
logToFile(`[LOCK] Acquired lock (stale/same project)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
fs.writeFileSync(lockFile, JSON.stringify({
|
|
||||||
project: projectName, pid: process.pid,
|
|
||||||
since: new Date().toISOString(), hostname: os.hostname()
|
|
||||||
}), 'utf-8');
|
|
||||||
logToFile(`[LOCK] Created lock for ${projectName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (lockErr) {
|
|
||||||
logToFile(`[LOCK] Error: ${lockErr.message}`);
|
|
||||||
}
|
|
||||||
// Status bar
|
// Status bar
|
||||||
statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
|
statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
|
||||||
statusBar.text = '$(sync~spin) Bridge';
|
statusBar.text = '$(sync~spin) Bridge';
|
||||||
@@ -2897,14 +2864,13 @@ async function activate(context) {
|
|||||||
isActive = true;
|
isActive = true;
|
||||||
}
|
}
|
||||||
function deactivate() {
|
function deactivate() {
|
||||||
// Release active project lock
|
// Clean up stale lock file if it exists (legacy cleanup)
|
||||||
try {
|
try {
|
||||||
const lockFile = path.join(bridgePath, 'active_project.lock');
|
const lockFile = path.join(bridgePath, 'active_project.lock');
|
||||||
if (fs.existsSync(lockFile)) {
|
if (fs.existsSync(lockFile)) {
|
||||||
const lockData = JSON.parse(fs.readFileSync(lockFile, 'utf-8'));
|
const lockData = JSON.parse(fs.readFileSync(lockFile, 'utf-8'));
|
||||||
if (lockData.pid === process.pid) {
|
if (lockData.pid === process.pid) {
|
||||||
fs.unlinkSync(lockFile);
|
fs.unlinkSync(lockFile);
|
||||||
logToFile(`[LOCK] Released lock for ${projectName}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
|||||||
"name": "gravity-bridge",
|
"name": "gravity-bridge",
|
||||||
"displayName": "Gravity Bridge",
|
"displayName": "Gravity Bridge",
|
||||||
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
||||||
"version": "0.3.6",
|
"version": "0.3.7",
|
||||||
"publisher": "variet",
|
"publisher": "variet",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.100.0"
|
"vscode": "^1.100.0"
|
||||||
|
|||||||
@@ -1598,6 +1598,8 @@ function setupMonitor() {
|
|||||||
const pfPath = path.join(bridgePath, 'pending', pf);
|
const pfPath = path.join(bridgePath, 'pending', pf);
|
||||||
const pd = JSON.parse(fs.readFileSync(pfPath, 'utf-8'));
|
const pd = JSON.parse(fs.readFileSync(pfPath, 'utf-8'));
|
||||||
if (pd.status !== 'pending') continue;
|
if (pd.status !== 'pending') continue;
|
||||||
|
// Skip other projects' pendings
|
||||||
|
if (pd.project_name && pd.project_name !== projectName) continue;
|
||||||
// Match by step_index OR by recency (< 60s, any source)
|
// Match by step_index OR by recency (< 60s, any source)
|
||||||
const ageMs = nowMs - (pd.timestamp * 1000);
|
const ageMs = nowMs - (pd.timestamp * 1000);
|
||||||
const isMatch = pd.step_index === lastPendingStepIndex
|
const isMatch = pd.step_index === lastPendingStepIndex
|
||||||
@@ -2325,7 +2327,18 @@ function writePendingApproval(data: { conversation_id: string; command: string;
|
|||||||
}
|
}
|
||||||
|
|
||||||
const id = nowMs.toString();
|
const id = nowMs.toString();
|
||||||
const payload = {
|
// Auto-inject 3-button array for file_permission steps
|
||||||
|
// (step_probe sets step_type but not buttons; DOM observer /pending handler
|
||||||
|
// only injects buttons when command contains 'allow' which misses step_probe paths)
|
||||||
|
let buttons = data.buttons;
|
||||||
|
if (!buttons && data.step_type === 'file_permission') {
|
||||||
|
buttons = [
|
||||||
|
{ text: 'Allow Once', index: 0 },
|
||||||
|
{ text: 'Allow This Conversation', index: 1 },
|
||||||
|
{ text: 'Deny', index: 2 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const payload: Record<string, any> = {
|
||||||
request_id: id,
|
request_id: id,
|
||||||
conversation_id: data.conversation_id,
|
conversation_id: data.conversation_id,
|
||||||
command: data.command,
|
command: data.command,
|
||||||
@@ -2337,6 +2350,7 @@ function writePendingApproval(data: { conversation_id: string; command: string;
|
|||||||
...(data.step_type ? { step_type: data.step_type } : {}),
|
...(data.step_type ? { step_type: data.step_type } : {}),
|
||||||
...(data.step_index !== undefined ? { step_index: data.step_index } : {}),
|
...(data.step_index !== undefined ? { step_index: data.step_index } : {}),
|
||||||
...(data.source ? { source: data.source } : {}),
|
...(data.source ? { source: data.source } : {}),
|
||||||
|
...(buttons ? { buttons } : {}),
|
||||||
};
|
};
|
||||||
fs.writeFileSync(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8');
|
fs.writeFileSync(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8');
|
||||||
console.log(`Gravity Bridge: pending approval written → ${id}.json`);
|
console.log(`Gravity Bridge: pending approval written → ${id}.json`);
|
||||||
@@ -2664,51 +2678,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
ensureBridgeDir();
|
ensureBridgeDir();
|
||||||
console.log(`Gravity Bridge: bridge path: ${bridgePath}`);
|
console.log(`Gravity Bridge: bridge path: ${bridgePath}`);
|
||||||
|
|
||||||
// ── Single Active Project Lock ──
|
// ── Multi-project: no lock file, each project uses project_name-based filtering ──
|
||||||
const lockFile = path.join(bridgePath, 'active_project.lock');
|
// (active_project.lock removed — was blocking concurrent multi-project usage)
|
||||||
let bridgeDisabled = false;
|
logToFile(`[INIT] project="${projectName}" pid=${process.pid} — multi-project mode (no lock)`);
|
||||||
try {
|
|
||||||
if (fs.existsSync(lockFile)) {
|
|
||||||
const lockData = JSON.parse(fs.readFileSync(lockFile, 'utf-8'));
|
|
||||||
// Check if lock holder is still alive
|
|
||||||
let lockAlive = false;
|
|
||||||
if (lockData.pid) {
|
|
||||||
try { process.kill(lockData.pid, 0); lockAlive = true; } catch { lockAlive = false; }
|
|
||||||
}
|
|
||||||
if (lockAlive && lockData.project !== projectName) {
|
|
||||||
vscode.window.showWarningMessage(
|
|
||||||
`⚠️ Discord Bridge: "${lockData.project}"가 이미 활성 중 (PID ${lockData.pid}). 이 프로젝트(${projectName})는 Discord에 연결되지 않습니다.`,
|
|
||||||
'무시하고 연결'
|
|
||||||
).then(choice => {
|
|
||||||
if (choice === '무시하고 연결') {
|
|
||||||
// Overwrite lock
|
|
||||||
fs.writeFileSync(lockFile, JSON.stringify({
|
|
||||||
project: projectName, pid: process.pid,
|
|
||||||
since: new Date().toISOString(), hostname: os.hostname()
|
|
||||||
}), 'utf-8');
|
|
||||||
logToFile(`[LOCK] Force-acquired lock (was: ${lockData.project})`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
bridgeDisabled = true;
|
|
||||||
logToFile(`[LOCK] Another project active: ${lockData.project} (PID ${lockData.pid}) — bridge disabled`);
|
|
||||||
} else {
|
|
||||||
// Stale lock or same project — overwrite
|
|
||||||
fs.writeFileSync(lockFile, JSON.stringify({
|
|
||||||
project: projectName, pid: process.pid,
|
|
||||||
since: new Date().toISOString(), hostname: os.hostname()
|
|
||||||
}), 'utf-8');
|
|
||||||
logToFile(`[LOCK] Acquired lock (stale/same project)`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fs.writeFileSync(lockFile, JSON.stringify({
|
|
||||||
project: projectName, pid: process.pid,
|
|
||||||
since: new Date().toISOString(), hostname: os.hostname()
|
|
||||||
}), 'utf-8');
|
|
||||||
logToFile(`[LOCK] Created lock for ${projectName}`);
|
|
||||||
}
|
|
||||||
} catch (lockErr: any) {
|
|
||||||
logToFile(`[LOCK] Error: ${lockErr.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status bar
|
// Status bar
|
||||||
statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
|
statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
|
||||||
@@ -2831,14 +2803,13 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate() {
|
export function deactivate() {
|
||||||
// Release active project lock
|
// Clean up stale lock file if it exists (legacy cleanup)
|
||||||
try {
|
try {
|
||||||
const lockFile = path.join(bridgePath, 'active_project.lock');
|
const lockFile = path.join(bridgePath, 'active_project.lock');
|
||||||
if (fs.existsSync(lockFile)) {
|
if (fs.existsSync(lockFile)) {
|
||||||
const lockData = JSON.parse(fs.readFileSync(lockFile, 'utf-8'));
|
const lockData = JSON.parse(fs.readFileSync(lockFile, 'utf-8'));
|
||||||
if (lockData.pid === process.pid) {
|
if (lockData.pid === process.pid) {
|
||||||
fs.unlinkSync(lockFile);
|
fs.unlinkSync(lockFile);
|
||||||
logToFile(`[LOCK] Released lock for ${projectName}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|||||||
Reference in New Issue
Block a user