From 4cf10ad7bf8d294743602b3f595b3dae52886afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Tue, 28 Apr 2026 07:26:53 +0000 Subject: [PATCH] feat: migrate senses to TypeScript source + esbuild bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move index.js → src/index.ts with proper types for all 4 senses - Move schema.ts → src/schema.ts - Add package.json with esbuild build script per sense - Bundle to index.js at sense root (daemon loads this) - Update sense-generator coder prompt with TypeScript conventions Fixes #224 --- package.json | 2 +- senses/hermes-gateway-health/index.js | 179 +++----- senses/hermes-gateway-health/package.json | 17 + senses/hermes-gateway-health/pnpm-lock.yaml | 310 +++++++++++++ senses/hermes-gateway-health/src/index.ts | 424 ++++++++++++++++++ .../hermes-gateway-health/{ => src}/schema.ts | 0 senses/hermes-session-message-stats/index.js | 61 ++- .../hermes-session-message-stats/package.json | 17 + .../pnpm-lock.yaml | 310 +++++++++++++ .../hermes-session-message-stats/src/index.ts | 128 ++++++ .../{ => src}/schema.ts | 0 senses/linux-system-health/index.js | 69 ++- senses/linux-system-health/package.json | 17 + senses/linux-system-health/pnpm-lock.yaml | 310 +++++++++++++ senses/linux-system-health/src/index.ts | 96 ++++ .../linux-system-health/{ => src}/schema.ts | 0 senses/worker-process-metrics/index.js | 25 +- senses/worker-process-metrics/package.json | 17 + senses/worker-process-metrics/pnpm-lock.yaml | 310 +++++++++++++ senses/worker-process-metrics/src/index.ts | 36 ++ .../{ => src}/schema.ts | 0 .../sense-generator/roles/coder/prompt.ts | 38 +- 22 files changed, 2195 insertions(+), 171 deletions(-) create mode 100644 senses/hermes-gateway-health/package.json create mode 100644 senses/hermes-gateway-health/pnpm-lock.yaml create mode 100644 senses/hermes-gateway-health/src/index.ts rename senses/hermes-gateway-health/{ => src}/schema.ts (100%) create mode 100644 senses/hermes-session-message-stats/package.json create mode 100644 senses/hermes-session-message-stats/pnpm-lock.yaml create mode 100644 senses/hermes-session-message-stats/src/index.ts rename senses/hermes-session-message-stats/{ => src}/schema.ts (100%) create mode 100644 senses/linux-system-health/package.json create mode 100644 senses/linux-system-health/pnpm-lock.yaml create mode 100644 senses/linux-system-health/src/index.ts rename senses/linux-system-health/{ => src}/schema.ts (100%) create mode 100644 senses/worker-process-metrics/package.json create mode 100644 senses/worker-process-metrics/pnpm-lock.yaml create mode 100644 senses/worker-process-metrics/src/index.ts rename senses/worker-process-metrics/{ => src}/schema.ts (100%) diff --git a/package.json b/package.json index cb8067d..68b864d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dependencies": { "@uncaged/nerve-core": "latest", "@uncaged/nerve-daemon": "latest", - "@uncaged/nerve-workflow-utils": "latest", + "@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils", "drizzle-orm": "latest", "zod": "^4.3.6" }, diff --git a/senses/hermes-gateway-health/index.js b/senses/hermes-gateway-health/index.js index 36bf98b..d85bca8 100644 --- a/senses/hermes-gateway-health/index.js +++ b/senses/hermes-gateway-health/index.js @@ -1,42 +1,44 @@ +// src/index.ts import { execFile } from "node:child_process"; -import { hermesGatewayHealth } from "./schema.ts"; -/** Keep subprocess deadlines slightly under typical sense timeout (30s). */ -const EXEC_TIMEOUT_MS = 25_000; - -/** HTTP probe stays below EXEC_TIMEOUT_MS and sense timeout (30s). */ -const HTTP_TIMEOUT_MS = Math.min(23_000, EXEC_TIMEOUT_MS - 2000); - -const HTTP_ERROR_MAX_LEN = 256; +// 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() +}); +// 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 ?? - ""; + 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 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; } - -/** - * GET the gateway URL; success = HTTP 200–399. - * URL must be set via HERMES_GATEWAY_HEALTH_URL or NERVE_HERMES_GATEWAY_URL. - */ async function probeGatewayHttp(url) { if (!url) { return { httpOk: 0, httpStatusCode: 0, httpLatencyMs: 0, - httpError: "missing_url", + httpError: "missing_url" }; } const t0 = Date.now(); @@ -45,7 +47,7 @@ async function probeGatewayHttp(url) { const res = await fetch(url, { method: "GET", signal, - redirect: "follow", + redirect: "follow" }); const httpLatencyMs = Date.now() - t0; const code = res.status; @@ -54,7 +56,7 @@ async function probeGatewayHttp(url) { httpOk: ok ? 1 : 0, httpStatusCode: code, httpLatencyMs, - httpError: ok ? "" : truncateHttpError({ message: `HTTP ${code}` }), + httpError: ok ? "" : truncateHttpError({ message: `HTTP ${code}` }) }; } catch (err) { const httpLatencyMs = Date.now() - t0; @@ -62,15 +64,10 @@ async function probeGatewayHttp(url) { httpOk: 0, httpStatusCode: 0, httpLatencyMs, - httpError: truncateHttpError(err), + httpError: truncateHttpError(err) }; } } - -/** - * When `ps` lacks `etimes` (wall-clock seconds since start), parse `etime` - * ([[dd-]hh:]mm:ss) into seconds. See ps(1) `etime` field description. - */ function etimeToSeconds(etime) { let s = String(etime).trim(); if (!s) return 0; @@ -84,17 +81,16 @@ function etimeToSeconds(etime) { 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 * 86_400 + parts[0] * 3600 + parts[1] * 60 + parts[2]); + return Math.trunc(days * 86400 + parts[0] * 3600 + parts[1] * 60 + parts[2]); } if (parts.length === 2) { - return Math.trunc(days * 86_400 + parts[0] * 60 + parts[1]); + return Math.trunc(days * 86400 + parts[0] * 60 + parts[1]); } if (parts.length === 1) { - return Math.trunc(days * 86_400 + parts[0]); + return Math.trunc(days * 86400 + parts[0]); } return 0; } - function execFileUtf8(file, args, opts = {}) { return new Promise((resolve) => { execFile( @@ -104,47 +100,43 @@ function execFileUtf8(file, args, opts = {}) { encoding: "utf8", maxBuffer: 8 * 1024 * 1024, timeout: EXEC_TIMEOUT_MS, - ...opts, + ...opts }, (err, stdout, stderr) => { - const exitCode = - err && typeof err.status === "number" ? err.status : err ? -1 : 0; + const exitCode = err && typeof err.status === "number" ? err.status : err ? -1 : 0; resolve({ exitCode, errCode: err?.code, stdout: String(stdout ?? ""), - stderr: String(stderr ?? ""), + stderr: String(stderr ?? "") }); - }, + } ); }); } - -function parseMainPidFromStatus(text) { - const m = text.match(/Main PID:\s*(\d+)/i); +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(text) { - for (const line of text.split("\n")) { +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"), + subRunning: m[2].toLowerCase().includes("running") }; } } } return { active: false, subRunning: false }; } - -function parseSystemctlShow(text) { +function parseSystemctlShow(text2) { let mainPid = 0; let active = false; let subRunning = false; - for (const line of text.split("\n")) { + 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; @@ -156,21 +148,18 @@ function parseSystemctlShow(text) { } return { mainPid, active, subRunning }; } - async function readSystemdState() { const status = await execFileUtf8("systemctl", [ "--user", "--no-pager", "status", - "hermes-gateway", + "hermes-gateway" ]); - const combined = `${status.stdout}\n${status.stderr}`.trim(); + const combined = `${status.stdout} +${status.stderr}`.trim(); let mainPid = parseMainPidFromStatus(combined); let { active, subRunning } = parseActiveLineFromStatus(combined); - - const needShow = - mainPid <= 0 || !active || !subRunning; - + const needShow = mainPid <= 0 || !active || !subRunning; if (needShow) { const show = await execFileUtf8("systemctl", [ "--user", @@ -182,25 +171,23 @@ async function readSystemdState() { "-p", "ActiveState", "-p", - "SubState", + "SubState" ]); - const showText = `${show.stdout}\n${show.stderr}`; + 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 }; @@ -209,7 +196,7 @@ async function readPsMetrics(mainPid) { "-p", String(mainPid), "-o", - "rss=,%cpu=,etimes=", + "rss=,%cpu=,etimes=" ]); let line = r.stdout.trim().replace(/\s+/g, " "); if (r.errCode === "ENOENT" || !line) { @@ -221,41 +208,34 @@ async function readPsMetrics(mainPid) { "-p", String(mainPid), "-o", - "rss=,%cpu=,etime=", + "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 rssKiB = Number(parts[0]); - const cpu = Number(parts[1]); - const uptimeSec = etimeToSeconds(parts.slice(2).join(" ")); - const rssBytes = Number.isFinite(rssKiB) - ? Math.trunc(rssKiB * 1024) - : 0; - const cpuPercent = Number.isFinite(cpu) - ? Math.round(cpu * 100) / 100 - : 0; - return { rssBytes, cpuPercent, uptimeSec }; + 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; + const uptimeSec = Number.isFinite(etimes) ? Math.trunc(etimes) : 0; return { rssBytes, cpuPercent, uptimeSec }; } - -function parseActiveSessionsFromHermesStats(text) { - const src = String(text); +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, + /^\s*Total\s+sessions?:\s*(\d+)/gim ]; for (const re of patterns) { re.lastIndex = 0; @@ -267,17 +247,16 @@ function parseActiveSessionsFromHermesStats(text) { } return 0; } - async function readActiveSessions() { try { const r = await execFileUtf8("hermes", ["sessions", "stats"]); if (r.errCode === "ENOENT") return 0; - return parseActiveSessionsFromHermesStats(`${r.stdout}\n${r.stderr}`); + return parseActiveSessionsFromHermesStats(`${r.stdout} +${r.stderr}`); } catch { return 0; } } - async function countDirectChildren(mainPid) { if (mainPid <= 0) return 0; try { @@ -286,25 +265,19 @@ async function countDirectChildren(mainPid) { "-o", "pid", "--ppid", - String(mainPid), + String(mainPid) ]); if (r.errCode === "ENOENT") return 0; - const lines = r.stdout - .split("\n") - .map((l) => l.trim()) - .filter(Boolean); + const lines = r.stdout.split("\n").map((l) => l.trim()).filter(Boolean); return lines.length; } catch { return 0; } } - -export async function compute(db, _peers) { +async function compute(db, _peers) { const ts = Date.now(); - let mainPid = 0; let systemdActiveRunning = false; - try { const st = await readSystemdState(); mainPid = st.mainPid; @@ -313,14 +286,12 @@ export async function compute(db, _peers) { mainPid = 0; systemdActiveRunning = false; } - let psOk = false; try { psOk = await processExists(mainPid); } catch { psOk = false; } - let rssBytes = 0; let cpuPercent = 0; let uptimeSec = 0; @@ -336,17 +307,13 @@ export async function compute(db, _peers) { uptimeSec = 0; } } - - const alive = - systemdActiveRunning && mainPid > 0 && psOk ? 1 : 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 { @@ -355,7 +322,6 @@ export async function compute(db, _peers) { childProcessCount = 0; } } - let httpOk = 0; let httpStatusCode = 0; let httpLatencyMs = 0; @@ -372,9 +338,7 @@ export async function compute(db, _peers) { httpLatencyMs = 0; httpError = "probe_failed"; } - const storedMainPid = mainPid > 0 ? mainPid : 0; - const row = { ts, alive, @@ -387,11 +351,9 @@ export async function compute(db, _peers) { httpOk, httpStatusCode, httpLatencyMs, - httpError, + httpError }; - await db.insert(hermesGatewayHealth).values(row); - return { ts: row.ts, alive: row.alive, @@ -404,6 +366,9 @@ export async function compute(db, _peers) { httpOk: row.httpOk, httpStatusCode: row.httpStatusCode, httpLatencyMs: row.httpLatencyMs, - httpError: row.httpError, + httpError: row.httpError }; } +export { + compute +}; diff --git a/senses/hermes-gateway-health/package.json b/senses/hermes-gateway-health/package.json new file mode 100644 index 0000000..c9506df --- /dev/null +++ b/senses/hermes-gateway-health/package.json @@ -0,0 +1,17 @@ +{ + "name": "sense-hermes-gateway-health", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=index.js --packages=external" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "esbuild": "^0.27.0", + "typescript": "^5.7.0" + }, + "pnpm": { + "onlyBuiltDependencies": ["esbuild"] + } +} diff --git a/senses/hermes-gateway-health/pnpm-lock.yaml b/senses/hermes-gateway-health/pnpm-lock.yaml new file mode 100644 index 0000000..e785a82 --- /dev/null +++ b/senses/hermes-gateway-health/pnpm-lock.yaml @@ -0,0 +1,310 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.17 + esbuild: + specifier: ^0.27.0 + version: 0.27.7 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + +packages: + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@types/node@22.19.17': + resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + +snapshots: + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@types/node@22.19.17': + dependencies: + undici-types: 6.21.0 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} diff --git a/senses/hermes-gateway-health/src/index.ts b/senses/hermes-gateway-health/src/index.ts new file mode 100644 index 0000000..c186bb5 --- /dev/null +++ b/senses/hermes-gateway-health/src/index.ts @@ -0,0 +1,424 @@ +import { execFile } from "node:child_process"; +import type { LibSQLDatabase } from "drizzle-orm/libsql"; +import { hermesGatewayHealth } from "./schema.ts"; + +/** Keep subprocess deadlines slightly under typical sense timeout (30s). */ +const EXEC_TIMEOUT_MS = 25_000; + +/** HTTP probe stays below EXEC_TIMEOUT_MS and sense timeout (30s). */ +const HTTP_TIMEOUT_MS = Math.min(23_000, EXEC_TIMEOUT_MS - 2000); + +const HTTP_ERROR_MAX_LEN = 256; + +function gatewayProbeUrl(): string { + const u = + process.env.HERMES_GATEWAY_HEALTH_URL ?? + process.env.NERVE_HERMES_GATEWAY_URL ?? + ""; + return String(u).trim(); +} + +function truncateHttpError(err: unknown): string { + const raw = + err && typeof err === "object" && "code" in err && (err as { code: unknown }).code + ? String((err as { code: unknown }).code) + : String((err as { message?: unknown } | null)?.message ?? err ?? "error"); + const s = raw.trim() || "error"; + return s.length > HTTP_ERROR_MAX_LEN ? s.slice(0, HTTP_ERROR_MAX_LEN) : s; +} + +interface HttpProbeResult { + httpOk: number; + httpStatusCode: number; + httpLatencyMs: number; + httpError: string; +} + +/** + * GET the gateway URL; success = HTTP 200–399. + * URL must be set via HERMES_GATEWAY_HEALTH_URL or NERVE_HERMES_GATEWAY_URL. + */ +async function probeGatewayHttp(url: string): Promise { + 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), + }; + } +} + +/** + * When `ps` lacks `etimes` (wall-clock seconds since start), parse `etime` + * ([[dd-]hh:]mm:ss) into seconds. See ps(1) `etime` field description. + */ +function etimeToSeconds(etime: string): number { + 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 * 86_400 + parts[0] * 3600 + parts[1] * 60 + parts[2]); + } + if (parts.length === 2) { + return Math.trunc(days * 86_400 + parts[0] * 60 + parts[1]); + } + if (parts.length === 1) { + return Math.trunc(days * 86_400 + parts[0]); + } + return 0; +} + +interface ExecResult { + exitCode: number; + errCode: string | undefined; + stdout: string; + stderr: string; +} + +function execFileUtf8(file: string, args: string[], opts: Record = {}): Promise { + return new Promise((resolve) => { + execFile( + file, + args, + { + encoding: "utf8", + maxBuffer: 8 * 1024 * 1024, + timeout: EXEC_TIMEOUT_MS, + ...opts, + } as Parameters[2], + (err, stdout, stderr) => { + const exitCode = + err && typeof (err as NodeJS.ErrnoException).status === "number" + ? (err as NodeJS.ErrnoException & { status: number }).status + : err ? -1 : 0; + resolve({ + exitCode, + errCode: (err as NodeJS.ErrnoException | null)?.code, + stdout: String(stdout ?? ""), + stderr: String(stderr ?? ""), + }); + }, + ); + }); +} + +function parseMainPidFromStatus(text: string): number { + const m = text.match(/Main PID:\s*(\d+)/i); + return m ? Math.trunc(Number.parseInt(m[1], 10)) || 0 : 0; +} + +function parseActiveLineFromStatus(text: string): { active: boolean; subRunning: boolean } { + for (const line of text.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(text: string): { mainPid: number; active: boolean; subRunning: boolean } { + let mainPid = 0; + let active = false; + let subRunning = false; + for (const line of text.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(): Promise<{ mainPid: number; systemdActiveRunning: boolean }> { + const status = await execFileUtf8("systemctl", [ + "--user", + "--no-pager", + "status", + "hermes-gateway", + ]); + const combined = `${status.stdout}\n${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}\n${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: number): Promise { + 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; +} + +interface PsMetrics { + rssBytes: number; + cpuPercent: number; + uptimeSec: number; +} + +async function readPsMetrics(mainPid: number): Promise { + 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 rssKiB = Number(parts[0]); + const cpu = Number(parts[1]); + const uptimeSec = etimeToSeconds(parts.slice(2).join(" ")); + const rssBytes = Number.isFinite(rssKiB) ? Math.trunc(rssKiB * 1024) : 0; + const cpuPercent = Number.isFinite(cpu) ? Math.round(cpu * 100) / 100 : 0; + return { rssBytes, cpuPercent, uptimeSec }; + } + 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(text: string): number { + const src = String(text); + 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(): Promise { + try { + const r = await execFileUtf8("hermes", ["sessions", "stats"]); + if (r.errCode === "ENOENT") return 0; + return parseActiveSessionsFromHermesStats(`${r.stdout}\n${r.stderr}`); + } catch { + return 0; + } +} + +async function countDirectChildren(mainPid: number): Promise { + 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; + } +} + +export async function compute(db: LibSQLDatabase, _peers: unknown) { + 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, + }; + + await db.insert(hermesGatewayHealth).values(row); + + return { + ts: row.ts, + alive: row.alive, + mainPid: row.mainPid, + rssBytes: row.rssBytes, + cpuPercent: row.cpuPercent, + uptimeSec: row.uptimeSec, + activeSessions: row.activeSessions, + childProcessCount: row.childProcessCount, + httpOk: row.httpOk, + httpStatusCode: row.httpStatusCode, + httpLatencyMs: row.httpLatencyMs, + httpError: row.httpError, + }; +} diff --git a/senses/hermes-gateway-health/schema.ts b/senses/hermes-gateway-health/src/schema.ts similarity index 100% rename from senses/hermes-gateway-health/schema.ts rename to senses/hermes-gateway-health/src/schema.ts diff --git a/senses/hermes-session-message-stats/index.js b/senses/hermes-session-message-stats/index.js index ee33873..a329a6b 100644 --- a/senses/hermes-session-message-stats/index.js +++ b/senses/hermes-session-message-stats/index.js @@ -1,25 +1,31 @@ +// 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"; -import { hermesSessionMessageStats } from "./schema.ts"; -const MEASUREMENT_WINDOW_MS = 900_000; -const MEASUREMENT_WINDOW_SECONDS = 900; +// 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() +}); -/** - * @param {string} filePath - * @param {number} cutoffMs - * @param {number} nowMs - * @returns {Promise<{ user: number; assistant: number; tool: number; fileHadActivity: boolean }>} - */ +// 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 { @@ -32,13 +38,13 @@ async function aggregateJsonlFile(filePath, cutoffMs, nowMs) { } catch { continue; } - if (typeof obj.role !== "string" || typeof obj.timestamp !== "string") { + if (typeof obj !== "object" || obj === null || typeof obj.role !== "string" || typeof obj.timestamp !== "string") { continue; } - const t = Date.parse(obj.timestamp); + const record = obj; + const t = Date.parse(record.timestamp); if (!Number.isFinite(t) || t < cutoffMs || t > nowMs) continue; - - const roleNorm = obj.role.trim().toLowerCase(); + const roleNorm = record.role.trim().toLowerCase(); if (roleNorm === "user") { user++; fileHadActivity = true; @@ -53,27 +59,21 @@ async function aggregateJsonlFile(filePath, cutoffMs, nowMs) { } finally { rl.close(); } - return { user, assistant, tool, fileHadActivity }; } - -export async function compute(db, _peers) { +async function compute(db, _peers) { 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)); + 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 = []; @@ -81,22 +81,18 @@ export async function compute(db, _peers) { throw err; } } - for (const filePath of files) { const { user, assistant, tool, fileHadActivity } = await aggregateJsonlFile( filePath, cutoffMs, - nowMs, + nowMs ); totalUserMessages += user; totalAssistantMessages += assistant; totalToolMessages += tool; if (fileHadActivity) activeSessions++; } - - const totalMessages = - totalUserMessages + totalAssistantMessages + totalToolMessages; - + const totalMessages = totalUserMessages + totalAssistantMessages + totalToolMessages; const row = { ts, totalUserMessages, @@ -104,11 +100,9 @@ export async function compute(db, _peers) { totalToolMessages, totalMessages, activeSessions, - measurementWindowSeconds: MEASUREMENT_WINDOW_SECONDS, + measurementWindowSeconds: MEASUREMENT_WINDOW_SECONDS }; - await db.insert(hermesSessionMessageStats).values(row); - return { ts: row.ts, totalUserMessages: row.totalUserMessages, @@ -116,6 +110,9 @@ export async function compute(db, _peers) { totalToolMessages: row.totalToolMessages, totalMessages: row.totalMessages, activeSessions: row.activeSessions, - measurementWindowSeconds: row.measurementWindowSeconds, + measurementWindowSeconds: row.measurementWindowSeconds }; } +export { + compute +}; diff --git a/senses/hermes-session-message-stats/package.json b/senses/hermes-session-message-stats/package.json new file mode 100644 index 0000000..a128c11 --- /dev/null +++ b/senses/hermes-session-message-stats/package.json @@ -0,0 +1,17 @@ +{ + "name": "sense-hermes-session-message-stats", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=index.js --packages=external" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "esbuild": "^0.27.0", + "typescript": "^5.7.0" + }, + "pnpm": { + "onlyBuiltDependencies": ["esbuild"] + } +} diff --git a/senses/hermes-session-message-stats/pnpm-lock.yaml b/senses/hermes-session-message-stats/pnpm-lock.yaml new file mode 100644 index 0000000..e785a82 --- /dev/null +++ b/senses/hermes-session-message-stats/pnpm-lock.yaml @@ -0,0 +1,310 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.17 + esbuild: + specifier: ^0.27.0 + version: 0.27.7 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + +packages: + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@types/node@22.19.17': + resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + +snapshots: + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@types/node@22.19.17': + dependencies: + undici-types: 6.21.0 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} diff --git a/senses/hermes-session-message-stats/src/index.ts b/senses/hermes-session-message-stats/src/index.ts new file mode 100644 index 0000000..3ab709c --- /dev/null +++ b/senses/hermes-session-message-stats/src/index.ts @@ -0,0 +1,128 @@ +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"; +import type { LibSQLDatabase } from "drizzle-orm/libsql"; +import { hermesSessionMessageStats } from "./schema.ts"; + +const MEASUREMENT_WINDOW_MS = 900_000; +const MEASUREMENT_WINDOW_SECONDS = 900; + +interface MessageCounts { + user: number; + assistant: number; + tool: number; + fileHadActivity: boolean; +} + +async function aggregateJsonlFile(filePath: string, cutoffMs: number, nowMs: number): Promise { + 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: unknown; + try { + obj = JSON.parse(trimmed); + } catch { + continue; + } + if ( + typeof obj !== "object" || obj === null || + typeof (obj as Record).role !== "string" || + typeof (obj as Record).timestamp !== "string" + ) { + continue; + } + const record = obj as { role: string; timestamp: string }; + 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 }; +} + +export async function compute(db: LibSQLDatabase, _peers: unknown) { + 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: string[] = []; + 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 as NodeJS.ErrnoException).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, + }; + + await db.insert(hermesSessionMessageStats).values(row); + + return { + ts: row.ts, + totalUserMessages: row.totalUserMessages, + totalAssistantMessages: row.totalAssistantMessages, + totalToolMessages: row.totalToolMessages, + totalMessages: row.totalMessages, + activeSessions: row.activeSessions, + measurementWindowSeconds: row.measurementWindowSeconds, + }; +} diff --git a/senses/hermes-session-message-stats/schema.ts b/senses/hermes-session-message-stats/src/schema.ts similarity index 100% rename from senses/hermes-session-message-stats/schema.ts rename to senses/hermes-session-message-stats/src/schema.ts diff --git a/senses/linux-system-health/index.js b/senses/linux-system-health/index.js index 69de1a0..2d3a892 100644 --- a/senses/linux-system-health/index.js +++ b/senses/linux-system-health/index.js @@ -1,13 +1,35 @@ +// src/index.ts import { loadavg, totalmem, freemem, uptime } from "node:os"; import { execSync } from "node:child_process"; import { readFile } from "node:fs/promises"; -import { snapshots } from "./schema.ts"; -const SOCKSTAT_PATH = "/proc/net/sockstat"; +// 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") +}); +// 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:")) { @@ -29,20 +51,16 @@ function parseSockstat(content) { tcpMemPages = map.mem ?? 0; } } - return { socketsUsed, tcpInuse, tcpOrphan, tcpTw, tcpAlloc, tcpMemPages }; } - -export async function compute(db, _peers) { +async function compute(db, _peers) { 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) * 10000) / 100; - + 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(); @@ -51,37 +69,44 @@ export async function compute(db, _peers) { 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) * 10000) / 100 : 0; - } catch {} - - // TCP socket stats + 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 {} - + } catch { + } const ts = Date.now(); const uptimeSec = Math.round(uptime()); - await db.insert(snapshots).values({ - ts, cpuLoad1m: load1, cpuLoad5m: load5, cpuLoad15m: load15, - memTotalMB, memUsedMB, memUsedPct, - diskTotalGB, diskUsedGB, diskUsedPct, + 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, + tcpMemPages: tcp.tcpMemPages }); - return { cpu: { load1m: load1, load5m: load5, load15m: load15 }, memory: { totalMB: memTotalMB, usedMB: memUsedMB, usedPct: memUsedPct }, disk: { totalGB: diskTotalGB, usedGB: diskUsedGB, usedPct: diskUsedPct }, tcp: { socketsUsed: tcp.socketsUsed, inuse: tcp.tcpInuse, orphan: tcp.tcpOrphan, tw: tcp.tcpTw, alloc: tcp.tcpAlloc, memPages: tcp.tcpMemPages }, - uptimeSec, + uptimeSec }; } +export { + compute +}; diff --git a/senses/linux-system-health/package.json b/senses/linux-system-health/package.json new file mode 100644 index 0000000..0b7a637 --- /dev/null +++ b/senses/linux-system-health/package.json @@ -0,0 +1,17 @@ +{ + "name": "sense-linux-system-health", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=index.js --packages=external" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "esbuild": "^0.27.0", + "typescript": "^5.7.0" + }, + "pnpm": { + "onlyBuiltDependencies": ["esbuild"] + } +} diff --git a/senses/linux-system-health/pnpm-lock.yaml b/senses/linux-system-health/pnpm-lock.yaml new file mode 100644 index 0000000..e785a82 --- /dev/null +++ b/senses/linux-system-health/pnpm-lock.yaml @@ -0,0 +1,310 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.17 + esbuild: + specifier: ^0.27.0 + version: 0.27.7 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + +packages: + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@types/node@22.19.17': + resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + +snapshots: + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@types/node@22.19.17': + dependencies: + undici-types: 6.21.0 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} diff --git a/senses/linux-system-health/src/index.ts b/senses/linux-system-health/src/index.ts new file mode 100644 index 0000000..337dfb7 --- /dev/null +++ b/senses/linux-system-health/src/index.ts @@ -0,0 +1,96 @@ +import { loadavg, totalmem, freemem, uptime } from "node:os"; +import { execSync } from "node:child_process"; +import { readFile } from "node:fs/promises"; +import type { LibSQLDatabase } from "drizzle-orm/libsql"; +import { snapshots } from "./schema.ts"; + +const SOCKSTAT_PATH = "/proc/net/sockstat"; + +interface SockstatResult { + socketsUsed: number; + tcpInuse: number; + tcpOrphan: number; + tcpTw: number; + tcpAlloc: number; + tcpMemPages: number; +} + +function parseSockstat(content: string): SockstatResult { + 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: Record = {}; + 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 }; +} + +export async function compute(db: LibSQLDatabase, _peers: unknown) { + 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) * 10000) / 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) * 10000) / 100 : 0; + } catch {} + + let tcp: SockstatResult = { 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()); + + await db.insert(snapshots).values({ + 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 { + cpu: { load1m: load1, load5m: load5, load15m: load15 }, + memory: { totalMB: memTotalMB, usedMB: memUsedMB, usedPct: memUsedPct }, + disk: { totalGB: diskTotalGB, usedGB: diskUsedGB, usedPct: diskUsedPct }, + tcp: { socketsUsed: tcp.socketsUsed, inuse: tcp.tcpInuse, orphan: tcp.tcpOrphan, tw: tcp.tcpTw, alloc: tcp.tcpAlloc, memPages: tcp.tcpMemPages }, + uptimeSec, + }; +} diff --git a/senses/linux-system-health/schema.ts b/senses/linux-system-health/src/schema.ts similarity index 100% rename from senses/linux-system-health/schema.ts rename to senses/linux-system-health/src/schema.ts diff --git a/senses/worker-process-metrics/index.js b/senses/worker-process-metrics/index.js index 59fc8aa..7a7ab07 100644 --- a/senses/worker-process-metrics/index.js +++ b/senses/worker-process-metrics/index.js @@ -1,10 +1,19 @@ -import { workerProcessMetrics } from "./schema.ts"; +// 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() +}); +// src/index.ts function round2(n) { return Math.round(n * 100) / 100; } - -export async function compute(db, _peers) { +async function compute(db, _peers) { const ts = Date.now(); const pid = process.pid; const uptimeSec = process.uptime(); @@ -12,24 +21,24 @@ export async function compute(db, _peers) { 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, + externalMB }; - await db.insert(workerProcessMetrics).values(row); - return { ts: row.ts, pid: row.pid, uptimeSec: row.uptimeSec, heapUsedMB: row.heapUsedMB, rssMB: row.rssMB, - externalMB: row.externalMB, + externalMB: row.externalMB }; } +export { + compute +}; diff --git a/senses/worker-process-metrics/package.json b/senses/worker-process-metrics/package.json new file mode 100644 index 0000000..8595d9d --- /dev/null +++ b/senses/worker-process-metrics/package.json @@ -0,0 +1,17 @@ +{ + "name": "sense-worker-process-metrics", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=index.js --packages=external" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "esbuild": "^0.27.0", + "typescript": "^5.7.0" + }, + "pnpm": { + "onlyBuiltDependencies": ["esbuild"] + } +} diff --git a/senses/worker-process-metrics/pnpm-lock.yaml b/senses/worker-process-metrics/pnpm-lock.yaml new file mode 100644 index 0000000..e785a82 --- /dev/null +++ b/senses/worker-process-metrics/pnpm-lock.yaml @@ -0,0 +1,310 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.17 + esbuild: + specifier: ^0.27.0 + version: 0.27.7 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + +packages: + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@types/node@22.19.17': + resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + +snapshots: + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@types/node@22.19.17': + dependencies: + undici-types: 6.21.0 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} diff --git a/senses/worker-process-metrics/src/index.ts b/senses/worker-process-metrics/src/index.ts new file mode 100644 index 0000000..328482e --- /dev/null +++ b/senses/worker-process-metrics/src/index.ts @@ -0,0 +1,36 @@ +import type { LibSQLDatabase } from "drizzle-orm/libsql"; +import { workerProcessMetrics } from "./schema.ts"; + +function round2(n: number): number { + return Math.round(n * 100) / 100; +} + +export async function compute(db: LibSQLDatabase, _peers: unknown) { + 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, + }; + + await db.insert(workerProcessMetrics).values(row); + + return { + ts: row.ts, + pid: row.pid, + uptimeSec: row.uptimeSec, + heapUsedMB: row.heapUsedMB, + rssMB: row.rssMB, + externalMB: row.externalMB, + }; +} diff --git a/senses/worker-process-metrics/schema.ts b/senses/worker-process-metrics/src/schema.ts similarity index 100% rename from senses/worker-process-metrics/schema.ts rename to senses/worker-process-metrics/src/schema.ts diff --git a/workflows/sense-generator/roles/coder/prompt.ts b/workflows/sense-generator/roles/coder/prompt.ts index 151822b..33b3b2d 100644 --- a/workflows/sense-generator/roles/coder/prompt.ts +++ b/workflows/sense-generator/roles/coder/prompt.ts @@ -3,5 +3,41 @@ export function coderPrompt({ threadId }: { threadId: string }): string { Read the nerve-dev skill for sense file structure and conventions: \`cat node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\` Implement the sense following the patterns from existing senses and the skill guide. -Create all required files and update nerve.yaml.`; + +File structure for each sense: +- \`senses//src/index.ts\` — TypeScript source with proper types; import schema as \`./schema.ts\` +- \`senses//src/schema.ts\` — Drizzle schema (TypeScript) +- \`senses//migrations/\` — Drizzle migration files (at sense root, not inside src/) +- \`senses//package.json\` — with esbuild build script (see below) +- \`senses//index.js\` — bundled output generated by \`pnpm build\` (do NOT edit by hand) + +package.json template for each sense: +\`\`\`json +{ + "name": "sense-", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=index.js --packages=external" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "esbuild": "^0.27.0", + "typescript": "^5.7.0" + }, + "pnpm": { + "onlyBuiltDependencies": ["esbuild"] + } +} +\`\`\` + +After creating all files, run inside the sense directory: +\`\`\` +pnpm install --no-cache && pnpm build +\`\`\` + +This generates the bundled \`index.js\` at the sense root that the daemon loads. + +Then update nerve.yaml and run any required migrations.`; }