@saadnvd1/agent-os 0.3.609 → 0.3.610
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/lib/load/alert.ts +5 -0
- package/lib/load/attribute.ts +217 -0
- package/lib/load/monitor.ts +32 -39
- package/lib/load/usage.ts +34 -25
- package/package.json +1 -1
package/lib/load/alert.ts
CHANGED
|
@@ -18,9 +18,14 @@ export function alertText(view: LoadView): string {
|
|
|
18
18
|
: "";
|
|
19
19
|
return `${s.name} ${s.cores.toFixed(1)} cores, ${gb(s.rssBytes)}${heavy}`;
|
|
20
20
|
});
|
|
21
|
+
const others = (view.unattributed ?? []).map(
|
|
22
|
+
(g) =>
|
|
23
|
+
`${g.name}${g.count > 1 ? ` ×${g.count}` : ""} ${g.cores.toFixed(1)} cores, ${gb(g.rssBytes)}`
|
|
24
|
+
);
|
|
21
25
|
return [
|
|
22
26
|
`Machine load red for 2+ minutes: ${view.load1} on ${view.cores} cores${memory}.`,
|
|
23
27
|
top.length ? `Top: ${top.join("; ")}.` : "",
|
|
28
|
+
others.length ? `Not from a session: ${others.join("; ")}.` : "",
|
|
24
29
|
]
|
|
25
30
|
.filter(Boolean)
|
|
26
31
|
.join(" ");
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Who is using the machine: every process goes to the session whose tree it
|
|
2
|
+
// is in, or, for a heavy one that left its tree (a build daemon reparented to
|
|
3
|
+
// init), to the session whose worktree it runs in. What's left is grouped by
|
|
4
|
+
// name so the orchestrator can tell its tasks from everything else.
|
|
5
|
+
import { execFile } from "child_process";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { readlink } from "fs/promises";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import type { ProcRow } from "../process-table";
|
|
10
|
+
import { workerTmuxName } from "../chat/worker/protocol";
|
|
11
|
+
import { claimTrees, type Usage } from "./usage";
|
|
12
|
+
|
|
13
|
+
// A row shows its usage, and a group is listed, from here up.
|
|
14
|
+
export const BUSY_CORES = 0.5;
|
|
15
|
+
const HOT_PCPU = 20;
|
|
16
|
+
const MAX_LOOKUPS = 40;
|
|
17
|
+
const MAX_HOT = 20;
|
|
18
|
+
const MAX_GROUPS = 3;
|
|
19
|
+
const BIG_RSS = 2 ** 30;
|
|
20
|
+
|
|
21
|
+
export interface SessionLoad extends Usage {
|
|
22
|
+
sessionId: string;
|
|
23
|
+
name: string;
|
|
24
|
+
heavy: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ProcGroup {
|
|
28
|
+
name: string;
|
|
29
|
+
count: number;
|
|
30
|
+
cores: number;
|
|
31
|
+
rssBytes: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SessionRow {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
tmux_name: string;
|
|
38
|
+
worktree_path: string | null;
|
|
39
|
+
working_directory: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const round1 = (n: number) => Math.round(n * 10) / 10;
|
|
43
|
+
const baseName = (comm: string) =>
|
|
44
|
+
comm.split("/").pop()?.replace(/^-/, "") || comm;
|
|
45
|
+
|
|
46
|
+
// Each session's own tmux session, plus its chat worker's.
|
|
47
|
+
export function claimSessions(
|
|
48
|
+
procs: ProcRow[],
|
|
49
|
+
panes: { tmux: string; pid: number }[],
|
|
50
|
+
rows: SessionRow[]
|
|
51
|
+
): Map<number, string> {
|
|
52
|
+
const roots: { key: string; pid: number }[] = [];
|
|
53
|
+
for (const row of rows)
|
|
54
|
+
for (const p of panes)
|
|
55
|
+
if (p.tmux === row.tmux_name || p.tmux === workerTmuxName(row.id))
|
|
56
|
+
roots.push({ key: row.id, pid: p.pid });
|
|
57
|
+
return claimTrees(procs, roots);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The directories a process's cwd can place it by, longest first. The
|
|
61
|
+
// worktree when there is one; otherwise the working directory, but only when
|
|
62
|
+
// it is specific to the session.
|
|
63
|
+
export function sessionPaths(
|
|
64
|
+
rows: SessionRow[],
|
|
65
|
+
home: string
|
|
66
|
+
): { sessionId: string; path: string }[] {
|
|
67
|
+
const count = new Map<string, number>();
|
|
68
|
+
for (const r of rows)
|
|
69
|
+
count.set(r.working_directory, (count.get(r.working_directory) ?? 0) + 1);
|
|
70
|
+
const out: { sessionId: string; path: string }[] = [];
|
|
71
|
+
for (const r of rows) {
|
|
72
|
+
let dir = r.worktree_path;
|
|
73
|
+
if (!dir) {
|
|
74
|
+
dir = r.working_directory.replace(/^~(?=$|\/)/, home);
|
|
75
|
+
if (
|
|
76
|
+
!path.isAbsolute(dir) ||
|
|
77
|
+
dir === "/" ||
|
|
78
|
+
dir === home ||
|
|
79
|
+
count.get(r.working_directory)! > 1
|
|
80
|
+
)
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
out.push({ sessionId: r.id, path: dir.replace(/\/+$/, "") });
|
|
84
|
+
}
|
|
85
|
+
return out.sort((a, b) => b.path.length - a.path.length);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function matchSession(
|
|
89
|
+
cwd: string,
|
|
90
|
+
paths: { sessionId: string; path: string }[]
|
|
91
|
+
): string | null {
|
|
92
|
+
const hit = paths.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
93
|
+
return hit?.sessionId ?? null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// The heavy processes no session owns, and their unowned ancestors (the
|
|
97
|
+
// daemon that spawned the workers), whose cwd is worth looking up.
|
|
98
|
+
export function cwdCandidates(
|
|
99
|
+
procs: ProcRow[],
|
|
100
|
+
owner: Map<number, string>
|
|
101
|
+
): number[] {
|
|
102
|
+
const byPid = new Map(procs.map((p) => [p.pid, p]));
|
|
103
|
+
const hot = procs
|
|
104
|
+
.filter((p) => !owner.has(p.pid) && p.pcpu >= HOT_PCPU)
|
|
105
|
+
.sort((a, b) => b.pcpu - a.pcpu)
|
|
106
|
+
.slice(0, MAX_HOT);
|
|
107
|
+
const out = new Set<number>();
|
|
108
|
+
for (const p of hot) {
|
|
109
|
+
let at: ProcRow | undefined = p;
|
|
110
|
+
while (at && at.pid > 1 && !owner.has(at.pid) && out.size < MAX_LOOKUPS) {
|
|
111
|
+
out.add(at.pid);
|
|
112
|
+
at = byPid.get(at.ppid);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return [...out];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// `lsof -a -d cwd -Fpn`: a `p<pid>` line, then `n<path>` for its cwd.
|
|
119
|
+
export function parseLsofCwd(out: string): Map<number, string> {
|
|
120
|
+
const cwds = new Map<number, string>();
|
|
121
|
+
let pid = 0;
|
|
122
|
+
for (const line of out.split("\n")) {
|
|
123
|
+
if (line.startsWith("p")) pid = Number(line.slice(1)) || 0;
|
|
124
|
+
else if (line.startsWith("n") && pid) cwds.set(pid, line.slice(1));
|
|
125
|
+
}
|
|
126
|
+
return cwds;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const LSOF = ["/usr/sbin/lsof", "/usr/bin/lsof"].find((p) => existsSync(p));
|
|
130
|
+
|
|
131
|
+
export async function readCwds(pids: number[]): Promise<Map<number, string>> {
|
|
132
|
+
const cwds = new Map<number, string>();
|
|
133
|
+
if (!pids.length) return cwds;
|
|
134
|
+
if (process.platform === "linux") {
|
|
135
|
+
await Promise.all(
|
|
136
|
+
pids.map((pid) =>
|
|
137
|
+
readlink(`/proc/${pid}/cwd`).then(
|
|
138
|
+
(dir) => void cwds.set(pid, dir),
|
|
139
|
+
() => undefined
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
return cwds;
|
|
144
|
+
}
|
|
145
|
+
return new Promise((resolve) =>
|
|
146
|
+
execFile(
|
|
147
|
+
LSOF ?? "lsof",
|
|
148
|
+
["-a", "-d", "cwd", "-Fpn", "-p", pids.join(",")],
|
|
149
|
+
{ timeout: 2000, maxBuffer: 1 << 20 },
|
|
150
|
+
// lsof exits 1 when some pids are gone but still prints the rest.
|
|
151
|
+
(_err, stdout) => resolve(parseLsofCwd(stdout ?? ""))
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function attributeLoad(input: {
|
|
157
|
+
procs: ProcRow[];
|
|
158
|
+
owner: Map<number, string>;
|
|
159
|
+
rows: SessionRow[];
|
|
160
|
+
cwds: Map<number, string>;
|
|
161
|
+
home: string;
|
|
162
|
+
}): { sessions: Record<string, SessionLoad>; unattributed: ProcGroup[] } {
|
|
163
|
+
const { procs, owner, rows, cwds, home } = input;
|
|
164
|
+
const byPid = new Map(procs.map((p) => [p.pid, p]));
|
|
165
|
+
const paths = sessionPaths(rows, home);
|
|
166
|
+
// Memoised: the session an unowned process belongs to, by its own cwd or
|
|
167
|
+
// its nearest unowned ancestor's.
|
|
168
|
+
const placed = new Map<number, string | null>();
|
|
169
|
+
const place = (p: ProcRow, depth = 0): string | null => {
|
|
170
|
+
if (placed.has(p.pid)) return placed.get(p.pid)!;
|
|
171
|
+
const cwd = cwds.get(p.pid);
|
|
172
|
+
let id = cwd ? matchSession(cwd, paths) : null;
|
|
173
|
+
const parent = byPid.get(p.ppid);
|
|
174
|
+
if (!id && parent && parent.pid > 1 && depth < 64 && !owner.has(parent.pid))
|
|
175
|
+
id = place(parent, depth + 1);
|
|
176
|
+
placed.set(p.pid, id);
|
|
177
|
+
return id;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const raw = new Map<string, Usage>();
|
|
181
|
+
const groups = new Map<string, ProcGroup>();
|
|
182
|
+
for (const p of procs) {
|
|
183
|
+
const id = owner.get(p.pid) ?? place(p);
|
|
184
|
+
if (id) {
|
|
185
|
+
const u = raw.get(id) ?? { cores: 0, rssBytes: 0 };
|
|
186
|
+
u.cores += p.pcpu / 100;
|
|
187
|
+
u.rssBytes += p.rss * 1024;
|
|
188
|
+
raw.set(id, u);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const name = baseName(p.comm);
|
|
192
|
+
const g = groups.get(name) ?? { name, count: 0, cores: 0, rssBytes: 0 };
|
|
193
|
+
g.count++;
|
|
194
|
+
g.cores += p.pcpu / 100;
|
|
195
|
+
g.rssBytes += p.rss * 1024;
|
|
196
|
+
groups.set(name, g);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const sessions: Record<string, SessionLoad> = {};
|
|
200
|
+
for (const row of rows) {
|
|
201
|
+
const u = raw.get(row.id);
|
|
202
|
+
if (!u) continue;
|
|
203
|
+
sessions[row.id] = {
|
|
204
|
+
sessionId: row.id,
|
|
205
|
+
name: row.name,
|
|
206
|
+
cores: round1(u.cores),
|
|
207
|
+
rssBytes: u.rssBytes,
|
|
208
|
+
heavy: [],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const unattributed = [...groups.values()]
|
|
212
|
+
.filter((g) => g.cores >= BUSY_CORES || g.rssBytes >= BIG_RSS)
|
|
213
|
+
.sort((a, b) => b.cores - a.cores)
|
|
214
|
+
.slice(0, MAX_GROUPS)
|
|
215
|
+
.map((g) => ({ ...g, cores: round1(g.cores) }));
|
|
216
|
+
return { sessions, unattributed };
|
|
217
|
+
}
|
package/lib/load/monitor.ts
CHANGED
|
@@ -9,21 +9,24 @@ import { LoadAlarm } from "./alarm";
|
|
|
9
9
|
import { HeavyRegistry, type HeavyRun } from "./heavy";
|
|
10
10
|
import { nextLevel, type LoadLevel, type Pressure } from "./level";
|
|
11
11
|
import { readPressure } from "./pressure";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import type { Usage } from "./usage";
|
|
13
|
+
import { readSnapshot } from "./usage";
|
|
14
|
+
import {
|
|
15
|
+
attributeLoad,
|
|
16
|
+
BUSY_CORES,
|
|
17
|
+
claimSessions,
|
|
18
|
+
cwdCandidates,
|
|
19
|
+
readCwds,
|
|
20
|
+
type ProcGroup,
|
|
21
|
+
type SessionLoad,
|
|
22
|
+
type SessionRow,
|
|
23
|
+
} from "./attribute";
|
|
14
24
|
import { alertText, raiseAlert } from "./alert";
|
|
15
25
|
import { LOCAL, feedGate, gateOpen } from "./start-gate";
|
|
16
26
|
|
|
17
27
|
export const SAMPLE_MS = 10_000;
|
|
18
28
|
export const USAGE_MS = 30_000;
|
|
19
|
-
|
|
20
|
-
const BUSY_CORES = 0.5;
|
|
21
|
-
|
|
22
|
-
export interface SessionLoad extends Usage {
|
|
23
|
-
sessionId: string;
|
|
24
|
-
name: string;
|
|
25
|
-
heavy: string[];
|
|
26
|
-
}
|
|
29
|
+
export type { SessionLoad, ProcGroup };
|
|
27
30
|
|
|
28
31
|
export interface LoadView {
|
|
29
32
|
level: LoadLevel;
|
|
@@ -32,6 +35,8 @@ export interface LoadView {
|
|
|
32
35
|
memUsedPct: number;
|
|
33
36
|
pressure: Pressure;
|
|
34
37
|
top: SessionLoad[];
|
|
38
|
+
// Heavy processes that belong to no session, grouped by name.
|
|
39
|
+
unattributed: ProcGroup[];
|
|
35
40
|
sessions: Record<string, Usage>;
|
|
36
41
|
}
|
|
37
42
|
|
|
@@ -39,6 +44,7 @@ interface State {
|
|
|
39
44
|
level: LoadLevel | null;
|
|
40
45
|
view: LoadView | null;
|
|
41
46
|
usage: Record<string, SessionLoad>;
|
|
47
|
+
unattributed: ProcGroup[];
|
|
42
48
|
registry: HeavyRegistry;
|
|
43
49
|
alarm: LoadAlarm;
|
|
44
50
|
timers: ReturnType<typeof setInterval>[];
|
|
@@ -50,6 +56,7 @@ const state: State = (g.__agentosLoad ??= {
|
|
|
50
56
|
level: null,
|
|
51
57
|
view: null,
|
|
52
58
|
usage: {},
|
|
59
|
+
unattributed: [],
|
|
53
60
|
registry: new HeavyRegistry(),
|
|
54
61
|
alarm: new LoadAlarm(),
|
|
55
62
|
timers: [],
|
|
@@ -107,6 +114,7 @@ async function sample(): Promise<void> {
|
|
|
107
114
|
memUsedPct: Math.round(((total - os.freemem()) / total) * 100),
|
|
108
115
|
pressure,
|
|
109
116
|
top: topSessions(),
|
|
117
|
+
unattributed: state.unattributed,
|
|
110
118
|
sessions,
|
|
111
119
|
};
|
|
112
120
|
state.view = view;
|
|
@@ -145,39 +153,24 @@ export function loadAlarmFromDb(): void {
|
|
|
145
153
|
state.alarm = new LoadAlarm(readArmed());
|
|
146
154
|
}
|
|
147
155
|
|
|
148
|
-
type UsageRow = { id: string; name: string; tmux_name: string };
|
|
149
|
-
|
|
150
|
-
// Each session's share of the usage: its own tmux session's, plus its chat
|
|
151
|
-
// worker's, where a chat's agent runs.
|
|
152
|
-
export function sessionUsage(
|
|
153
|
-
byTmux: Record<string, Usage>,
|
|
154
|
-
rows: UsageRow[]
|
|
155
|
-
): Record<string, SessionLoad> {
|
|
156
|
-
const usage: Record<string, SessionLoad> = {};
|
|
157
|
-
for (const row of rows) {
|
|
158
|
-
const parts = [byTmux[row.tmux_name], byTmux[workerTmuxName(row.id)]];
|
|
159
|
-
const found = parts.filter((u): u is Usage => !!u);
|
|
160
|
-
if (!found.length) continue;
|
|
161
|
-
const cores = found.reduce((n, u) => n + u.cores, 0);
|
|
162
|
-
usage[row.id] = {
|
|
163
|
-
sessionId: row.id,
|
|
164
|
-
name: row.name,
|
|
165
|
-
cores: Math.round(cores * 10) / 10,
|
|
166
|
-
rssBytes: found.reduce((n, u) => n + u.rssBytes, 0),
|
|
167
|
-
heavy: [],
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
return usage;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
156
|
async function sampleUsage(): Promise<void> {
|
|
174
|
-
const
|
|
157
|
+
const { procs, panes } = await readSnapshot();
|
|
175
158
|
const rows = db
|
|
176
159
|
.prepare(
|
|
177
|
-
`SELECT id, name, tmux_name FROM sessions WHERE archived_at IS NULL AND host_id = 'local'`
|
|
160
|
+
`SELECT id, name, tmux_name, worktree_path, working_directory FROM sessions WHERE archived_at IS NULL AND host_id = 'local'`
|
|
178
161
|
)
|
|
179
|
-
.all() as
|
|
180
|
-
|
|
162
|
+
.all() as SessionRow[];
|
|
163
|
+
const owner = claimSessions(procs, panes, rows);
|
|
164
|
+
const cwds = await readCwds(cwdCandidates(procs, owner));
|
|
165
|
+
const { sessions, unattributed } = attributeLoad({
|
|
166
|
+
procs,
|
|
167
|
+
owner,
|
|
168
|
+
rows,
|
|
169
|
+
cwds,
|
|
170
|
+
home: os.homedir(),
|
|
171
|
+
});
|
|
172
|
+
state.usage = sessions;
|
|
173
|
+
state.unattributed = unattributed;
|
|
181
174
|
}
|
|
182
175
|
|
|
183
176
|
// Off the hot path: each tick is async, never overlaps the last, and logs
|
package/lib/load/usage.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// What each session's processes use: one `ps` for the whole machine and one
|
|
2
|
-
// `tmux list-panes`, then each pane's process tree
|
|
2
|
+
// `tmux list-panes`, then each pane's process tree walked. Never a call per
|
|
3
3
|
// session.
|
|
4
4
|
import { execFile } from "child_process";
|
|
5
|
+
import type { ProcRow } from "../process-table";
|
|
5
6
|
|
|
6
7
|
export interface Proc {
|
|
7
8
|
pid: number;
|
|
@@ -37,36 +38,45 @@ export function parsePanes(out: string): { tmux: string; pid: number }[] {
|
|
|
37
38
|
.map(([tmux, pid]) => ({ tmux, pid: Number(pid) }));
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
export function
|
|
41
|
+
// The key that owns each process: its panes and everything under them. A
|
|
42
|
+
// process goes to the first key that reaches it.
|
|
43
|
+
export function claimTrees(
|
|
43
44
|
procs: Proc[],
|
|
44
45
|
roots: { key: string; pid: number }[]
|
|
45
|
-
):
|
|
46
|
-
const byPid = new Map(procs.map((p) => [p.pid, p]));
|
|
46
|
+
): Map<number, string> {
|
|
47
47
|
const children = new Map<number, number[]>();
|
|
48
48
|
for (const p of procs) {
|
|
49
49
|
const list = children.get(p.ppid);
|
|
50
50
|
if (list) list.push(p.pid);
|
|
51
51
|
else children.set(p.ppid, [p.pid]);
|
|
52
52
|
}
|
|
53
|
-
const
|
|
54
|
-
const out: Record<string, Usage> = {};
|
|
53
|
+
const owner = new Map<number, string>();
|
|
55
54
|
for (const { key, pid } of roots) {
|
|
56
|
-
const usage = (out[key] ??= { cores: 0, rssBytes: 0 });
|
|
57
55
|
const stack = [pid];
|
|
58
56
|
while (stack.length) {
|
|
59
57
|
const at = stack.pop()!;
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
const p = byPid.get(at);
|
|
63
|
-
if (p) {
|
|
64
|
-
usage.cores += p.pcpu / 100;
|
|
65
|
-
usage.rssBytes += p.rss * 1024;
|
|
66
|
-
}
|
|
58
|
+
if (owner.has(at)) continue;
|
|
59
|
+
owner.set(at, key);
|
|
67
60
|
stack.push(...(children.get(at) ?? []));
|
|
68
61
|
}
|
|
69
62
|
}
|
|
63
|
+
return owner;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Each key's panes and everything under them, summed.
|
|
67
|
+
export function sumTrees(
|
|
68
|
+
procs: Proc[],
|
|
69
|
+
roots: { key: string; pid: number }[]
|
|
70
|
+
): Record<string, Usage> {
|
|
71
|
+
const byPid = new Map(procs.map((p) => [p.pid, p]));
|
|
72
|
+
const out: Record<string, Usage> = {};
|
|
73
|
+
for (const { key } of roots) out[key] ??= { cores: 0, rssBytes: 0 };
|
|
74
|
+
for (const [pid, key] of claimTrees(procs, roots)) {
|
|
75
|
+
const p = byPid.get(pid);
|
|
76
|
+
if (!p) continue;
|
|
77
|
+
out[key].cores += p.pcpu / 100;
|
|
78
|
+
out[key].rssBytes += p.rss * 1024;
|
|
79
|
+
}
|
|
70
80
|
return out;
|
|
71
81
|
}
|
|
72
82
|
|
|
@@ -77,11 +87,13 @@ const run = (cmd: string, args: string[], timeout: number) =>
|
|
|
77
87
|
)
|
|
78
88
|
);
|
|
79
89
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
// The process table and the tmux panes, for attributing load to sessions.
|
|
91
|
+
// Empty when ps or tmux can't be read. The table is the one shared with the
|
|
92
|
+
// rest of the server.
|
|
93
|
+
export async function readSnapshot(timeoutMs = 3000): Promise<{
|
|
94
|
+
procs: ProcRow[];
|
|
95
|
+
panes: { tmux: string; pid: number }[];
|
|
96
|
+
}> {
|
|
85
97
|
const { processTable } = await import("../process-table");
|
|
86
98
|
const [procs, panes] = await Promise.all([
|
|
87
99
|
processTable(),
|
|
@@ -91,8 +103,5 @@ export async function readUsage(
|
|
|
91
103
|
timeoutMs
|
|
92
104
|
),
|
|
93
105
|
]);
|
|
94
|
-
return
|
|
95
|
-
procs ?? [],
|
|
96
|
-
parsePanes(panes).map((p) => ({ key: p.tmux, pid: p.pid }))
|
|
97
|
-
);
|
|
106
|
+
return { procs: procs ?? [], panes: parsePanes(panes) };
|
|
98
107
|
}
|