Files

210 lines
6.5 KiB
JavaScript

// 안티그래비티 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);