@way_marks/server 4.0.2 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/routes/agent-monitor.js +187 -0
- package/dist/api/server.js +13 -0
- package/dist/approvals/handler.js +13 -2
- package/dist/collectors/claude.js +753 -0
- package/dist/collectors/codex.js +516 -0
- package/dist/collectors/copilot.js +373 -0
- package/dist/collectors/multi-collector.js +221 -0
- package/dist/collectors/process.js +257 -0
- package/dist/collectors/rate-limit.js +161 -0
- package/dist/collectors/secrets.js +43 -0
- package/dist/collectors/types.js +47 -0
- package/dist/mcp/server.js +14 -0
- package/dist/mcp/tools/agent-monitor.js +272 -0
- package/package.json +1 -1
- package/src/ui-dist/assets/{index-DNdosrlQ.css → index-BJAvjazt.css} +1 -1
- package/src/ui-dist/assets/index-CcybEu0u.js +87 -0
- package/src/ui-dist/index.html +2 -2
- package/src/ui-dist/assets/index-BEo79vjN.js +0 -87
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GitHub Copilot CLI session collector.
|
|
4
|
+
*
|
|
5
|
+
* Reads rich session data from ~/.copilot/session-state/<uuid>/:
|
|
6
|
+
* - workspace.yaml — sessionId, cwd, branch, summary (= current task)
|
|
7
|
+
* - inuse.<pid>.lock — active PID
|
|
8
|
+
* - events.jsonl — model, output tokens, tool calls, turn count
|
|
9
|
+
*
|
|
10
|
+
* Event types parsed:
|
|
11
|
+
* session.start → startedAt, initial context
|
|
12
|
+
* session.model_change → current model name
|
|
13
|
+
* session.compaction_complete → preCompactionTokens (context window usage)
|
|
14
|
+
* assistant.message → outputTokens per turn
|
|
15
|
+
* assistant.turn_end → turn count
|
|
16
|
+
* tool.execution_start → tool calls (name, arg)
|
|
17
|
+
* user.message → last user prompt (current task)
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.CopilotCollector = void 0;
|
|
54
|
+
const fs = __importStar(require("fs"));
|
|
55
|
+
const os = __importStar(require("os"));
|
|
56
|
+
const path = __importStar(require("path"));
|
|
57
|
+
const types_1 = require("./types");
|
|
58
|
+
const process_1 = require("./process");
|
|
59
|
+
/** Minimal flat-YAML parser for Copilot CLI workspace.yaml. */
|
|
60
|
+
function parseWorkspaceYaml(filePath) {
|
|
61
|
+
try {
|
|
62
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
63
|
+
const result = {};
|
|
64
|
+
for (const line of text.split('\n')) {
|
|
65
|
+
const idx = line.indexOf(': ');
|
|
66
|
+
if (idx === -1)
|
|
67
|
+
continue;
|
|
68
|
+
result[line.slice(0, idx).trim()] = line.slice(idx + 2).trim();
|
|
69
|
+
}
|
|
70
|
+
if (!result['id'] || !result['cwd'])
|
|
71
|
+
return null;
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
+
function applyEvents(acc, events) {
|
|
80
|
+
for (const e of events) {
|
|
81
|
+
const ts = e.timestamp ? new Date(e.timestamp).getTime() : 0;
|
|
82
|
+
const data = e.data ?? {};
|
|
83
|
+
switch (e.type) {
|
|
84
|
+
case 'session.start':
|
|
85
|
+
if (acc.startedAt === 0) {
|
|
86
|
+
acc.startedAt = new Date(data.startTime ?? e.timestamp ?? Date.now()).getTime();
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case 'session.model_change':
|
|
90
|
+
if (data.newModel)
|
|
91
|
+
acc.model = data.newModel;
|
|
92
|
+
break;
|
|
93
|
+
case 'session.compaction_complete':
|
|
94
|
+
if (data.preCompactionTokens) {
|
|
95
|
+
acc.contextTokens = data.preCompactionTokens;
|
|
96
|
+
acc.compactionCount++;
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
case 'assistant.message':
|
|
100
|
+
if (data.outputTokens)
|
|
101
|
+
acc.totalOutputTokens += data.outputTokens;
|
|
102
|
+
if (ts) {
|
|
103
|
+
acc.lastAssistantMsgMs = ts;
|
|
104
|
+
acc.lastActivityMs = ts;
|
|
105
|
+
}
|
|
106
|
+
acc.pendingToolCallId = null;
|
|
107
|
+
break;
|
|
108
|
+
case 'assistant.turn_end':
|
|
109
|
+
acc.turnCount++;
|
|
110
|
+
if (ts)
|
|
111
|
+
acc.lastActivityMs = ts;
|
|
112
|
+
break;
|
|
113
|
+
case 'tool.execution_start': {
|
|
114
|
+
const rawName = data.toolName ?? '';
|
|
115
|
+
const toolName = rawName.includes('.') ? rawName.split('.').pop() ?? rawName : rawName;
|
|
116
|
+
const args = data.arguments ?? {};
|
|
117
|
+
const rawArg = args.path ?? args.command ?? args.pattern ?? args.query ?? args.url ?? '';
|
|
118
|
+
const tc = {
|
|
119
|
+
name: toolName || 'unknown',
|
|
120
|
+
arg: typeof rawArg === 'string' ? rawArg.slice(0, 120) : '',
|
|
121
|
+
durationMs: 0,
|
|
122
|
+
};
|
|
123
|
+
acc.toolCalls.push(tc);
|
|
124
|
+
if (acc.toolCalls.length > 200)
|
|
125
|
+
acc.toolCalls = acc.toolCalls.slice(-200);
|
|
126
|
+
if (ts)
|
|
127
|
+
acc.lastActivityMs = ts;
|
|
128
|
+
acc.pendingToolCallId = data.toolCallId ?? null;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case 'tool.execution_complete':
|
|
132
|
+
if (ts)
|
|
133
|
+
acc.lastActivityMs = ts;
|
|
134
|
+
acc.pendingToolCallId = null;
|
|
135
|
+
break;
|
|
136
|
+
case 'user.message': {
|
|
137
|
+
const content = data.content ?? '';
|
|
138
|
+
if (content.trim())
|
|
139
|
+
acc.currentTask = content.trim().slice(0, 200);
|
|
140
|
+
if (ts) {
|
|
141
|
+
acc.lastUserMsgMs = ts;
|
|
142
|
+
acc.lastActivityMs = ts;
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Read new lines from a JSONL file starting at startOffset. */
|
|
150
|
+
function readEventsFrom(filePath, startOffset) {
|
|
151
|
+
try {
|
|
152
|
+
const stat = fs.statSync(filePath);
|
|
153
|
+
if (stat.size <= startOffset)
|
|
154
|
+
return { events: [], newOffset: startOffset };
|
|
155
|
+
const fd = fs.openSync(filePath, 'r');
|
|
156
|
+
const bufSize = stat.size - startOffset;
|
|
157
|
+
const buf = Buffer.alloc(bufSize);
|
|
158
|
+
const bytesRead = fs.readSync(fd, buf, 0, bufSize, startOffset);
|
|
159
|
+
fs.closeSync(fd);
|
|
160
|
+
const text = buf.slice(0, bytesRead).toString('utf8');
|
|
161
|
+
const lastNl = text.lastIndexOf('\n');
|
|
162
|
+
if (lastNl === -1)
|
|
163
|
+
return { events: [], newOffset: startOffset };
|
|
164
|
+
const chunk = text.slice(0, lastNl);
|
|
165
|
+
const newOffset = startOffset + Buffer.byteLength(chunk + '\n', 'utf8');
|
|
166
|
+
const events = [];
|
|
167
|
+
for (const line of chunk.split('\n')) {
|
|
168
|
+
if (!line.trim())
|
|
169
|
+
continue;
|
|
170
|
+
try {
|
|
171
|
+
events.push(JSON.parse(line));
|
|
172
|
+
}
|
|
173
|
+
catch { /* skip malformed */ }
|
|
174
|
+
}
|
|
175
|
+
return { events, newOffset };
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return { events: [], newOffset: startOffset };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// ─── Collector class ──────────────────────────────────────────────────────────
|
|
182
|
+
class CopilotCollector {
|
|
183
|
+
/**
|
|
184
|
+
* @param sessionsDir Directory holding `<uuid>/` per-session subdirs.
|
|
185
|
+
* Defaults to `~/.copilot/session-state` (Copilot CLI's
|
|
186
|
+
* real location); tests can pass a fixture path.
|
|
187
|
+
*/
|
|
188
|
+
constructor(sessionsDir = path.join(os.homedir(), '.copilot', 'session-state')) {
|
|
189
|
+
this.sessionsDir = sessionsDir;
|
|
190
|
+
this.eventsCache = {};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Collect all live GitHub Copilot CLI sessions.
|
|
194
|
+
*
|
|
195
|
+
* Discovery uses lock files at <sessionsDir>/{uuid}/inuse.{PID}.lock rather
|
|
196
|
+
* than scanning the ps snapshot, giving us access to rich session data.
|
|
197
|
+
*
|
|
198
|
+
* @param processInfo Current ps snapshot (pid to ProcInfo)
|
|
199
|
+
* @param childrenMap Parent to children adjacency
|
|
200
|
+
* @param ports pid to listening port list
|
|
201
|
+
* @param gitMap cwd to {added, modified}
|
|
202
|
+
*/
|
|
203
|
+
collect(processInfo, childrenMap, ports, gitMap) {
|
|
204
|
+
const sessionsDir = this.sessionsDir;
|
|
205
|
+
if (!fs.existsSync(sessionsDir))
|
|
206
|
+
return [];
|
|
207
|
+
const sessions = [];
|
|
208
|
+
let sessionDirs;
|
|
209
|
+
try {
|
|
210
|
+
sessionDirs = fs.readdirSync(sessionsDir);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
for (const dirName of sessionDirs) {
|
|
216
|
+
const sessionDir = path.join(sessionsDir, dirName);
|
|
217
|
+
const session = this.loadSession(sessionDir, processInfo, childrenMap, ports, gitMap);
|
|
218
|
+
if (session)
|
|
219
|
+
sessions.push(session);
|
|
220
|
+
}
|
|
221
|
+
this.evictStaleCache(sessions);
|
|
222
|
+
sessions.sort((a, b) => b.startedAt - a.startedAt);
|
|
223
|
+
return sessions;
|
|
224
|
+
}
|
|
225
|
+
evictStaleCache(sessions) {
|
|
226
|
+
const activeIds = new Set(sessions.map((s) => s.sessionId));
|
|
227
|
+
for (const sid of Object.keys(this.eventsCache)) {
|
|
228
|
+
if (!activeIds.has(sid))
|
|
229
|
+
delete this.eventsCache[sid];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
loadSession(sessionDir, processInfo, childrenMap, ports, gitMap) {
|
|
233
|
+
// Require a workspace.yaml
|
|
234
|
+
const workspacePath = path.join(sessionDir, 'workspace.yaml');
|
|
235
|
+
if (!fs.existsSync(workspacePath))
|
|
236
|
+
return null;
|
|
237
|
+
const meta = parseWorkspaceYaml(workspacePath);
|
|
238
|
+
if (!meta)
|
|
239
|
+
return null;
|
|
240
|
+
// Find any lock file: inuse.<pid>.lock → extract PID
|
|
241
|
+
let activePid = null;
|
|
242
|
+
try {
|
|
243
|
+
for (const f of fs.readdirSync(sessionDir)) {
|
|
244
|
+
const m = f.match(/^inuse\.(\d+)\.lock$/);
|
|
245
|
+
if (m) {
|
|
246
|
+
activePid = parseInt(m[1], 10);
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
// Skip sessions with no lock file (completed, not currently running)
|
|
255
|
+
if (activePid === null)
|
|
256
|
+
return null;
|
|
257
|
+
// Confirm the PID is actually alive in the ps snapshot
|
|
258
|
+
const proc = processInfo.get(activePid);
|
|
259
|
+
const pidAlive = proc != null;
|
|
260
|
+
// Parse events.jsonl incrementally
|
|
261
|
+
const eventsPath = path.join(sessionDir, 'events.jsonl');
|
|
262
|
+
const sessionId = meta.id;
|
|
263
|
+
let cached = this.eventsCache[sessionId];
|
|
264
|
+
if (!cached) {
|
|
265
|
+
cached = {
|
|
266
|
+
model: '',
|
|
267
|
+
startedAt: 0,
|
|
268
|
+
totalOutputTokens: 0,
|
|
269
|
+
contextTokens: 0,
|
|
270
|
+
compactionCount: 0,
|
|
271
|
+
turnCount: 0,
|
|
272
|
+
toolCalls: [],
|
|
273
|
+
currentTask: '',
|
|
274
|
+
lastActivityMs: 0,
|
|
275
|
+
lastUserMsgMs: 0,
|
|
276
|
+
lastAssistantMsgMs: 0,
|
|
277
|
+
pendingToolCallId: null,
|
|
278
|
+
newOffset: 0,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (fs.existsSync(eventsPath)) {
|
|
282
|
+
const { events, newOffset } = readEventsFrom(eventsPath, cached.newOffset);
|
|
283
|
+
if (events.length > 0) {
|
|
284
|
+
applyEvents(cached, events);
|
|
285
|
+
cached.newOffset = newOffset;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
this.eventsCache[sessionId] = cached;
|
|
289
|
+
// Determine status
|
|
290
|
+
const now = Date.now();
|
|
291
|
+
const idleSecs = (now - cached.lastActivityMs) / 1000;
|
|
292
|
+
let status;
|
|
293
|
+
if (!pidAlive) {
|
|
294
|
+
status = 'done';
|
|
295
|
+
}
|
|
296
|
+
else if (cached.pendingToolCallId) {
|
|
297
|
+
status = 'executing';
|
|
298
|
+
}
|
|
299
|
+
else if (cached.lastUserMsgMs > cached.lastAssistantMsgMs && idleSecs < 60) {
|
|
300
|
+
status = 'thinking';
|
|
301
|
+
}
|
|
302
|
+
else if (idleSecs > 30) {
|
|
303
|
+
status = 'waiting';
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
status = 'thinking';
|
|
307
|
+
}
|
|
308
|
+
// Resolve context window + percent
|
|
309
|
+
const contextWindow = (0, types_1.contextWindowForModel)(cached.model, cached.contextTokens);
|
|
310
|
+
const contextPercent = cached.contextTokens > 0
|
|
311
|
+
? Math.min(100, Math.round((cached.contextTokens / contextWindow) * 100))
|
|
312
|
+
: 0;
|
|
313
|
+
// Current task: prefer workspace summary (set after compaction), else last user message
|
|
314
|
+
const taskStr = meta.summary || cached.currentTask || '';
|
|
315
|
+
const currentTasks = taskStr ? [taskStr] : [];
|
|
316
|
+
// Child processes
|
|
317
|
+
const childPids = (proc ? childrenMap.get(activePid) ?? [] : []);
|
|
318
|
+
const children = childPids
|
|
319
|
+
.map((cpid) => {
|
|
320
|
+
const cp = processInfo.get(cpid);
|
|
321
|
+
if (!cp)
|
|
322
|
+
return null;
|
|
323
|
+
return {
|
|
324
|
+
pid: cpid,
|
|
325
|
+
command: cp.command.split(/\s+/)[0].split('/').pop() ?? cp.command,
|
|
326
|
+
memKb: cp.rssKb,
|
|
327
|
+
port: (ports.get(cpid) ?? [])[0],
|
|
328
|
+
};
|
|
329
|
+
})
|
|
330
|
+
.filter((c) => c !== null);
|
|
331
|
+
// Git stats
|
|
332
|
+
const cwd = meta.cwd;
|
|
333
|
+
const git = gitMap.get(cwd) ?? (0, process_1.collectGitStats)(cwd);
|
|
334
|
+
return {
|
|
335
|
+
agentCli: 'copilot',
|
|
336
|
+
pid: activePid,
|
|
337
|
+
sessionId,
|
|
338
|
+
cwd,
|
|
339
|
+
projectName: path.basename(cwd) || cwd,
|
|
340
|
+
startedAt: cached.startedAt || new Date(meta.created_at || Date.now()).getTime(),
|
|
341
|
+
status,
|
|
342
|
+
model: cached.model || 'github-copilot',
|
|
343
|
+
effort: '',
|
|
344
|
+
contextPercent,
|
|
345
|
+
totalInputTokens: 0,
|
|
346
|
+
totalOutputTokens: cached.totalOutputTokens,
|
|
347
|
+
totalCacheRead: 0,
|
|
348
|
+
totalCacheCreate: 0,
|
|
349
|
+
turnCount: cached.turnCount,
|
|
350
|
+
currentTasks,
|
|
351
|
+
memMb: proc ? Math.round(proc.rssKb / 1024) : 0,
|
|
352
|
+
version: '',
|
|
353
|
+
gitBranch: meta.branch || '',
|
|
354
|
+
gitAdded: git.added,
|
|
355
|
+
gitModified: git.modified,
|
|
356
|
+
tokenHistory: [],
|
|
357
|
+
contextHistory: [],
|
|
358
|
+
compactionCount: cached.compactionCount,
|
|
359
|
+
contextWindow,
|
|
360
|
+
subagents: [],
|
|
361
|
+
memFileCount: 0,
|
|
362
|
+
memLineCount: 0,
|
|
363
|
+
children,
|
|
364
|
+
initialPrompt: currentTasks[0] ?? '',
|
|
365
|
+
firstAssistantText: '',
|
|
366
|
+
toolCalls: cached.toolCalls,
|
|
367
|
+
pendingSinceMs: cached.pendingToolCallId ? cached.lastActivityMs : 0,
|
|
368
|
+
thinkingSinceMs: cached.lastUserMsgMs > cached.lastAssistantMsgMs ? cached.lastUserMsgMs : 0,
|
|
369
|
+
fileAccesses: [],
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
exports.CopilotCollector = CopilotCollector;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Multi-collector orchestrator.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors abtop/src/collector/mod.rs `MultiCollector`.
|
|
6
|
+
* Runs Claude and Codex collectors on every tick, staggered so the
|
|
7
|
+
* expensive operations (lsof, git) only run on slow ticks.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const mc = new MultiCollector();
|
|
11
|
+
* setInterval(() => {
|
|
12
|
+
* const snapshot = mc.tick();
|
|
13
|
+
* // use snapshot.sessions, snapshot.rateLimits, snapshot.orphanPorts
|
|
14
|
+
* }, 2000);
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.MultiCollector = void 0;
|
|
18
|
+
const process_1 = require("./process");
|
|
19
|
+
const claude_1 = require("./claude");
|
|
20
|
+
const codex_1 = require("./codex");
|
|
21
|
+
const copilot_1 = require("./copilot");
|
|
22
|
+
const rate_limit_1 = require("./rate-limit");
|
|
23
|
+
const secrets_1 = require("./secrets");
|
|
24
|
+
class MultiCollector {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.claude = new claude_1.ClaudeCollector();
|
|
27
|
+
this.codex = new codex_1.CodexCollector();
|
|
28
|
+
this.copilot = new copilot_1.CopilotCollector();
|
|
29
|
+
this.tickCount = 0;
|
|
30
|
+
/** Ports seen in the previous tick that had a live session behind them. */
|
|
31
|
+
this.prevSessionPids = new Set();
|
|
32
|
+
/** Known orphan ports (pid → port[]) persisted across ticks. */
|
|
33
|
+
this.orphanPortMap = new Map();
|
|
34
|
+
/** Polling interval config (mirrors abtop defaults). */
|
|
35
|
+
this.SLOW_TICK_EVERY = 5; // slow tick every 5 fast ticks (= every 10s at 2s interval)
|
|
36
|
+
}
|
|
37
|
+
tick() {
|
|
38
|
+
this.tickCount++;
|
|
39
|
+
const slowTick = this.tickCount % this.SLOW_TICK_EVERY === 0;
|
|
40
|
+
// ── Process table (fast tick) ───────────────────────────────────────────
|
|
41
|
+
const processInfo = (0, process_1.getProcessInfo)();
|
|
42
|
+
const childrenMap = (0, process_1.getChildrenMap)(processInfo);
|
|
43
|
+
// ── Ports + git (slow tick only) ─────────────────────────────────────────
|
|
44
|
+
let ports = new Map();
|
|
45
|
+
let gitMap = new Map();
|
|
46
|
+
if (slowTick) {
|
|
47
|
+
ports = (0, process_1.getListeningPorts)();
|
|
48
|
+
}
|
|
49
|
+
// ── Collect sessions ──────────────────────────────────────────────────────
|
|
50
|
+
const claudeSessions = this.claude.collect(processInfo, childrenMap, ports, gitMap);
|
|
51
|
+
const codexSessions = this.codex.collect(processInfo, childrenMap, ports, gitMap);
|
|
52
|
+
const copilotSessions = this.copilot.collect(processInfo, childrenMap, ports, gitMap);
|
|
53
|
+
const sessions = [...claudeSessions, ...codexSessions, ...copilotSessions];
|
|
54
|
+
// ── Git stats (slow tick, after session list is known) ────────────────────
|
|
55
|
+
if (slowTick) {
|
|
56
|
+
const cwds = new Set(sessions.map((s) => s.cwd));
|
|
57
|
+
for (const cwd of cwds) {
|
|
58
|
+
gitMap.set(cwd, (0, process_1.collectGitStats)(cwd));
|
|
59
|
+
}
|
|
60
|
+
// Backfill git stats into sessions
|
|
61
|
+
for (const s of sessions) {
|
|
62
|
+
const g = gitMap.get(s.cwd);
|
|
63
|
+
if (g) {
|
|
64
|
+
s.gitAdded = g.added;
|
|
65
|
+
s.gitModified = g.modified;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// ── Rate limits ───────────────────────────────────────────────────────────
|
|
70
|
+
const rateLimits = [];
|
|
71
|
+
if (slowTick) {
|
|
72
|
+
rateLimits.push(...(0, rate_limit_1.readClaudeRateLimits)());
|
|
73
|
+
}
|
|
74
|
+
const codexRl = this.codex.lastRateLimit ?? (0, rate_limit_1.readCodexRateLimitCache)();
|
|
75
|
+
if (codexRl)
|
|
76
|
+
rateLimits.push(codexRl);
|
|
77
|
+
// ── Orphan port detection ─────────────────────────────────────────────────
|
|
78
|
+
const orphanPorts = this.detectOrphanPorts(sessions, processInfo, ports, slowTick);
|
|
79
|
+
return {
|
|
80
|
+
sessions: sessions.map(normalizeSession),
|
|
81
|
+
rateLimits,
|
|
82
|
+
orphanPorts,
|
|
83
|
+
collectedAt: Date.now(),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
detectOrphanPorts(sessions, processInfo, ports, slowTick) {
|
|
87
|
+
if (!slowTick)
|
|
88
|
+
return [...this.flattenOrphans()];
|
|
89
|
+
const sessionPids = new Set(sessions.map((s) => s.pid));
|
|
90
|
+
const sessionChildPids = new Set();
|
|
91
|
+
for (const s of sessions) {
|
|
92
|
+
for (const c of s.children)
|
|
93
|
+
sessionChildPids.add(c.pid);
|
|
94
|
+
}
|
|
95
|
+
// Any port-holding PID that is NOT under a live session → orphan candidate
|
|
96
|
+
for (const [pid, pidPorts] of ports) {
|
|
97
|
+
if (sessionPids.has(pid) || sessionChildPids.has(pid)) {
|
|
98
|
+
// Still alive under a session — remove from orphan map if present
|
|
99
|
+
this.orphanPortMap.delete(pid);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// Port-holding PID with no parent session
|
|
103
|
+
const proc = processInfo.get(pid);
|
|
104
|
+
if (!proc) {
|
|
105
|
+
this.orphanPortMap.delete(pid);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
this.orphanPortMap.set(pid, pidPorts.map((port) => ({
|
|
109
|
+
port,
|
|
110
|
+
command: proc.command,
|
|
111
|
+
projectName: '?',
|
|
112
|
+
})));
|
|
113
|
+
}
|
|
114
|
+
// Remove orphan entries whose PID has died
|
|
115
|
+
for (const pid of this.orphanPortMap.keys()) {
|
|
116
|
+
if (!processInfo.has(pid))
|
|
117
|
+
this.orphanPortMap.delete(pid);
|
|
118
|
+
}
|
|
119
|
+
return [...this.flattenOrphans()];
|
|
120
|
+
}
|
|
121
|
+
flattenOrphans() {
|
|
122
|
+
const result = [];
|
|
123
|
+
for (const [pid, entries] of this.orphanPortMap) {
|
|
124
|
+
for (const e of entries) {
|
|
125
|
+
result.push({ pid, port: e.port, command: e.command, projectName: e.projectName });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
exports.MultiCollector = MultiCollector;
|
|
132
|
+
// ─── Snapshot normalization ───────────────────────────────────────────────────
|
|
133
|
+
const VALID_STATUSES = new Set([
|
|
134
|
+
'thinking',
|
|
135
|
+
'executing',
|
|
136
|
+
'waiting',
|
|
137
|
+
'rateLimited',
|
|
138
|
+
'done',
|
|
139
|
+
]);
|
|
140
|
+
function num(n) {
|
|
141
|
+
return typeof n === 'number' && Number.isFinite(n) ? n : 0;
|
|
142
|
+
}
|
|
143
|
+
function str(s) {
|
|
144
|
+
return typeof s === 'string' ? s : '';
|
|
145
|
+
}
|
|
146
|
+
function arr(a) {
|
|
147
|
+
return Array.isArray(a) ? a : [];
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Single source of truth for the snapshot wire shape. Every `AgentSession` that
|
|
151
|
+
* leaves `tick()` is run through this so:
|
|
152
|
+
*
|
|
153
|
+
* • numeric fields are real numbers (never null / undefined / NaN);
|
|
154
|
+
* • array fields are arrays (never null / undefined);
|
|
155
|
+
* • status is a known `SessionStatus` (any unrecognized value defaults to
|
|
156
|
+
* 'waiting' — keeps the front end's filter sets honest);
|
|
157
|
+
* • free-text fields that originate from agent-controlled content
|
|
158
|
+
* (`currentTasks`, `initialPrompt`, `firstAssistantText`, `toolCalls[].arg`,
|
|
159
|
+
* `fileAccesses[].path`) are run through `redactSecrets()` so a pasted
|
|
160
|
+
* `sk-ant-…` doesn't end up rendered in the dashboard.
|
|
161
|
+
*
|
|
162
|
+
* Front-end null-coalescing guards (e.g. `value ?? 0`) become defence-in-depth
|
|
163
|
+
* after this runs server-side.
|
|
164
|
+
*/
|
|
165
|
+
function normalizeSession(s) {
|
|
166
|
+
const status = VALID_STATUSES.has(s.status) ? s.status : 'waiting';
|
|
167
|
+
return {
|
|
168
|
+
agentCli: str(s.agentCli),
|
|
169
|
+
pid: num(s.pid),
|
|
170
|
+
sessionId: str(s.sessionId),
|
|
171
|
+
cwd: str(s.cwd),
|
|
172
|
+
projectName: str(s.projectName),
|
|
173
|
+
startedAt: num(s.startedAt),
|
|
174
|
+
status,
|
|
175
|
+
model: str(s.model),
|
|
176
|
+
effort: str(s.effort),
|
|
177
|
+
contextPercent: num(s.contextPercent),
|
|
178
|
+
totalInputTokens: num(s.totalInputTokens),
|
|
179
|
+
totalOutputTokens: num(s.totalOutputTokens),
|
|
180
|
+
totalCacheRead: num(s.totalCacheRead),
|
|
181
|
+
totalCacheCreate: num(s.totalCacheCreate),
|
|
182
|
+
turnCount: num(s.turnCount),
|
|
183
|
+
currentTasks: arr(s.currentTasks).map((t) => (0, secrets_1.redactSecrets)(str(t))),
|
|
184
|
+
memMb: num(s.memMb),
|
|
185
|
+
version: str(s.version),
|
|
186
|
+
gitBranch: str(s.gitBranch),
|
|
187
|
+
gitAdded: num(s.gitAdded),
|
|
188
|
+
gitModified: num(s.gitModified),
|
|
189
|
+
tokenHistory: arr(s.tokenHistory).map(num),
|
|
190
|
+
contextHistory: arr(s.contextHistory).map(num),
|
|
191
|
+
compactionCount: num(s.compactionCount),
|
|
192
|
+
contextWindow: num(s.contextWindow) || 200000,
|
|
193
|
+
subagents: arr(s.subagents).map((sa) => ({
|
|
194
|
+
name: str(sa?.name),
|
|
195
|
+
status: sa?.status === 'done' ? 'done' : 'working',
|
|
196
|
+
tokens: num(sa?.tokens),
|
|
197
|
+
})),
|
|
198
|
+
memFileCount: num(s.memFileCount),
|
|
199
|
+
memLineCount: num(s.memLineCount),
|
|
200
|
+
children: arr(s.children).map((c) => ({
|
|
201
|
+
pid: num(c?.pid),
|
|
202
|
+
command: str(c?.command),
|
|
203
|
+
memKb: num(c?.memKb),
|
|
204
|
+
port: typeof c?.port === 'number' ? c.port : undefined,
|
|
205
|
+
})),
|
|
206
|
+
initialPrompt: (0, secrets_1.redactSecrets)(str(s.initialPrompt)),
|
|
207
|
+
firstAssistantText: (0, secrets_1.redactSecrets)(str(s.firstAssistantText)),
|
|
208
|
+
toolCalls: arr(s.toolCalls).map((tc) => ({
|
|
209
|
+
name: str(tc?.name),
|
|
210
|
+
arg: (0, secrets_1.redactSecrets)(str(tc?.arg)),
|
|
211
|
+
durationMs: num(tc?.durationMs),
|
|
212
|
+
})),
|
|
213
|
+
pendingSinceMs: num(s.pendingSinceMs),
|
|
214
|
+
thinkingSinceMs: num(s.thinkingSinceMs),
|
|
215
|
+
fileAccesses: arr(s.fileAccesses).map((fa) => ({
|
|
216
|
+
path: (0, secrets_1.redactSecrets)(str(fa?.path)),
|
|
217
|
+
operation: fa?.operation === 'Write' || fa?.operation === 'Edit' ? fa.operation : 'Read',
|
|
218
|
+
turnIndex: num(fa?.turnIndex),
|
|
219
|
+
})),
|
|
220
|
+
};
|
|
221
|
+
}
|