// 안티그래비티 Orca 상태 및 EST 5175 헬스 백그라운드 감시 스크립트 // 상태 변경 시에만 기록하며, 안티그래비티 완료/대기 또는 서버 장애 시 Codex Orchestration Run으로 알림 전송. // 자동 재시작이나 코드 수정 지시는 일절 포함하지 않음. const http = require("node:http"); const { execFile } = require("node:child_process"); const fs = require("node:fs"); const path = require("node:path"); const TARGET_RUN = "run_cdb1558b9280"; const logFile = path.join(__dirname, "monitor.log"); const pidFile = path.join(__dirname, "monitor.pid"); const MAX_LOG_BYTES = 512 * 1024; // 512KB 순환 // 중복 실행 방지 if (fs.existsSync(pidFile)) { try { const existingPid = Number(fs.readFileSync(pidFile, "utf8").trim()); process.kill(existingPid, 0); console.log(`[monitor] 이미 감시 프로세스가 실행 중입니다 (PID: ${existingPid}). 종료합니다.`); process.exit(0); } catch {} } fs.writeFileSync(pidFile, String(process.pid), "utf8"); function cleanup() { try { if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile); } catch {} process.exit(0); } process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); process.on("exit", () => { try { if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile); } catch {} }); function log(message) { const line = `[${new Date().toISOString()}] ${message}\n`; try { if (fs.existsSync(logFile) && fs.statSync(logFile).size > MAX_LOG_BYTES) { const old = fs.readFileSync(logFile, "utf8"); fs.writeFileSync(logFile, old.slice(-MAX_LOG_BYTES / 2), "utf8"); } fs.appendFileSync(logFile, line, "utf8"); } catch {} } log(`감시 시작 (PID: ${process.pid})`); let lastAntigravityState = null; let lastHealthStatus = null; let notifiedDone = false; let notifiedFailure = false; const ORCA_BIN = "C:\\Users\\umsan\\AppData\\Local\\Programs\\orca\\resources\\bin\\orca.exe"; function sendOrchestrationNotification(subject, body, type = "status") { // 민감한 환경변수 노출 방지 및 타임아웃 5초 설정 (shell: false로 공백/인자 안정성 확보) execFile( ORCA_BIN, [ "orchestration", "send", "--to", `run:${TARGET_RUN}`, "--type", type, "--subject", subject, "--body", body, "--json", ], { timeout: 5000, shell: false }, (error, stdout, stderr) => { if (error) { log(`Orchestration 알림 실패: ${error.message}`); return; } try { const res = JSON.parse(stdout); if (res.ok) { log(`Orchestration 알림 성공: messageId=${res.result?.messageId}`); } else { log(`Orchestration 알림 거부: ${JSON.stringify(res.error || res)}`); } } catch (e) { log(`Orchestration 응답 파싱 오류: ${stdout}`); } }, ); } function checkOrcaAntigravity() { execFile( ORCA_BIN, ["worktree", "ps", "--json"], { timeout: 4000, maxBuffer: 1024 * 1024, shell: false }, (error, stdout) => { if (error) { if (lastAntigravityState !== `error:${error.message}`) { lastAntigravityState = `error:${error.message}`; log(`Orca 확인 실패: ${error.message}`); } return; } try { const data = JSON.parse(stdout); const codexWorktree = data.result?.worktrees?.find((w) => /D:\/aislo-wt\/CODEX/i.test(w.path || ""), ); const antigravityAgent = codexWorktree?.agents?.find((a) => a.agentType === "antigravity"); const currentState = antigravityAgent?.state || codexWorktree?.status || "unknown"; if (currentState !== lastAntigravityState) { const prevState = lastAntigravityState; lastAntigravityState = currentState; log(`안티그래비티 작업 상태 변경: ${prevState} -> ${currentState}`); // 안티그래비티 완료/대기 전환 시 단발성 자동 알림 if ((currentState === "done" || currentState === "idle") && !notifiedDone) { notifiedDone = true; sendOrchestrationNotification( "안티그래비티 작업 완료 보고", `안티그래비티 상태가 ${currentState}로 전이되었습니다. 검토를 재개할 수 있습니다.`, "status", ); } else if (currentState === "working") { notifiedDone = false; // 다시 작업 중이 되면 플래그 리셋 } } } catch (parseErr) { if (lastAntigravityState !== `parse-error:${parseErr.message}`) { lastAntigravityState = `parse-error:${parseErr.message}`; log(`Orca JSON 파싱 오류: ${parseErr.message}`); } } }, ); } function checkServerHealth() { const req = http.get("http://100.65.141.15:5175/api/health", { timeout: 3000 }, (res) => { let data = ""; res.on("data", (chunk) => (data += chunk)); res.on("end", () => { const statusStr = `HTTP ${res.statusCode}`; if (statusStr !== lastHealthStatus) { lastHealthStatus = statusStr; log(`EST 5175 서버 상태 변경: ${statusStr} (${data.trim()})`); } if (res.statusCode === 200) { notifiedFailure = false; } else if (!notifiedFailure) { notifiedFailure = true; sendOrchestrationNotification( "EST 5175 서버 장애 경고", `EST 독립 서버 응답이 비정상입니다 (${statusStr}). 확인이 필요합니다.`, "status", ); } }); }); req.on("error", (err) => { const errStr = `error:${err.message}`; if (errStr !== lastHealthStatus) { lastHealthStatus = errStr; log(`EST 5175 접속 실패: ${err.message}`); } if (!notifiedFailure) { notifiedFailure = true; sendOrchestrationNotification( "EST 5175 서버 접속 불가", `EST 독립 서버 연결에 실패했습니다 (${err.message}).`, "status", ); } }); req.on("timeout", () => { req.destroy(); const timeoutStr = "timeout"; if (timeoutStr !== lastHealthStatus) { lastHealthStatus = timeoutStr; log("EST 5175 응답 시간 초과"); } if (!notifiedFailure) { notifiedFailure = true; sendOrchestrationNotification( "EST 5175 서버 응답 시간 초과", "EST 독립 서버 헬스 체크 응답 시간이 초과되었습니다.", "status", ); } }); } function poll() { checkOrcaAntigravity(); checkServerHealth(); } poll(); setInterval(poll, 15000);