Compare commits

...

4 Commits

Author SHA1 Message Date
Variet Worker
e4f674ec9f docs: restore unintentionally deleted known issues (mend destructive commit) 2026-04-09 22:36:50 +09:00
Variet Worker
47c0602427 fix(extension): pin point CodeLens exclusion filter to prevent native Agent UI freezing (v0.5.22) #task-602 2026-04-09 22:32:40 +09:00
Variet Worker
75762964e3 fix(extension): adapt DOM observer to Native Agent panel and Tailwind migration (v0.5.21) 2026-04-09 21:56:29 +09:00
Variet Worker
d2023321bd fix(extension): remove redundant SafeToAutoRun chat snapshot for Discord relay
* Removed writeChatSnapshot calls in step-probe.ts to prevent duplicate ' 자동 실행됨' notifications since bot.py already broadcasts '🤖 자동 승인됨'.
* Docs: Update devlog and known-issues with Discord Bot cache deletion bugs and multi-workspace LS connection conflicts.
* Fixes Vikunja #593
2026-04-08 17:59:40 +09:00
16 changed files with 227 additions and 21 deletions

View File

@@ -27,8 +27,26 @@
--- ---
### [2026-04-08] [Discord Bot] Channel Deletion Cache Desync
- **증상**: 봇이 켜져 있는 상태에서 Discord 채널(g-project-name)을 삭제하면, 봇이 삭제를 인지하지 못하고 새 채널을 생성하지 않으며 메시지도 증발함.
- **원인**: ot.py의 self.project_channels 딕셔너리에 채널 객체가 캐시되어 있어, API 호출 없이 캐시된(삭제된) 채널로 메시지를 보내려 시도하다 404 에러 발생 후 실패함.
- **해결**: 채널 맵핑이 꼬였을 때는 **Python 봇(Docker 컨테이너)을 재시작**하여 캐시를 초기화하고 채널 목록을 새로 갱신하게 함.
- **주의**: 채널 관리는 캐시에 의존하기 때문에 강제로 Discord UI에서 채널을 지웠을 때는 반드시 봇을 재구동해야 함.
### [2026-04-08] [Extension] Multiple Workspace LS Cross-Connection
- **증상**: ariet-llm 창에서 켰으나 gravity_control의 백그라운드 구동 중인 LS에 연결되어 자기 자신 창의 신호를 잡지 못함.
- **원인**: 여러 VS Code 창을 띄웠을 때 어떤 창에서는 Antigravity 패널을 누르지 않아 전용 LS가 시작되지 않음. ixLSConnection()이 자기 몫의 LS를 찾지 못하고 fallback으로 기존에 떠 있던 다른 창의 LS에 연결됨.
- **해결**: 대상 창에서 Developer: Reload Window 실행 후 **사이드바의 로컬 Antigravity 챗봇 패널을 한 번 열어** 자신의 LS 프로세스를 띄운 뒤에 Gravity Bridge를 Start함.
- **주의**: LS는 자동으로 시작되지 않고 사용자가 채팅 패널을 한 번 클릭/활성화해야만 Spawn 됨.
## 🔴 Active/Recent Issues ## 🔴 Active/Recent Issues
### [2026-04-09] [Extension] Agent UI Native Migration & CodeLens False Positive Filter
- **증상**: UI Tailwind/Native 마이그레이션 적용 후, Discord 브릿지로 신호가 전혀 전송되지 않음
- **원인**: Agent 패널이 탭/에디터 본문에 직접 렌더링되면서, 기존 오작동 방지 로직(`if (b.closest('.monaco-editor'))`)에 패널 전체 버튼이 포착되어 무시됨
- **해결**: 너무 광범위한 `.monaco-editor` 방어를 해제하고, 코드 렌즈 고유 컨테이너인 `.codelens-decoration` 내부일 경우에만 무시하도록 핀포인트 수정
- **주의**: DOM 옵저버 필터 조건 작성 시 래퍼 클래스는 UI 디자인 개편(Native, Editor Tab 등 위치 변경)에 매우 취약함. 가장 구체적인 내부 노드 클래스나 타겟 고유 속성을 통해 필터링할 것
### [2026-03-31] [step-probe] GetAllCascadeTrajectories 10-Item Hard Limit (Signal Drop) ### [2026-03-31] [step-probe] GetAllCascadeTrajectories 10-Item Hard Limit (Signal Drop)
- **증상**: `guitar_score` 등에서 활성화된 세션의 디스코드 승인 신호를 "계속해서" 잡지 못함. (WS 60초 타임아웃보다 더 치명적으로 신호가 아예 가지 않음) - **증상**: `guitar_score` 등에서 활성화된 세션의 디스코드 승인 신호를 "계속해서" 잡지 못함. (WS 60초 타임아웃보다 더 치명적으로 신호가 아예 가지 않음)
- **원인**: Extension이 활성 세션을 찾기 위해 호출하는 `GetAllCascadeTrajectories` LS API가 `{}`(빈 인자)로 호출될 때, 기본적으로 **10개의 세션만 반환하는 하드 리밋(Pagination Limit)**이 걸려있음. 이로 인해 작업 내역이 누적되면 수많은 최신/진행 중 세션들이 10개 목록에서 밀려나 누락됨. 익스텐션은 세션이 없다고 판단해 강제로 `IDLE` 모드에 진입하며, 승인 대기열(WAITING) 자체를 검사하지 않게 됨. - **원인**: Extension이 활성 세션을 찾기 위해 호출하는 `GetAllCascadeTrajectories` LS API가 `{}`(빈 인자)로 호출될 때, 기본적으로 **10개의 세션만 반환하는 하드 리밋(Pagination Limit)**이 걸려있음. 이로 인해 작업 내역이 누적되면 수많은 최신/진행 중 세션들이 10개 목록에서 밀려나 누락됨. 익스텐션은 세션이 없다고 판단해 강제로 `IDLE` 모드에 진입하며, 승인 대기열(WAITING) 자체를 검사하지 않게 됨.

View File

@@ -1,7 +1,8 @@
# 2026-04-08 # 2026-04-08
| NNN | HH:MM | 작업 설명 | `커밋해시` | 상태 | | NNN | HH:MM | 작업 설명 | `커밋해시` | 상태 |
|---|---|---|---|---| |---|---|---|---|---|
| 004 | 14:00 | SafeToAutoRun 알림 누락 복구 (v0.5.18) | `8f2a1b3` | ✅ | | 004 | 14:00 | SafeToAutoRun 알림 누락 복구 (v0.5.18) | `8f2a1b3` | ✅ |
| 005 | 16:30 | SafeToAutoRun pending skip으로 인한 데드락 원인 파악 및 롤백 | `13f13ee` | ✅ | | 005 | 16:30 | SafeToAutoRun pending skip으로 인한 데드락 원인 파악 및 롤백 | `13f13ee` | ✅ |
| 006 | 07:30 | SafeToAutoRun 데드락 완전 해결을 위한 Agnostic Bridge 도입 및 프리징 방어 (v0.5.20) | `임시해시` | ✅ | | 006 | 07:30 | SafeToAutoRun 데드락 완전 해결을 위한 Agnostic Bridge 도입 및 프리징 방어 (v0.5.20) | `임시해시` | ✅ |
| 007 | 17:57 | Gravity Bridge 안정화: 중복 알림(SafeToAutoRun) 제거 설계 확정 및 Discord 봇 캐시/로컬 LS 크로스매칭 증상 디버깅 완료 | \-\ | ✅ |

View File

@@ -0,0 +1,6 @@
# 2026-04-09
| NNN | HH:MM | 작업 설명 | `커밋해시` | 상태 |
|---|---|---|---|---|
| 001 | 21:55 | Agent UI Tailwind/Native 마이그레이션 대응 (DOM 옵저버 구조 개편) | `HEAD` | ✅ |
| 002 | 22:30 | Agent UI 버튼 무시 버그 긴급수정 (CodeLens 필터교정) | `HEAD` | ✅ |

View 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`) 컨테이너 재가동을 통해 **봇 프로세스 캐시 초기화** → 채널 자동 재생성, 완벽 디버깅에 성공했습니다.
## 미완료
- 없음. 모두 성공적으로 동작 중.

View 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` 등으로 완화.
## 미완료
- 없음

View 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 재배포.
## 미완료
- 없음 (검증은 유저 몫으로 인계)

View File

@@ -2,7 +2,7 @@
"name": "gravity-bridge", "name": "gravity-bridge",
"displayName": "Gravity Bridge", "displayName": "Gravity Bridge",
"description": "Antigravity ↔ Discord 브리지 연동 확장", "description": "Antigravity ↔ Discord 브리지 연동 확장",
"version": "0.5.20", "version": "0.5.22",
"publisher": "variet", "publisher": "variet",
"engines": { "engines": {
"vscode": "^1.100.0" "vscode": "^1.100.0"

View File

@@ -273,16 +273,16 @@ export function generateApprovalObserverScript(_port: number): string {
// ONLY positive triggers should initiate a pending request group. // ONLY positive triggers should initiate a pending request group.
// Negative/secondary buttons (Deny, Reject, Dismiss) will be collected as siblings. // Negative/secondary buttons (Deny, Reject, Dismiss) will be collected as siblings.
var PATS=[ var PATS=[
{re:/^Run/i, type:'terminal_command'}, {re:/^(?:Always\s*)?Run/i, type:'terminal_command'},
{re:/^Accept all/i, type:'diff_review'}, {re:/^(?:Always\s*)?Accept all/i, type:'diff_review'},
{re:/^Accept/i, type:'agent_step'}, {re:/^(?:Always\s*)?Accept/i, type:'agent_step'},
{re:/^(?:Always )?Allow/i, type:'permission'}, {re:/^(?:Always\s*)?Allow/i, type:'permission'},
{re:/^Approve/i, type:'agent_step'}, {re:/^(?:Always\s*)?Approve/i, type:'agent_step'},
{re:/^Retry/i, type:'error_recovery'}, {re:/^Retry/i, type:'error_recovery'},
]; ];
// ALL actionable button patterns (for grouping siblings in same container) // ALL actionable button patterns (for grouping siblings in same container)
var ALL_ACTION_RE=[/^Run/i,/^Accept/i,/^Reject/i,/^(?:Always )?Allow/i,/^Deny/i,/^Approve/i,/^Cancel$/i,/^Retry$/i,/^Dismiss$/i,/^Stop$/i,/^Decline$/i]; var ALL_ACTION_RE=[/^(?:Always\s*)?Run/i,/^(?:Always\s*)?Accept/i,/^Reject/i,/^(?:Always\s*)?Allow/i,/^Deny/i,/^(?:Always\s*)?Approve/i,/^Cancel$/i,/^Retry$/i,/^Dismiss$/i,/^Stop$/i,/^Decline$/i];
// Reject button patterns for finding the counterpart // Reject button patterns for finding the counterpart
var REJECT_RE=[/^reject$/i,/^reject all$/i,/^cancel$/i,/^deny$/i,/^stop$/i,/^decline$/i,/^dismiss$/i]; var REJECT_RE=[/^reject$/i,/^reject all$/i,/^cancel$/i,/^deny$/i,/^stop$/i,/^decline$/i,/^dismiss$/i];
@@ -372,6 +372,9 @@ export function generateApprovalObserverScript(_port: number): string {
'.react-app-container', '.react-app-container',
'[class*="agent-panel"]', '[class*="agent-panel"]',
'[class*="agentPanel"]', '[class*="agentPanel"]',
'.chat-body',
'.interactive-session',
'[class*="sidebar"]',
]; ];
for(var i=0;i<selectors.length;i++){ for(var i=0;i<selectors.length;i++){
var el=document.querySelector(selectors[i]); var el=document.querySelector(selectors[i]);
@@ -419,9 +422,9 @@ export function generateApprovalObserverScript(_port: number): string {
var matchedType=null; var matchedType=null;
for(var p=0;p<PATS.length;p++){ for(var p=0;p<PATS.length;p++){
if(PATS[p].re.test(txt)){ if(PATS[p].re.test(txt)){
// STRUCTURAL CONSTRAINT: If we are scanning the main VS Code Editor body, reject Agent/Terminal buttons // STRUCTURAL CONSTRAINT: To prevent freezing on CodeLens 'Run' or 'Accept' false positives within editor files,
// to prevent freezing on CodeLens 'Run' or 'Accept' false positives. // ignore these if found inside a CodeLens container.
if (isVSCodeMainWindow && isBodyRoot && PATS[p].type !== 'diff_review' && PATS[p].type !== 'permission') { if (b.closest('.codelens-decoration') && PATS[p].type !== 'diff_review' && PATS[p].type !== 'permission') {
continue; continue;
} }
// Prevent duplicates if already scanned via panel root // Prevent duplicates if already scanned via panel root
@@ -651,7 +654,7 @@ export function generateApprovalObserverScript(_port: number): string {
if(!d.action)return; if(!d.action)return;
log('🔔 TRIGGER-CLICK received: action='+d.action); log('🔔 TRIGGER-CLICK received: action='+d.action);
var approveRe=[/^Run/i,/^Accept/i,/^Accept all/i,/^(?:Always )?Allow/i,/^Approve/i,/^Continue$/i,/^Proceed$/i,/^Retry$/i]; var approveRe=[/^(?:Always\s*)?Run/i,/^(?:Always\s*)?Accept/i,/^(?:Always\s*)?Accept all/i,/^(?:Always\s*)?Allow/i,/^(?:Always\s*)?Approve/i,/^Continue$/i,/^Proceed$/i,/^Retry$/i];
var rejectRe=[/^Reject/i,/^Cancel$/i,/^Deny$/i,/^Stop$/i,/^Decline$/i,/^Dismiss$/i]; var rejectRe=[/^Reject/i,/^Cancel$/i,/^Deny$/i,/^Stop$/i,/^Decline$/i,/^Dismiss$/i];
var patterns=(d.action==='approve')?approveRe:rejectRe; var patterns=(d.action==='approve')?approveRe:rejectRe;
var emoji=(d.action==='approve')?'✅':'❌'; var emoji=(d.action==='approve')?'✅':'❌';

View File

@@ -1,4 +1,4 @@
/** /**
* Step Probe Module - SDK polling, response handling, approval strategies. * Step Probe Module - SDK polling, response handling, approval strategies.
* All shared state accessed through BridgeContext. * All shared state accessed through BridgeContext.
* Extracted from extension.ts. * Extracted from extension.ts.
@@ -591,10 +591,6 @@ function setupMonitor() {
source: 'step_probe', source: 'step_probe',
safe_to_auto_run: isSafeToAutoRun, safe_to_auto_run: isSafeToAutoRun,
}); });
if (isSafeToAutoRun) {
const truncatedCmd = command.length > 500 ? command.substring(0, 500) + '\n...(이하 생략)' : command;
ctx.writeChatSnapshot(`⚡ **자동 실행됨** (step ${si})\n\n\`\`\`\n${truncatedCmd}\n\`\`\``);
}
} }
} }
// NOTE: no break — process ALL parallel WAITING steps // NOTE: no break — process ALL parallel WAITING steps
@@ -667,10 +663,6 @@ function setupMonitor() {
source: 'step_probe_utf8_offset', source: 'step_probe_utf8_offset',
safe_to_auto_run: isSafeToAutoRun, safe_to_auto_run: isSafeToAutoRun,
}); });
if (isSafeToAutoRun) {
const truncatedCmd = command.length > 500 ? command.substring(0, 500) + '\n...(이하 생략)' : command;
ctx.writeChatSnapshot(`⚡ **자동 실행됨** (step ${actualIndex})\n\n\`\`\`\n${truncatedCmd}\n\`\`\``);
}
} }
} }
// NOTE: no break — process ALL parallel WAITING steps // NOTE: no break — process ALL parallel WAITING steps

1
inspect-dump.json Normal file
View File

@@ -0,0 +1 @@
{"status":"timeout","message":"Renderer did not respond in 10s. Is the v3 script loaded?"}

50
scratch_parse.py Normal file
View File

@@ -0,0 +1,50 @@
import json
try:
with open(r'C:\Users\Variet-Worker\.gemini\antigravity\bridge\deep-inspect-result.json', 'r', encoding='utf-8', errors='ignore') as f:
data = json.load(f)
# Print first 50 buttons from nodes
count = 0
print('=== First 100 Buttons in DOM Nodes ===')
for node in data.get('nodes', []):
label = node.get('label', 'unknown')
btns = node.get('buttons', [])
if btns:
print(f'\n[Node] {label}')
for b in btns:
t = b.get('text', '').replace('\n', ' ').strip()
hidden = b.get("hidden")
cls = b.get("class")
if t:
print(f' - "{t[:50]}" (Hidden: {hidden}, Class: {cls[:30]})')
count += 1
if count > 100:
break
if count > 100:
break
# Print first 50 buttons from webviews
count = 0
print('\n=== First 100 Buttons in Webviews ===')
for probe in data.get('webviewProbes', []):
if probe.get('success'):
pd = probe.get('data', {})
btns = pd.get('buttons', [])
label = pd.get('title', 'Unknown Title') + f" (URL: {pd.get('url', 'Unknown URL')})"
if btns:
print(f'\n[WebviewProbe {probe.get("index")}] {label}')
for b in btns:
t = b.get('text', '').replace('\n', ' ').strip()
hidden = b.get("hidden")
cls = b.get("class")
if t:
print(f' - "{t[:50]}" (Hidden: {hidden}, Class: {cls[:30]})')
count += 1
if count > 100:
break
if count > 100:
break
except Exception as e:
print(f'Error reading JSON: {e}')

27
scratch_parse2.py Normal file
View File

@@ -0,0 +1,27 @@
import json
try:
with open(r'C:\Users\Variet-Worker\.gemini\antigravity\bridge\deep-inspect-result.json', 'r', encoding='utf-8', errors='ignore') as f:
data = json.load(f)
print(f"Total Nodes: {len(data.get('nodes', []))}")
for node in data.get('nodes', []):
label = node.get('label', 'unknown')
iframes = node.get('iframes', [])
webviews = node.get('webviews', [])
buttons = node.get('buttons', [])
print(f"[Node] {label}")
print(f" URL: {node.get('url', '')[:50]}")
print(f" Total Elements: {node.get('totalElements', 0)}")
print(f" Buttons count: {len(buttons)}")
print(f" Iframes count: {len(iframes)}")
print(f" Webviews count: {len(webviews)}")
if iframes:
for iframe in iframes:
print(f" - iframe[{iframe.get('index')}]: accessible={iframe.get('accessible')} src={iframe.get('src', '')[:50]}")
if webviews:
for w in webviews:
print(f" - webview[{w.get('index')}]: src={w.get('src', '')[:50]}")
except Exception as e:
print(f'Error reading JSON: {e}')

20
scratch_parse3.py Normal file
View File

@@ -0,0 +1,20 @@
import json
try:
with open(r'C:\Users\Variet-Worker\.gemini\antigravity\bridge\deep-inspect-result.json', 'r', encoding='utf-8', errors='ignore') as f:
data = json.load(f)
print("SEARCHING FOR CHAT TEXT IN DUMP...")
found = False
with open(r'C:\Users\Variet-Worker\.gemini\antigravity\bridge\deep-inspect-result.json', 'r', encoding='utf-8', errors='ignore') as f:
raw_text = f.read()
if '스스로 만들고 스스로' in raw_text:
print("YES! The chat text IS in the raw JSON dump!")
found = True
elif 'Variet' in raw_text:
print("Found Variet in dump.")
else:
print("Chat text not found in raw dump.")
except Exception as e:
print(f'Error reading JSON: {e}')

18
scratch_parse4.py Normal file
View File

@@ -0,0 +1,18 @@
import json
try:
with open(r'C:\Users\Variet-Worker\.gemini\antigravity\bridge\deep-inspect-result.json', 'r', encoding='utf-8', errors='ignore') as f:
data = json.load(f)
for node in data.get('nodes', []):
label = node.get('label', 'unknown')
btns = node.get('buttons', [])
print(f"\n[Node] {label} (Total btns: {len(btns)})")
for i, b in enumerate(btns):
t = b.get('text', '').replace('\n', ' ').strip()
hidden = b.get("hidden")
cls = b.get("class")
print(f" {i:3d}: \"{t[:50]}\" (Hidden: {hidden}, Class: {cls[:30]})")
except Exception as e:
print(f'Error reading JSON: {e}')

1
test.txt Normal file
View File

@@ -0,0 +1 @@
world

18
trigger_capture.py Normal file
View File

@@ -0,0 +1,18 @@
import urllib.request, time
print("Waiting 3 seconds for UI to render...")
time.sleep(3)
name = 'gravity_control'
h = 0
for c in name:
h = ((h << 5) - h + ord(c)) & 0xFFFFFFFF
if h > 0x7FFFFFFF: h -= 0x100000000
port = 10000 + (abs(h) % 50000)
print(f"Triggering deep-inspect on port {port}...")
try:
urllib.request.urlopen(urllib.request.Request(f'http://127.0.0.1:{port}/deep-inspect'), timeout=12)
print("Trigger signal sent successfully")
except Exception as e:
print("Trigger failed:", e)