local-commander-agent 0.3.48 → 0.3.49
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/CHANGELOG.md +218 -212
- package/README.md +216 -216
- package/bin/command-path.mjs +223 -223
- package/bin/localcommander.mjs +872 -872
- package/bin/monitor-agents.mjs +162 -162
- package/bin/monitor-context.d.mts +7 -7
- package/bin/monitor-context.mjs +65 -65
- package/bin/monitor-mcp.mjs +136 -136
- package/bin/monitor-savings.d.mts +20 -20
- package/bin/monitor-savings.mjs +71 -71
- package/bin/monitor.mjs +530 -530
- package/bin/network-package.mjs +38 -38
- package/bin/posix.mjs +84 -84
- package/package.json +57 -57
- package/runtime/JobHost.exe +0 -0
- package/runtime/LocalCommanderAgent.exe +0 -0
- package/runtime/OwnerBridge.exe +0 -0
- package/runtime/agent.mjs +29 -5
package/bin/monitor.mjs
CHANGED
|
@@ -1,530 +1,530 @@
|
|
|
1
|
-
import { lstat, open, readFile, readdir } from "node:fs/promises";
|
|
2
|
-
import { lifetimeSavingsView, formatLifetimeSavings } from "./monitor-savings.mjs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
-
import { readAgentActivity } from "./monitor-agents.mjs";
|
|
6
|
-
import { readMcpActivity, readMcpHealth, contextView } from "./monitor-mcp.mjs";
|
|
7
|
-
|
|
8
|
-
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
9
|
-
const STATES = new Set(["starting", "connecting", "connected", "disconnected", "stopped", "failed"]);
|
|
10
|
-
const EVENTS = new Set([...STATES, "execute", "prepare", "status", "output", "cancel", "finished", "cancelled", "unknown", "result", "request_failed", "rejected", "connection_error", "compacted", "passthrough"]);
|
|
11
|
-
const iso = value => typeof value === "string" && /^\d{4}-\d\d-\d\dT[\d:.]+Z$/.test(value) && Number.isFinite(Date.parse(value));
|
|
12
|
-
const invalid = () => { throw Error("MONITOR_DATA_INVALID"); };
|
|
13
|
-
const count = value => Number.isSafeInteger(value) && value >= 0 && value <= 1_000_000_000_000;
|
|
14
|
-
const label = value => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,79}$/.test(value);
|
|
15
|
-
const EXECUTION_STATES = new Set(["requested", "prepared", "accepted", "running", "finished", "failed", "cancelled", "rejected", "unknown", "missing"]);
|
|
16
|
-
const PROCESS_NAMES = new Set(["node", "npm", "npx", "git", "rg", "powershell", "pwsh", "cmd", "tsc", "python", "dotnet", "command",
|
|
17
|
-
"fs_list", "fs_read", "fs_search", "fs_write", "fs_replace", "git_status", "git_diff", "git_log", "project_snapshot", "localhost_request"]);
|
|
18
|
-
const COMMANDS = new Set(["node --test", ...["status", "diff", "log", "show", "fetch", "pull", "push", "commit"].map(value => "git " + value),
|
|
19
|
-
...["test", "install", "ci", "pack", "publish"].map(value => "npm " + value),
|
|
20
|
-
...["test", "build", "typecheck", "lint", "dev", "start"].map(value => "npm run " + value),
|
|
21
|
-
...[...PROCESS_NAMES].flatMap(value => [value, value + " [args]"])]);
|
|
22
|
-
function profileView(raw) {
|
|
23
|
-
if (raw === null) return null;
|
|
24
|
-
if (!raw || !label(raw.model)) invalid();
|
|
25
|
-
return { model: raw.model };
|
|
26
|
-
}
|
|
27
|
-
function efficiencyView(raw) {
|
|
28
|
-
if (raw === undefined) return null; // Older agents did not measure monitor savings.
|
|
29
|
-
if (!raw || raw.scope !== "this_run" || !count(raw.calls) || !count(raw.polls?.status) || !count(raw.polls?.output) ||
|
|
30
|
-
!Array.isArray(raw.models) || raw.models.length > 32) invalid();
|
|
31
|
-
const models = raw.models.map(group => {
|
|
32
|
-
if (!group || (group.model !== null && !label(group.model)) || !count(group.calls) || !count(group.savedBytes) || !count(group.sentBytes)) invalid();
|
|
33
|
-
return { model: group.model, calls: group.calls, savedBytes: group.savedBytes,
|
|
34
|
-
sentBytes: group.sentBytes, estimatedSavedTokens: Math.round(group.savedBytes / 4) };
|
|
35
|
-
});
|
|
36
|
-
if (models.reduce((sum, group) => sum + group.calls, 0) !== raw.calls) invalid();
|
|
37
|
-
const savedBytes = models.reduce((sum, group) => sum + group.savedBytes, 0);
|
|
38
|
-
return { scope: "this_run", calls: raw.calls, polls: { status: raw.polls.status, output: raw.polls.output }, models,
|
|
39
|
-
savedBytes, estimatedSavedTokens: Math.round(savedBytes / 4),
|
|
40
|
-
unclassifiedSavedBytes: models.filter(group => group.model === null).reduce((sum, group) => sum + group.savedBytes, 0) };
|
|
41
|
-
}
|
|
42
|
-
function pidAlive(pid) {
|
|
43
|
-
try { process.kill(pid, 0); return true; }
|
|
44
|
-
catch (error) { return error?.code === "EPERM"; }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Explicit allowlist projection: never print extra fields from a local file.
|
|
48
|
-
export function monitorView(raw, now = Date.now(), alive = pidAlive, transport = null) {
|
|
49
|
-
if (!raw || raw.schemaVersion !== 1 || !UUID.test(raw.instanceId) || !Number.isSafeInteger(raw.pid) || raw.pid <= 0 ||
|
|
50
|
-
typeof raw.agentVersion !== "string" || !/^[A-Za-z0-9._+-]{1,64}$/.test(raw.agentVersion) || !iso(raw.updatedAt) ||
|
|
51
|
-
!STATES.has(raw.connection?.state) || !Array.isArray(raw.sessions) || raw.sessions.length > 32 ||
|
|
52
|
-
!Array.isArray(raw.events) || raw.events.length > 100) invalid();
|
|
53
|
-
let origin = null;
|
|
54
|
-
if (raw.connection.origin !== null) {
|
|
55
|
-
const url = new URL(raw.connection.origin);
|
|
56
|
-
if (url.origin !== raw.connection.origin || url.username || url.password ||
|
|
57
|
-
!(url.protocol === "https:" || (url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)))) invalid();
|
|
58
|
-
origin = url.origin;
|
|
59
|
-
}
|
|
60
|
-
const sessions = raw.sessions.map(session => {
|
|
61
|
-
if (!session || !UUID.test(session.sessionId) || !UUID.test(session.workspaceGrantId) || !iso(session.lastSeenAt)) invalid();
|
|
62
|
-
return { sessionId: session.sessionId, workspaceGrantId: session.workspaceGrantId, lastSeenAt: session.lastSeenAt, model: null };
|
|
63
|
-
});
|
|
64
|
-
const events = raw.events.map(event => {
|
|
65
|
-
if (!event || !Number.isSafeInteger(event.seq) || event.seq < 1 || !iso(event.at) || !EVENTS.has(event.event) ||
|
|
66
|
-
(event.sessionId !== undefined && !UUID.test(event.sessionId)) || (event.executionId !== undefined && !UUID.test(event.executionId)) ||
|
|
67
|
-
(event.tool !== undefined && (typeof event.tool !== "string" || !/^[a-z_]{1,64}$/.test(event.tool))) ||
|
|
68
|
-
(event.code !== undefined && (typeof event.code !== "string" || !/^[A-Z0-9_]{1,64}$/.test(event.code)))) invalid();
|
|
69
|
-
return { seq: event.seq, at: event.at, event: event.event,
|
|
70
|
-
...(event.sessionId ? { sessionId: event.sessionId } : {}), ...(event.executionId ? { executionId: event.executionId } : {}),
|
|
71
|
-
...(event.tool ? { tool: event.tool } : {}), ...(event.code ? { code: event.code } : {}) };
|
|
72
|
-
});
|
|
73
|
-
const running = alive(raw.pid);
|
|
74
|
-
if (raw.transportAt !== undefined && !iso(raw.transportAt)) invalid();
|
|
75
|
-
// A heartbeat file counts only for this snapshot's instance and connection (#31).
|
|
76
|
-
const heartbeat = raw.transportAt !== undefined && transport?.instanceId === raw.instanceId && transport.epoch === raw.connection.epoch &&
|
|
77
|
-
iso(transport.transportAt) && Date.parse(transport.transportAt) > Date.parse(raw.transportAt) ? transport.transportAt : raw.transportAt;
|
|
78
|
-
const age = now - Date.parse(raw.connection.state === "connected" ? heartbeat ?? raw.updatedAt : raw.updatedAt);
|
|
79
|
-
const freshnessMs = raw.freshnessMs ?? 10_000;
|
|
80
|
-
if (!Number.isInteger(freshnessMs) || freshnessMs < 1000 || freshnessMs > 180_000) invalid();
|
|
81
|
-
const connection = !running ? "stopped" : age < -5000 || age > freshnessMs ? "stale" : raw.connection.state;
|
|
82
|
-
if (raw.computer !== undefined && (typeof raw.computer !== "string" || !/^[A-Za-z0-9._-]{1,64}$/.test(raw.computer))) invalid();
|
|
83
|
-
if (raw.processName !== undefined && raw.processName !== "lc-agent") invalid();
|
|
84
|
-
if (raw.executions !== undefined && (!Array.isArray(raw.executions) || raw.executions.length > 64)) invalid();
|
|
85
|
-
const executions = (raw.executions ?? []).map(execution => {
|
|
86
|
-
if (!execution || !UUID.test(execution.executionId) || !UUID.test(execution.sessionId) || !PROCESS_NAMES.has(execution.name) ||
|
|
87
|
-
!COMMANDS.has(execution.command) || !EXECUTION_STATES.has(execution.state) ||
|
|
88
|
-
(execution.pid !== null && (!Number.isSafeInteger(execution.pid) || execution.pid <= 0)) ||
|
|
89
|
-
![execution.originalBytes, execution.sentBytes, execution.savedBytes].every(value => value === null || count(value)) ||
|
|
90
|
-
(execution.reason !== null && (typeof execution.reason !== "string" || !/^[a-z_]{1,80}$/.test(execution.reason)))) invalid();
|
|
91
|
-
const measured = execution.originalBytes !== null;
|
|
92
|
-
if (measured ? execution.sentBytes === null || execution.savedBytes === null || execution.sentBytes > execution.originalBytes ||
|
|
93
|
-
execution.savedBytes !== execution.originalBytes - execution.sentBytes : execution.sentBytes !== null || execution.savedBytes !== null) invalid();
|
|
94
|
-
if (execution.preview !== undefined && (typeof execution.preview !== "string" || execution.preview.length > 240 || /[\u0000-\u001f\u007f-\u009f]/u.test(execution.preview))) invalid();
|
|
95
|
-
for (const field of ["startedAt", "endedAt"]) if (execution[field] !== undefined && execution[field] !== null && (!Number.isSafeInteger(execution[field]) || execution[field] < 0 || execution[field] > now + 5000)) invalid();
|
|
96
|
-
if (execution.exitCode !== undefined && execution.exitCode !== null && (!Number.isInteger(execution.exitCode) || Math.abs(execution.exitCode) > 2147483648)) invalid();
|
|
97
|
-
const profile = profileView(execution.profile);
|
|
98
|
-
const active = ["requested", "prepared", "accepted", "running"].includes(execution.state);
|
|
99
|
-
const state = active && connection !== "connected" ? connection === "stale" ? "stale" : "unknown" : execution.state;
|
|
100
|
-
return { executionId: execution.executionId, sessionId: execution.sessionId, name: execution.name, command: execution.command, preview: execution.preview ?? execution.command,
|
|
101
|
-
startedAt: execution.startedAt ?? null, endedAt: execution.endedAt ?? null, exitCode: execution.exitCode ?? null, timedOut: execution.timedOut === true, state, pid: execution.pid, profile, context: execution.context === undefined ? null : contextView(execution.context), originalBytes: execution.originalBytes, sentBytes: execution.sentBytes,
|
|
102
|
-
savedBytes: execution.savedBytes, reason: execution.reason, estimatedSavedTokens: measured ? Math.round(execution.savedBytes / 4) : null };
|
|
103
|
-
});
|
|
104
|
-
return { instanceId: raw.instanceId, pid: raw.pid, agentVersion: raw.agentVersion, updatedAt: raw.updatedAt,
|
|
105
|
-
running, connection, origin, sessions, events, computer: raw.computer ?? null, processName: "lc-agent", executions,
|
|
106
|
-
efficiency: efficiencyView(raw.efficiency),
|
|
107
|
-
savingsSourceId: typeof raw.savingsSourceId === "string" && /^[a-f0-9]{64}$/.test(raw.savingsSourceId) ? raw.savingsSourceId : null,
|
|
108
|
-
lifetimeSavings: lifetimeSavingsView(raw.lifetimeSavings) };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Relay heartbeats refresh this tiny sibling file instead of rewriting the snapshot (#31). It is advisory.
|
|
112
|
-
async function readTransport(file) {
|
|
113
|
-
try {
|
|
114
|
-
const stat = await lstat(file);
|
|
115
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024) return null;
|
|
116
|
-
return JSON.parse(await readFile(file, "utf8"));
|
|
117
|
-
} catch { return null; }
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export async function readMonitors(directory) {
|
|
121
|
-
let entries;
|
|
122
|
-
try {
|
|
123
|
-
const stat = await lstat(directory);
|
|
124
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) invalid();
|
|
125
|
-
entries = (await readdir(directory)).filter(name => /^[a-f0-9]{64}\.json$/.test(name));
|
|
126
|
-
} catch (error) {
|
|
127
|
-
if (error?.code === "ENOENT") return { instances: [], unavailable: 0 };
|
|
128
|
-
return { instances: [], unavailable: 1 };
|
|
129
|
-
}
|
|
130
|
-
const instances = [];
|
|
131
|
-
let unavailable = Math.max(0, entries.length - 32);
|
|
132
|
-
for (const name of entries.slice(0, 32)) {
|
|
133
|
-
try {
|
|
134
|
-
const file = path.join(directory, name);
|
|
135
|
-
const stat = await lstat(file);
|
|
136
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 128 * 1024) invalid();
|
|
137
|
-
const handle = await open(file, "r");
|
|
138
|
-
let contents;
|
|
139
|
-
try {
|
|
140
|
-
const buffer = Buffer.alloc(128 * 1024 + 1);
|
|
141
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
142
|
-
if (bytesRead > 128 * 1024) invalid();
|
|
143
|
-
contents = buffer.subarray(0, bytesRead).toString("utf8");
|
|
144
|
-
} finally { await handle.close(); }
|
|
145
|
-
instances.push(monitorView(JSON.parse(contents), Date.now(), pidAlive, await readTransport(file.replace(/\.json$/, ".transport"))));
|
|
146
|
-
} catch { unavailable++; }
|
|
147
|
-
}
|
|
148
|
-
instances.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
149
|
-
return { instances, unavailable };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function logLine(instance, event) {
|
|
153
|
-
const execution = instance.executions.find(row => row.executionId === event.executionId);
|
|
154
|
-
return [event.at, "pc=" + (instance.computer ?? "unknown"), "pid=" + instance.pid, event.event, event.tool, event.code,
|
|
155
|
-
execution && "cmd=" + (execution.preview ?? execution.command),
|
|
156
|
-
execution?.profile && "model=" + execution.profile.model,
|
|
157
|
-
execution && ["compacted", "passthrough"].includes(event.event) && "saved=~" + compact(execution.estimatedSavedTokens) +
|
|
158
|
-
"tok sent=" + execution.sentBytes + "B reason=" + execution.reason,
|
|
159
|
-
execution?.exitCode !== null && execution?.exitCode !== undefined && "exit=" + execution.exitCode,
|
|
160
|
-
execution?.timedOut && "timeout=true",
|
|
161
|
-
execution?.endedAt && execution.startedAt && "elapsed=" + (execution.endedAt - execution.startedAt) + "ms",
|
|
162
|
-
execution?.context?.directory && "dir=" + execution.context.directory, execution?.context?.taskId && "task=" + execution.context.taskId,
|
|
163
|
-
event.sessionId && "session=" + event.sessionId.slice(0, 8), event.executionId && "execution=" + event.executionId.slice(0, 8)].filter(Boolean).join(" ");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const compact = value => value === null ? "n/a" : value >= 1e6 ? (value / 1e6).toFixed(1) + "M" : value >= 1000 ? (value / 1000).toFixed(1) + "k" : String(value);
|
|
167
|
-
const graphemes = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
168
|
-
const cellWidth = text => /^\p{Mark}+$/u.test(text) ? 0 :
|
|
169
|
-
/[\u1100-\u115f\u2329\u232a\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6\u{20000}-\u{3fffd}]|\p{Emoji_Presentation}|\ufe0f/u.test(text) ? 2 : 1;
|
|
170
|
-
const fit = (value, width, right = false) => {
|
|
171
|
-
width = Math.max(0, width);
|
|
172
|
-
const parts = [...graphemes.segment(String(value).replace(/[\u0000-\u001f\u007f-\u009f]/g, " "))].map(part => part.segment);
|
|
173
|
-
const length = parts.reduce((sum, part) => sum + cellWidth(part), 0);
|
|
174
|
-
const clipped = length > width, limit = clipped && width >= 4 ? width - 3 : width;
|
|
175
|
-
let text = "", used = 0;
|
|
176
|
-
for (const part of parts) {
|
|
177
|
-
const cells = cellWidth(part);
|
|
178
|
-
if (used + cells > limit) break;
|
|
179
|
-
text += part; used += cells;
|
|
180
|
-
}
|
|
181
|
-
if (clipped && width >= 4) { text += "..."; used += 3; }
|
|
182
|
-
const padding = " ".repeat(width - used);
|
|
183
|
-
return right ? padding + text : text + padding;
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
function storyLines(result, computer) {
|
|
187
|
-
const personal = (result.mcp?.instances ?? []).filter(i => i.running && (!computer || i.computer === computer));
|
|
188
|
-
const native = result.instances.filter(i => i.running && (!computer || i.computer === computer));
|
|
189
|
-
const current = personal.flatMap(i => i.calls).find(c => c.state === "running") ?? personal.flatMap(i => i.calls)[0];
|
|
190
|
-
const history = [
|
|
191
|
-
...personal.flatMap(i => [...(i.transitions ?? []), ...i.events].map(e => ({ ...e, computer: i.computer }))),
|
|
192
|
-
...native.flatMap(i => i.events.filter(e => ["execute", "finished", "failed", "cancelled", "unknown"].includes(e.event)).map(e => {
|
|
193
|
-
const work = i.executions.find(x => x.executionId === e.executionId);
|
|
194
|
-
return { ...e, computer: i.computer, state: e.event, preview: work?.preview ?? work?.command ?? e.tool,
|
|
195
|
-
name: work?.executionId.slice(0, 8), context: work?.context, durationMs: work?.endedAt && work.startedAt ? work.endedAt - work.startedAt : null };
|
|
196
|
-
}))
|
|
197
|
-
].sort((a,b) => a.at.localeCompare(b.at));
|
|
198
|
-
const latest = history.at(-1), context = current?.context ?? latest?.context;
|
|
199
|
-
const lines = [
|
|
200
|
-
"INVESTIGATION " + (computer ?? "all PCs") + (result.paused ? " [VIEW FROZEN; work continues]" : " [LIVE]"),
|
|
201
|
-
" Task: " + (context?.taskId ?? context?.project ?? "not reported") + (context?.pr ? " | PR #" + context.pr : "") +
|
|
202
|
-
" | Where: " + (context?.directory ?? "not reported"),
|
|
203
|
-
" " + (current ? "Now: " + (current.agent?.name ?? "unbound caller") + " " + (current.preview ?? current.command) + " [" + current.state + "]" :
|
|
204
|
-
latest ? "Latest: " + (latest.name ?? "unbound caller") + " " + (latest.preview ?? latest.command ?? latest.state) + " [" + latest.state + "]" : "Waiting for the first reported operation.")
|
|
205
|
-
];
|
|
206
|
-
for (const event of history.slice(-(result.storyDepth ?? 2))) lines.push(" " + event.at.slice(11,19) + "Z " +
|
|
207
|
-
(event.name ?? "unbound") + " > " + (event.preview ?? event.command ?? event.state) + " > " + event.state.toUpperCase() +
|
|
208
|
-
(event.durationMs != null ? " (" + event.durationMs + "ms)" : "") + (event.code ? " " + event.code : ""));
|
|
209
|
-
return lines;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function formatMonitor(result, columns = process.stdout.columns ?? 120, height = 40, color = false) {
|
|
213
|
-
const width = Math.max(1, Math.min(160, Number.isFinite(columns) ? Math.floor(columns) : 120));
|
|
214
|
-
height = Math.max(1, Number.isFinite(height) ? Math.floor(height) : 40);
|
|
215
|
-
const live = result.instances.filter(instance => instance.running && ["starting", "connecting", "connected"].includes(instance.connection));
|
|
216
|
-
const pcs = new Map();
|
|
217
|
-
const mcp = (result.mcp?.instances ?? []).filter(instance => instance.running);
|
|
218
|
-
const mcpMetrics = mcp.flatMap(instance => instance.metrics);
|
|
219
|
-
const mcpCalls = mcp.flatMap(instance => instance.calls);
|
|
220
|
-
const queued = mcp.reduce((sum, instance) => sum + (instance.queue ?? 0), 0);
|
|
221
|
-
const errors = mcpMetrics.reduce((sum, item) => sum + item.errors, 0);
|
|
222
|
-
for (const instance of result.instances.filter(item => item.running && (item.computer || item.connection !== "stale")).sort((a, b) => a.instanceId.localeCompare(b.instanceId))) {
|
|
223
|
-
const name = instance.computer ?? "unknown";
|
|
224
|
-
if (!pcs.has(name)) pcs.set(name, []);
|
|
225
|
-
pcs.get(name).push(instance);
|
|
226
|
-
}
|
|
227
|
-
// An unused registration or empty idle slot is not a reported agent at work.
|
|
228
|
-
const agents = (result.activity?.rows ?? []).filter(row => row.kind === "host" ||
|
|
229
|
-
row.state !== "REGISTERED" && !!row.activity?.trim() &&
|
|
230
|
-
!(row.phase === "idle" && row.activity === "presence reported; no activity supplied"));
|
|
231
|
-
for (const agent of agents) if (!pcs.has(agent.computer)) pcs.set(agent.computer, []);
|
|
232
|
-
for (const instance of mcp) if (!pcs.has(instance.computer)) pcs.set(instance.computer, []);
|
|
233
|
-
const agentCount = agents.filter(row => row.kind === "agent").length;
|
|
234
|
-
const hostCount = agents.filter(row => row.kind === "host").length;
|
|
235
|
-
const agentUnavailable = result.activity?.unavailable.some(source => ["presence", "workers", "state_directory"].includes(source));
|
|
236
|
-
const agentSummary = agentUnavailable ? agentCount ? agentCount + " known agent records" : "agent records unavailable" : agentCount + " agent records";
|
|
237
|
-
const hostSummary = result.activity?.unavailable.some(source => source.startsWith("hosts")) ? "hosts unavailable" : hostCount + " observed hosts";
|
|
238
|
-
const active = instance => instance.executions.filter(row => ["requested", "prepared", "accepted", "running"].includes(row.state));
|
|
239
|
-
const jobs = live.reduce((sum, instance) => sum + active(instance).length, 0) + mcpCalls.filter(row => row.state === "running").length + mcp.reduce((sum, instance) => sum + instance.processes.length, 0);
|
|
240
|
-
const stats = live.map(instance => instance.efficiency).filter(Boolean);
|
|
241
|
-
const saved = stats.reduce((sum, item) => sum + item.savedBytes, 0);
|
|
242
|
-
const sent = stats.reduce((sum, item) => sum + item.models.reduce((bytes, group) => bytes + group.sentBytes, 0), 0);
|
|
243
|
-
const tokens = bytes => "~" + Math.round(bytes / 4).toLocaleString("en-US");
|
|
244
|
-
const usageSummary = rows => {
|
|
245
|
-
const sum = field => rows.reduce((total, row) => total + row[field], 0), calls = sum("calls");
|
|
246
|
-
return calls + " calls | avg " + (calls ? Math.round(sum("durationMs") / calls) + "ms" : "n/a") +
|
|
247
|
-
" | output " + compact(sum("outputBytes")) + "B | sent " + compact(sum("sentBytes")) + "B | errors " + sum("errors");
|
|
248
|
-
};
|
|
249
|
-
const pair = (left, right) => fit(left, Math.max(0, width - right.length - 1)) + " " + right;
|
|
250
|
-
const lines = [pair("LC >_ Local Commander MCP", "LIVE MONITOR"), "-".repeat(width), "> fleet status", ""];
|
|
251
|
-
const connected = [...pcs].filter(([name, instances]) => instances.some(instance => instance.connection === "connected") || mcp.some(instance => instance.computer === name)).length;
|
|
252
|
-
if (height >= 36 && width >= 60) lines.push(
|
|
253
|
-
"+-------------+ " + connected + " PCs reporting",
|
|
254
|
-
"| >_ " + fit(jobs ? "RUNNING" : agents.length ? "OBSERVING" : connected ? "READY" : "WAITING", 9) + "| " + jobs + " active commands",
|
|
255
|
-
"| LC >_ | " + agentSummary + " | " + hostSummary,
|
|
256
|
-
"+-------------+ Queue: " + (mcp.length ? queued : "n/a") + " | MCP errors: " + (mcp.length ? errors : "n/a"),
|
|
257
|
-
" ||",
|
|
258
|
-
" ___||___", "");
|
|
259
|
-
else lines.push(connected + " PCs connected | " + jobs + " calls | queue " + (mcp.length ? queued : "n/a") + " | errors " + (mcp.length ? errors : "n/a"), "");
|
|
260
|
-
|
|
261
|
-
const body = [];
|
|
262
|
-
let shown = 0, shownPcs = 0, shownAgents = 0;
|
|
263
|
-
const footer = [
|
|
264
|
-
"-".repeat(width),
|
|
265
|
-
...formatLifetimeSavings(result),
|
|
266
|
-
stats.length ? "Native this run - Raw: " + tokens(saved + sent) + " tokens Sent: " + tokens(sent) + " Saved: " + tokens(saved) +
|
|
267
|
-
" " + (saved + sent ? Math.round(100 * saved / (saved + sent)) : 0) + "%" + (stats.length < live.length ? " (partial)" : "") :
|
|
268
|
-
"Native this run - Raw: n/a Sent: n/a Saved: n/a",
|
|
269
|
-
mcp.length ? "MCP this run: " + usageSummary(mcpMetrics) + " | saved stdout " + tokens(mcpMetrics.reduce((sum, row) => sum + row.savedBytes, 0)) + " tok" :
|
|
270
|
-
"~ Tokens: bytes / 4, this run | model: declared or not reported",
|
|
271
|
-
mcpMetrics.length ? "Review: " + [...mcpMetrics].sort((a, b) => b.sentBytes - a.sentBytes).slice(0, 2)
|
|
272
|
-
.map(row => row.tool + " " + compact(row.sentBytes) + "B / " + row.calls + " calls / " + Math.round(row.durationMs / Math.max(1, row.calls)) + "ms avg").join(" | ") :
|
|
273
|
-
"Server: " + ([...new Set(live.map(instance => instance.origin).filter(Boolean))].join(", ") || "not connected"),
|
|
274
|
-
"Space freeze/resume | Tab focus PC | A all PCs | L timeline | Ctrl+C exit" +
|
|
275
|
-
(result.mcp?.unavailable ? " | MCP snapshots unavailable: " + result.mcp.unavailable : "") +
|
|
276
|
-
(result.activity?.unavailable.length ? " | unavailable: " + result.activity.unavailable.join(",") : "")
|
|
277
|
-
];
|
|
278
|
-
if (height >= 30 && width >= 60) footer.unshift(...storyLines(result, result.focusComputer));
|
|
279
|
-
const capacity = Math.max(0, height - lines.length - footer.length - 1);
|
|
280
|
-
for (const [name, instances] of [...pcs].filter(([name]) => !result.focusComputer || name === result.focusComputer).sort(([a], [b]) => a.localeCompare(b))) {
|
|
281
|
-
const pcStats = instances.filter(instance => live.includes(instance)).map(instance => instance.efficiency).filter(Boolean);
|
|
282
|
-
const pcMcp = mcp.filter(instance => instance.computer === name);
|
|
283
|
-
const pcSaved = pcStats.length || pcMcp.length ? tokens(pcStats.reduce((sum, item) => sum + item.savedBytes, 0) + pcMcp.flatMap(instance => instance.metrics).reduce((sum, row) => sum + row.savedBytes, 0)) : "n/a";
|
|
284
|
-
const executions = new Map(instances.flatMap(instance => instance.executions).map(row => [row.executionId, row]));
|
|
285
|
-
const latest = instances.flatMap(instance => instance.events.filter(event => ["finished", "failed", "cancelled", "unknown"].includes(event.event)))
|
|
286
|
-
.sort((a, b) => b.at.localeCompare(a.at))[0];
|
|
287
|
-
const recent = latest && executions.get(latest.executionId);
|
|
288
|
-
const pcUsage = pcMcp.flatMap(instance => instance.agents);
|
|
289
|
-
const usageFor = runtimeId => pcUsage.filter(row => row.runtimeId === runtimeId);
|
|
290
|
-
const savingsFor = runtimeId => {
|
|
291
|
-
const rows = usageFor(runtimeId);
|
|
292
|
-
return rows.length ? Math.round(rows.reduce((sum, row) => sum + row.savedBytes, 0) / 4) : null;
|
|
293
|
-
};
|
|
294
|
-
const mcpRecent = pcMcp.flatMap(instance => instance.events).sort((a, b) => b.at.localeCompare(a.at))[0];
|
|
295
|
-
const mcpError = pcMcp.flatMap(instance => instance.events).filter(event => event.state === "failed").sort((a, b) => b.at.localeCompare(a.at))[0];
|
|
296
|
-
const mcpMessage = pcMcp.flatMap(instance => instance.events).filter(event => event.tool === "agent_message").sort((a, b) => b.at.localeCompare(a.at))[0];
|
|
297
|
-
const statusLines = [
|
|
298
|
-
" Health: " + (instances.map(i => "relay " + i.connection.toUpperCase()).join(", ") || "relay not observed") +
|
|
299
|
-
(result.health ? " | MCP " + result.health.state + (result.health.status ? " HTTP " + result.health.status : "") + " @ " + result.health.at.slice(11, 19) + "Z" : " | MCP probe not run"),
|
|
300
|
-
...(pcStats.length ? [" Native: " + pcStats.reduce((sum, item) => sum + item.calls, 0) +
|
|
301
|
-
" measured calls | status polls " + pcStats.reduce((sum, item) => sum + item.polls.status, 0) +
|
|
302
|
-
" | output polls " + pcStats.reduce((sum, item) => sum + item.polls.output, 0) +
|
|
303
|
-
" | sessions " + instances.filter(instance => live.includes(instance)).reduce((sum, item) => sum + item.sessions.length, 0)] : []),
|
|
304
|
-
...(pcMcp.length ? [" Diagnostics: queue " + pcMcp.reduce((s,i) => s + (i.queue ?? 0), 0) + " | write errors " +
|
|
305
|
-
pcMcp.reduce((s,i) => s + i.writeErrors, 0) + " | dropped " + pcMcp.reduce((s,i) => s + i.dropped, 0)] : []),
|
|
306
|
-
...(recent ? [" Last: " + latest.at.slice(11, 19) + "Z " + recent.executionId.slice(0, 8) + " " + (recent.preview ?? recent.command) + " " + recent.state.toUpperCase()] : []),
|
|
307
|
-
...(mcpRecent ? [" MCP last: " + mcpRecent.at.slice(11, 19) + "Z " + (mcpRecent.name ?? "unbound") + " " + (mcpRecent.preview ?? mcpRecent.command) + " " + mcpRecent.state.toUpperCase()] : []),
|
|
308
|
-
...(mcpError ? [ " Last error: " + mcpError.at.slice(11, 19) + "Z " + (mcpError.preview ?? mcpError.command) + " " + mcpError.code] : []),
|
|
309
|
-
...(mcpMessage && mcpMessage !== mcpRecent ? [" Message: " + (mcpMessage.name ?? "unbound") + " " + mcpMessage.command + " | task " + (mcpMessage.context.taskId ?? "unreported")] : [])
|
|
310
|
-
];
|
|
311
|
-
const rowCapacity = capacity - statusLines.length;
|
|
312
|
-
const pcAgents = agents.filter(agent => agent.computer === name);
|
|
313
|
-
const rows = [
|
|
314
|
-
...pcMcp.flatMap(instance => instance.processes.map(row => ({ executionId: "process:" + row.id,
|
|
315
|
-
displayId: row.agent?.name ?? row.id.slice(0, 8), kind: "call", pid: row.pid, order: "0:process:" + row.id,
|
|
316
|
-
command: row.preview ?? row.command, context: row.context, state: "running", profile: row.agent?.model ? { model: row.agent.model } : null, estimatedSavedTokens: null }))),
|
|
317
|
-
...pcMcp.flatMap(instance => instance.calls.map(call => ({ executionId: "mcp:" + instance.instanceId + ":" + call.id,
|
|
318
|
-
displayId: call.agent?.name ?? ("mcp" + call.id).slice(0, 8).padEnd(8), kind: "call", pid: instance.pid,
|
|
319
|
-
order: "0:" + String(call.id).padStart(12, "0"), command: (call.agent ? "" : "unbound: ") + (call.preview ?? call.command),
|
|
320
|
-
state: call.state, profile: call.agent?.model ? { model: call.agent.model } : null, estimatedSavedTokens: call.agent ? savingsFor(call.agent.runtimeId) : null,
|
|
321
|
-
detail: [call.context.taskId && "task " + call.context.taskId, call.context.pr && "PR #" + call.context.pr + (call.context.inferred ? " (path)" : ""),
|
|
322
|
-
call.context.project && "project " + call.context.project, call.agent?.phase === "analysis" && "analysis (reported)",
|
|
323
|
-
"queue " + (instance.queue ?? "n/a"), call.context.directory && "dir " + call.context.directory].filter(Boolean).join(" | ") }))),
|
|
324
|
-
...pcAgents.flatMap(agent => {
|
|
325
|
-
const measured = agent.executionIds.map(id => executions.get(id)).filter(row => row?.estimatedSavedTokens !== null && row?.estimatedSavedTokens !== undefined);
|
|
326
|
-
const linked = agent.executionIds.map(id => executions.get(id)).filter(Boolean);
|
|
327
|
-
const running = linked.find(row => ["requested", "prepared", "accepted", "running"].includes(row.state));
|
|
328
|
-
const usage = usageFor(agent.id);
|
|
329
|
-
return [{ executionId: agent.id, displayId: agent.name, kind: agent.kind, pid: agent.pid,
|
|
330
|
-
order: (agent.kind === "host" ? "3:" : "2:") + agent.id,
|
|
331
|
-
command: (agent.phase === "analysis" ? "analysis (reported): " : "") + agent.activity,
|
|
332
|
-
detail: [agent.context?.taskId && "task " + agent.context.taskId,
|
|
333
|
-
agent.context?.pr && "PR #" + agent.context.pr, agent.unread > 0 && "messages " + agent.unread,
|
|
334
|
-
usage.length && usageSummary(usage), agent.context?.directory && "dir " + agent.context.directory].filter(Boolean).join(" | "),
|
|
335
|
-
state: running?.state ?? (agent.state === "RUNNING" ? "REPORTED" : agent.state),
|
|
336
|
-
profile: agent.model ? { model: agent.model } : null,
|
|
337
|
-
estimatedSavedTokens: measured.length || usage.length ? Math.round((measured.reduce((sum, row) => sum + row.savedBytes, 0) + usage.reduce((sum, row) => sum + row.savedBytes, 0)) / 4) : null,
|
|
338
|
-
partial: measured.length > 0 && measured.length < agent.executionIds.length },
|
|
339
|
-
...linked.filter(row => ["requested", "prepared", "accepted", "running"].includes(row.state))
|
|
340
|
-
.map(row => ({ ...row, command: "-> " + (row.preview ?? row.command), order: "1:" + agent.id + ":work:" + row.executionId }))];
|
|
341
|
-
}),
|
|
342
|
-
...instances.flatMap(active).filter(row => !pcAgents.some(agent => agent.executionIds.includes(row.executionId))).map(row => ({ ...row, command: row.preview ?? row.command }))
|
|
343
|
-
];
|
|
344
|
-
const state = instances.some(instance => active(instance).length) || pcMcp.some(instance => instance.calls.length) ? "RUNNING" : pcAgents.length || pcMcp.length ? "OBSERVING" :
|
|
345
|
-
instances.some(instance => instance.connection === "connected") ? "READY" : instances[0]?.connection.toUpperCase() ?? "OBSERVING";
|
|
346
|
-
// Keep each process with its PC heading, including in a short terminal.
|
|
347
|
-
if (rowCapacity - body.length < (rows.length ? width < 60 ? 5 : 4 : 3)) break;
|
|
348
|
-
body.push(pair("PC " + name + " | saved " + pcSaved + " tokens", state));
|
|
349
|
-
shownPcs++;
|
|
350
|
-
if (rows.length) {
|
|
351
|
-
if (width >= 100) body.push(" " + fit("AGENT/ID", 8) + " " + fit("PID", 7) + " " + fit("DOING", width - 71) +
|
|
352
|
-
" " + fit("MODEL", 24) + " " + fit("SAVED TOK", 10, true) + " STATE");
|
|
353
|
-
for (const row of rows.sort((a, b) => (a.order ?? "1:" + a.executionId).localeCompare(b.order ?? "1:" + b.executionId))) {
|
|
354
|
-
const id = row.displayId ?? row.executionId.slice(0, 8), model = row.profile?.model ?? (row.kind === "host" ? "process only" : "not reported");
|
|
355
|
-
const saving = row.estimatedSavedTokens === null ? "n/a" : "~" + compact(row.estimatedSavedTokens) + (row.partial ? "+" : "");
|
|
356
|
-
const entry = width >= 100 ? [" " + fit(id, 8) + " " + fit(row.pid ?? "-", 7, true) + " " + fit(row.command, width - 71) +
|
|
357
|
-
" " + fit(model, 24) + " " + fit(saving, 10, true) + " " + fit(row.state.toUpperCase(), 10)] : [
|
|
358
|
-
pair(" " + id + " " + row.command, row.state.toUpperCase()),
|
|
359
|
-
...(width >= 60 ? [" PID " + (row.pid ?? "-") + " | model " + fit(model, width - 51).trimEnd() + " | saved " + saving + " tok"] :
|
|
360
|
-
[" model " + model, " PID " + (row.pid ?? "-") + " | saved " + saving + " tok"])
|
|
361
|
-
];
|
|
362
|
-
const context = row.context;
|
|
363
|
-
const detail = row.detail || (context ? [context.taskId && "task " + context.taskId,
|
|
364
|
-
context.pr && "PR #" + context.pr, context.directory && "dir " + context.directory].filter(Boolean).join(" | ") : "");
|
|
365
|
-
if (detail) entry.push(" " + detail);
|
|
366
|
-
if (body.length + entry.length + 1 > rowCapacity) break;
|
|
367
|
-
body.push(...entry);
|
|
368
|
-
if (row.kind && row.kind !== "call") shownAgents++;
|
|
369
|
-
else shown++;
|
|
370
|
-
}
|
|
371
|
-
} else body.push(" Waiting for work");
|
|
372
|
-
for (const line of statusLines) if (body.length + 2 <= capacity) body.push(line);
|
|
373
|
-
body.push("");
|
|
374
|
-
}
|
|
375
|
-
if (!pcs.size) body.push(result.instances.length ? "No live PC reported. See logs for completed work and connection history." :
|
|
376
|
-
"No monitoring snapshots. Update/start a monitoring-capable agent.");
|
|
377
|
-
lines.push(...body.slice(0, capacity));
|
|
378
|
-
while (lines.length < height - footer.length - 1) lines.push("");
|
|
379
|
-
lines.push((jobs > shown ? (jobs - shown) + " more active commands | " : "") +
|
|
380
|
-
(agents.length > shownAgents ? (agents.length - shownAgents) + " more agents/hosts | " : "") +
|
|
381
|
-
(pcs.size > shownPcs ? (pcs.size - shownPcs) + " more PCs | " : "") +
|
|
382
|
-
"Unavailable/stale/stopped snapshots: " + (result.unavailable + result.instances.length - live.length));
|
|
383
|
-
lines.push(...footer);
|
|
384
|
-
// Paint after clipping so ANSI sequences never count toward the terminal width.
|
|
385
|
-
return lines.slice(0, height).map(line => {
|
|
386
|
-
const text = fit(line, width);
|
|
387
|
-
if (!color) return text.trimEnd();
|
|
388
|
-
const cyan = /^LC |^> |^PC |^[+|]|^\s+[_|]/.test(line);
|
|
389
|
-
return "\x1b[48;2;15;23;42m\x1b[38;2;" + (cyan ? "0;255;213" : "191;219;254") + "m" +
|
|
390
|
-
text.replace(/\b(RUNNING|READY|CONNECTED|OBSERVING|OBSERVED|REPORTING|QUEUED|FAILED)\b|Saved:.*$/g, value =>
|
|
391
|
-
"\x1b[38;2;" + (value === "FAILED" ? "248;113;113" : value === "QUEUED" ? "251;191;36" : "0;255;213") + "m" + value + "\x1b[38;2;191;219;254m") + "\x1b[0m";
|
|
392
|
-
}).join("\n") + "\n";
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
export function formatInsights(result) {
|
|
396
|
-
const lines = ["OUTPUT TO REVIEW (recent measured bytes; potential savings unproven):"];
|
|
397
|
-
const candidates = result.instances.flatMap(instance => instance.executions.filter(row => row.sentBytes > 0)
|
|
398
|
-
.map(row => ({ ...row, computer: instance.computer ?? "unknown" })))
|
|
399
|
-
.sort((a, b) => b.sentBytes - a.sentBytes).slice(0, 5);
|
|
400
|
-
if (!candidates.length) lines.push(" No measured output candidates yet.");
|
|
401
|
-
for (const row of candidates) lines.push(" " + row.computer + " " + row.executionId.slice(0, 8) + " " + row.command + " | " + compact(row.sentBytes) +
|
|
402
|
-
" B sent | " + (row.reason ?? "unknown") + " | " +
|
|
403
|
-
(row.reason === "validated_compact" ? "already compacted; review remaining detail" :
|
|
404
|
-
row.name === "fs_read" ? "request a focused line range" : row.name === "fs_search" ? "narrow query and maxResults" :
|
|
405
|
-
"review command verbosity; preserve failures/warnings"));
|
|
406
|
-
for (const instance of result.instances) {
|
|
407
|
-
const stats = instance.efficiency;
|
|
408
|
-
if (stats && stats.polls.status + stats.polls.output >= 10) lines.push("POLLING: " + stats.polls.status + " status / " + stats.polls.output +
|
|
409
|
-
" output calls on " + (instance.computer ?? "unknown") + "; wait between polls and fetch final output once when practical.");
|
|
410
|
-
}
|
|
411
|
-
for (const instance of (result.mcp?.instances ?? []).filter(item => item.running)) {
|
|
412
|
-
lines.push("MCP " + instance.computer + " | queue " + (instance.queue ?? "n/a") + " | diagnostics writes " + instance.writes +
|
|
413
|
-
" | write errors " + instance.writeErrors + " | dropped observations " + instance.dropped);
|
|
414
|
-
for (const row of [...instance.metrics].sort((a, b) => b.sentBytes - a.sentBytes).slice(0, 5)) {
|
|
415
|
-
lines.push(" " + row.tool + ": " + row.calls + " calls | " + row.errors + " errors | " + row.sentBytes + "B responses | " + row.outputBytes +
|
|
416
|
-
"B output | saved stdout ~" + Math.round(row.savedBytes / 4) + " tok | " + Math.round(row.durationMs / Math.max(1, row.calls)) + "ms avg");
|
|
417
|
-
if (["process_output", "scheduler_status", "agent_monitor"].includes(row.tool) && row.calls >= 10) lines.push(" Repeated polling: wait between polls; fetch the final result once when practical.");
|
|
418
|
-
if (row.sentBytes > 16_384) lines.push(" Review " + (row.tool === "fs_read" ? "line range" : row.tool === "fs_search" ? "query/maxResults" : "verbosity") + "; preserve exit status, errors, warnings and required detail.");
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
return lines.join("\n") + "\n";
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Fixed cursor positions: rewrite changed lines only, with no clear/redraw or scrolling.
|
|
425
|
-
export function monitorFrame(previous, next, maxRows = Infinity) {
|
|
426
|
-
const before = previous ? previous.trimEnd().split("\n") : [];
|
|
427
|
-
const after = next.trimEnd().split("\n");
|
|
428
|
-
let output = "";
|
|
429
|
-
for (let index = 0; index < Math.min(maxRows, Math.max(before.length, after.length)); index++) {
|
|
430
|
-
if (before[index] !== after[index]) output += "\x1b[" + (index + 1) + ";1H\x1b[2K" + (after[index] ?? "");
|
|
431
|
-
}
|
|
432
|
-
return output;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
export async function runMonitor(directory, command, args) {
|
|
436
|
-
const allowed = command === "logs" ? ["--follow"] : ["--watch", "--json"];
|
|
437
|
-
if (new Set(args).size !== args.length || args.some(arg => !allowed.includes(arg))) throw Error("ARGUMENT_INVALID");
|
|
438
|
-
const repeat = args.includes("--watch") || args.includes("--follow");
|
|
439
|
-
const stop = new AbortController();
|
|
440
|
-
const abort = () => stop.abort();
|
|
441
|
-
process.once("SIGINT", abort);
|
|
442
|
-
process.once("SIGTERM", abort);
|
|
443
|
-
const seen = new Map();
|
|
444
|
-
const previousHosts = new Map();
|
|
445
|
-
const fixedScreen = command === "monitor" && repeat && process.stdout.isTTY && !args.includes("--json");
|
|
446
|
-
let previous = "", previousSize = "", initial = true, insightCheckpoint = "";
|
|
447
|
-
let paused = false, latestResult, focusComputer = null, storyDepth = 2;
|
|
448
|
-
const interactive = fixedScreen && process.stdin.isTTY && typeof process.stdin.setRawMode === "function";
|
|
449
|
-
const wasRaw = process.stdin.isRaw, wasPaused = process.stdin.isPaused();
|
|
450
|
-
const input = chunk => {
|
|
451
|
-
const key = chunk.toString().toLowerCase();
|
|
452
|
-
if (key === "\u0003" || key === "q") stop.abort();
|
|
453
|
-
else if (key === " ") paused = !paused;
|
|
454
|
-
else if (key === "a") focusComputer = null;
|
|
455
|
-
else if (key === "l") storyDepth = storyDepth === 2 ? 5 : 2;
|
|
456
|
-
else if (key === "\t" && latestResult) {
|
|
457
|
-
const computers = [...new Set([...latestResult.instances.map(i => i.computer),
|
|
458
|
-
...latestResult.mcp.instances.map(i => i.computer), ...(latestResult.activity?.rows ?? []).map(i => i.computer)].filter(Boolean))].sort();
|
|
459
|
-
focusComputer = computers[(computers.indexOf(focusComputer) + 1) % computers.length] ?? null;
|
|
460
|
-
}
|
|
461
|
-
};
|
|
462
|
-
if (interactive) { process.stdin.setRawMode(true); process.stdin.on("data", input); process.stdin.resume(); }
|
|
463
|
-
if (fixedScreen) process.stdout.write("\x1b[?1049h\x1b[?25l");
|
|
464
|
-
try {
|
|
465
|
-
do {
|
|
466
|
-
const result = paused && latestResult ? latestResult : await readMonitors(directory);
|
|
467
|
-
if (!(paused && latestResult)) {
|
|
468
|
-
result.mcp = await readMcpActivity();
|
|
469
|
-
result.health = await readMcpHealth();
|
|
470
|
-
if (command === "monitor") result.activity = await readAgentActivity(directory, { previousHosts, signal: stop.signal });
|
|
471
|
-
latestResult = result;
|
|
472
|
-
}
|
|
473
|
-
result.paused = paused; result.focusComputer = focusComputer; result.storyDepth = storyDepth;
|
|
474
|
-
if (command === "logs" && (result.instances.length || result.mcp.instances.length)) {
|
|
475
|
-
const checkpoint = result.mcp.instances.map(instance => instance.instanceId + ":" + Math.floor(instance.metrics.reduce((sum, row) => sum + row.calls, 0) / 5)).join(",");
|
|
476
|
-
if (initial || checkpoint !== insightCheckpoint) { process.stdout.write(formatInsights(result)); insightCheckpoint = checkpoint; }
|
|
477
|
-
for (const instance of result.instances) {
|
|
478
|
-
const after = seen.get(instance.instanceId) ?? 0;
|
|
479
|
-
if (after && instance.events[0]?.seq > after + 1) process.stdout.write("Earlier events were overwritten in the 100-event buffer.\n");
|
|
480
|
-
for (const event of instance.events) if (event.seq > after && !["status", "output"].includes(event.event)) process.stdout.write(logLine(instance, event) + "\n");
|
|
481
|
-
seen.set(instance.instanceId, instance.events.at(-1)?.seq ?? after);
|
|
482
|
-
}
|
|
483
|
-
for (const instance of result.mcp.instances) {
|
|
484
|
-
const key = "mcp:" + instance.instanceId;
|
|
485
|
-
const transitionsKey = key + ":starts";
|
|
486
|
-
const afterStart = seen.get(transitionsKey) ?? 0;
|
|
487
|
-
for (const event of instance.transitions ?? []) if (event.seq > afterStart) {
|
|
488
|
-
process.stdout.write([event.at, "pc=" + instance.computer, "agent=" + (event.name ?? "unbound"), event.preview, "RUNNING",
|
|
489
|
-
"model=" + (event.model ?? "not_reported"), event.context.directory && "dir=" + event.context.directory,
|
|
490
|
-
event.context.taskId && "task=" + event.context.taskId].filter(Boolean).join(" ") + "\n");
|
|
491
|
-
}
|
|
492
|
-
seen.set(transitionsKey, instance.transitions?.at(-1)?.seq ?? afterStart);
|
|
493
|
-
// Request IDs finish out of order: a per-event key tracks actual completions, not numeric high-water IDs.
|
|
494
|
-
for (const event of instance.events) {
|
|
495
|
-
const eventKey = key + ":" + event.id;
|
|
496
|
-
if (seen.has(eventKey)) continue;
|
|
497
|
-
process.stdout.write([event.at, "pc=" + instance.computer, "agent=" + (event.name ?? "unbound"), event.preview ?? event.command, event.state, "model=" + (event.model ?? "not_reported"),
|
|
498
|
-
"elapsed=" + event.durationMs + "ms", event.exitCode !== null && "exit=" + event.exitCode, event.timedOut && "timeout=true",
|
|
499
|
-
event.code, event.context.directory && "dir=" + event.context.directory, event.context.taskId && "task=" + event.context.taskId,
|
|
500
|
-
event.context.pr && "PR=" + event.context.pr, "sent=" + event.sentBytes + "B", "saved=~" + Math.round(event.savedBytes / 4) + "tok"].filter(Boolean).join(" ") + "\n");
|
|
501
|
-
seen.set(eventKey, 1);
|
|
502
|
-
}
|
|
503
|
-
for (const seenKey of seen.keys()) if (seenKey.startsWith(key + ":") && seenKey !== transitionsKey && !instance.events.some(event => seenKey === key + ":" + event.id)) seen.delete(seenKey);
|
|
504
|
-
}
|
|
505
|
-
for (const key of seen.keys()) if (!result.instances.some(instance => instance.instanceId === key) &&
|
|
506
|
-
!result.mcp.instances.some(instance => key.startsWith("mcp:" + instance.instanceId + ":"))) seen.delete(key);
|
|
507
|
-
if (result.unavailable) process.stderr.write("Some monitoring snapshots are unavailable.\n");
|
|
508
|
-
} else {
|
|
509
|
-
const columns = Math.max(1, (process.stdout.columns ?? 120) - 1);
|
|
510
|
-
const rows = Math.max(1, (process.stdout.rows ?? 40) - 1);
|
|
511
|
-
const size = columns + "x" + rows;
|
|
512
|
-
// A resized terminal must repaint even when the snapshot is unchanged.
|
|
513
|
-
if (fixedScreen && size !== previousSize) previous = "";
|
|
514
|
-
const next = args.includes("--json") ? JSON.stringify(result) + "\n" :
|
|
515
|
-
formatMonitor(result, fixedScreen ? columns : undefined, fixedScreen ? rows : undefined,
|
|
516
|
-
process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb");
|
|
517
|
-
process.stdout.write(fixedScreen ? monitorFrame(previous, next, Math.max(1, (process.stdout.rows ?? 40) - 1)) : next);
|
|
518
|
-
previous = next;
|
|
519
|
-
previousSize = size;
|
|
520
|
-
}
|
|
521
|
-
initial = false;
|
|
522
|
-
if (repeat) await delay(2000, undefined, { signal: stop.signal }).catch(error => { if (error.name !== "AbortError") throw error; });
|
|
523
|
-
} while (repeat && !stop.signal.aborted);
|
|
524
|
-
} finally {
|
|
525
|
-
if (interactive) { process.stdin.removeListener("data", input); process.stdin.setRawMode(!!wasRaw); if (wasPaused) process.stdin.pause(); }
|
|
526
|
-
if (fixedScreen) process.stdout.write("\x1b[0m\x1b[?25h\x1b[?1049l");
|
|
527
|
-
process.removeListener("SIGINT", abort);
|
|
528
|
-
process.removeListener("SIGTERM", abort);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
1
|
+
import { lstat, open, readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { lifetimeSavingsView, formatLifetimeSavings } from "./monitor-savings.mjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
import { readAgentActivity } from "./monitor-agents.mjs";
|
|
6
|
+
import { readMcpActivity, readMcpHealth, contextView } from "./monitor-mcp.mjs";
|
|
7
|
+
|
|
8
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
9
|
+
const STATES = new Set(["starting", "connecting", "connected", "disconnected", "stopped", "failed"]);
|
|
10
|
+
const EVENTS = new Set([...STATES, "execute", "prepare", "status", "output", "cancel", "finished", "cancelled", "unknown", "result", "request_failed", "rejected", "connection_error", "compacted", "passthrough"]);
|
|
11
|
+
const iso = value => typeof value === "string" && /^\d{4}-\d\d-\d\dT[\d:.]+Z$/.test(value) && Number.isFinite(Date.parse(value));
|
|
12
|
+
const invalid = () => { throw Error("MONITOR_DATA_INVALID"); };
|
|
13
|
+
const count = value => Number.isSafeInteger(value) && value >= 0 && value <= 1_000_000_000_000;
|
|
14
|
+
const label = value => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,79}$/.test(value);
|
|
15
|
+
const EXECUTION_STATES = new Set(["requested", "prepared", "accepted", "running", "finished", "failed", "cancelled", "rejected", "unknown", "missing"]);
|
|
16
|
+
const PROCESS_NAMES = new Set(["node", "npm", "npx", "git", "rg", "powershell", "pwsh", "cmd", "tsc", "python", "dotnet", "command",
|
|
17
|
+
"fs_list", "fs_read", "fs_search", "fs_write", "fs_replace", "git_status", "git_diff", "git_log", "project_snapshot", "localhost_request"]);
|
|
18
|
+
const COMMANDS = new Set(["node --test", ...["status", "diff", "log", "show", "fetch", "pull", "push", "commit"].map(value => "git " + value),
|
|
19
|
+
...["test", "install", "ci", "pack", "publish"].map(value => "npm " + value),
|
|
20
|
+
...["test", "build", "typecheck", "lint", "dev", "start"].map(value => "npm run " + value),
|
|
21
|
+
...[...PROCESS_NAMES].flatMap(value => [value, value + " [args]"])]);
|
|
22
|
+
function profileView(raw) {
|
|
23
|
+
if (raw === null) return null;
|
|
24
|
+
if (!raw || !label(raw.model)) invalid();
|
|
25
|
+
return { model: raw.model };
|
|
26
|
+
}
|
|
27
|
+
function efficiencyView(raw) {
|
|
28
|
+
if (raw === undefined) return null; // Older agents did not measure monitor savings.
|
|
29
|
+
if (!raw || raw.scope !== "this_run" || !count(raw.calls) || !count(raw.polls?.status) || !count(raw.polls?.output) ||
|
|
30
|
+
!Array.isArray(raw.models) || raw.models.length > 32) invalid();
|
|
31
|
+
const models = raw.models.map(group => {
|
|
32
|
+
if (!group || (group.model !== null && !label(group.model)) || !count(group.calls) || !count(group.savedBytes) || !count(group.sentBytes)) invalid();
|
|
33
|
+
return { model: group.model, calls: group.calls, savedBytes: group.savedBytes,
|
|
34
|
+
sentBytes: group.sentBytes, estimatedSavedTokens: Math.round(group.savedBytes / 4) };
|
|
35
|
+
});
|
|
36
|
+
if (models.reduce((sum, group) => sum + group.calls, 0) !== raw.calls) invalid();
|
|
37
|
+
const savedBytes = models.reduce((sum, group) => sum + group.savedBytes, 0);
|
|
38
|
+
return { scope: "this_run", calls: raw.calls, polls: { status: raw.polls.status, output: raw.polls.output }, models,
|
|
39
|
+
savedBytes, estimatedSavedTokens: Math.round(savedBytes / 4),
|
|
40
|
+
unclassifiedSavedBytes: models.filter(group => group.model === null).reduce((sum, group) => sum + group.savedBytes, 0) };
|
|
41
|
+
}
|
|
42
|
+
function pidAlive(pid) {
|
|
43
|
+
try { process.kill(pid, 0); return true; }
|
|
44
|
+
catch (error) { return error?.code === "EPERM"; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Explicit allowlist projection: never print extra fields from a local file.
|
|
48
|
+
export function monitorView(raw, now = Date.now(), alive = pidAlive, transport = null) {
|
|
49
|
+
if (!raw || raw.schemaVersion !== 1 || !UUID.test(raw.instanceId) || !Number.isSafeInteger(raw.pid) || raw.pid <= 0 ||
|
|
50
|
+
typeof raw.agentVersion !== "string" || !/^[A-Za-z0-9._+-]{1,64}$/.test(raw.agentVersion) || !iso(raw.updatedAt) ||
|
|
51
|
+
!STATES.has(raw.connection?.state) || !Array.isArray(raw.sessions) || raw.sessions.length > 32 ||
|
|
52
|
+
!Array.isArray(raw.events) || raw.events.length > 100) invalid();
|
|
53
|
+
let origin = null;
|
|
54
|
+
if (raw.connection.origin !== null) {
|
|
55
|
+
const url = new URL(raw.connection.origin);
|
|
56
|
+
if (url.origin !== raw.connection.origin || url.username || url.password ||
|
|
57
|
+
!(url.protocol === "https:" || (url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)))) invalid();
|
|
58
|
+
origin = url.origin;
|
|
59
|
+
}
|
|
60
|
+
const sessions = raw.sessions.map(session => {
|
|
61
|
+
if (!session || !UUID.test(session.sessionId) || !UUID.test(session.workspaceGrantId) || !iso(session.lastSeenAt)) invalid();
|
|
62
|
+
return { sessionId: session.sessionId, workspaceGrantId: session.workspaceGrantId, lastSeenAt: session.lastSeenAt, model: null };
|
|
63
|
+
});
|
|
64
|
+
const events = raw.events.map(event => {
|
|
65
|
+
if (!event || !Number.isSafeInteger(event.seq) || event.seq < 1 || !iso(event.at) || !EVENTS.has(event.event) ||
|
|
66
|
+
(event.sessionId !== undefined && !UUID.test(event.sessionId)) || (event.executionId !== undefined && !UUID.test(event.executionId)) ||
|
|
67
|
+
(event.tool !== undefined && (typeof event.tool !== "string" || !/^[a-z_]{1,64}$/.test(event.tool))) ||
|
|
68
|
+
(event.code !== undefined && (typeof event.code !== "string" || !/^[A-Z0-9_]{1,64}$/.test(event.code)))) invalid();
|
|
69
|
+
return { seq: event.seq, at: event.at, event: event.event,
|
|
70
|
+
...(event.sessionId ? { sessionId: event.sessionId } : {}), ...(event.executionId ? { executionId: event.executionId } : {}),
|
|
71
|
+
...(event.tool ? { tool: event.tool } : {}), ...(event.code ? { code: event.code } : {}) };
|
|
72
|
+
});
|
|
73
|
+
const running = alive(raw.pid);
|
|
74
|
+
if (raw.transportAt !== undefined && !iso(raw.transportAt)) invalid();
|
|
75
|
+
// A heartbeat file counts only for this snapshot's instance and connection (#31).
|
|
76
|
+
const heartbeat = raw.transportAt !== undefined && transport?.instanceId === raw.instanceId && transport.epoch === raw.connection.epoch &&
|
|
77
|
+
iso(transport.transportAt) && Date.parse(transport.transportAt) > Date.parse(raw.transportAt) ? transport.transportAt : raw.transportAt;
|
|
78
|
+
const age = now - Date.parse(raw.connection.state === "connected" ? heartbeat ?? raw.updatedAt : raw.updatedAt);
|
|
79
|
+
const freshnessMs = raw.freshnessMs ?? 10_000;
|
|
80
|
+
if (!Number.isInteger(freshnessMs) || freshnessMs < 1000 || freshnessMs > 180_000) invalid();
|
|
81
|
+
const connection = !running ? "stopped" : age < -5000 || age > freshnessMs ? "stale" : raw.connection.state;
|
|
82
|
+
if (raw.computer !== undefined && (typeof raw.computer !== "string" || !/^[A-Za-z0-9._-]{1,64}$/.test(raw.computer))) invalid();
|
|
83
|
+
if (raw.processName !== undefined && raw.processName !== "lc-agent") invalid();
|
|
84
|
+
if (raw.executions !== undefined && (!Array.isArray(raw.executions) || raw.executions.length > 64)) invalid();
|
|
85
|
+
const executions = (raw.executions ?? []).map(execution => {
|
|
86
|
+
if (!execution || !UUID.test(execution.executionId) || !UUID.test(execution.sessionId) || !PROCESS_NAMES.has(execution.name) ||
|
|
87
|
+
!COMMANDS.has(execution.command) || !EXECUTION_STATES.has(execution.state) ||
|
|
88
|
+
(execution.pid !== null && (!Number.isSafeInteger(execution.pid) || execution.pid <= 0)) ||
|
|
89
|
+
![execution.originalBytes, execution.sentBytes, execution.savedBytes].every(value => value === null || count(value)) ||
|
|
90
|
+
(execution.reason !== null && (typeof execution.reason !== "string" || !/^[a-z_]{1,80}$/.test(execution.reason)))) invalid();
|
|
91
|
+
const measured = execution.originalBytes !== null;
|
|
92
|
+
if (measured ? execution.sentBytes === null || execution.savedBytes === null || execution.sentBytes > execution.originalBytes ||
|
|
93
|
+
execution.savedBytes !== execution.originalBytes - execution.sentBytes : execution.sentBytes !== null || execution.savedBytes !== null) invalid();
|
|
94
|
+
if (execution.preview !== undefined && (typeof execution.preview !== "string" || execution.preview.length > 240 || /[\u0000-\u001f\u007f-\u009f]/u.test(execution.preview))) invalid();
|
|
95
|
+
for (const field of ["startedAt", "endedAt"]) if (execution[field] !== undefined && execution[field] !== null && (!Number.isSafeInteger(execution[field]) || execution[field] < 0 || execution[field] > now + 5000)) invalid();
|
|
96
|
+
if (execution.exitCode !== undefined && execution.exitCode !== null && (!Number.isInteger(execution.exitCode) || Math.abs(execution.exitCode) > 2147483648)) invalid();
|
|
97
|
+
const profile = profileView(execution.profile);
|
|
98
|
+
const active = ["requested", "prepared", "accepted", "running"].includes(execution.state);
|
|
99
|
+
const state = active && connection !== "connected" ? connection === "stale" ? "stale" : "unknown" : execution.state;
|
|
100
|
+
return { executionId: execution.executionId, sessionId: execution.sessionId, name: execution.name, command: execution.command, preview: execution.preview ?? execution.command,
|
|
101
|
+
startedAt: execution.startedAt ?? null, endedAt: execution.endedAt ?? null, exitCode: execution.exitCode ?? null, timedOut: execution.timedOut === true, state, pid: execution.pid, profile, context: execution.context === undefined ? null : contextView(execution.context), originalBytes: execution.originalBytes, sentBytes: execution.sentBytes,
|
|
102
|
+
savedBytes: execution.savedBytes, reason: execution.reason, estimatedSavedTokens: measured ? Math.round(execution.savedBytes / 4) : null };
|
|
103
|
+
});
|
|
104
|
+
return { instanceId: raw.instanceId, pid: raw.pid, agentVersion: raw.agentVersion, updatedAt: raw.updatedAt,
|
|
105
|
+
running, connection, origin, sessions, events, computer: raw.computer ?? null, processName: "lc-agent", executions,
|
|
106
|
+
efficiency: efficiencyView(raw.efficiency),
|
|
107
|
+
savingsSourceId: typeof raw.savingsSourceId === "string" && /^[a-f0-9]{64}$/.test(raw.savingsSourceId) ? raw.savingsSourceId : null,
|
|
108
|
+
lifetimeSavings: lifetimeSavingsView(raw.lifetimeSavings) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Relay heartbeats refresh this tiny sibling file instead of rewriting the snapshot (#31). It is advisory.
|
|
112
|
+
async function readTransport(file) {
|
|
113
|
+
try {
|
|
114
|
+
const stat = await lstat(file);
|
|
115
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024) return null;
|
|
116
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
117
|
+
} catch { return null; }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function readMonitors(directory) {
|
|
121
|
+
let entries;
|
|
122
|
+
try {
|
|
123
|
+
const stat = await lstat(directory);
|
|
124
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) invalid();
|
|
125
|
+
entries = (await readdir(directory)).filter(name => /^[a-f0-9]{64}\.json$/.test(name));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error?.code === "ENOENT") return { instances: [], unavailable: 0 };
|
|
128
|
+
return { instances: [], unavailable: 1 };
|
|
129
|
+
}
|
|
130
|
+
const instances = [];
|
|
131
|
+
let unavailable = Math.max(0, entries.length - 32);
|
|
132
|
+
for (const name of entries.slice(0, 32)) {
|
|
133
|
+
try {
|
|
134
|
+
const file = path.join(directory, name);
|
|
135
|
+
const stat = await lstat(file);
|
|
136
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 128 * 1024) invalid();
|
|
137
|
+
const handle = await open(file, "r");
|
|
138
|
+
let contents;
|
|
139
|
+
try {
|
|
140
|
+
const buffer = Buffer.alloc(128 * 1024 + 1);
|
|
141
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
142
|
+
if (bytesRead > 128 * 1024) invalid();
|
|
143
|
+
contents = buffer.subarray(0, bytesRead).toString("utf8");
|
|
144
|
+
} finally { await handle.close(); }
|
|
145
|
+
instances.push(monitorView(JSON.parse(contents), Date.now(), pidAlive, await readTransport(file.replace(/\.json$/, ".transport"))));
|
|
146
|
+
} catch { unavailable++; }
|
|
147
|
+
}
|
|
148
|
+
instances.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
149
|
+
return { instances, unavailable };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function logLine(instance, event) {
|
|
153
|
+
const execution = instance.executions.find(row => row.executionId === event.executionId);
|
|
154
|
+
return [event.at, "pc=" + (instance.computer ?? "unknown"), "pid=" + instance.pid, event.event, event.tool, event.code,
|
|
155
|
+
execution && "cmd=" + (execution.preview ?? execution.command),
|
|
156
|
+
execution?.profile && "model=" + execution.profile.model,
|
|
157
|
+
execution && ["compacted", "passthrough"].includes(event.event) && "saved=~" + compact(execution.estimatedSavedTokens) +
|
|
158
|
+
"tok sent=" + execution.sentBytes + "B reason=" + execution.reason,
|
|
159
|
+
execution?.exitCode !== null && execution?.exitCode !== undefined && "exit=" + execution.exitCode,
|
|
160
|
+
execution?.timedOut && "timeout=true",
|
|
161
|
+
execution?.endedAt && execution.startedAt && "elapsed=" + (execution.endedAt - execution.startedAt) + "ms",
|
|
162
|
+
execution?.context?.directory && "dir=" + execution.context.directory, execution?.context?.taskId && "task=" + execution.context.taskId,
|
|
163
|
+
event.sessionId && "session=" + event.sessionId.slice(0, 8), event.executionId && "execution=" + event.executionId.slice(0, 8)].filter(Boolean).join(" ");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const compact = value => value === null ? "n/a" : value >= 1e6 ? (value / 1e6).toFixed(1) + "M" : value >= 1000 ? (value / 1000).toFixed(1) + "k" : String(value);
|
|
167
|
+
const graphemes = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
168
|
+
const cellWidth = text => /^\p{Mark}+$/u.test(text) ? 0 :
|
|
169
|
+
/[\u1100-\u115f\u2329\u232a\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff00-\uff60\uffe0-\uffe6\u{20000}-\u{3fffd}]|\p{Emoji_Presentation}|\ufe0f/u.test(text) ? 2 : 1;
|
|
170
|
+
const fit = (value, width, right = false) => {
|
|
171
|
+
width = Math.max(0, width);
|
|
172
|
+
const parts = [...graphemes.segment(String(value).replace(/[\u0000-\u001f\u007f-\u009f]/g, " "))].map(part => part.segment);
|
|
173
|
+
const length = parts.reduce((sum, part) => sum + cellWidth(part), 0);
|
|
174
|
+
const clipped = length > width, limit = clipped && width >= 4 ? width - 3 : width;
|
|
175
|
+
let text = "", used = 0;
|
|
176
|
+
for (const part of parts) {
|
|
177
|
+
const cells = cellWidth(part);
|
|
178
|
+
if (used + cells > limit) break;
|
|
179
|
+
text += part; used += cells;
|
|
180
|
+
}
|
|
181
|
+
if (clipped && width >= 4) { text += "..."; used += 3; }
|
|
182
|
+
const padding = " ".repeat(width - used);
|
|
183
|
+
return right ? padding + text : text + padding;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
function storyLines(result, computer) {
|
|
187
|
+
const personal = (result.mcp?.instances ?? []).filter(i => i.running && (!computer || i.computer === computer));
|
|
188
|
+
const native = result.instances.filter(i => i.running && (!computer || i.computer === computer));
|
|
189
|
+
const current = personal.flatMap(i => i.calls).find(c => c.state === "running") ?? personal.flatMap(i => i.calls)[0];
|
|
190
|
+
const history = [
|
|
191
|
+
...personal.flatMap(i => [...(i.transitions ?? []), ...i.events].map(e => ({ ...e, computer: i.computer }))),
|
|
192
|
+
...native.flatMap(i => i.events.filter(e => ["execute", "finished", "failed", "cancelled", "unknown"].includes(e.event)).map(e => {
|
|
193
|
+
const work = i.executions.find(x => x.executionId === e.executionId);
|
|
194
|
+
return { ...e, computer: i.computer, state: e.event, preview: work?.preview ?? work?.command ?? e.tool,
|
|
195
|
+
name: work?.executionId.slice(0, 8), context: work?.context, durationMs: work?.endedAt && work.startedAt ? work.endedAt - work.startedAt : null };
|
|
196
|
+
}))
|
|
197
|
+
].sort((a,b) => a.at.localeCompare(b.at));
|
|
198
|
+
const latest = history.at(-1), context = current?.context ?? latest?.context;
|
|
199
|
+
const lines = [
|
|
200
|
+
"INVESTIGATION " + (computer ?? "all PCs") + (result.paused ? " [VIEW FROZEN; work continues]" : " [LIVE]"),
|
|
201
|
+
" Task: " + (context?.taskId ?? context?.project ?? "not reported") + (context?.pr ? " | PR #" + context.pr : "") +
|
|
202
|
+
" | Where: " + (context?.directory ?? "not reported"),
|
|
203
|
+
" " + (current ? "Now: " + (current.agent?.name ?? "unbound caller") + " " + (current.preview ?? current.command) + " [" + current.state + "]" :
|
|
204
|
+
latest ? "Latest: " + (latest.name ?? "unbound caller") + " " + (latest.preview ?? latest.command ?? latest.state) + " [" + latest.state + "]" : "Waiting for the first reported operation.")
|
|
205
|
+
];
|
|
206
|
+
for (const event of history.slice(-(result.storyDepth ?? 2))) lines.push(" " + event.at.slice(11,19) + "Z " +
|
|
207
|
+
(event.name ?? "unbound") + " > " + (event.preview ?? event.command ?? event.state) + " > " + event.state.toUpperCase() +
|
|
208
|
+
(event.durationMs != null ? " (" + event.durationMs + "ms)" : "") + (event.code ? " " + event.code : ""));
|
|
209
|
+
return lines;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function formatMonitor(result, columns = process.stdout.columns ?? 120, height = 40, color = false) {
|
|
213
|
+
const width = Math.max(1, Math.min(160, Number.isFinite(columns) ? Math.floor(columns) : 120));
|
|
214
|
+
height = Math.max(1, Number.isFinite(height) ? Math.floor(height) : 40);
|
|
215
|
+
const live = result.instances.filter(instance => instance.running && ["starting", "connecting", "connected"].includes(instance.connection));
|
|
216
|
+
const pcs = new Map();
|
|
217
|
+
const mcp = (result.mcp?.instances ?? []).filter(instance => instance.running);
|
|
218
|
+
const mcpMetrics = mcp.flatMap(instance => instance.metrics);
|
|
219
|
+
const mcpCalls = mcp.flatMap(instance => instance.calls);
|
|
220
|
+
const queued = mcp.reduce((sum, instance) => sum + (instance.queue ?? 0), 0);
|
|
221
|
+
const errors = mcpMetrics.reduce((sum, item) => sum + item.errors, 0);
|
|
222
|
+
for (const instance of result.instances.filter(item => item.running && (item.computer || item.connection !== "stale")).sort((a, b) => a.instanceId.localeCompare(b.instanceId))) {
|
|
223
|
+
const name = instance.computer ?? "unknown";
|
|
224
|
+
if (!pcs.has(name)) pcs.set(name, []);
|
|
225
|
+
pcs.get(name).push(instance);
|
|
226
|
+
}
|
|
227
|
+
// An unused registration or empty idle slot is not a reported agent at work.
|
|
228
|
+
const agents = (result.activity?.rows ?? []).filter(row => row.kind === "host" ||
|
|
229
|
+
row.state !== "REGISTERED" && !!row.activity?.trim() &&
|
|
230
|
+
!(row.phase === "idle" && row.activity === "presence reported; no activity supplied"));
|
|
231
|
+
for (const agent of agents) if (!pcs.has(agent.computer)) pcs.set(agent.computer, []);
|
|
232
|
+
for (const instance of mcp) if (!pcs.has(instance.computer)) pcs.set(instance.computer, []);
|
|
233
|
+
const agentCount = agents.filter(row => row.kind === "agent").length;
|
|
234
|
+
const hostCount = agents.filter(row => row.kind === "host").length;
|
|
235
|
+
const agentUnavailable = result.activity?.unavailable.some(source => ["presence", "workers", "state_directory"].includes(source));
|
|
236
|
+
const agentSummary = agentUnavailable ? agentCount ? agentCount + " known agent records" : "agent records unavailable" : agentCount + " agent records";
|
|
237
|
+
const hostSummary = result.activity?.unavailable.some(source => source.startsWith("hosts")) ? "hosts unavailable" : hostCount + " observed hosts";
|
|
238
|
+
const active = instance => instance.executions.filter(row => ["requested", "prepared", "accepted", "running"].includes(row.state));
|
|
239
|
+
const jobs = live.reduce((sum, instance) => sum + active(instance).length, 0) + mcpCalls.filter(row => row.state === "running").length + mcp.reduce((sum, instance) => sum + instance.processes.length, 0);
|
|
240
|
+
const stats = live.map(instance => instance.efficiency).filter(Boolean);
|
|
241
|
+
const saved = stats.reduce((sum, item) => sum + item.savedBytes, 0);
|
|
242
|
+
const sent = stats.reduce((sum, item) => sum + item.models.reduce((bytes, group) => bytes + group.sentBytes, 0), 0);
|
|
243
|
+
const tokens = bytes => "~" + Math.round(bytes / 4).toLocaleString("en-US");
|
|
244
|
+
const usageSummary = rows => {
|
|
245
|
+
const sum = field => rows.reduce((total, row) => total + row[field], 0), calls = sum("calls");
|
|
246
|
+
return calls + " calls | avg " + (calls ? Math.round(sum("durationMs") / calls) + "ms" : "n/a") +
|
|
247
|
+
" | output " + compact(sum("outputBytes")) + "B | sent " + compact(sum("sentBytes")) + "B | errors " + sum("errors");
|
|
248
|
+
};
|
|
249
|
+
const pair = (left, right) => fit(left, Math.max(0, width - right.length - 1)) + " " + right;
|
|
250
|
+
const lines = [pair("LC >_ Local Commander MCP", "LIVE MONITOR"), "-".repeat(width), "> fleet status", ""];
|
|
251
|
+
const connected = [...pcs].filter(([name, instances]) => instances.some(instance => instance.connection === "connected") || mcp.some(instance => instance.computer === name)).length;
|
|
252
|
+
if (height >= 36 && width >= 60) lines.push(
|
|
253
|
+
"+-------------+ " + connected + " PCs reporting",
|
|
254
|
+
"| >_ " + fit(jobs ? "RUNNING" : agents.length ? "OBSERVING" : connected ? "READY" : "WAITING", 9) + "| " + jobs + " active commands",
|
|
255
|
+
"| LC >_ | " + agentSummary + " | " + hostSummary,
|
|
256
|
+
"+-------------+ Queue: " + (mcp.length ? queued : "n/a") + " | MCP errors: " + (mcp.length ? errors : "n/a"),
|
|
257
|
+
" ||",
|
|
258
|
+
" ___||___", "");
|
|
259
|
+
else lines.push(connected + " PCs connected | " + jobs + " calls | queue " + (mcp.length ? queued : "n/a") + " | errors " + (mcp.length ? errors : "n/a"), "");
|
|
260
|
+
|
|
261
|
+
const body = [];
|
|
262
|
+
let shown = 0, shownPcs = 0, shownAgents = 0;
|
|
263
|
+
const footer = [
|
|
264
|
+
"-".repeat(width),
|
|
265
|
+
...formatLifetimeSavings(result),
|
|
266
|
+
stats.length ? "Native this run - Raw: " + tokens(saved + sent) + " tokens Sent: " + tokens(sent) + " Saved: " + tokens(saved) +
|
|
267
|
+
" " + (saved + sent ? Math.round(100 * saved / (saved + sent)) : 0) + "%" + (stats.length < live.length ? " (partial)" : "") :
|
|
268
|
+
"Native this run - Raw: n/a Sent: n/a Saved: n/a",
|
|
269
|
+
mcp.length ? "MCP this run: " + usageSummary(mcpMetrics) + " | saved stdout " + tokens(mcpMetrics.reduce((sum, row) => sum + row.savedBytes, 0)) + " tok" :
|
|
270
|
+
"~ Tokens: bytes / 4, this run | model: declared or not reported",
|
|
271
|
+
mcpMetrics.length ? "Review: " + [...mcpMetrics].sort((a, b) => b.sentBytes - a.sentBytes).slice(0, 2)
|
|
272
|
+
.map(row => row.tool + " " + compact(row.sentBytes) + "B / " + row.calls + " calls / " + Math.round(row.durationMs / Math.max(1, row.calls)) + "ms avg").join(" | ") :
|
|
273
|
+
"Server: " + ([...new Set(live.map(instance => instance.origin).filter(Boolean))].join(", ") || "not connected"),
|
|
274
|
+
"Space freeze/resume | Tab focus PC | A all PCs | L timeline | Ctrl+C exit" +
|
|
275
|
+
(result.mcp?.unavailable ? " | MCP snapshots unavailable: " + result.mcp.unavailable : "") +
|
|
276
|
+
(result.activity?.unavailable.length ? " | unavailable: " + result.activity.unavailable.join(",") : "")
|
|
277
|
+
];
|
|
278
|
+
if (height >= 30 && width >= 60) footer.unshift(...storyLines(result, result.focusComputer));
|
|
279
|
+
const capacity = Math.max(0, height - lines.length - footer.length - 1);
|
|
280
|
+
for (const [name, instances] of [...pcs].filter(([name]) => !result.focusComputer || name === result.focusComputer).sort(([a], [b]) => a.localeCompare(b))) {
|
|
281
|
+
const pcStats = instances.filter(instance => live.includes(instance)).map(instance => instance.efficiency).filter(Boolean);
|
|
282
|
+
const pcMcp = mcp.filter(instance => instance.computer === name);
|
|
283
|
+
const pcSaved = pcStats.length || pcMcp.length ? tokens(pcStats.reduce((sum, item) => sum + item.savedBytes, 0) + pcMcp.flatMap(instance => instance.metrics).reduce((sum, row) => sum + row.savedBytes, 0)) : "n/a";
|
|
284
|
+
const executions = new Map(instances.flatMap(instance => instance.executions).map(row => [row.executionId, row]));
|
|
285
|
+
const latest = instances.flatMap(instance => instance.events.filter(event => ["finished", "failed", "cancelled", "unknown"].includes(event.event)))
|
|
286
|
+
.sort((a, b) => b.at.localeCompare(a.at))[0];
|
|
287
|
+
const recent = latest && executions.get(latest.executionId);
|
|
288
|
+
const pcUsage = pcMcp.flatMap(instance => instance.agents);
|
|
289
|
+
const usageFor = runtimeId => pcUsage.filter(row => row.runtimeId === runtimeId);
|
|
290
|
+
const savingsFor = runtimeId => {
|
|
291
|
+
const rows = usageFor(runtimeId);
|
|
292
|
+
return rows.length ? Math.round(rows.reduce((sum, row) => sum + row.savedBytes, 0) / 4) : null;
|
|
293
|
+
};
|
|
294
|
+
const mcpRecent = pcMcp.flatMap(instance => instance.events).sort((a, b) => b.at.localeCompare(a.at))[0];
|
|
295
|
+
const mcpError = pcMcp.flatMap(instance => instance.events).filter(event => event.state === "failed").sort((a, b) => b.at.localeCompare(a.at))[0];
|
|
296
|
+
const mcpMessage = pcMcp.flatMap(instance => instance.events).filter(event => event.tool === "agent_message").sort((a, b) => b.at.localeCompare(a.at))[0];
|
|
297
|
+
const statusLines = [
|
|
298
|
+
" Health: " + (instances.map(i => "relay " + i.connection.toUpperCase()).join(", ") || "relay not observed") +
|
|
299
|
+
(result.health ? " | MCP " + result.health.state + (result.health.status ? " HTTP " + result.health.status : "") + " @ " + result.health.at.slice(11, 19) + "Z" : " | MCP probe not run"),
|
|
300
|
+
...(pcStats.length ? [" Native: " + pcStats.reduce((sum, item) => sum + item.calls, 0) +
|
|
301
|
+
" measured calls | status polls " + pcStats.reduce((sum, item) => sum + item.polls.status, 0) +
|
|
302
|
+
" | output polls " + pcStats.reduce((sum, item) => sum + item.polls.output, 0) +
|
|
303
|
+
" | sessions " + instances.filter(instance => live.includes(instance)).reduce((sum, item) => sum + item.sessions.length, 0)] : []),
|
|
304
|
+
...(pcMcp.length ? [" Diagnostics: queue " + pcMcp.reduce((s,i) => s + (i.queue ?? 0), 0) + " | write errors " +
|
|
305
|
+
pcMcp.reduce((s,i) => s + i.writeErrors, 0) + " | dropped " + pcMcp.reduce((s,i) => s + i.dropped, 0)] : []),
|
|
306
|
+
...(recent ? [" Last: " + latest.at.slice(11, 19) + "Z " + recent.executionId.slice(0, 8) + " " + (recent.preview ?? recent.command) + " " + recent.state.toUpperCase()] : []),
|
|
307
|
+
...(mcpRecent ? [" MCP last: " + mcpRecent.at.slice(11, 19) + "Z " + (mcpRecent.name ?? "unbound") + " " + (mcpRecent.preview ?? mcpRecent.command) + " " + mcpRecent.state.toUpperCase()] : []),
|
|
308
|
+
...(mcpError ? [ " Last error: " + mcpError.at.slice(11, 19) + "Z " + (mcpError.preview ?? mcpError.command) + " " + mcpError.code] : []),
|
|
309
|
+
...(mcpMessage && mcpMessage !== mcpRecent ? [" Message: " + (mcpMessage.name ?? "unbound") + " " + mcpMessage.command + " | task " + (mcpMessage.context.taskId ?? "unreported")] : [])
|
|
310
|
+
];
|
|
311
|
+
const rowCapacity = capacity - statusLines.length;
|
|
312
|
+
const pcAgents = agents.filter(agent => agent.computer === name);
|
|
313
|
+
const rows = [
|
|
314
|
+
...pcMcp.flatMap(instance => instance.processes.map(row => ({ executionId: "process:" + row.id,
|
|
315
|
+
displayId: row.agent?.name ?? row.id.slice(0, 8), kind: "call", pid: row.pid, order: "0:process:" + row.id,
|
|
316
|
+
command: row.preview ?? row.command, context: row.context, state: "running", profile: row.agent?.model ? { model: row.agent.model } : null, estimatedSavedTokens: null }))),
|
|
317
|
+
...pcMcp.flatMap(instance => instance.calls.map(call => ({ executionId: "mcp:" + instance.instanceId + ":" + call.id,
|
|
318
|
+
displayId: call.agent?.name ?? ("mcp" + call.id).slice(0, 8).padEnd(8), kind: "call", pid: instance.pid,
|
|
319
|
+
order: "0:" + String(call.id).padStart(12, "0"), command: (call.agent ? "" : "unbound: ") + (call.preview ?? call.command),
|
|
320
|
+
state: call.state, profile: call.agent?.model ? { model: call.agent.model } : null, estimatedSavedTokens: call.agent ? savingsFor(call.agent.runtimeId) : null,
|
|
321
|
+
detail: [call.context.taskId && "task " + call.context.taskId, call.context.pr && "PR #" + call.context.pr + (call.context.inferred ? " (path)" : ""),
|
|
322
|
+
call.context.project && "project " + call.context.project, call.agent?.phase === "analysis" && "analysis (reported)",
|
|
323
|
+
"queue " + (instance.queue ?? "n/a"), call.context.directory && "dir " + call.context.directory].filter(Boolean).join(" | ") }))),
|
|
324
|
+
...pcAgents.flatMap(agent => {
|
|
325
|
+
const measured = agent.executionIds.map(id => executions.get(id)).filter(row => row?.estimatedSavedTokens !== null && row?.estimatedSavedTokens !== undefined);
|
|
326
|
+
const linked = agent.executionIds.map(id => executions.get(id)).filter(Boolean);
|
|
327
|
+
const running = linked.find(row => ["requested", "prepared", "accepted", "running"].includes(row.state));
|
|
328
|
+
const usage = usageFor(agent.id);
|
|
329
|
+
return [{ executionId: agent.id, displayId: agent.name, kind: agent.kind, pid: agent.pid,
|
|
330
|
+
order: (agent.kind === "host" ? "3:" : "2:") + agent.id,
|
|
331
|
+
command: (agent.phase === "analysis" ? "analysis (reported): " : "") + agent.activity,
|
|
332
|
+
detail: [agent.context?.taskId && "task " + agent.context.taskId,
|
|
333
|
+
agent.context?.pr && "PR #" + agent.context.pr, agent.unread > 0 && "messages " + agent.unread,
|
|
334
|
+
usage.length && usageSummary(usage), agent.context?.directory && "dir " + agent.context.directory].filter(Boolean).join(" | "),
|
|
335
|
+
state: running?.state ?? (agent.state === "RUNNING" ? "REPORTED" : agent.state),
|
|
336
|
+
profile: agent.model ? { model: agent.model } : null,
|
|
337
|
+
estimatedSavedTokens: measured.length || usage.length ? Math.round((measured.reduce((sum, row) => sum + row.savedBytes, 0) + usage.reduce((sum, row) => sum + row.savedBytes, 0)) / 4) : null,
|
|
338
|
+
partial: measured.length > 0 && measured.length < agent.executionIds.length },
|
|
339
|
+
...linked.filter(row => ["requested", "prepared", "accepted", "running"].includes(row.state))
|
|
340
|
+
.map(row => ({ ...row, command: "-> " + (row.preview ?? row.command), order: "1:" + agent.id + ":work:" + row.executionId }))];
|
|
341
|
+
}),
|
|
342
|
+
...instances.flatMap(active).filter(row => !pcAgents.some(agent => agent.executionIds.includes(row.executionId))).map(row => ({ ...row, command: row.preview ?? row.command }))
|
|
343
|
+
];
|
|
344
|
+
const state = instances.some(instance => active(instance).length) || pcMcp.some(instance => instance.calls.length) ? "RUNNING" : pcAgents.length || pcMcp.length ? "OBSERVING" :
|
|
345
|
+
instances.some(instance => instance.connection === "connected") ? "READY" : instances[0]?.connection.toUpperCase() ?? "OBSERVING";
|
|
346
|
+
// Keep each process with its PC heading, including in a short terminal.
|
|
347
|
+
if (rowCapacity - body.length < (rows.length ? width < 60 ? 5 : 4 : 3)) break;
|
|
348
|
+
body.push(pair("PC " + name + " | saved " + pcSaved + " tokens", state));
|
|
349
|
+
shownPcs++;
|
|
350
|
+
if (rows.length) {
|
|
351
|
+
if (width >= 100) body.push(" " + fit("AGENT/ID", 8) + " " + fit("PID", 7) + " " + fit("DOING", width - 71) +
|
|
352
|
+
" " + fit("MODEL", 24) + " " + fit("SAVED TOK", 10, true) + " STATE");
|
|
353
|
+
for (const row of rows.sort((a, b) => (a.order ?? "1:" + a.executionId).localeCompare(b.order ?? "1:" + b.executionId))) {
|
|
354
|
+
const id = row.displayId ?? row.executionId.slice(0, 8), model = row.profile?.model ?? (row.kind === "host" ? "process only" : "not reported");
|
|
355
|
+
const saving = row.estimatedSavedTokens === null ? "n/a" : "~" + compact(row.estimatedSavedTokens) + (row.partial ? "+" : "");
|
|
356
|
+
const entry = width >= 100 ? [" " + fit(id, 8) + " " + fit(row.pid ?? "-", 7, true) + " " + fit(row.command, width - 71) +
|
|
357
|
+
" " + fit(model, 24) + " " + fit(saving, 10, true) + " " + fit(row.state.toUpperCase(), 10)] : [
|
|
358
|
+
pair(" " + id + " " + row.command, row.state.toUpperCase()),
|
|
359
|
+
...(width >= 60 ? [" PID " + (row.pid ?? "-") + " | model " + fit(model, width - 51).trimEnd() + " | saved " + saving + " tok"] :
|
|
360
|
+
[" model " + model, " PID " + (row.pid ?? "-") + " | saved " + saving + " tok"])
|
|
361
|
+
];
|
|
362
|
+
const context = row.context;
|
|
363
|
+
const detail = row.detail || (context ? [context.taskId && "task " + context.taskId,
|
|
364
|
+
context.pr && "PR #" + context.pr, context.directory && "dir " + context.directory].filter(Boolean).join(" | ") : "");
|
|
365
|
+
if (detail) entry.push(" " + detail);
|
|
366
|
+
if (body.length + entry.length + 1 > rowCapacity) break;
|
|
367
|
+
body.push(...entry);
|
|
368
|
+
if (row.kind && row.kind !== "call") shownAgents++;
|
|
369
|
+
else shown++;
|
|
370
|
+
}
|
|
371
|
+
} else body.push(" Waiting for work");
|
|
372
|
+
for (const line of statusLines) if (body.length + 2 <= capacity) body.push(line);
|
|
373
|
+
body.push("");
|
|
374
|
+
}
|
|
375
|
+
if (!pcs.size) body.push(result.instances.length ? "No live PC reported. See logs for completed work and connection history." :
|
|
376
|
+
"No monitoring snapshots. Update/start a monitoring-capable agent.");
|
|
377
|
+
lines.push(...body.slice(0, capacity));
|
|
378
|
+
while (lines.length < height - footer.length - 1) lines.push("");
|
|
379
|
+
lines.push((jobs > shown ? (jobs - shown) + " more active commands | " : "") +
|
|
380
|
+
(agents.length > shownAgents ? (agents.length - shownAgents) + " more agents/hosts | " : "") +
|
|
381
|
+
(pcs.size > shownPcs ? (pcs.size - shownPcs) + " more PCs | " : "") +
|
|
382
|
+
"Unavailable/stale/stopped snapshots: " + (result.unavailable + result.instances.length - live.length));
|
|
383
|
+
lines.push(...footer);
|
|
384
|
+
// Paint after clipping so ANSI sequences never count toward the terminal width.
|
|
385
|
+
return lines.slice(0, height).map(line => {
|
|
386
|
+
const text = fit(line, width);
|
|
387
|
+
if (!color) return text.trimEnd();
|
|
388
|
+
const cyan = /^LC |^> |^PC |^[+|]|^\s+[_|]/.test(line);
|
|
389
|
+
return "\x1b[48;2;15;23;42m\x1b[38;2;" + (cyan ? "0;255;213" : "191;219;254") + "m" +
|
|
390
|
+
text.replace(/\b(RUNNING|READY|CONNECTED|OBSERVING|OBSERVED|REPORTING|QUEUED|FAILED)\b|Saved:.*$/g, value =>
|
|
391
|
+
"\x1b[38;2;" + (value === "FAILED" ? "248;113;113" : value === "QUEUED" ? "251;191;36" : "0;255;213") + "m" + value + "\x1b[38;2;191;219;254m") + "\x1b[0m";
|
|
392
|
+
}).join("\n") + "\n";
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function formatInsights(result) {
|
|
396
|
+
const lines = ["OUTPUT TO REVIEW (recent measured bytes; potential savings unproven):"];
|
|
397
|
+
const candidates = result.instances.flatMap(instance => instance.executions.filter(row => row.sentBytes > 0)
|
|
398
|
+
.map(row => ({ ...row, computer: instance.computer ?? "unknown" })))
|
|
399
|
+
.sort((a, b) => b.sentBytes - a.sentBytes).slice(0, 5);
|
|
400
|
+
if (!candidates.length) lines.push(" No measured output candidates yet.");
|
|
401
|
+
for (const row of candidates) lines.push(" " + row.computer + " " + row.executionId.slice(0, 8) + " " + row.command + " | " + compact(row.sentBytes) +
|
|
402
|
+
" B sent | " + (row.reason ?? "unknown") + " | " +
|
|
403
|
+
(row.reason === "validated_compact" ? "already compacted; review remaining detail" :
|
|
404
|
+
row.name === "fs_read" ? "request a focused line range" : row.name === "fs_search" ? "narrow query and maxResults" :
|
|
405
|
+
"review command verbosity; preserve failures/warnings"));
|
|
406
|
+
for (const instance of result.instances) {
|
|
407
|
+
const stats = instance.efficiency;
|
|
408
|
+
if (stats && stats.polls.status + stats.polls.output >= 10) lines.push("POLLING: " + stats.polls.status + " status / " + stats.polls.output +
|
|
409
|
+
" output calls on " + (instance.computer ?? "unknown") + "; wait between polls and fetch final output once when practical.");
|
|
410
|
+
}
|
|
411
|
+
for (const instance of (result.mcp?.instances ?? []).filter(item => item.running)) {
|
|
412
|
+
lines.push("MCP " + instance.computer + " | queue " + (instance.queue ?? "n/a") + " | diagnostics writes " + instance.writes +
|
|
413
|
+
" | write errors " + instance.writeErrors + " | dropped observations " + instance.dropped);
|
|
414
|
+
for (const row of [...instance.metrics].sort((a, b) => b.sentBytes - a.sentBytes).slice(0, 5)) {
|
|
415
|
+
lines.push(" " + row.tool + ": " + row.calls + " calls | " + row.errors + " errors | " + row.sentBytes + "B responses | " + row.outputBytes +
|
|
416
|
+
"B output | saved stdout ~" + Math.round(row.savedBytes / 4) + " tok | " + Math.round(row.durationMs / Math.max(1, row.calls)) + "ms avg");
|
|
417
|
+
if (["process_output", "scheduler_status", "agent_monitor"].includes(row.tool) && row.calls >= 10) lines.push(" Repeated polling: wait between polls; fetch the final result once when practical.");
|
|
418
|
+
if (row.sentBytes > 16_384) lines.push(" Review " + (row.tool === "fs_read" ? "line range" : row.tool === "fs_search" ? "query/maxResults" : "verbosity") + "; preserve exit status, errors, warnings and required detail.");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return lines.join("\n") + "\n";
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Fixed cursor positions: rewrite changed lines only, with no clear/redraw or scrolling.
|
|
425
|
+
export function monitorFrame(previous, next, maxRows = Infinity) {
|
|
426
|
+
const before = previous ? previous.trimEnd().split("\n") : [];
|
|
427
|
+
const after = next.trimEnd().split("\n");
|
|
428
|
+
let output = "";
|
|
429
|
+
for (let index = 0; index < Math.min(maxRows, Math.max(before.length, after.length)); index++) {
|
|
430
|
+
if (before[index] !== after[index]) output += "\x1b[" + (index + 1) + ";1H\x1b[2K" + (after[index] ?? "");
|
|
431
|
+
}
|
|
432
|
+
return output;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export async function runMonitor(directory, command, args) {
|
|
436
|
+
const allowed = command === "logs" ? ["--follow"] : ["--watch", "--json"];
|
|
437
|
+
if (new Set(args).size !== args.length || args.some(arg => !allowed.includes(arg))) throw Error("ARGUMENT_INVALID");
|
|
438
|
+
const repeat = args.includes("--watch") || args.includes("--follow");
|
|
439
|
+
const stop = new AbortController();
|
|
440
|
+
const abort = () => stop.abort();
|
|
441
|
+
process.once("SIGINT", abort);
|
|
442
|
+
process.once("SIGTERM", abort);
|
|
443
|
+
const seen = new Map();
|
|
444
|
+
const previousHosts = new Map();
|
|
445
|
+
const fixedScreen = command === "monitor" && repeat && process.stdout.isTTY && !args.includes("--json");
|
|
446
|
+
let previous = "", previousSize = "", initial = true, insightCheckpoint = "";
|
|
447
|
+
let paused = false, latestResult, focusComputer = null, storyDepth = 2;
|
|
448
|
+
const interactive = fixedScreen && process.stdin.isTTY && typeof process.stdin.setRawMode === "function";
|
|
449
|
+
const wasRaw = process.stdin.isRaw, wasPaused = process.stdin.isPaused();
|
|
450
|
+
const input = chunk => {
|
|
451
|
+
const key = chunk.toString().toLowerCase();
|
|
452
|
+
if (key === "\u0003" || key === "q") stop.abort();
|
|
453
|
+
else if (key === " ") paused = !paused;
|
|
454
|
+
else if (key === "a") focusComputer = null;
|
|
455
|
+
else if (key === "l") storyDepth = storyDepth === 2 ? 5 : 2;
|
|
456
|
+
else if (key === "\t" && latestResult) {
|
|
457
|
+
const computers = [...new Set([...latestResult.instances.map(i => i.computer),
|
|
458
|
+
...latestResult.mcp.instances.map(i => i.computer), ...(latestResult.activity?.rows ?? []).map(i => i.computer)].filter(Boolean))].sort();
|
|
459
|
+
focusComputer = computers[(computers.indexOf(focusComputer) + 1) % computers.length] ?? null;
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
if (interactive) { process.stdin.setRawMode(true); process.stdin.on("data", input); process.stdin.resume(); }
|
|
463
|
+
if (fixedScreen) process.stdout.write("\x1b[?1049h\x1b[?25l");
|
|
464
|
+
try {
|
|
465
|
+
do {
|
|
466
|
+
const result = paused && latestResult ? latestResult : await readMonitors(directory);
|
|
467
|
+
if (!(paused && latestResult)) {
|
|
468
|
+
result.mcp = await readMcpActivity();
|
|
469
|
+
result.health = await readMcpHealth();
|
|
470
|
+
if (command === "monitor") result.activity = await readAgentActivity(directory, { previousHosts, signal: stop.signal });
|
|
471
|
+
latestResult = result;
|
|
472
|
+
}
|
|
473
|
+
result.paused = paused; result.focusComputer = focusComputer; result.storyDepth = storyDepth;
|
|
474
|
+
if (command === "logs" && (result.instances.length || result.mcp.instances.length)) {
|
|
475
|
+
const checkpoint = result.mcp.instances.map(instance => instance.instanceId + ":" + Math.floor(instance.metrics.reduce((sum, row) => sum + row.calls, 0) / 5)).join(",");
|
|
476
|
+
if (initial || checkpoint !== insightCheckpoint) { process.stdout.write(formatInsights(result)); insightCheckpoint = checkpoint; }
|
|
477
|
+
for (const instance of result.instances) {
|
|
478
|
+
const after = seen.get(instance.instanceId) ?? 0;
|
|
479
|
+
if (after && instance.events[0]?.seq > after + 1) process.stdout.write("Earlier events were overwritten in the 100-event buffer.\n");
|
|
480
|
+
for (const event of instance.events) if (event.seq > after && !["status", "output"].includes(event.event)) process.stdout.write(logLine(instance, event) + "\n");
|
|
481
|
+
seen.set(instance.instanceId, instance.events.at(-1)?.seq ?? after);
|
|
482
|
+
}
|
|
483
|
+
for (const instance of result.mcp.instances) {
|
|
484
|
+
const key = "mcp:" + instance.instanceId;
|
|
485
|
+
const transitionsKey = key + ":starts";
|
|
486
|
+
const afterStart = seen.get(transitionsKey) ?? 0;
|
|
487
|
+
for (const event of instance.transitions ?? []) if (event.seq > afterStart) {
|
|
488
|
+
process.stdout.write([event.at, "pc=" + instance.computer, "agent=" + (event.name ?? "unbound"), event.preview, "RUNNING",
|
|
489
|
+
"model=" + (event.model ?? "not_reported"), event.context.directory && "dir=" + event.context.directory,
|
|
490
|
+
event.context.taskId && "task=" + event.context.taskId].filter(Boolean).join(" ") + "\n");
|
|
491
|
+
}
|
|
492
|
+
seen.set(transitionsKey, instance.transitions?.at(-1)?.seq ?? afterStart);
|
|
493
|
+
// Request IDs finish out of order: a per-event key tracks actual completions, not numeric high-water IDs.
|
|
494
|
+
for (const event of instance.events) {
|
|
495
|
+
const eventKey = key + ":" + event.id;
|
|
496
|
+
if (seen.has(eventKey)) continue;
|
|
497
|
+
process.stdout.write([event.at, "pc=" + instance.computer, "agent=" + (event.name ?? "unbound"), event.preview ?? event.command, event.state, "model=" + (event.model ?? "not_reported"),
|
|
498
|
+
"elapsed=" + event.durationMs + "ms", event.exitCode !== null && "exit=" + event.exitCode, event.timedOut && "timeout=true",
|
|
499
|
+
event.code, event.context.directory && "dir=" + event.context.directory, event.context.taskId && "task=" + event.context.taskId,
|
|
500
|
+
event.context.pr && "PR=" + event.context.pr, "sent=" + event.sentBytes + "B", "saved=~" + Math.round(event.savedBytes / 4) + "tok"].filter(Boolean).join(" ") + "\n");
|
|
501
|
+
seen.set(eventKey, 1);
|
|
502
|
+
}
|
|
503
|
+
for (const seenKey of seen.keys()) if (seenKey.startsWith(key + ":") && seenKey !== transitionsKey && !instance.events.some(event => seenKey === key + ":" + event.id)) seen.delete(seenKey);
|
|
504
|
+
}
|
|
505
|
+
for (const key of seen.keys()) if (!result.instances.some(instance => instance.instanceId === key) &&
|
|
506
|
+
!result.mcp.instances.some(instance => key.startsWith("mcp:" + instance.instanceId + ":"))) seen.delete(key);
|
|
507
|
+
if (result.unavailable) process.stderr.write("Some monitoring snapshots are unavailable.\n");
|
|
508
|
+
} else {
|
|
509
|
+
const columns = Math.max(1, (process.stdout.columns ?? 120) - 1);
|
|
510
|
+
const rows = Math.max(1, (process.stdout.rows ?? 40) - 1);
|
|
511
|
+
const size = columns + "x" + rows;
|
|
512
|
+
// A resized terminal must repaint even when the snapshot is unchanged.
|
|
513
|
+
if (fixedScreen && size !== previousSize) previous = "";
|
|
514
|
+
const next = args.includes("--json") ? JSON.stringify(result) + "\n" :
|
|
515
|
+
formatMonitor(result, fixedScreen ? columns : undefined, fixedScreen ? rows : undefined,
|
|
516
|
+
process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb");
|
|
517
|
+
process.stdout.write(fixedScreen ? monitorFrame(previous, next, Math.max(1, (process.stdout.rows ?? 40) - 1)) : next);
|
|
518
|
+
previous = next;
|
|
519
|
+
previousSize = size;
|
|
520
|
+
}
|
|
521
|
+
initial = false;
|
|
522
|
+
if (repeat) await delay(2000, undefined, { signal: stop.signal }).catch(error => { if (error.name !== "AbortError") throw error; });
|
|
523
|
+
} while (repeat && !stop.signal.aborted);
|
|
524
|
+
} finally {
|
|
525
|
+
if (interactive) { process.stdin.removeListener("data", input); process.stdin.setRawMode(!!wasRaw); if (wasPaused) process.stdin.pause(); }
|
|
526
|
+
if (fixedScreen) process.stdout.write("\x1b[0m\x1b[?25h\x1b[?1049l");
|
|
527
|
+
process.removeListener("SIGINT", abort);
|
|
528
|
+
process.removeListener("SIGTERM", abort);
|
|
529
|
+
}
|
|
530
|
+
}
|