fix(bridge): approval flow tuning — dedup + text cleanup + stall fallback removal + safe reject #task-256
This commit is contained in:
@@ -249,3 +249,15 @@
|
|||||||
- **해결**: CSP의 `script-src`에 `'unsafe-inline'` 추가. Extension `setupApprovalObserver()`에 CSP 자동 패치 로직 영구 추가
|
- **해결**: CSP의 `script-src`에 `'unsafe-inline'` 추가. Extension `setupApprovalObserver()`에 CSP 자동 패치 로직 영구 추가
|
||||||
- **주의**: `style-src`에는 `'unsafe-inline'`이 있어 스타일은 동작 → 스크립트만 차단되는 것이 함정. `connect-src`에 `http://127.0.0.1:*` 존재하여 HTTP fetch는 허용됨 (CSP 통과 시 통신은 문제없음). 이 CSP 이슈는 **V8 CachedData 이슈와 동시에 존재**하여 진단이 매우 어려웠음
|
- **주의**: `style-src`에는 `'unsafe-inline'`이 있어 스타일은 동작 → 스크립트만 차단되는 것이 함정. `connect-src`에 `http://127.0.0.1:*` 존재하여 HTTP fetch는 허용됨 (CSP 통과 시 통신은 문제없음). 이 CSP 이슈는 **V8 CachedData 이슈와 동시에 존재**하여 진단이 매우 어려웠음
|
||||||
|
|
||||||
|
### [2026-03-09] 중복 승인 요청 — DOM scan + Step probe 동시 발동
|
||||||
|
- **증상**: Discord에 같은 명령에 대해 승인 요청이 2개 도착
|
||||||
|
- **원인**: DOM Observer가 `RunAlt+↵` 버튼을 감지하여 pending 생성 + Step probe가 `CORTEX_STEP_STATUS_WAITING` 감지하여 pending 생성 → 동일 step에 대해 2개 파일
|
||||||
|
- **해결**: `writePendingApproval()`에 15초 dedup 윈도우 추가 — 최근 DOM observer pending이 있으면 step probe pending 스킵
|
||||||
|
- **주의**: DOM scan은 제거 불가 — `Allow Once`, `Allow This Conversation` 등 step probe가 감지 못하는 UI 버튼 존재. 버튼 텍스트도 `(Alt|Ctrl|Shift|Meta)\+.*` 정규식으로 키보드 단축키 정제
|
||||||
|
|
||||||
|
### [2026-03-09] Step probe reject → ResolveOutstandingSteps가 AI 작업 취소
|
||||||
|
- **증상**: Discord에서 거부 클릭 → AI의 현재 step뿐 아니라 진행 중인 작업 전체가 중단
|
||||||
|
- **원인**: step probe 경로의 `tryApprovalStrategies(approved=false)` → `ResolveOutstandingSteps` RPC 호출 → 이것은 step을 **CANCEL**하는 파괴적 동작
|
||||||
|
- **해결**: step probe 경로에서 reject 시 `tryApprovalStrategies` 호출 제거. reject은 로그만 남기고 파기적 동작 없음. DOM observer 경로만 실제 Reject 버튼 클릭 허용
|
||||||
|
- **주의**: `ResolveOutstandingSteps`는 이름과 달리 "해결"이 아닌 "취소". 승인에 절대 사용 금지 (이전 이슈 #55~59 참조)
|
||||||
|
|
||||||
|
|||||||
@@ -9,4 +9,5 @@
|
|||||||
| 005 | 18:30~19:28 | workbench.html inline v3 패치 누락 수정 + pre-patch 검증 | `b61cff1` | ✅ |
|
| 005 | 18:30~19:28 | workbench.html inline v3 패치 누락 수정 + pre-patch 검증 | `b61cff1` | ✅ |
|
||||||
| 006 | 19:38~19:56 | V8 CachedData 진단 + 캐시 삭제 (renderer 미실행 근본 원인) | docs only | ✅ |
|
| 006 | 19:38~19:56 | V8 CachedData 진단 + 캐시 삭제 (renderer 미실행 근본 원인) | docs only | ✅ |
|
||||||
| 007 | 20:04~20:28 | CSP script-src `'unsafe-inline'` 패치 (renderer 미실행 진짜 근본 원인) | `08077e8` | ✅ |
|
| 007 | 20:04~20:28 | CSP script-src `'unsafe-inline'` 패치 (renderer 미실행 진짜 근본 원인) | `08077e8` | ✅ |
|
||||||
| 008 | 21:00~21:30 | **E2E 승인 플로우 성공 검증** — AG 재시작 후 renderer v3 실행 확인 + Discord 승인→명령 실행 | pending | ✅ |
|
| 008 | 21:00~21:30 | **E2E 승인 플로우 성공 검증** — AG 재시작 후 renderer v3 실행 확인 + Discord 승인→명령 실행 | `520d36e` | ✅ |
|
||||||
|
| 009 | 21:33~22:28 | 승인 플로우 튜닝 — dedup + 텍스트 정제 + stall fallback 제거 + reject 안전화 | pending | 🔧 |
|
||||||
|
|||||||
@@ -980,6 +980,9 @@ function generateApprovalObserverScript(_port) {
|
|||||||
|
|
||||||
var txt=(b.textContent||'').trim();
|
var txt=(b.textContent||'').trim();
|
||||||
if(!txt)continue;
|
if(!txt)continue;
|
||||||
|
// Strip keyboard shortcut suffixes (e.g. "RunAlt+↵" → "Run")
|
||||||
|
txt=txt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/,'').trim();
|
||||||
|
if(!txt)continue;
|
||||||
|
|
||||||
// Match against patterns
|
// Match against patterns
|
||||||
var matchedType=null;
|
var matchedType=null;
|
||||||
@@ -1480,28 +1483,8 @@ function setupMonitor() {
|
|||||||
logToFile(`[STEP-PROBE] error: ${e.message}`);
|
logToFile(`[STEP-PROBE] error: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const now = Date.now();
|
// Stall fallback REMOVED — step probe is sole fallback source
|
||||||
const cooldownOk = (now - lastPendingTime) > 60_000;
|
// (stall fallback was generating false positives and is now redundant)
|
||||||
const fallbackThreshold = (currentCount > 770) ? 4 : 8; // 775-limit: faster fallback
|
|
||||||
if (consecutiveIdleCount >= fallbackThreshold && sawRunningAfterPending && cooldownOk) {
|
|
||||||
// Dynamic fallback: 20s (>770 steps) or 40s (normal)
|
|
||||||
lastPendingStepIndex = currentCount;
|
|
||||||
lastPendingTime = now;
|
|
||||||
sawRunningAfterPending = false;
|
|
||||||
const command = `Stall at step ${currentCount} (fallback)`;
|
|
||||||
const description = `승인 대기 감지 — fallback (${consecutiveIdleCount * 5}초 정지), Title: "${currentTitle}"`;
|
|
||||||
logToFile(`[STALL-FALLBACK] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`);
|
|
||||||
writePendingApproval({ conversation_id: activeSessionId, command, description, source: 'stall_fallback' });
|
|
||||||
}
|
|
||||||
else if (consecutiveIdleCount === fallbackThreshold) {
|
|
||||||
const reasons = [];
|
|
||||||
if (!sawRunningAfterPending)
|
|
||||||
reasons.push('needDelta>0');
|
|
||||||
if (!cooldownOk)
|
|
||||||
reasons.push(`cooldown(${Math.round((60000 - (now - lastPendingTime)) / 1000)}s)`);
|
|
||||||
if (reasons.length > 0)
|
|
||||||
logToFile(`[STALL] SKIP: ${reasons.join(', ')}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (!isRunning) {
|
else if (!isRunning) {
|
||||||
consecutiveIdleCount = 0;
|
consecutiveIdleCount = 0;
|
||||||
@@ -1614,19 +1597,22 @@ async function processResponseFile(filePath) {
|
|||||||
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Step probe / stall path: try all approval strategies
|
// Step probe path: approve → trigger renderer click, reject → log only
|
||||||
logToFile(`[RESPONSE] step_probe/stall → trying multi-strategy approval`);
|
if (approved) {
|
||||||
const approvalResult = await tryApprovalStrategies(approved, sessionId);
|
logToFile(`[RESPONSE] step_probe → approve via trigger-click`);
|
||||||
logToFile(`[RESPONSE] approval result: ${approvalResult}`);
|
clickTrigger = { action: 'approve', timestamp: Date.now() };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// SAFE: reject logs only — NO ResolveOutstandingSteps (it cancels AI work!)
|
||||||
|
logToFile(`[RESPONSE] step_probe → reject (log only, no destructive action)`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
||||||
// Cleanup response file — BUT NOT for DOM observer!
|
// Cleanup response file
|
||||||
if (!isDomObserver) {
|
try {
|
||||||
try {
|
fs.unlinkSync(filePath);
|
||||||
fs.unlinkSync(filePath);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
}
|
||||||
|
catch { }
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
const log = `[RESPONSE] error: ${e.message}`;
|
const log = `[RESPONSE] error: ${e.message}`;
|
||||||
@@ -1762,13 +1748,33 @@ function writePendingApproval(data) {
|
|||||||
if (!fs.existsSync(pendingDir)) {
|
if (!fs.existsSync(pendingDir)) {
|
||||||
fs.mkdirSync(pendingDir, { recursive: true });
|
fs.mkdirSync(pendingDir, { recursive: true });
|
||||||
}
|
}
|
||||||
const id = Date.now().toString();
|
// ── Dedup: skip if DOM observer already created a pending for same action recently ──
|
||||||
|
const nowMs = Date.now();
|
||||||
|
const DEDUP_WINDOW_MS = 15_000; // 15 second dedup window
|
||||||
|
try {
|
||||||
|
const existingFiles = fs.readdirSync(pendingDir).filter((f) => f.endsWith('.json'));
|
||||||
|
for (const ef of existingFiles) {
|
||||||
|
const efPath = path.join(pendingDir, ef);
|
||||||
|
const existing = JSON.parse(fs.readFileSync(efPath, 'utf-8'));
|
||||||
|
if (existing.source === 'dom_observer' && existing.status === 'pending') {
|
||||||
|
const age = nowMs - (existing.timestamp * 1000);
|
||||||
|
if (age < DEDUP_WINDOW_MS && age >= 0) {
|
||||||
|
logToFile(`[DEDUP] skip step_probe pending — DOM observer pending exists: ${ef} (${Math.round(age / 1000)}s ago)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (dedupErr) {
|
||||||
|
logToFile(`[DEDUP] check error (non-fatal): ${dedupErr.message}`);
|
||||||
|
}
|
||||||
|
const id = nowMs.toString();
|
||||||
const payload = {
|
const payload = {
|
||||||
request_id: id,
|
request_id: id,
|
||||||
conversation_id: data.conversation_id,
|
conversation_id: data.conversation_id,
|
||||||
command: data.command,
|
command: data.command,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
timestamp: Date.now() / 1000,
|
timestamp: nowMs / 1000,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
discord_message_id: 0,
|
discord_message_id: 0,
|
||||||
project_name: projectName,
|
project_name: projectName,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -569,29 +569,29 @@ function startObserverHttpBridge(): Promise<number> {
|
|||||||
// Listen on deterministic port (derived from projectName), fallback to random
|
// Listen on deterministic port (derived from projectName), fallback to random
|
||||||
deterministicPort = getDeterministicPort(projectName);
|
deterministicPort = getDeterministicPort(projectName);
|
||||||
const tryListen = (targetPort: number) => {
|
const tryListen = (targetPort: number) => {
|
||||||
server.listen(targetPort, '127.0.0.1', () => {
|
server.listen(targetPort, '127.0.0.1', () => {
|
||||||
const port = server.address().port;
|
const port = server.address().port;
|
||||||
observerHttpServer = server;
|
observerHttpServer = server;
|
||||||
logToFile(`[HTTP] bridge server started on port ${port}`);
|
logToFile(`[HTTP] bridge server started on port ${port}`);
|
||||||
|
|
||||||
// Write port to shared ports JSON (multi-bridge support)
|
// Write port to shared ports JSON (multi-bridge support)
|
||||||
const patcher = (sdk.integration as any)?._patcher;
|
const patcher = (sdk.integration as any)?._patcher;
|
||||||
if (patcher && typeof patcher.getWorkbenchDir === 'function') {
|
if (patcher && typeof patcher.getWorkbenchDir === 'function') {
|
||||||
const workbenchDir = patcher.getWorkbenchDir();
|
const workbenchDir = patcher.getWorkbenchDir();
|
||||||
const portsFile = path.join(workbenchDir, 'ag-bridge-ports.json');
|
const portsFile = path.join(workbenchDir, 'ag-bridge-ports.json');
|
||||||
let portsData: Record<string, number> = {};
|
let portsData: Record<string, number> = {};
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(portsFile)) {
|
if (fs.existsSync(portsFile)) {
|
||||||
portsData = JSON.parse(fs.readFileSync(portsFile, 'utf-8'));
|
portsData = JSON.parse(fs.readFileSync(portsFile, 'utf-8'));
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
portsData[projectName] = port;
|
portsData[projectName] = port;
|
||||||
fs.writeFileSync(portsFile, JSON.stringify(portsData), 'utf-8');
|
fs.writeFileSync(portsFile, JSON.stringify(portsData), 'utf-8');
|
||||||
logToFile(`[HTTP] ports JSON updated → ${portsFile} (${projectName}=${port})`);
|
logToFile(`[HTTP] ports JSON updated → ${portsFile} (${projectName}=${port})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(port);
|
resolve(port);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
server.on('error', (e: any) => {
|
server.on('error', (e: any) => {
|
||||||
@@ -957,6 +957,9 @@ function generateApprovalObserverScript(_port: number): string {
|
|||||||
|
|
||||||
var txt=(b.textContent||'').trim();
|
var txt=(b.textContent||'').trim();
|
||||||
if(!txt)continue;
|
if(!txt)continue;
|
||||||
|
// Strip keyboard shortcut suffixes (e.g. "RunAlt+↵" → "Run")
|
||||||
|
txt=txt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/,'').trim();
|
||||||
|
if(!txt)continue;
|
||||||
|
|
||||||
// Match against patterns
|
// Match against patterns
|
||||||
var matchedType=null;
|
var matchedType=null;
|
||||||
@@ -1406,7 +1409,7 @@ function setupMonitor() {
|
|||||||
if (steps.length < currentCount) {
|
if (steps.length < currentCount) {
|
||||||
logToFile(`[STEP-PROBE] ⚠️ 775-limit hit! steps=${steps.length} < stepCount=${currentCount}`);
|
logToFile(`[STEP-PROBE] ⚠️ 775-limit hit! steps=${steps.length} < stepCount=${currentCount}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan last 5 steps backwards to find WAITING (RUN_COMMAND may not be last)
|
// Scan last 5 steps backwards to find WAITING (RUN_COMMAND may not be last)
|
||||||
let foundWaiting = false;
|
let foundWaiting = false;
|
||||||
for (let si = steps.length - 1; si >= Math.max(0, steps.length - 5); si--) {
|
for (let si = steps.length - 1; si >= Math.max(0, steps.length - 5); si--) {
|
||||||
@@ -1420,7 +1423,7 @@ function setupMonitor() {
|
|||||||
const toolCall = step?.metadata?.toolCall;
|
const toolCall = step?.metadata?.toolCall;
|
||||||
const toolName = toolCall?.name || stepType.replace('CORTEX_STEP_TYPE_', '').toLowerCase();
|
const toolName = toolCall?.name || stepType.replace('CORTEX_STEP_TYPE_', '').toLowerCase();
|
||||||
let command = toolName;
|
let command = toolName;
|
||||||
|
|
||||||
// Parse argumentsJson for command details
|
// Parse argumentsJson for command details
|
||||||
if (toolCall?.argumentsJson) {
|
if (toolCall?.argumentsJson) {
|
||||||
try {
|
try {
|
||||||
@@ -1465,27 +1468,8 @@ function setupMonitor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
// Stall fallback REMOVED — step probe is sole fallback source
|
||||||
const cooldownOk = (now - lastPendingTime) > 60_000;
|
// (stall fallback was generating false positives and is now redundant)
|
||||||
|
|
||||||
const fallbackThreshold = (currentCount > 770) ? 4 : 8; // 775-limit: faster fallback
|
|
||||||
if (consecutiveIdleCount >= fallbackThreshold && sawRunningAfterPending && cooldownOk) {
|
|
||||||
// Dynamic fallback: 20s (>770 steps) or 40s (normal)
|
|
||||||
lastPendingStepIndex = currentCount;
|
|
||||||
lastPendingTime = now;
|
|
||||||
sawRunningAfterPending = false;
|
|
||||||
|
|
||||||
const command = `Stall at step ${currentCount} (fallback)`;
|
|
||||||
const description = `승인 대기 감지 — fallback (${consecutiveIdleCount * 5}초 정지), Title: "${currentTitle}"`;
|
|
||||||
|
|
||||||
logToFile(`[STALL-FALLBACK] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`);
|
|
||||||
writePendingApproval({ conversation_id: activeSessionId, command, description, source: 'stall_fallback' });
|
|
||||||
} else if (consecutiveIdleCount === fallbackThreshold) {
|
|
||||||
const reasons = [];
|
|
||||||
if (!sawRunningAfterPending) reasons.push('needDelta>0');
|
|
||||||
if (!cooldownOk) reasons.push(`cooldown(${Math.round((60000 - (now - lastPendingTime)) / 1000)}s)`);
|
|
||||||
if (reasons.length > 0) logToFile(`[STALL] SKIP: ${reasons.join(', ')}`);
|
|
||||||
}
|
|
||||||
} else if (!isRunning) {
|
} else if (!isRunning) {
|
||||||
consecutiveIdleCount = 0;
|
consecutiveIdleCount = 0;
|
||||||
lastModTime = currentModTime;
|
lastModTime = currentModTime;
|
||||||
@@ -1601,18 +1585,20 @@ async function processResponseFile(filePath: string) {
|
|||||||
// DOM observer path: renderer polls /response/:rid and clicks directly
|
// DOM observer path: renderer polls /response/:rid and clicks directly
|
||||||
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
||||||
} else {
|
} else {
|
||||||
// Step probe / stall path: try all approval strategies
|
// Step probe path: approve → trigger renderer click, reject → log only
|
||||||
logToFile(`[RESPONSE] step_probe/stall → trying multi-strategy approval`);
|
if (approved) {
|
||||||
const approvalResult = await tryApprovalStrategies(approved, sessionId);
|
logToFile(`[RESPONSE] step_probe → approve via trigger-click`);
|
||||||
logToFile(`[RESPONSE] approval result: ${approvalResult}`);
|
clickTrigger = { action: 'approve' as const, timestamp: Date.now() };
|
||||||
|
} else {
|
||||||
|
// SAFE: reject logs only — NO ResolveOutstandingSteps (it cancels AI work!)
|
||||||
|
logToFile(`[RESPONSE] step_probe → reject (log only, no destructive action)`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
||||||
|
|
||||||
// Cleanup response file — BUT NOT for DOM observer!
|
// Cleanup response file
|
||||||
if (!isDomObserver) {
|
try { fs.unlinkSync(filePath); } catch { }
|
||||||
try { fs.unlinkSync(filePath); } catch { }
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const log = `[RESPONSE] error: ${e.message}`;
|
const log = `[RESPONSE] error: ${e.message}`;
|
||||||
console.log(`Gravity Bridge: ${log}`);
|
console.log(`Gravity Bridge: ${log}`);
|
||||||
@@ -1738,13 +1724,34 @@ function writePendingApproval(data: { conversation_id: string; command: string;
|
|||||||
try {
|
try {
|
||||||
const pendingDir = path.join(bridgePath, 'pending');
|
const pendingDir = path.join(bridgePath, 'pending');
|
||||||
if (!fs.existsSync(pendingDir)) { fs.mkdirSync(pendingDir, { recursive: true }); }
|
if (!fs.existsSync(pendingDir)) { fs.mkdirSync(pendingDir, { recursive: true }); }
|
||||||
const id = Date.now().toString();
|
|
||||||
|
// ── Dedup: skip if DOM observer already created a pending for same action recently ──
|
||||||
|
const nowMs = Date.now();
|
||||||
|
const DEDUP_WINDOW_MS = 15_000; // 15 second dedup window
|
||||||
|
try {
|
||||||
|
const existingFiles = fs.readdirSync(pendingDir).filter((f: string) => f.endsWith('.json'));
|
||||||
|
for (const ef of existingFiles) {
|
||||||
|
const efPath = path.join(pendingDir, ef);
|
||||||
|
const existing = JSON.parse(fs.readFileSync(efPath, 'utf-8'));
|
||||||
|
if (existing.source === 'dom_observer' && existing.status === 'pending') {
|
||||||
|
const age = nowMs - (existing.timestamp * 1000);
|
||||||
|
if (age < DEDUP_WINDOW_MS && age >= 0) {
|
||||||
|
logToFile(`[DEDUP] skip step_probe pending — DOM observer pending exists: ${ef} (${Math.round(age/1000)}s ago)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (dedupErr: any) {
|
||||||
|
logToFile(`[DEDUP] check error (non-fatal): ${dedupErr.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = nowMs.toString();
|
||||||
const payload = {
|
const payload = {
|
||||||
request_id: id,
|
request_id: id,
|
||||||
conversation_id: data.conversation_id,
|
conversation_id: data.conversation_id,
|
||||||
command: data.command,
|
command: data.command,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
timestamp: Date.now() / 1000,
|
timestamp: nowMs / 1000,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
discord_message_id: 0,
|
discord_message_id: 0,
|
||||||
project_name: projectName,
|
project_name: projectName,
|
||||||
@@ -1908,7 +1915,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
logToFile(`[CMD-DISCOVERY] Total antigravity.* commands: ${agCmds.length}`);
|
logToFile(`[CMD-DISCOVERY] Total antigravity.* commands: ${agCmds.length}`);
|
||||||
// Log approval-related commands specifically
|
// Log approval-related commands specifically
|
||||||
const approvalKeywords = ['accept', 'reject', 'approve', 'terminal', 'agent', 'cascade', 'step', 'run', 'command.'];
|
const approvalKeywords = ['accept', 'reject', 'approve', 'terminal', 'agent', 'cascade', 'step', 'run', 'command.'];
|
||||||
const relevantCmds = agCmds.filter((c: string) =>
|
const relevantCmds = agCmds.filter((c: string) =>
|
||||||
approvalKeywords.some(kw => c.toLowerCase().includes(kw))
|
approvalKeywords.some(kw => c.toLowerCase().includes(kw))
|
||||||
);
|
);
|
||||||
logToFile(`[CMD-DISCOVERY] Approval-related commands (${relevantCmds.length}):`);
|
logToFile(`[CMD-DISCOVERY] Approval-related commands (${relevantCmds.length}):`);
|
||||||
|
|||||||
Reference in New Issue
Block a user