@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
@@ -0,0 +1,350 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Opencode Telemetry Parser
5
+ *
6
+ * Parses `opencode export` JSON output to extract real token-usage data for
7
+ * custom/opencode sessions. The exported JSON may contain token usage in various shapes
8
+ * depending on the opencode version, so this parser is resilient to:
9
+ * - Missing token fields (substitutes 0)
10
+ * - Nested vs flat structures
11
+ * - Different field name conventions
12
+ *
13
+ * Provider and model fields are set to "opencode" and "custom" respectively,
14
+ * matching the convention used by other telemetry modules.
15
+ *
16
+ * See task-1285 for the full telemetry credibility design.
17
+ */
18
+
19
+ const PROVIDER = 'opencode';
20
+ const MODEL = 'custom';
21
+
22
+ function num(value: any) {
23
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
24
+ }
25
+
26
+ /**
27
+ * @param {{[key: string]: any}} obj
28
+ * @returns {{input_tokens?: number, output_tokens?: number, cached_input_tokens?: number, total_tokens?: number}|null}
29
+ */
30
+ function findTokenUsage(obj: {[key: string]: any}): {input_tokens?: number, output_tokens?: number, cached_input_tokens?: number, total_tokens?: number} | null {
31
+ if (!obj || typeof obj !== 'object') {return null;}
32
+
33
+ // Direct token usage fields at top level
34
+ if ('input_tokens' in obj || 'output_tokens' in obj) {
35
+ return {
36
+ input_tokens: /** @type {number} */(obj.input_tokens),
37
+ output_tokens: /** @type {number} */(obj.output_tokens),
38
+ cached_input_tokens: /** @type {number} */(obj.cached_input_tokens || obj.cached_tokens || 0),
39
+ total_tokens: /** @type {number} */(obj.total_tokens || 0),
40
+ };
41
+ }
42
+
43
+ // Nested under total_token_usage (Codex-compatible shape)
44
+ if (obj.total_token_usage && typeof obj.total_token_usage === 'object') {
45
+ return {
46
+ input_tokens: /** @type {number} */(obj.total_token_usage.input_tokens || 0),
47
+ output_tokens: /** @type {number} */(obj.total_token_usage.output_tokens || 0),
48
+ cached_input_tokens: /** @type {number} */(obj.total_token_usage.cached_input_tokens || obj.total_token_usage.cached_tokens || 0),
49
+ total_tokens: /** @type {number} */(obj.total_token_usage.total_tokens || 0),
50
+ };
51
+ }
52
+
53
+ // Nested under token_usage
54
+ if (obj.token_usage && typeof obj.token_usage === 'object') {
55
+ return {
56
+ input_tokens: /** @type {number} */(obj.token_usage.input_tokens || 0),
57
+ output_tokens: /** @type {number} */(obj.token_usage.output_tokens || 0),
58
+ cached_input_tokens: /** @type {number} */(obj.token_usage.cached_input_tokens || obj.token_usage.cached_tokens || 0),
59
+ total_tokens: /** @type {number} */(obj.token_usage.total_tokens || 0),
60
+ };
61
+ }
62
+
63
+ // Nested under usage
64
+ if (obj.usage && typeof obj.usage === 'object') {
65
+ return {
66
+ input_tokens: /** @type {number} */(obj.usage.input_tokens || 0),
67
+ output_tokens: /** @type {number} */(obj.usage.output_tokens || 0),
68
+ cached_input_tokens: /** @type {number} */(obj.usage.cached_input_tokens || obj.usage.cached_tokens || 0),
69
+ total_tokens: /** @type {number} */(obj.usage.total_tokens || 0),
70
+ };
71
+ }
72
+
73
+ // Nested under meta or metadata
74
+ const metaOrMeta = obj.meta || obj.metadata;
75
+ if (metaOrMeta && typeof metaOrMeta === 'object') {
76
+ if ('input_tokens' in metaOrMeta || 'output_tokens' in metaOrMeta) {
77
+ return {
78
+ input_tokens: /** @type {number} */(metaOrMeta.input_tokens || 0),
79
+ output_tokens: /** @type {number} */(metaOrMeta.output_tokens || 0),
80
+ cached_input_tokens: /** @type {number} */(metaOrMeta.cached_input_tokens || metaOrMeta.cached_tokens || 0),
81
+ total_tokens: /** @type {number} */(metaOrMeta.total_tokens || 0),
82
+ };
83
+ }
84
+ if (metaOrMeta.total_token_usage && typeof metaOrMeta.total_token_usage === 'object') {
85
+ return {
86
+ input_tokens: /** @type {number} */(metaOrMeta.total_token_usage.input_tokens || 0),
87
+ output_tokens: /** @type {number} */(metaOrMeta.total_token_usage.output_tokens || 0),
88
+ cached_input_tokens: /** @type {number} */(metaOrMeta.total_token_usage.cached_input_tokens || metaOrMeta.total_token_usage.cached_tokens || 0),
89
+ total_tokens: /** @type {number} */(metaOrMeta.total_token_usage.total_tokens || 0),
90
+ };
91
+ }
92
+ }
93
+
94
+ // Nested under info (opencode v2.x export format)
95
+ // Shape: { info: { tokens: { input, output, cache: { read, write }, reasoning } } }
96
+ if (obj.info && typeof obj.info === 'object' && obj.info.tokens && typeof obj.info.tokens === 'object') {
97
+ const t = obj.info.tokens;
98
+ if ('input' in t || 'output' in t) {
99
+ return {
100
+ input_tokens: /** @type {number} */(t.input || 0),
101
+ output_tokens: /** @type {number} */(t.output || 0),
102
+ cached_input_tokens: (t.cache && typeof t.cache === 'object') ? /** @type {number} */(t.cache.read || 0) : 0,
103
+ total_tokens: /** @type {number} */(t.input + t.output || 0),
104
+ };
105
+ }
106
+ }
107
+
108
+ // Search recursively through array elements (e.g., events array)
109
+ for (const key of Object.keys(obj)) {
110
+ const val = obj[key];
111
+ if (Array.isArray(val)) {
112
+ for (const item of val) {
113
+ if (item && typeof item === 'object') {
114
+ const found = findTokenUsage(/** @type {{[key: string]: any}} */(item));
115
+ if (found) {return found;}
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ return null;
122
+ }
123
+
124
+ /**
125
+ * @param {{[key: string]: any}} parsed
126
+ * @returns {string|null}
127
+ */
128
+ function extractSessionId(parsed: {[key: string]: any}) {
129
+ if (!parsed || typeof parsed !== 'object') {return null;}
130
+
131
+ // Direct session_id
132
+ if (parsed.session_id) {return String(parsed.session_id);}
133
+ if (parsed.sessionId) {return String(parsed.sessionId);}
134
+
135
+ // Nested under info (opencode v2.x export format)
136
+ if (parsed.info && typeof parsed.info === 'object') {
137
+ if (parsed.info.id) {return String(parsed.info.id);}
138
+ }
139
+
140
+ // Nested in metadata/meta/session
141
+ if (parsed.metadata && typeof parsed.metadata === 'object') {
142
+ if (parsed.metadata.session_id) {return String(parsed.metadata.session_id);}
143
+ if (parsed.metadata.sessionId) {return String(parsed.metadata.sessionId);}
144
+ }
145
+ if (parsed.meta && typeof parsed.meta === 'object') {
146
+ if (parsed.meta.session_id) {return String(parsed.meta.session_id);}
147
+ if (parsed.meta.sessionId) {return String(parsed.meta.sessionId);}
148
+ }
149
+ if (parsed.session && typeof parsed.session === 'object') {
150
+ if (parsed.session.id) {return String(parsed.session.id);}
151
+ if (parsed.session.session_id) {return String(parsed.session.session_id);}
152
+ }
153
+
154
+ return null;
155
+ }
156
+
157
+ /**
158
+ * @param {{[key: string]: any}} parsed
159
+ * @returns {string|null}
160
+ */
161
+ function extractModelName(parsed: {[key: string]: any}) {
162
+ if (!parsed || typeof parsed !== 'object') {return null;}
163
+
164
+ // Direct model fields
165
+ if (parsed.model) {return String(parsed.model);}
166
+ if (parsed.model_name) {return String(parsed.model_name);}
167
+
168
+ // Nested under info.model (opencode v2.x format)
169
+ if (parsed.info && typeof parsed.info === 'object' && parsed.info.model && typeof parsed.info.model === 'object') {
170
+ if (parsed.info.model.id) {return String(parsed.info.model.id);}
171
+ if (parsed.info.model.name) {return String(parsed.info.model.name);}
172
+ }
173
+
174
+ // Nested in metadata/meta/session
175
+ if (parsed.metadata && typeof parsed.metadata === 'object') {
176
+ if (parsed.metadata.model) {return String(parsed.metadata.model);}
177
+ if (parsed.metadata.model_name) {return String(parsed.metadata.model_name);}
178
+ }
179
+ if (parsed.meta && typeof parsed.meta === 'object') {
180
+ if (parsed.meta.model) {return String(parsed.meta.model);}
181
+ }
182
+ if (parsed.session && typeof parsed.session === 'object') {
183
+ if (parsed.session.model) {return String(parsed.session.model);}
184
+ }
185
+
186
+ return null;
187
+ }
188
+
189
+ /**
190
+ * @param {{[key: string]: any}} parsed
191
+ * @returns {number}
192
+ */
193
+ function countToolCalls(parsed: {[key: string]: any}) {
194
+ if (!parsed || typeof parsed !== 'object') {return 0;}
195
+
196
+ let count = 0;
197
+
198
+ // Direct tool_calls field
199
+ if (typeof parsed.tool_calls === 'number') {count += parsed.tool_calls;}
200
+ if (typeof parsed.toolCalls === 'number') {count += parsed.toolCalls;}
201
+ if (typeof parsed.usage?.tool_calls === 'number') {count += parsed.usage.tool_calls;}
202
+
203
+ // Real opencode v2.x export schema: tool calls live in
204
+ // messages[].parts[] where part.type === 'tool'.
205
+ if (Array.isArray(parsed.messages)) {
206
+ for (const msg of parsed.messages) {
207
+ if (msg && typeof msg === 'object' && Array.isArray(msg.parts)) {
208
+ for (const part of msg.parts) {
209
+ if (part && typeof part === 'object' && part.type === 'tool') {count += 1;}
210
+ }
211
+ }
212
+ }
213
+ }
214
+
215
+ // Nested arrays of tool calls
216
+ if (Array.isArray(parsed.events)) {
217
+ for (const evt of parsed.events) {
218
+ if (evt && typeof evt === 'object') {
219
+ if (evt.type === 'tool_use') {count += 1;}
220
+ if (evt.type === 'tool_call') {count += 1;}
221
+ if (evt.type === 'function_call') {count += 1;}
222
+ if (Array.isArray(evt.tool_calls)) {count += evt.tool_calls.length;}
223
+ if (Array.isArray(evt.content_block_start)) {
224
+ for (const cb of evt.content_block_start) {
225
+ if (cb && cb.type === 'tool_use') {count += 1;}
226
+ }
227
+ }
228
+ }
229
+ }
230
+ }
231
+
232
+ if (Array.isArray(parsed.tool_use_events)) {
233
+ count += parsed.tool_use_events.length;
234
+ }
235
+
236
+ return count;
237
+ }
238
+
239
+ /**
240
+ * Parse an `opencode export` JSON string and return a normalized telemetry
241
+ * object compatible with `stats.telemetryToStatsFields()`.
242
+ *
243
+ * Returns null when the content yields no usable token signal (empty string,
244
+ * non-JSON, or JSON without any token-usage fields).
245
+ *
246
+ * When the JSON contains token data but no model field, falls back to
247
+ * `fallbackModel` — the model the launcher was configured with for this run
248
+ * (from workflow.config.json) — so telemetry records the actual model id
249
+ * instead of the generic family label. When no model is configured, falls
250
+ * back to the generic family label `MODEL`.
251
+ *
252
+ * @param {string} jsonString - Raw JSON string from `opencode export`
253
+ * @param {string=} [fallbackModel] - Configured model id for this run, used
254
+ * when the export JSON omits the model field
255
+ * @returns {object|null} Normalized telemetry object or null
256
+ */
257
+ function extractOpencodeTelemetryFromExport(jsonString: string, fallbackModel?: string) {
258
+ if (!jsonString || typeof jsonString !== 'string' || !jsonString.trim()) {
259
+ return null;
260
+ }
261
+
262
+ let parsed;
263
+ try {
264
+ parsed = JSON.parse(jsonString);
265
+ } catch (_) {
266
+ return null;
267
+ }
268
+
269
+ if (!parsed || typeof parsed !== 'object') {
270
+ return null;
271
+ }
272
+
273
+ const tokenUsage = findTokenUsage(parsed);
274
+ if (!tokenUsage) {
275
+ return null;
276
+ }
277
+
278
+ // Verify there is at least some non-zero token data
279
+ const inputTokens = num(tokenUsage.input_tokens);
280
+ const outputTokens = num(tokenUsage.output_tokens);
281
+ const cachedTokens = num(tokenUsage.cached_input_tokens);
282
+
283
+ // If all tokens are zero, treat as no signal (unless total_tokens explicitly exists and is non-zero)
284
+ if (inputTokens === 0 && outputTokens === 0 && cachedTokens === 0) {
285
+ if (num(tokenUsage.total_tokens) === 0) {
286
+ return null;
287
+ }
288
+ // total_tokens is set but individual fields are zero — treat total_tokens as the real signal
289
+ // and derive input/output from it conservatively
290
+ const totalTokens = num(tokenUsage.total_tokens);
291
+ return {
292
+ sessionId: extractSessionId(parsed),
293
+ provider: PROVIDER,
294
+ model: extractModelName(parsed) || fallbackModel || MODEL,
295
+ inputTokens: 0,
296
+ outputTokens: 0,
297
+ cachedTokens: 0,
298
+ totalTokens,
299
+ toolCalls: countToolCalls(parsed),
300
+ usagePercent: null,
301
+ };
302
+ }
303
+
304
+ const totalTokens = num(tokenUsage.total_tokens) || (inputTokens + outputTokens);
305
+
306
+ return {
307
+ sessionId: extractSessionId(parsed),
308
+ provider: PROVIDER,
309
+ model: extractModelName(parsed) || fallbackModel || MODEL,
310
+ inputTokens,
311
+ outputTokens,
312
+ cachedTokens,
313
+ totalTokens,
314
+ toolCalls: countToolCalls(parsed),
315
+ usagePercent: null,
316
+ };
317
+ }
318
+
319
+ /**
320
+ * @param {{exportJson?: string, telemetry?: object}} result
321
+ * @returns {object|null}
322
+ */
323
+ function extractOpencodeTelemetry(result: {exportJson?: string, telemetry?: any}) {
324
+ if (result && result.exportJson && typeof result.exportJson === 'string') {
325
+ return extractOpencodeTelemetryFromExport(result.exportJson);
326
+ }
327
+ if (result && result.telemetry) {
328
+ return result.telemetry;
329
+ }
330
+ return null;
331
+ }
332
+
333
+ /**
334
+ * Return the provider/model pair for opencode tasks.
335
+ * Used as fallback when telemetry is null.
336
+ *
337
+ * @param {string=} [defaultModel] - Optional configured model id; falls back
338
+ * to the generic family label when absent.
339
+ * @returns {{provider: string, model: string}}
340
+ */
341
+ function getOpencodeProviderModel(defaultModel?: string) {
342
+ return { provider: PROVIDER, model: defaultModel || MODEL };
343
+ }
344
+
345
+ export {
346
+ extractOpencodeTelemetryFromExport,
347
+ extractOpencodeTelemetry,
348
+ getOpencodeProviderModel,
349
+ extractOpencodeTelemetryFromExport as parseOpencodeExport,
350
+ };