codex-session-insights 0.1.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/LICENSE +21 -0
- package/README.md +199 -0
- package/bin/codex-insights.js +9 -0
- package/lib/cli.js +1002 -0
- package/lib/codex-data.js +640 -0
- package/lib/llm-insights.js +1486 -0
- package/lib/model-provider.js +589 -0
- package/lib/report.js +1383 -0
- package/lib/types.d.ts +87 -0
- package/package.json +47 -0
|
@@ -0,0 +1,1486 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
import { promises as fs } from 'node:fs'
|
|
5
|
+
import { buildReport } from './report.js'
|
|
6
|
+
import { callStructuredModel, callTextModel } from './model-provider.js'
|
|
7
|
+
import { filterSubstantiveThreads } from './codex-data.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} InsightRunOptions
|
|
11
|
+
* @property {string=} provider
|
|
12
|
+
* @property {string=} apiKey
|
|
13
|
+
* @property {string=} apiBase
|
|
14
|
+
* @property {string=} codexBin
|
|
15
|
+
* @property {string=} cacheDir
|
|
16
|
+
* @property {string=} facetModel
|
|
17
|
+
* @property {string=} facetEffort
|
|
18
|
+
* @property {string=} fastSectionModel
|
|
19
|
+
* @property {string=} fastSectionEffort
|
|
20
|
+
* @property {string=} insightModel
|
|
21
|
+
* @property {string=} insightEffort
|
|
22
|
+
* @property {number=} facetLimit
|
|
23
|
+
* @property {string=} lang
|
|
24
|
+
* @property {(usage: any) => void=} onUsage
|
|
25
|
+
* @property {(event: any) => void=} onProgress
|
|
26
|
+
* @property {string=} usageStage
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const DEFAULT_PROVIDER = 'codex-cli'
|
|
30
|
+
const DEFAULT_FACET_MODEL = 'gpt-5.4-mini'
|
|
31
|
+
const DEFAULT_FAST_SECTION_MODEL = 'gpt-5.4-mini'
|
|
32
|
+
const DEFAULT_INSIGHT_MODEL = 'gpt-5.4'
|
|
33
|
+
const DEFAULT_FACET_EFFORT = 'low'
|
|
34
|
+
const DEFAULT_FAST_SECTION_EFFORT = 'low'
|
|
35
|
+
const DEFAULT_INSIGHT_EFFORT = 'high'
|
|
36
|
+
const DEFAULT_FACET_LIMIT = 50
|
|
37
|
+
const MAX_FACET_EXTRACTIONS = 50
|
|
38
|
+
const LONG_TRANSCRIPT_THRESHOLD = 30000
|
|
39
|
+
const TRANSCRIPT_CHUNK_SIZE = 25000
|
|
40
|
+
const FACET_TRANSCRIPT_SUMMARY_DIRECTIVE =
|
|
41
|
+
'Summarize this coding-session transcript segment into compact bullets (4-8 max). Keep only user goals, assistant outcomes, tool failures, and terminal failures that materially affect results. Do not include raw tool arguments or command output.'
|
|
42
|
+
const MAX_CONTEXT_FACETS = 24
|
|
43
|
+
const MAX_FRICTION_DETAILS = 12
|
|
44
|
+
const MAX_USER_INSTRUCTIONS = 8
|
|
45
|
+
const MAX_RECENT_THREADS = 8
|
|
46
|
+
const SECTION_CONCURRENCY = 3
|
|
47
|
+
const FACET_SCHEMA_VERSION = 2
|
|
48
|
+
const SECTION_SYSTEM_PROMPT =
|
|
49
|
+
'You are generating a Codex session insights report. Use only the provided evidence. Be concrete, diagnostic, and concise. Use second person. Do not flatter. Do not speculate past the data. If evidence is weak or mixed, be conservative and say less.'
|
|
50
|
+
const AT_A_GLANCE_SYSTEM_PROMPT =
|
|
51
|
+
'You are writing the At a Glance section for a Codex usage report. Use only the supplied report context and section digest. Be high-signal, specific, and coaching rather than promotional. Use second person. Do not mention exact token or usage numbers. Do not speculate.'
|
|
52
|
+
|
|
53
|
+
const FACET_SCHEMA = {
|
|
54
|
+
type: 'object',
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
properties: {
|
|
57
|
+
underlying_goal: { type: 'string' },
|
|
58
|
+
goal_categories: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
additionalProperties: { type: 'number' },
|
|
61
|
+
},
|
|
62
|
+
outcome: { type: 'string' },
|
|
63
|
+
user_satisfaction_counts: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
additionalProperties: { type: 'number' },
|
|
66
|
+
},
|
|
67
|
+
assistant_helpfulness: { type: 'string' },
|
|
68
|
+
session_type: { type: 'string' },
|
|
69
|
+
friction_counts: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
additionalProperties: { type: 'number' },
|
|
72
|
+
},
|
|
73
|
+
friction_detail: { type: 'string' },
|
|
74
|
+
primary_success: { type: 'string' },
|
|
75
|
+
brief_summary: { type: 'string' },
|
|
76
|
+
user_instructions: {
|
|
77
|
+
type: 'array',
|
|
78
|
+
items: { type: 'string' },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
required: [
|
|
82
|
+
'underlying_goal',
|
|
83
|
+
'goal_categories',
|
|
84
|
+
'outcome',
|
|
85
|
+
'user_satisfaction_counts',
|
|
86
|
+
'assistant_helpfulness',
|
|
87
|
+
'session_type',
|
|
88
|
+
'friction_counts',
|
|
89
|
+
'friction_detail',
|
|
90
|
+
'primary_success',
|
|
91
|
+
'brief_summary',
|
|
92
|
+
'user_instructions',
|
|
93
|
+
],
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const PROJECT_AREAS_SCHEMA = {
|
|
97
|
+
type: 'object',
|
|
98
|
+
additionalProperties: false,
|
|
99
|
+
properties: {
|
|
100
|
+
areas: {
|
|
101
|
+
type: 'array',
|
|
102
|
+
items: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
additionalProperties: false,
|
|
105
|
+
properties: {
|
|
106
|
+
name: { type: 'string' },
|
|
107
|
+
session_count: { type: 'number' },
|
|
108
|
+
description: { type: 'string' },
|
|
109
|
+
},
|
|
110
|
+
required: ['name', 'session_count', 'description'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
required: ['areas'],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const INTERACTION_STYLE_SCHEMA = {
|
|
118
|
+
type: 'object',
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
properties: {
|
|
121
|
+
narrative: { type: 'string' },
|
|
122
|
+
key_pattern: { type: 'string' },
|
|
123
|
+
},
|
|
124
|
+
required: ['narrative', 'key_pattern'],
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const WHAT_WORKS_SCHEMA = {
|
|
128
|
+
type: 'object',
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
properties: {
|
|
131
|
+
intro: { type: 'string' },
|
|
132
|
+
impressive_workflows: {
|
|
133
|
+
type: 'array',
|
|
134
|
+
items: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
additionalProperties: false,
|
|
137
|
+
properties: {
|
|
138
|
+
title: { type: 'string' },
|
|
139
|
+
description: { type: 'string' },
|
|
140
|
+
},
|
|
141
|
+
required: ['title', 'description'],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
required: ['intro', 'impressive_workflows'],
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const FRICTION_SCHEMA = {
|
|
149
|
+
type: 'object',
|
|
150
|
+
additionalProperties: false,
|
|
151
|
+
properties: {
|
|
152
|
+
intro: { type: 'string' },
|
|
153
|
+
categories: {
|
|
154
|
+
type: 'array',
|
|
155
|
+
items: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
additionalProperties: false,
|
|
158
|
+
properties: {
|
|
159
|
+
category: { type: 'string' },
|
|
160
|
+
description: { type: 'string' },
|
|
161
|
+
examples: {
|
|
162
|
+
type: 'array',
|
|
163
|
+
items: { type: 'string' },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
required: ['category', 'description', 'examples'],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
required: ['intro', 'categories'],
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const SUGGESTIONS_SCHEMA = {
|
|
174
|
+
type: 'object',
|
|
175
|
+
additionalProperties: false,
|
|
176
|
+
properties: {
|
|
177
|
+
agents_md_additions: {
|
|
178
|
+
type: 'array',
|
|
179
|
+
items: {
|
|
180
|
+
type: 'object',
|
|
181
|
+
additionalProperties: false,
|
|
182
|
+
properties: {
|
|
183
|
+
addition: { type: 'string' },
|
|
184
|
+
why: { type: 'string' },
|
|
185
|
+
prompt_scaffold: { type: 'string' },
|
|
186
|
+
},
|
|
187
|
+
required: ['addition', 'why', 'prompt_scaffold'],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
features_to_try: {
|
|
191
|
+
type: 'array',
|
|
192
|
+
items: {
|
|
193
|
+
type: 'object',
|
|
194
|
+
additionalProperties: false,
|
|
195
|
+
properties: {
|
|
196
|
+
feature: { type: 'string' },
|
|
197
|
+
one_liner: { type: 'string' },
|
|
198
|
+
why_for_you: { type: 'string' },
|
|
199
|
+
example_code: { type: 'string' },
|
|
200
|
+
},
|
|
201
|
+
required: ['feature', 'one_liner', 'why_for_you', 'example_code'],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
usage_patterns: {
|
|
205
|
+
type: 'array',
|
|
206
|
+
items: {
|
|
207
|
+
type: 'object',
|
|
208
|
+
additionalProperties: false,
|
|
209
|
+
properties: {
|
|
210
|
+
title: { type: 'string' },
|
|
211
|
+
suggestion: { type: 'string' },
|
|
212
|
+
detail: { type: 'string' },
|
|
213
|
+
copyable_prompt: { type: 'string' },
|
|
214
|
+
},
|
|
215
|
+
required: ['title', 'suggestion', 'detail', 'copyable_prompt'],
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
required: ['agents_md_additions', 'features_to_try', 'usage_patterns'],
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const HORIZON_SCHEMA = {
|
|
223
|
+
type: 'object',
|
|
224
|
+
additionalProperties: false,
|
|
225
|
+
properties: {
|
|
226
|
+
intro: { type: 'string' },
|
|
227
|
+
opportunities: {
|
|
228
|
+
type: 'array',
|
|
229
|
+
items: {
|
|
230
|
+
type: 'object',
|
|
231
|
+
additionalProperties: false,
|
|
232
|
+
properties: {
|
|
233
|
+
title: { type: 'string' },
|
|
234
|
+
whats_possible: { type: 'string' },
|
|
235
|
+
how_to_try: { type: 'string' },
|
|
236
|
+
copyable_prompt: { type: 'string' },
|
|
237
|
+
},
|
|
238
|
+
required: ['title', 'whats_possible', 'how_to_try', 'copyable_prompt'],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
required: ['intro', 'opportunities'],
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const FUN_ENDING_SCHEMA = {
|
|
246
|
+
type: 'object',
|
|
247
|
+
additionalProperties: false,
|
|
248
|
+
properties: {
|
|
249
|
+
headline: { type: 'string' },
|
|
250
|
+
detail: { type: 'string' },
|
|
251
|
+
},
|
|
252
|
+
required: ['headline', 'detail'],
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const AT_A_GLANCE_SCHEMA = {
|
|
256
|
+
type: 'object',
|
|
257
|
+
additionalProperties: false,
|
|
258
|
+
properties: {
|
|
259
|
+
whats_working: { type: 'string' },
|
|
260
|
+
whats_hindering: { type: 'string' },
|
|
261
|
+
quick_wins: { type: 'string' },
|
|
262
|
+
ambitious_workflows: { type: 'string' },
|
|
263
|
+
},
|
|
264
|
+
required: [
|
|
265
|
+
'whats_working',
|
|
266
|
+
'whats_hindering',
|
|
267
|
+
'quick_wins',
|
|
268
|
+
'ambitious_workflows',
|
|
269
|
+
],
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const SECTION_DEFS = [
|
|
273
|
+
{
|
|
274
|
+
name: 'project_areas',
|
|
275
|
+
modelTier: 'fast',
|
|
276
|
+
contextKind: 'project_areas',
|
|
277
|
+
schemaName: 'codex_project_areas',
|
|
278
|
+
schema: PROJECT_AREAS_SCHEMA,
|
|
279
|
+
prompt: `Analyze this Codex usage data and identify project areas.
|
|
280
|
+
|
|
281
|
+
RESPOND WITH ONLY A VALID JSON OBJECT:
|
|
282
|
+
{
|
|
283
|
+
"areas": [
|
|
284
|
+
{"name": "Area name", "session_count": 0, "description": "2-3 sentences about what was worked on and how Codex was used."}
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
Include 4-5 areas. Skip Codex self-hosting/meta work unless it is a dominant project area.
|
|
289
|
+
|
|
290
|
+
Guardrails:
|
|
291
|
+
- Use concrete project or workstream names, not generic labels like "coding" or "development"
|
|
292
|
+
- Base areas on repeated evidence across summaries, not one-off threads
|
|
293
|
+
- Prefer project + task framing over tool-centric framing
|
|
294
|
+
- Group related tasks into a coherent workstream instead of listing each task separately
|
|
295
|
+
- Each description should sound like a mini report paragraph: what kinds of work clustered together, then how Codex contributed
|
|
296
|
+
- Prefer descriptions that mention representative tasks or artifacts instead of vague labels`,
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'interaction_style',
|
|
300
|
+
modelTier: 'full',
|
|
301
|
+
contextKind: 'interaction_style',
|
|
302
|
+
schemaName: 'codex_interaction_style',
|
|
303
|
+
schema: INTERACTION_STYLE_SCHEMA,
|
|
304
|
+
prompt: `Analyze this Codex usage data and describe the user's interaction style.
|
|
305
|
+
|
|
306
|
+
RESPOND WITH ONLY A VALID JSON OBJECT:
|
|
307
|
+
{
|
|
308
|
+
"narrative": "2-3 paragraphs analyzing how the user interacts with Codex. Use second person. Focus on planning style, interruption pattern, trust in execution, and how they balance exploration vs edits.",
|
|
309
|
+
"key_pattern": "One sentence summary of the most distinctive interaction pattern"
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
Guardrails:
|
|
313
|
+
- Focus on stable interaction patterns, not isolated moments
|
|
314
|
+
- Talk about how the user scopes work, interrupts, redirects, or trusts execution
|
|
315
|
+
- Do not infer user preference from Codex's default tool mix or harness behavior; high exec/tool usage can reflect the agent's operating style rather than the user's instructions
|
|
316
|
+
- Treat shell usage, file reads, and verification commands as weak evidence unless the user explicitly asked for that working style
|
|
317
|
+
- If evidence is mixed, describe the tension instead of forcing one clean story`,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: 'what_works',
|
|
321
|
+
modelTier: 'fast',
|
|
322
|
+
contextKind: 'what_works',
|
|
323
|
+
schemaName: 'codex_what_works',
|
|
324
|
+
schema: WHAT_WORKS_SCHEMA,
|
|
325
|
+
prompt: `Analyze this Codex usage data and identify what is working well. Use second person.
|
|
326
|
+
|
|
327
|
+
RESPOND WITH ONLY A VALID JSON OBJECT:
|
|
328
|
+
{
|
|
329
|
+
"intro": "1 sentence of context",
|
|
330
|
+
"impressive_workflows": [
|
|
331
|
+
{"title": "Short title", "description": "2-3 sentences describing the workflow or habit that works well."}
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
Include 3 impressive workflows.
|
|
336
|
+
|
|
337
|
+
Guardrails:
|
|
338
|
+
- Only include workflows supported by repeated evidence or clearly strong outcomes
|
|
339
|
+
- Prefer user habits and collaboration patterns over tool name lists
|
|
340
|
+
- Avoid generic praise; explain why the workflow works`,
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: 'friction_analysis',
|
|
344
|
+
modelTier: 'full',
|
|
345
|
+
contextKind: 'friction_analysis',
|
|
346
|
+
schemaName: 'codex_friction_analysis',
|
|
347
|
+
schema: FRICTION_SCHEMA,
|
|
348
|
+
prompt: `Analyze this Codex usage data and identify friction points. Use second person.
|
|
349
|
+
|
|
350
|
+
RESPOND WITH ONLY A VALID JSON OBJECT:
|
|
351
|
+
{
|
|
352
|
+
"intro": "1 sentence summarizing the friction pattern",
|
|
353
|
+
"categories": [
|
|
354
|
+
{"category": "Concrete category name", "description": "1-2 sentences describing the pattern", "examples": ["specific example", "another example"]}
|
|
355
|
+
]
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
Include 3 friction categories with 2 examples each.
|
|
359
|
+
|
|
360
|
+
Guardrails:
|
|
361
|
+
- Separate model-side friction from user/workflow-side friction when useful
|
|
362
|
+
- Examples must be concrete and tied to the supplied evidence
|
|
363
|
+
- Do not invent root causes that are not visible in the data`,
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
name: 'suggestions',
|
|
367
|
+
modelTier: 'fast',
|
|
368
|
+
contextKind: 'suggestions',
|
|
369
|
+
schemaName: 'codex_suggestions',
|
|
370
|
+
schema: SUGGESTIONS_SCHEMA,
|
|
371
|
+
prompt: `Analyze this Codex usage data and suggest improvements that the user can immediately act on.
|
|
372
|
+
|
|
373
|
+
## CODEX FEATURES REFERENCE
|
|
374
|
+
1. **MCP Servers**
|
|
375
|
+
- Good for databases, GitHub/Linear, internal APIs, external tools
|
|
376
|
+
2. **Skills**
|
|
377
|
+
- Reusable local workflows triggered by intent or explicit skill usage
|
|
378
|
+
3. **codex exec**
|
|
379
|
+
- Non-interactive Codex runs for scripts, CI, and repeatable workflows
|
|
380
|
+
4. **Sub-agents**
|
|
381
|
+
- Split bounded work across focused agents when tasks can run independently
|
|
382
|
+
5. **AGENTS.md**
|
|
383
|
+
- Persistent repo instructions so repeated context does not need to be restated
|
|
384
|
+
|
|
385
|
+
RESPOND WITH ONLY A VALID JSON OBJECT:
|
|
386
|
+
{
|
|
387
|
+
"agents_md_additions": [
|
|
388
|
+
{"addition": "A specific AGENTS.md addition", "why": "Why this would help based on repeated sessions", "prompt_scaffold": "Where to place it"}
|
|
389
|
+
],
|
|
390
|
+
"features_to_try": [
|
|
391
|
+
{"feature": "Feature name", "one_liner": "What it does", "why_for_you": "Why it helps this user", "example_code": "Copyable command, config, or snippet"}
|
|
392
|
+
],
|
|
393
|
+
"usage_patterns": [
|
|
394
|
+
{"title": "Short title", "suggestion": "1 sentence telling the user what to do", "detail": "2-4 sentences explaining why now and how it helps", "copyable_prompt": "Specific prompt to paste into Codex"}
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
Prioritize suggestions grounded in repeated patterns, not isolated sessions.
|
|
399
|
+
|
|
400
|
+
Guardrails:
|
|
401
|
+
- Suggest only actions that clearly connect to repeated evidence
|
|
402
|
+
- Avoid generic advice like "give more context" unless it is overwhelmingly justified
|
|
403
|
+
- Prefer changes with strong leverage: repo memory, repeatable workflows, automation, or parallelism
|
|
404
|
+
- Write AGENTS.md additions as directly pasteable instruction lines, not commentary about instructions
|
|
405
|
+
- Make feature examples immediately usable; avoid placeholders like "insert your repo path here" unless unavoidable
|
|
406
|
+
- Make usage pattern suggestions sound like concrete next actions the user can try today, not abstract best practices`,
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: 'on_the_horizon',
|
|
410
|
+
modelTier: 'full',
|
|
411
|
+
contextKind: 'on_the_horizon',
|
|
412
|
+
schemaName: 'codex_on_the_horizon',
|
|
413
|
+
schema: HORIZON_SCHEMA,
|
|
414
|
+
prompt: `Analyze this Codex usage data and identify future opportunities that are ambitious but still actionable.
|
|
415
|
+
|
|
416
|
+
RESPOND WITH ONLY A VALID JSON OBJECT:
|
|
417
|
+
{
|
|
418
|
+
"intro": "1 sentence about how the user's workflows could evolve",
|
|
419
|
+
"opportunities": [
|
|
420
|
+
{"title": "Short title", "whats_possible": "2-3 ambitious sentences", "how_to_try": "1-2 sentences mentioning concrete tooling", "copyable_prompt": "Detailed prompt to try"}
|
|
421
|
+
]
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
Include 3 opportunities. Think in terms of stronger multi-step execution, persistent repo memory, and parallel work.
|
|
425
|
+
|
|
426
|
+
Guardrails:
|
|
427
|
+
- Stay adjacent to the user's actual workflows; do not jump to fantasies detached from evidence
|
|
428
|
+
- Push toward ambitious but plausible next-step workflows
|
|
429
|
+
- Mention concrete Codex capabilities when relevant, not vague future AI claims
|
|
430
|
+
- The "how_to_try" field should read like a getting-started instruction, not a vague observation
|
|
431
|
+
- The "copyable_prompt" should be detailed enough that the user could paste it into Codex with minimal edits`,
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: 'fun_ending',
|
|
435
|
+
modelTier: 'fast',
|
|
436
|
+
contextKind: 'fun_ending',
|
|
437
|
+
schemaName: 'codex_fun_ending',
|
|
438
|
+
schema: FUN_ENDING_SCHEMA,
|
|
439
|
+
prompt: `Analyze this Codex usage data and find one memorable qualitative moment.
|
|
440
|
+
|
|
441
|
+
RESPOND WITH ONLY A VALID JSON OBJECT:
|
|
442
|
+
{
|
|
443
|
+
"headline": "A memorable moment from the sessions",
|
|
444
|
+
"detail": "Brief context"
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
Pick something human, funny, or surprising, not a statistic.
|
|
448
|
+
|
|
449
|
+
Guardrails:
|
|
450
|
+
- Prefer a memorable pattern or moment visible in the supplied summaries
|
|
451
|
+
- Do not fabricate narrative detail that is not present in the data`,
|
|
452
|
+
},
|
|
453
|
+
]
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* @param {{ threadSummaries: any[], options?: InsightRunOptions }} param0
|
|
457
|
+
*/
|
|
458
|
+
export async function generateLlmInsights({ threadSummaries, options = {} }) {
|
|
459
|
+
const provider = options.provider || DEFAULT_PROVIDER
|
|
460
|
+
const usageTracker = createUsageTracker(provider)
|
|
461
|
+
const providerOptions = buildProviderOptions(options, provider, usage => usageTracker.add(usage))
|
|
462
|
+
const facetModel = options.facetModel || DEFAULT_FACET_MODEL
|
|
463
|
+
const fastSectionModel = options.fastSectionModel || DEFAULT_FAST_SECTION_MODEL
|
|
464
|
+
const insightModel = options.insightModel || DEFAULT_INSIGHT_MODEL
|
|
465
|
+
const facetEffort = options.facetEffort || DEFAULT_FACET_EFFORT
|
|
466
|
+
const fastSectionEffort = options.fastSectionEffort || DEFAULT_FAST_SECTION_EFFORT
|
|
467
|
+
const insightEffort = options.insightEffort || DEFAULT_INSIGHT_EFFORT
|
|
468
|
+
const cacheRoot = resolveCacheRoot(options.cacheDir)
|
|
469
|
+
const facetCacheDir = path.join(cacheRoot, 'facets')
|
|
470
|
+
await fs.mkdir(facetCacheDir, { recursive: true })
|
|
471
|
+
|
|
472
|
+
const candidateThreads = filterSubstantiveThreads(threadSummaries)
|
|
473
|
+
const facetLimit = Math.min(
|
|
474
|
+
Number(options.facetLimit ?? DEFAULT_FACET_LIMIT),
|
|
475
|
+
MAX_FACET_EXTRACTIONS,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
const facetJobs = await planFacetJobs(candidateThreads, {
|
|
479
|
+
cacheDir: facetCacheDir,
|
|
480
|
+
model: facetModel,
|
|
481
|
+
uncachedLimit: facetLimit,
|
|
482
|
+
})
|
|
483
|
+
emitProgress(options, {
|
|
484
|
+
kind: 'facets:planned',
|
|
485
|
+
total: facetJobs.length,
|
|
486
|
+
uncached: facetJobs.filter(job => !job.cachedFacet).length,
|
|
487
|
+
cached: facetJobs.filter(job => Boolean(job.cachedFacet)).length,
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
let completedFacetJobs = 0
|
|
491
|
+
const rawFacets = await mapLimit(facetJobs, 4, async job => {
|
|
492
|
+
const facet =
|
|
493
|
+
job.cachedFacet ||
|
|
494
|
+
(await getFacetForThread(job.thread, {
|
|
495
|
+
cacheDir: facetCacheDir,
|
|
496
|
+
model: facetModel,
|
|
497
|
+
provider,
|
|
498
|
+
providerOptions,
|
|
499
|
+
}))
|
|
500
|
+
completedFacetJobs += 1
|
|
501
|
+
emitProgress(options, {
|
|
502
|
+
kind: 'facets:progress',
|
|
503
|
+
completed: completedFacetJobs,
|
|
504
|
+
total: facetJobs.length,
|
|
505
|
+
cached: Boolean(job.cachedFacet),
|
|
506
|
+
})
|
|
507
|
+
return facet
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
const minimalThreadIds = new Set(
|
|
511
|
+
rawFacets.filter(isWarmupMinimalFacet).map(facet => facet.threadId),
|
|
512
|
+
)
|
|
513
|
+
const reportThreads = candidateThreads.filter(thread => !minimalThreadIds.has(thread.id))
|
|
514
|
+
const facets = rawFacets.filter(facet => !minimalThreadIds.has(facet.threadId))
|
|
515
|
+
const report = buildReport(reportThreads, { threadPreviewLimit: 50 })
|
|
516
|
+
const context = buildInsightContext(report, reportThreads, facets)
|
|
517
|
+
|
|
518
|
+
emitProgress(options, {
|
|
519
|
+
kind: 'sections:planned',
|
|
520
|
+
total: SECTION_DEFS.length + 1,
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
let completedSections = 0
|
|
524
|
+
const sectionResults = await mapLimit(SECTION_DEFS, SECTION_CONCURRENCY, async section => {
|
|
525
|
+
const sectionContext = buildSectionContext(context, section.contextKind)
|
|
526
|
+
const result = await callStructuredModel({
|
|
527
|
+
provider,
|
|
528
|
+
model: resolveSectionModel(section, { fastSectionModel, insightModel }),
|
|
529
|
+
schemaName: section.schemaName,
|
|
530
|
+
schema: section.schema,
|
|
531
|
+
systemPrompt: `${SECTION_SYSTEM_PROMPT} ${getNarrativeLanguageInstruction(options.lang)}`.trim(),
|
|
532
|
+
userPrompt: `${section.prompt}\n\n${getNarrativeLanguageInstruction(options.lang)}\n\nDATA:\n${compactJson(sectionContext)}`,
|
|
533
|
+
options: {
|
|
534
|
+
...providerOptions,
|
|
535
|
+
fallbackModels: resolveModelFallbacks(
|
|
536
|
+
resolveSectionModel(section, { fastSectionModel, insightModel }),
|
|
537
|
+
),
|
|
538
|
+
usageStage: `section:${section.name}`,
|
|
539
|
+
reasoningEffort: resolveSectionEffort(section, { fastSectionEffort, insightEffort }),
|
|
540
|
+
},
|
|
541
|
+
})
|
|
542
|
+
completedSections += 1
|
|
543
|
+
emitProgress(options, {
|
|
544
|
+
kind: 'sections:progress',
|
|
545
|
+
completed: completedSections,
|
|
546
|
+
total: SECTION_DEFS.length + 1,
|
|
547
|
+
section: section.name,
|
|
548
|
+
})
|
|
549
|
+
return { name: section.name, result }
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
const insights = {}
|
|
553
|
+
for (const section of sectionResults) {
|
|
554
|
+
insights[section.name] = section.result
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const atAGlance = await callStructuredModel({
|
|
558
|
+
provider,
|
|
559
|
+
model: insightModel,
|
|
560
|
+
schemaName: 'codex_at_a_glance',
|
|
561
|
+
schema: AT_A_GLANCE_SCHEMA,
|
|
562
|
+
systemPrompt: `${AT_A_GLANCE_SYSTEM_PROMPT} ${getNarrativeLanguageInstruction(options.lang)}`.trim(),
|
|
563
|
+
userPrompt: buildAtAGlancePrompt(buildSectionContext(context, 'at_a_glance'), insights),
|
|
564
|
+
options: {
|
|
565
|
+
...providerOptions,
|
|
566
|
+
fallbackModels: resolveModelFallbacks(insightModel),
|
|
567
|
+
usageStage: 'section:at_a_glance',
|
|
568
|
+
reasoningEffort: insightEffort,
|
|
569
|
+
},
|
|
570
|
+
})
|
|
571
|
+
completedSections += 1
|
|
572
|
+
emitProgress(options, {
|
|
573
|
+
kind: 'sections:progress',
|
|
574
|
+
completed: completedSections,
|
|
575
|
+
total: SECTION_DEFS.length + 1,
|
|
576
|
+
section: 'at_a_glance',
|
|
577
|
+
})
|
|
578
|
+
insights.at_a_glance = atAGlance
|
|
579
|
+
|
|
580
|
+
return { insights, facets, reportThreads, analysisUsage: usageTracker.snapshot() }
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* @param {{ threadSummaries: any[], options?: InsightRunOptions }} param0
|
|
585
|
+
*/
|
|
586
|
+
export async function estimateLlmAnalysisCost({ threadSummaries, options = {} }) {
|
|
587
|
+
const facetModel = options.facetModel || DEFAULT_FACET_MODEL
|
|
588
|
+
const fastSectionModel = options.fastSectionModel || DEFAULT_FAST_SECTION_MODEL
|
|
589
|
+
const insightModel = options.insightModel || DEFAULT_INSIGHT_MODEL
|
|
590
|
+
const cacheRoot = resolveCacheRoot(options.cacheDir)
|
|
591
|
+
const facetCacheDir = path.join(cacheRoot, 'facets')
|
|
592
|
+
await fs.mkdir(facetCacheDir, { recursive: true })
|
|
593
|
+
|
|
594
|
+
const candidateThreads = filterSubstantiveThreads(threadSummaries)
|
|
595
|
+
const facetLimit = Math.min(
|
|
596
|
+
Number(options.facetLimit ?? DEFAULT_FACET_LIMIT),
|
|
597
|
+
MAX_FACET_EXTRACTIONS,
|
|
598
|
+
)
|
|
599
|
+
const facetJobs = await planFacetJobs(candidateThreads, {
|
|
600
|
+
cacheDir: facetCacheDir,
|
|
601
|
+
model: facetModel,
|
|
602
|
+
uncachedLimit: facetLimit,
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
const uncachedFacetJobs = facetJobs.filter(job => !job.cachedFacet)
|
|
606
|
+
const cachedFacetJobs = facetJobs.length - uncachedFacetJobs.length
|
|
607
|
+
let chunkSummaryCalls = 0
|
|
608
|
+
let combineSummaryCalls = 0
|
|
609
|
+
let estimatedFacetInputTokens = 0
|
|
610
|
+
let estimatedFacetOutputTokens = 0
|
|
611
|
+
let estimatedSummaryInputTokens = 0
|
|
612
|
+
let estimatedSummaryOutputTokens = 0
|
|
613
|
+
|
|
614
|
+
for (const job of uncachedFacetJobs) {
|
|
615
|
+
const transcript = String(job.thread.transcriptForAnalysis || '').trim()
|
|
616
|
+
const transcriptChars = transcript.length
|
|
617
|
+
if (!transcriptChars) {
|
|
618
|
+
estimatedFacetInputTokens += 600
|
|
619
|
+
estimatedFacetOutputTokens += 350
|
|
620
|
+
continue
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (transcriptChars <= LONG_TRANSCRIPT_THRESHOLD) {
|
|
624
|
+
estimatedFacetInputTokens += estimateTokensFromChars(transcriptChars) + 1200
|
|
625
|
+
estimatedFacetOutputTokens += 350
|
|
626
|
+
continue
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const chunks = chunkText(transcript, TRANSCRIPT_CHUNK_SIZE)
|
|
630
|
+
chunkSummaryCalls += chunks.length
|
|
631
|
+
for (const chunk of chunks) {
|
|
632
|
+
estimatedSummaryInputTokens += estimateTokensFromChars(chunk.length) + 220
|
|
633
|
+
estimatedSummaryOutputTokens += 260
|
|
634
|
+
}
|
|
635
|
+
const combinedSummaryChars = chunks.length * 1100
|
|
636
|
+
if (combinedSummaryChars > LONG_TRANSCRIPT_THRESHOLD) {
|
|
637
|
+
combineSummaryCalls += 1
|
|
638
|
+
estimatedSummaryInputTokens += estimateTokensFromChars(combinedSummaryChars) + 180
|
|
639
|
+
estimatedSummaryOutputTokens += 320
|
|
640
|
+
}
|
|
641
|
+
estimatedFacetInputTokens += 2400
|
|
642
|
+
estimatedFacetOutputTokens += 350
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const estimatedSectionInputs = estimateSectionInputs(candidateThreads, facetJobs)
|
|
646
|
+
const fastSectionCalls = SECTION_DEFS.filter(section => section.modelTier === 'fast').length
|
|
647
|
+
const fullSectionCalls = SECTION_DEFS.filter(section => section.modelTier !== 'fast').length
|
|
648
|
+
const estimatedFastSectionInputTokens = SECTION_DEFS.filter(section => section.modelTier === 'fast')
|
|
649
|
+
.reduce((sum, section) => sum + estimatedSectionInputs[section.contextKind] + 500, 0)
|
|
650
|
+
const estimatedFastSectionOutputTokens = fastSectionCalls * 500
|
|
651
|
+
const estimatedFullSectionInputTokens = SECTION_DEFS.filter(section => section.modelTier !== 'fast')
|
|
652
|
+
.reduce((sum, section) => sum + estimatedSectionInputs[section.contextKind] + 650, 0)
|
|
653
|
+
const estimatedFullSectionOutputTokens = fullSectionCalls * 700
|
|
654
|
+
const estimatedAtAGlanceInputTokens = estimatedSectionInputs.at_a_glance + 2200
|
|
655
|
+
const estimatedAtAGlanceOutputTokens = 260
|
|
656
|
+
|
|
657
|
+
const byStage = [
|
|
658
|
+
buildEstimateBucket(
|
|
659
|
+
'facet_extraction',
|
|
660
|
+
uncachedFacetJobs.length,
|
|
661
|
+
estimatedFacetInputTokens,
|
|
662
|
+
0,
|
|
663
|
+
estimatedFacetOutputTokens,
|
|
664
|
+
),
|
|
665
|
+
buildEstimateBucket(
|
|
666
|
+
'transcript_summary:chunk',
|
|
667
|
+
chunkSummaryCalls,
|
|
668
|
+
estimatedSummaryInputTokens - (combineSummaryCalls ? estimateTokensFromChars(combineSummaryCalls * 1100) + combineSummaryCalls * 180 : 0),
|
|
669
|
+
0,
|
|
670
|
+
estimatedSummaryOutputTokens - combineSummaryCalls * 320,
|
|
671
|
+
),
|
|
672
|
+
buildEstimateBucket(
|
|
673
|
+
'transcript_summary:combine',
|
|
674
|
+
combineSummaryCalls,
|
|
675
|
+
combineSummaryCalls ? estimateTokensFromChars(combineSummaryCalls * 1100) + combineSummaryCalls * 180 : 0,
|
|
676
|
+
0,
|
|
677
|
+
combineSummaryCalls * 320,
|
|
678
|
+
),
|
|
679
|
+
buildEstimateBucket(
|
|
680
|
+
'section:fast',
|
|
681
|
+
fastSectionCalls,
|
|
682
|
+
estimatedFastSectionInputTokens,
|
|
683
|
+
0,
|
|
684
|
+
estimatedFastSectionOutputTokens,
|
|
685
|
+
),
|
|
686
|
+
buildEstimateBucket(
|
|
687
|
+
'section:full',
|
|
688
|
+
fullSectionCalls,
|
|
689
|
+
estimatedFullSectionInputTokens,
|
|
690
|
+
0,
|
|
691
|
+
estimatedFullSectionOutputTokens,
|
|
692
|
+
),
|
|
693
|
+
buildEstimateBucket(
|
|
694
|
+
'section:at_a_glance',
|
|
695
|
+
1,
|
|
696
|
+
estimatedAtAGlanceInputTokens,
|
|
697
|
+
0,
|
|
698
|
+
estimatedAtAGlanceOutputTokens,
|
|
699
|
+
),
|
|
700
|
+
].filter(bucket => bucket.calls > 0)
|
|
701
|
+
|
|
702
|
+
const byModel = aggregateEstimateByModel([
|
|
703
|
+
{
|
|
704
|
+
label: facetModel,
|
|
705
|
+
calls: uncachedFacetJobs.length + chunkSummaryCalls + combineSummaryCalls,
|
|
706
|
+
inputTokens: estimatedFacetInputTokens + estimatedSummaryInputTokens,
|
|
707
|
+
cachedInputTokens: 0,
|
|
708
|
+
outputTokens: estimatedFacetOutputTokens + estimatedSummaryOutputTokens,
|
|
709
|
+
totalTokens:
|
|
710
|
+
estimatedFacetInputTokens +
|
|
711
|
+
estimatedSummaryInputTokens +
|
|
712
|
+
estimatedFacetOutputTokens +
|
|
713
|
+
estimatedSummaryOutputTokens,
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
label: fastSectionModel,
|
|
717
|
+
calls: fastSectionCalls,
|
|
718
|
+
inputTokens: estimatedFastSectionInputTokens,
|
|
719
|
+
cachedInputTokens: 0,
|
|
720
|
+
outputTokens: estimatedFastSectionOutputTokens,
|
|
721
|
+
totalTokens: estimatedFastSectionInputTokens + estimatedFastSectionOutputTokens,
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
label: insightModel,
|
|
725
|
+
calls: fullSectionCalls + 1,
|
|
726
|
+
inputTokens: estimatedFullSectionInputTokens + estimatedAtAGlanceInputTokens,
|
|
727
|
+
cachedInputTokens: 0,
|
|
728
|
+
outputTokens: estimatedFullSectionOutputTokens + estimatedAtAGlanceOutputTokens,
|
|
729
|
+
totalTokens:
|
|
730
|
+
estimatedFullSectionInputTokens +
|
|
731
|
+
estimatedAtAGlanceInputTokens +
|
|
732
|
+
estimatedFullSectionOutputTokens +
|
|
733
|
+
estimatedAtAGlanceOutputTokens,
|
|
734
|
+
},
|
|
735
|
+
])
|
|
736
|
+
|
|
737
|
+
const totalInputTokens =
|
|
738
|
+
estimatedFacetInputTokens +
|
|
739
|
+
estimatedSummaryInputTokens +
|
|
740
|
+
estimatedFastSectionInputTokens +
|
|
741
|
+
estimatedFullSectionInputTokens +
|
|
742
|
+
estimatedAtAGlanceInputTokens
|
|
743
|
+
const totalOutputTokens =
|
|
744
|
+
estimatedFacetOutputTokens +
|
|
745
|
+
estimatedSummaryOutputTokens +
|
|
746
|
+
estimatedFastSectionOutputTokens +
|
|
747
|
+
estimatedFullSectionOutputTokens +
|
|
748
|
+
estimatedAtAGlanceOutputTokens
|
|
749
|
+
const totalTokens = totalInputTokens + totalOutputTokens
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
provider: options.provider || DEFAULT_PROVIDER,
|
|
753
|
+
candidateThreads: candidateThreads.length,
|
|
754
|
+
plannedFacetThreads: facetJobs.length,
|
|
755
|
+
uncachedFacetThreads: uncachedFacetJobs.length,
|
|
756
|
+
cachedFacetThreads: cachedFacetJobs,
|
|
757
|
+
longTranscriptThreads: uncachedFacetJobs.filter(job => String(job.thread.transcriptForAnalysis || '').trim().length > LONG_TRANSCRIPT_THRESHOLD).length,
|
|
758
|
+
estimatedCalls:
|
|
759
|
+
uncachedFacetJobs.length +
|
|
760
|
+
chunkSummaryCalls +
|
|
761
|
+
combineSummaryCalls +
|
|
762
|
+
fastSectionCalls +
|
|
763
|
+
fullSectionCalls +
|
|
764
|
+
1,
|
|
765
|
+
estimatedInputTokens: Math.round(totalInputTokens),
|
|
766
|
+
estimatedOutputTokens: Math.round(totalOutputTokens),
|
|
767
|
+
estimatedTotalTokens: Math.round(totalTokens),
|
|
768
|
+
estimatedRange: {
|
|
769
|
+
low: Math.round(totalTokens * 0.8),
|
|
770
|
+
high: Math.round(totalTokens * 1.35),
|
|
771
|
+
},
|
|
772
|
+
byStage,
|
|
773
|
+
byModel,
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
async function planFacetJobs(threadSummaries, { cacheDir, model, uncachedLimit }) {
|
|
778
|
+
const jobs = []
|
|
779
|
+
let uncachedCount = 0
|
|
780
|
+
|
|
781
|
+
for (const thread of threadSummaries) {
|
|
782
|
+
const cachedFacet = await readCachedFacet(thread, { cacheDir, model })
|
|
783
|
+
if (cachedFacet) {
|
|
784
|
+
jobs.push({ thread, cachedFacet })
|
|
785
|
+
continue
|
|
786
|
+
}
|
|
787
|
+
if (uncachedCount >= uncachedLimit) continue
|
|
788
|
+
uncachedCount += 1
|
|
789
|
+
jobs.push({ thread, cachedFacet: null })
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return jobs
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function readCachedFacet(thread, { cacheDir, model }) {
|
|
796
|
+
const cachePath = path.join(cacheDir, `${thread.id}.json`)
|
|
797
|
+
const cached = await readJson(cachePath)
|
|
798
|
+
if (!cached?.facet || cached.versionKey !== buildFacetVersionKey(thread, model)) {
|
|
799
|
+
return null
|
|
800
|
+
}
|
|
801
|
+
return cached.facet
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function getFacetForThread(thread, { cacheDir, model, provider, providerOptions }) {
|
|
805
|
+
const cachePath = path.join(cacheDir, `${thread.id}.json`)
|
|
806
|
+
const versionKey = buildFacetVersionKey(thread, model)
|
|
807
|
+
const cached = await readJson(cachePath)
|
|
808
|
+
if (cached?.versionKey === versionKey && cached?.facet) {
|
|
809
|
+
return cached.facet
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const transcript = await prepareTranscriptForFacetExtraction(thread, {
|
|
813
|
+
model,
|
|
814
|
+
provider,
|
|
815
|
+
providerOptions,
|
|
816
|
+
})
|
|
817
|
+
const prompt = `Analyze this Codex coding session and extract structured facets.
|
|
818
|
+
|
|
819
|
+
CRITICAL GUIDELINES:
|
|
820
|
+
1. goal_categories should count only what the user explicitly asked for.
|
|
821
|
+
2. user_satisfaction_counts should rely on explicit user signals or strong transcript evidence.
|
|
822
|
+
3. friction_counts should be specific: misunderstood_request, wrong_approach, buggy_code, user_rejected_action, excessive_changes, tool_failed, slow_or_verbose, user_unclear, external_issue.
|
|
823
|
+
4. If the session is mostly warmup, rehearsal, or cache-filling, use warmup_minimal as the only goal category.
|
|
824
|
+
5. If evidence is insufficient after transcript compression, use conservative values such as unclear_from_transcript rather than guessing.
|
|
825
|
+
6. Do not infer the user's goal from assistant or tool activity alone.
|
|
826
|
+
7. Do not count assistant-led exploration or extra implementation work unless the user clearly asked for it.
|
|
827
|
+
|
|
828
|
+
Allowed values:
|
|
829
|
+
- outcome: fully_achieved | mostly_achieved | partially_achieved | not_achieved | unclear_from_transcript
|
|
830
|
+
- assistant_helpfulness: unhelpful | slightly_helpful | moderately_helpful | very_helpful | essential
|
|
831
|
+
- session_type: single_task | multi_task | iterative_refinement | exploration | quick_question
|
|
832
|
+
- primary_success: none | fast_accurate_search | correct_code_edits | good_explanations | proactive_help | multi_file_changes | good_debugging
|
|
833
|
+
|
|
834
|
+
Language:
|
|
835
|
+
- Keep enum values and keys exactly as requested.
|
|
836
|
+
- Write free-text fields in ${describeLanguage(langFromProviderOptions(providerOptions))}.
|
|
837
|
+
|
|
838
|
+
Transcript:
|
|
839
|
+
${transcript}
|
|
840
|
+
|
|
841
|
+
Summary stats:
|
|
842
|
+
${JSON.stringify(
|
|
843
|
+
{
|
|
844
|
+
title: thread.title,
|
|
845
|
+
cwd: thread.cwd,
|
|
846
|
+
durationMinutes: thread.durationMinutes,
|
|
847
|
+
userMessages: thread.userMessages,
|
|
848
|
+
assistantMessages: thread.assistantMessages,
|
|
849
|
+
totalToolCalls: thread.totalToolCalls,
|
|
850
|
+
totalCommandFailures: thread.totalCommandFailures,
|
|
851
|
+
toolCounts: thread.toolCounts,
|
|
852
|
+
toolFailures: thread.toolFailures,
|
|
853
|
+
userInterruptions: thread.userInterruptions,
|
|
854
|
+
usesTaskAgent: thread.usesTaskAgent,
|
|
855
|
+
usesMcp: thread.usesMcp,
|
|
856
|
+
usesWebSearch: thread.usesWebSearch,
|
|
857
|
+
usesWebFetch: thread.usesWebFetch,
|
|
858
|
+
},
|
|
859
|
+
null,
|
|
860
|
+
2,
|
|
861
|
+
)}
|
|
862
|
+
|
|
863
|
+
RESPOND WITH ONLY A VALID JSON OBJECT matching the requested schema.`
|
|
864
|
+
|
|
865
|
+
const rawFacet = await callStructuredModel({
|
|
866
|
+
provider,
|
|
867
|
+
model,
|
|
868
|
+
schemaName: 'codex_session_facet',
|
|
869
|
+
schema: FACET_SCHEMA,
|
|
870
|
+
systemPrompt:
|
|
871
|
+
`You extract structured coding-session facets from compressed transcripts. Use only transcript evidence. Be conservative when evidence is weak. Do not infer intent from tool activity alone. ${getStructuredLanguageInstruction(langFromProviderOptions(providerOptions))}`.trim(),
|
|
872
|
+
userPrompt: prompt,
|
|
873
|
+
options: {
|
|
874
|
+
...providerOptions,
|
|
875
|
+
fallbackModels: resolveModelFallbacks(model),
|
|
876
|
+
usageStage: 'facet_extraction',
|
|
877
|
+
reasoningEffort: provider === 'codex-cli' ? providerOptions.facetEffort : undefined,
|
|
878
|
+
},
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
const facet = {
|
|
882
|
+
threadId: thread.id,
|
|
883
|
+
title: thread.title,
|
|
884
|
+
cwd: thread.cwd,
|
|
885
|
+
updatedAt: thread.updatedAt,
|
|
886
|
+
durationMinutes: thread.durationMinutes,
|
|
887
|
+
userMessages: thread.userMessages,
|
|
888
|
+
assistantMessages: thread.assistantMessages,
|
|
889
|
+
totalToolCalls: thread.totalToolCalls,
|
|
890
|
+
totalCommandFailures: thread.totalCommandFailures,
|
|
891
|
+
...rawFacet,
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
await fs.writeFile(cachePath, JSON.stringify({ versionKey, facet }, null, 2), 'utf8')
|
|
895
|
+
return facet
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async function prepareTranscriptForFacetExtraction(thread, { model, provider, providerOptions }) {
|
|
899
|
+
const transcript = String(thread.transcriptForAnalysis || '').trim()
|
|
900
|
+
if (!transcript) {
|
|
901
|
+
return `${thread.title || '(untitled)'}\n${thread.firstUserMessage || ''}`.trim()
|
|
902
|
+
}
|
|
903
|
+
if (transcript.length <= LONG_TRANSCRIPT_THRESHOLD) {
|
|
904
|
+
return transcript
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const chunks = chunkText(transcript, TRANSCRIPT_CHUNK_SIZE)
|
|
908
|
+
const chunkSummaries = await mapLimit(chunks, 3, async (chunk, index) =>
|
|
909
|
+
callTextModel({
|
|
910
|
+
provider,
|
|
911
|
+
model,
|
|
912
|
+
systemPrompt:
|
|
913
|
+
`${FACET_TRANSCRIPT_SUMMARY_DIRECTIVE}\n\nPreserve user goal, outcome, friction, command/tool issues, and what the assistant actually achieved.`,
|
|
914
|
+
userPrompt: `Chunk ${index + 1} of ${chunks.length}\n\n${chunk}`,
|
|
915
|
+
options: {
|
|
916
|
+
...providerOptions,
|
|
917
|
+
fallbackModels: resolveModelFallbacks(model),
|
|
918
|
+
usageStage: 'transcript_summary:chunk',
|
|
919
|
+
reasoningEffort: provider === 'codex-cli' ? providerOptions.facetEffort : undefined,
|
|
920
|
+
},
|
|
921
|
+
}),
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
const combined = chunkSummaries
|
|
925
|
+
.map((summary, index) => `Chunk ${index + 1} summary:\n${summary.trim()}`)
|
|
926
|
+
.join('\n\n')
|
|
927
|
+
|
|
928
|
+
if (combined.length <= LONG_TRANSCRIPT_THRESHOLD) {
|
|
929
|
+
return `[Long transcript summarized before facet extraction]\n${combined}`
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const finalSummary = await callTextModel({
|
|
933
|
+
provider,
|
|
934
|
+
model,
|
|
935
|
+
systemPrompt:
|
|
936
|
+
'Combine these coding-session chunk summaries into one compact transcript summary. Keep only material signal for later facet extraction. Do not carry boilerplate, stack traces, or command details.',
|
|
937
|
+
userPrompt: combined,
|
|
938
|
+
options: {
|
|
939
|
+
...providerOptions,
|
|
940
|
+
fallbackModels: resolveModelFallbacks(model),
|
|
941
|
+
usageStage: 'transcript_summary:combine',
|
|
942
|
+
reasoningEffort: provider === 'codex-cli' ? providerOptions.facetEffort : undefined,
|
|
943
|
+
},
|
|
944
|
+
})
|
|
945
|
+
|
|
946
|
+
return `[Long transcript summarized before facet extraction]\n${finalSummary.trim()}`
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function buildInsightContext(report, threadSummaries, facets) {
|
|
950
|
+
const goalCategories = {}
|
|
951
|
+
const outcomes = {}
|
|
952
|
+
const satisfaction = {}
|
|
953
|
+
const sessionTypes = {}
|
|
954
|
+
const friction = {}
|
|
955
|
+
const success = {}
|
|
956
|
+
|
|
957
|
+
for (const facet of facets) {
|
|
958
|
+
addCounts(goalCategories, facet.goal_categories)
|
|
959
|
+
addCounts(satisfaction, facet.user_satisfaction_counts)
|
|
960
|
+
addCounts(friction, facet.friction_counts)
|
|
961
|
+
if (facet.session_type) {
|
|
962
|
+
sessionTypes[facet.session_type] = (sessionTypes[facet.session_type] || 0) + 1
|
|
963
|
+
}
|
|
964
|
+
if (facet.outcome) outcomes[facet.outcome] = (outcomes[facet.outcome] || 0) + 1
|
|
965
|
+
if (facet.primary_success && facet.primary_success !== 'none') {
|
|
966
|
+
success[facet.primary_success] = (success[facet.primary_success] || 0) + 1
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const sortedFacets = [...facets].sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
|
|
971
|
+
const frictionDetails = sortedFacets
|
|
972
|
+
.filter(facet => facet.friction_detail && facet.friction_detail !== 'none')
|
|
973
|
+
.slice(0, MAX_FRICTION_DETAILS)
|
|
974
|
+
.map(facet => ({
|
|
975
|
+
threadId: facet.threadId,
|
|
976
|
+
title: facet.title,
|
|
977
|
+
cwd: facet.cwd,
|
|
978
|
+
friction_counts: facet.friction_counts,
|
|
979
|
+
friction_detail: facet.friction_detail,
|
|
980
|
+
outcome: facet.outcome,
|
|
981
|
+
}))
|
|
982
|
+
|
|
983
|
+
const userInstructions = Array.from(
|
|
984
|
+
new Set(
|
|
985
|
+
sortedFacets
|
|
986
|
+
.flatMap(facet => facet.user_instructions || [])
|
|
987
|
+
.map(sanitizeContextText)
|
|
988
|
+
.filter(Boolean),
|
|
989
|
+
),
|
|
990
|
+
).slice(0, MAX_USER_INSTRUCTIONS)
|
|
991
|
+
|
|
992
|
+
return {
|
|
993
|
+
metadata: {
|
|
994
|
+
generated_at: report.metadata.generatedAt,
|
|
995
|
+
thread_count: report.metadata.threadCount,
|
|
996
|
+
date_range: report.metadata.dateRange,
|
|
997
|
+
},
|
|
998
|
+
summary: {
|
|
999
|
+
total_user_messages: report.summary.totalUserMessages,
|
|
1000
|
+
total_tool_calls: report.summary.totalToolCalls,
|
|
1001
|
+
total_failures: report.summary.totalFailures,
|
|
1002
|
+
total_duration_hours: report.summary.totalDurationHours,
|
|
1003
|
+
total_tokens: report.summary.totalTokens,
|
|
1004
|
+
average_response_time_seconds: report.summary.averageResponseTimeSeconds,
|
|
1005
|
+
total_git_commits: report.summary.totalGitCommits,
|
|
1006
|
+
total_tool_errors: report.summary.totalToolErrors,
|
|
1007
|
+
total_files_modified: report.summary.totalFilesModified,
|
|
1008
|
+
total_lines_added: report.summary.totalLinesAdded,
|
|
1009
|
+
total_lines_removed: report.summary.totalLinesRemoved,
|
|
1010
|
+
overlap: report.summary.overlap,
|
|
1011
|
+
},
|
|
1012
|
+
charts: {
|
|
1013
|
+
projects: report.charts.projects.slice(0, 6),
|
|
1014
|
+
models: report.charts.models.slice(0, 4),
|
|
1015
|
+
tools: report.charts.tools.slice(0, 8),
|
|
1016
|
+
tool_failures: report.charts.toolFailures.slice(0, 6),
|
|
1017
|
+
active_hours: compressActiveHours(report.charts.activeHours),
|
|
1018
|
+
},
|
|
1019
|
+
aggregate_facets: {
|
|
1020
|
+
sessions_with_facets: facets.length,
|
|
1021
|
+
goal_categories: goalCategories,
|
|
1022
|
+
outcomes,
|
|
1023
|
+
satisfaction,
|
|
1024
|
+
session_types: sessionTypes,
|
|
1025
|
+
friction,
|
|
1026
|
+
success,
|
|
1027
|
+
},
|
|
1028
|
+
session_summaries: sortedFacets.slice(0, MAX_CONTEXT_FACETS).map(facet => ({
|
|
1029
|
+
thread_id: facet.threadId,
|
|
1030
|
+
title: truncateForContext(facet.title, 80),
|
|
1031
|
+
project: compactProjectPath(facet.cwd),
|
|
1032
|
+
goal: truncateForContext(facet.underlying_goal, 120),
|
|
1033
|
+
outcome: facet.outcome,
|
|
1034
|
+
session_type: facet.session_type,
|
|
1035
|
+
primary_success: facet.primary_success,
|
|
1036
|
+
summary: truncateForContext(facet.brief_summary, 160),
|
|
1037
|
+
friction: compactCountObject(facet.friction_counts, 2),
|
|
1038
|
+
failures: facet.totalCommandFailures,
|
|
1039
|
+
})),
|
|
1040
|
+
friction_details: frictionDetails,
|
|
1041
|
+
user_instructions: userInstructions,
|
|
1042
|
+
recent_threads: threadSummaries.slice(0, MAX_RECENT_THREADS).map(thread => ({
|
|
1043
|
+
id: thread.id,
|
|
1044
|
+
title: truncateForContext(thread.title, 80),
|
|
1045
|
+
project: compactProjectPath(thread.cwd),
|
|
1046
|
+
duration_minutes: thread.durationMinutes,
|
|
1047
|
+
user_messages: thread.userMessages,
|
|
1048
|
+
tool_calls: thread.totalToolCalls,
|
|
1049
|
+
files_modified: thread.filesModified,
|
|
1050
|
+
})),
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function buildAtAGlancePrompt(context, insights) {
|
|
1055
|
+
return `You are writing an "At a Glance" summary for a Codex usage insights report.
|
|
1056
|
+
|
|
1057
|
+
Use this 4-part structure:
|
|
1058
|
+
1. What's working
|
|
1059
|
+
2. What's hindering you
|
|
1060
|
+
3. Quick wins to try
|
|
1061
|
+
4. Ambitious workflows
|
|
1062
|
+
|
|
1063
|
+
Keep each field to 2-3 compact sentences. Be specific, not flattering.
|
|
1064
|
+
|
|
1065
|
+
Additional constraints:
|
|
1066
|
+
- "What's working" should emphasize distinctive strengths, not generic success
|
|
1067
|
+
- "What's hindering you" should include both model-side and workflow-side friction when supported
|
|
1068
|
+
- "Quick wins" should be immediately actionable and high leverage
|
|
1069
|
+
- "Ambitious workflows" should be plausible next-step workflows, not science fiction
|
|
1070
|
+
- Do not repeat the same idea across multiple fields
|
|
1071
|
+
|
|
1072
|
+
REPORT CONTEXT:
|
|
1073
|
+
${compactJson(context)}
|
|
1074
|
+
|
|
1075
|
+
SECTION DIGEST:
|
|
1076
|
+
${compactJson(compactInsightDigest(insights))}
|
|
1077
|
+
|
|
1078
|
+
RESPOND WITH ONLY A VALID JSON OBJECT matching the schema.`
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* @param {InsightRunOptions} options
|
|
1083
|
+
* @param {string} provider
|
|
1084
|
+
* @param {(usage: any) => void} onUsage
|
|
1085
|
+
*/
|
|
1086
|
+
function buildProviderOptions(options, provider, onUsage) {
|
|
1087
|
+
return {
|
|
1088
|
+
provider,
|
|
1089
|
+
lang: options.lang,
|
|
1090
|
+
apiKey: options.apiKey,
|
|
1091
|
+
apiBase: options.apiBase,
|
|
1092
|
+
codexBin: options.codexBin,
|
|
1093
|
+
cwd: process.cwd(),
|
|
1094
|
+
onUsage,
|
|
1095
|
+
facetEffort: options.facetEffort || DEFAULT_FACET_EFFORT,
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function estimateSectionInputs(candidateThreads, facetJobs) {
|
|
1100
|
+
const contextShape = {
|
|
1101
|
+
metadata: {
|
|
1102
|
+
thread_count: candidateThreads.length,
|
|
1103
|
+
},
|
|
1104
|
+
summary: {
|
|
1105
|
+
total_user_messages: candidateThreads.reduce((sum, thread) => sum + Number(thread.userMessages || 0), 0),
|
|
1106
|
+
total_tool_calls: candidateThreads.reduce((sum, thread) => sum + Number(thread.totalToolCalls || 0), 0),
|
|
1107
|
+
total_failures: candidateThreads.reduce((sum, thread) => sum + Number(thread.totalCommandFailures || 0), 0),
|
|
1108
|
+
},
|
|
1109
|
+
charts: {
|
|
1110
|
+
projects: candidateThreads.slice(0, 6).map(thread => compactProjectPath(thread.cwd)),
|
|
1111
|
+
models: Array.from(new Set(candidateThreads.map(thread => thread.model).filter(Boolean))).slice(0, 4),
|
|
1112
|
+
tools: [],
|
|
1113
|
+
},
|
|
1114
|
+
aggregate_facets: {
|
|
1115
|
+
sessions_with_facets: facetJobs.length,
|
|
1116
|
+
},
|
|
1117
|
+
session_summaries: facetJobs.slice(0, MAX_CONTEXT_FACETS).map(job => {
|
|
1118
|
+
if (job.cachedFacet) {
|
|
1119
|
+
return {
|
|
1120
|
+
title: truncateForContext(job.cachedFacet.title, 80),
|
|
1121
|
+
project: compactProjectPath(job.cachedFacet.cwd),
|
|
1122
|
+
goal: truncateForContext(job.cachedFacet.underlying_goal, 120),
|
|
1123
|
+
outcome: job.cachedFacet.outcome,
|
|
1124
|
+
primary_success: job.cachedFacet.primary_success,
|
|
1125
|
+
summary: truncateForContext(job.cachedFacet.brief_summary, 160),
|
|
1126
|
+
friction: compactCountObject(job.cachedFacet.friction_counts, 2),
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
title: truncateForContext(job.thread.title, 80),
|
|
1131
|
+
project: compactProjectPath(job.thread.cwd),
|
|
1132
|
+
goal: truncateForContext(job.thread.firstUserMessage, 120),
|
|
1133
|
+
outcome: 'unknown',
|
|
1134
|
+
primary_success: 'unknown',
|
|
1135
|
+
summary: truncateForContext(job.thread.firstUserMessage, 160),
|
|
1136
|
+
friction: {},
|
|
1137
|
+
}
|
|
1138
|
+
}),
|
|
1139
|
+
recent_threads: candidateThreads.slice(0, MAX_RECENT_THREADS).map(thread => ({
|
|
1140
|
+
title: truncateForContext(thread.title, 80),
|
|
1141
|
+
project: compactProjectPath(thread.cwd),
|
|
1142
|
+
duration_minutes: thread.durationMinutes,
|
|
1143
|
+
user_messages: thread.userMessages,
|
|
1144
|
+
tool_calls: thread.totalToolCalls,
|
|
1145
|
+
files_modified: thread.filesModified,
|
|
1146
|
+
})),
|
|
1147
|
+
friction_details: [],
|
|
1148
|
+
user_instructions: [],
|
|
1149
|
+
}
|
|
1150
|
+
const sectionKinds = [
|
|
1151
|
+
'project_areas',
|
|
1152
|
+
'interaction_style',
|
|
1153
|
+
'what_works',
|
|
1154
|
+
'friction_analysis',
|
|
1155
|
+
'suggestions',
|
|
1156
|
+
'on_the_horizon',
|
|
1157
|
+
'fun_ending',
|
|
1158
|
+
'at_a_glance',
|
|
1159
|
+
]
|
|
1160
|
+
const estimated = {}
|
|
1161
|
+
for (const kind of sectionKinds) {
|
|
1162
|
+
estimated[kind] = estimateTokensFromChars(compactJson(buildSectionContext(contextShape, kind)).length)
|
|
1163
|
+
}
|
|
1164
|
+
return estimated
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function resolveSectionModel(section, { fastSectionModel, insightModel }) {
|
|
1168
|
+
return section.modelTier === 'fast' ? fastSectionModel : insightModel
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function resolveSectionEffort(section, { fastSectionEffort, insightEffort }) {
|
|
1172
|
+
return section.modelTier === 'fast' ? fastSectionEffort : insightEffort
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function resolveModelFallbacks(model) {
|
|
1176
|
+
if (model === 'gpt-5.3-codex-spark') {
|
|
1177
|
+
return ['gpt-5.4-mini', 'gpt-5.4']
|
|
1178
|
+
}
|
|
1179
|
+
if (model === 'gpt-5.4') {
|
|
1180
|
+
return ['gpt-5.4-mini']
|
|
1181
|
+
}
|
|
1182
|
+
return []
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function buildEstimateBucket(label, calls, inputTokens, cachedInputTokens, outputTokens) {
|
|
1186
|
+
return {
|
|
1187
|
+
label,
|
|
1188
|
+
calls,
|
|
1189
|
+
inputTokens: Math.round(inputTokens),
|
|
1190
|
+
cachedInputTokens: Math.round(cachedInputTokens),
|
|
1191
|
+
outputTokens: Math.round(outputTokens),
|
|
1192
|
+
totalTokens: Math.round(inputTokens + cachedInputTokens + outputTokens),
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function aggregateEstimateByModel(items) {
|
|
1197
|
+
const buckets = {}
|
|
1198
|
+
for (const item of items) {
|
|
1199
|
+
if (!item.calls) continue
|
|
1200
|
+
if (!buckets[item.label]) buckets[item.label] = emptyUsageBucket()
|
|
1201
|
+
buckets[item.label].calls += item.calls
|
|
1202
|
+
buckets[item.label].inputTokens += Math.round(item.inputTokens)
|
|
1203
|
+
buckets[item.label].cachedInputTokens += Math.round(item.cachedInputTokens)
|
|
1204
|
+
buckets[item.label].outputTokens += Math.round(item.outputTokens)
|
|
1205
|
+
buckets[item.label].totalTokens += Math.round(item.totalTokens)
|
|
1206
|
+
}
|
|
1207
|
+
return sortBuckets(buckets)
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function createUsageTracker(provider) {
|
|
1211
|
+
const totals = {
|
|
1212
|
+
provider,
|
|
1213
|
+
calls: 0,
|
|
1214
|
+
inputTokens: 0,
|
|
1215
|
+
cachedInputTokens: 0,
|
|
1216
|
+
outputTokens: 0,
|
|
1217
|
+
totalTokens: 0,
|
|
1218
|
+
byModel: {},
|
|
1219
|
+
byStage: {},
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
return {
|
|
1223
|
+
add(usage) {
|
|
1224
|
+
totals.calls += 1
|
|
1225
|
+
totals.inputTokens += Number(usage.inputTokens ?? 0)
|
|
1226
|
+
totals.cachedInputTokens += Number(usage.cachedInputTokens ?? 0)
|
|
1227
|
+
totals.outputTokens += Number(usage.outputTokens ?? 0)
|
|
1228
|
+
totals.totalTokens += Number(usage.totalTokens ?? 0)
|
|
1229
|
+
|
|
1230
|
+
const modelKey = usage.model || '(unknown model)'
|
|
1231
|
+
if (!totals.byModel[modelKey]) totals.byModel[modelKey] = emptyUsageBucket()
|
|
1232
|
+
addToBucket(totals.byModel[modelKey], usage)
|
|
1233
|
+
|
|
1234
|
+
const stageKey = usage.stage || 'unspecified'
|
|
1235
|
+
if (!totals.byStage[stageKey]) totals.byStage[stageKey] = emptyUsageBucket()
|
|
1236
|
+
addToBucket(totals.byStage[stageKey], usage)
|
|
1237
|
+
},
|
|
1238
|
+
snapshot() {
|
|
1239
|
+
return {
|
|
1240
|
+
provider: totals.provider,
|
|
1241
|
+
calls: totals.calls,
|
|
1242
|
+
inputTokens: totals.inputTokens,
|
|
1243
|
+
cachedInputTokens: totals.cachedInputTokens,
|
|
1244
|
+
outputTokens: totals.outputTokens,
|
|
1245
|
+
totalTokens: totals.totalTokens,
|
|
1246
|
+
byModel: sortBuckets(totals.byModel),
|
|
1247
|
+
byStage: sortBuckets(totals.byStage),
|
|
1248
|
+
}
|
|
1249
|
+
},
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function emptyUsageBucket() {
|
|
1254
|
+
return {
|
|
1255
|
+
calls: 0,
|
|
1256
|
+
inputTokens: 0,
|
|
1257
|
+
cachedInputTokens: 0,
|
|
1258
|
+
outputTokens: 0,
|
|
1259
|
+
totalTokens: 0,
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function addToBucket(bucket, usage) {
|
|
1264
|
+
bucket.calls += 1
|
|
1265
|
+
bucket.inputTokens += Number(usage.inputTokens ?? 0)
|
|
1266
|
+
bucket.cachedInputTokens += Number(usage.cachedInputTokens ?? 0)
|
|
1267
|
+
bucket.outputTokens += Number(usage.outputTokens ?? 0)
|
|
1268
|
+
bucket.totalTokens += Number(usage.totalTokens ?? 0)
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function sortBuckets(buckets) {
|
|
1272
|
+
return Object.entries(buckets)
|
|
1273
|
+
.map(([label, value]) => ({ label, ...value }))
|
|
1274
|
+
.sort((a, b) => b.totalTokens - a.totalTokens)
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function isWarmupMinimalFacet(facet) {
|
|
1278
|
+
const categories = Object.entries(facet.goal_categories || {}).filter(([, count]) => count > 0)
|
|
1279
|
+
return categories.length === 1 && categories[0][0] === 'warmup_minimal'
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function addCounts(target, source) {
|
|
1283
|
+
for (const [key, value] of Object.entries(source || {})) {
|
|
1284
|
+
if (value > 0) target[key] = (target[key] || 0) + value
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function buildFacetVersionKey(thread, model) {
|
|
1289
|
+
return hashObject({
|
|
1290
|
+
schemaVersion: FACET_SCHEMA_VERSION,
|
|
1291
|
+
id: thread.id,
|
|
1292
|
+
updatedAt: thread.updatedAt,
|
|
1293
|
+
model,
|
|
1294
|
+
transcript: thread.transcriptForAnalysis,
|
|
1295
|
+
})
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function resolveCacheRoot(explicitDir) {
|
|
1299
|
+
if (explicitDir) return path.resolve(explicitDir)
|
|
1300
|
+
return path.join(os.homedir(), '.codex-insights-cache')
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function chunkText(text, chunkSize) {
|
|
1304
|
+
const chunks = []
|
|
1305
|
+
let start = 0
|
|
1306
|
+
while (start < text.length) {
|
|
1307
|
+
let end = Math.min(text.length, start + chunkSize)
|
|
1308
|
+
if (end < text.length) {
|
|
1309
|
+
const lastBreak = text.lastIndexOf('\n', end)
|
|
1310
|
+
if (lastBreak > start + chunkSize * 0.6) {
|
|
1311
|
+
end = lastBreak
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
chunks.push(text.slice(start, end))
|
|
1315
|
+
start = end
|
|
1316
|
+
}
|
|
1317
|
+
return chunks
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function estimateTokensFromChars(chars) {
|
|
1321
|
+
return Math.ceil(Number(chars || 0) / 4)
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function getNarrativeLanguageInstruction(lang) {
|
|
1325
|
+
if (lang === 'zh-CN') {
|
|
1326
|
+
return 'Write all free-text narrative fields in Simplified Chinese.'
|
|
1327
|
+
}
|
|
1328
|
+
return 'Write all free-text narrative fields in English.'
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function getStructuredLanguageInstruction(lang) {
|
|
1332
|
+
if (lang === 'zh-CN') {
|
|
1333
|
+
return 'Keep keys and enum values unchanged. Write only free-text fields in Simplified Chinese.'
|
|
1334
|
+
}
|
|
1335
|
+
return 'Keep keys and enum values unchanged. Write free-text fields in English.'
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function langFromProviderOptions(options) {
|
|
1339
|
+
return options?.lang === 'zh-CN' ? 'zh-CN' : 'en'
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function describeLanguage(lang) {
|
|
1343
|
+
return lang === 'zh-CN' ? 'Simplified Chinese' : 'English'
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function emitProgress(options, event) {
|
|
1347
|
+
if (typeof options?.onProgress === 'function') {
|
|
1348
|
+
options.onProgress(event)
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function sanitizeContextText(value) {
|
|
1353
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim()
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function truncateForContext(value, limit) {
|
|
1357
|
+
const text = sanitizeContextText(value)
|
|
1358
|
+
if (!text) return ''
|
|
1359
|
+
if (text.length <= limit) return text
|
|
1360
|
+
return `${text.slice(0, limit)}...`
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function compactProjectPath(value) {
|
|
1364
|
+
const text = String(value ?? '').trim()
|
|
1365
|
+
if (!text) return ''
|
|
1366
|
+
const parts = text.split('/').filter(Boolean)
|
|
1367
|
+
return parts.slice(-2).join('/')
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function compactCountObject(value, limit) {
|
|
1371
|
+
return Object.fromEntries(
|
|
1372
|
+
Object.entries(value || {})
|
|
1373
|
+
.filter(([, count]) => Number(count) > 0)
|
|
1374
|
+
.sort((a, b) => Number(b[1]) - Number(a[1]))
|
|
1375
|
+
.slice(0, limit),
|
|
1376
|
+
)
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function compressActiveHours(hourSeries) {
|
|
1380
|
+
return [
|
|
1381
|
+
{ label: 'night', value: sumHours(hourSeries, [0, 1, 2, 3, 4, 5]) },
|
|
1382
|
+
{ label: 'morning', value: sumHours(hourSeries, [6, 7, 8, 9, 10, 11]) },
|
|
1383
|
+
{ label: 'afternoon', value: sumHours(hourSeries, [12, 13, 14, 15, 16, 17]) },
|
|
1384
|
+
{ label: 'evening', value: sumHours(hourSeries, [18, 19, 20, 21, 22, 23]) },
|
|
1385
|
+
]
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function sumHours(hourSeries, hours) {
|
|
1389
|
+
return hours.reduce((sum, hour) => sum + Number(hourSeries[hour]?.value || 0), 0)
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function compactJson(value) {
|
|
1393
|
+
return JSON.stringify(value)
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function buildFullSectionContext(context) {
|
|
1397
|
+
return {
|
|
1398
|
+
metadata: context.metadata,
|
|
1399
|
+
summary: context.summary,
|
|
1400
|
+
charts: context.charts,
|
|
1401
|
+
aggregate_facets: context.aggregate_facets,
|
|
1402
|
+
session_summaries: context.session_summaries,
|
|
1403
|
+
friction_details: context.friction_details,
|
|
1404
|
+
user_instructions: context.user_instructions,
|
|
1405
|
+
recent_threads: context.recent_threads,
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function buildSectionContext(context, kind) {
|
|
1410
|
+
if (kind === 'at_a_glance') {
|
|
1411
|
+
return {
|
|
1412
|
+
metadata: context.metadata,
|
|
1413
|
+
summary: context.summary,
|
|
1414
|
+
aggregate_facets: context.aggregate_facets,
|
|
1415
|
+
charts: {
|
|
1416
|
+
projects: context.charts.projects,
|
|
1417
|
+
tools: context.charts.tools,
|
|
1418
|
+
tool_failures: context.charts.tool_failures,
|
|
1419
|
+
},
|
|
1420
|
+
friction_details: context.friction_details.slice(0, 8),
|
|
1421
|
+
recent_threads: context.recent_threads,
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
return buildFullSectionContext(context)
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function compactInsightDigest(insights) {
|
|
1429
|
+
return {
|
|
1430
|
+
project_areas: (insights.project_areas?.areas || []).slice(0, 4).map(item => ({
|
|
1431
|
+
name: item.name,
|
|
1432
|
+
sessions: item.session_count,
|
|
1433
|
+
})),
|
|
1434
|
+
interaction_style: {
|
|
1435
|
+
key_pattern: insights.interaction_style?.key_pattern || '',
|
|
1436
|
+
narrative: truncateForContext(insights.interaction_style?.narrative || '', 240),
|
|
1437
|
+
},
|
|
1438
|
+
what_works: (insights.what_works?.impressive_workflows || []).slice(0, 3).map(item => ({
|
|
1439
|
+
title: item.title,
|
|
1440
|
+
description: truncateForContext(item.description, 160),
|
|
1441
|
+
})),
|
|
1442
|
+
friction_analysis: (insights.friction_analysis?.categories || []).slice(0, 3).map(item => ({
|
|
1443
|
+
category: item.category,
|
|
1444
|
+
description: truncateForContext(item.description, 140),
|
|
1445
|
+
})),
|
|
1446
|
+
suggestions: {
|
|
1447
|
+
features: (insights.suggestions?.features_to_try || []).slice(0, 3).map(item => item.feature),
|
|
1448
|
+
usage_patterns: (insights.suggestions?.usage_patterns || []).slice(0, 2).map(item => item.title),
|
|
1449
|
+
agents_md_additions: (insights.suggestions?.agents_md_additions || []).slice(0, 2).map(item =>
|
|
1450
|
+
truncateForContext(item.addition, 120),
|
|
1451
|
+
),
|
|
1452
|
+
},
|
|
1453
|
+
on_the_horizon: (insights.on_the_horizon?.opportunities || []).slice(0, 3).map(item => ({
|
|
1454
|
+
title: item.title,
|
|
1455
|
+
whats_possible: truncateForContext(item.whats_possible, 140),
|
|
1456
|
+
})),
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function hashObject(value) {
|
|
1461
|
+
return crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex')
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
async function readJson(filePath) {
|
|
1465
|
+
try {
|
|
1466
|
+
return JSON.parse(await fs.readFile(filePath, 'utf8'))
|
|
1467
|
+
} catch {
|
|
1468
|
+
return null
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
async function mapLimit(items, limit, fn) {
|
|
1473
|
+
const results = new Array(items.length)
|
|
1474
|
+
let index = 0
|
|
1475
|
+
|
|
1476
|
+
async function worker() {
|
|
1477
|
+
while (index < items.length) {
|
|
1478
|
+
const current = index
|
|
1479
|
+
index += 1
|
|
1480
|
+
results[current] = await fn(items[current], current)
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker))
|
|
1485
|
+
return results
|
|
1486
|
+
}
|