@magnusekdahl/parallix 1.1.0 → 1.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/config/integration-pipelines.json +9 -0
- package/config/workflow.config.schema.json +12 -0
- package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
- package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
- package/docs/adr/0041-integration-pipeline-gates.md +1 -1
- package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
- package/docs/adr/index.md +1 -0
- package/docs/use-cases.md +44 -0
- package/lib/agents/agents.js +149 -100
- package/lib/agents/claude-telemetry.js +14 -10
- package/lib/agents/claude.js +32 -11
- package/lib/agents/codex-telemetry.js +32 -19
- package/lib/agents/codex.js +36 -11
- package/lib/agents/limit-hit.js +42 -25
- package/lib/agents/mistral-telemetry.js +1 -1
- package/lib/agents/mistral.js +13 -3
- package/lib/agents/opencode-export.js +11 -5
- package/lib/agents/opencode-telemetry.js +75 -90
- package/lib/agents/opencode.js +86 -29
- package/lib/agents/stage-telemetry.js +3 -7
- package/lib/commands/active.js +139 -94
- package/lib/commands/checkpoint.js +3 -1
- package/lib/commands/config.js +5 -3
- package/lib/commands/coverage-gate.js +17 -10
- package/lib/commands/diff.js +11 -5
- package/lib/commands/draft.js +95 -55
- package/lib/commands/handoff.js +253 -65
- package/lib/commands/integrate.js +288 -188
- package/lib/commands/mission-start.js +39 -34
- package/lib/commands/rebase.js +32 -21
- package/lib/commands/repair-handoff.js +21 -18
- package/lib/commands/resolve-conflict.js +5 -2
- package/lib/commands/review.js +1 -0
- package/lib/commands/setup-review.js +1 -0
- package/lib/commands/stats-backfill.js +50 -41
- package/lib/commands/stats.js +653 -204
- package/lib/commands/status.js +33 -28
- package/lib/core/fmt.js +58 -15
- package/lib/core/git.js +7 -5
- package/lib/core/gitignore.js +6 -4
- package/lib/core/mission-utils.js +161 -88
- package/lib/core/nels.js +199 -0
- package/lib/core/persistent-data-migration.js +55 -23
- package/lib/core/product-config.js +44 -23
- package/lib/core/runtime-matrix.js +15 -2
- package/lib/core/spawn-tee.js +40 -13
- package/lib/core/state-map.js +32 -13
- package/lib/core/storage.js +19 -5
- package/lib/core/subagent-limit.js +28 -0
- package/lib/core/verification.js +29 -12
- package/lib/review/rebase.js +11 -3
- package/lib/review/review-adapter.js +93 -6
- package/lib/review/review-artifacts.js +89 -23
- package/lib/review/review-commands.js +139 -45
- package/lib/review/review-events.js +103 -52
- package/lib/review/review-loop.js +117 -87
- package/lib/review/review-polling.js +25 -5
- package/lib/review/review-prompts.js +36 -5
- package/lib/review/review-state.js +39 -15
- package/lib/tools/backlog.js +162 -71
- package/lib/tools/forgejo.js +290 -129
- package/lib/tools/gatekeeper.js +15 -0
- package/lib/tools/redgreen.js +220 -0
- package/lib/tools/sessions.js +12 -5
- package/lib/tools/setup-review.js +170 -70
- package/package.json +11 -3
- package/prompts/draft.md +6 -0
- package/templates/mission-scaffold.md +1 -1
package/lib/agents/limit-hit.js
CHANGED
|
@@ -39,10 +39,12 @@ const PATTERN_SETS = Object.freeze({
|
|
|
39
39
|
]
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
/** @param {string} agent */
|
|
42
43
|
function getPatternsForAgent(agent) {
|
|
43
|
-
return PATTERN_SETS[agent] || [];
|
|
44
|
+
return PATTERN_SETS[/** @type {keyof typeof PATTERN_SETS} */(agent)] || [];
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/** @param {string} agent @param {string} text */
|
|
46
48
|
function findLimitHitMatch(agent, text) {
|
|
47
49
|
const patterns = getPatternsForAgent(agent);
|
|
48
50
|
for (const pattern of patterns) {
|
|
@@ -54,6 +56,7 @@ function findLimitHitMatch(agent, text) {
|
|
|
54
56
|
return null;
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
/** @param {string} text @param {number} index @param {number} length */
|
|
57
60
|
function clipContext(text, index, length) {
|
|
58
61
|
const before = Math.max(0, index - 200);
|
|
59
62
|
const after = Math.min(text.length, index + length + 200);
|
|
@@ -67,18 +70,20 @@ const RELATIVE_PATTERN = /\bin\s+(\d+)\s+(hour|hours|minute|minutes|second|secon
|
|
|
67
70
|
const RETRY_AFTER_PATTERN = /retry[- ]?after[:\s]+(\d+)\s*(seconds?|s|minutes?|m)?/i;
|
|
68
71
|
const RETRY_AT_DATE_PATTERN = /(?:retry|reset|try again|reset(?:s)? at)[^\n]{0,40}?(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)/i;
|
|
69
72
|
|
|
73
|
+
/** @param {string | undefined} value */
|
|
70
74
|
function parseIsoOffset(value) {
|
|
71
|
-
if (!value) return null;
|
|
72
|
-
if (value === 'Z') return 0;
|
|
75
|
+
if (!value) {return null;}
|
|
76
|
+
if (value === 'Z') {return 0;}
|
|
73
77
|
const m = /([+-])(\d{2}):?(\d{2})/.exec(value);
|
|
74
|
-
if (!m) return null;
|
|
78
|
+
if (!m) {return null;}
|
|
75
79
|
const sign = m[1] === '-' ? -1 : 1;
|
|
76
80
|
return sign * (Number(m[2]) * 60 + Number(m[3]));
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
/** @param {string} text */
|
|
79
84
|
function parseIso(text) {
|
|
80
85
|
const m = ISO_PATTERN.exec(text);
|
|
81
|
-
if (!m) return null;
|
|
86
|
+
if (!m) {return null;}
|
|
82
87
|
const [, year, month, day, hour, minute, second, offset] = m;
|
|
83
88
|
const offsetMinutes = parseIsoOffset(offset);
|
|
84
89
|
if (offsetMinutes === null) {
|
|
@@ -88,55 +93,61 @@ function parseIso(text) {
|
|
|
88
93
|
return new Date(utcMs - offsetMinutes * 60 * 1000);
|
|
89
94
|
}
|
|
90
95
|
|
|
96
|
+
/** @param {string} text @param {Date} now */
|
|
91
97
|
function parseTwelveHour(text, now) {
|
|
92
98
|
const m = TWELVE_HOUR_PATTERN.exec(text);
|
|
93
|
-
if (!m) return null;
|
|
99
|
+
if (!m) {return null;}
|
|
94
100
|
let hour = Number(m[1]);
|
|
95
101
|
const minute = Number(m[2] || 0);
|
|
96
102
|
const meridiem = m[3].toLowerCase();
|
|
97
|
-
if (hour < 1 || hour > 12) return null;
|
|
98
|
-
if (meridiem === 'pm' && hour !== 12) hour += 12;
|
|
99
|
-
if (meridiem === 'am' && hour === 12) hour = 0;
|
|
103
|
+
if (hour < 1 || hour > 12) {return null;}
|
|
104
|
+
if (meridiem === 'pm' && hour !== 12) {hour += 12;}
|
|
105
|
+
if (meridiem === 'am' && hour === 12) {hour = 0;}
|
|
100
106
|
return projectClockTime(now, hour, minute);
|
|
101
107
|
}
|
|
102
108
|
|
|
109
|
+
/** @param {string} text @param {Date} now */
|
|
103
110
|
function parseTwentyFourHour(text, now) {
|
|
104
111
|
const m = TWENTY_FOUR_HOUR_PATTERN.exec(text);
|
|
105
|
-
if (!m) return null;
|
|
112
|
+
if (!m) {return null;}
|
|
106
113
|
const hour = Number(m[1]);
|
|
107
114
|
const minute = Number(m[2]);
|
|
108
115
|
return projectClockTime(now, hour, minute);
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
/** @param {string} text @param {Date} now */
|
|
111
119
|
function parseRelative(text, now) {
|
|
112
120
|
const m = RELATIVE_PATTERN.exec(text);
|
|
113
|
-
if (!m) return null;
|
|
121
|
+
if (!m) {return null;}
|
|
114
122
|
const amount = Number(m[1]);
|
|
115
123
|
const unit = m[2].toLowerCase();
|
|
116
124
|
let ms;
|
|
117
|
-
if (unit.startsWith('hour')) ms = amount * 60 * 60 * 1000;
|
|
118
|
-
else if (unit.startsWith('minute')) ms = amount * 60 * 1000;
|
|
119
|
-
else ms = amount * 1000;
|
|
125
|
+
if (unit.startsWith('hour')) {ms = amount * 60 * 60 * 1000;}
|
|
126
|
+
else if (unit.startsWith('minute')) {ms = amount * 60 * 1000;}
|
|
127
|
+
else {ms = amount * 1000;}
|
|
120
128
|
return new Date(now.getTime() + ms);
|
|
121
129
|
}
|
|
122
130
|
|
|
131
|
+
/** @param {string} text @param {Date} now */
|
|
123
132
|
function parseRetryAfter(text, now) {
|
|
124
133
|
const m = RETRY_AFTER_PATTERN.exec(text);
|
|
125
|
-
if (!m) return null;
|
|
134
|
+
if (!m) {return null;}
|
|
126
135
|
const amount = Number(m[1]);
|
|
127
136
|
const unit = (m[2] || 's').toLowerCase();
|
|
128
137
|
let ms;
|
|
129
|
-
if (unit.startsWith('m') && unit !== 's') ms = amount * 60 * 1000;
|
|
130
|
-
else ms = amount * 1000;
|
|
138
|
+
if (unit.startsWith('m') && unit !== 's') {ms = amount * 60 * 1000;}
|
|
139
|
+
else {ms = amount * 1000;}
|
|
131
140
|
return new Date(now.getTime() + ms);
|
|
132
141
|
}
|
|
133
142
|
|
|
143
|
+
/** @param {string} text */
|
|
134
144
|
function parseRetryAtDate(text) {
|
|
135
145
|
const m = RETRY_AT_DATE_PATTERN.exec(text);
|
|
136
|
-
if (!m) return null;
|
|
146
|
+
if (!m) {return null;}
|
|
137
147
|
return parseIso(m[1]);
|
|
138
148
|
}
|
|
139
149
|
|
|
150
|
+
/** @param {Date} now @param {number} hour @param {number} minute */
|
|
140
151
|
function projectClockTime(now, hour, minute) {
|
|
141
152
|
const candidate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute, 0, 0);
|
|
142
153
|
if (candidate.getTime() <= now.getTime()) {
|
|
@@ -145,8 +156,9 @@ function projectClockTime(now, hour, minute) {
|
|
|
145
156
|
return candidate;
|
|
146
157
|
}
|
|
147
158
|
|
|
159
|
+
/** @param {string} text @param {Date} [now] */
|
|
148
160
|
function parseResetTime(text, now = new Date()) {
|
|
149
|
-
if (!text) return null;
|
|
161
|
+
if (!text) {return null;}
|
|
150
162
|
return (
|
|
151
163
|
parseRetryAtDate(text) ||
|
|
152
164
|
parseIso(text) ||
|
|
@@ -157,6 +169,7 @@ function parseResetTime(text, now = new Date()) {
|
|
|
157
169
|
);
|
|
158
170
|
}
|
|
159
171
|
|
|
172
|
+
/** @param {Date} date */
|
|
160
173
|
function formatBlockUntil(date) {
|
|
161
174
|
const year = date.getFullYear();
|
|
162
175
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
@@ -165,6 +178,7 @@ function formatBlockUntil(date) {
|
|
|
165
178
|
return `${year}-${month}-${day} ${hour}`;
|
|
166
179
|
}
|
|
167
180
|
|
|
181
|
+
/** @param {Date} date */
|
|
168
182
|
function ceilToNextHour(date) {
|
|
169
183
|
const ceil = new Date(date.getTime());
|
|
170
184
|
if (ceil.getMinutes() === 0 && ceil.getSeconds() === 0 && ceil.getMilliseconds() === 0) {
|
|
@@ -175,6 +189,9 @@ function ceilToNextHour(date) {
|
|
|
175
189
|
return ceil;
|
|
176
190
|
}
|
|
177
191
|
|
|
192
|
+
/**
|
|
193
|
+
* @param {{ agent: string, stdout?: string, stderr?: string, status?: number, signal?: string | null, error?: { code?: string } | null, now?: Date }} [opts]
|
|
194
|
+
*/
|
|
178
195
|
function detectLimitHit({
|
|
179
196
|
agent,
|
|
180
197
|
stdout = '',
|
|
@@ -183,8 +200,8 @@ function detectLimitHit({
|
|
|
183
200
|
signal,
|
|
184
201
|
error,
|
|
185
202
|
now = new Date()
|
|
186
|
-
} = {}) {
|
|
187
|
-
if (!agent) return null;
|
|
203
|
+
} = /** @type {{ agent: string, stdout?: string, stderr?: string, status?: number, signal?: string | null, error?: { code?: string } | null, now?: Date }} */({})) {
|
|
204
|
+
if (!agent) {return null;}
|
|
188
205
|
// Gate detection on a failed launcher invocation. A successful child (exit 0,
|
|
189
206
|
// no signal, no spawn error) means any limit-hit phrases in the transcript
|
|
190
207
|
// are quoted text — typically when the agent itself reviewed code, tests, or
|
|
@@ -193,11 +210,11 @@ function detectLimitHit({
|
|
|
193
210
|
// `status === undefined` means the caller did not pass exit metadata; preserve
|
|
194
211
|
// the legacy behavior so callers that haven't been updated still get detection.
|
|
195
212
|
const launcherFailed =
|
|
196
|
-
error
|
|
197
|
-
signal
|
|
213
|
+
error !== null && error !== undefined ||
|
|
214
|
+
signal !== null && signal !== undefined ||
|
|
198
215
|
(typeof status === 'number' && status !== 0) ||
|
|
199
216
|
typeof status === 'undefined';
|
|
200
|
-
if (!launcherFailed) return null;
|
|
217
|
+
if (!launcherFailed) {return null;}
|
|
201
218
|
|
|
202
219
|
const combined = `${stdout || ''}\n${stderr || ''}`;
|
|
203
220
|
const match = findLimitHitMatch(agent, combined);
|
|
@@ -216,7 +233,7 @@ function detectLimitHit({
|
|
|
216
233
|
target = new Date(now.getTime() + DEFAULT_FALLBACK_HOURS * 60 * 60 * 1000);
|
|
217
234
|
source = 'fallback';
|
|
218
235
|
}
|
|
219
|
-
} else if (signal
|
|
236
|
+
} else if (signal !== null && signal !== undefined) {
|
|
220
237
|
// Agent was killed by a signal (e.g. SIGINT/Ctrl-C) but no limit-hit
|
|
221
238
|
// pattern matched. Apply a short-term block so the next invocation
|
|
222
239
|
// selects a different agent instead of re-picking the same one.
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* @param {object} result - The launcher result object
|
|
24
24
|
* @returns {null} Always null — mistral/vibe has no telemetry parser
|
|
25
25
|
*/
|
|
26
|
-
function extractMistralTelemetry(
|
|
26
|
+
function extractMistralTelemetry() {
|
|
27
27
|
// Mistral Vibe does not emit token usage data. Honest zero.
|
|
28
28
|
return null;
|
|
29
29
|
}
|
package/lib/agents/mistral.js
CHANGED
|
@@ -12,9 +12,11 @@ const { spawnAndTee } = require('../core/spawn-tee');
|
|
|
12
12
|
// Telemetry: mistral/vibe does not expose token-usage data. See mistral-telemetry.js
|
|
13
13
|
// for the honest-zero stub. Stats hooks in active.js and review-loop.js call
|
|
14
14
|
// recordStageStats which defaults to '0' for tokens when telemetry is null.
|
|
15
|
-
const MISTRAL_SESSION_ID_RE = null;
|
|
16
15
|
|
|
16
|
+
|
|
17
|
+
/** @param {string} stdout */
|
|
17
18
|
function extractMistralSessionId(stdout) {
|
|
19
|
+
void stdout;
|
|
18
20
|
// Vibe does not currently emit a resume hint in programmatic mode.
|
|
19
21
|
// Return null to indicate no resume capability via stdout parsing.
|
|
20
22
|
return null;
|
|
@@ -24,7 +26,12 @@ function resolveMistralCommand() {
|
|
|
24
26
|
return 'vibe';
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
/**
|
|
30
|
+
* @param {{ prompt: string, worktree: string, env?: object, resume?: boolean, sessionId?: string | null, model?: string | null }} opts
|
|
31
|
+
*/
|
|
32
|
+
function buildMistralInvocation({ prompt, worktree, env, resume, sessionId, model = null }) {
|
|
33
|
+
void resume;
|
|
34
|
+
void sessionId;
|
|
28
35
|
const args = ['--prompt', prompt, '--trust', '--output', 'text'];
|
|
29
36
|
|
|
30
37
|
// Vibe programmatic mode does not support --resume flag in the same way
|
|
@@ -48,9 +55,12 @@ function buildMistralInvocation({ prompt, worktree, env, resume = false, session
|
|
|
48
55
|
};
|
|
49
56
|
}
|
|
50
57
|
|
|
58
|
+
/**
|
|
59
|
+
* @param {{ prompt: string, worktree: string, env?: object, resume?: boolean, sessionId?: string | null, model?: string | null, teeOptions?: object }} opts
|
|
60
|
+
*/
|
|
51
61
|
function startMistralAgent({ prompt, worktree, env, resume = false, sessionId = null, model = null, teeOptions = {} }) {
|
|
52
62
|
const invocation = buildMistralInvocation({ prompt, worktree, env, resume, sessionId, model });
|
|
53
|
-
const resultPromise = spawnAndTee(invocation.command, invocation.args, { ...invocation.options, ...teeOptions }).then(result => {
|
|
63
|
+
const resultPromise = spawnAndTee(invocation.command, invocation.args, /** @type {any} */({ ...invocation.options, ...teeOptions })).then(result => {
|
|
54
64
|
if (result && result.stdout) {
|
|
55
65
|
result.sessionId = extractMistralSessionId(result.stdout);
|
|
56
66
|
}
|
|
@@ -50,15 +50,19 @@ function captureOpencodeExport(sessionId, opts = {}) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
let settled = false;
|
|
53
|
+
/** @type {NodeJS.Timeout | null} */
|
|
53
54
|
let timer = null;
|
|
54
55
|
let size = 0;
|
|
56
|
+
/** @type {number | null} */
|
|
55
57
|
let tmpFd = null;
|
|
58
|
+
/** @type {string | null} */
|
|
56
59
|
let tmpPath = null;
|
|
57
60
|
|
|
61
|
+
/** @param {string | null} value */
|
|
58
62
|
const finish = (value) => {
|
|
59
|
-
if (settled) return;
|
|
63
|
+
if (settled) {return;}
|
|
60
64
|
settled = true;
|
|
61
|
-
if (timer) clearTimeout(timer);
|
|
65
|
+
if (timer) {clearTimeout(timer);}
|
|
62
66
|
// Clean up temp file
|
|
63
67
|
if (tmpFd !== null) {
|
|
64
68
|
try { fs.closeSync(tmpFd); } catch (_) { /* already closed */ }
|
|
@@ -69,8 +73,9 @@ function captureOpencodeExport(sessionId, opts = {}) {
|
|
|
69
73
|
resolve(value);
|
|
70
74
|
};
|
|
71
75
|
|
|
76
|
+
/** @param {import('child_process').ChildProcess | undefined} child */
|
|
72
77
|
const killChild = (child) => {
|
|
73
|
-
try { child.kill('SIGKILL'); } catch (_) { /* already gone */ }
|
|
78
|
+
try { if (child) { child.kill('SIGKILL'); } } catch (_) { /* already gone */ }
|
|
74
79
|
};
|
|
75
80
|
|
|
76
81
|
// Create a temp file for stdout to avoid pipe-buffer truncation
|
|
@@ -117,8 +122,8 @@ function captureOpencodeExport(sessionId, opts = {}) {
|
|
|
117
122
|
|
|
118
123
|
child.on('error', () => finish(null));
|
|
119
124
|
|
|
120
|
-
child.on('close', (
|
|
121
|
-
if (settled) return;
|
|
125
|
+
child.on('close', () => {
|
|
126
|
+
if (settled) {return;}
|
|
122
127
|
if (tmpFd === null) {
|
|
123
128
|
finish(null);
|
|
124
129
|
return;
|
|
@@ -130,6 +135,7 @@ function captureOpencodeExport(sessionId, opts = {}) {
|
|
|
130
135
|
tmpFd = null;
|
|
131
136
|
|
|
132
137
|
let content;
|
|
138
|
+
if (!tmpPath) { finish(null); return; }
|
|
133
139
|
try {
|
|
134
140
|
content = fs.readFileSync(tmpPath, 'utf8');
|
|
135
141
|
} catch (_) {
|
|
@@ -19,80 +19,75 @@
|
|
|
19
19
|
const PROVIDER = 'opencode';
|
|
20
20
|
const MODEL = 'custom';
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Safely extract a finite number from a value, defaulting to 0.
|
|
24
|
-
*/
|
|
22
|
+
/** @param {*} value */
|
|
25
23
|
function num(value) {
|
|
26
24
|
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* @param {object} obj - Parsed JSON object
|
|
34
|
-
* @returns {object|null} Normalized token usage object or null
|
|
28
|
+
* @param {{[key: string]: any}} obj
|
|
29
|
+
* @returns {{input_tokens?: number, output_tokens?: number, cached_input_tokens?: number, total_tokens?: number}|null}
|
|
35
30
|
*/
|
|
36
31
|
function findTokenUsage(obj) {
|
|
37
|
-
if (!obj || typeof obj !== 'object') return null;
|
|
32
|
+
if (!obj || typeof obj !== 'object') {return null;}
|
|
38
33
|
|
|
39
34
|
// Direct token usage fields at top level
|
|
40
35
|
if ('input_tokens' in obj || 'output_tokens' in obj) {
|
|
41
36
|
return {
|
|
42
|
-
input_tokens: obj.input_tokens,
|
|
43
|
-
output_tokens: obj.output_tokens,
|
|
44
|
-
cached_input_tokens: obj.cached_input_tokens || obj.cached_tokens || 0,
|
|
45
|
-
total_tokens: obj.total_tokens || 0,
|
|
37
|
+
input_tokens: /** @type {number} */(obj.input_tokens),
|
|
38
|
+
output_tokens: /** @type {number} */(obj.output_tokens),
|
|
39
|
+
cached_input_tokens: /** @type {number} */(obj.cached_input_tokens || obj.cached_tokens || 0),
|
|
40
|
+
total_tokens: /** @type {number} */(obj.total_tokens || 0),
|
|
46
41
|
};
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
// Nested under total_token_usage (Codex-compatible shape)
|
|
50
45
|
if (obj.total_token_usage && typeof obj.total_token_usage === 'object') {
|
|
51
46
|
return {
|
|
52
|
-
input_tokens: obj.total_token_usage.input_tokens || 0,
|
|
53
|
-
output_tokens: obj.total_token_usage.output_tokens || 0,
|
|
54
|
-
cached_input_tokens: obj.total_token_usage.cached_input_tokens || obj.total_token_usage.cached_tokens || 0,
|
|
55
|
-
total_tokens: obj.total_token_usage.total_tokens || 0,
|
|
47
|
+
input_tokens: /** @type {number} */(obj.total_token_usage.input_tokens || 0),
|
|
48
|
+
output_tokens: /** @type {number} */(obj.total_token_usage.output_tokens || 0),
|
|
49
|
+
cached_input_tokens: /** @type {number} */(obj.total_token_usage.cached_input_tokens || obj.total_token_usage.cached_tokens || 0),
|
|
50
|
+
total_tokens: /** @type {number} */(obj.total_token_usage.total_tokens || 0),
|
|
56
51
|
};
|
|
57
52
|
}
|
|
58
53
|
|
|
59
54
|
// Nested under token_usage
|
|
60
55
|
if (obj.token_usage && typeof obj.token_usage === 'object') {
|
|
61
56
|
return {
|
|
62
|
-
input_tokens: obj.token_usage.input_tokens || 0,
|
|
63
|
-
output_tokens: obj.token_usage.output_tokens || 0,
|
|
64
|
-
cached_input_tokens: obj.token_usage.cached_input_tokens || obj.token_usage.cached_tokens || 0,
|
|
65
|
-
total_tokens: obj.token_usage.total_tokens || 0,
|
|
57
|
+
input_tokens: /** @type {number} */(obj.token_usage.input_tokens || 0),
|
|
58
|
+
output_tokens: /** @type {number} */(obj.token_usage.output_tokens || 0),
|
|
59
|
+
cached_input_tokens: /** @type {number} */(obj.token_usage.cached_input_tokens || obj.token_usage.cached_tokens || 0),
|
|
60
|
+
total_tokens: /** @type {number} */(obj.token_usage.total_tokens || 0),
|
|
66
61
|
};
|
|
67
62
|
}
|
|
68
63
|
|
|
69
64
|
// Nested under usage
|
|
70
65
|
if (obj.usage && typeof obj.usage === 'object') {
|
|
71
66
|
return {
|
|
72
|
-
input_tokens: obj.usage.input_tokens || 0,
|
|
73
|
-
output_tokens: obj.usage.output_tokens || 0,
|
|
74
|
-
cached_input_tokens: obj.usage.cached_input_tokens || obj.usage.cached_tokens || 0,
|
|
75
|
-
total_tokens: obj.usage.total_tokens || 0,
|
|
67
|
+
input_tokens: /** @type {number} */(obj.usage.input_tokens || 0),
|
|
68
|
+
output_tokens: /** @type {number} */(obj.usage.output_tokens || 0),
|
|
69
|
+
cached_input_tokens: /** @type {number} */(obj.usage.cached_input_tokens || obj.usage.cached_tokens || 0),
|
|
70
|
+
total_tokens: /** @type {number} */(obj.usage.total_tokens || 0),
|
|
76
71
|
};
|
|
77
72
|
}
|
|
78
73
|
|
|
79
74
|
// Nested under meta or metadata
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if ('input_tokens' in
|
|
75
|
+
const metaOrMeta = obj.meta || obj.metadata;
|
|
76
|
+
if (metaOrMeta && typeof metaOrMeta === 'object') {
|
|
77
|
+
if ('input_tokens' in metaOrMeta || 'output_tokens' in metaOrMeta) {
|
|
83
78
|
return {
|
|
84
|
-
input_tokens:
|
|
85
|
-
output_tokens:
|
|
86
|
-
cached_input_tokens:
|
|
87
|
-
total_tokens:
|
|
79
|
+
input_tokens: /** @type {number} */(metaOrMeta.input_tokens || 0),
|
|
80
|
+
output_tokens: /** @type {number} */(metaOrMeta.output_tokens || 0),
|
|
81
|
+
cached_input_tokens: /** @type {number} */(metaOrMeta.cached_input_tokens || metaOrMeta.cached_tokens || 0),
|
|
82
|
+
total_tokens: /** @type {number} */(metaOrMeta.total_tokens || 0),
|
|
88
83
|
};
|
|
89
84
|
}
|
|
90
|
-
if (
|
|
85
|
+
if (metaOrMeta.total_token_usage && typeof metaOrMeta.total_token_usage === 'object') {
|
|
91
86
|
return {
|
|
92
|
-
input_tokens:
|
|
93
|
-
output_tokens:
|
|
94
|
-
cached_input_tokens:
|
|
95
|
-
total_tokens:
|
|
87
|
+
input_tokens: /** @type {number} */(metaOrMeta.total_token_usage.input_tokens || 0),
|
|
88
|
+
output_tokens: /** @type {number} */(metaOrMeta.total_token_usage.output_tokens || 0),
|
|
89
|
+
cached_input_tokens: /** @type {number} */(metaOrMeta.total_token_usage.cached_input_tokens || metaOrMeta.total_token_usage.cached_tokens || 0),
|
|
90
|
+
total_tokens: /** @type {number} */(metaOrMeta.total_token_usage.total_tokens || 0),
|
|
96
91
|
};
|
|
97
92
|
}
|
|
98
93
|
}
|
|
@@ -103,10 +98,10 @@ function findTokenUsage(obj) {
|
|
|
103
98
|
const t = obj.info.tokens;
|
|
104
99
|
if ('input' in t || 'output' in t) {
|
|
105
100
|
return {
|
|
106
|
-
input_tokens: t.input || 0,
|
|
107
|
-
output_tokens: t.output || 0,
|
|
108
|
-
cached_input_tokens: (t.cache && typeof t.cache === 'object') ? (t.cache.read || 0) : 0,
|
|
109
|
-
total_tokens: t.input + t.output || 0,
|
|
101
|
+
input_tokens: /** @type {number} */(t.input || 0),
|
|
102
|
+
output_tokens: /** @type {number} */(t.output || 0),
|
|
103
|
+
cached_input_tokens: (t.cache && typeof t.cache === 'object') ? /** @type {number} */(t.cache.read || 0) : 0,
|
|
104
|
+
total_tokens: /** @type {number} */(t.input + t.output || 0),
|
|
110
105
|
};
|
|
111
106
|
}
|
|
112
107
|
}
|
|
@@ -117,8 +112,8 @@ function findTokenUsage(obj) {
|
|
|
117
112
|
if (Array.isArray(val)) {
|
|
118
113
|
for (const item of val) {
|
|
119
114
|
if (item && typeof item === 'object') {
|
|
120
|
-
const found = findTokenUsage(item);
|
|
121
|
-
if (found) return found;
|
|
115
|
+
const found = findTokenUsage(/** @type {{[key: string]: any}} */(item));
|
|
116
|
+
if (found) {return found;}
|
|
122
117
|
}
|
|
123
118
|
}
|
|
124
119
|
}
|
|
@@ -128,89 +123,83 @@ function findTokenUsage(obj) {
|
|
|
128
123
|
}
|
|
129
124
|
|
|
130
125
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* @param {object} parsed - Parsed JSON object
|
|
134
|
-
* @returns {string|null} Session ID or null
|
|
126
|
+
* @param {{[key: string]: any}} parsed
|
|
127
|
+
* @returns {string|null}
|
|
135
128
|
*/
|
|
136
129
|
function extractSessionId(parsed) {
|
|
137
|
-
if (!parsed || typeof parsed !== 'object') return null;
|
|
130
|
+
if (!parsed || typeof parsed !== 'object') {return null;}
|
|
138
131
|
|
|
139
132
|
// Direct session_id
|
|
140
|
-
if (parsed.session_id) return String(parsed.session_id);
|
|
141
|
-
if (parsed.sessionId) return String(parsed.sessionId);
|
|
133
|
+
if (parsed.session_id) {return String(parsed.session_id);}
|
|
134
|
+
if (parsed.sessionId) {return String(parsed.sessionId);}
|
|
142
135
|
|
|
143
136
|
// Nested under info (opencode v2.x export format)
|
|
144
137
|
if (parsed.info && typeof parsed.info === 'object') {
|
|
145
|
-
if (parsed.info.id) return String(parsed.info.id);
|
|
138
|
+
if (parsed.info.id) {return String(parsed.info.id);}
|
|
146
139
|
}
|
|
147
140
|
|
|
148
141
|
// Nested in metadata/meta/session
|
|
149
142
|
if (parsed.metadata && typeof parsed.metadata === 'object') {
|
|
150
|
-
if (parsed.metadata.session_id) return String(parsed.metadata.session_id);
|
|
151
|
-
if (parsed.metadata.sessionId) return String(parsed.metadata.sessionId);
|
|
143
|
+
if (parsed.metadata.session_id) {return String(parsed.metadata.session_id);}
|
|
144
|
+
if (parsed.metadata.sessionId) {return String(parsed.metadata.sessionId);}
|
|
152
145
|
}
|
|
153
146
|
if (parsed.meta && typeof parsed.meta === 'object') {
|
|
154
|
-
if (parsed.meta.session_id) return String(parsed.meta.session_id);
|
|
155
|
-
if (parsed.meta.sessionId) return String(parsed.meta.sessionId);
|
|
147
|
+
if (parsed.meta.session_id) {return String(parsed.meta.session_id);}
|
|
148
|
+
if (parsed.meta.sessionId) {return String(parsed.meta.sessionId);}
|
|
156
149
|
}
|
|
157
150
|
if (parsed.session && typeof parsed.session === 'object') {
|
|
158
|
-
if (parsed.session.id) return String(parsed.session.id);
|
|
159
|
-
if (parsed.session.session_id) return String(parsed.session.session_id);
|
|
151
|
+
if (parsed.session.id) {return String(parsed.session.id);}
|
|
152
|
+
if (parsed.session.session_id) {return String(parsed.session.session_id);}
|
|
160
153
|
}
|
|
161
154
|
|
|
162
155
|
return null;
|
|
163
156
|
}
|
|
164
157
|
|
|
165
158
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* @param {object} parsed - Parsed JSON object
|
|
169
|
-
* @returns {string|null} Model name or null
|
|
159
|
+
* @param {{[key: string]: any}} parsed
|
|
160
|
+
* @returns {string|null}
|
|
170
161
|
*/
|
|
171
162
|
function extractModelName(parsed) {
|
|
172
|
-
if (!parsed || typeof parsed !== 'object') return null;
|
|
163
|
+
if (!parsed || typeof parsed !== 'object') {return null;}
|
|
173
164
|
|
|
174
165
|
// Direct model fields
|
|
175
|
-
if (parsed.model) return String(parsed.model);
|
|
176
|
-
if (parsed.model_name) return String(parsed.model_name);
|
|
166
|
+
if (parsed.model) {return String(parsed.model);}
|
|
167
|
+
if (parsed.model_name) {return String(parsed.model_name);}
|
|
177
168
|
|
|
178
169
|
// Nested under info.model (opencode v2.x format)
|
|
179
170
|
if (parsed.info && typeof parsed.info === 'object' && parsed.info.model && typeof parsed.info.model === 'object') {
|
|
180
|
-
if (parsed.info.model.id) return String(parsed.info.model.id);
|
|
181
|
-
if (parsed.info.model.name) return String(parsed.info.model.name);
|
|
171
|
+
if (parsed.info.model.id) {return String(parsed.info.model.id);}
|
|
172
|
+
if (parsed.info.model.name) {return String(parsed.info.model.name);}
|
|
182
173
|
}
|
|
183
174
|
|
|
184
175
|
// Nested in metadata/meta/session
|
|
185
176
|
if (parsed.metadata && typeof parsed.metadata === 'object') {
|
|
186
|
-
if (parsed.metadata.model) return String(parsed.metadata.model);
|
|
187
|
-
if (parsed.metadata.model_name) return String(parsed.metadata.model_name);
|
|
177
|
+
if (parsed.metadata.model) {return String(parsed.metadata.model);}
|
|
178
|
+
if (parsed.metadata.model_name) {return String(parsed.metadata.model_name);}
|
|
188
179
|
}
|
|
189
180
|
if (parsed.meta && typeof parsed.meta === 'object') {
|
|
190
|
-
if (parsed.meta.model) return String(parsed.meta.model);
|
|
181
|
+
if (parsed.meta.model) {return String(parsed.meta.model);}
|
|
191
182
|
}
|
|
192
183
|
if (parsed.session && typeof parsed.session === 'object') {
|
|
193
|
-
if (parsed.session.model) return String(parsed.session.model);
|
|
184
|
+
if (parsed.session.model) {return String(parsed.session.model);}
|
|
194
185
|
}
|
|
195
186
|
|
|
196
187
|
return null;
|
|
197
188
|
}
|
|
198
189
|
|
|
199
190
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* @param {object} parsed - Parsed JSON object
|
|
203
|
-
* @returns {number} Tool call count
|
|
191
|
+
* @param {{[key: string]: any}} parsed
|
|
192
|
+
* @returns {number}
|
|
204
193
|
*/
|
|
205
194
|
function countToolCalls(parsed) {
|
|
206
|
-
if (!parsed || typeof parsed !== 'object') return 0;
|
|
195
|
+
if (!parsed || typeof parsed !== 'object') {return 0;}
|
|
207
196
|
|
|
208
197
|
let count = 0;
|
|
209
198
|
|
|
210
199
|
// Direct tool_calls field
|
|
211
|
-
if (typeof parsed.tool_calls === 'number') count += parsed.tool_calls;
|
|
212
|
-
if (typeof parsed.toolCalls === 'number') count += parsed.toolCalls;
|
|
213
|
-
if (typeof parsed.usage?.tool_calls === 'number') count += parsed.usage.tool_calls;
|
|
200
|
+
if (typeof parsed.tool_calls === 'number') {count += parsed.tool_calls;}
|
|
201
|
+
if (typeof parsed.toolCalls === 'number') {count += parsed.toolCalls;}
|
|
202
|
+
if (typeof parsed.usage?.tool_calls === 'number') {count += parsed.usage.tool_calls;}
|
|
214
203
|
|
|
215
204
|
// Real opencode v2.x export schema: tool calls live in
|
|
216
205
|
// messages[].parts[] where part.type === 'tool'.
|
|
@@ -218,7 +207,7 @@ function countToolCalls(parsed) {
|
|
|
218
207
|
for (const msg of parsed.messages) {
|
|
219
208
|
if (msg && typeof msg === 'object' && Array.isArray(msg.parts)) {
|
|
220
209
|
for (const part of msg.parts) {
|
|
221
|
-
if (part && typeof part === 'object' && part.type === 'tool') count += 1;
|
|
210
|
+
if (part && typeof part === 'object' && part.type === 'tool') {count += 1;}
|
|
222
211
|
}
|
|
223
212
|
}
|
|
224
213
|
}
|
|
@@ -228,13 +217,13 @@ function countToolCalls(parsed) {
|
|
|
228
217
|
if (Array.isArray(parsed.events)) {
|
|
229
218
|
for (const evt of parsed.events) {
|
|
230
219
|
if (evt && typeof evt === 'object') {
|
|
231
|
-
if (evt.type === 'tool_use') count += 1;
|
|
232
|
-
if (evt.type === 'tool_call') count += 1;
|
|
233
|
-
if (evt.type === 'function_call') count += 1;
|
|
234
|
-
if (Array.isArray(evt.tool_calls)) count += evt.tool_calls.length;
|
|
220
|
+
if (evt.type === 'tool_use') {count += 1;}
|
|
221
|
+
if (evt.type === 'tool_call') {count += 1;}
|
|
222
|
+
if (evt.type === 'function_call') {count += 1;}
|
|
223
|
+
if (Array.isArray(evt.tool_calls)) {count += evt.tool_calls.length;}
|
|
235
224
|
if (Array.isArray(evt.content_block_start)) {
|
|
236
225
|
for (const cb of evt.content_block_start) {
|
|
237
|
-
if (cb && cb.type === 'tool_use') count += 1;
|
|
226
|
+
if (cb && cb.type === 'tool_use') {count += 1;}
|
|
238
227
|
}
|
|
239
228
|
}
|
|
240
229
|
}
|
|
@@ -329,12 +318,8 @@ function extractOpencodeTelemetryFromExport(jsonString, fallbackModel) {
|
|
|
329
318
|
}
|
|
330
319
|
|
|
331
320
|
/**
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
* Kept for backward compatibility with existing call sites.
|
|
335
|
-
*
|
|
336
|
-
* @param {object} result - The launcher result object
|
|
337
|
-
* @returns {object|null} Telemetry object or null
|
|
321
|
+
* @param {{exportJson?: string, telemetry?: object}} result
|
|
322
|
+
* @returns {object|null}
|
|
338
323
|
*/
|
|
339
324
|
function extractOpencodeTelemetry(result) {
|
|
340
325
|
if (result && result.exportJson && typeof result.exportJson === 'string') {
|