diff --git a/.gitignore b/.gitignore index 180530c..7598219 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,4 @@ nerve.pid nerve.sock false/ *.db - -# build output -senses/*/index.js -workflows/*/dist/ +dist/ diff --git a/scripts/build.mjs b/scripts/build.mjs index 82079e9..61bef53 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -4,6 +4,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); +const dist = path.join(root, "dist"); const opts = { bundle: true, @@ -16,34 +17,29 @@ function listDirs(dir) { if (!fs.existsSync(dir)) return []; return fs .readdirSync(dir) - .filter((name) => !name.startsWith(".")) - .map((name) => path.join(dir, name)) - .filter((p) => fs.statSync(p).isDirectory()); + .filter((name) => !name.startsWith(".") && !name.startsWith("_")) + .map((name) => ({ name, full: path.join(dir, name) })) + .filter(({ full }) => fs.statSync(full).isDirectory()); } async function main() { - for (const dir of listDirs(path.join(root, "senses"))) { - const entry = path.join(dir, "src", "index.ts"); + // Clean dist/ + fs.rmSync(dist, { recursive: true, force: true }); + + for (const { name, full } of listDirs(path.join(root, "senses"))) { + const entry = path.join(full, "src", "index.ts"); if (!fs.existsSync(entry)) continue; - await esbuild.build({ - ...opts, - entryPoints: [entry], - outfile: path.join(dir, "index.js"), - }); + const outfile = path.join(dist, "senses", name, "index.js"); + fs.mkdirSync(path.dirname(outfile), { recursive: true }); + await esbuild.build({ ...opts, entryPoints: [entry], outfile }); } - for (const dir of listDirs(path.join(root, "workflows"))) { - const base = path.basename(dir); - if (base.startsWith("_")) continue; - const entry = path.join(dir, "index.ts"); + for (const { name, full } of listDirs(path.join(root, "workflows"))) { + const entry = path.join(full, "index.ts"); if (!fs.existsSync(entry)) continue; - const outDir = path.join(dir, "dist"); - fs.mkdirSync(outDir, { recursive: true }); - await esbuild.build({ - ...opts, - entryPoints: [entry], - outfile: path.join(outDir, "index.js"), - }); + const outfile = path.join(dist, "workflows", name, "index.js"); + fs.mkdirSync(path.dirname(outfile), { recursive: true }); + await esbuild.build({ ...opts, entryPoints: [entry], outfile }); } } diff --git a/senses/git-workspace-status/index.js b/senses/git-workspace-status/index.js new file mode 100644 index 0000000..cc60d6a --- /dev/null +++ b/senses/git-workspace-status/index.js @@ -0,0 +1,85 @@ +// senses/git-workspace-status/src/index.ts +import { execFileSync } from "node:child_process"; +import { resolve } from "node:path"; + +// senses/git-workspace-status/src/schema.ts +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; +var snapshots = sqliteTable("snapshots", { + ts: integer("ts").primaryKey(), + branch: text("branch").notNull(), + headShort: text("head_short").notNull(), + porcelainLines: integer("porcelain_lines").notNull(), + hasUpstream: integer("has_upstream").notNull(), + aheadCount: integer("ahead_count").notNull(), + behindCount: integer("behind_count").notNull(), + /** Empty string when the snapshot succeeded; otherwise a short error summary. */ + gitError: text("git_error").notNull() +}); + +// senses/git-workspace-status/src/index.ts +var GIT_TIMEOUT_MS = 15e3; +function workspaceRoot() { + const raw = process.env.GIT_WORKSPACE_ROOT; + return raw ? resolve(raw) : resolve(process.cwd()); +} +function gitErrorMessage(err) { + if (err instanceof Error) { + const m = err.message.trim(); + return m.length > 200 ? `${m.slice(0, 197)}...` : m; + } + return String(err); +} +function runGit(cwd, args) { + return execFileSync("git", args, { + cwd, + encoding: "utf8", + timeout: GIT_TIMEOUT_MS, + maxBuffer: 2 * 1024 * 1024 + }).trimEnd(); +} +function countPorcelainLines(output) { + if (!output) return 0; + return output.split("\n").filter((line) => line.length > 0).length; +} +async function compute() { + const root = workspaceRoot(); + const ts = Date.now(); + let branch = ""; + let headShort = ""; + let porcelainLines = 0; + let hasUpstream = 0; + let aheadCount = 0; + let behindCount = 0; + let gitError = ""; + try { + const inside = runGit(root, ["rev-parse", "--is-inside-work-tree"]).trim(); + if (inside !== "true") { + gitError = "not a git work tree"; + return { signal: { ts, branch, headShort, porcelainLines, hasUpstream, aheadCount, behindCount, gitError }, workflow: null }; + } + branch = runGit(root, ["rev-parse", "--abbrev-ref", "HEAD"]); + headShort = runGit(root, ["rev-parse", "--short", "HEAD"]); + porcelainLines = countPorcelainLines(runGit(root, ["status", "--porcelain"])); + try { + runGit(root, ["rev-parse", "--abbrev-ref", "@{upstream}"]); + hasUpstream = 1; + const lb = runGit(root, ["rev-list", "--left-right", "--count", "HEAD...@{upstream}"]); + const parts = lb.split(/[\t\s]+/).filter(Boolean); + if (parts.length >= 2) { + aheadCount = Number.parseInt(parts[0], 10) || 0; + behindCount = Number.parseInt(parts[1], 10) || 0; + } + } catch { + hasUpstream = 0; + aheadCount = 0; + behindCount = 0; + } + } catch (e) { + gitError = gitErrorMessage(e); + } + return { signal: { ts, branch, headShort, porcelainLines, hasUpstream, aheadCount, behindCount, gitError }, workflow: null }; +} +export { + compute, + snapshots as table +}; diff --git a/senses/hermes-gateway-health/index.js b/senses/hermes-gateway-health/index.js new file mode 100644 index 0000000..995943b --- /dev/null +++ b/senses/hermes-gateway-health/index.js @@ -0,0 +1,361 @@ +// senses/hermes-gateway-health/src/index.ts +import { execFile } from "node:child_process"; + +// senses/hermes-gateway-health/src/schema.ts +import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core"; +var hermesGatewayHealth = sqliteTable("hermes_gateway_health", { + id: integer("id").primaryKey({ autoIncrement: true }), + ts: integer("ts").notNull(), + alive: integer("alive").notNull(), + mainPid: integer("main_pid").notNull(), + rssBytes: integer("rss_bytes").notNull(), + cpuPercent: real("cpu_percent").notNull(), + uptimeSec: integer("uptime_sec").notNull(), + activeSessions: integer("active_sessions").notNull(), + childProcessCount: integer("child_process_count").notNull(), + httpOk: integer("http_ok").notNull(), + httpStatusCode: integer("http_status_code").notNull(), + httpLatencyMs: integer("http_latency_ms").notNull(), + httpError: text("http_error").notNull() +}); + +// senses/hermes-gateway-health/src/index.ts +var EXEC_TIMEOUT_MS = 25e3; +var HTTP_TIMEOUT_MS = Math.min(23e3, EXEC_TIMEOUT_MS - 2e3); +var HTTP_ERROR_MAX_LEN = 256; +function gatewayProbeUrl() { + const u = process.env.HERMES_GATEWAY_HEALTH_URL ?? process.env.NERVE_HERMES_GATEWAY_URL ?? ""; + return String(u).trim(); +} +function truncateHttpError(err) { + const raw = err && typeof err === "object" && "code" in err && err.code ? String(err.code) : String(err?.message ?? err ?? "error"); + const s = raw.trim() || "error"; + return s.length > HTTP_ERROR_MAX_LEN ? s.slice(0, HTTP_ERROR_MAX_LEN) : s; +} +async function probeGatewayHttp(url) { + if (!url) { + return { + httpOk: 0, + httpStatusCode: 0, + httpLatencyMs: 0, + httpError: "missing_url" + }; + } + const t0 = Date.now(); + try { + const signal = AbortSignal.timeout(HTTP_TIMEOUT_MS); + const res = await fetch(url, { + method: "GET", + signal, + redirect: "follow" + }); + const httpLatencyMs = Date.now() - t0; + const code = res.status; + const ok = code >= 200 && code < 400; + return { + httpOk: ok ? 1 : 0, + httpStatusCode: code, + httpLatencyMs, + httpError: ok ? "" : truncateHttpError({ message: `HTTP ${code}` }) + }; + } catch (err) { + const httpLatencyMs = Date.now() - t0; + return { + httpOk: 0, + httpStatusCode: 0, + httpLatencyMs, + httpError: truncateHttpError(err) + }; + } +} +function etimeToSeconds(etime) { + let s = String(etime).trim(); + if (!s) return 0; + let days = 0; + if (s.includes("-")) { + const idx = s.indexOf("-"); + const d = Number.parseInt(s.slice(0, idx), 10); + days = Number.isFinite(d) ? d : 0; + s = s.slice(idx + 1); + } + const parts = s.split(":").map((x) => Number.parseInt(String(x).trim(), 10)); + if (parts.some((n) => !Number.isFinite(n))) return 0; + if (parts.length === 3) { + return Math.trunc(days * 86400 + parts[0] * 3600 + parts[1] * 60 + parts[2]); + } + if (parts.length === 2) { + return Math.trunc(days * 86400 + parts[0] * 60 + parts[1]); + } + if (parts.length === 1) { + return Math.trunc(days * 86400 + parts[0]); + } + return 0; +} +function execFileUtf8(file, args, opts = {}) { + return new Promise((resolve) => { + execFile( + file, + args, + { + encoding: "utf8", + maxBuffer: 8 * 1024 * 1024, + timeout: EXEC_TIMEOUT_MS, + ...opts + }, + (err, stdout, stderr) => { + const exitCode = err && typeof err.status === "number" ? err.status : err ? -1 : 0; + resolve({ + exitCode, + errCode: err?.code, + stdout: String(stdout ?? ""), + stderr: String(stderr ?? "") + }); + } + ); + }); +} +function parseMainPidFromStatus(text2) { + const m = text2.match(/Main PID:\s*(\d+)/i); + return m ? Math.trunc(Number.parseInt(m[1], 10)) || 0 : 0; +} +function parseActiveLineFromStatus(text2) { + for (const line of text2.split("\n")) { + if (/^\s*Active:/i.test(line)) { + const m = line.match(/Active:\s*(\S+)\s*\(([^)]*)\)/i); + if (m) { + return { + active: m[1].toLowerCase() === "active", + subRunning: m[2].toLowerCase().includes("running") + }; + } + } + } + return { active: false, subRunning: false }; +} +function parseSystemctlShow(text2) { + let mainPid = 0; + let active = false; + let subRunning = false; + for (const line of text2.split("\n")) { + const t = line.trim(); + if (t.startsWith("MainPID=")) { + mainPid = Math.trunc(Number.parseInt(t.slice("MainPID=".length), 10)) || 0; + } else if (t.startsWith("ActiveState=")) { + active = t.slice("ActiveState=".length).trim().toLowerCase() === "active"; + } else if (t.startsWith("SubState=")) { + subRunning = t.slice("SubState=".length).trim().toLowerCase() === "running"; + } + } + return { mainPid, active, subRunning }; +} +async function readSystemdState() { + const status = await execFileUtf8("systemctl", [ + "--user", + "--no-pager", + "status", + "hermes-gateway" + ]); + const combined = `${status.stdout} +${status.stderr}`.trim(); + let mainPid = parseMainPidFromStatus(combined); + let { active, subRunning } = parseActiveLineFromStatus(combined); + const needShow = mainPid <= 0 || !active || !subRunning; + if (needShow) { + const show = await execFileUtf8("systemctl", [ + "--user", + "--no-pager", + "show", + "hermes-gateway", + "-p", + "MainPID", + "-p", + "ActiveState", + "-p", + "SubState" + ]); + const showText = `${show.stdout} +${show.stderr}`; + const s = parseSystemctlShow(showText); + if (mainPid <= 0 && s.mainPid > 0) mainPid = s.mainPid; + if (!active) active = s.active; + if (!subRunning) subRunning = s.subRunning; + } + return { mainPid, systemdActiveRunning: active && subRunning }; +} +async function processExists(mainPid) { + if (mainPid <= 0) return false; + const r = await execFileUtf8("ps", ["-p", String(mainPid), "-o", "pid="]); + if (r.errCode === "ENOENT") return false; + return r.stdout.trim().length > 0; +} +async function readPsMetrics(mainPid) { + if (mainPid <= 0) { + return { rssBytes: 0, cpuPercent: 0, uptimeSec: 0 }; + } + let r = await execFileUtf8("ps", [ + "-p", + String(mainPid), + "-o", + "rss=,%cpu=,etimes=" + ]); + let line = r.stdout.trim().replace(/\s+/g, " "); + if (r.errCode === "ENOENT" || !line) { + return { rssBytes: 0, cpuPercent: 0, uptimeSec: 0 }; + } + let parts = line.split(" ").filter(Boolean); + if (parts.length < 3) { + r = await execFileUtf8("ps", [ + "-p", + String(mainPid), + "-o", + "rss=,%cpu=,etime=" + ]); + line = r.stdout.trim().replace(/\s+/g, " "); + parts = line.split(" ").filter(Boolean); + if (parts.length < 3) { + return { rssBytes: 0, cpuPercent: 0, uptimeSec: 0 }; + } + const rssKiB2 = Number(parts[0]); + const cpu2 = Number(parts[1]); + const uptimeSec2 = etimeToSeconds(parts.slice(2).join(" ")); + const rssBytes2 = Number.isFinite(rssKiB2) ? Math.trunc(rssKiB2 * 1024) : 0; + const cpuPercent2 = Number.isFinite(cpu2) ? Math.round(cpu2 * 100) / 100 : 0; + return { rssBytes: rssBytes2, cpuPercent: cpuPercent2, uptimeSec: uptimeSec2 }; + } + const rssKiB = Number(parts[0]); + const cpu = Number(parts[1]); + const etimes = Number(parts[2]); + const rssBytes = Number.isFinite(rssKiB) ? Math.trunc(rssKiB * 1024) : 0; + const cpuPercent = Number.isFinite(cpu) ? Math.round(cpu * 100) / 100 : 0; + const uptimeSec = Number.isFinite(etimes) ? Math.trunc(etimes) : 0; + return { rssBytes, cpuPercent, uptimeSec }; +} +function parseActiveSessionsFromHermesStats(text2) { + const src = String(text2); + const patterns = [ + /^\s*Active\s+sessions?:\s*(\d+)/gim, + /^\s*active\s+sessions?:\s*(\d+)/gim, + /^\s*Total\s+sessions?:\s*(\d+)/gim + ]; + for (const re of patterns) { + re.lastIndex = 0; + const m = re.exec(src); + if (m) { + const n = Math.trunc(Number.parseInt(m[1], 10)); + return Number.isFinite(n) ? n : 0; + } + } + return 0; +} +async function readActiveSessions() { + try { + const r = await execFileUtf8("hermes", ["sessions", "stats"]); + if (r.errCode === "ENOENT") return 0; + return parseActiveSessionsFromHermesStats(`${r.stdout} +${r.stderr}`); + } catch { + return 0; + } +} +async function countDirectChildren(mainPid) { + if (mainPid <= 0) return 0; + try { + const r = await execFileUtf8("ps", [ + "--no-headers", + "-o", + "pid", + "--ppid", + String(mainPid) + ]); + if (r.errCode === "ENOENT") return 0; + const lines = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean); + return lines.length; + } catch { + return 0; + } +} +async function compute() { + const ts = Date.now(); + let mainPid = 0; + let systemdActiveRunning = false; + try { + const st = await readSystemdState(); + mainPid = st.mainPid; + systemdActiveRunning = st.systemdActiveRunning; + } catch { + mainPid = 0; + systemdActiveRunning = false; + } + let psOk = false; + try { + psOk = await processExists(mainPid); + } catch { + psOk = false; + } + let rssBytes = 0; + let cpuPercent = 0; + let uptimeSec = 0; + if (psOk) { + try { + const m = await readPsMetrics(mainPid); + rssBytes = m.rssBytes; + cpuPercent = m.cpuPercent; + uptimeSec = m.uptimeSec; + } catch { + rssBytes = 0; + cpuPercent = 0; + uptimeSec = 0; + } + } + const alive = systemdActiveRunning && mainPid > 0 && psOk ? 1 : 0; + let activeSessions = 0; + try { + activeSessions = await readActiveSessions(); + } catch { + activeSessions = 0; + } + let childProcessCount = 0; + if (alive && mainPid > 0) { + try { + childProcessCount = await countDirectChildren(mainPid); + } catch { + childProcessCount = 0; + } + } + let httpOk = 0; + let httpStatusCode = 0; + let httpLatencyMs = 0; + let httpError = ""; + try { + const h = await probeGatewayHttp(gatewayProbeUrl()); + httpOk = h.httpOk; + httpStatusCode = h.httpStatusCode; + httpLatencyMs = h.httpLatencyMs; + httpError = h.httpError; + } catch { + httpOk = 0; + httpStatusCode = 0; + httpLatencyMs = 0; + httpError = "probe_failed"; + } + const storedMainPid = mainPid > 0 ? mainPid : 0; + const row = { + ts, + alive, + mainPid: storedMainPid, + rssBytes: alive ? rssBytes : 0, + cpuPercent: alive ? cpuPercent : 0, + uptimeSec: alive ? uptimeSec : 0, + activeSessions, + childProcessCount: alive ? childProcessCount : 0, + httpOk, + httpStatusCode, + httpLatencyMs, + httpError + }; + return { signal: row, workflow: null }; +} +export { + compute, + hermesGatewayHealth as table +}; diff --git a/senses/hermes-session-message-stats/index.js b/senses/hermes-session-message-stats/index.js new file mode 100644 index 0000000..0c15a34 --- /dev/null +++ b/senses/hermes-session-message-stats/index.js @@ -0,0 +1,110 @@ +// senses/hermes-session-message-stats/src/index.ts +import { createReadStream } from "node:fs"; +import { readdir } from "node:fs/promises"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { createInterface } from "node:readline"; + +// senses/hermes-session-message-stats/src/schema.ts +import { integer, sqliteTable } from "drizzle-orm/sqlite-core"; +var hermesSessionMessageStats = sqliteTable("hermes_session_message_stats", { + id: integer("id").primaryKey({ autoIncrement: true }), + ts: integer("ts").notNull(), + totalUserMessages: integer("total_user_messages").notNull(), + totalAssistantMessages: integer("total_assistant_messages").notNull(), + totalToolMessages: integer("total_tool_messages").notNull(), + totalMessages: integer("total_messages").notNull(), + activeSessions: integer("active_sessions").notNull(), + measurementWindowSeconds: integer("measurement_window_seconds").notNull() +}); + +// senses/hermes-session-message-stats/src/index.ts +var MEASUREMENT_WINDOW_MS = 9e5; +var MEASUREMENT_WINDOW_SECONDS = 900; +async function aggregateJsonlFile(filePath, cutoffMs, nowMs) { + let user = 0; + let assistant = 0; + let tool = 0; + let fileHadActivity = false; + const input = createReadStream(filePath, { encoding: "utf8" }); + const rl = createInterface({ input, crlfDelay: Infinity }); + try { + for await (const line of rl) { + const trimmed = line.trim(); + if (!trimmed) continue; + let obj; + try { + obj = JSON.parse(trimmed); + } catch { + continue; + } + if (typeof obj !== "object" || obj === null || typeof obj.role !== "string" || typeof obj.timestamp !== "string") { + continue; + } + const record = obj; + const t = Date.parse(record.timestamp); + if (!Number.isFinite(t) || t < cutoffMs || t > nowMs) continue; + const roleNorm = record.role.trim().toLowerCase(); + if (roleNorm === "user") { + user++; + fileHadActivity = true; + } else if (roleNorm === "assistant") { + assistant++; + fileHadActivity = true; + } else if (roleNorm === "tool") { + tool++; + fileHadActivity = true; + } + } + } finally { + rl.close(); + } + return { user, assistant, tool, fileHadActivity }; +} +async function compute() { + const nowMs = Date.now(); + const cutoffMs = nowMs - MEASUREMENT_WINDOW_MS; + const ts = nowMs; + let totalUserMessages = 0; + let totalAssistantMessages = 0; + let totalToolMessages = 0; + let activeSessions = 0; + const sessionsDir = join(homedir(), ".hermes", "sessions"); + let files = []; + try { + const entries = await readdir(sessionsDir, { withFileTypes: true }); + files = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => join(sessionsDir, e.name)); + } catch (err) { + if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") { + files = []; + } else { + throw err; + } + } + for (const filePath of files) { + const { user, assistant, tool, fileHadActivity } = await aggregateJsonlFile( + filePath, + cutoffMs, + nowMs + ); + totalUserMessages += user; + totalAssistantMessages += assistant; + totalToolMessages += tool; + if (fileHadActivity) activeSessions++; + } + const totalMessages = totalUserMessages + totalAssistantMessages + totalToolMessages; + const row = { + ts, + totalUserMessages, + totalAssistantMessages, + totalToolMessages, + totalMessages, + activeSessions, + measurementWindowSeconds: MEASUREMENT_WINDOW_SECONDS + }; + return { signal: row, workflow: null }; +} +export { + compute, + hermesSessionMessageStats as table +}; diff --git a/senses/linux-system-health/index.js b/senses/linux-system-health/index.js new file mode 100644 index 0000000..ae44f58 --- /dev/null +++ b/senses/linux-system-health/index.js @@ -0,0 +1,107 @@ +// senses/linux-system-health/src/index.ts +import { loadavg, totalmem, freemem, uptime } from "node:os"; +import { execSync } from "node:child_process"; +import { readFile } from "node:fs/promises"; + +// senses/linux-system-health/src/schema.ts +import { integer, real, sqliteTable } from "drizzle-orm/sqlite-core"; +var snapshots = sqliteTable("snapshots", { + ts: integer("ts").primaryKey(), + cpuLoad1m: real("cpu_load_1m").notNull(), + cpuLoad5m: real("cpu_load_5m").notNull(), + cpuLoad15m: real("cpu_load_15m").notNull(), + memTotalMB: integer("mem_total_mb").notNull(), + memUsedMB: integer("mem_used_mb").notNull(), + memUsedPct: real("mem_used_pct").notNull(), + diskTotalGB: real("disk_total_gb").notNull(), + diskUsedGB: real("disk_used_gb").notNull(), + diskUsedPct: real("disk_used_pct").notNull(), + uptimeSec: integer("uptime_sec").notNull(), + // TCP socket stats (merged from linux-tcp-socket-stats) + socketsUsed: integer("sockets_used"), + tcpInuse: integer("tcp_inuse"), + tcpOrphan: integer("tcp_orphan"), + tcpTw: integer("tcp_tw"), + tcpAlloc: integer("tcp_alloc"), + tcpMemPages: integer("tcp_mem_pages") +}); + +// senses/linux-system-health/src/index.ts +var SOCKSTAT_PATH = "/proc/net/sockstat"; +function parseSockstat(content) { + let socketsUsed = 0, tcpInuse = 0, tcpOrphan = 0, tcpTw = 0, tcpAlloc = 0, tcpMemPages = 0; + for (const line of content.split("\n")) { + const trimmed = line.trim(); + if (trimmed.startsWith("sockets:")) { + const parts = trimmed.split(/\s+/); + const idx = parts.indexOf("used"); + if (idx !== -1 && idx + 1 < parts.length) { + socketsUsed = Number.parseInt(parts[idx + 1], 10) || 0; + } + } else if (trimmed.startsWith("TCP:")) { + const parts = trimmed.split(/\s+/); + const map = {}; + for (let i = 1; i + 1 < parts.length; i += 2) { + map[parts[i]] = Number.parseInt(parts[i + 1], 10) || 0; + } + tcpInuse = map.inuse ?? 0; + tcpOrphan = map.orphan ?? 0; + tcpTw = map.tw ?? 0; + tcpAlloc = map.alloc ?? 0; + tcpMemPages = map.mem ?? 0; + } + } + return { socketsUsed, tcpInuse, tcpOrphan, tcpTw, tcpAlloc, tcpMemPages }; +} +async function compute() { + const [load1, load5, load15] = loadavg(); + const memTotal = totalmem(); + const memFree = freemem(); + const memUsed = memTotal - memFree; + const memTotalMB = Math.round(memTotal / 1024 / 1024); + const memUsedMB = Math.round(memUsed / 1024 / 1024); + const memUsedPct = Math.round(memUsed / memTotal * 1e4) / 100; + let diskTotalGB = 0, diskUsedGB = 0, diskUsedPct = 0; + try { + const df = execSync("df -B1 / | tail -1", { encoding: "utf-8" }).trim(); + const parts = df.split(/\s+/); + const total = Number(parts[1]); + const used = Number(parts[2]); + diskTotalGB = Math.round(total / 1024 / 1024 / 1024 * 100) / 100; + diskUsedGB = Math.round(used / 1024 / 1024 / 1024 * 100) / 100; + diskUsedPct = total > 0 ? Math.round(used / total * 1e4) / 100 : 0; + } catch { + } + let tcp = { socketsUsed: 0, tcpInuse: 0, tcpOrphan: 0, tcpTw: 0, tcpAlloc: 0, tcpMemPages: 0 }; + try { + const content = await readFile(SOCKSTAT_PATH, "utf8"); + tcp = parseSockstat(content); + } catch { + } + const ts = Date.now(); + const uptimeSec = Math.round(uptime()); + const data = { + ts, + cpuLoad1m: load1, + cpuLoad5m: load5, + cpuLoad15m: load15, + memTotalMB, + memUsedMB, + memUsedPct, + diskTotalGB, + diskUsedGB, + diskUsedPct, + uptimeSec, + socketsUsed: tcp.socketsUsed, + tcpInuse: tcp.tcpInuse, + tcpOrphan: tcp.tcpOrphan, + tcpTw: tcp.tcpTw, + tcpAlloc: tcp.tcpAlloc, + tcpMemPages: tcp.tcpMemPages + }; + return { signal: data, workflow: null }; +} +export { + compute, + snapshots as table +}; diff --git a/senses/worker-process-metrics/index.js b/senses/worker-process-metrics/index.js new file mode 100644 index 0000000..226d16c --- /dev/null +++ b/senses/worker-process-metrics/index.js @@ -0,0 +1,37 @@ +// senses/worker-process-metrics/src/schema.ts +import { integer, real, sqliteTable } from "drizzle-orm/sqlite-core"; +var workerProcessMetrics = sqliteTable("worker_process_metrics", { + ts: integer("ts").primaryKey(), + pid: integer("pid").notNull(), + uptimeSec: real("uptime_sec").notNull(), + heapUsedMB: real("heap_used_mb").notNull(), + rssMB: real("rss_mb").notNull(), + externalMB: real("external_mb").notNull() +}); + +// senses/worker-process-metrics/src/index.ts +function round2(n) { + return Math.round(n * 100) / 100; +} +async function compute() { + const ts = Date.now(); + const pid = process.pid; + const uptimeSec = process.uptime(); + const m = process.memoryUsage(); + const heapUsedMB = round2(m.heapUsed / 1024 / 1024); + const rssMB = round2(m.rss / 1024 / 1024); + const externalMB = round2(m.external / 1024 / 1024); + const row = { + ts, + pid, + uptimeSec, + heapUsedMB, + rssMB, + externalMB + }; + return { signal: row, workflow: null }; +} +export { + compute, + workerProcessMetrics as table +};