@way_marks/server 4.0.2 → 4.3.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,516 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Codex CLI session collector.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors abtop/src/collector/codex.rs:
|
|
6
|
+
* - Discovery: find running `codex` processes via ps, map PID → open rollout-*.jsonl via lsof
|
|
7
|
+
* - Parse ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
|
|
8
|
+
* - Extract session_meta, token_count, event_msg, response_item events
|
|
9
|
+
* - Expose rate limit info from token_count events
|
|
10
|
+
*
|
|
11
|
+
* Data sources are undocumented Codex CLI internals — use defensive parsing throughout.
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.CodexCollector = void 0;
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const os = __importStar(require("os"));
|
|
51
|
+
const types_1 = require("./types");
|
|
52
|
+
const process_1 = require("./process");
|
|
53
|
+
const rate_limit_1 = require("./rate-limit");
|
|
54
|
+
const secrets_1 = require("./secrets");
|
|
55
|
+
// ─── Collector class ──────────────────────────────────────────────────────────
|
|
56
|
+
class CodexCollector {
|
|
57
|
+
constructor() {
|
|
58
|
+
this.lastRateLimit = null;
|
|
59
|
+
this.sessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Collect all live and recently finished Codex sessions.
|
|
63
|
+
*/
|
|
64
|
+
collect(processInfo, childrenMap, ports, gitMap) {
|
|
65
|
+
if (!fs.existsSync(this.sessionsDir)) {
|
|
66
|
+
this.lastRateLimit = null;
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
this.lastRateLimit = null;
|
|
70
|
+
const codexPids = findCodexPids(processInfo);
|
|
71
|
+
const justPids = codexPids.map(([p]) => p);
|
|
72
|
+
const pidToJsonl = mapPidsToJsonl(justPids);
|
|
73
|
+
const pidIsExec = new Map(codexPids);
|
|
74
|
+
const sessions = [];
|
|
75
|
+
const seenJsonl = new Set();
|
|
76
|
+
// Active sessions: running codex processes with open JSONL files
|
|
77
|
+
for (const [pid, jsonlPath] of pidToJsonl) {
|
|
78
|
+
const isExec = pidIsExec.get(pid) ?? false;
|
|
79
|
+
const parsed = this.loadSessionWithRateLimit(pid, isExec, jsonlPath, processInfo, childrenMap, ports, gitMap);
|
|
80
|
+
if (!parsed)
|
|
81
|
+
continue;
|
|
82
|
+
const [session, rl] = parsed;
|
|
83
|
+
seenJsonl.add(jsonlPath);
|
|
84
|
+
if (rl) {
|
|
85
|
+
const newer = !this.lastRateLimit || (rl.updatedAt ?? 0) > (this.lastRateLimit.updatedAt ?? 0);
|
|
86
|
+
if (newer) {
|
|
87
|
+
(0, rate_limit_1.writeCodexRateLimitCache)(rl);
|
|
88
|
+
this.lastRateLimit = rl;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
sessions.push(session);
|
|
92
|
+
}
|
|
93
|
+
// Recently finished sessions: scan today's JSONL not owned by a running process
|
|
94
|
+
const todayDir = getTodaySessionDir(this.sessionsDir);
|
|
95
|
+
if (todayDir && fs.existsSync(todayDir)) {
|
|
96
|
+
try {
|
|
97
|
+
const entries = fs.readdirSync(todayDir, { withFileTypes: true });
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
if (entry.isSymbolicLink())
|
|
100
|
+
continue;
|
|
101
|
+
if (!entry.name.endsWith('.jsonl'))
|
|
102
|
+
continue;
|
|
103
|
+
const filePath = path.join(todayDir, entry.name);
|
|
104
|
+
if (seenJsonl.has(filePath))
|
|
105
|
+
continue;
|
|
106
|
+
// Only include recently finished (<5 min old)
|
|
107
|
+
const stat = safeStatSync(filePath);
|
|
108
|
+
if (!stat || (Date.now() - stat.mtimeMs) / 1000 > 300)
|
|
109
|
+
continue;
|
|
110
|
+
const parsed = this.loadSessionWithRateLimit(null, false, filePath, processInfo, childrenMap, ports, gitMap);
|
|
111
|
+
if (!parsed)
|
|
112
|
+
continue;
|
|
113
|
+
const [session, rl] = parsed;
|
|
114
|
+
if (rl) {
|
|
115
|
+
const newer = !this.lastRateLimit || (rl.updatedAt ?? 0) > (this.lastRateLimit.updatedAt ?? 0);
|
|
116
|
+
if (newer) {
|
|
117
|
+
(0, rate_limit_1.writeCodexRateLimitCache)(rl);
|
|
118
|
+
this.lastRateLimit = rl;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
sessions.push(session);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch { /* ignore */ }
|
|
125
|
+
}
|
|
126
|
+
sessions.sort((a, b) => b.startedAt - a.startedAt);
|
|
127
|
+
return sessions;
|
|
128
|
+
}
|
|
129
|
+
loadSessionWithRateLimit(pid, isExec, jsonlPath, processInfo, childrenMap, ports, gitMap) {
|
|
130
|
+
const result = parseCodexJSONL(jsonlPath);
|
|
131
|
+
if (!result)
|
|
132
|
+
return null;
|
|
133
|
+
const proc = pid != null ? processInfo.get(pid) : null;
|
|
134
|
+
const memMb = proc ? Math.floor(proc.rssKb / 1024) : 0;
|
|
135
|
+
const displayPid = pid ?? 0;
|
|
136
|
+
const pidAlive = proc != null;
|
|
137
|
+
const status = !pidAlive || (isExec && result.taskComplete)
|
|
138
|
+
? 'done'
|
|
139
|
+
: (() => {
|
|
140
|
+
const hasActiveChild = pid != null && (0, process_1.hasActiveDescendant)(pid, childrenMap, processInfo, 5.0);
|
|
141
|
+
if (hasActiveChild || result.pendingSinceMs > 0)
|
|
142
|
+
return 'executing';
|
|
143
|
+
if (result.modelGenerating)
|
|
144
|
+
return 'thinking';
|
|
145
|
+
return 'waiting';
|
|
146
|
+
})();
|
|
147
|
+
const currentTasks = result.currentTask
|
|
148
|
+
? [result.currentTask]
|
|
149
|
+
: !pidAlive || (isExec && result.taskComplete)
|
|
150
|
+
? ['finished']
|
|
151
|
+
: status === 'waiting'
|
|
152
|
+
? ['waiting for input']
|
|
153
|
+
: ['thinking...'];
|
|
154
|
+
const contextWindow = (0, types_1.contextWindowForModel)(result.model, 0);
|
|
155
|
+
const contextPercent = contextWindow > 0 && result.lastContextTokens > 0
|
|
156
|
+
? (result.lastContextTokens / contextWindow) * 100
|
|
157
|
+
: 0;
|
|
158
|
+
const children = pid != null
|
|
159
|
+
? collectDescendants(pid, childrenMap, processInfo, ports)
|
|
160
|
+
: [];
|
|
161
|
+
const git = gitMap.get(result.cwd) ?? { added: 0, modified: 0 };
|
|
162
|
+
const session = {
|
|
163
|
+
agentCli: 'codex',
|
|
164
|
+
pid: displayPid,
|
|
165
|
+
sessionId: result.sessionId,
|
|
166
|
+
cwd: result.cwd,
|
|
167
|
+
projectName: result.cwd.split('/').pop() || '?',
|
|
168
|
+
startedAt: result.startedAt,
|
|
169
|
+
status,
|
|
170
|
+
model: result.model,
|
|
171
|
+
effort: result.effort,
|
|
172
|
+
contextPercent,
|
|
173
|
+
totalInputTokens: result.totalInput,
|
|
174
|
+
totalOutputTokens: result.totalOutput,
|
|
175
|
+
totalCacheRead: result.totalCacheRead,
|
|
176
|
+
totalCacheCreate: 0, // Codex doesn't report cache writes
|
|
177
|
+
turnCount: result.turnCount,
|
|
178
|
+
currentTasks,
|
|
179
|
+
memMb,
|
|
180
|
+
version: result.version,
|
|
181
|
+
gitBranch: result.gitBranch,
|
|
182
|
+
gitAdded: git.added,
|
|
183
|
+
gitModified: git.modified,
|
|
184
|
+
tokenHistory: result.tokenHistory,
|
|
185
|
+
contextHistory: [],
|
|
186
|
+
compactionCount: 0,
|
|
187
|
+
contextWindow,
|
|
188
|
+
subagents: [],
|
|
189
|
+
memFileCount: 0,
|
|
190
|
+
memLineCount: 0,
|
|
191
|
+
children,
|
|
192
|
+
initialPrompt: result.initialPrompt,
|
|
193
|
+
firstAssistantText: '',
|
|
194
|
+
toolCalls: result.toolCalls,
|
|
195
|
+
pendingSinceMs: result.pendingSinceMs,
|
|
196
|
+
thinkingSinceMs: result.thinkingSinceMs,
|
|
197
|
+
fileAccesses: [],
|
|
198
|
+
};
|
|
199
|
+
return [session, result.rateLimit];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
exports.CodexCollector = CodexCollector;
|
|
203
|
+
// ─── Process discovery ────────────────────────────────────────────────────────
|
|
204
|
+
function findCodexPids(processInfo) {
|
|
205
|
+
const pids = [];
|
|
206
|
+
for (const [pid, info] of processInfo) {
|
|
207
|
+
if (!(0, process_1.cmdHasBinary)(info.command, 'codex'))
|
|
208
|
+
continue;
|
|
209
|
+
if (info.command.includes('app-server') || info.command.includes('grep'))
|
|
210
|
+
continue;
|
|
211
|
+
const isExec = info.command.includes(' exec');
|
|
212
|
+
pids.push([pid, isExec]);
|
|
213
|
+
}
|
|
214
|
+
return pids;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Map codex PIDs to their open rollout-*.jsonl files via lsof.
|
|
218
|
+
* Mirrors abtop's `map_pid_to_jsonl` (non-Linux path).
|
|
219
|
+
*/
|
|
220
|
+
function mapPidsToJsonl(pids) {
|
|
221
|
+
const map = new Map();
|
|
222
|
+
if (pids.length === 0)
|
|
223
|
+
return map;
|
|
224
|
+
const openPaths = (0, process_1.mapPidsToOpenPaths)(pids);
|
|
225
|
+
for (const [pid, paths] of openPaths) {
|
|
226
|
+
for (const p of paths) {
|
|
227
|
+
const base = p.split('/').pop() ?? '';
|
|
228
|
+
if (base.startsWith('rollout-') && base.endsWith('.jsonl')) {
|
|
229
|
+
map.set(pid, p);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return map;
|
|
235
|
+
}
|
|
236
|
+
// ─── JSONL parser ─────────────────────────────────────────────────────────────
|
|
237
|
+
const MAX_LINE_BYTES = 10 * 1024 * 1024;
|
|
238
|
+
function parseCodexJSONL(filePath) {
|
|
239
|
+
let content;
|
|
240
|
+
try {
|
|
241
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const result = {
|
|
247
|
+
sessionId: '',
|
|
248
|
+
cwd: '',
|
|
249
|
+
startedAt: 0,
|
|
250
|
+
model: '-',
|
|
251
|
+
effort: '',
|
|
252
|
+
version: '',
|
|
253
|
+
gitBranch: '',
|
|
254
|
+
contextWindow: 0,
|
|
255
|
+
turnCount: 0,
|
|
256
|
+
currentTask: '',
|
|
257
|
+
taskComplete: false,
|
|
258
|
+
modelGenerating: false,
|
|
259
|
+
lastActivityMs: 0,
|
|
260
|
+
initialPrompt: '',
|
|
261
|
+
totalInput: 0,
|
|
262
|
+
totalOutput: 0,
|
|
263
|
+
totalCacheRead: 0,
|
|
264
|
+
lastContextTokens: 0,
|
|
265
|
+
tokenHistory: [],
|
|
266
|
+
rateLimit: null,
|
|
267
|
+
toolCalls: [],
|
|
268
|
+
pendingSinceMs: 0,
|
|
269
|
+
thinkingSinceMs: 0,
|
|
270
|
+
};
|
|
271
|
+
const callIndices = new Map();
|
|
272
|
+
const callStarts = new Map();
|
|
273
|
+
const pendingTasks = [];
|
|
274
|
+
for (const rawLine of content.split('\n')) {
|
|
275
|
+
if (Buffer.byteLength(rawLine, 'utf8') > MAX_LINE_BYTES)
|
|
276
|
+
break;
|
|
277
|
+
const line = rawLine.trim();
|
|
278
|
+
if (!line)
|
|
279
|
+
continue;
|
|
280
|
+
let val;
|
|
281
|
+
try {
|
|
282
|
+
val = JSON.parse(line);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
// Update lastActivityMs from timestamp
|
|
288
|
+
const tsMs = parseIso(val['timestamp']);
|
|
289
|
+
if (tsMs > result.lastActivityMs)
|
|
290
|
+
result.lastActivityMs = tsMs;
|
|
291
|
+
switch (val['type']) {
|
|
292
|
+
case 'session_meta': {
|
|
293
|
+
const payload = val['payload'];
|
|
294
|
+
if (!payload)
|
|
295
|
+
break;
|
|
296
|
+
if (typeof payload['id'] === 'string')
|
|
297
|
+
result.sessionId = payload['id'];
|
|
298
|
+
if (typeof payload['cwd'] === 'string')
|
|
299
|
+
result.cwd = payload['cwd'];
|
|
300
|
+
if (typeof payload['cli_version'] === 'string')
|
|
301
|
+
result.version = payload['cli_version'];
|
|
302
|
+
const startTs = parseIso(payload['timestamp']);
|
|
303
|
+
if (startTs > 0)
|
|
304
|
+
result.startedAt = startTs;
|
|
305
|
+
const git = payload['git'];
|
|
306
|
+
if (typeof git?.['branch'] === 'string')
|
|
307
|
+
result.gitBranch = git['branch'];
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case 'event_msg': {
|
|
311
|
+
const payload = val['payload'];
|
|
312
|
+
if (!payload)
|
|
313
|
+
break;
|
|
314
|
+
switch (payload['type']) {
|
|
315
|
+
case 'task_started': {
|
|
316
|
+
const cw = payload['model_context_window'];
|
|
317
|
+
if (cw)
|
|
318
|
+
result.contextWindow = cw;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case 'user_message': {
|
|
322
|
+
result.modelGenerating = true;
|
|
323
|
+
result.thinkingSinceMs = tsMs;
|
|
324
|
+
if (!result.initialPrompt && typeof payload['message'] === 'string') {
|
|
325
|
+
result.initialPrompt = (0, secrets_1.redactSecrets)(payload['message'].slice(0, 120));
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
case 'agent_message': {
|
|
330
|
+
result.modelGenerating = false;
|
|
331
|
+
result.turnCount++;
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
case 'task_complete': {
|
|
335
|
+
result.taskComplete = true;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
case 'token_count': {
|
|
339
|
+
const info = payload['info'];
|
|
340
|
+
if (!info)
|
|
341
|
+
break;
|
|
342
|
+
// Cumulative totals
|
|
343
|
+
const total = info['total_token_usage'];
|
|
344
|
+
if (total) {
|
|
345
|
+
result.totalInput = total['input_tokens'] || 0;
|
|
346
|
+
result.totalOutput = total['output_tokens'] || 0;
|
|
347
|
+
result.totalCacheRead =
|
|
348
|
+
(total['cached_input_tokens'] || total['cache_read_input_tokens']) || 0;
|
|
349
|
+
}
|
|
350
|
+
// Per-turn context (for sparkline and %)
|
|
351
|
+
const last = info['last_token_usage'];
|
|
352
|
+
if (last) {
|
|
353
|
+
const inp = last['input_tokens'] || 0;
|
|
354
|
+
const out = last['output_tokens'] || 0;
|
|
355
|
+
const cache = (last['cached_input_tokens'] || last['cache_read_input_tokens']) || 0;
|
|
356
|
+
result.lastContextTokens = inp + cache;
|
|
357
|
+
if (result.tokenHistory.length < 10000) {
|
|
358
|
+
result.tokenHistory.push(inp + out + cache);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (info['model_context_window'])
|
|
362
|
+
result.contextWindow = info['model_context_window'];
|
|
363
|
+
// Rate limits
|
|
364
|
+
const rl = payload['rate_limits'];
|
|
365
|
+
if (rl) {
|
|
366
|
+
const eventSecs = tsMs > 0 ? Math.floor(tsMs / 1000) : undefined;
|
|
367
|
+
const rateLimit = { source: 'codex', updatedAt: eventSecs };
|
|
368
|
+
for (const slot of ['primary', 'secondary']) {
|
|
369
|
+
const w = rl[slot];
|
|
370
|
+
if (!w)
|
|
371
|
+
continue;
|
|
372
|
+
const mins = w['window_minutes'] || 0;
|
|
373
|
+
const pct = w['used_percent'];
|
|
374
|
+
const resets = w['resets_at'];
|
|
375
|
+
if (mins <= 300) {
|
|
376
|
+
rateLimit.fiveHourPct = pct;
|
|
377
|
+
rateLimit.fiveHourResetsAt = resets;
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
rateLimit.sevenDayPct = pct;
|
|
381
|
+
rateLimit.sevenDayResetsAt = resets;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (rateLimit.fiveHourPct != null || rateLimit.sevenDayPct != null) {
|
|
385
|
+
result.rateLimit = rateLimit;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
case 'turn_context': {
|
|
394
|
+
const payload = val['payload'];
|
|
395
|
+
if (!payload)
|
|
396
|
+
break;
|
|
397
|
+
if (typeof payload['model'] === 'string')
|
|
398
|
+
result.model = payload['model'];
|
|
399
|
+
if (typeof payload['effort'] === 'string')
|
|
400
|
+
result.effort = payload['effort'];
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
case 'response_item': {
|
|
404
|
+
const payload = val['payload'];
|
|
405
|
+
if (!payload || payload['type'] !== 'function_call')
|
|
406
|
+
break;
|
|
407
|
+
const name = payload['name'] || '';
|
|
408
|
+
const callId = payload['call_id'] || '';
|
|
409
|
+
const argsStr = payload['arguments'] || '{}';
|
|
410
|
+
const arg = parseCodexToolArg(argsStr);
|
|
411
|
+
if (!result.currentTask)
|
|
412
|
+
result.currentTask = `${name} ${arg}`.trim();
|
|
413
|
+
if (callId && result.toolCalls.length < 500) {
|
|
414
|
+
const idx = result.toolCalls.length;
|
|
415
|
+
result.toolCalls.push({ name, arg, durationMs: 0 });
|
|
416
|
+
callIndices.set(callId, idx);
|
|
417
|
+
callStarts.set(callId, tsMs);
|
|
418
|
+
pendingTasks.push([callId, name]);
|
|
419
|
+
// Track pending_since: earliest open tool call (0 when none)
|
|
420
|
+
const opens = Array.from(callStarts.values()).filter((v) => v > 0);
|
|
421
|
+
result.pendingSinceMs = opens.length > 0 ? Math.min(...opens) : 0;
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
case 'function_call_output': {
|
|
426
|
+
const payload = val['payload'];
|
|
427
|
+
const callId = payload?.['call_id'] || '';
|
|
428
|
+
if (!callId)
|
|
429
|
+
break;
|
|
430
|
+
const startMs = callStarts.get(callId);
|
|
431
|
+
if (startMs != null) {
|
|
432
|
+
const idx = callIndices.get(callId);
|
|
433
|
+
if (idx != null)
|
|
434
|
+
result.toolCalls[idx].durationMs = Math.max(0, tsMs - startMs);
|
|
435
|
+
callStarts.delete(callId);
|
|
436
|
+
callIndices.delete(callId);
|
|
437
|
+
}
|
|
438
|
+
pendingTasks.splice(0, pendingTasks.length, ...pendingTasks.filter(([id]) => id !== callId));
|
|
439
|
+
// Recompute pending_since (guard against empty iterator → Infinity)
|
|
440
|
+
const remaining = callStarts.size > 0 ? Array.from(callStarts.values()) : [];
|
|
441
|
+
result.pendingSinceMs = remaining.length > 0 ? Math.min(...remaining) : 0;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (!result.sessionId && !result.cwd)
|
|
447
|
+
return null;
|
|
448
|
+
return result;
|
|
449
|
+
}
|
|
450
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
451
|
+
function parseCodexToolArg(argumentsStr) {
|
|
452
|
+
let val;
|
|
453
|
+
try {
|
|
454
|
+
val = JSON.parse(argumentsStr);
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
return '';
|
|
458
|
+
}
|
|
459
|
+
for (const key of ['file_path', 'path']) {
|
|
460
|
+
if (typeof val[key] === 'string') {
|
|
461
|
+
const short = val[key].split('/').pop() ?? val[key];
|
|
462
|
+
return (0, secrets_1.redactSecrets)(short).slice(0, 120);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
for (const key of ['cmd', 'command', 'chars', 'target', 'session_id']) {
|
|
466
|
+
const raw = val[key];
|
|
467
|
+
if (typeof raw === 'string')
|
|
468
|
+
return (0, secrets_1.redactSecrets)(raw).slice(0, 120);
|
|
469
|
+
if (typeof raw === 'number' || typeof raw === 'boolean')
|
|
470
|
+
return String(raw).slice(0, 120);
|
|
471
|
+
}
|
|
472
|
+
for (const v of Object.values(val)) {
|
|
473
|
+
if (typeof v === 'string' && v.length > 0)
|
|
474
|
+
return (0, secrets_1.redactSecrets)(v).slice(0, 120);
|
|
475
|
+
}
|
|
476
|
+
return '';
|
|
477
|
+
}
|
|
478
|
+
function getTodaySessionDir(sessionsDir) {
|
|
479
|
+
const now = new Date();
|
|
480
|
+
const yyyy = now.getFullYear().toString();
|
|
481
|
+
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
482
|
+
const dd = String(now.getDate()).padStart(2, '0');
|
|
483
|
+
return path.join(sessionsDir, yyyy, mm, dd);
|
|
484
|
+
}
|
|
485
|
+
function parseIso(ts) {
|
|
486
|
+
if (!ts)
|
|
487
|
+
return 0;
|
|
488
|
+
const ms = new Date(ts).getTime();
|
|
489
|
+
return isNaN(ms) ? 0 : ms;
|
|
490
|
+
}
|
|
491
|
+
function safeStatSync(filePath) {
|
|
492
|
+
try {
|
|
493
|
+
return fs.statSync(filePath);
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function collectDescendants(pid, childrenMap, processInfo, ports) {
|
|
500
|
+
const result = [];
|
|
501
|
+
const stack = [...(childrenMap.get(pid) ?? [])];
|
|
502
|
+
const visited = new Set();
|
|
503
|
+
while (stack.length > 0) {
|
|
504
|
+
const cpid = stack.pop();
|
|
505
|
+
if (visited.has(cpid))
|
|
506
|
+
continue;
|
|
507
|
+
visited.add(cpid);
|
|
508
|
+
const proc = processInfo.get(cpid);
|
|
509
|
+
if (proc) {
|
|
510
|
+
const port = ports.get(cpid)?.[0];
|
|
511
|
+
result.push({ pid: cpid, command: proc.command, memKb: proc.rssKb, port });
|
|
512
|
+
}
|
|
513
|
+
stack.push(...(childrenMap.get(cpid) ?? []));
|
|
514
|
+
}
|
|
515
|
+
return result;
|
|
516
|
+
}
|