codex-session-insights 0.2.1 → 0.2.3
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/README.md +9 -0
- package/lib/cli.js +44 -1
- package/lib/codex-data.js +56 -2
- package/lib/llm-insights.js +255 -129
- package/lib/report.js +169 -7
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -60,6 +60,12 @@ Default run:
|
|
|
60
60
|
npx codex-session-insights
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
Lite local run for prompt and layout testing:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx codex-session-insights --preset lite
|
|
67
|
+
```
|
|
68
|
+
|
|
63
69
|
Estimate first, then decide:
|
|
64
70
|
|
|
65
71
|
```bash
|
|
@@ -120,6 +126,7 @@ Current default analysis plan:
|
|
|
120
126
|
|
|
121
127
|
Important behavior defaults:
|
|
122
128
|
|
|
129
|
+
- `--preset lite` maps to `days=7`, `limit=20`, `facet-limit=8`, `preview=10`
|
|
123
130
|
- `limit` means the target number of substantive threads to include in the report, not just the first 50 indexed threads
|
|
124
131
|
- `facet-limit` means the max number of uncached per-thread facet analyses to run in a single report
|
|
125
132
|
- Report language follows a best-effort system locale check
|
|
@@ -195,7 +202,9 @@ Useful local commands:
|
|
|
195
202
|
npm install
|
|
196
203
|
npm test
|
|
197
204
|
npm run check
|
|
205
|
+
npm run report:lite
|
|
198
206
|
npm run generate:test-report
|
|
199
207
|
```
|
|
200
208
|
|
|
209
|
+
`npm run report:lite` runs a smaller local analysis preset for testing prompt and layout changes without paying the full 200/50 default cost.
|
|
201
210
|
`npm run generate:test-report` writes a deterministic sample report page to `test-artifacts/sample-report/`.
|
package/lib/cli.js
CHANGED
|
@@ -99,6 +99,7 @@ export async function runCli(argv) {
|
|
|
99
99
|
})
|
|
100
100
|
report.analysisMode = 'llm'
|
|
101
101
|
report.provider = parsed.options.provider
|
|
102
|
+
report.analysisEstimate = estimate
|
|
102
103
|
report.analysisUsage = llmResult.analysisUsage
|
|
103
104
|
|
|
104
105
|
progress.startStage(parsed.options, getUiText(parsed.options.lang).writingFiles)
|
|
@@ -135,11 +136,18 @@ export async function runCli(argv) {
|
|
|
135
136
|
|
|
136
137
|
function parseArgs(argv) {
|
|
137
138
|
let command = null
|
|
139
|
+
const explicit = {
|
|
140
|
+
days: false,
|
|
141
|
+
limit: false,
|
|
142
|
+
preview: false,
|
|
143
|
+
facetLimit: false,
|
|
144
|
+
}
|
|
138
145
|
const options = {
|
|
139
146
|
codexHome: null,
|
|
140
147
|
outDir: null,
|
|
141
148
|
jsonPath: null,
|
|
142
149
|
htmlPath: null,
|
|
150
|
+
preset: DEFAULT_SCOPE_PRESET,
|
|
143
151
|
days: 30,
|
|
144
152
|
limit: 200,
|
|
145
153
|
preview: 50,
|
|
@@ -195,14 +203,21 @@ function parseArgs(argv) {
|
|
|
195
203
|
continue
|
|
196
204
|
}
|
|
197
205
|
if (arg === '--days') {
|
|
206
|
+
explicit.days = true
|
|
198
207
|
options.days = toPositiveInt(requireValue(argv, ++i, '--days'), '--days')
|
|
199
208
|
continue
|
|
200
209
|
}
|
|
210
|
+
if (arg === '--preset') {
|
|
211
|
+
options.preset = normalizeScopePreset(requireValue(argv, ++i, '--preset'))
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
201
214
|
if (arg === '--limit') {
|
|
215
|
+
explicit.limit = true
|
|
202
216
|
options.limit = toPositiveInt(requireValue(argv, ++i, '--limit'), '--limit')
|
|
203
217
|
continue
|
|
204
218
|
}
|
|
205
219
|
if (arg === '--preview') {
|
|
220
|
+
explicit.preview = true
|
|
206
221
|
options.preview = toPositiveInt(requireValue(argv, ++i, '--preview'), '--preview')
|
|
207
222
|
continue
|
|
208
223
|
}
|
|
@@ -255,6 +270,7 @@ function parseArgs(argv) {
|
|
|
255
270
|
continue
|
|
256
271
|
}
|
|
257
272
|
if (arg === '--facet-limit') {
|
|
273
|
+
explicit.facetLimit = true
|
|
258
274
|
options.facetLimit = toPositiveInt(requireValue(argv, ++i, '--facet-limit'), '--facet-limit')
|
|
259
275
|
continue
|
|
260
276
|
}
|
|
@@ -298,6 +314,19 @@ function parseArgs(argv) {
|
|
|
298
314
|
throw new Error(`Invalid provider "${options.provider}". Expected codex-cli or openai.`)
|
|
299
315
|
}
|
|
300
316
|
|
|
317
|
+
options.preset = normalizeScopePreset(options.preset)
|
|
318
|
+
const explicitValues = {
|
|
319
|
+
days: options.days,
|
|
320
|
+
limit: options.limit,
|
|
321
|
+
preview: options.preview,
|
|
322
|
+
facetLimit: options.facetLimit,
|
|
323
|
+
}
|
|
324
|
+
Object.assign(options, applyScopePreset(options, options.preset))
|
|
325
|
+
if (explicit.days) options.days = explicitValues.days
|
|
326
|
+
if (explicit.limit) options.limit = explicitValues.limit
|
|
327
|
+
if (explicit.preview) options.preview = explicitValues.preview
|
|
328
|
+
if (explicit.facetLimit) options.facetLimit = explicitValues.facetLimit
|
|
329
|
+
|
|
301
330
|
return { command, options, help }
|
|
302
331
|
}
|
|
303
332
|
|
|
@@ -330,6 +359,7 @@ Options:
|
|
|
330
359
|
--json-path <path> Exact path for report.json
|
|
331
360
|
--html-path <path> Exact path for report.html
|
|
332
361
|
--days <n> Only include threads updated in the last N days (default: 30)
|
|
362
|
+
--preset <name> Scope preset: lite, standard, or deep (default: standard)
|
|
333
363
|
--limit <n> Target number of substantive threads to include (default: 200)
|
|
334
364
|
--preview <n> Number of threads to embed in the HTML report (default: 50)
|
|
335
365
|
--provider <name> Model provider: codex-cli or openai (default: codex-cli)
|
|
@@ -681,11 +711,22 @@ function inferQualityPreset(options) {
|
|
|
681
711
|
}
|
|
682
712
|
|
|
683
713
|
function applyScopePreset(options, preset) {
|
|
684
|
-
if (preset === '
|
|
714
|
+
if (preset === 'lite' || preset === 'conservative') {
|
|
715
|
+
return { ...options, days: 7, limit: 20, facetLimit: 8, preview: 10 }
|
|
716
|
+
}
|
|
685
717
|
if (preset === 'deep') return { ...options, limit: 400, facetLimit: 50 }
|
|
686
718
|
return { ...options, limit: 200, facetLimit: 50 }
|
|
687
719
|
}
|
|
688
720
|
|
|
721
|
+
function normalizeScopePreset(value) {
|
|
722
|
+
const preset = String(value || '').trim().toLowerCase()
|
|
723
|
+
if (preset === 'lite') return 'lite'
|
|
724
|
+
if (preset === 'conservative') return 'conservative'
|
|
725
|
+
if (preset === 'deep') return 'deep'
|
|
726
|
+
if (preset === 'standard' || !preset) return 'standard'
|
|
727
|
+
throw new Error(`Invalid preset "${value}". Expected lite, standard, or deep.`)
|
|
728
|
+
}
|
|
729
|
+
|
|
689
730
|
function applyQualityPreset(options, preset) {
|
|
690
731
|
if (preset === 'cheaper') {
|
|
691
732
|
return {
|
|
@@ -997,6 +1038,8 @@ export const __test = {
|
|
|
997
1038
|
applyScopePreset,
|
|
998
1039
|
applyQualityPreset,
|
|
999
1040
|
buildEquivalentCommand,
|
|
1041
|
+
parseArgs,
|
|
1042
|
+
normalizeScopePreset,
|
|
1000
1043
|
normalizeLang,
|
|
1001
1044
|
detectSystemLanguage,
|
|
1002
1045
|
}
|
package/lib/codex-data.js
CHANGED
|
@@ -8,8 +8,16 @@ import { promisify } from 'node:util'
|
|
|
8
8
|
const execFileAsync = promisify(execFile)
|
|
9
9
|
const MAX_TRANSCRIPT_CHARS = 30000
|
|
10
10
|
const USER_TRANSCRIPT_LIMIT = 500
|
|
11
|
-
const ASSISTANT_TRANSCRIPT_LIMIT =
|
|
11
|
+
const ASSISTANT_TRANSCRIPT_LIMIT = 300
|
|
12
12
|
const FAILURE_SUMMARY_LIMIT = 120
|
|
13
|
+
const MAX_TOOL_LINES_PER_BURST = 2
|
|
14
|
+
const LOW_SIGNAL_ASSISTANT_PATTERNS = [
|
|
15
|
+
/^i('| a)?m\s+(checking|looking|reviewing|reading|investigating)\b/i,
|
|
16
|
+
/^i('| wi)?ll\s+(check|look|review|inspect|read|investigate|start by)\b/i,
|
|
17
|
+
/^let me\b/i,
|
|
18
|
+
/^(checking|reviewing|reading|looking at|investigating)\b/i,
|
|
19
|
+
/^(next|now)\b/i,
|
|
20
|
+
]
|
|
13
21
|
const TASK_AGENT_TOOLS = new Set([
|
|
14
22
|
'spawn_agent',
|
|
15
23
|
'send_input',
|
|
@@ -422,7 +430,7 @@ export function summarizeThread(thread, events) {
|
|
|
422
430
|
averageResponseTimeSeconds: average(responseTimesSeconds),
|
|
423
431
|
activeHours,
|
|
424
432
|
userMessageTimestamps: sortedUserTs.map(tsMs => new Date(tsMs).toISOString()),
|
|
425
|
-
transcriptForAnalysis: clampTranscript(transcriptLines.join('\n')),
|
|
433
|
+
transcriptForAnalysis: clampTranscript(compactTranscriptLines(transcriptLines).join('\n')),
|
|
426
434
|
gitCommits,
|
|
427
435
|
gitPushes,
|
|
428
436
|
userInterruptions,
|
|
@@ -627,6 +635,38 @@ function clampTranscript(text) {
|
|
|
627
635
|
return `${clean.slice(0, MAX_TRANSCRIPT_CHARS)}\n[Transcript truncated]`
|
|
628
636
|
}
|
|
629
637
|
|
|
638
|
+
function compactTranscriptLines(lines) {
|
|
639
|
+
const compacted = []
|
|
640
|
+
let pendingToolBurst = []
|
|
641
|
+
|
|
642
|
+
const flushToolBurst = () => {
|
|
643
|
+
if (!pendingToolBurst.length) return
|
|
644
|
+
compacted.push(...pendingToolBurst.slice(0, MAX_TOOL_LINES_PER_BURST))
|
|
645
|
+
const remaining = pendingToolBurst.length - MAX_TOOL_LINES_PER_BURST
|
|
646
|
+
if (remaining > 0) {
|
|
647
|
+
compacted.push(`[Tool activity truncated: ${remaining} more tool calls]`)
|
|
648
|
+
}
|
|
649
|
+
pendingToolBurst = []
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
for (const line of lines) {
|
|
653
|
+
if (isLowSignalAssistantLine(line)) continue
|
|
654
|
+
|
|
655
|
+
if (isToolLine(line)) {
|
|
656
|
+
pendingToolBurst.push(line)
|
|
657
|
+
continue
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
flushToolBurst()
|
|
661
|
+
if (!compacted.length || compacted[compacted.length - 1] !== line) {
|
|
662
|
+
compacted.push(line)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
flushToolBurst()
|
|
667
|
+
return compacted
|
|
668
|
+
}
|
|
669
|
+
|
|
630
670
|
function appendTranscriptLine(lines, line, setLastLine, lastLine) {
|
|
631
671
|
const clean = String(line ?? '').trim()
|
|
632
672
|
if (!clean || clean === lastLine) return
|
|
@@ -634,6 +674,20 @@ function appendTranscriptLine(lines, line, setLastLine, lastLine) {
|
|
|
634
674
|
setLastLine(clean)
|
|
635
675
|
}
|
|
636
676
|
|
|
677
|
+
function isToolLine(line) {
|
|
678
|
+
const text = String(line || '')
|
|
679
|
+
return text.startsWith('[Tool: ')
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function isLowSignalAssistantLine(line) {
|
|
683
|
+
const text = String(line || '')
|
|
684
|
+
if (!text.startsWith('[Assistant] ')) return false
|
|
685
|
+
const body = text.slice('[Assistant] '.length).trim()
|
|
686
|
+
if (!body) return true
|
|
687
|
+
if (body.length > 60) return false
|
|
688
|
+
return LOW_SIGNAL_ASSISTANT_PATTERNS.some(pattern => pattern.test(body))
|
|
689
|
+
}
|
|
690
|
+
|
|
637
691
|
function sanitizeTranscriptText(value, limit) {
|
|
638
692
|
const text = String(value ?? '')
|
|
639
693
|
.replace(/<system_instruction>[\s\S]*?<\/system_instruction>/g, ' ')
|
package/lib/llm-insights.js
CHANGED
|
@@ -411,6 +411,7 @@ Guardrails:
|
|
|
411
411
|
- Do not recommend first-time adoption of AGENTS.md, Skills, codex exec, Sub-agents, or MCP Servers when the capability_adoption evidence shows the user already uses them in a moderate or strong way
|
|
412
412
|
- When a capability is already adopted, suggest a deeper refinement or a tighter operating pattern instead of basic adoption
|
|
413
413
|
- Distinguish "you should start using this" from "you should formalize or deepen how you already use this"
|
|
414
|
+
- Use AGENTS.md as the canonical repo instruction filename in examples; do not mention CLAUDE.md
|
|
414
415
|
- Write AGENTS.md additions as directly pasteable instruction lines, not commentary about instructions
|
|
415
416
|
- Make feature examples immediately usable; avoid placeholders like "insert your repo path here" unless unavoidable
|
|
416
417
|
- Make usage pattern suggestions sound like concrete next actions the user can try today, not abstract best practices`,
|
|
@@ -597,6 +598,7 @@ export async function estimateLlmAnalysisCost({ threadSummaries, options = {} })
|
|
|
597
598
|
const facetModel = options.facetModel || DEFAULT_FACET_MODEL
|
|
598
599
|
const fastSectionModel = options.fastSectionModel || DEFAULT_FAST_SECTION_MODEL
|
|
599
600
|
const insightModel = options.insightModel || DEFAULT_INSIGHT_MODEL
|
|
601
|
+
const provider = options.provider || DEFAULT_PROVIDER
|
|
600
602
|
const cacheRoot = resolveCacheRoot(options.cacheDir)
|
|
601
603
|
const facetCacheDir = path.join(cacheRoot, 'facets')
|
|
602
604
|
await fs.mkdir(facetCacheDir, { recursive: true })
|
|
@@ -618,20 +620,35 @@ export async function estimateLlmAnalysisCost({ threadSummaries, options = {} })
|
|
|
618
620
|
let combineSummaryCalls = 0
|
|
619
621
|
let estimatedFacetInputTokens = 0
|
|
620
622
|
let estimatedFacetOutputTokens = 0
|
|
621
|
-
let
|
|
622
|
-
let
|
|
623
|
+
let estimatedChunkSummaryInputTokens = 0
|
|
624
|
+
let estimatedChunkSummaryOutputTokens = 0
|
|
625
|
+
let estimatedCombineSummaryInputTokens = 0
|
|
626
|
+
let estimatedCombineSummaryOutputTokens = 0
|
|
627
|
+
const facetSystemPrompt = buildFacetSystemPrompt(options.lang)
|
|
623
628
|
|
|
624
629
|
for (const job of uncachedFacetJobs) {
|
|
625
630
|
const transcript = String(job.thread.transcriptForAnalysis || '').trim()
|
|
626
631
|
const transcriptChars = transcript.length
|
|
627
632
|
if (!transcriptChars) {
|
|
628
|
-
estimatedFacetInputTokens +=
|
|
633
|
+
estimatedFacetInputTokens += estimateModelInputTokens({
|
|
634
|
+
provider,
|
|
635
|
+
systemPrompt: facetSystemPrompt,
|
|
636
|
+
userPrompt: buildFacetExtractionPrompt(job.thread, `${job.thread.title || '(untitled)'}\n${job.thread.firstUserMessage || ''}`.trim(), options.lang),
|
|
637
|
+
schema: FACET_SCHEMA,
|
|
638
|
+
structured: true,
|
|
639
|
+
})
|
|
629
640
|
estimatedFacetOutputTokens += 350
|
|
630
641
|
continue
|
|
631
642
|
}
|
|
632
643
|
|
|
633
644
|
if (transcriptChars <= LONG_TRANSCRIPT_THRESHOLD) {
|
|
634
|
-
estimatedFacetInputTokens +=
|
|
645
|
+
estimatedFacetInputTokens += estimateModelInputTokens({
|
|
646
|
+
provider,
|
|
647
|
+
systemPrompt: facetSystemPrompt,
|
|
648
|
+
userPrompt: buildFacetExtractionPrompt(job.thread, transcript, options.lang),
|
|
649
|
+
schema: FACET_SCHEMA,
|
|
650
|
+
structured: true,
|
|
651
|
+
})
|
|
635
652
|
estimatedFacetOutputTokens += 350
|
|
636
653
|
continue
|
|
637
654
|
}
|
|
@@ -639,30 +656,65 @@ export async function estimateLlmAnalysisCost({ threadSummaries, options = {} })
|
|
|
639
656
|
const chunks = chunkText(transcript, TRANSCRIPT_CHUNK_SIZE)
|
|
640
657
|
chunkSummaryCalls += chunks.length
|
|
641
658
|
for (const chunk of chunks) {
|
|
642
|
-
|
|
643
|
-
|
|
659
|
+
estimatedChunkSummaryInputTokens += estimateModelInputTokens({
|
|
660
|
+
provider,
|
|
661
|
+
systemPrompt: `${FACET_TRANSCRIPT_SUMMARY_DIRECTIVE}\n\nPreserve user goal, outcome, friction, command/tool issues, and what the assistant actually achieved.`,
|
|
662
|
+
userPrompt: `Chunk 1 of ${chunks.length}\n\n${chunk}`,
|
|
663
|
+
structured: false,
|
|
664
|
+
})
|
|
665
|
+
estimatedChunkSummaryOutputTokens += 260
|
|
644
666
|
}
|
|
645
667
|
const combinedSummaryChars = chunks.length * 1100
|
|
646
668
|
if (combinedSummaryChars > LONG_TRANSCRIPT_THRESHOLD) {
|
|
647
669
|
combineSummaryCalls += 1
|
|
648
|
-
|
|
649
|
-
|
|
670
|
+
estimatedCombineSummaryInputTokens += estimateModelInputTokens({
|
|
671
|
+
provider,
|
|
672
|
+
systemPrompt:
|
|
673
|
+
'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.',
|
|
674
|
+
userPrompt: makePlaceholderText(combinedSummaryChars, 'Chunk summaries'),
|
|
675
|
+
structured: false,
|
|
676
|
+
})
|
|
677
|
+
estimatedCombineSummaryOutputTokens += 320
|
|
650
678
|
}
|
|
651
|
-
estimatedFacetInputTokens +=
|
|
679
|
+
estimatedFacetInputTokens += estimateModelInputTokens({
|
|
680
|
+
provider,
|
|
681
|
+
systemPrompt: facetSystemPrompt,
|
|
682
|
+
userPrompt: buildFacetExtractionPrompt(
|
|
683
|
+
job.thread,
|
|
684
|
+
makePlaceholderText(
|
|
685
|
+
combinedSummaryChars > LONG_TRANSCRIPT_THRESHOLD ? 1200 : combinedSummaryChars,
|
|
686
|
+
'[Long transcript summarized before facet extraction]',
|
|
687
|
+
),
|
|
688
|
+
options.lang,
|
|
689
|
+
),
|
|
690
|
+
schema: FACET_SCHEMA,
|
|
691
|
+
structured: true,
|
|
692
|
+
})
|
|
652
693
|
estimatedFacetOutputTokens += 350
|
|
653
694
|
}
|
|
654
695
|
|
|
655
|
-
const estimatedSectionInputs = estimateSectionInputs(candidateThreads, facetJobs)
|
|
696
|
+
const estimatedSectionInputs = estimateSectionInputs(candidateThreads, facetJobs, options)
|
|
656
697
|
const fastSectionCalls = SECTION_DEFS.filter(section => section.modelTier === 'fast').length
|
|
657
698
|
const fullSectionCalls = SECTION_DEFS.filter(section => section.modelTier !== 'fast').length
|
|
658
|
-
|
|
699
|
+
let estimatedFastSectionInputTokens = SECTION_DEFS.filter(section => section.modelTier === 'fast')
|
|
659
700
|
.reduce((sum, section) => sum + estimatedSectionInputs[section.contextKind] + 500, 0)
|
|
660
701
|
const estimatedFastSectionOutputTokens = fastSectionCalls * 500
|
|
661
|
-
|
|
702
|
+
let estimatedFullSectionInputTokens = SECTION_DEFS.filter(section => section.modelTier !== 'fast')
|
|
662
703
|
.reduce((sum, section) => sum + estimatedSectionInputs[section.contextKind] + 650, 0)
|
|
663
704
|
const estimatedFullSectionOutputTokens = fullSectionCalls * 700
|
|
664
|
-
|
|
705
|
+
let estimatedAtAGlanceInputTokens = estimatedSectionInputs.at_a_glance + 2200
|
|
665
706
|
const estimatedAtAGlanceOutputTokens = 260
|
|
707
|
+
const estimatedSummaryInputTokens =
|
|
708
|
+
estimatedChunkSummaryInputTokens + estimatedCombineSummaryInputTokens
|
|
709
|
+
const estimatedSummaryOutputTokens =
|
|
710
|
+
estimatedChunkSummaryOutputTokens + estimatedCombineSummaryOutputTokens
|
|
711
|
+
|
|
712
|
+
estimatedFacetInputTokens += estimateCodexCliFreshOverhead(provider, facetModel, uncachedFacetJobs.length)
|
|
713
|
+
estimatedChunkSummaryInputTokens += estimateCodexCliFreshOverhead(provider, facetModel, chunkSummaryCalls)
|
|
714
|
+
estimatedCombineSummaryInputTokens += estimateCodexCliFreshOverhead(provider, facetModel, combineSummaryCalls)
|
|
715
|
+
estimatedFastSectionInputTokens += estimateCodexCliFreshOverhead(provider, fastSectionModel, fastSectionCalls)
|
|
716
|
+
estimatedFullSectionInputTokens += estimateCodexCliFreshOverhead(provider, insightModel, fullSectionCalls)
|
|
717
|
+
estimatedAtAGlanceInputTokens += estimateCodexCliFreshOverhead(provider, insightModel, 1)
|
|
666
718
|
|
|
667
719
|
const byStage = [
|
|
668
720
|
buildEstimateBucket(
|
|
@@ -675,16 +727,16 @@ export async function estimateLlmAnalysisCost({ threadSummaries, options = {} })
|
|
|
675
727
|
buildEstimateBucket(
|
|
676
728
|
'transcript_summary:chunk',
|
|
677
729
|
chunkSummaryCalls,
|
|
678
|
-
|
|
730
|
+
estimatedChunkSummaryInputTokens,
|
|
679
731
|
0,
|
|
680
|
-
|
|
732
|
+
estimatedChunkSummaryOutputTokens,
|
|
681
733
|
),
|
|
682
734
|
buildEstimateBucket(
|
|
683
735
|
'transcript_summary:combine',
|
|
684
736
|
combineSummaryCalls,
|
|
685
|
-
|
|
737
|
+
estimatedCombineSummaryInputTokens,
|
|
686
738
|
0,
|
|
687
|
-
|
|
739
|
+
estimatedCombineSummaryOutputTokens,
|
|
688
740
|
),
|
|
689
741
|
buildEstimateBucket(
|
|
690
742
|
'section:fast',
|
|
@@ -784,6 +836,14 @@ export async function estimateLlmAnalysisCost({ threadSummaries, options = {} })
|
|
|
784
836
|
}
|
|
785
837
|
}
|
|
786
838
|
|
|
839
|
+
function estimateCodexCliFreshOverhead(provider, model, calls) {
|
|
840
|
+
if (provider !== 'codex-cli' || !calls) return 0
|
|
841
|
+
const normalized = String(model || '').trim()
|
|
842
|
+
if (normalized === 'gpt-5.4') return calls * 25_000
|
|
843
|
+
if (normalized === 'gpt-5.4-mini' || normalized === 'gpt-5.3-codex-spark') return calls * 4_500
|
|
844
|
+
return calls * 8_000
|
|
845
|
+
}
|
|
846
|
+
|
|
787
847
|
async function planFacetJobs(threadSummaries, { cacheDir, model, uncachedLimit }) {
|
|
788
848
|
const jobs = []
|
|
789
849
|
let uncachedCount = 0
|
|
@@ -824,61 +884,14 @@ async function getFacetForThread(thread, { cacheDir, model, provider, providerOp
|
|
|
824
884
|
provider,
|
|
825
885
|
providerOptions,
|
|
826
886
|
})
|
|
827
|
-
const prompt =
|
|
828
|
-
|
|
829
|
-
CRITICAL GUIDELINES:
|
|
830
|
-
1. goal_categories should count only what the user explicitly asked for.
|
|
831
|
-
2. user_satisfaction_counts should rely on explicit user signals or strong transcript evidence.
|
|
832
|
-
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.
|
|
833
|
-
4. If the session is mostly warmup, rehearsal, or cache-filling, use warmup_minimal as the only goal category.
|
|
834
|
-
5. If evidence is insufficient after transcript compression, use conservative values such as unclear_from_transcript rather than guessing.
|
|
835
|
-
6. Do not infer the user's goal from assistant or tool activity alone.
|
|
836
|
-
7. Do not count assistant-led exploration or extra implementation work unless the user clearly asked for it.
|
|
837
|
-
|
|
838
|
-
Allowed values:
|
|
839
|
-
- outcome: fully_achieved | mostly_achieved | partially_achieved | not_achieved | unclear_from_transcript
|
|
840
|
-
- assistant_helpfulness: unhelpful | slightly_helpful | moderately_helpful | very_helpful | essential
|
|
841
|
-
- session_type: single_task | multi_task | iterative_refinement | exploration | quick_question
|
|
842
|
-
- primary_success: none | fast_accurate_search | correct_code_edits | good_explanations | proactive_help | multi_file_changes | good_debugging
|
|
843
|
-
|
|
844
|
-
Language:
|
|
845
|
-
- Keep enum values and keys exactly as requested.
|
|
846
|
-
- Write free-text fields in ${describeLanguage(langFromProviderOptions(providerOptions))}.
|
|
847
|
-
|
|
848
|
-
Transcript:
|
|
849
|
-
${transcript}
|
|
850
|
-
|
|
851
|
-
Summary stats:
|
|
852
|
-
${JSON.stringify(
|
|
853
|
-
{
|
|
854
|
-
title: thread.title,
|
|
855
|
-
cwd: thread.cwd,
|
|
856
|
-
durationMinutes: thread.durationMinutes,
|
|
857
|
-
userMessages: thread.userMessages,
|
|
858
|
-
assistantMessages: thread.assistantMessages,
|
|
859
|
-
totalToolCalls: thread.totalToolCalls,
|
|
860
|
-
totalCommandFailures: thread.totalCommandFailures,
|
|
861
|
-
toolCounts: thread.toolCounts,
|
|
862
|
-
toolFailures: thread.toolFailures,
|
|
863
|
-
userInterruptions: thread.userInterruptions,
|
|
864
|
-
usesTaskAgent: thread.usesTaskAgent,
|
|
865
|
-
usesMcp: thread.usesMcp,
|
|
866
|
-
usesWebSearch: thread.usesWebSearch,
|
|
867
|
-
usesWebFetch: thread.usesWebFetch,
|
|
868
|
-
},
|
|
869
|
-
null,
|
|
870
|
-
2,
|
|
871
|
-
)}
|
|
872
|
-
|
|
873
|
-
RESPOND WITH ONLY A VALID JSON OBJECT matching the requested schema.`
|
|
887
|
+
const prompt = buildFacetExtractionPrompt(thread, transcript, langFromProviderOptions(providerOptions))
|
|
874
888
|
|
|
875
889
|
const rawFacet = await callStructuredModel({
|
|
876
890
|
provider,
|
|
877
891
|
model,
|
|
878
892
|
schemaName: 'codex_session_facet',
|
|
879
893
|
schema: FACET_SCHEMA,
|
|
880
|
-
systemPrompt:
|
|
881
|
-
`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(),
|
|
894
|
+
systemPrompt: buildFacetSystemPrompt(langFromProviderOptions(providerOptions)),
|
|
882
895
|
userPrompt: prompt,
|
|
883
896
|
options: {
|
|
884
897
|
...providerOptions,
|
|
@@ -1154,6 +1167,124 @@ ${compactJson(compactInsightDigest(insights))}
|
|
|
1154
1167
|
RESPOND WITH ONLY A VALID JSON OBJECT matching the schema.`
|
|
1155
1168
|
}
|
|
1156
1169
|
|
|
1170
|
+
function buildFacetSystemPrompt(lang) {
|
|
1171
|
+
return `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(lang)}`.trim()
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function buildFacetExtractionPrompt(thread, transcript, lang) {
|
|
1175
|
+
return `Analyze this Codex coding session and extract structured facets.
|
|
1176
|
+
|
|
1177
|
+
CRITICAL GUIDELINES:
|
|
1178
|
+
1. goal_categories should count only what the user explicitly asked for.
|
|
1179
|
+
2. user_satisfaction_counts should rely on explicit user signals or strong transcript evidence.
|
|
1180
|
+
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.
|
|
1181
|
+
4. If the session is mostly warmup, rehearsal, or cache-filling, use warmup_minimal as the only goal category.
|
|
1182
|
+
5. If evidence is insufficient after transcript compression, use conservative values such as unclear_from_transcript rather than guessing.
|
|
1183
|
+
6. Do not infer the user's goal from assistant or tool activity alone.
|
|
1184
|
+
7. Do not count assistant-led exploration or extra implementation work unless the user clearly asked for it.
|
|
1185
|
+
|
|
1186
|
+
Allowed values:
|
|
1187
|
+
- outcome: fully_achieved | mostly_achieved | partially_achieved | not_achieved | unclear_from_transcript
|
|
1188
|
+
- assistant_helpfulness: unhelpful | slightly_helpful | moderately_helpful | very_helpful | essential
|
|
1189
|
+
- session_type: single_task | multi_task | iterative_refinement | exploration | quick_question
|
|
1190
|
+
- primary_success: none | fast_accurate_search | correct_code_edits | good_explanations | proactive_help | multi_file_changes | good_debugging
|
|
1191
|
+
|
|
1192
|
+
Language:
|
|
1193
|
+
- Keep enum values and keys exactly as requested.
|
|
1194
|
+
- Write free-text fields in ${describeLanguage(lang)}.
|
|
1195
|
+
|
|
1196
|
+
Transcript:
|
|
1197
|
+
${transcript}
|
|
1198
|
+
|
|
1199
|
+
Summary stats:
|
|
1200
|
+
${JSON.stringify(
|
|
1201
|
+
{
|
|
1202
|
+
title: thread.title,
|
|
1203
|
+
cwd: thread.cwd,
|
|
1204
|
+
durationMinutes: thread.durationMinutes,
|
|
1205
|
+
userMessages: thread.userMessages,
|
|
1206
|
+
assistantMessages: thread.assistantMessages,
|
|
1207
|
+
totalToolCalls: thread.totalToolCalls,
|
|
1208
|
+
totalCommandFailures: thread.totalCommandFailures,
|
|
1209
|
+
toolCounts: thread.toolCounts,
|
|
1210
|
+
toolFailures: thread.toolFailures,
|
|
1211
|
+
userInterruptions: thread.userInterruptions,
|
|
1212
|
+
usesTaskAgent: thread.usesTaskAgent,
|
|
1213
|
+
usesMcp: thread.usesMcp,
|
|
1214
|
+
usesWebSearch: thread.usesWebSearch,
|
|
1215
|
+
usesWebFetch: thread.usesWebFetch,
|
|
1216
|
+
},
|
|
1217
|
+
null,
|
|
1218
|
+
2,
|
|
1219
|
+
)}
|
|
1220
|
+
|
|
1221
|
+
RESPOND WITH ONLY A VALID JSON OBJECT matching the requested schema.`
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function buildEstimatedFacet(thread) {
|
|
1225
|
+
return {
|
|
1226
|
+
threadId: thread.id,
|
|
1227
|
+
title: thread.title,
|
|
1228
|
+
cwd: thread.cwd,
|
|
1229
|
+
updatedAt: thread.updatedAt,
|
|
1230
|
+
durationMinutes: thread.durationMinutes,
|
|
1231
|
+
userMessages: thread.userMessages,
|
|
1232
|
+
assistantMessages: thread.assistantMessages,
|
|
1233
|
+
totalToolCalls: thread.totalToolCalls,
|
|
1234
|
+
totalCommandFailures: thread.totalCommandFailures,
|
|
1235
|
+
underlying_goal: truncateForContext(thread.firstUserMessage || thread.title, 160),
|
|
1236
|
+
goal_categories: {},
|
|
1237
|
+
outcome: thread.totalCommandFailures > 0 ? 'partially_achieved' : 'unclear_from_transcript',
|
|
1238
|
+
user_satisfaction_counts: {},
|
|
1239
|
+
assistant_helpfulness: 'moderately_helpful',
|
|
1240
|
+
session_type: thread.userMessages > 2 ? 'iterative_refinement' : 'single_task',
|
|
1241
|
+
friction_counts: thread.totalCommandFailures > 0 ? { tool_failed: 1 } : {},
|
|
1242
|
+
friction_detail: 'none',
|
|
1243
|
+
primary_success: 'none',
|
|
1244
|
+
brief_summary: truncateForContext(thread.firstUserMessage || thread.title, 180),
|
|
1245
|
+
user_instructions: [],
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function buildEstimatedInsightsPlaceholder(context) {
|
|
1250
|
+
return {
|
|
1251
|
+
project_areas: {
|
|
1252
|
+
areas: (context.session_summaries || []).slice(0, 3).map((item, index) => ({
|
|
1253
|
+
name: item.project || item.title || `Workstream ${index + 1}`,
|
|
1254
|
+
session_count: 1,
|
|
1255
|
+
description: truncateForContext(item.summary || item.goal || '', 120),
|
|
1256
|
+
})),
|
|
1257
|
+
},
|
|
1258
|
+
interaction_style: {
|
|
1259
|
+
key_pattern: 'Tight scope before execution',
|
|
1260
|
+
narrative: truncateForContext('You first align scope and constraints, then execute and verify against explicit acceptance bars.', 180),
|
|
1261
|
+
},
|
|
1262
|
+
what_works: {
|
|
1263
|
+
impressive_workflows: [
|
|
1264
|
+
{ title: 'Scope first', description: 'You repeatedly tighten scope before execution.' },
|
|
1265
|
+
{ title: 'Verification loop', description: 'You use evidence to confirm changes before closing.' },
|
|
1266
|
+
],
|
|
1267
|
+
},
|
|
1268
|
+
friction_analysis: {
|
|
1269
|
+
categories: [
|
|
1270
|
+
{ category: 'Direction drift', description: 'Some sessions need scope tightening.', examples: ['Scope had to be pulled back.'] },
|
|
1271
|
+
],
|
|
1272
|
+
},
|
|
1273
|
+
suggestions: {
|
|
1274
|
+
features_to_try: [{ feature: 'AGENTS.md' }],
|
|
1275
|
+
usage_patterns: [{ title: 'Split review and execution' }],
|
|
1276
|
+
agents_md_additions: [{ addition: 'Read existing docs before editing.' }],
|
|
1277
|
+
},
|
|
1278
|
+
on_the_horizon: {
|
|
1279
|
+
opportunities: [{ title: 'Longer workflows', how_to_try: 'Add staged execution.' }],
|
|
1280
|
+
},
|
|
1281
|
+
fun_ending: {
|
|
1282
|
+
headline: 'Memorable moment',
|
|
1283
|
+
detail: 'A compact placeholder for estimate sizing.',
|
|
1284
|
+
},
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1157
1288
|
/**
|
|
1158
1289
|
* @param {InsightRunOptions} options
|
|
1159
1290
|
* @param {string} provider
|
|
@@ -1172,71 +1303,36 @@ function buildProviderOptions(options, provider, onUsage) {
|
|
|
1172
1303
|
}
|
|
1173
1304
|
}
|
|
1174
1305
|
|
|
1175
|
-
function estimateSectionInputs(candidateThreads, facetJobs) {
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
total_user_messages: candidateThreads.reduce((sum, thread) => sum + Number(thread.userMessages || 0), 0),
|
|
1182
|
-
total_tool_calls: candidateThreads.reduce((sum, thread) => sum + Number(thread.totalToolCalls || 0), 0),
|
|
1183
|
-
total_failures: candidateThreads.reduce((sum, thread) => sum + Number(thread.totalCommandFailures || 0), 0),
|
|
1184
|
-
},
|
|
1185
|
-
charts: {
|
|
1186
|
-
projects: candidateThreads.slice(0, 6).map(thread => compactProjectPath(thread.cwd)),
|
|
1187
|
-
models: Array.from(new Set(candidateThreads.map(thread => thread.model).filter(Boolean))).slice(0, 4),
|
|
1188
|
-
tools: [],
|
|
1189
|
-
},
|
|
1190
|
-
aggregate_facets: {
|
|
1191
|
-
sessions_with_facets: facetJobs.length,
|
|
1192
|
-
},
|
|
1193
|
-
session_summaries: facetJobs.slice(0, MAX_CONTEXT_FACETS).map(job => {
|
|
1194
|
-
if (job.cachedFacet) {
|
|
1195
|
-
return {
|
|
1196
|
-
title: truncateForContext(job.cachedFacet.title, 80),
|
|
1197
|
-
project: compactProjectPath(job.cachedFacet.cwd),
|
|
1198
|
-
goal: truncateForContext(job.cachedFacet.underlying_goal, 120),
|
|
1199
|
-
outcome: job.cachedFacet.outcome,
|
|
1200
|
-
primary_success: job.cachedFacet.primary_success,
|
|
1201
|
-
summary: truncateForContext(job.cachedFacet.brief_summary, 160),
|
|
1202
|
-
friction: compactCountObject(job.cachedFacet.friction_counts, 2),
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
return {
|
|
1206
|
-
title: truncateForContext(job.thread.title, 80),
|
|
1207
|
-
project: compactProjectPath(job.thread.cwd),
|
|
1208
|
-
goal: truncateForContext(job.thread.firstUserMessage, 120),
|
|
1209
|
-
outcome: 'unknown',
|
|
1210
|
-
primary_success: 'unknown',
|
|
1211
|
-
summary: truncateForContext(job.thread.firstUserMessage, 160),
|
|
1212
|
-
friction: {},
|
|
1213
|
-
}
|
|
1214
|
-
}),
|
|
1215
|
-
recent_threads: candidateThreads.slice(0, MAX_RECENT_THREADS).map(thread => ({
|
|
1216
|
-
title: truncateForContext(thread.title, 80),
|
|
1217
|
-
project: compactProjectPath(thread.cwd),
|
|
1218
|
-
duration_minutes: thread.durationMinutes,
|
|
1219
|
-
user_messages: thread.userMessages,
|
|
1220
|
-
tool_calls: thread.totalToolCalls,
|
|
1221
|
-
files_modified: thread.filesModified,
|
|
1222
|
-
})),
|
|
1223
|
-
friction_details: [],
|
|
1224
|
-
user_instructions: [],
|
|
1225
|
-
}
|
|
1226
|
-
const sectionKinds = [
|
|
1227
|
-
'project_areas',
|
|
1228
|
-
'interaction_style',
|
|
1229
|
-
'what_works',
|
|
1230
|
-
'friction_analysis',
|
|
1231
|
-
'suggestions',
|
|
1232
|
-
'on_the_horizon',
|
|
1233
|
-
'fun_ending',
|
|
1234
|
-
'at_a_glance',
|
|
1235
|
-
]
|
|
1306
|
+
function estimateSectionInputs(candidateThreads, facetJobs, options = {}) {
|
|
1307
|
+
const provider = options.provider || DEFAULT_PROVIDER
|
|
1308
|
+
const lang = options.lang || 'en'
|
|
1309
|
+
const estimatedFacets = facetJobs.map(job => job.cachedFacet || buildEstimatedFacet(job.thread))
|
|
1310
|
+
const report = buildReport(candidateThreads, { facets: estimatedFacets })
|
|
1311
|
+
const context = buildInsightContext(report, candidateThreads, estimatedFacets)
|
|
1236
1312
|
const estimated = {}
|
|
1237
|
-
|
|
1238
|
-
|
|
1313
|
+
|
|
1314
|
+
for (const section of SECTION_DEFS) {
|
|
1315
|
+
const systemPrompt = `${SECTION_SYSTEM_PROMPT} ${getNarrativeLanguageInstruction(lang)}`.trim()
|
|
1316
|
+
const sectionContext = buildSectionContext(context, section.contextKind)
|
|
1317
|
+
const userPrompt = `${section.prompt}\n\n${getNarrativeLanguageInstruction(lang)}\n\nDATA:\n${compactJson(sectionContext)}`
|
|
1318
|
+
estimated[section.contextKind] = estimateModelInputTokens({
|
|
1319
|
+
provider,
|
|
1320
|
+
systemPrompt,
|
|
1321
|
+
userPrompt,
|
|
1322
|
+
schema: section.schema,
|
|
1323
|
+
structured: true,
|
|
1324
|
+
})
|
|
1239
1325
|
}
|
|
1326
|
+
|
|
1327
|
+
const placeholderInsights = buildEstimatedInsightsPlaceholder(context)
|
|
1328
|
+
estimated.at_a_glance = estimateModelInputTokens({
|
|
1329
|
+
provider,
|
|
1330
|
+
systemPrompt: `${AT_A_GLANCE_SYSTEM_PROMPT} ${getNarrativeLanguageInstruction(lang)}`.trim(),
|
|
1331
|
+
userPrompt: buildAtAGlancePrompt(buildSectionContext(context, 'at_a_glance'), placeholderInsights),
|
|
1332
|
+
schema: AT_A_GLANCE_SCHEMA,
|
|
1333
|
+
structured: true,
|
|
1334
|
+
})
|
|
1335
|
+
|
|
1240
1336
|
return estimated
|
|
1241
1337
|
}
|
|
1242
1338
|
|
|
@@ -1397,6 +1493,36 @@ function estimateTokensFromChars(chars) {
|
|
|
1397
1493
|
return Math.ceil(Number(chars || 0) / 4)
|
|
1398
1494
|
}
|
|
1399
1495
|
|
|
1496
|
+
function estimateModelInputTokens({ provider, systemPrompt, userPrompt, schema = null, structured }) {
|
|
1497
|
+
const promptText =
|
|
1498
|
+
provider === 'codex-cli'
|
|
1499
|
+
? structured
|
|
1500
|
+
? buildStructuredEstimatePrompt(systemPrompt, userPrompt, schema)
|
|
1501
|
+
: buildPlainEstimatePrompt(systemPrompt, userPrompt)
|
|
1502
|
+
: buildApiEstimatePrompt(systemPrompt, userPrompt, schema, structured)
|
|
1503
|
+
return estimateTokensFromChars(promptText.length)
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function buildPlainEstimatePrompt(systemPrompt, userPrompt) {
|
|
1507
|
+
return `${String(systemPrompt || '').trim()}\n\n${String(userPrompt || '').trim()}`
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function buildStructuredEstimatePrompt(systemPrompt, userPrompt, schema) {
|
|
1511
|
+
return `${buildPlainEstimatePrompt(systemPrompt, userPrompt)}\n\nRESPOND WITH ONLY A VALID JSON OBJECT matching this schema:\n${JSON.stringify(schema, null, 2)}`
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function buildApiEstimatePrompt(systemPrompt, userPrompt, schema, structured) {
|
|
1515
|
+
if (!structured) return buildPlainEstimatePrompt(systemPrompt, userPrompt)
|
|
1516
|
+
return `${buildPlainEstimatePrompt(systemPrompt, userPrompt)}\n\nJSON schema:\n${JSON.stringify(schema, null, 2)}`
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function makePlaceholderText(length, prefix = '') {
|
|
1520
|
+
const target = Math.max(0, Number(length || 0))
|
|
1521
|
+
const seed = prefix ? `${prefix}\n` : ''
|
|
1522
|
+
if (seed.length >= target) return seed.slice(0, target)
|
|
1523
|
+
return `${seed}${'x'.repeat(Math.max(0, target - seed.length))}`
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1400
1526
|
function getNarrativeLanguageInstruction(lang) {
|
|
1401
1527
|
if (lang === 'zh-CN') {
|
|
1402
1528
|
return 'Write all free-text narrative fields in Simplified Chinese.'
|
package/lib/report.js
CHANGED
|
@@ -178,6 +178,7 @@ export async function writeReportFiles(report, options) {
|
|
|
178
178
|
|
|
179
179
|
export function renderTerminalSummary(report) {
|
|
180
180
|
const text = getReportText(report.metadata.language)
|
|
181
|
+
const estimateComparison = buildEstimateComparison(report)
|
|
181
182
|
const lines = []
|
|
182
183
|
lines.push(text.reportTitle)
|
|
183
184
|
lines.push(
|
|
@@ -190,6 +191,11 @@ export function renderTerminalSummary(report) {
|
|
|
190
191
|
lines.push(
|
|
191
192
|
` ${text.inputLabel}=${formatMillionTokens(report.analysisUsage.inputTokens)} | ${text.cachedLabel}=${formatMillionTokens(report.analysisUsage.cachedInputTokens)} | ${text.outputLabel}=${formatMillionTokens(report.analysisUsage.outputTokens)}`,
|
|
192
193
|
)
|
|
194
|
+
if (estimateComparison) {
|
|
195
|
+
lines.push(
|
|
196
|
+
` ${text.estimateVsActualLabel}: ${formatMillionTokens(estimateComparison.estimatedTotalTokens)} -> ${formatMillionTokens(estimateComparison.actualTotalTokens)} ${text.actualFreshSuffix} (${formatSignedMillionTokens(estimateComparison.deltaTokens)}, ${formatSignedPercent(estimateComparison.deltaPercent)})`,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
193
199
|
}
|
|
194
200
|
if (report.metadata.dateRange.start && report.metadata.dateRange.end) {
|
|
195
201
|
lines.push(`${report.metadata.dateRange.start} -> ${report.metadata.dateRange.end}`)
|
|
@@ -214,8 +220,12 @@ function renderHtmlReport(report) {
|
|
|
214
220
|
const analysisUsage = report.analysisUsage || null
|
|
215
221
|
const topProjects = renderBarList(report.charts.projects, { formatLabel: formatProjectLabel })
|
|
216
222
|
const modelMix = renderBarList(report.charts.models)
|
|
217
|
-
const sessionTypes = renderBarList(report.charts.sessionTypes
|
|
218
|
-
|
|
223
|
+
const sessionTypes = renderBarList(report.charts.sessionTypes, {
|
|
224
|
+
formatLabel: value => formatSessionTypeLabel(value, report.metadata.language),
|
|
225
|
+
})
|
|
226
|
+
const outcomes = renderBarList(report.charts.outcomes, {
|
|
227
|
+
formatLabel: value => formatOutcomeLabel(value, report.metadata.language),
|
|
228
|
+
})
|
|
219
229
|
const capabilitySignals = renderBarList(report.charts.capabilities)
|
|
220
230
|
const toolFailures = renderBarList(report.charts.toolFailures)
|
|
221
231
|
const toolErrorCategories = renderBarList(report.charts.toolErrorCategories)
|
|
@@ -793,7 +803,7 @@ function renderProjectAreas(insights) {
|
|
|
793
803
|
<div class="project-area">
|
|
794
804
|
<div class="area-header">
|
|
795
805
|
<span class="area-name">${escapeHtml(area.name)}</span>
|
|
796
|
-
<span class="area-count"
|
|
806
|
+
<span class="area-count">${escapeHtml(text.workstreamBadge)}</span>
|
|
797
807
|
</div>
|
|
798
808
|
<div class="area-desc">${escapeHtml(area.description)}</div>
|
|
799
809
|
</div>
|
|
@@ -875,9 +885,9 @@ function renderSuggestions(insights) {
|
|
|
875
885
|
const suggestions = insights.suggestions
|
|
876
886
|
if (!suggestions) return ''
|
|
877
887
|
const text = getReportText(insights.__lang)
|
|
878
|
-
const agentItems = suggestions.agents_md_additions || []
|
|
879
|
-
const featureItems = suggestions.features_to_try || []
|
|
880
|
-
const patternItems = suggestions.usage_patterns || []
|
|
888
|
+
const agentItems = (suggestions.agents_md_additions || []).map(normalizeSuggestionTextItem)
|
|
889
|
+
const featureItems = (suggestions.features_to_try || []).map(normalizeSuggestionTextItem)
|
|
890
|
+
const patternItems = (suggestions.usage_patterns || []).map(normalizeSuggestionTextItem)
|
|
881
891
|
|
|
882
892
|
return `
|
|
883
893
|
<section class="panel">
|
|
@@ -995,6 +1005,7 @@ function renderReportMeta(report, context = {}) {
|
|
|
995
1005
|
const text = getReportText(report.metadata.language)
|
|
996
1006
|
const usage = report.analysisUsage
|
|
997
1007
|
const hasUsage = Boolean(usage?.totalTokens)
|
|
1008
|
+
const estimateComparison = buildEstimateComparison(report)
|
|
998
1009
|
const analysisByStage = context.analysisByStage || ''
|
|
999
1010
|
const analysisByModel = context.analysisByModel || ''
|
|
1000
1011
|
|
|
@@ -1010,6 +1021,7 @@ function renderReportMeta(report, context = {}) {
|
|
|
1010
1021
|
${hasUsage ? renderStat(text.analysisTokens, formatMillionTokens(usage.totalTokens)) : ''}
|
|
1011
1022
|
${hasUsage ? renderStat(text.modelCalls, formatNumber(usage.calls)) : ''}
|
|
1012
1023
|
${hasUsage ? renderStat(text.cachedInput, formatMillionTokens(usage.cachedInputTokens)) : ''}
|
|
1024
|
+
${estimateComparison ? renderStat(text.estimateDelta, `${formatSignedMillionTokens(estimateComparison.deltaTokens)} (${formatSignedPercent(estimateComparison.deltaPercent)})`) : ''}
|
|
1013
1025
|
${renderStat(text.historicalSessionTokens, formatNumber(report.summary.totalTokens))}
|
|
1014
1026
|
</div>
|
|
1015
1027
|
${
|
|
@@ -1035,6 +1047,14 @@ function renderReportMeta(report, context = {}) {
|
|
|
1035
1047
|
})}
|
|
1036
1048
|
<p class="meta">${escapeHtml(text.freshInput)}: ${formatMillionTokens(freshInputTokens)}. ${escapeHtml(text.analysisCostFootnote)}</p>
|
|
1037
1049
|
</div>
|
|
1050
|
+
${
|
|
1051
|
+
estimateComparison
|
|
1052
|
+
? `<div class="usage-breakdown">
|
|
1053
|
+
<h3>${escapeHtml(text.estimateVsActualHeading)}</h3>
|
|
1054
|
+
${renderEstimateComparisonCard(estimateComparison, text)}
|
|
1055
|
+
</div>`
|
|
1056
|
+
: ''
|
|
1057
|
+
}
|
|
1038
1058
|
</div>`
|
|
1039
1059
|
: ''
|
|
1040
1060
|
}
|
|
@@ -1079,6 +1099,29 @@ function renderUsageCard(item) {
|
|
|
1079
1099
|
`
|
|
1080
1100
|
}
|
|
1081
1101
|
|
|
1102
|
+
function renderEstimateComparisonCard(comparison, text) {
|
|
1103
|
+
return `
|
|
1104
|
+
<div class="usage-card">
|
|
1105
|
+
<div class="topline">
|
|
1106
|
+
<h3>${escapeHtml(text.estimateVsActualLabel)}</h3>
|
|
1107
|
+
<span class="pill">${escapeHtml(comparison.verdictLabel)}</span>
|
|
1108
|
+
</div>
|
|
1109
|
+
<div class="meta-row">
|
|
1110
|
+
<span>${escapeHtml(text.estimatedLabel)}: ${formatMillionTokens(comparison.estimatedTotalTokens)}</span>
|
|
1111
|
+
<span>${escapeHtml(text.actualFreshLabel)}: ${formatMillionTokens(comparison.actualTotalTokens)}</span>
|
|
1112
|
+
<span>${escapeHtml(text.estimateDelta)}: ${formatSignedMillionTokens(comparison.deltaTokens)} (${formatSignedPercent(comparison.deltaPercent)})</span>
|
|
1113
|
+
</div>
|
|
1114
|
+
<div class="token-row">
|
|
1115
|
+
<div class="token-box"><strong>${escapeHtml(text.estimatedLabel)}</strong><br>${formatMillionTokens(comparison.estimatedTotalTokens)}</div>
|
|
1116
|
+
<div class="token-box"><strong>${escapeHtml(text.actualFreshLabel)}</strong><br>${formatMillionTokens(comparison.actualTotalTokens)}</div>
|
|
1117
|
+
<div class="token-box"><strong>${escapeHtml(text.estimateRangeLabel)}</strong><br>${formatMillionTokens(comparison.lowEstimate)} -> ${formatMillionTokens(comparison.highEstimate)}</div>
|
|
1118
|
+
<div class="token-box"><strong>${escapeHtml(text.estimateDelta)}</strong><br>${formatSignedMillionTokens(comparison.deltaTokens)}</div>
|
|
1119
|
+
<div class="token-box"><strong>${escapeHtml(text.estimateError)}</strong><br>${formatSignedPercent(comparison.deltaPercent)}</div>
|
|
1120
|
+
</div>
|
|
1121
|
+
</div>
|
|
1122
|
+
`
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1082
1125
|
function renderStat(label, value) {
|
|
1083
1126
|
return `<div class="stat"><div class="value">${escapeHtml(String(value))}</div><div>${escapeHtml(label)}</div></div>`
|
|
1084
1127
|
}
|
|
@@ -1147,6 +1190,20 @@ function renderCopyRow(value, text) {
|
|
|
1147
1190
|
`
|
|
1148
1191
|
}
|
|
1149
1192
|
|
|
1193
|
+
function normalizeSuggestionTextItem(item) {
|
|
1194
|
+
if (!item || typeof item !== 'object') return item
|
|
1195
|
+
return Object.fromEntries(
|
|
1196
|
+
Object.entries(item).map(([key, value]) => [key, normalizeSuggestionText(value)]),
|
|
1197
|
+
)
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function normalizeSuggestionText(value) {
|
|
1201
|
+
if (typeof value !== 'string') return value
|
|
1202
|
+
return value
|
|
1203
|
+
.replaceAll('CLAUDE.md / AGENTS.md', 'AGENTS.md')
|
|
1204
|
+
.replaceAll('CLAUDE.md', 'AGENTS.md')
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1150
1207
|
function formatAgentInstruction(item) {
|
|
1151
1208
|
const placement = String(item?.prompt_scaffold || '').trim()
|
|
1152
1209
|
const addition = String(item?.addition || '').trim()
|
|
@@ -1171,6 +1228,44 @@ function formatProjectLabel(value) {
|
|
|
1171
1228
|
return formatDisplayPath(value, { tailSegments: 2, preferHomeAlias: false, ellipsis: true })
|
|
1172
1229
|
}
|
|
1173
1230
|
|
|
1231
|
+
function formatSessionTypeLabel(value, lang) {
|
|
1232
|
+
const key = String(value || '')
|
|
1233
|
+
const zh = {
|
|
1234
|
+
iterative_refinement: '反复收敛',
|
|
1235
|
+
exploration: '探索调研',
|
|
1236
|
+
single_task: '单任务推进',
|
|
1237
|
+
multi_task: '多任务并行',
|
|
1238
|
+
quick_question: '快速提问',
|
|
1239
|
+
}
|
|
1240
|
+
const en = {
|
|
1241
|
+
iterative_refinement: 'Iterative refinement',
|
|
1242
|
+
exploration: 'Exploration',
|
|
1243
|
+
single_task: 'Single-task execution',
|
|
1244
|
+
multi_task: 'Multi-task coordination',
|
|
1245
|
+
quick_question: 'Quick question',
|
|
1246
|
+
}
|
|
1247
|
+
return lang === 'zh-CN' ? zh[key] || key : en[key] || key
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function formatOutcomeLabel(value, lang) {
|
|
1251
|
+
const key = String(value || '')
|
|
1252
|
+
const zh = {
|
|
1253
|
+
fully_achieved: '完全达成',
|
|
1254
|
+
mostly_achieved: '基本达成',
|
|
1255
|
+
partially_achieved: '部分达成',
|
|
1256
|
+
not_achieved: '未达成',
|
|
1257
|
+
unclear_from_transcript: '从记录中无法判断',
|
|
1258
|
+
}
|
|
1259
|
+
const en = {
|
|
1260
|
+
fully_achieved: 'Fully achieved',
|
|
1261
|
+
mostly_achieved: 'Mostly achieved',
|
|
1262
|
+
partially_achieved: 'Partially achieved',
|
|
1263
|
+
not_achieved: 'Not achieved',
|
|
1264
|
+
unclear_from_transcript: 'Unclear from transcript',
|
|
1265
|
+
}
|
|
1266
|
+
return lang === 'zh-CN' ? zh[key] || key : en[key] || key
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1174
1269
|
function formatDisplayPath(value, options = {}) {
|
|
1175
1270
|
const text = String(value || '').trim()
|
|
1176
1271
|
if (!text) return '(unknown)'
|
|
@@ -1225,6 +1320,12 @@ function formatMillionTokens(value) {
|
|
|
1225
1320
|
return `${round(number / 1_000)}K tokens`
|
|
1226
1321
|
}
|
|
1227
1322
|
|
|
1323
|
+
function formatSignedMillionTokens(value) {
|
|
1324
|
+
const number = Number(value || 0)
|
|
1325
|
+
const prefix = number > 0 ? '+' : number < 0 ? '-' : ''
|
|
1326
|
+
return `${prefix}${formatMillionTokens(Math.abs(number))}`
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1228
1329
|
function formatPercent(value, total) {
|
|
1229
1330
|
const numerator = Number(value || 0)
|
|
1230
1331
|
const denominator = Number(total || 0)
|
|
@@ -1232,6 +1333,12 @@ function formatPercent(value, total) {
|
|
|
1232
1333
|
return `${round((numerator / denominator) * 100)}%`
|
|
1233
1334
|
}
|
|
1234
1335
|
|
|
1336
|
+
function formatSignedPercent(value) {
|
|
1337
|
+
const number = Number(value || 0)
|
|
1338
|
+
const prefix = number > 0 ? '+' : number < 0 ? '-' : ''
|
|
1339
|
+
return `${prefix}${round(Math.abs(number))}%`
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1235
1342
|
function average(values) {
|
|
1236
1343
|
if (!values.length) return 0
|
|
1237
1344
|
return round(values.reduce((sum, value) => sum + value, 0) / values.length)
|
|
@@ -1286,6 +1393,41 @@ function escapeAttribute(value) {
|
|
|
1286
1393
|
return escapeHtml(String(value)).replaceAll("'", ''').replaceAll('\n', ' ')
|
|
1287
1394
|
}
|
|
1288
1395
|
|
|
1396
|
+
function buildEstimateComparison(report) {
|
|
1397
|
+
const estimate = report.analysisEstimate
|
|
1398
|
+
const usage = report.analysisUsage
|
|
1399
|
+
if (!estimate?.estimatedTotalTokens || !usage?.totalTokens) return null
|
|
1400
|
+
|
|
1401
|
+
const estimated = Number(estimate.estimatedTotalTokens || 0)
|
|
1402
|
+
const actualFresh =
|
|
1403
|
+
Math.max(0, Number(usage.inputTokens || 0) - Number(usage.cachedInputTokens || 0)) +
|
|
1404
|
+
Number(usage.outputTokens || 0)
|
|
1405
|
+
const delta = actualFresh - estimated
|
|
1406
|
+
const deltaPercent = estimated > 0 ? (delta / estimated) * 100 : 0
|
|
1407
|
+
|
|
1408
|
+
return {
|
|
1409
|
+
estimatedTotalTokens: estimated,
|
|
1410
|
+
actualTotalTokens: actualFresh,
|
|
1411
|
+
lowEstimate: Number(estimate.estimatedRange?.low || estimated),
|
|
1412
|
+
highEstimate: Number(estimate.estimatedRange?.high || estimated),
|
|
1413
|
+
deltaTokens: delta,
|
|
1414
|
+
deltaPercent,
|
|
1415
|
+
verdictLabel: classifyEstimateDelta(deltaPercent, report.metadata.language),
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function classifyEstimateDelta(deltaPercent, lang) {
|
|
1420
|
+
const abs = Math.abs(Number(deltaPercent || 0))
|
|
1421
|
+
if (lang === 'zh-CN') {
|
|
1422
|
+
if (abs <= 10) return '估算接近'
|
|
1423
|
+
if (deltaPercent > 0) return '实际偏高'
|
|
1424
|
+
return '实际偏低'
|
|
1425
|
+
}
|
|
1426
|
+
if (abs <= 10) return 'Close estimate'
|
|
1427
|
+
if (deltaPercent > 0) return 'Actual higher'
|
|
1428
|
+
return 'Actual lower'
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1289
1431
|
function getReportText(lang) {
|
|
1290
1432
|
if (lang === 'zh-CN') {
|
|
1291
1433
|
return {
|
|
@@ -1304,7 +1446,7 @@ function getReportText(lang) {
|
|
|
1304
1446
|
filesModified: '修改文件',
|
|
1305
1447
|
toolErrors: '工具错误',
|
|
1306
1448
|
avgResponse: '平均响应',
|
|
1307
|
-
topProjects: '
|
|
1449
|
+
topProjects: '项目分布',
|
|
1308
1450
|
modelMix: '模型分布',
|
|
1309
1451
|
sessionTypes: '会话类型',
|
|
1310
1452
|
outcomes: '结果分布',
|
|
@@ -1318,6 +1460,7 @@ function getReportText(lang) {
|
|
|
1318
1460
|
quickWins: '可以立刻尝试的优化',
|
|
1319
1461
|
ambitiousWorkflows: '值得尝试的更强工作流',
|
|
1320
1462
|
sessionsLabel: '次会话',
|
|
1463
|
+
workstreamBadge: '代表性工作流',
|
|
1321
1464
|
impressiveThingsLink: '做得好的地方',
|
|
1322
1465
|
whereThingsGoWrongLink: '容易出问题的地方',
|
|
1323
1466
|
whatYouWorkOn: '你主要在做什么',
|
|
@@ -1348,6 +1491,15 @@ function getReportText(lang) {
|
|
|
1348
1491
|
dateRange: '时间范围',
|
|
1349
1492
|
modelCalls: '模型调用',
|
|
1350
1493
|
cachedInput: '缓存输入',
|
|
1494
|
+
estimatedLabel: '预估',
|
|
1495
|
+
actualLabel: '实际',
|
|
1496
|
+
actualFreshLabel: '实际(不含缓存)',
|
|
1497
|
+
actualFreshSuffix: '(不含缓存)',
|
|
1498
|
+
estimateRangeLabel: '预估区间',
|
|
1499
|
+
estimateVsActualHeading: '预估与实际',
|
|
1500
|
+
estimateVsActualLabel: '预估 vs 实际',
|
|
1501
|
+
estimateDelta: '偏差',
|
|
1502
|
+
estimateError: '偏差比例',
|
|
1351
1503
|
historicalSessionTokens: '历史会话 Tokens',
|
|
1352
1504
|
analysisCostByStage: '按阶段拆分的分析成本',
|
|
1353
1505
|
analysisCostByModel: '按模型拆分的分析成本',
|
|
@@ -1397,6 +1549,7 @@ function getReportText(lang) {
|
|
|
1397
1549
|
quickWins: 'Quick wins to try',
|
|
1398
1550
|
ambitiousWorkflows: 'Ambitious workflows',
|
|
1399
1551
|
sessionsLabel: 'sessions',
|
|
1552
|
+
workstreamBadge: 'Representative workstream',
|
|
1400
1553
|
impressiveThingsLink: 'Impressive Things You Did',
|
|
1401
1554
|
whereThingsGoWrongLink: 'Where Things Go Wrong',
|
|
1402
1555
|
whatYouWorkOn: 'What You Work On',
|
|
@@ -1427,6 +1580,15 @@ function getReportText(lang) {
|
|
|
1427
1580
|
dateRange: 'Date Range',
|
|
1428
1581
|
modelCalls: 'Model Calls',
|
|
1429
1582
|
cachedInput: 'Cached Input',
|
|
1583
|
+
estimatedLabel: 'Estimated',
|
|
1584
|
+
actualLabel: 'Actual',
|
|
1585
|
+
actualFreshLabel: 'Actual (fresh)',
|
|
1586
|
+
actualFreshSuffix: '(fresh)',
|
|
1587
|
+
estimateRangeLabel: 'Estimate range',
|
|
1588
|
+
estimateVsActualHeading: 'Estimate vs Actual',
|
|
1589
|
+
estimateVsActualLabel: 'Estimate vs Actual',
|
|
1590
|
+
estimateDelta: 'Delta',
|
|
1591
|
+
estimateError: 'Error',
|
|
1430
1592
|
historicalSessionTokens: 'Historical Session Tokens',
|
|
1431
1593
|
analysisCostByStage: 'Analysis Cost by Stage',
|
|
1432
1594
|
analysisCostByModel: 'Analysis Cost by Model',
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-session-insights",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Generate a report analyzing your Codex sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"codex-session-insights": "
|
|
7
|
+
"codex-session-insights": "bin/codex-insights.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"report": "node ./bin/codex-insights.js report",
|
|
28
|
+
"report:lite": "node ./bin/codex-insights.js --preset lite --yes",
|
|
28
29
|
"generate:test-report": "node ./scripts/generate-test-report.mjs",
|
|
29
30
|
"test": "node --test",
|
|
30
31
|
"check": "node --check ./bin/codex-insights.js && node --check ./lib/cli.js && node --check ./lib/codex-data.js && node --check ./lib/report.js && node --check ./lib/llm-insights.js && node --check ./lib/model-provider.js && npm run typecheck",
|