Compare commits

...

3 Commits

Author SHA1 Message Date
nntrivi2001 2ea2184372 chore(release): bump version to 1.8.3 2026-09-23 16:46:40 +07:00
nntrivi2001 aff863c69f fix(plugins): retry claude plugin install|update without -y on an old CLI
An older Claude Code has no -y on `plugin install|update` and exits with
"error: unknown option '-y'", so the dashboard's Update now chain failed at
its plugin_update step (and so did Update all, the plugin buttons and the
superpowers auto-install). Those versions also predate the confirmation
prompt -y answers, so every wrapper now retries the same argv without it
when, and only when, the CLI rejects the flag. /agenthub-update, which the
failure message points users to, falls back the same way.
2026-09-23 16:46:20 +07:00
nntrivi2001 764b967856 fix(dashboard): measure V8 heap against its limit, not its current size
The Health Score's heap component divided heapUsed by heapTotal, which is
only what V8 has grown the heap to so far, so a healthy server read 70-90%
and dragged the score down. GET /api/settings/info now also reports
server.memory.heapLimit (v8 heap_size_limit, the ceiling the process dies
at); the V8 Heap bar and the Health Score divide by it, falling back to
heapTotal against an older server.
2026-09-23 16:46:00 +07:00
16 changed files with 120 additions and 16 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+8 -1
View File
@@ -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;
+13 -6
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{
"name": "agenthub",
"version": "1.8.2",
"version": "1.8.3",
"bin": {
"agenthub": "bin/agenthub.js"
},
+3 -1
View File
@@ -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
```
+15
View File
@@ -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",
+15
View File
@@ -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",
+8
View File
@@ -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(
+16
View File
@@ -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 {
+17 -1
View File
@@ -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
+12
View File
@@ -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, {
+1
View File
@@ -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],
+5 -1
View File
@@ -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(),