@hellcoder/companion 0.116.1-preview.20260814050532.93fba26 → 0.117.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AgentsPage-DPt9YKyQ.js → AgentsPage-DJIGGU86.js} +1 -1
- package/dist/assets/{CronManager-BxRWyTp3.js → CronManager-D7aAMVgs.js} +1 -1
- package/dist/assets/{DashboardPage-BSnJNNNL.js → DashboardPage-Bporj7Vr.js} +1 -1
- package/dist/assets/{IntegrationsPage-DWrnBrBa.js → IntegrationsPage-BSY0-fsO.js} +1 -1
- package/dist/assets/{LinearOAuthSettingsPage-DZJpyUr3.js → LinearOAuthSettingsPage-Cl5dP05g.js} +1 -1
- package/dist/assets/{LinearSettingsPage-CUV6wc7-.js → LinearSettingsPage-dwMpnpxL.js} +1 -1
- package/dist/assets/{MagicUIDashboard-CbruKd65.js → MagicUIDashboard-Bpa4AupX.js} +1 -1
- package/dist/assets/{MagicUIView-NSITfXce.js → MagicUIView-BoHMBELi.js} +1 -1
- package/dist/assets/{Playground-BoyZfARc.js → Playground-By2aXWQF.js} +1 -1
- package/dist/assets/{PromptsPage-D3aJBO9h.js → PromptsPage-DVv0Qn3d.js} +1 -1
- package/dist/assets/{RunsPage-C6YY6SGq.js → RunsPage-bu3TZ6ai.js} +1 -1
- package/dist/assets/{SandboxManager-_mggajaE.js → SandboxManager-CDjGbD5N.js} +1 -1
- package/dist/assets/{SettingsPage-DBjt4SLp.js → SettingsPage-D0v_BZTU.js} +1 -1
- package/dist/assets/{TailscalePage-BaAGOCMu.js → TailscalePage-B8SJQpMF.js} +1 -1
- package/dist/assets/index-BqOdTpct.css +1 -0
- package/dist/assets/index-DqIZ_Ol5.js +140 -0
- package/dist/assets/{sw-register-BH5kpgnP.js → sw-register-CvyRJNug.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/package.json +1 -1
- package/server/linear-agent-bridge.ts +0 -1
- package/server/magic-ui-watcher.test.ts +85 -0
- package/server/magic-ui-watcher.ts +0 -1
- package/server/routes/system-routes.test.ts +401 -0
- package/server/routes/system-routes.ts +10 -0
- package/server/system-disk.test.ts +48 -0
- package/server/system-disk.ts +55 -0
- package/server/system-memory.test.ts +32 -0
- package/server/system-memory.ts +32 -0
- package/dist/assets/index-BaDsUsyb.js +0 -140
- package/dist/assets/index-DSCRqqpK.css +0 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { getSystemDisk } from "./system-disk.js";
|
|
3
|
+
|
|
4
|
+
// getSystemDisk reads the real filesystem via statfs(2), so exact byte counts
|
|
5
|
+
// are machine-dependent. The invariants below must hold on any host where
|
|
6
|
+
// statfs succeeds. On a host without statfs the function returns null, which
|
|
7
|
+
// every test tolerates — the meter is best-effort by design.
|
|
8
|
+
describe("getSystemDisk", () => {
|
|
9
|
+
it("returns a coherent snapshot with sane invariants", () => {
|
|
10
|
+
const d = getSystemDisk();
|
|
11
|
+
if (!d) return; // statfs unavailable — nothing to assert
|
|
12
|
+
|
|
13
|
+
// A mounted filesystem always has positive capacity.
|
|
14
|
+
expect(d.total_bytes).toBeGreaterThan(0);
|
|
15
|
+
|
|
16
|
+
expect(d.used_bytes).toBeGreaterThanOrEqual(0);
|
|
17
|
+
expect(d.available_bytes).toBeGreaterThanOrEqual(0);
|
|
18
|
+
expect(d.used_bytes).toBeLessThanOrEqual(d.total_bytes);
|
|
19
|
+
expect(d.available_bytes).toBeLessThanOrEqual(d.total_bytes);
|
|
20
|
+
|
|
21
|
+
// used is derived as total - available (never bfree), so the identity
|
|
22
|
+
// must hold exactly. This is the guard against a future refactor
|
|
23
|
+
// switching to bfree and silently under-reporting usage.
|
|
24
|
+
expect(d.used_bytes).toBe(Math.max(0, d.total_bytes - d.available_bytes));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("reports used_percent in 0–100 rounded to one decimal", () => {
|
|
28
|
+
const d = getSystemDisk();
|
|
29
|
+
if (!d) return;
|
|
30
|
+
expect(d.used_percent).toBeGreaterThanOrEqual(0);
|
|
31
|
+
expect(d.used_percent).toBeLessThanOrEqual(100);
|
|
32
|
+
expect(Math.round(d.used_percent * 10)).toBe(d.used_percent * 10);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("reports the Companion data dir as the measured path", () => {
|
|
36
|
+
const d = getSystemDisk();
|
|
37
|
+
if (!d) return;
|
|
38
|
+
// The meter must describe the volume that session/recording files grow
|
|
39
|
+
// into, which is not necessarily the one holding "/".
|
|
40
|
+
expect(d.path).toBeTruthy();
|
|
41
|
+
expect(typeof d.path).toBe("string");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("never throws, even though the underlying syscall can fail", () => {
|
|
45
|
+
// The route calls this on every poll with no try/catch of its own.
|
|
46
|
+
expect(() => getSystemDisk()).not.toThrow();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { statfsSync } from "node:fs";
|
|
2
|
+
import { COMPANION_HOME } from "./paths.js";
|
|
3
|
+
|
|
4
|
+
export interface SystemDiskInfo {
|
|
5
|
+
/** Total size of the filesystem holding COMPANION_HOME, in bytes. */
|
|
6
|
+
total_bytes: number;
|
|
7
|
+
/** Space in use (total - available), in bytes. */
|
|
8
|
+
used_bytes: number;
|
|
9
|
+
/** Space available to this (unprivileged) user, in bytes. */
|
|
10
|
+
available_bytes: number;
|
|
11
|
+
/** Used percentage of total, 0–100, rounded to one decimal. */
|
|
12
|
+
used_percent: number;
|
|
13
|
+
/** Path the figures were measured for. */
|
|
14
|
+
path: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Snapshot of free space on the filesystem that holds the Companion data
|
|
19
|
+
* directory — that's the volume sessions, recordings and logs actually grow
|
|
20
|
+
* into, which is not necessarily the one holding "/".
|
|
21
|
+
*
|
|
22
|
+
* Deliberately uses statfs(2) rather than shelling out to `df`, and never
|
|
23
|
+
* walks the tree the way `du` would: this is a single syscall against the
|
|
24
|
+
* already-in-memory superblock (~0.1ms, no disk I/O), so it is safe to call
|
|
25
|
+
* on a poll interval and needs no caching layer.
|
|
26
|
+
*
|
|
27
|
+
* We report `bavail` (blocks free to unprivileged users) rather than `bfree`,
|
|
28
|
+
* which excludes the root-reserved reserve. That mirrors how getSystemMemory()
|
|
29
|
+
* prefers MemAvailable over MemFree: both answer "how much can I actually
|
|
30
|
+
* still use" rather than "how much is technically unallocated". It is also why
|
|
31
|
+
* this can read a percent or two above `df` for the same filesystem.
|
|
32
|
+
*/
|
|
33
|
+
export function getSystemDisk(): SystemDiskInfo | null {
|
|
34
|
+
try {
|
|
35
|
+
const stats = statfsSync(COMPANION_HOME);
|
|
36
|
+
// bsize is the preferred I/O block size that blocks/bavail are counted in.
|
|
37
|
+
const total = stats.blocks * stats.bsize;
|
|
38
|
+
const available = stats.bavail * stats.bsize;
|
|
39
|
+
const used = Math.max(0, total - available);
|
|
40
|
+
const used_percent =
|
|
41
|
+
total > 0 ? Math.round((used / total) * 1000) / 10 : 0;
|
|
42
|
+
return {
|
|
43
|
+
total_bytes: total,
|
|
44
|
+
used_bytes: used,
|
|
45
|
+
available_bytes: available,
|
|
46
|
+
used_percent,
|
|
47
|
+
path: COMPANION_HOME,
|
|
48
|
+
};
|
|
49
|
+
} catch {
|
|
50
|
+
// statfs is unavailable (very old runtime) or the path does not exist yet
|
|
51
|
+
// on a first run. The meter is best-effort — the route returns 204 and the
|
|
52
|
+
// UI renders nothing rather than showing a bogus zeroed bar.
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -28,4 +28,36 @@ describe("getSystemMemory", () => {
|
|
|
28
28
|
// At most one decimal place.
|
|
29
29
|
expect(Math.round(m.used_percent * 10)).toBe(m.used_percent * 10);
|
|
30
30
|
});
|
|
31
|
+
|
|
32
|
+
// Swap is read from the same /proc/meminfo snapshot as RAM. A host may have
|
|
33
|
+
// no swap at all, in which case every swap field must be 0 — the UI keys off
|
|
34
|
+
// swap_total_bytes === 0 to hide the meter entirely.
|
|
35
|
+
it("returns coherent swap figures, or zeroes when swap is absent", () => {
|
|
36
|
+
const m = getSystemMemory();
|
|
37
|
+
|
|
38
|
+
expect(m.swap_total_bytes).toBeGreaterThanOrEqual(0);
|
|
39
|
+
expect(m.swap_used_bytes).toBeGreaterThanOrEqual(0);
|
|
40
|
+
|
|
41
|
+
if (m.swap_total_bytes === 0) {
|
|
42
|
+
// No swap configured (or a non-Linux host): must not report phantom use.
|
|
43
|
+
expect(m.swap_used_bytes).toBe(0);
|
|
44
|
+
expect(m.swap_used_percent).toBe(0);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Used swap can never exceed the configured total, even though SwapFree
|
|
49
|
+
// can briefly exceed SwapTotal during swapoff — that's what the clamp is for.
|
|
50
|
+
expect(m.swap_used_bytes).toBeLessThanOrEqual(m.swap_total_bytes);
|
|
51
|
+
expect(m.swap_used_percent).toBeGreaterThanOrEqual(0);
|
|
52
|
+
expect(m.swap_used_percent).toBeLessThanOrEqual(100);
|
|
53
|
+
expect(Math.round(m.swap_used_percent * 10)).toBe(m.swap_used_percent * 10);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("keeps swap independent of the RAM figures", () => {
|
|
57
|
+
// MemAvailable deliberately excludes swap, so a swapping box shows healthy
|
|
58
|
+
// RAM headroom while thrashing. Guards against a refactor that folds swap
|
|
59
|
+
// into used_bytes and hides exactly the condition we want to surface.
|
|
60
|
+
const m = getSystemMemory();
|
|
61
|
+
expect(m.used_bytes).toBe(Math.max(0, m.total_bytes - m.available_bytes));
|
|
62
|
+
});
|
|
31
63
|
});
|
package/server/system-memory.ts
CHANGED
|
@@ -10,6 +10,15 @@ export interface SystemMemoryInfo {
|
|
|
10
10
|
available_bytes: number;
|
|
11
11
|
/** Used percentage of total, 0–100, rounded to one decimal. */
|
|
12
12
|
used_percent: number;
|
|
13
|
+
/**
|
|
14
|
+
* Total swap, in bytes. 0 when no swap is configured, or on platforms where
|
|
15
|
+
* we cannot read it — callers should treat 0 as "no swap meter to show".
|
|
16
|
+
*/
|
|
17
|
+
swap_total_bytes: number;
|
|
18
|
+
/** Swap in use (total - free), in bytes. */
|
|
19
|
+
swap_used_bytes: number;
|
|
20
|
+
/** Used percentage of swap, 0–100, rounded to one decimal. 0 when no swap. */
|
|
21
|
+
swap_used_percent: number;
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
/**
|
|
@@ -25,10 +34,19 @@ function readMeminfoKb(meminfo: string, key: string): number | null {
|
|
|
25
34
|
* MemAvailable, which accounts for reclaimable page cache and therefore
|
|
26
35
|
* reflects true OOM headroom far better than os.freemem(). Everywhere else
|
|
27
36
|
* (or if /proc is unreadable) we fall back to the os module.
|
|
37
|
+
*
|
|
38
|
+
* Swap comes from the same single /proc/meminfo read, so reporting it costs
|
|
39
|
+
* nothing extra. It is worth surfacing separately rather than folding into the
|
|
40
|
+
* RAM figure: a box that has begun swapping heavily is already thrashing, and
|
|
41
|
+
* because MemAvailable excludes swap the RAM meter alone cannot show it. Swap
|
|
42
|
+
* exhaustion on top of high RAM use is the state immediately preceding the OOM
|
|
43
|
+
* killer, which is exactly what these meters exist to warn about.
|
|
28
44
|
*/
|
|
29
45
|
export function getSystemMemory(): SystemMemoryInfo {
|
|
30
46
|
let total = os.totalmem();
|
|
31
47
|
let available = os.freemem();
|
|
48
|
+
let swapTotal = 0;
|
|
49
|
+
let swapUsed = 0;
|
|
32
50
|
|
|
33
51
|
try {
|
|
34
52
|
const meminfo = readFileSync("/proc/meminfo", "utf8");
|
|
@@ -38,16 +56,30 @@ export function getSystemMemory(): SystemMemoryInfo {
|
|
|
38
56
|
total = totalKb * 1024;
|
|
39
57
|
available = availableKb * 1024;
|
|
40
58
|
}
|
|
59
|
+
const swapTotalKb = readMeminfoKb(meminfo, "SwapTotal");
|
|
60
|
+
const swapFreeKb = readMeminfoKb(meminfo, "SwapFree");
|
|
61
|
+
if (swapTotalKb !== null && swapFreeKb !== null) {
|
|
62
|
+
swapTotal = swapTotalKb * 1024;
|
|
63
|
+
// Clamp: SwapFree can momentarily exceed SwapTotal mid-swapoff.
|
|
64
|
+
swapUsed = Math.max(0, swapTotal - swapFreeKb * 1024);
|
|
65
|
+
}
|
|
41
66
|
} catch {
|
|
42
67
|
// Not Linux, or /proc/meminfo unavailable — keep the os module values.
|
|
68
|
+
// The os module exposes no swap figures, so swap stays 0 (= "unknown",
|
|
69
|
+
// rendered as no meter) rather than being reported as 0-bytes-used.
|
|
43
70
|
}
|
|
44
71
|
|
|
45
72
|
const used = Math.max(0, total - available);
|
|
46
73
|
const used_percent = total > 0 ? Math.round((used / total) * 1000) / 10 : 0;
|
|
74
|
+
const swap_used_percent =
|
|
75
|
+
swapTotal > 0 ? Math.round((swapUsed / swapTotal) * 1000) / 10 : 0;
|
|
47
76
|
return {
|
|
48
77
|
total_bytes: total,
|
|
49
78
|
used_bytes: used,
|
|
50
79
|
available_bytes: available,
|
|
51
80
|
used_percent,
|
|
81
|
+
swap_total_bytes: swapTotal,
|
|
82
|
+
swap_used_bytes: swapUsed,
|
|
83
|
+
swap_used_percent,
|
|
52
84
|
};
|
|
53
85
|
}
|