Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ea2184372 | |||
| aff863c69f | |||
| 764b967856 |
@@ -957,7 +957,14 @@ export const api = {
|
||||
node_version: string;
|
||||
platform: string;
|
||||
ws_connections: number;
|
||||
memory: { rss: number; heapTotal: number; heapUsed: number; external: number };
|
||||
memory: {
|
||||
rss: number;
|
||||
heapTotal: number;
|
||||
heapUsed: number;
|
||||
external: number;
|
||||
/** V8's heap_size_limit; absent on older servers (fall back to heapTotal). */
|
||||
heapLimit?: number;
|
||||
};
|
||||
cpu_load: number[];
|
||||
arch: string;
|
||||
total_mem: number;
|
||||
|
||||
@@ -132,7 +132,14 @@ interface SystemInfo {
|
||||
node_version: string;
|
||||
platform: string;
|
||||
ws_connections: number;
|
||||
memory: { rss: number; heapTotal: number; heapUsed: number; external: number };
|
||||
memory: {
|
||||
rss: number;
|
||||
heapTotal: number;
|
||||
heapUsed: number;
|
||||
external: number;
|
||||
/** V8's heap_size_limit; absent on older servers (fall back to heapTotal). */
|
||||
heapLimit?: number;
|
||||
};
|
||||
cpu_load: number[];
|
||||
arch: string;
|
||||
total_mem: number;
|
||||
@@ -1562,10 +1569,10 @@ function SystemHealthTab() {
|
||||
|
||||
const memUsedPct =
|
||||
info.server.total_mem > 0 ? (1 - info.server.free_mem / info.server.total_mem) * 100 : 0;
|
||||
const heapUsedPct =
|
||||
info.server.memory.heapTotal > 0
|
||||
? (info.server.memory.heapUsed / info.server.memory.heapTotal) * 100
|
||||
: 0;
|
||||
// Against the heap LIMIT: heapTotal is only what V8 has grown to so far, so
|
||||
// used/total reads 70-90% on a healthy process and dragged the health score.
|
||||
const heapMax = info.server.memory.heapLimit || info.server.memory.heapTotal;
|
||||
const heapUsedPct = heapMax > 0 ? (info.server.memory.heapUsed / heapMax) * 100 : 0;
|
||||
|
||||
return {
|
||||
totalEntries,
|
||||
@@ -1701,7 +1708,7 @@ function SystemHealthTab() {
|
||||
</Tip>
|
||||
<Tip
|
||||
block
|
||||
raw={`V8 Heap: ${heapUsedPct.toFixed(1)}% used\n${formatBytes(info.server.memory.heapUsed)} / ${formatBytes(info.server.memory.heapTotal)}\nExternal: ${formatBytes(info.server.memory.external)}`}
|
||||
raw={`V8 Heap: ${heapUsedPct.toFixed(1)}% used\n${formatBytes(info.server.memory.heapUsed)} / ${formatBytes(info.server.memory.heapLimit || info.server.memory.heapTotal)} limit\nAllocated: ${formatBytes(info.server.memory.heapTotal)}\nExternal: ${formatBytes(info.server.memory.external)}`}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-[10px]">
|
||||
|
||||
@@ -9617,7 +9617,7 @@ exports[`screen snapshots > Settings 1`] = `
|
||||
<p
|
||||
class="text-[10px] text-status-warning/90 mt-1"
|
||||
>
|
||||
UI build v1.8.2 (rebuild client to match)
|
||||
UI build v1.8.3 (rebuild client to match)
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
|
||||
+2
-1
@@ -3,7 +3,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Agent Dashboard for Claude Code API
|
||||
version: 1.8.2
|
||||
version: 1.8.3
|
||||
description: HTTP API for real-time Claude Code session monitoring, agent lifecycle tracking, analytics, pricing, hooks ingestion, and workflow intelligence.
|
||||
contact:
|
||||
name: Nguyễn Ngọc Trí Vĩ
|
||||
@@ -6196,6 +6196,7 @@ paths:
|
||||
heapTotal: 71303168
|
||||
heapUsed: 58392104
|
||||
external: 3211884
|
||||
heapLimit: 4345298944
|
||||
arrayBuffers: 1048576
|
||||
cpu_load:
|
||||
- 2.13
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "agenthub",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "agenthub",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.3",
|
||||
"hasInstallScript": true,
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agenthub",
|
||||
"version": "1.8.2",
|
||||
"version": "1.8.3",
|
||||
"bin": {
|
||||
"agenthub": "bin/agenthub.js"
|
||||
},
|
||||
|
||||
@@ -17,7 +17,9 @@ if [ -z "$MARKETPLACE" ]; then
|
||||
echo "agenthub is not installed as a plugin (checkout install?) - nothing to update this way."
|
||||
else
|
||||
claude plugin marketplace update "$MARKETPLACE"
|
||||
claude plugin update "agenthub@$MARKETPLACE" -y
|
||||
# An older Claude Code has no `-y` (and no prompt for it to answer).
|
||||
claude plugin update "agenthub@$MARKETPLACE" -y 2>/dev/null ||
|
||||
claude plugin update "agenthub@$MARKETPLACE"
|
||||
fi
|
||||
```
|
||||
|
||||
|
||||
@@ -83,6 +83,21 @@ function resolveClaude() {
|
||||
}
|
||||
|
||||
function run(cmd, args, timeout) {
|
||||
try {
|
||||
return runOnce(cmd, args, timeout);
|
||||
} catch (err) {
|
||||
// An older Claude Code rejects `-y` ("unknown option") and never prompts,
|
||||
// so the same command without the flag is the right retry.
|
||||
if (!args.includes("-y") || !/unknown option '-y'/.test(String(err.stderr || ""))) throw err;
|
||||
return runOnce(
|
||||
cmd,
|
||||
args.filter((a) => a !== "-y"),
|
||||
timeout
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function runOnce(cmd, args, timeout) {
|
||||
log(`$ ${cmd} ${args.join(" ")}`);
|
||||
const out = execFileSync(cmd, args, {
|
||||
encoding: "utf8",
|
||||
|
||||
@@ -102,6 +102,21 @@ function findNewRoot(exclude) {
|
||||
}
|
||||
|
||||
function run(cmd, args, timeout) {
|
||||
try {
|
||||
return runOnce(cmd, args, timeout);
|
||||
} catch (err) {
|
||||
// An older Claude Code rejects `-y` ("unknown option") and never prompts,
|
||||
// so the same command without the flag is the right retry.
|
||||
if (!args.includes("-y") || !/unknown option '-y'/.test(String(err.stderr || ""))) throw err;
|
||||
return runOnce(
|
||||
cmd,
|
||||
args.filter((a) => a !== "-y"),
|
||||
timeout
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function runOnce(cmd, args, timeout) {
|
||||
log(`$ ${cmd} ${args.join(" ")}`);
|
||||
const out = execFileSync(cmd, args, {
|
||||
encoding: "utf8",
|
||||
|
||||
@@ -1896,6 +1896,14 @@ describe("Transcript cache integration", () => {
|
||||
assert.equal(res.body.server.version, pkg.version);
|
||||
});
|
||||
|
||||
it("reports the V8 heap LIMIT, not just the currently allocated heapTotal", async () => {
|
||||
const res = await fetch("/api/settings/info");
|
||||
const { heapUsed, heapTotal, heapLimit } = res.body.server.memory;
|
||||
assert.ok(Number.isFinite(heapLimit), "heapLimit should be a number");
|
||||
// heapTotal is what V8 has grown to so far; the limit is the ceiling.
|
||||
assert.ok(heapLimit >= heapTotal && heapLimit > heapUsed);
|
||||
});
|
||||
|
||||
it("should evict cache entry on SessionEnd", async () => {
|
||||
const tmpTranscript = path.join(os.tmpdir(), `test-evict-${Date.now()}.jsonl`);
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -116,6 +116,22 @@ describe("cc-plugin-cli argv construction", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("retries without -y on a Claude Code too old to know the flag", async () => {
|
||||
// Older CLIs answer `error: unknown option '-y'` and exit 1; they also
|
||||
// never prompt, so the same command without the flag is the right retry.
|
||||
const restore = stubClaude(
|
||||
'for a in "$@"; do [ "$a" = "-y" ] && { echo "error: unknown option \'-y\'" >&2; exit 1; }; done\n' +
|
||||
'for a in "$@"; do echo "$a"; done'
|
||||
);
|
||||
try {
|
||||
const r = await cli.pluginUpdate({ spec: "demo@m" });
|
||||
assert.equal(r.ok, true);
|
||||
assert.deepEqual(argvOf(r), ["plugin", "update", "demo@m"]);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("passes -y on install so a non-TTY run never waits for a prompt", async () => {
|
||||
const restore = stubClaude();
|
||||
try {
|
||||
|
||||
@@ -130,7 +130,23 @@ function serialize(fn) {
|
||||
* @param {{timeoutMs?:number}} [opts]
|
||||
* @returns {Promise<{ok:boolean, code:number|null, stdout:string, stderr:string, args:string[]}>}
|
||||
*/
|
||||
function runClaude(args, opts = {}) {
|
||||
// A Claude Code older than `plugin install|update -y` rejects the flag outright.
|
||||
// It also predates the confirmation prompt the flag answers, so the same
|
||||
// command without it is the correct retry, not a weaker one.
|
||||
const NO_YES_FLAG = /unknown option '-y'/;
|
||||
|
||||
async function runClaude(args, opts = {}) {
|
||||
const result = await runClaudeOnce(args, opts);
|
||||
if (!result.ok && args.includes("-y") && NO_YES_FLAG.test(result.stderr)) {
|
||||
return runClaudeOnce(
|
||||
args.filter((a) => a !== "-y"),
|
||||
opts
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function runClaudeOnce(args, opts = {}) {
|
||||
const timeout = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
||||
return new Promise((resolve, reject) => {
|
||||
// Only pin CLAUDE_CONFIG_DIR when the operator explicitly set CLAUDE_HOME
|
||||
|
||||
@@ -128,6 +128,18 @@ function missingSkillPlugins() {
|
||||
* @returns {{ok: boolean, output: string}}
|
||||
*/
|
||||
function runClaudePlugin(args, timeoutMs) {
|
||||
const result = runClaudePluginOnce(args, timeoutMs);
|
||||
// An older Claude Code rejects `-y` and never prompts; retry without it.
|
||||
if (!result.ok && args.includes("-y") && /unknown option '-y'/.test(result.output)) {
|
||||
return runClaudePluginOnce(
|
||||
args.filter((a) => a !== "-y"),
|
||||
timeoutMs
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function runClaudePluginOnce(args, timeoutMs) {
|
||||
const env = { ...process.env };
|
||||
if (process.env.CLAUDE_HOME) env.CLAUDE_CONFIG_DIR = getClaudeHome();
|
||||
const result = spawnSync("claude", args, {
|
||||
|
||||
@@ -139,6 +139,7 @@ const SETTINGS_INFO_EXAMPLE = {
|
||||
heapTotal: 71303168,
|
||||
heapUsed: 58392104,
|
||||
external: 3211884,
|
||||
heapLimit: 4345298944,
|
||||
arrayBuffers: 1048576,
|
||||
},
|
||||
cpu_load: [2.13, 2.45, 2.31],
|
||||
|
||||
@@ -7,6 +7,7 @@ const { Router } = require("express");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const v8 = require("v8");
|
||||
const { db, stmts, DB_PATH, DEFAULT_PRICING, applyIntroPricing } = require("../db");
|
||||
const { getConnectionCount } = require("../websocket");
|
||||
const { transcriptCache } = require("./hooks");
|
||||
@@ -121,7 +122,10 @@ router.get("/info", (req, res) => {
|
||||
node_version: process.version,
|
||||
platform: process.platform,
|
||||
ws_connections: getConnectionCount(),
|
||||
memory: process.memoryUsage(),
|
||||
// heapTotal is only what V8 has grown the heap to SO FAR, so used/total
|
||||
// sits at 70-90% on a perfectly healthy process. heapLimit is the ceiling
|
||||
// the process actually dies at — the denominator a "heap used %" needs.
|
||||
memory: { ...process.memoryUsage(), heapLimit: v8.getHeapStatistics().heap_size_limit },
|
||||
cpu_load: os.loadavg(),
|
||||
arch: os.arch(),
|
||||
total_mem: os.totalmem(),
|
||||
|
||||
Reference in New Issue
Block a user