@magnusekdahl/parallix 1.2.0 → 1.2.1
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/agents/agents.js +737 -838
- package/lib/agents/agents.ts +930 -0
- package/lib/agents/claude-telemetry.js +149 -154
- package/lib/agents/claude-telemetry.ts +233 -0
- package/lib/agents/claude.js +121 -131
- package/lib/agents/claude.ts +156 -0
- package/lib/agents/codex-telemetry.js +176 -164
- package/lib/agents/codex-telemetry.ts +205 -0
- package/lib/agents/codex.js +168 -201
- package/lib/agents/codex.ts +236 -0
- package/lib/agents/limit-hit.js +216 -226
- package/lib/agents/limit-hit.ts +262 -0
- package/lib/agents/mistral-telemetry.js +7 -10
- package/lib/agents/mistral-telemetry.ts +44 -0
- package/lib/agents/mistral.js +40 -58
- package/lib/agents/mistral.ts +84 -0
- package/lib/agents/opencode-export.js +126 -127
- package/lib/agents/opencode-export.ts +160 -0
- package/lib/agents/opencode-telemetry.js +282 -261
- package/lib/agents/opencode-telemetry.ts +350 -0
- package/lib/agents/opencode.js +263 -305
- package/lib/agents/opencode.ts +370 -0
- package/lib/agents/stage-telemetry.js +23 -24
- package/lib/agents/stage-telemetry.ts +47 -0
- package/lib/commands/integrate.js +9 -3
- package/lib/core/fmt.js +140 -211
- package/lib/core/fmt.ts +189 -0
- package/lib/core/git.js +121 -102
- package/lib/core/git.ts +146 -0
- package/lib/core/gitignore.js +59 -106
- package/lib/core/gitignore.ts +103 -0
- package/lib/core/mission-utils.js +833 -875
- package/lib/core/mission-utils.ts +998 -0
- package/lib/core/persistent-data-migration.js +247 -205
- package/lib/core/persistent-data-migration.ts +255 -0
- package/lib/core/product-config.js +400 -476
- package/lib/core/product-config.ts +518 -0
- package/lib/core/runtime-matrix.js +41 -87
- package/lib/core/runtime-matrix.ts +84 -0
- package/lib/core/spawn-tee.js +152 -192
- package/lib/core/spawn-tee.ts +202 -0
- package/lib/core/state-map.js +92 -92
- package/lib/core/state-map.ts +115 -0
- package/lib/core/storage.js +101 -129
- package/lib/core/storage.ts +182 -0
- package/lib/core/verification.js +148 -138
- package/lib/core/verification.ts +200 -0
- package/package.json +4 -2
- package/prompts/draft.md +1 -0
- package/prompts/portfolio.md +1 -1
- package/px.js +0 -2
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.codexSessionsDir = codexSessionsDir;
|
|
7
|
+
exports.parseCodexRollout = parseCodexRollout;
|
|
8
|
+
exports.collectRolloutFiles = collectRolloutFiles;
|
|
9
|
+
exports.extractCodexTelemetry = extractCodexTelemetry;
|
|
3
10
|
// Codex telemetry is read from the per-session rollout JSONL that the Codex CLI
|
|
4
11
|
// always writes under `$CODEX_HOME/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl`.
|
|
5
12
|
// The headless launcher (codex.js) points `HOME` at `<worktree>/.workflow/codex-home`,
|
|
@@ -12,96 +19,104 @@
|
|
|
12
19
|
// {"type":"turn_context","payload":{"model","effort",...}}
|
|
13
20
|
// {"type":"response_item","payload":{"type":"function_call",...}}
|
|
14
21
|
// {"type":"event_msg","payload":{"type":"token_count","info":{...},"rate_limits":{...}}}
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const path = require('path');
|
|
18
|
-
|
|
19
|
-
/** @param {string} codexHome */
|
|
22
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
23
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
20
24
|
function codexSessionsDir(codexHome) {
|
|
21
|
-
|
|
25
|
+
return node_path_1.default.join(codexHome, '.codex', 'sessions');
|
|
22
26
|
}
|
|
23
|
-
|
|
24
27
|
/**
|
|
25
28
|
* Parse a rollout JSONL string into a telemetry object. Returns null when the
|
|
26
29
|
* content yields no usable signal (e.g. a failed turn with no token_count).
|
|
27
30
|
*/
|
|
28
|
-
/** @param {string} content */
|
|
29
31
|
function parseCodexRollout(content) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
let sessionId = null;
|
|
33
|
-
let provider = null;
|
|
34
|
-
let model = null;
|
|
35
|
-
let effort = null;
|
|
36
|
-
let toolCalls = 0;
|
|
37
|
-
let lastUsage = null;
|
|
38
|
-
let contextWindow = 0;
|
|
39
|
-
let usagePercent = null;
|
|
40
|
-
|
|
41
|
-
for (const line of String(content).split('\n')) {
|
|
42
|
-
const trimmed = line.trim();
|
|
43
|
-
if (!trimmed.startsWith('{')) {continue;}
|
|
44
|
-
let evt;
|
|
45
|
-
try {
|
|
46
|
-
evt = JSON.parse(trimmed);
|
|
47
|
-
} catch (_) {
|
|
48
|
-
continue;
|
|
32
|
+
if (!content) {
|
|
33
|
+
return null;
|
|
49
34
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const pct = payload.rate_limits
|
|
76
|
-
&& payload.rate_limits.primary
|
|
77
|
-
&& payload.rate_limits.primary.used_percent;
|
|
78
|
-
if (typeof pct === 'number') {usagePercent = pct;}
|
|
35
|
+
let sessionId = null;
|
|
36
|
+
let provider = null;
|
|
37
|
+
let model = null;
|
|
38
|
+
let effort = null;
|
|
39
|
+
let toolCalls = 0;
|
|
40
|
+
let lastUsage = null;
|
|
41
|
+
let contextWindow = 0;
|
|
42
|
+
let usagePercent = null;
|
|
43
|
+
for (const line of String(content).split('\n')) {
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
if (!trimmed.startsWith('{')) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
let evt;
|
|
49
|
+
try {
|
|
50
|
+
evt = JSON.parse(trimmed);
|
|
51
|
+
}
|
|
52
|
+
catch (_) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const payload = evt && evt.payload;
|
|
56
|
+
if (!payload) {
|
|
57
|
+
continue;
|
|
79
58
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
59
|
+
switch (evt.type) {
|
|
60
|
+
case 'session_meta':
|
|
61
|
+
sessionId = payload.id || sessionId;
|
|
62
|
+
provider = payload.model_provider || provider;
|
|
63
|
+
if (payload.model) {
|
|
64
|
+
model = payload.model;
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
case 'turn_context':
|
|
68
|
+
// The latest turn_context reflects the model/effort actually used.
|
|
69
|
+
if (payload.model) {
|
|
70
|
+
model = payload.model;
|
|
71
|
+
}
|
|
72
|
+
if (payload.effort) {
|
|
73
|
+
effort = payload.effort;
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
case 'response_item':
|
|
77
|
+
if (payload.type === 'function_call') {
|
|
78
|
+
toolCalls += 1;
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
case 'event_msg':
|
|
82
|
+
if (payload.type === 'token_count') {
|
|
83
|
+
if (payload.info && payload.info.total_token_usage) {
|
|
84
|
+
lastUsage = payload.info.total_token_usage;
|
|
85
|
+
if (payload.info.model_context_window) {
|
|
86
|
+
contextWindow = payload.info.model_context_window;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const pct = payload.rate_limits
|
|
90
|
+
&& payload.rate_limits.primary
|
|
91
|
+
&& payload.rate_limits.primary.used_percent;
|
|
92
|
+
if (typeof pct === 'number') {
|
|
93
|
+
usagePercent = pct;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
default:
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!lastUsage && !model && !sessionId) {
|
|
102
|
+
return null;
|
|
83
103
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
contextWindow: contextWindow || 0,
|
|
100
|
-
toolCalls,
|
|
101
|
-
usagePercent,
|
|
102
|
-
};
|
|
104
|
+
const usage = lastUsage || {};
|
|
105
|
+
return {
|
|
106
|
+
sessionId,
|
|
107
|
+
provider: provider || null,
|
|
108
|
+
model: model || null,
|
|
109
|
+
effort: effort || null,
|
|
110
|
+
inputTokens: usage.input_tokens || 0,
|
|
111
|
+
outputTokens: usage.output_tokens || 0,
|
|
112
|
+
cachedTokens: usage.cached_input_tokens || 0,
|
|
113
|
+
reasoningTokens: usage.reasoning_output_tokens || 0,
|
|
114
|
+
totalTokens: usage.total_tokens || 0,
|
|
115
|
+
contextWindow: contextWindow || 0,
|
|
116
|
+
toolCalls,
|
|
117
|
+
usagePercent,
|
|
118
|
+
};
|
|
103
119
|
}
|
|
104
|
-
|
|
105
120
|
/**
|
|
106
121
|
* Collect rollout files under a Codex sessions tree modified at or after
|
|
107
122
|
* `sinceMs`, sorted oldest-first by mtime. Each `codex exec` invocation —
|
|
@@ -109,46 +124,49 @@ function parseCodexRollout(content) {
|
|
|
109
124
|
* `total_token_usage` counter, so a multi-round stage produces several files
|
|
110
125
|
* whose totals must be summed to get the stage's real quota consumption.
|
|
111
126
|
*/
|
|
112
|
-
/**
|
|
113
|
-
* @param {string} sessionsDir
|
|
114
|
-
* @param {{ sinceMs?: number }} [opts]
|
|
115
|
-
*/
|
|
116
127
|
function collectRolloutFiles(sessionsDir, { sinceMs = 0 } = {}) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const found = [];
|
|
120
|
-
const stack = [sessionsDir];
|
|
121
|
-
while (stack.length > 0) {
|
|
122
|
-
const dir = stack.pop();
|
|
123
|
-
let entries;
|
|
124
|
-
try {
|
|
125
|
-
entries = /** @type {Array<import('fs').Dirent>} */(/** @type {any} */(fs).readdirSync(dir, { withFileTypes: true }));
|
|
126
|
-
} catch (_) {
|
|
127
|
-
continue;
|
|
128
|
+
if (!sessionsDir || !node_fs_1.default.existsSync(sessionsDir)) {
|
|
129
|
+
return [];
|
|
128
130
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
stack.push(full);
|
|
135
|
-
} else if (entry.isFile() && entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) {
|
|
136
|
-
let stat;
|
|
131
|
+
const found = [];
|
|
132
|
+
const stack = [sessionsDir];
|
|
133
|
+
while (stack.length > 0) {
|
|
134
|
+
const dir = stack.pop();
|
|
135
|
+
let entries;
|
|
137
136
|
try {
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
137
|
+
entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
138
|
+
}
|
|
139
|
+
catch (_) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
if (!entry.name) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const name = entry.name ?? '';
|
|
147
|
+
const full = node_path_1.default.join(dir || '', name);
|
|
148
|
+
if (entry.isDirectory()) {
|
|
149
|
+
stack.push(full);
|
|
150
|
+
}
|
|
151
|
+
else if (entry.isFile() && entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) {
|
|
152
|
+
let stat;
|
|
153
|
+
try {
|
|
154
|
+
stat = node_fs_1.default.statSync(full);
|
|
155
|
+
}
|
|
156
|
+
catch (_) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const mtime = stat.mtimeMs;
|
|
160
|
+
if (mtime + 1000 < sinceMs) {
|
|
161
|
+
continue;
|
|
162
|
+
} // 1s grace for clock skew
|
|
163
|
+
found.push({ full, mtime });
|
|
164
|
+
}
|
|
141
165
|
}
|
|
142
|
-
const mtime = stat.mtimeMs;
|
|
143
|
-
if (mtime + 1000 < sinceMs) {continue;} // 1s grace for clock skew
|
|
144
|
-
found.push({ full, mtime });
|
|
145
|
-
}
|
|
146
166
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return found.map(f => f.full);
|
|
167
|
+
found.sort((a, b) => a.mtime - b.mtime);
|
|
168
|
+
return found.map(f => f.full);
|
|
150
169
|
}
|
|
151
|
-
|
|
152
170
|
/**
|
|
153
171
|
* Sum codex telemetry across every rollout written at or after `sinceMs` in
|
|
154
172
|
* `codexHome`. Token counts and tool calls are summed across all rollouts in the
|
|
@@ -160,56 +178,50 @@ function collectRolloutFiles(sessionsDir, { sinceMs = 0 } = {}) {
|
|
|
160
178
|
* re-running a stage (e.g. after the workflow process is resumed mid-mission)
|
|
161
179
|
* yields the same total rather than double-counting.
|
|
162
180
|
*/
|
|
163
|
-
/**
|
|
164
|
-
* @param {string} codexHome
|
|
165
|
-
* @param {{ sinceMs?: number }} [opts]
|
|
166
|
-
*/
|
|
167
181
|
function extractCodexTelemetry(codexHome, { sinceMs = 0 } = {}) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
const files = collectRolloutFiles(codexSessionsDir(codexHome), { sinceMs });
|
|
183
|
+
if (files.length === 0) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const agg = {
|
|
187
|
+
inputTokens: 0, outputTokens: 0, cachedTokens: 0,
|
|
188
|
+
reasoningTokens: 0, totalTokens: 0, toolCalls: 0,
|
|
189
|
+
};
|
|
190
|
+
let newest = null;
|
|
191
|
+
let newestPath = null;
|
|
192
|
+
for (const file of files) {
|
|
193
|
+
let content;
|
|
194
|
+
try {
|
|
195
|
+
content = node_fs_1.default.readFileSync(file, 'utf8');
|
|
196
|
+
}
|
|
197
|
+
catch (_) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const t = parseCodexRollout(content);
|
|
201
|
+
if (!t) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
agg.inputTokens += t.inputTokens;
|
|
205
|
+
agg.outputTokens += t.outputTokens;
|
|
206
|
+
agg.cachedTokens += t.cachedTokens;
|
|
207
|
+
agg.reasoningTokens += t.reasoningTokens;
|
|
208
|
+
agg.totalTokens += t.totalTokens;
|
|
209
|
+
agg.toolCalls += t.toolCalls;
|
|
210
|
+
newest = t; // files are oldest-first, so the last assignment is the newest
|
|
211
|
+
newestPath = file;
|
|
212
|
+
}
|
|
213
|
+
if (!newest) {
|
|
214
|
+
return null;
|
|
183
215
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (!newest) {return null;}
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
-
sessionId: newest.sessionId,
|
|
199
|
-
provider: newest.provider,
|
|
200
|
-
model: newest.model,
|
|
201
|
-
effort: newest.effort,
|
|
202
|
-
contextWindow: newest.contextWindow,
|
|
203
|
-
usagePercent: newest.usagePercent,
|
|
204
|
-
rolloutPath: newestPath,
|
|
205
|
-
rolloutCount: files.length,
|
|
206
|
-
...agg,
|
|
207
|
-
};
|
|
216
|
+
return {
|
|
217
|
+
sessionId: newest.sessionId,
|
|
218
|
+
provider: newest.provider,
|
|
219
|
+
model: newest.model,
|
|
220
|
+
effort: newest.effort,
|
|
221
|
+
contextWindow: newest.contextWindow,
|
|
222
|
+
usagePercent: newest.usagePercent,
|
|
223
|
+
rolloutPath: newestPath,
|
|
224
|
+
rolloutCount: files.length,
|
|
225
|
+
...agg,
|
|
226
|
+
};
|
|
208
227
|
}
|
|
209
|
-
|
|
210
|
-
module.exports = {
|
|
211
|
-
codexSessionsDir,
|
|
212
|
-
parseCodexRollout,
|
|
213
|
-
collectRolloutFiles,
|
|
214
|
-
extractCodexTelemetry,
|
|
215
|
-
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Codex telemetry is read from the per-session rollout JSONL that the Codex CLI
|
|
4
|
+
// always writes under `$CODEX_HOME/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl`.
|
|
5
|
+
// The headless launcher (codex.js) points `HOME` at `<worktree>/.workflow/codex-home`,
|
|
6
|
+
// so each mission worktree owns an isolated `…/.codex/sessions` tree. Reading the
|
|
7
|
+
// rollout — rather than parsing the `--json` stdout stream — keeps the human-readable
|
|
8
|
+
// transcript and session-resume behaviour intact while still yielding real usage data.
|
|
9
|
+
//
|
|
10
|
+
// Relevant rollout events (each line is one JSON object):
|
|
11
|
+
// {"type":"session_meta","payload":{"id","model_provider","model"?,...}}
|
|
12
|
+
// {"type":"turn_context","payload":{"model","effort",...}}
|
|
13
|
+
// {"type":"response_item","payload":{"type":"function_call",...}}
|
|
14
|
+
// {"type":"event_msg","payload":{"type":"token_count","info":{...},"rate_limits":{...}}}
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
|
|
19
|
+
function codexSessionsDir(codexHome: string) {
|
|
20
|
+
return path.join(codexHome, '.codex', 'sessions');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse a rollout JSONL string into a telemetry object. Returns null when the
|
|
25
|
+
* content yields no usable signal (e.g. a failed turn with no token_count).
|
|
26
|
+
*/
|
|
27
|
+
function parseCodexRollout(content: string) {
|
|
28
|
+
if (!content) {return null;}
|
|
29
|
+
|
|
30
|
+
let sessionId = null;
|
|
31
|
+
let provider = null;
|
|
32
|
+
let model = null;
|
|
33
|
+
let effort = null;
|
|
34
|
+
let toolCalls = 0;
|
|
35
|
+
let lastUsage = null;
|
|
36
|
+
let contextWindow = 0;
|
|
37
|
+
let usagePercent = null;
|
|
38
|
+
|
|
39
|
+
for (const line of String(content).split('\n')) {
|
|
40
|
+
const trimmed = line.trim();
|
|
41
|
+
if (!trimmed.startsWith('{')) {continue;}
|
|
42
|
+
let evt;
|
|
43
|
+
try {
|
|
44
|
+
evt = JSON.parse(trimmed);
|
|
45
|
+
} catch (_) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const payload = evt && evt.payload;
|
|
49
|
+
if (!payload) {continue;}
|
|
50
|
+
|
|
51
|
+
switch (evt.type) {
|
|
52
|
+
case 'session_meta':
|
|
53
|
+
sessionId = payload.id || sessionId;
|
|
54
|
+
provider = payload.model_provider || provider;
|
|
55
|
+
if (payload.model) {model = payload.model;}
|
|
56
|
+
break;
|
|
57
|
+
case 'turn_context':
|
|
58
|
+
// The latest turn_context reflects the model/effort actually used.
|
|
59
|
+
if (payload.model) {model = payload.model;}
|
|
60
|
+
if (payload.effort) {effort = payload.effort;}
|
|
61
|
+
break;
|
|
62
|
+
case 'response_item':
|
|
63
|
+
if (payload.type === 'function_call') {toolCalls += 1;}
|
|
64
|
+
break;
|
|
65
|
+
case 'event_msg':
|
|
66
|
+
if (payload.type === 'token_count') {
|
|
67
|
+
if (payload.info && payload.info.total_token_usage) {
|
|
68
|
+
lastUsage = payload.info.total_token_usage;
|
|
69
|
+
if (payload.info.model_context_window) {
|
|
70
|
+
contextWindow = payload.info.model_context_window;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const pct = payload.rate_limits
|
|
74
|
+
&& payload.rate_limits.primary
|
|
75
|
+
&& payload.rate_limits.primary.used_percent;
|
|
76
|
+
if (typeof pct === 'number') {usagePercent = pct;}
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
default:
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!lastUsage && !model && !sessionId) {return null;}
|
|
85
|
+
|
|
86
|
+
const usage = lastUsage || {};
|
|
87
|
+
return {
|
|
88
|
+
sessionId,
|
|
89
|
+
provider: provider || null,
|
|
90
|
+
model: model || null,
|
|
91
|
+
effort: effort || null,
|
|
92
|
+
inputTokens: usage.input_tokens || 0,
|
|
93
|
+
outputTokens: usage.output_tokens || 0,
|
|
94
|
+
cachedTokens: usage.cached_input_tokens || 0,
|
|
95
|
+
reasoningTokens: usage.reasoning_output_tokens || 0,
|
|
96
|
+
totalTokens: usage.total_tokens || 0,
|
|
97
|
+
contextWindow: contextWindow || 0,
|
|
98
|
+
toolCalls,
|
|
99
|
+
usagePercent,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Collect rollout files under a Codex sessions tree modified at or after
|
|
105
|
+
* `sinceMs`, sorted oldest-first by mtime. Each `codex exec` invocation —
|
|
106
|
+
* including `exec resume` — writes a NEW rollout file with a fresh
|
|
107
|
+
* `total_token_usage` counter, so a multi-round stage produces several files
|
|
108
|
+
* whose totals must be summed to get the stage's real quota consumption.
|
|
109
|
+
*/
|
|
110
|
+
function collectRolloutFiles(sessionsDir: string, { sinceMs = 0 }: { sinceMs?: number } = {}) {
|
|
111
|
+
if (!sessionsDir || !fs.existsSync(sessionsDir)) {return [];}
|
|
112
|
+
|
|
113
|
+
const found = [];
|
|
114
|
+
const stack = [sessionsDir];
|
|
115
|
+
while (stack.length > 0) {
|
|
116
|
+
const dir = stack.pop();
|
|
117
|
+
let entries;
|
|
118
|
+
try {
|
|
119
|
+
entries = (fs as any).readdirSync(dir, { withFileTypes: true });
|
|
120
|
+
} catch (_) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
for (const entry of entries as Array<{name: string, isDirectory: () => boolean, isFile: () => boolean}>) {
|
|
124
|
+
if (!entry.name) {continue;}
|
|
125
|
+
const name: string = entry.name ?? '';
|
|
126
|
+
const full = path.join(dir || '', name);
|
|
127
|
+
if (entry.isDirectory()) {
|
|
128
|
+
stack.push(full);
|
|
129
|
+
} else if (entry.isFile() && entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) {
|
|
130
|
+
let stat;
|
|
131
|
+
try {
|
|
132
|
+
stat = fs.statSync(full);
|
|
133
|
+
} catch (_) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const mtime = stat.mtimeMs;
|
|
137
|
+
if (mtime + 1000 < sinceMs) {continue;} // 1s grace for clock skew
|
|
138
|
+
found.push({ full, mtime });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
found.sort((a, b) => a.mtime - b.mtime);
|
|
143
|
+
return found.map(f => f.full);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Sum codex telemetry across every rollout written at or after `sinceMs` in
|
|
148
|
+
* `codexHome`. Token counts and tool calls are summed across all rollouts in the
|
|
149
|
+
* window (so all rounds of a resumed session are counted); model/effort/provider
|
|
150
|
+
* and the rate-limit usage snapshot are taken from the newest rollout. Returns
|
|
151
|
+
* null when no rollout/usable data is found.
|
|
152
|
+
*
|
|
153
|
+
* Idempotent by construction: it recomputes from the rollout files each call, so
|
|
154
|
+
* re-running a stage (e.g. after the workflow process is resumed mid-mission)
|
|
155
|
+
* yields the same total rather than double-counting.
|
|
156
|
+
*/
|
|
157
|
+
function extractCodexTelemetry(codexHome: string, { sinceMs = 0 }: { sinceMs?: number } = {}) {
|
|
158
|
+
const files = collectRolloutFiles(codexSessionsDir(codexHome), { sinceMs });
|
|
159
|
+
if (files.length === 0) {return null;}
|
|
160
|
+
|
|
161
|
+
const agg = {
|
|
162
|
+
inputTokens: 0, outputTokens: 0, cachedTokens: 0,
|
|
163
|
+
reasoningTokens: 0, totalTokens: 0, toolCalls: 0,
|
|
164
|
+
};
|
|
165
|
+
let newest = null;
|
|
166
|
+
let newestPath = null;
|
|
167
|
+
for (const file of files) {
|
|
168
|
+
let content;
|
|
169
|
+
try {
|
|
170
|
+
content = fs.readFileSync(file, 'utf8');
|
|
171
|
+
} catch (_) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const t = parseCodexRollout(content);
|
|
175
|
+
if (!t) {continue;}
|
|
176
|
+
agg.inputTokens += t.inputTokens;
|
|
177
|
+
agg.outputTokens += t.outputTokens;
|
|
178
|
+
agg.cachedTokens += t.cachedTokens;
|
|
179
|
+
agg.reasoningTokens += t.reasoningTokens;
|
|
180
|
+
agg.totalTokens += t.totalTokens;
|
|
181
|
+
agg.toolCalls += t.toolCalls;
|
|
182
|
+
newest = t; // files are oldest-first, so the last assignment is the newest
|
|
183
|
+
newestPath = file;
|
|
184
|
+
}
|
|
185
|
+
if (!newest) {return null;}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
sessionId: newest.sessionId,
|
|
189
|
+
provider: newest.provider,
|
|
190
|
+
model: newest.model,
|
|
191
|
+
effort: newest.effort,
|
|
192
|
+
contextWindow: newest.contextWindow,
|
|
193
|
+
usagePercent: newest.usagePercent,
|
|
194
|
+
rolloutPath: newestPath,
|
|
195
|
+
rolloutCount: files.length,
|
|
196
|
+
...agg,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
codexSessionsDir,
|
|
202
|
+
parseCodexRollout,
|
|
203
|
+
collectRolloutFiles,
|
|
204
|
+
extractCodexTelemetry,
|
|
205
|
+
};
|