@appkit/llamacpp-cli 1.5.0 → 1.6.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.
Files changed (95) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/MONITORING-ACCURACY-FIX.md +199 -0
  3. package/PER-PROCESS-METRICS.md +190 -0
  4. package/README.md +57 -8
  5. package/dist/cli.js +9 -6
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/create.d.ts.map +1 -1
  8. package/dist/commands/create.js +12 -3
  9. package/dist/commands/create.js.map +1 -1
  10. package/dist/commands/monitor.d.ts.map +1 -1
  11. package/dist/commands/monitor.js +51 -1
  12. package/dist/commands/monitor.js.map +1 -1
  13. package/dist/commands/ps.d.ts +3 -1
  14. package/dist/commands/ps.d.ts.map +1 -1
  15. package/dist/commands/ps.js +75 -5
  16. package/dist/commands/ps.js.map +1 -1
  17. package/dist/commands/server-show.d.ts.map +1 -1
  18. package/dist/commands/server-show.js +10 -3
  19. package/dist/commands/server-show.js.map +1 -1
  20. package/dist/commands/start.d.ts.map +1 -1
  21. package/dist/commands/start.js +14 -2
  22. package/dist/commands/start.js.map +1 -1
  23. package/dist/lib/history-manager.d.ts +46 -0
  24. package/dist/lib/history-manager.d.ts.map +1 -0
  25. package/dist/lib/history-manager.js +157 -0
  26. package/dist/lib/history-manager.js.map +1 -0
  27. package/dist/lib/metrics-aggregator.d.ts +2 -1
  28. package/dist/lib/metrics-aggregator.d.ts.map +1 -1
  29. package/dist/lib/metrics-aggregator.js +15 -4
  30. package/dist/lib/metrics-aggregator.js.map +1 -1
  31. package/dist/lib/system-collector.d.ts +9 -4
  32. package/dist/lib/system-collector.d.ts.map +1 -1
  33. package/dist/lib/system-collector.js +29 -28
  34. package/dist/lib/system-collector.js.map +1 -1
  35. package/dist/tui/HistoricalMonitorApp.d.ts +5 -0
  36. package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -0
  37. package/dist/tui/HistoricalMonitorApp.js +490 -0
  38. package/dist/tui/HistoricalMonitorApp.js.map +1 -0
  39. package/dist/tui/MonitorApp.d.ts.map +1 -1
  40. package/dist/tui/MonitorApp.js +84 -62
  41. package/dist/tui/MonitorApp.js.map +1 -1
  42. package/dist/tui/MultiServerMonitorApp.d.ts +1 -1
  43. package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
  44. package/dist/tui/MultiServerMonitorApp.js +293 -77
  45. package/dist/tui/MultiServerMonitorApp.js.map +1 -1
  46. package/dist/types/history-types.d.ts +30 -0
  47. package/dist/types/history-types.d.ts.map +1 -0
  48. package/dist/types/history-types.js +11 -0
  49. package/dist/types/history-types.js.map +1 -0
  50. package/dist/types/monitor-types.d.ts +1 -0
  51. package/dist/types/monitor-types.d.ts.map +1 -1
  52. package/dist/types/server-config.d.ts +1 -0
  53. package/dist/types/server-config.d.ts.map +1 -1
  54. package/dist/types/server-config.js.map +1 -1
  55. package/dist/utils/downsample-utils.d.ts +35 -0
  56. package/dist/utils/downsample-utils.d.ts.map +1 -0
  57. package/dist/utils/downsample-utils.js +107 -0
  58. package/dist/utils/downsample-utils.js.map +1 -0
  59. package/dist/utils/file-utils.d.ts +6 -0
  60. package/dist/utils/file-utils.d.ts.map +1 -1
  61. package/dist/utils/file-utils.js +38 -0
  62. package/dist/utils/file-utils.js.map +1 -1
  63. package/dist/utils/process-utils.d.ts +19 -1
  64. package/dist/utils/process-utils.d.ts.map +1 -1
  65. package/dist/utils/process-utils.js +79 -1
  66. package/dist/utils/process-utils.js.map +1 -1
  67. package/docs/images/.gitkeep +1 -0
  68. package/package.json +3 -1
  69. package/src/cli.ts +9 -6
  70. package/src/commands/create.ts +14 -4
  71. package/src/commands/monitor.ts +21 -1
  72. package/src/commands/ps.ts +88 -5
  73. package/src/commands/server-show.ts +10 -3
  74. package/src/commands/start.ts +15 -2
  75. package/src/lib/history-manager.ts +172 -0
  76. package/src/lib/metrics-aggregator.ts +18 -5
  77. package/src/lib/system-collector.ts +31 -28
  78. package/src/tui/HistoricalMonitorApp.ts +548 -0
  79. package/src/tui/MonitorApp.ts +89 -64
  80. package/src/tui/MultiServerMonitorApp.ts +348 -103
  81. package/src/types/history-types.ts +39 -0
  82. package/src/types/monitor-types.ts +1 -0
  83. package/src/types/server-config.ts +1 -0
  84. package/src/utils/downsample-utils.ts +128 -0
  85. package/src/utils/file-utils.ts +40 -0
  86. package/src/utils/process-utils.ts +85 -1
  87. package/test-load.sh +100 -0
  88. package/dist/tui/components/ErrorState.d.ts +0 -8
  89. package/dist/tui/components/ErrorState.d.ts.map +0 -1
  90. package/dist/tui/components/ErrorState.js +0 -22
  91. package/dist/tui/components/ErrorState.js.map +0 -1
  92. package/dist/tui/components/LoadingState.d.ts +0 -8
  93. package/dist/tui/components/LoadingState.d.ts.map +0 -1
  94. package/dist/tui/components/LoadingState.js +0 -21
  95. package/dist/tui/components/LoadingState.js.map +0 -1
@@ -1,11 +1,14 @@
1
1
  import chalk from 'chalk';
2
2
  import Table from 'cli-table3';
3
+ import blessed from 'blessed';
3
4
  import { stateManager } from '../lib/state-manager';
4
5
  import { statusChecker } from '../lib/status-checker';
5
6
  import { formatUptime, formatBytes } from '../utils/format-utils';
6
7
  import { getProcessMemory } from '../utils/process-utils';
8
+ import { createMultiServerMonitorUI } from '../tui/MultiServerMonitorApp.js';
9
+ import { ServerConfig } from '../types/server-config.js';
7
10
 
8
- export async function psCommand(): Promise<void> {
11
+ async function showStaticTable(): Promise<void> {
9
12
  const servers = await stateManager.getAllServers();
10
13
 
11
14
  if (servers.length === 0) {
@@ -52,12 +55,14 @@ export async function psCommand(): Promise<void> {
52
55
  ? formatUptime(server.lastStarted)
53
56
  : '-';
54
57
 
55
- // Get memory usage for running servers
58
+ // Get memory usage for running servers (CPU + Metal GPU memory)
56
59
  let memoryText = '-';
57
60
  if (server.status === 'running' && server.pid) {
58
- const memoryBytes = await getProcessMemory(server.pid);
59
- if (memoryBytes !== null) {
60
- memoryText = formatBytes(memoryBytes);
61
+ const cpuMemoryBytes = await getProcessMemory(server.pid);
62
+ if (cpuMemoryBytes !== null) {
63
+ const metalMemoryBytes = server.metalMemoryMB ? server.metalMemoryMB * 1024 * 1024 : 0;
64
+ const totalMemoryBytes = cpuMemoryBytes + metalMemoryBytes;
65
+ memoryText = formatBytes(totalMemoryBytes);
61
66
  }
62
67
  }
63
68
 
@@ -88,3 +93,81 @@ export async function psCommand(): Promise<void> {
88
93
  console.log(chalk.red('\n⚠️ Some servers have crashed. Check logs with: llamacpp server logs <id> --errors'));
89
94
  }
90
95
  }
96
+
97
+ export async function psCommand(identifier?: string, options?: { table?: boolean }): Promise<void> {
98
+ // If --table flag is set, show static table (backward compatibility)
99
+ if (options?.table) {
100
+ await showStaticTable();
101
+ return;
102
+ }
103
+
104
+ // Get all servers and update their statuses
105
+ const servers = await stateManager.getAllServers();
106
+
107
+ if (servers.length === 0) {
108
+ console.log(chalk.yellow('No servers configured.'));
109
+ console.log(chalk.dim('\nCreate a server: llamacpp server create <model-filename>'));
110
+ return;
111
+ }
112
+
113
+ // Update all server statuses
114
+ const updated = await statusChecker.updateAllServerStatuses();
115
+
116
+ // If identifier is provided, find the server and jump to detail view
117
+ if (identifier) {
118
+ const server = await findServer(identifier, updated);
119
+ if (!server) {
120
+ console.log(chalk.red(`❌ Server not found: ${identifier}`));
121
+ console.log(chalk.dim('\nAvailable servers:'));
122
+ updated.forEach((s: ServerConfig) => {
123
+ console.log(chalk.dim(` - ${s.id} (port ${s.port})`));
124
+ });
125
+ process.exit(1);
126
+ }
127
+
128
+ // Find the server index for direct jump
129
+ const serverIndex = updated.findIndex(s => s.id === server.id);
130
+
131
+ // Launch multi-server TUI with direct jump to detail view
132
+ const screen = blessed.screen({
133
+ smartCSR: true,
134
+ title: 'llama.cpp Multi-Server Monitor',
135
+ fullUnicode: true,
136
+ });
137
+
138
+ await createMultiServerMonitorUI(screen, updated, true, serverIndex); // fromPs = true, directJumpIndex
139
+ return;
140
+ }
141
+
142
+ // No identifier - launch multi-server TUI
143
+ const runningServers = updated.filter((s: ServerConfig) => s.status === 'running');
144
+
145
+ // Launch multi-server TUI (shows all servers, not just running ones)
146
+ const screen = blessed.screen({
147
+ smartCSR: true,
148
+ title: 'llama.cpp Multi-Server Monitor',
149
+ fullUnicode: true,
150
+ });
151
+
152
+ await createMultiServerMonitorUI(screen, updated, true); // fromPs = true
153
+ }
154
+
155
+ // Helper function to find server by identifier
156
+ async function findServer(identifier: string, servers: ServerConfig[]): Promise<ServerConfig | null> {
157
+ // Try by port
158
+ const port = parseInt(identifier);
159
+ if (!isNaN(port)) {
160
+ const server = servers.find(s => s.port === port);
161
+ if (server) return server;
162
+ }
163
+
164
+ // Try by exact ID
165
+ const byId = servers.find(s => s.id === identifier);
166
+ if (byId) return byId;
167
+
168
+ // Try by partial model name
169
+ const byModel = servers.find(s => s.modelName.toLowerCase().includes(identifier.toLowerCase()));
170
+ if (byModel) return byModel;
171
+
172
+ return null;
173
+ }
@@ -68,9 +68,16 @@ export async function serverShowCommand(identifier: string): Promise<void> {
68
68
  }
69
69
 
70
70
  if (updatedServer.pid) {
71
- const memoryBytes = await getProcessMemory(updatedServer.pid);
72
- if (memoryBytes !== null) {
73
- console.log(`${chalk.bold('Memory:')} ${formatBytes(memoryBytes)}`);
71
+ const cpuMemoryBytes = await getProcessMemory(updatedServer.pid);
72
+ if (cpuMemoryBytes !== null) {
73
+ const metalMemoryBytes = updatedServer.metalMemoryMB ? updatedServer.metalMemoryMB * 1024 * 1024 : 0;
74
+ const totalMemoryBytes = cpuMemoryBytes + metalMemoryBytes;
75
+
76
+ if (metalMemoryBytes > 0) {
77
+ console.log(`${chalk.bold('Memory:')} ${formatBytes(totalMemoryBytes)} (CPU: ${formatBytes(cpuMemoryBytes)}, GPU: ${formatBytes(metalMemoryBytes)})`);
78
+ } else {
79
+ console.log(`${chalk.bold('Memory:')} ${formatBytes(cpuMemoryBytes)} (CPU only)`);
80
+ }
74
81
  }
75
82
  }
76
83
  }
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import { stateManager } from '../lib/state-manager';
3
3
  import { launchctlManager } from '../lib/launchctl-manager';
4
4
  import { statusChecker } from '../lib/status-checker';
5
+ import { parseMetalMemoryFromLog } from '../utils/file-utils';
5
6
 
6
7
  export async function startCommand(identifier: string): Promise<void> {
7
8
  // Initialize state manager
@@ -61,9 +62,21 @@ export async function startCommand(identifier: string): Promise<void> {
61
62
  }
62
63
 
63
64
  // 7. Update server status
64
- await statusChecker.updateServerStatus(server);
65
+ let updatedServer = await statusChecker.updateServerStatus(server);
65
66
 
66
- // 8. Display success
67
+ // 8. Parse Metal (GPU) memory allocation if not already captured
68
+ if (!updatedServer.metalMemoryMB) {
69
+ console.log(chalk.dim('Detecting Metal (GPU) memory allocation...'));
70
+ await new Promise(resolve => setTimeout(resolve, 8000)); // 8 second delay
71
+ const metalMemoryMB = await parseMetalMemoryFromLog(updatedServer.stderrPath);
72
+ if (metalMemoryMB) {
73
+ updatedServer = { ...updatedServer, metalMemoryMB };
74
+ await stateManager.saveServerConfig(updatedServer);
75
+ console.log(chalk.dim(`Metal memory: ${metalMemoryMB.toFixed(0)} MB`));
76
+ }
77
+ }
78
+
79
+ // 9. Display success
67
80
  console.log();
68
81
  console.log(chalk.green('✅ Server started successfully!'));
69
82
  console.log();
@@ -0,0 +1,172 @@
1
+ import { mkdir, readFile, writeFile, access, rename } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { ServerMetrics, SystemMetrics } from '../types/monitor-types.js';
5
+ import { HistoryData, HistorySnapshot, TIME_WINDOW_HOURS, TimeWindow } from '../types/history-types.js';
6
+
7
+ export class HistoryManager {
8
+ private serverId: string;
9
+ private historyDir: string;
10
+ private historyPath: string;
11
+ private readonly MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
12
+
13
+ constructor(serverId: string) {
14
+ this.serverId = serverId;
15
+ this.historyDir = join(homedir(), '.llamacpp', 'history');
16
+ this.historyPath = join(this.historyDir, `${serverId}.json`);
17
+ }
18
+
19
+ /**
20
+ * Append a new snapshot to history (with auto-pruning)
21
+ */
22
+ async appendSnapshot(serverMetrics: ServerMetrics, systemMetrics?: SystemMetrics): Promise<void> {
23
+ try {
24
+ // Ensure history directory exists
25
+ await mkdir(this.historyDir, { recursive: true });
26
+
27
+ // Load existing history
28
+ const historyData = await this.loadHistoryData();
29
+
30
+ // Create new snapshot
31
+ const snapshot: HistorySnapshot = {
32
+ timestamp: Date.now(),
33
+ server: {
34
+ healthy: serverMetrics.healthy,
35
+ uptime: serverMetrics.uptime,
36
+ activeSlots: serverMetrics.activeSlots,
37
+ idleSlots: serverMetrics.idleSlots,
38
+ totalSlots: serverMetrics.totalSlots,
39
+ avgPromptSpeed: serverMetrics.avgPromptSpeed,
40
+ avgGenerateSpeed: serverMetrics.avgGenerateSpeed,
41
+ processMemory: serverMetrics.processMemory,
42
+ processCpuUsage: serverMetrics.processCpuUsage,
43
+ },
44
+ system: systemMetrics ? {
45
+ gpuUsage: systemMetrics.gpuUsage,
46
+ cpuUsage: systemMetrics.cpuUsage,
47
+ aneUsage: systemMetrics.aneUsage,
48
+ temperature: systemMetrics.temperature,
49
+ memoryUsed: systemMetrics.memoryUsed,
50
+ memoryTotal: systemMetrics.memoryTotal,
51
+ } : undefined,
52
+ };
53
+
54
+ // Append new snapshot
55
+ historyData.snapshots.push(snapshot);
56
+
57
+ // Prune old snapshots (keep only last 24h)
58
+ historyData.snapshots = this.pruneOldSnapshots(historyData.snapshots, this.MAX_AGE_MS);
59
+
60
+ // Atomic write: write to temp file in same directory, then rename
61
+ // This prevents read collisions during concurrent access
62
+ // IMPORTANT: temp file MUST be in same directory as destination for rename to work across filesystems
63
+ const tempPath = join(this.historyDir, `.${this.serverId}-${Date.now()}.tmp`);
64
+ await writeFile(tempPath, JSON.stringify(historyData, null, 2), 'utf-8');
65
+ await rename(tempPath, this.historyPath);
66
+ } catch (error) {
67
+ // Silent failure - don't interrupt monitoring
68
+ // Don't throw - just return silently to avoid polluting console
69
+ return;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Load all snapshots within specified time window
75
+ */
76
+ async loadHistory(windowHours: number): Promise<HistorySnapshot[]> {
77
+ // Retry logic for file I/O collisions during concurrent read/write
78
+ const maxRetries = 3;
79
+ let lastError: Error | null = null;
80
+
81
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
82
+ try {
83
+ const historyData = await this.loadHistoryData();
84
+ return this.filterByTimeWindow(historyData.snapshots, windowHours);
85
+ } catch (error) {
86
+ lastError = error as Error;
87
+ // Wait briefly before retry (exponential backoff)
88
+ if (attempt < maxRetries - 1) {
89
+ await new Promise(resolve => setTimeout(resolve, 50 * Math.pow(2, attempt)));
90
+ }
91
+ }
92
+ }
93
+
94
+ // All retries failed - throw error so it can be handled upstream
95
+ throw new Error(`Failed to load history after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`);
96
+ }
97
+
98
+ /**
99
+ * Load history for specific time window type
100
+ */
101
+ async loadHistoryByWindow(window: TimeWindow): Promise<HistorySnapshot[]> {
102
+ return this.loadHistory(TIME_WINDOW_HOURS[window]);
103
+ }
104
+
105
+ /**
106
+ * Get file path for server history
107
+ */
108
+ getHistoryPath(): string {
109
+ return this.historyPath;
110
+ }
111
+
112
+ /**
113
+ * Check if history file exists
114
+ */
115
+ async hasHistory(): Promise<boolean> {
116
+ try {
117
+ await access(this.historyPath);
118
+ return true;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Clear all history for server
126
+ */
127
+ async clearHistory(): Promise<void> {
128
+ const emptyHistory: HistoryData = {
129
+ serverId: this.serverId,
130
+ snapshots: [],
131
+ };
132
+
133
+ await mkdir(this.historyDir, { recursive: true });
134
+
135
+ // Atomic write - temp file in same directory as destination
136
+ const tempPath = join(this.historyDir, `.${this.serverId}-${Date.now()}.tmp`);
137
+ await writeFile(tempPath, JSON.stringify(emptyHistory, null, 2), 'utf-8');
138
+ await rename(tempPath, this.historyPath);
139
+ }
140
+
141
+ /**
142
+ * Load full history data from file
143
+ */
144
+ private async loadHistoryData(): Promise<HistoryData> {
145
+ try {
146
+ const content = await readFile(this.historyPath, 'utf-8');
147
+ return JSON.parse(content) as HistoryData;
148
+ } catch (error) {
149
+ // File doesn't exist or is corrupted, return empty history
150
+ return {
151
+ serverId: this.serverId,
152
+ snapshots: [],
153
+ };
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Prune snapshots older than maxAge
159
+ */
160
+ private pruneOldSnapshots(snapshots: HistorySnapshot[], maxAgeMs: number): HistorySnapshot[] {
161
+ const cutoff = Date.now() - maxAgeMs;
162
+ return snapshots.filter(s => s.timestamp >= cutoff);
163
+ }
164
+
165
+ /**
166
+ * Filter snapshots by time window
167
+ */
168
+ private filterByTimeWindow(snapshots: HistorySnapshot[], windowHours: number): HistorySnapshot[] {
169
+ const cutoff = Date.now() - (windowHours * 60 * 60 * 1000);
170
+ return snapshots.filter(s => s.timestamp >= cutoff);
171
+ }
172
+ }
@@ -2,7 +2,7 @@ import { ServerConfig } from '../types/server-config.js';
2
2
  import { ServerMetrics, SlotInfo, MonitorData } from '../types/monitor-types.js';
3
3
  import { statusChecker } from './status-checker.js';
4
4
  import { systemCollector } from './system-collector.js';
5
- import { getProcessMemory } from '../utils/process-utils.js';
5
+ import { getProcessMemory, getProcessCpu } from '../utils/process-utils.js';
6
6
 
7
7
  /**
8
8
  * Aggregates metrics from llama.cpp server API endpoints
@@ -128,10 +128,12 @@ export class MetricsAggregator {
128
128
  * Aggregate all server metrics
129
129
  * @param server - Server configuration
130
130
  * @param processMemory - Optional pre-fetched process memory (for batch collection)
131
+ * @param processCpuUsage - Optional pre-fetched process CPU usage (for batch collection)
131
132
  */
132
133
  async collectServerMetrics(
133
134
  server: ServerConfig,
134
- processMemory?: number | null
135
+ processMemory?: number | null,
136
+ processCpuUsage?: number | null
135
137
  ): Promise<ServerMetrics> {
136
138
  const now = Date.now();
137
139
 
@@ -167,14 +169,17 @@ export class MetricsAggregator {
167
169
  }
168
170
 
169
171
  // Fetch detailed metrics in parallel
170
- // If processMemory was pre-fetched (batch mode), use it; otherwise fetch individually
171
- const [healthy, props, slots, fetchedMemory] = await Promise.all([
172
+ // If processMemory/CPU were pre-fetched (batch mode), use them; otherwise fetch individually
173
+ const [healthy, props, slots, fetchedMemory, fetchedCpu] = await Promise.all([
172
174
  this.getHealth(),
173
175
  this.getProps(),
174
176
  this.getSlots(),
175
177
  processMemory !== undefined
176
178
  ? Promise.resolve(processMemory)
177
179
  : (server.pid ? getProcessMemory(server.pid) : Promise.resolve(null)),
180
+ processCpuUsage !== undefined
181
+ ? Promise.resolve(processCpuUsage)
182
+ : (server.pid ? getProcessCpu(server.pid) : Promise.resolve(null)),
178
183
  ]);
179
184
 
180
185
  // Calculate slot statistics
@@ -201,6 +206,13 @@ export class MetricsAggregator {
201
206
  ) / processingSlots.length
202
207
  : undefined;
203
208
 
209
+ // Calculate total memory (CPU + Metal GPU memory if available)
210
+ let totalMemory = fetchedMemory ?? undefined;
211
+ if (totalMemory !== undefined && server.metalMemoryMB) {
212
+ // Add Metal memory (convert MB to bytes)
213
+ totalMemory += server.metalMemoryMB * 1024 * 1024;
214
+ }
215
+
204
216
  return {
205
217
  server,
206
218
  healthy,
@@ -214,7 +226,8 @@ export class MetricsAggregator {
214
226
  slots,
215
227
  avgPromptSpeed,
216
228
  avgGenerateSpeed,
217
- processMemory: fetchedMemory ?? undefined,
229
+ processMemory: totalMemory,
230
+ processCpuUsage: fetchedCpu ?? undefined,
218
231
  timestamp: now,
219
232
  stale: false,
220
233
  };
@@ -178,16 +178,12 @@ export class SystemCollector {
178
178
  */
179
179
  private parseVmStatOutput(output: string): {
180
180
  memoryUsed: number;
181
- memoryTotal: number;
182
181
  } {
183
182
  const lines = output.split('\n');
184
183
  const pageSize = 16384; // 16KB on Apple Silicon
185
184
  let pagesActive = 0;
186
185
  let pagesWired = 0;
187
186
  let pagesCompressed = 0;
188
- let pagesFree = 0;
189
- let pagesInactive = 0;
190
- let pagesSpeculative = 0;
191
187
 
192
188
  for (const line of lines) {
193
189
  const match = line.match(/Pages (.*?):\s+(\d+)\./);
@@ -198,49 +194,56 @@ export class SystemCollector {
198
194
  if (name === 'active') pagesActive = value;
199
195
  else if (name === 'wired down') pagesWired = value;
200
196
  else if (name === 'compressed') pagesCompressed = value;
201
- else if (name === 'free') pagesFree = value;
202
- else if (name === 'inactive') pagesInactive = value;
203
- else if (name === 'speculative') pagesSpeculative = value;
204
197
  }
205
198
  }
206
199
 
207
200
  // Calculate used memory (active + wired + compressed)
201
+ // This matches what Activity Monitor and macmon report as "used"
208
202
  const usedPages = pagesActive + pagesWired + pagesCompressed;
209
203
  const memoryUsed = usedPages * pageSize;
210
204
 
211
- // Calculate total memory (used + free + inactive + speculative)
212
- const totalPages =
213
- pagesActive +
214
- pagesWired +
215
- pagesCompressed +
216
- pagesFree +
217
- pagesInactive +
218
- pagesSpeculative;
219
- const memoryTotal = totalPages * pageSize;
220
-
221
- return { memoryUsed, memoryTotal };
205
+ return { memoryUsed };
206
+ }
207
+
208
+ /**
209
+ * Get total system memory from sysctl
210
+ * Returns installed RAM size in bytes
211
+ */
212
+ private async getTotalMemory(): Promise<number> {
213
+ try {
214
+ const output = await execCommand('sysctl -n hw.memsize 2>/dev/null');
215
+ return parseInt(output.trim(), 10) || 0;
216
+ } catch {
217
+ return 0;
218
+ }
222
219
  }
223
220
 
224
221
  /**
225
- * Collect vm_stat memory metrics
222
+ * Collect vm_stat memory metrics + total system memory from sysctl
226
223
  */
227
- private async getVmStatMetrics(): Promise<{
224
+ private async getMemoryMetrics(): Promise<{
228
225
  memoryUsed: number;
229
226
  memoryTotal: number;
230
227
  }> {
231
228
  try {
232
- const output = await execCommand('vm_stat 2>/dev/null');
233
- return this.parseVmStatOutput(output);
229
+ // Get used memory from vm_stat
230
+ const vmStatOutput = await execCommand('vm_stat 2>/dev/null');
231
+ const { memoryUsed } = this.parseVmStatOutput(vmStatOutput);
232
+
233
+ // Get total installed RAM from sysctl (this is accurate)
234
+ const memoryTotal = await this.getTotalMemory();
235
+
236
+ return { memoryUsed, memoryTotal };
234
237
  } catch {
235
- // Fallback to zeros if vm_stat fails
238
+ // Fallback to zeros if commands fail
236
239
  return { memoryUsed: 0, memoryTotal: 0 };
237
240
  }
238
241
  }
239
242
 
240
243
  /**
241
244
  * Collect all system metrics
242
- * Attempts macmon first (GPU/CPU/ANE + memory), falls back to vm_stat (memory only)
243
- * Caches results for 1.5s to prevent spawning multiple macmon processes
245
+ * Attempts macmon first (GPU/CPU/ANE), always gets memory from vm_stat + sysctl
246
+ * Caches results for 4s to prevent spawning multiple macmon processes
244
247
  */
245
248
  async collectSystemMetrics(): Promise<SystemMetrics> {
246
249
  const now = Date.now();
@@ -276,11 +279,11 @@ export class SystemCollector {
276
279
  const warnings: string[] = [];
277
280
  const now = Date.now();
278
281
 
279
- // Try macmon first
282
+ // Try macmon first for GPU/CPU/ANE
280
283
  const macmonMetrics = await this.getMacmonMetrics();
281
284
 
282
- // Always get memory from vm_stat (more reliable than macmon)
283
- const memoryMetrics = await this.getVmStatMetrics();
285
+ // Always get memory from vm_stat + sysctl (accurate total from sysctl)
286
+ const memoryMetrics = await this.getMemoryMetrics();
284
287
 
285
288
  // Determine source and add warnings
286
289
  let source: 'macmon' | 'vm_stat' | 'none';