@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.
Files changed (51) hide show
  1. package/lib/agents/agents.js +737 -838
  2. package/lib/agents/agents.ts +930 -0
  3. package/lib/agents/claude-telemetry.js +149 -154
  4. package/lib/agents/claude-telemetry.ts +233 -0
  5. package/lib/agents/claude.js +121 -131
  6. package/lib/agents/claude.ts +156 -0
  7. package/lib/agents/codex-telemetry.js +176 -164
  8. package/lib/agents/codex-telemetry.ts +205 -0
  9. package/lib/agents/codex.js +168 -201
  10. package/lib/agents/codex.ts +236 -0
  11. package/lib/agents/limit-hit.js +216 -226
  12. package/lib/agents/limit-hit.ts +262 -0
  13. package/lib/agents/mistral-telemetry.js +7 -10
  14. package/lib/agents/mistral-telemetry.ts +44 -0
  15. package/lib/agents/mistral.js +40 -58
  16. package/lib/agents/mistral.ts +84 -0
  17. package/lib/agents/opencode-export.js +126 -127
  18. package/lib/agents/opencode-export.ts +160 -0
  19. package/lib/agents/opencode-telemetry.js +282 -261
  20. package/lib/agents/opencode-telemetry.ts +350 -0
  21. package/lib/agents/opencode.js +263 -305
  22. package/lib/agents/opencode.ts +370 -0
  23. package/lib/agents/stage-telemetry.js +23 -24
  24. package/lib/agents/stage-telemetry.ts +47 -0
  25. package/lib/commands/integrate.js +9 -3
  26. package/lib/core/fmt.js +140 -211
  27. package/lib/core/fmt.ts +189 -0
  28. package/lib/core/git.js +121 -102
  29. package/lib/core/git.ts +146 -0
  30. package/lib/core/gitignore.js +59 -106
  31. package/lib/core/gitignore.ts +103 -0
  32. package/lib/core/mission-utils.js +833 -875
  33. package/lib/core/mission-utils.ts +998 -0
  34. package/lib/core/persistent-data-migration.js +247 -205
  35. package/lib/core/persistent-data-migration.ts +255 -0
  36. package/lib/core/product-config.js +400 -476
  37. package/lib/core/product-config.ts +518 -0
  38. package/lib/core/runtime-matrix.js +41 -87
  39. package/lib/core/runtime-matrix.ts +84 -0
  40. package/lib/core/spawn-tee.js +152 -192
  41. package/lib/core/spawn-tee.ts +202 -0
  42. package/lib/core/state-map.js +92 -92
  43. package/lib/core/state-map.ts +115 -0
  44. package/lib/core/storage.js +101 -129
  45. package/lib/core/storage.ts +182 -0
  46. package/lib/core/verification.js +148 -138
  47. package/lib/core/verification.ts +200 -0
  48. package/package.json +4 -2
  49. package/prompts/draft.md +1 -0
  50. package/prompts/portfolio.md +1 -1
  51. 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 fs = require('fs');
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
- return path.join(codexHome, '.codex', 'sessions');
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
- if (!content) {return null;}
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
- const payload = evt && evt.payload;
51
- if (!payload) {continue;}
52
-
53
- switch (evt.type) {
54
- case 'session_meta':
55
- sessionId = payload.id || sessionId;
56
- provider = payload.model_provider || provider;
57
- if (payload.model) {model = payload.model;}
58
- break;
59
- case 'turn_context':
60
- // The latest turn_context reflects the model/effort actually used.
61
- if (payload.model) {model = payload.model;}
62
- if (payload.effort) {effort = payload.effort;}
63
- break;
64
- case 'response_item':
65
- if (payload.type === 'function_call') {toolCalls += 1;}
66
- break;
67
- case 'event_msg':
68
- if (payload.type === 'token_count') {
69
- if (payload.info && payload.info.total_token_usage) {
70
- lastUsage = payload.info.total_token_usage;
71
- if (payload.info.model_context_window) {
72
- contextWindow = payload.info.model_context_window;
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
- break;
81
- default:
82
- break;
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
- if (!lastUsage && !model && !sessionId) {return null;}
87
-
88
- const usage = lastUsage || {};
89
- return {
90
- sessionId,
91
- provider: provider || null,
92
- model: model || null,
93
- effort: effort || null,
94
- inputTokens: usage.input_tokens || 0,
95
- outputTokens: usage.output_tokens || 0,
96
- cachedTokens: usage.cached_input_tokens || 0,
97
- reasoningTokens: usage.reasoning_output_tokens || 0,
98
- totalTokens: usage.total_tokens || 0,
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
- if (!sessionsDir || !fs.existsSync(sessionsDir)) {return [];}
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
- for (const entry of /** @type {Array<{name: string, isDirectory: () => boolean, isFile: () => boolean}>} */(entries)) {
130
- if (!entry.name) {continue;}
131
- const /** @type {string} */ name = entry.name ?? '';
132
- const full = path.join(dir || '', name);
133
- if (entry.isDirectory()) {
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
- stat = fs.statSync(full);
139
- } catch (_) {
140
- continue;
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
- found.sort((a, b) => a.mtime - b.mtime);
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
- const files = collectRolloutFiles(codexSessionsDir(codexHome), { sinceMs });
169
- if (files.length === 0) {return null;}
170
-
171
- const agg = {
172
- inputTokens: 0, outputTokens: 0, cachedTokens: 0,
173
- reasoningTokens: 0, totalTokens: 0, toolCalls: 0,
174
- };
175
- let newest = null;
176
- let newestPath = null;
177
- for (const file of files) {
178
- let content;
179
- try {
180
- content = fs.readFileSync(file, 'utf8');
181
- } catch (_) {
182
- continue;
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
- const t = parseCodexRollout(content);
185
- if (!t) {continue;}
186
- agg.inputTokens += t.inputTokens;
187
- agg.outputTokens += t.outputTokens;
188
- agg.cachedTokens += t.cachedTokens;
189
- agg.reasoningTokens += t.reasoningTokens;
190
- agg.totalTokens += t.totalTokens;
191
- agg.toolCalls += t.toolCalls;
192
- newest = t; // files are oldest-first, so the last assignment is the newest
193
- newestPath = file;
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
+ };