Files
AgentHub/server/lib/pool-usage.js
T
nntrivi2001 1c6dfea5b5 feat(health): System Health in three groups, a score that sees lag; admin user detail
Dashboard → System Health is grouped into AgentHub, Machine and Agent
activity. The machine (GET /api/host, every signed-in user) is drawn by
components/health/HostHealth.tsx: host card beside the CPU/memory history,
then CPU (threads as compact cells + temperatures/GPUs), disks & network,
busiest processes, and per-user tiles. Concurrency bars each show their own
agent type on hover.

The Health Score (client/src/lib/healthScore.ts) is now 100 minus what each
check costs, between an amber and a red threshold: event-loop p99
(server/lib/loop-delay.js, shared with /api/metrics), Linux PSI pressure for
CPU, memory and disk I/O (/api/host `pressure`; load per core without PSI),
RAM, the database's disk, V8 heap, hooks silent while agents work, and agent
errors in the last 24 h. All-time success rate, API errors and the transcript
cache hit rate no longer count. /api/settings/info adds server.event_loop and
activity {last_event_at, working_agents, agents_24h}.

Administration: a user row opens its detail in place (GET
/api/users/:id/details — spend, account pool share, live panes, workspaces,
newest sessions); the server panel is gone, member tmux folded into the table;
the Sessions list shows each row's owner in server mode.
2026-10-27 16:39:38 +07:00

74 lines
3.3 KiB
JavaScript

/**
* @file pool-usage.js
* @description $ and tokens one account-pool account spent in the last 5 hours
* and 7 days, for the balancer and `GET /api/accounts` (claude-pool.js,
* codex-pool.js). Tokens are booked to hours by `hourlyTokenRows` — the same
* booking as the daily cost split, so sub-agent buckets and pre-compaction
* baselines count — and priced by `calculateCost`, never a second formula.
* A Claude session counts only for the hours one of THIS account's runs held
* it (start hour to end hour), so a session resumed on another account is
* not booked twice; Codex has no hourly rows, so its session counts whole
* once a run of the account started in the window, as before. With `ownerId`
* only the runs of that user's workspaces count — one user's share of the
* account, for the Administration page's user detail.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const WINDOWS = { "5h": 5, "7d": 24 * 7 };
const hourAgo = (hours, now) => new Date(now - hours * 3600e3).toISOString().slice(0, 13);
const tokensOf = (r) =>
r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_write_tokens;
/**
* `{spend5h, spend7d, tokens5h, tokens7d}` of `account` on `tool`. With
* `wholeSession`, a session's every row counts in a window its run started in;
* with `ownerId`, only runs on that user's workspaces.
*/
function accountUsage(
tool,
account,
{ wholeSession = false, now = Date.now(), ownerId = null } = {}
) {
const { db, stmts } = require("../db");
const { calculateCost, hourlyTokenRows } = require("../routes/pricing");
const since7d = new Date(now - WINDOWS["7d"] * 3600e3).toISOString();
const owned = ownerId == null ? "" : "AND lane_id IN (SELECT id FROM lanes WHERE owner_id = ?)";
const ownedArgs = ownerId == null ? [] : [ownerId];
const runs = db
.prepare(
`SELECT session_id, substr(started_at, 1, 13) AS start_hour, started_at,
substr(COALESCE(ended_at, '9999'), 1, 13) AS end_hour
FROM dashboard_runs
WHERE tool = ? AND account = ? AND session_id IS NOT NULL
AND COALESCE(ended_at, '9999') >= ? ${owned}`
)
.all(tool, account, since7d, ...ownedArgs);
const out = { spend5h: 0, spend7d: 0, tokens5h: 0, tokens7d: 0 };
if (runs.length === 0) return out;
const runsOf = new Map();
for (const r of runs) runsOf.set(r.session_id, [...(runsOf.get(r.session_id) || []), r]);
const rows = hourlyTokenRows(
`WHERE s.id IN (SELECT session_id FROM dashboard_runs
WHERE tool = ? AND account = ? AND session_id IS NOT NULL
AND COALESCE(ended_at, '9999') >= ? ${owned})`,
[tool, account, since7d, ...ownedArgs]
);
const rules = stmts.listPricing.all();
for (const [label, hours] of Object.entries(WINDOWS)) {
const sinceHour = hourAgo(hours, now);
const sinceIso = new Date(now - hours * 3600e3).toISOString();
const held = rows.filter((row) =>
(runsOf.get(row.session_id) || []).some((run) =>
wholeSession
? run.started_at >= sinceIso
: row.date >= sinceHour && row.date >= run.start_hour && row.date <= run.end_hour
)
);
out[`spend${label}`] = held.length ? calculateCost(held, rules).total_cost : 0;
out[`tokens${label}`] = Math.round(held.reduce((n, r) => n + tokensOf(r), 0));
}
return out;
}
module.exports = { accountUsage };