idletime 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -1
- package/assets/idle-time-notification-icon.png +0 -0
- package/assets/idle-time-notification-icon.svg +33 -0
- package/assets/idle-time-readme.png +0 -0
- package/dist/idletime.js +1864 -131
- package/package.json +3 -3
package/dist/idletime.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// package.json
|
|
3
3
|
var package_default = {
|
|
4
4
|
name: "idletime",
|
|
5
|
-
version: "0.
|
|
5
|
+
version: "0.2.0",
|
|
6
6
|
description: "Visual CLI for Codex focus, token burn, spikes, and idle time from local session logs.",
|
|
7
7
|
author: "ParkerRex",
|
|
8
8
|
main: "./dist/idletime.js",
|
|
@@ -46,10 +46,10 @@ var package_default = {
|
|
|
46
46
|
sideEffects: false,
|
|
47
47
|
scripts: {
|
|
48
48
|
build: "bun run src/release/build-package.ts",
|
|
49
|
-
"check:release": "bun run typecheck && bun test && bun run qa && npm pack --dry-run",
|
|
49
|
+
"check:release": "bun run build && bun run typecheck && bun test && bun run qa && npm pack --dry-run",
|
|
50
50
|
dev: "bun run src/cli/idletime-bin.ts",
|
|
51
51
|
idletime: "bun run src/cli/idletime-bin.ts",
|
|
52
|
-
"pack:dry-run": "npm pack --dry-run",
|
|
52
|
+
"pack:dry-run": "bun run build && npm pack --dry-run",
|
|
53
53
|
"publish:dry-run": "bun run build && bun publish --dry-run --access public",
|
|
54
54
|
prepublishOnly: "bun run check:release",
|
|
55
55
|
qa: "bun run qa:gaps && bun run qa:journeys",
|
|
@@ -64,6 +64,211 @@ var package_default = {
|
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
+
// src/reporting/types.ts
|
|
68
|
+
var jsonReportSchemaVersion = 1;
|
|
69
|
+
|
|
70
|
+
// src/reporting/serialize-json-common.ts
|
|
71
|
+
function stringifyJsonSnapshot(snapshot) {
|
|
72
|
+
return `${JSON.stringify(snapshot, null, 2)}
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
function serializeIsoTimestamp(timestamp) {
|
|
76
|
+
return timestamp.toISOString();
|
|
77
|
+
}
|
|
78
|
+
function serializeTimeInterval(interval) {
|
|
79
|
+
return {
|
|
80
|
+
start: serializeIsoTimestamp(interval.start),
|
|
81
|
+
end: serializeIsoTimestamp(interval.end)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function serializeReportWindow(window) {
|
|
85
|
+
return {
|
|
86
|
+
label: window.label,
|
|
87
|
+
start: serializeIsoTimestamp(window.start),
|
|
88
|
+
end: serializeIsoTimestamp(window.end),
|
|
89
|
+
timeZone: window.timeZone
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function serializeSessionFilters(filters) {
|
|
93
|
+
return {
|
|
94
|
+
workspaceOnlyPrefix: filters.workspaceOnlyPrefix,
|
|
95
|
+
sessionKind: filters.sessionKind,
|
|
96
|
+
model: filters.model,
|
|
97
|
+
reasoningEffort: filters.reasoningEffort
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function serializeActivityMetrics(metrics) {
|
|
101
|
+
return {
|
|
102
|
+
strictEngagementBlocks: metrics.strictEngagementBlocks.map(serializeTimeInterval),
|
|
103
|
+
directActivityBlocks: metrics.directActivityBlocks.map(serializeTimeInterval),
|
|
104
|
+
agentCoverageBlocks: metrics.agentCoverageBlocks.map(serializeTimeInterval),
|
|
105
|
+
agentOnlyBlocks: metrics.agentOnlyBlocks.map(serializeTimeInterval),
|
|
106
|
+
perAgentTaskBlocks: metrics.perAgentTaskBlocks.map((taskBlocks) => taskBlocks.map(serializeTimeInterval)),
|
|
107
|
+
strictEngagementMs: metrics.strictEngagementMs,
|
|
108
|
+
directActivityMs: metrics.directActivityMs,
|
|
109
|
+
agentCoverageMs: metrics.agentCoverageMs,
|
|
110
|
+
agentOnlyMs: metrics.agentOnlyMs,
|
|
111
|
+
cumulativeAgentMs: metrics.cumulativeAgentMs,
|
|
112
|
+
peakConcurrentAgents: metrics.peakConcurrentAgents
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function serializeWakeWindowSummary(wakeWindowSummary) {
|
|
116
|
+
return {
|
|
117
|
+
wakeDurationMs: wakeWindowSummary.wakeDurationMs,
|
|
118
|
+
strictEngagementMs: wakeWindowSummary.strictEngagementMs,
|
|
119
|
+
directActivityMs: wakeWindowSummary.directActivityMs,
|
|
120
|
+
agentOnlyMs: wakeWindowSummary.agentOnlyMs,
|
|
121
|
+
awakeIdleMs: wakeWindowSummary.awakeIdleMs,
|
|
122
|
+
awakeIdlePercentage: wakeWindowSummary.awakeIdlePercentage,
|
|
123
|
+
longestIdleGapMs: wakeWindowSummary.longestIdleGapMs
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/reporting/serialize-hourly-report.ts
|
|
128
|
+
function serializeHourlyReportPayload(hourlyReport) {
|
|
129
|
+
return {
|
|
130
|
+
appliedFilters: serializeSessionFilters(hourlyReport.appliedFilters),
|
|
131
|
+
agentConcurrencySource: hourlyReport.agentConcurrencySource,
|
|
132
|
+
buckets: hourlyReport.buckets.map(serializeHourlyBucket),
|
|
133
|
+
hasWakeWindow: hourlyReport.hasWakeWindow,
|
|
134
|
+
idleCutoffMs: hourlyReport.idleCutoffMs,
|
|
135
|
+
maxValues: {
|
|
136
|
+
agentOnlyMs: hourlyReport.maxValues.agentOnlyMs,
|
|
137
|
+
directActivityMs: hourlyReport.maxValues.directActivityMs,
|
|
138
|
+
engagedMs: hourlyReport.maxValues.engagedMs,
|
|
139
|
+
practicalBurn: hourlyReport.maxValues.practicalBurn
|
|
140
|
+
},
|
|
141
|
+
window: serializeReportWindow(hourlyReport.window)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function serializeHourlySnapshot(input) {
|
|
145
|
+
const snapshot = {
|
|
146
|
+
schemaVersion: jsonReportSchemaVersion,
|
|
147
|
+
mode: "hourly",
|
|
148
|
+
generatedAt: input.generatedAt.toISOString(),
|
|
149
|
+
command: {
|
|
150
|
+
idleCutoffMs: input.command.idleCutoffMs,
|
|
151
|
+
filters: serializeSessionFilters(input.command.filters),
|
|
152
|
+
wakeWindow: input.command.wakeWindow ? { ...input.command.wakeWindow } : null
|
|
153
|
+
},
|
|
154
|
+
hourlyReport: serializeHourlyReportPayload(input.hourlyReport)
|
|
155
|
+
};
|
|
156
|
+
return stringifyJsonSnapshot(snapshot);
|
|
157
|
+
}
|
|
158
|
+
function serializeHourlyBucket(bucket) {
|
|
159
|
+
return {
|
|
160
|
+
start: serializeTimeInterval({
|
|
161
|
+
start: bucket.start,
|
|
162
|
+
end: bucket.end
|
|
163
|
+
}).start,
|
|
164
|
+
end: serializeTimeInterval({
|
|
165
|
+
start: bucket.start,
|
|
166
|
+
end: bucket.end
|
|
167
|
+
}).end,
|
|
168
|
+
agentOnlyMs: bucket.agentOnlyMs,
|
|
169
|
+
awakeIdleMs: bucket.awakeIdleMs,
|
|
170
|
+
directActivityMs: bucket.directActivityMs,
|
|
171
|
+
engagedMs: bucket.engagedMs,
|
|
172
|
+
peakConcurrentAgents: bucket.peakConcurrentAgents,
|
|
173
|
+
practicalBurn: bucket.practicalBurn,
|
|
174
|
+
rawTotalTokens: bucket.rawTotalTokens,
|
|
175
|
+
sessionCount: bucket.sessionCount
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/reporting/serialize-live-report.ts
|
|
180
|
+
function serializeLiveReportPayload(liveReport) {
|
|
181
|
+
return {
|
|
182
|
+
appliedFilters: serializeSessionFilters(liveReport.appliedFilters),
|
|
183
|
+
doneRecentCount: liveReport.doneRecentCount,
|
|
184
|
+
doneRecentWindowMs: liveReport.doneRecentWindowMs,
|
|
185
|
+
doneThisTurnCount: liveReport.doneThisTurnCount,
|
|
186
|
+
observedAt: serializeIsoTimestamp(liveReport.observedAt),
|
|
187
|
+
peakTodayCount: liveReport.peakTodayCount,
|
|
188
|
+
recentConcurrencyValues: [...liveReport.recentConcurrencyValues],
|
|
189
|
+
runningCount: liveReport.runningCount,
|
|
190
|
+
runningLocations: liveReport.runningLocations.map((location) => ({
|
|
191
|
+
cwd: location.cwd,
|
|
192
|
+
runningCount: location.runningCount
|
|
193
|
+
})),
|
|
194
|
+
waitingThreads: liveReport.waitingThreads.map((waitingThread) => ({
|
|
195
|
+
cwd: waitingThread.cwd,
|
|
196
|
+
sessionId: waitingThread.sessionId,
|
|
197
|
+
waitDurationMs: waitingThread.waitDurationMs
|
|
198
|
+
})),
|
|
199
|
+
waitingOnUserCount: liveReport.waitingOnUserCount,
|
|
200
|
+
waitingOnUserLocations: liveReport.waitingOnUserLocations.map((location) => ({
|
|
201
|
+
cwd: location.cwd,
|
|
202
|
+
waitingCount: location.waitingCount
|
|
203
|
+
})),
|
|
204
|
+
scope: liveReport.scope,
|
|
205
|
+
workspacePrefix: liveReport.workspacePrefix
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function serializeLiveSnapshot(input) {
|
|
209
|
+
const snapshot = {
|
|
210
|
+
schemaVersion: jsonReportSchemaVersion,
|
|
211
|
+
mode: "live",
|
|
212
|
+
generatedAt: input.generatedAt.toISOString(),
|
|
213
|
+
command: {
|
|
214
|
+
filters: serializeSessionFilters(input.command.filters)
|
|
215
|
+
},
|
|
216
|
+
liveReport: serializeLiveReportPayload(input.liveReport)
|
|
217
|
+
};
|
|
218
|
+
return stringifyJsonSnapshot(snapshot);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/reporting/serialize-summary-report.ts
|
|
222
|
+
function serializeSummarySnapshot(input) {
|
|
223
|
+
const snapshot = {
|
|
224
|
+
schemaVersion: jsonReportSchemaVersion,
|
|
225
|
+
mode: input.mode,
|
|
226
|
+
generatedAt: input.generatedAt.toISOString(),
|
|
227
|
+
command: {
|
|
228
|
+
idleCutoffMs: input.command.idleCutoffMs,
|
|
229
|
+
filters: serializeSessionFilters(input.command.filters),
|
|
230
|
+
groupBy: [...input.command.groupBy],
|
|
231
|
+
wakeWindow: input.command.wakeWindow ? { ...input.command.wakeWindow } : null
|
|
232
|
+
},
|
|
233
|
+
hourlyReport: input.hourlyReport ? serializeHourlyReportPayload(input.hourlyReport) : null,
|
|
234
|
+
summaryReport: serializeSummaryReportPayload(input.summaryReport)
|
|
235
|
+
};
|
|
236
|
+
return stringifyJsonSnapshot(snapshot);
|
|
237
|
+
}
|
|
238
|
+
function serializeSummaryReportPayload(summaryReport) {
|
|
239
|
+
return {
|
|
240
|
+
activityWindow: summaryReport.activityWindow ? serializeTimeInterval(summaryReport.activityWindow) : null,
|
|
241
|
+
appliedFilters: serializeSessionFilters(summaryReport.appliedFilters),
|
|
242
|
+
comparisonCutoffMs: summaryReport.comparisonCutoffMs,
|
|
243
|
+
comparisonMetrics: serializeActivityMetrics(summaryReport.comparisonMetrics),
|
|
244
|
+
directTokenTotals: { ...summaryReport.directTokenTotals },
|
|
245
|
+
groupBreakdowns: summaryReport.groupBreakdowns.map(serializeSummaryBreakdown),
|
|
246
|
+
idleCutoffMs: summaryReport.idleCutoffMs,
|
|
247
|
+
metrics: serializeActivityMetrics(summaryReport.metrics),
|
|
248
|
+
sessionCounts: { ...summaryReport.sessionCounts },
|
|
249
|
+
tokenTotals: { ...summaryReport.tokenTotals },
|
|
250
|
+
wakeSummary: summaryReport.wakeSummary ? serializeWakeWindowSummary(summaryReport.wakeSummary) : null,
|
|
251
|
+
window: serializeReportWindow(summaryReport.window)
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function serializeSummaryBreakdown(breakdown) {
|
|
255
|
+
return {
|
|
256
|
+
dimension: breakdown.dimension,
|
|
257
|
+
rows: breakdown.rows.map(serializeSummaryBreakdownRow)
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function serializeSummaryBreakdownRow(row) {
|
|
261
|
+
return {
|
|
262
|
+
key: row.key,
|
|
263
|
+
sessionCount: row.sessionCount,
|
|
264
|
+
directActivityMs: row.directActivityMs,
|
|
265
|
+
agentCoverageMs: row.agentCoverageMs,
|
|
266
|
+
cumulativeAgentMs: row.cumulativeAgentMs,
|
|
267
|
+
practicalBurn: row.practicalBurn,
|
|
268
|
+
rawTotalTokens: row.rawTotalTokens
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
67
272
|
// src/report-window/parse-duration.ts
|
|
68
273
|
var durationPattern = /^(\d+)(m|h|d)$/;
|
|
69
274
|
function parseDurationToMs(durationText) {
|
|
@@ -313,6 +518,7 @@ function parseIdletimeCommand(argv) {
|
|
|
313
518
|
let helpRequested = false;
|
|
314
519
|
let hourlyWindowMs = defaultWindowMs;
|
|
315
520
|
let idleCutoffMs = defaultIdleCutoffMs;
|
|
521
|
+
let outputFormat = "text";
|
|
316
522
|
let shareMode = false;
|
|
317
523
|
let versionRequested = false;
|
|
318
524
|
let wakeWindow = null;
|
|
@@ -329,6 +535,10 @@ function parseIdletimeCommand(argv) {
|
|
|
329
535
|
versionRequested = true;
|
|
330
536
|
continue;
|
|
331
537
|
}
|
|
538
|
+
if (argument === "--json") {
|
|
539
|
+
outputFormat = "json";
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
332
542
|
if (argument === "--window") {
|
|
333
543
|
hourlyWindowMs = parseDurationToMs(readFlagValue(argument, args));
|
|
334
544
|
continue;
|
|
@@ -341,6 +551,10 @@ function parseIdletimeCommand(argv) {
|
|
|
341
551
|
filters.workspaceOnlyPrefix = readFlagValue(argument, args);
|
|
342
552
|
continue;
|
|
343
553
|
}
|
|
554
|
+
if (argument === "--global") {
|
|
555
|
+
filters.workspaceOnlyPrefix = null;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
344
558
|
if (argument === "--session-kind") {
|
|
345
559
|
filters.sessionKind = parseSessionKind(readFlagValue(argument, args));
|
|
346
560
|
continue;
|
|
@@ -373,6 +587,18 @@ function parseIdletimeCommand(argv) {
|
|
|
373
587
|
}
|
|
374
588
|
throw new Error(`Unknown argument "${argument}".`);
|
|
375
589
|
}
|
|
590
|
+
if (!helpRequested && !versionRequested) {
|
|
591
|
+
validateParsedCommand({
|
|
592
|
+
commandName,
|
|
593
|
+
filters,
|
|
594
|
+
groupBy,
|
|
595
|
+
hourlyWindowMs,
|
|
596
|
+
idleCutoffMs,
|
|
597
|
+
outputFormat,
|
|
598
|
+
shareMode,
|
|
599
|
+
wakeWindow
|
|
600
|
+
});
|
|
601
|
+
}
|
|
376
602
|
return {
|
|
377
603
|
commandName,
|
|
378
604
|
filters,
|
|
@@ -380,6 +606,7 @@ function parseIdletimeCommand(argv) {
|
|
|
380
606
|
helpRequested,
|
|
381
607
|
hourlyWindowMs,
|
|
382
608
|
idleCutoffMs,
|
|
609
|
+
outputFormat,
|
|
383
610
|
shareMode,
|
|
384
611
|
versionRequested,
|
|
385
612
|
wakeWindow
|
|
@@ -391,13 +618,15 @@ function renderHelpText() {
|
|
|
391
618
|
"Track Codex focus, activity, idle time, and token burn from local session logs.",
|
|
392
619
|
"",
|
|
393
620
|
"Usage:",
|
|
394
|
-
" idletime [last24h|today|hourly] [options]",
|
|
395
|
-
" inside this repo: bun run idletime [last24h|today|hourly] [options]",
|
|
621
|
+
" idletime [last24h|today|hourly|live|refresh-bests] [options]",
|
|
622
|
+
" inside this repo: bun run idletime [last24h|today|hourly|live|refresh-bests] [options]",
|
|
396
623
|
"",
|
|
397
624
|
"Modes:",
|
|
398
625
|
" last24h default. visual trailing-24h dashboard with rhythm, spikes, and stats",
|
|
399
626
|
" today local-midnight-to-now summary for the current day",
|
|
400
627
|
" hourly trailing-window chart plus the detailed per-hour table",
|
|
628
|
+
" live repainting task scoreboard; global by default",
|
|
629
|
+
" refresh-bests full-history best-metrics refresh; updates BEST records",
|
|
401
630
|
"",
|
|
402
631
|
"How To Read The Dashboard:",
|
|
403
632
|
" focus strict engagement inferred from actual user_message arrivals",
|
|
@@ -408,7 +637,9 @@ function renderHelpText() {
|
|
|
408
637
|
"",
|
|
409
638
|
"Options:",
|
|
410
639
|
" --window <24h> trailing window for hourly or last24h",
|
|
640
|
+
" --json print a machine-readable JSON snapshot",
|
|
411
641
|
" --idle-cutoff <15m> how long activity stays live after the last event",
|
|
642
|
+
" --global clear workspace scoping and read all sessions",
|
|
412
643
|
" --workspace-only <dir> include only sessions whose cwd starts with this path",
|
|
413
644
|
" --session-kind <kind> direct or subagent",
|
|
414
645
|
" --model <name> include only one primary model",
|
|
@@ -424,12 +655,59 @@ function renderHelpText() {
|
|
|
424
655
|
" idletime --wake 07:45-23:30 --share",
|
|
425
656
|
" idletime today --workspace-only /path/to/demo-workspace",
|
|
426
657
|
" idletime hourly --window 24h --workspace-only /path/to/demo-workspace",
|
|
658
|
+
" idletime live",
|
|
659
|
+
" idletime live --workspace-only /path/to/demo-workspace",
|
|
660
|
+
" idletime --json",
|
|
661
|
+
" idletime live --json",
|
|
662
|
+
" idletime refresh-bests",
|
|
427
663
|
" idletime --version"
|
|
428
664
|
].join(`
|
|
429
665
|
`);
|
|
430
666
|
}
|
|
431
667
|
function isCommandName(value) {
|
|
432
|
-
return value === "hourly" || value === "last24h" || value === "today";
|
|
668
|
+
return value === "hourly" || value === "last24h" || value === "live" || value === "refresh-bests" || value === "today";
|
|
669
|
+
}
|
|
670
|
+
function validateParsedCommand(input) {
|
|
671
|
+
if (input.outputFormat === "json" && input.shareMode) {
|
|
672
|
+
throw new Error("--share is only supported for human-readable output.");
|
|
673
|
+
}
|
|
674
|
+
if (input.commandName !== "refresh-bests") {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const unsupportedFlags = [];
|
|
678
|
+
if (input.outputFormat !== "text") {
|
|
679
|
+
unsupportedFlags.push("--json");
|
|
680
|
+
}
|
|
681
|
+
if (input.shareMode) {
|
|
682
|
+
unsupportedFlags.push("--share");
|
|
683
|
+
}
|
|
684
|
+
if (input.hourlyWindowMs !== defaultWindowMs) {
|
|
685
|
+
unsupportedFlags.push("--window");
|
|
686
|
+
}
|
|
687
|
+
if (input.idleCutoffMs !== defaultIdleCutoffMs) {
|
|
688
|
+
unsupportedFlags.push("--idle-cutoff");
|
|
689
|
+
}
|
|
690
|
+
if (input.wakeWindow) {
|
|
691
|
+
unsupportedFlags.push("--wake");
|
|
692
|
+
}
|
|
693
|
+
if (input.groupBy.length > 0) {
|
|
694
|
+
unsupportedFlags.push("--group-by");
|
|
695
|
+
}
|
|
696
|
+
if (input.filters.workspaceOnlyPrefix) {
|
|
697
|
+
unsupportedFlags.push("--workspace-only");
|
|
698
|
+
}
|
|
699
|
+
if (input.filters.sessionKind) {
|
|
700
|
+
unsupportedFlags.push("--session-kind");
|
|
701
|
+
}
|
|
702
|
+
if (input.filters.model) {
|
|
703
|
+
unsupportedFlags.push("--model");
|
|
704
|
+
}
|
|
705
|
+
if (input.filters.reasoningEffort) {
|
|
706
|
+
unsupportedFlags.push("--effort");
|
|
707
|
+
}
|
|
708
|
+
if (unsupportedFlags.length > 0) {
|
|
709
|
+
throw new Error(`refresh-bests does not support ${unsupportedFlags.join(", ")}.`);
|
|
710
|
+
}
|
|
433
711
|
}
|
|
434
712
|
function parseSessionKind(sessionKindText) {
|
|
435
713
|
if (sessionKindText === "direct" || sessionKindText === "subagent") {
|
|
@@ -674,6 +952,145 @@ function resolvePrimaryReasoningEffort(turnAttributions) {
|
|
|
674
952
|
return null;
|
|
675
953
|
}
|
|
676
954
|
|
|
955
|
+
// src/codex-session-log/task-windows.ts
|
|
956
|
+
var defaultTaskWindowStaleAfterMs = 5 * 60000;
|
|
957
|
+
var standardTaskWindowStaleAfterMs = 2 * 60000;
|
|
958
|
+
function extractTaskWindows(records, options) {
|
|
959
|
+
const turnContexts = new Map;
|
|
960
|
+
const activeTaskWindows = new Map;
|
|
961
|
+
const orderedTaskWindows = [];
|
|
962
|
+
let currentTurnId = null;
|
|
963
|
+
for (const record of records) {
|
|
964
|
+
if (record.type === "session_meta") {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
if (record.type === "turn_context") {
|
|
968
|
+
const payload2 = expectObject(record.payload, "turn_context.payload");
|
|
969
|
+
const turnId = readString(payload2, "turn_id", "turn_context.payload");
|
|
970
|
+
const turnContext = {
|
|
971
|
+
cwd: readString(payload2, "cwd", "turn_context.payload"),
|
|
972
|
+
model: readOptionalString(payload2, "model"),
|
|
973
|
+
reasoningEffort: readOptionalString(payload2, "effort")
|
|
974
|
+
};
|
|
975
|
+
turnContexts.set(turnId, turnContext);
|
|
976
|
+
const activeTaskWindow = activeTaskWindows.get(turnId);
|
|
977
|
+
if (activeTaskWindow) {
|
|
978
|
+
activeTaskWindow.cwd = turnContext.cwd;
|
|
979
|
+
activeTaskWindow.model = turnContext.model;
|
|
980
|
+
activeTaskWindow.reasoningEffort = turnContext.reasoningEffort;
|
|
981
|
+
activeTaskWindow.lastActivityAt = record.timestamp;
|
|
982
|
+
currentTurnId = turnId;
|
|
983
|
+
}
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
if (record.type !== "event_msg") {
|
|
987
|
+
touchCurrentTaskWindow(activeTaskWindows, currentTurnId, record.timestamp);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
const payload = expectObject(record.payload, "event_msg.payload");
|
|
991
|
+
const eventType = readOptionalString(payload, "type");
|
|
992
|
+
if (eventType === "task_started") {
|
|
993
|
+
const turnId = readString(payload, "turn_id", "event_msg.payload");
|
|
994
|
+
const turnContext = turnContexts.get(turnId);
|
|
995
|
+
const taskWindow = {
|
|
996
|
+
completedAt: null,
|
|
997
|
+
cwd: turnContext?.cwd ?? options.cwd,
|
|
998
|
+
lastActivityAt: record.timestamp,
|
|
999
|
+
model: turnContext?.model ?? null,
|
|
1000
|
+
reasoningEffort: turnContext?.reasoningEffort ?? null,
|
|
1001
|
+
startedAt: record.timestamp,
|
|
1002
|
+
turnId
|
|
1003
|
+
};
|
|
1004
|
+
activeTaskWindows.set(turnId, taskWindow);
|
|
1005
|
+
orderedTaskWindows.push(taskWindow);
|
|
1006
|
+
currentTurnId = turnId;
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
if (eventType === "task_complete") {
|
|
1010
|
+
const turnId = readString(payload, "turn_id", "event_msg.payload");
|
|
1011
|
+
const activeTaskWindow = activeTaskWindows.get(turnId) ?? createFallbackTaskWindow(record.timestamp, turnId, turnContexts.get(turnId), options.cwd);
|
|
1012
|
+
if (!activeTaskWindows.has(turnId)) {
|
|
1013
|
+
orderedTaskWindows.push(activeTaskWindow);
|
|
1014
|
+
}
|
|
1015
|
+
activeTaskWindow.lastActivityAt = record.timestamp;
|
|
1016
|
+
activeTaskWindow.completedAt = record.timestamp;
|
|
1017
|
+
activeTaskWindows.delete(turnId);
|
|
1018
|
+
currentTurnId = currentTurnId === turnId ? null : currentTurnId;
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
const eventTurnId = readOptionalString(payload, "turn_id");
|
|
1022
|
+
if (eventTurnId && activeTaskWindows.has(eventTurnId)) {
|
|
1023
|
+
touchCurrentTaskWindow(activeTaskWindows, eventTurnId, record.timestamp);
|
|
1024
|
+
currentTurnId = eventTurnId;
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
touchCurrentTaskWindow(activeTaskWindows, currentTurnId, record.timestamp);
|
|
1028
|
+
}
|
|
1029
|
+
return orderedTaskWindows.map((taskWindow, taskWindowIndex) => ({
|
|
1030
|
+
taskId: `${options.sessionId}:${taskWindow.turnId}:${taskWindowIndex}`,
|
|
1031
|
+
sessionId: options.sessionId,
|
|
1032
|
+
parentSessionId: options.parentSessionId,
|
|
1033
|
+
sessionKind: options.sessionKind,
|
|
1034
|
+
cwd: taskWindow.cwd,
|
|
1035
|
+
turnId: taskWindow.turnId,
|
|
1036
|
+
model: taskWindow.model,
|
|
1037
|
+
reasoningEffort: taskWindow.reasoningEffort,
|
|
1038
|
+
startedAt: taskWindow.startedAt,
|
|
1039
|
+
lastActivityAt: taskWindow.lastActivityAt,
|
|
1040
|
+
completedAt: taskWindow.completedAt,
|
|
1041
|
+
staleAfterMs: resolveTaskWindowStaleAfterMs(taskWindow.reasoningEffort)
|
|
1042
|
+
}));
|
|
1043
|
+
}
|
|
1044
|
+
function buildTaskWindowInterval(taskWindow, observedAt) {
|
|
1045
|
+
const intervalEnd = resolveTaskWindowEnd(taskWindow, observedAt);
|
|
1046
|
+
if (intervalEnd.getTime() <= taskWindow.startedAt.getTime()) {
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
return {
|
|
1050
|
+
start: taskWindow.startedAt,
|
|
1051
|
+
end: intervalEnd
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
function isTaskWindowRunning(taskWindow, observedAt) {
|
|
1055
|
+
return taskWindow.completedAt === null && taskWindow.lastActivityAt.getTime() + taskWindow.staleAfterMs > observedAt.getTime();
|
|
1056
|
+
}
|
|
1057
|
+
function isTaskWindowCompletedBetween(taskWindow, windowStart, windowEnd) {
|
|
1058
|
+
if (!taskWindow.completedAt) {
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
return taskWindow.completedAt.getTime() > windowStart.getTime() && taskWindow.completedAt.getTime() <= windowEnd.getTime();
|
|
1062
|
+
}
|
|
1063
|
+
function resolveTaskWindowEnd(taskWindow, observedAt) {
|
|
1064
|
+
if (taskWindow.completedAt) {
|
|
1065
|
+
return taskWindow.completedAt;
|
|
1066
|
+
}
|
|
1067
|
+
const staleDeadline = new Date(taskWindow.lastActivityAt.getTime() + taskWindow.staleAfterMs);
|
|
1068
|
+
return staleDeadline.getTime() < observedAt.getTime() ? staleDeadline : observedAt;
|
|
1069
|
+
}
|
|
1070
|
+
function resolveTaskWindowStaleAfterMs(reasoningEffort) {
|
|
1071
|
+
return reasoningEffort === "medium" || reasoningEffort === "high" ? standardTaskWindowStaleAfterMs : defaultTaskWindowStaleAfterMs;
|
|
1072
|
+
}
|
|
1073
|
+
function createFallbackTaskWindow(timestamp, turnId, turnContext, defaultCwd) {
|
|
1074
|
+
return {
|
|
1075
|
+
completedAt: null,
|
|
1076
|
+
cwd: turnContext?.cwd ?? defaultCwd,
|
|
1077
|
+
lastActivityAt: timestamp,
|
|
1078
|
+
model: turnContext?.model ?? null,
|
|
1079
|
+
reasoningEffort: turnContext?.reasoningEffort ?? null,
|
|
1080
|
+
startedAt: timestamp,
|
|
1081
|
+
turnId
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function touchCurrentTaskWindow(activeTaskWindows, turnId, timestamp) {
|
|
1085
|
+
if (!turnId) {
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const activeTaskWindow = activeTaskWindows.get(turnId);
|
|
1089
|
+
if (activeTaskWindow) {
|
|
1090
|
+
activeTaskWindow.lastActivityAt = timestamp;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
677
1094
|
// src/codex-session-log/extract-user-message-timestamps.ts
|
|
678
1095
|
function extractUserMessageTimestamps(records) {
|
|
679
1096
|
const timestamps = [];
|
|
@@ -708,13 +1125,17 @@ async function parseCodexSession(sourceFilePath) {
|
|
|
708
1125
|
const tokenPoints = extractTokenPoints(records);
|
|
709
1126
|
const userMessageTimestamps = extractUserMessageTimestamps(records);
|
|
710
1127
|
const { turnAttributions, agentSpawnRequests } = extractTurnAttribution(records);
|
|
1128
|
+
const sessionId = readString(sessionMetaPayload, "id", "session_meta.payload");
|
|
1129
|
+
const cwd = readString(sessionMetaPayload, "cwd", "session_meta.payload");
|
|
1130
|
+
const forkedFromSessionId = readOptionalString(sessionMetaPayload, "forked_from_id");
|
|
1131
|
+
const kind = classifySessionKind(sessionMetaPayload.source);
|
|
711
1132
|
const eventTimestamps = records.filter((record) => record.type !== "session_meta").map((record) => record.timestamp);
|
|
712
1133
|
return {
|
|
713
|
-
sessionId
|
|
1134
|
+
sessionId,
|
|
714
1135
|
sourceFilePath,
|
|
715
|
-
cwd
|
|
716
|
-
kind
|
|
717
|
-
forkedFromSessionId
|
|
1136
|
+
cwd,
|
|
1137
|
+
kind,
|
|
1138
|
+
forkedFromSessionId,
|
|
718
1139
|
firstTimestamp: firstRecord.timestamp,
|
|
719
1140
|
lastTimestamp: lastRecord.timestamp,
|
|
720
1141
|
eventTimestamps: eventTimestamps.length > 0 ? eventTimestamps : [firstRecord.timestamp],
|
|
@@ -723,6 +1144,12 @@ async function parseCodexSession(sourceFilePath) {
|
|
|
723
1144
|
userMessageTimestamps,
|
|
724
1145
|
turnAttributions,
|
|
725
1146
|
agentSpawnRequests,
|
|
1147
|
+
taskWindows: extractTaskWindows(records, {
|
|
1148
|
+
cwd,
|
|
1149
|
+
parentSessionId: forkedFromSessionId,
|
|
1150
|
+
sessionId,
|
|
1151
|
+
sessionKind: kind
|
|
1152
|
+
}),
|
|
726
1153
|
primaryModel: resolvePrimaryModel(turnAttributions),
|
|
727
1154
|
primaryReasoningEffort: resolvePrimaryReasoningEffort(turnAttributions)
|
|
728
1155
|
};
|
|
@@ -815,26 +1242,29 @@ function buildActivityBlocks(timestamps, idleCutoffMs) {
|
|
|
815
1242
|
}
|
|
816
1243
|
|
|
817
1244
|
// src/reporting/activity-metrics.ts
|
|
818
|
-
function buildActivityMetrics(sessions, idleCutoffMs) {
|
|
1245
|
+
function buildActivityMetrics(sessions, idleCutoffMs, observedAt = new Date) {
|
|
819
1246
|
const directSessions = sessions.filter((session) => session.kind === "direct");
|
|
820
1247
|
const subagentSessions = sessions.filter((session) => session.kind === "subagent");
|
|
821
1248
|
const strictEngagementBlocks = buildActivityBlocks(directSessions.flatMap((session) => session.userMessageTimestamps), idleCutoffMs);
|
|
822
1249
|
const directActivityBlocks = buildActivityBlocks(directSessions.flatMap((session) => session.eventTimestamps), idleCutoffMs);
|
|
823
|
-
const
|
|
824
|
-
|
|
1250
|
+
const perAgentTaskBlocks = subagentSessions.map((session) => session.taskWindows.length > 0 ? session.taskWindows.flatMap((taskWindow) => {
|
|
1251
|
+
const taskWindowInterval = buildTaskWindowInterval(taskWindow, observedAt);
|
|
1252
|
+
return taskWindowInterval ? [taskWindowInterval] : [];
|
|
1253
|
+
}) : buildActivityBlocks(session.eventTimestamps, idleCutoffMs));
|
|
1254
|
+
const agentCoverageBlocks = mergeTimeIntervals(perAgentTaskBlocks.flat());
|
|
825
1255
|
const agentOnlyBlocks = subtractTimeIntervals(agentCoverageBlocks, directActivityBlocks);
|
|
826
1256
|
return {
|
|
827
1257
|
strictEngagementBlocks,
|
|
828
1258
|
directActivityBlocks,
|
|
829
1259
|
agentCoverageBlocks,
|
|
830
1260
|
agentOnlyBlocks,
|
|
831
|
-
|
|
1261
|
+
perAgentTaskBlocks,
|
|
832
1262
|
strictEngagementMs: sumTimeIntervalsMs(strictEngagementBlocks),
|
|
833
1263
|
directActivityMs: sumTimeIntervalsMs(directActivityBlocks),
|
|
834
1264
|
agentCoverageMs: sumTimeIntervalsMs(agentCoverageBlocks),
|
|
835
1265
|
agentOnlyMs: sumTimeIntervalsMs(agentOnlyBlocks),
|
|
836
|
-
cumulativeAgentMs:
|
|
837
|
-
peakConcurrentAgents: peakConcurrency(
|
|
1266
|
+
cumulativeAgentMs: perAgentTaskBlocks.reduce((totalDurationMs, taskBlocks) => totalDurationMs + sumTimeIntervalsMs(taskBlocks), 0),
|
|
1267
|
+
peakConcurrentAgents: peakConcurrency(perAgentTaskBlocks)
|
|
838
1268
|
};
|
|
839
1269
|
}
|
|
840
1270
|
|
|
@@ -870,11 +1300,12 @@ function groupSessions(sessions, dimension) {
|
|
|
870
1300
|
// src/reporting/build-hourly-report.ts
|
|
871
1301
|
function buildHourlyReport(sessions, query) {
|
|
872
1302
|
const filteredSessions = filterSessions(sessions, query.filters);
|
|
873
|
-
const metrics = buildActivityMetrics(filteredSessions, query.idleCutoffMs);
|
|
1303
|
+
const metrics = buildActivityMetrics(filteredSessions, query.idleCutoffMs, query.window.end);
|
|
874
1304
|
const wakeIntervals = query.wakeWindow ? buildWakeIntervalsForReportWindow(query.wakeWindow, query.window) : null;
|
|
875
1305
|
const buckets = buildBuckets(query.window.start, query.window.end, filteredSessions, metrics, wakeIntervals);
|
|
876
1306
|
return {
|
|
877
1307
|
appliedFilters: query.filters,
|
|
1308
|
+
agentConcurrencySource: filteredSessions.some((session) => session.kind === "subagent" && session.taskWindows.length === 0) ? "task-window-adapter-with-session-fallback" : "task-window-adapter",
|
|
878
1309
|
buckets,
|
|
879
1310
|
hasWakeWindow: query.wakeWindow !== null,
|
|
880
1311
|
idleCutoffMs: query.idleCutoffMs,
|
|
@@ -903,7 +1334,7 @@ function buildBuckets(windowStart, windowEnd, sessions, metrics, wakeIntervals)
|
|
|
903
1334
|
awakeIdleMs,
|
|
904
1335
|
directActivityMs,
|
|
905
1336
|
engagedMs: measureOverlapMs(metrics.strictEngagementBlocks, bucketInterval),
|
|
906
|
-
peakConcurrentAgents: peakConcurrency(metrics.
|
|
1337
|
+
peakConcurrentAgents: peakConcurrency(metrics.perAgentTaskBlocks.map((taskBlocks) => clipIntervals(taskBlocks, bucketInterval))),
|
|
907
1338
|
practicalBurn: sumTokenBurn(sessions, bucketInterval),
|
|
908
1339
|
rawTotalTokens: sumRawTokenDeltas(sessions, bucketInterval),
|
|
909
1340
|
sessionCount: countSessionsInBucket(sessions, bucketInterval)
|
|
@@ -1028,6 +1459,13 @@ function formatHourOfDay(timestamp, reportWindow) {
|
|
|
1028
1459
|
hour12: false
|
|
1029
1460
|
}).format(timestamp);
|
|
1030
1461
|
}
|
|
1462
|
+
function formatAxisTimeLabel(timestamp, reportWindow) {
|
|
1463
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
1464
|
+
timeZone: reportWindow.timeZone,
|
|
1465
|
+
hour: "numeric",
|
|
1466
|
+
hour12: true
|
|
1467
|
+
}).format(timestamp).toLowerCase().replace(/\s+/g, "");
|
|
1468
|
+
}
|
|
1031
1469
|
function buildSparkline(values) {
|
|
1032
1470
|
const levels = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
1033
1471
|
const maxValue = Math.max(...values, 0);
|
|
@@ -1061,18 +1499,66 @@ function shortenPath(pathText, maxLength) {
|
|
|
1061
1499
|
return `...${shortenedPath || pathText.slice(-(maxLength - 3))}`;
|
|
1062
1500
|
}
|
|
1063
1501
|
|
|
1502
|
+
// src/reporting/render-best-plaque.ts
|
|
1503
|
+
function buildBestPlaque(ledger) {
|
|
1504
|
+
return {
|
|
1505
|
+
label: "BEST",
|
|
1506
|
+
concurrentAgentsText: `${formatInteger(ledger?.bestConcurrentAgents?.value ?? 0)} concurrent agents`,
|
|
1507
|
+
rawBurnText: `${formatCompactInteger(ledger?.best24hRawBurn?.value ?? 0).toUpperCase()} 24hr raw burn`,
|
|
1508
|
+
agentSumText: `${formatAgentSumHours(ledger?.best24hAgentSumMs?.value ?? 0)} agent sum`
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
function buildBestPlaqueRows(bestPlaque, availableWidth) {
|
|
1512
|
+
const wideRows = [
|
|
1513
|
+
bestPlaque.label,
|
|
1514
|
+
bestPlaque.concurrentAgentsText,
|
|
1515
|
+
bestPlaque.rawBurnText,
|
|
1516
|
+
bestPlaque.agentSumText,
|
|
1517
|
+
""
|
|
1518
|
+
];
|
|
1519
|
+
if (rowsFitWidth(wideRows, availableWidth)) {
|
|
1520
|
+
return wideRows;
|
|
1521
|
+
}
|
|
1522
|
+
const compactRows = [
|
|
1523
|
+
bestPlaque.label,
|
|
1524
|
+
bestPlaque.concurrentAgentsText.replace(" agents", ""),
|
|
1525
|
+
bestPlaque.rawBurnText.replace(" 24hr ", " "),
|
|
1526
|
+
bestPlaque.agentSumText,
|
|
1527
|
+
""
|
|
1528
|
+
];
|
|
1529
|
+
if (rowsFitWidth(compactRows, availableWidth)) {
|
|
1530
|
+
return compactRows;
|
|
1531
|
+
}
|
|
1532
|
+
const microRows = [
|
|
1533
|
+
bestPlaque.label,
|
|
1534
|
+
compactRows[1]?.replace("concurrent", "conc") ?? "",
|
|
1535
|
+
compactRows[2]?.replace(" burn", "") ?? "",
|
|
1536
|
+
compactRows[3]?.replace(" agent sum", " sum") ?? "",
|
|
1537
|
+
""
|
|
1538
|
+
];
|
|
1539
|
+
return rowsFitWidth(microRows, availableWidth) ? microRows : null;
|
|
1540
|
+
}
|
|
1541
|
+
function formatAgentSumHours(durationMs) {
|
|
1542
|
+
const hours = durationMs / 3600000;
|
|
1543
|
+
const roundedHours = hours >= 10 ? Math.round(hours).toString() : (Math.round(hours * 10) / 10).toString();
|
|
1544
|
+
return roundedHours.endsWith(".0") ? roundedHours.slice(0, -2) : roundedHours;
|
|
1545
|
+
}
|
|
1546
|
+
function rowsFitWidth(rows, availableWidth) {
|
|
1547
|
+
return availableWidth > 0 && rows.every((row) => row.length <= availableWidth);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1064
1550
|
// src/reporting/render-theme.ts
|
|
1065
1551
|
var roleStyles = {
|
|
1066
|
-
focus: "1;38;2;
|
|
1067
|
-
active: "1;38;2;
|
|
1068
|
-
agent: "1;38;2;
|
|
1069
|
-
idle: "1;38;2;
|
|
1070
|
-
burn: "1;38;2;
|
|
1071
|
-
raw: "1;38;2;
|
|
1072
|
-
frame: "1;38;2;
|
|
1073
|
-
heading: "1;38;2;
|
|
1074
|
-
muted: "38;2;
|
|
1075
|
-
value: "1;38;2;
|
|
1552
|
+
focus: "1;38;2;190;184;86",
|
|
1553
|
+
active: "1;38;2;157;168;60",
|
|
1554
|
+
agent: "1;38;2;124;138;55",
|
|
1555
|
+
idle: "1;38;2;105;118;50",
|
|
1556
|
+
burn: "1;38;2;190;161;55",
|
|
1557
|
+
raw: "1;38;2;153;132;52",
|
|
1558
|
+
frame: "1;38;2;118;126;50",
|
|
1559
|
+
heading: "1;38;2;201;190;102",
|
|
1560
|
+
muted: "38;2;111;115;78",
|
|
1561
|
+
value: "1;38;2;178;161;68"
|
|
1076
1562
|
};
|
|
1077
1563
|
function createRenderOptions(shareMode) {
|
|
1078
1564
|
return {
|
|
@@ -1110,7 +1596,9 @@ function measureVisibleTextWidth(text) {
|
|
|
1110
1596
|
|
|
1111
1597
|
// src/reporting/render-logo-section.ts
|
|
1112
1598
|
var baseBackgroundStyle = "48;2;12;15;8";
|
|
1113
|
-
var
|
|
1599
|
+
var plaqueInsetColumns = 3;
|
|
1600
|
+
var plaqueTextStyle = "1;38;2;244;235;164";
|
|
1601
|
+
var wordmarkStyle = `${baseBackgroundStyle};1;38;2;210;198;108`;
|
|
1114
1602
|
var wordmarkLines = [
|
|
1115
1603
|
" ▄▄ ▄▄",
|
|
1116
1604
|
"▀▀ ██ ██ ██ ▀▀",
|
|
@@ -1120,59 +1608,76 @@ var wordmarkLines = [
|
|
|
1120
1608
|
];
|
|
1121
1609
|
var patternColors = [
|
|
1122
1610
|
{ red: 20, green: 24, blue: 10 },
|
|
1123
|
-
{ red:
|
|
1124
|
-
{ red:
|
|
1125
|
-
{ red:
|
|
1126
|
-
{ red:
|
|
1127
|
-
{ red:
|
|
1611
|
+
{ red: 42, green: 50, blue: 16 },
|
|
1612
|
+
{ red: 71, green: 81, blue: 24 },
|
|
1613
|
+
{ red: 101, green: 112, blue: 31 },
|
|
1614
|
+
{ red: 136, green: 145, blue: 39 },
|
|
1615
|
+
{ red: 177, green: 169, blue: 58 }
|
|
1128
1616
|
];
|
|
1129
1617
|
var monochromePatternCharacters = ["░", "░", "▒", "▓", "█"];
|
|
1130
|
-
function buildLogoSection(requestedWidth, options) {
|
|
1618
|
+
function buildLogoSection(requestedWidth, options, bestPlaque = null) {
|
|
1131
1619
|
const wordmarkWidth = Math.max(...wordmarkLines.map((line) => line.length));
|
|
1132
1620
|
const sectionWidth = Math.max(requestedWidth, wordmarkWidth);
|
|
1133
1621
|
const patternWidth = Math.max(0, sectionWidth - wordmarkWidth);
|
|
1622
|
+
const plaqueRows = bestPlaque ? buildBestPlaqueRows(bestPlaque, Math.max(0, patternWidth - plaqueInsetColumns)) : null;
|
|
1134
1623
|
return wordmarkLines.map((line, rowIndex) => {
|
|
1135
1624
|
const paddedWordmark = padRight(line, wordmarkWidth);
|
|
1136
|
-
const patternTail = buildPatternTail(patternWidth, rowIndex, options);
|
|
1625
|
+
const patternTail = buildPatternTail(patternWidth, rowIndex, options, plaqueRows?.[rowIndex] ?? "");
|
|
1137
1626
|
return `${paintAnsi(paddedWordmark, wordmarkStyle, options)}${patternTail}`;
|
|
1138
1627
|
});
|
|
1139
1628
|
}
|
|
1140
1629
|
function resolveLogoSectionWidth(minimumWidth, options) {
|
|
1141
1630
|
return Math.max(minimumWidth, options.terminalWidth ?? 0);
|
|
1142
1631
|
}
|
|
1143
|
-
function buildPatternTail(width, rowIndex, options) {
|
|
1632
|
+
function buildPatternTail(width, rowIndex, options, plaqueRowText) {
|
|
1144
1633
|
if (!options.colorEnabled) {
|
|
1145
|
-
return buildMonochromePatternTail(width, rowIndex);
|
|
1634
|
+
return buildMonochromePatternTail(width, rowIndex, plaqueRowText);
|
|
1146
1635
|
}
|
|
1636
|
+
const overlayCharacters = createOverlayCharacters(width, plaqueRowText);
|
|
1637
|
+
const cellStyles = Array.from({ length: width }, (_, columnIndex) => getPatternCellStyle(getPatternIntensity(width, rowIndex, columnIndex)));
|
|
1147
1638
|
let patternTail = "";
|
|
1148
1639
|
let currentStyle = "";
|
|
1149
|
-
let
|
|
1640
|
+
let currentSegment = "";
|
|
1150
1641
|
for (let columnIndex = 0;columnIndex < width; columnIndex += 1) {
|
|
1151
|
-
const
|
|
1642
|
+
const overlayCharacter = overlayCharacters[columnIndex];
|
|
1643
|
+
const style = overlayCharacter === null ? cellStyles[columnIndex] : `${cellStyles[columnIndex]};${plaqueTextStyle}`;
|
|
1152
1644
|
if (style === currentStyle) {
|
|
1153
|
-
|
|
1645
|
+
currentSegment += overlayCharacter ?? " ";
|
|
1154
1646
|
continue;
|
|
1155
1647
|
}
|
|
1156
|
-
if (
|
|
1157
|
-
patternTail += paintAnsi(
|
|
1648
|
+
if (currentSegment.length > 0) {
|
|
1649
|
+
patternTail += paintAnsi(currentSegment, currentStyle, options);
|
|
1158
1650
|
}
|
|
1159
1651
|
currentStyle = style;
|
|
1160
|
-
|
|
1652
|
+
currentSegment = overlayCharacter ?? " ";
|
|
1161
1653
|
}
|
|
1162
|
-
if (
|
|
1163
|
-
patternTail += paintAnsi(
|
|
1654
|
+
if (currentSegment.length > 0) {
|
|
1655
|
+
patternTail += paintAnsi(currentSegment, currentStyle, options);
|
|
1164
1656
|
}
|
|
1165
1657
|
return patternTail;
|
|
1166
1658
|
}
|
|
1167
|
-
function buildMonochromePatternTail(width, rowIndex) {
|
|
1659
|
+
function buildMonochromePatternTail(width, rowIndex, plaqueRowText) {
|
|
1660
|
+
const overlayCharacters = createOverlayCharacters(width, plaqueRowText);
|
|
1168
1661
|
let patternTail = "";
|
|
1169
1662
|
for (let columnIndex = 0;columnIndex < width; columnIndex += 1) {
|
|
1663
|
+
const overlayCharacter = overlayCharacters[columnIndex];
|
|
1664
|
+
if (overlayCharacter !== null) {
|
|
1665
|
+
patternTail += overlayCharacter;
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1170
1668
|
const intensity = getPatternIntensity(width, rowIndex, columnIndex);
|
|
1171
1669
|
const characterIndex = Math.min(monochromePatternCharacters.length - 1, Math.floor(intensity * monochromePatternCharacters.length));
|
|
1172
1670
|
patternTail += monochromePatternCharacters[characterIndex];
|
|
1173
1671
|
}
|
|
1174
1672
|
return patternTail;
|
|
1175
1673
|
}
|
|
1674
|
+
function createOverlayCharacters(width, plaqueRowText) {
|
|
1675
|
+
const overlayCharacters = Array.from({ length: width }, () => null);
|
|
1676
|
+
for (let index = 0;index < plaqueRowText.length && plaqueInsetColumns + index < width; index += 1) {
|
|
1677
|
+
overlayCharacters[plaqueInsetColumns + index] = plaqueRowText[index];
|
|
1678
|
+
}
|
|
1679
|
+
return overlayCharacters;
|
|
1680
|
+
}
|
|
1176
1681
|
function getPatternIntensity(width, rowIndex, columnIndex) {
|
|
1177
1682
|
const normalizedColumn = width <= 1 ? 0 : columnIndex / Math.max(1, width - 1);
|
|
1178
1683
|
const envelope = 0.18 + 0.82 * Math.pow(normalizedColumn, 0.82);
|
|
@@ -1189,6 +1694,27 @@ function clamp(value, minValue, maxValue) {
|
|
|
1189
1694
|
return Math.min(maxValue, Math.max(minValue, value));
|
|
1190
1695
|
}
|
|
1191
1696
|
|
|
1697
|
+
// src/reporting/render-time-axis.ts
|
|
1698
|
+
var axisGroupSize = 4;
|
|
1699
|
+
function buildGroupedTrack(text) {
|
|
1700
|
+
const groups = [];
|
|
1701
|
+
for (let index = 0;index < text.length; index += axisGroupSize) {
|
|
1702
|
+
groups.push(text.slice(index, index + axisGroupSize));
|
|
1703
|
+
}
|
|
1704
|
+
return groups.join("│");
|
|
1705
|
+
}
|
|
1706
|
+
function buildTimeAxisLine(report) {
|
|
1707
|
+
const axisGroups = [];
|
|
1708
|
+
for (let bucketIndex = 0;bucketIndex < report.buckets.length; bucketIndex += axisGroupSize) {
|
|
1709
|
+
const bucket = report.buckets[bucketIndex];
|
|
1710
|
+
if (!bucket) {
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
axisGroups.push(padRight(formatAxisTimeLabel(bucket.start, report.window), Math.min(axisGroupSize, report.buckets.length - bucketIndex)));
|
|
1714
|
+
}
|
|
1715
|
+
return axisGroups.join("│");
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1192
1718
|
// src/reporting/render-layout.ts
|
|
1193
1719
|
function buildPanel(title, lines) {
|
|
1194
1720
|
const innerWidth = Math.max(56, title.length + 4, ...lines.map((line) => line.length));
|
|
@@ -1215,8 +1741,17 @@ function renderSectionTitle(title, options) {
|
|
|
1215
1741
|
];
|
|
1216
1742
|
}
|
|
1217
1743
|
|
|
1744
|
+
// src/reporting/render-agent-section.ts
|
|
1745
|
+
function buildAgentSection(report, options) {
|
|
1746
|
+
return [
|
|
1747
|
+
...renderSectionTitle("Agents", options),
|
|
1748
|
+
paint(` time ${buildTimeAxisLine(report)}`, "muted", options),
|
|
1749
|
+
`${paint(" conc ", "agent", options)}${paint(buildGroupedTrack(buildSparkline(report.buckets.map((bucket) => bucket.peakConcurrentAgents))), "agent", options)} ${paint(`${Math.max(...report.buckets.map((bucket) => bucket.peakConcurrentAgents), 0)} peak`, "value", options)}`,
|
|
1750
|
+
`${paint(" unit ", "muted", options)}${paint(report.agentConcurrencySource === "task-window-adapter" ? "task windows" : "task windows with session fallback", "muted", options)} ${paint(padRight(formatDurationCompact(report.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + bucket.agentOnlyMs, 0)), 5), "agent", options)} ${paint("agent-only", "muted", options)}`
|
|
1751
|
+
];
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1218
1754
|
// src/reporting/render-rhythm-section.ts
|
|
1219
|
-
var groupSize = 4;
|
|
1220
1755
|
function buildRhythmSection(report, options) {
|
|
1221
1756
|
const quietValues = report.buckets.map((bucket) => Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs));
|
|
1222
1757
|
const idleValues = report.hasWakeWindow ? report.buckets.map((bucket) => bucket.awakeIdleMs) : quietValues;
|
|
@@ -1224,7 +1759,7 @@ function buildRhythmSection(report, options) {
|
|
|
1224
1759
|
const idleTotal = formatDurationCompact(idleValues.reduce((totalDurationMs, idleDurationMs) => totalDurationMs + idleDurationMs, 0));
|
|
1225
1760
|
const lines = [
|
|
1226
1761
|
...renderSectionTitle("24h Rhythm", options),
|
|
1227
|
-
paint(`
|
|
1762
|
+
paint(` time ${buildTimeAxisLine(report)}`, "muted", options),
|
|
1228
1763
|
renderRhythmRow("focus", buildGroupedTrack(buildSparkline(report.buckets.map((bucket) => bucket.engagedMs))), formatDurationCompact(report.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + bucket.engagedMs, 0)), "focus", options),
|
|
1229
1764
|
renderRhythmRow("active", buildGroupedTrack(buildSparkline(report.buckets.map((bucket) => bucket.directActivityMs))), formatDurationCompact(report.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + bucket.directActivityMs, 0)), "active", options)
|
|
1230
1765
|
];
|
|
@@ -1232,24 +1767,6 @@ function buildRhythmSection(report, options) {
|
|
|
1232
1767
|
lines.push(renderRhythmRow("burn", buildGroupedTrack(buildSparkline(report.buckets.map((bucket) => bucket.practicalBurn))), formatCompactInteger(report.buckets.reduce((totalBurn, bucket) => totalBurn + bucket.practicalBurn, 0)), "burn", options));
|
|
1233
1768
|
return lines;
|
|
1234
1769
|
}
|
|
1235
|
-
function buildGroupedTrack(text) {
|
|
1236
|
-
const groups = [];
|
|
1237
|
-
for (let i = 0;i < text.length; i += groupSize) {
|
|
1238
|
-
groups.push(text.slice(i, i + groupSize));
|
|
1239
|
-
}
|
|
1240
|
-
return groups.join("│");
|
|
1241
|
-
}
|
|
1242
|
-
function buildHourMarkerLine(report) {
|
|
1243
|
-
const markerGroups = [];
|
|
1244
|
-
for (let index = 0;index < report.buckets.length; index += groupSize) {
|
|
1245
|
-
const bucket = report.buckets[index];
|
|
1246
|
-
if (!bucket) {
|
|
1247
|
-
continue;
|
|
1248
|
-
}
|
|
1249
|
-
markerGroups.push(padRight(formatHourOfDay(bucket.start, report.window), Math.min(groupSize, report.buckets.length - index)));
|
|
1250
|
-
}
|
|
1251
|
-
return markerGroups.join("│");
|
|
1252
|
-
}
|
|
1253
1770
|
function renderRhythmRow(label, sparkline, totalText, role, options) {
|
|
1254
1771
|
return `${paint(` ${padRight(label, 6)}`, role, options)} ${paint(sparkline, role, options)} ${paint(totalText, "value", options)}`;
|
|
1255
1772
|
}
|
|
@@ -1286,6 +1803,8 @@ function renderFullHourlyReport(report, options) {
|
|
|
1286
1803
|
lines.push("");
|
|
1287
1804
|
lines.push(...panelLines);
|
|
1288
1805
|
lines.push("");
|
|
1806
|
+
lines.push(...buildAgentSection(report, options));
|
|
1807
|
+
lines.push("");
|
|
1289
1808
|
lines.push(...buildRhythmSection(report, options));
|
|
1290
1809
|
lines.push("");
|
|
1291
1810
|
lines.push(...buildSpikeSection(report, options));
|
|
@@ -1315,6 +1834,8 @@ function renderShareHourlyReport(report, options) {
|
|
|
1315
1834
|
lines.push("");
|
|
1316
1835
|
lines.push(...panelLines);
|
|
1317
1836
|
lines.push("");
|
|
1837
|
+
lines.push(...buildAgentSection(report, options));
|
|
1838
|
+
lines.push("");
|
|
1318
1839
|
lines.push(...buildRhythmSection(report, options));
|
|
1319
1840
|
lines.push("");
|
|
1320
1841
|
lines.push(...buildSpikeSection(report, options));
|
|
@@ -1341,18 +1862,111 @@ function buildFilterLine(report) {
|
|
|
1341
1862
|
}
|
|
1342
1863
|
|
|
1343
1864
|
// src/cli/run-hourly-command.ts
|
|
1344
|
-
async function
|
|
1345
|
-
const window = resolveTrailingReportWindow({
|
|
1865
|
+
async function buildHourlyCommandResult(command, options = {}) {
|
|
1866
|
+
const window = resolveTrailingReportWindow({
|
|
1867
|
+
durationMs: command.hourlyWindowMs,
|
|
1868
|
+
now: options.now
|
|
1869
|
+
});
|
|
1346
1870
|
const sessions = await readCodexSessions({
|
|
1347
1871
|
windowStart: window.start,
|
|
1348
|
-
windowEnd: window.end
|
|
1872
|
+
windowEnd: window.end,
|
|
1873
|
+
sessionRootDirectory: options.sessionRootDirectory
|
|
1349
1874
|
});
|
|
1350
|
-
return
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1875
|
+
return {
|
|
1876
|
+
hourlyReport: buildHourlyReport(sessions, {
|
|
1877
|
+
filters: command.filters,
|
|
1878
|
+
idleCutoffMs: command.idleCutoffMs,
|
|
1879
|
+
wakeWindow: command.wakeWindow,
|
|
1880
|
+
window
|
|
1881
|
+
})
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
async function runHourlyCommand(command, options = {}) {
|
|
1885
|
+
const commandResult = await buildHourlyCommandResult(command, options);
|
|
1886
|
+
return renderHourlyReport(commandResult.hourlyReport, createRenderOptions(command.shareMode));
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// src/best-metrics/read-best-ledger.ts
|
|
1890
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
1891
|
+
import { homedir as homedir2 } from "node:os";
|
|
1892
|
+
import { join as join2 } from "node:path";
|
|
1893
|
+
|
|
1894
|
+
// src/best-metrics/types.ts
|
|
1895
|
+
var bestMetricsLedgerVersion = 1;
|
|
1896
|
+
var rollingWindowDurationMs = 24 * 60 * 60 * 1000;
|
|
1897
|
+
var defaultBestMetricsIdleCutoffMs = 15 * 60 * 1000;
|
|
1898
|
+
|
|
1899
|
+
// src/best-metrics/read-best-ledger.ts
|
|
1900
|
+
var bestLedgerFileName = "bests-v1.json";
|
|
1901
|
+
async function readBestLedger(options = {}) {
|
|
1902
|
+
try {
|
|
1903
|
+
const rawLedgerText = await readFile2(resolveBestLedgerPath(options), "utf8");
|
|
1904
|
+
return parseBestLedger(JSON.parse(rawLedgerText));
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
if (isMissingFileError(error)) {
|
|
1907
|
+
return null;
|
|
1908
|
+
}
|
|
1909
|
+
throw error;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
function parseBestLedger(value) {
|
|
1913
|
+
const ledgerRecord = expectObject(value, "bestMetricsLedger");
|
|
1914
|
+
const version = readNumber(ledgerRecord, "version", "bestMetricsLedger");
|
|
1915
|
+
if (version !== bestMetricsLedgerVersion) {
|
|
1916
|
+
throw new Error(`bestMetricsLedger.version must be ${bestMetricsLedgerVersion}.`);
|
|
1917
|
+
}
|
|
1918
|
+
return {
|
|
1919
|
+
version: bestMetricsLedgerVersion,
|
|
1920
|
+
initializedAt: readIsoTimestamp(ledgerRecord.initializedAt, "bestMetricsLedger.initializedAt"),
|
|
1921
|
+
lastScannedAt: readIsoTimestamp(ledgerRecord.lastScannedAt, "bestMetricsLedger.lastScannedAt"),
|
|
1922
|
+
bestConcurrentAgents: parseBestMetricRecord(ledgerRecord.bestConcurrentAgents, "bestMetricsLedger.bestConcurrentAgents"),
|
|
1923
|
+
best24hRawBurn: parseBestMetricRecord(ledgerRecord.best24hRawBurn, "bestMetricsLedger.best24hRawBurn"),
|
|
1924
|
+
best24hAgentSumMs: parseBestMetricRecord(ledgerRecord.best24hAgentSumMs, "bestMetricsLedger.best24hAgentSumMs")
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
function resolveBestLedgerPath(options = {}) {
|
|
1928
|
+
return join2(resolveBestStateDirectory(options), bestLedgerFileName);
|
|
1929
|
+
}
|
|
1930
|
+
function serializeBestLedger(ledger) {
|
|
1931
|
+
const serializedLedger = {
|
|
1932
|
+
version: ledger.version,
|
|
1933
|
+
initializedAt: ledger.initializedAt.toISOString(),
|
|
1934
|
+
lastScannedAt: ledger.lastScannedAt.toISOString(),
|
|
1935
|
+
bestConcurrentAgents: serializeBestMetricRecord(ledger.bestConcurrentAgents),
|
|
1936
|
+
best24hRawBurn: serializeBestMetricRecord(ledger.best24hRawBurn),
|
|
1937
|
+
best24hAgentSumMs: serializeBestMetricRecord(ledger.best24hAgentSumMs)
|
|
1938
|
+
};
|
|
1939
|
+
return `${JSON.stringify(serializedLedger, null, 2)}
|
|
1940
|
+
`;
|
|
1941
|
+
}
|
|
1942
|
+
function parseBestMetricRecord(value, label) {
|
|
1943
|
+
if (value === null || value === undefined) {
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
const record = expectObject(value, label);
|
|
1947
|
+
return {
|
|
1948
|
+
value: readNumber(record, "value", label),
|
|
1949
|
+
observedAt: readIsoTimestamp(record.observedAt, `${label}.observedAt`),
|
|
1950
|
+
windowStart: readIsoTimestamp(record.windowStart, `${label}.windowStart`),
|
|
1951
|
+
windowEnd: readIsoTimestamp(record.windowEnd, `${label}.windowEnd`)
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
function serializeBestMetricRecord(record) {
|
|
1955
|
+
if (!record) {
|
|
1956
|
+
return null;
|
|
1957
|
+
}
|
|
1958
|
+
return {
|
|
1959
|
+
value: record.value,
|
|
1960
|
+
observedAt: record.observedAt.toISOString(),
|
|
1961
|
+
windowStart: record.windowStart.toISOString(),
|
|
1962
|
+
windowEnd: record.windowEnd.toISOString()
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
function resolveBestStateDirectory(options) {
|
|
1966
|
+
return options.stateDirectory ?? join2(homedir2(), ".idletime");
|
|
1967
|
+
}
|
|
1968
|
+
function isMissingFileError(error) {
|
|
1969
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1356
1970
|
}
|
|
1357
1971
|
|
|
1358
1972
|
// src/reporting/build-summary-report.ts
|
|
@@ -1362,7 +1976,7 @@ function buildSummaryReport(sessions, query) {
|
|
|
1362
1976
|
start: query.window.start,
|
|
1363
1977
|
end: query.window.end
|
|
1364
1978
|
};
|
|
1365
|
-
const metrics = clipActivityMetricsToWindow(buildActivityMetrics(filteredSessions, query.idleCutoffMs), windowInterval);
|
|
1979
|
+
const metrics = clipActivityMetricsToWindow(buildActivityMetrics(filteredSessions, query.idleCutoffMs, query.window.end), windowInterval);
|
|
1366
1980
|
const comparisonCutoffMs = parseDurationToMs("30m");
|
|
1367
1981
|
const sessionCounts = {
|
|
1368
1982
|
total: filteredSessions.length,
|
|
@@ -1373,7 +1987,7 @@ function buildSummaryReport(sessions, query) {
|
|
|
1373
1987
|
activityWindow: resolveActivityWindow(filteredSessions, windowInterval),
|
|
1374
1988
|
appliedFilters: query.filters,
|
|
1375
1989
|
comparisonCutoffMs,
|
|
1376
|
-
comparisonMetrics: query.idleCutoffMs === comparisonCutoffMs ? metrics : clipActivityMetricsToWindow(buildActivityMetrics(filteredSessions, comparisonCutoffMs), windowInterval),
|
|
1990
|
+
comparisonMetrics: query.idleCutoffMs === comparisonCutoffMs ? metrics : clipActivityMetricsToWindow(buildActivityMetrics(filteredSessions, comparisonCutoffMs, query.window.end), windowInterval),
|
|
1377
1991
|
directTokenTotals: sumTokenTotals(filteredSessions.filter((session) => session.kind === "direct"), windowInterval),
|
|
1378
1992
|
groupBreakdowns: buildGroupBreakdowns(filteredSessions, query.groupBy, query.idleCutoffMs, windowInterval),
|
|
1379
1993
|
idleCutoffMs: query.idleCutoffMs,
|
|
@@ -1387,11 +2001,11 @@ function buildSummaryReport(sessions, query) {
|
|
|
1387
2001
|
function buildGroupBreakdowns(sessions, dimensions, idleCutoffMs, windowInterval) {
|
|
1388
2002
|
return dimensions.map((dimension) => ({
|
|
1389
2003
|
dimension,
|
|
1390
|
-
rows: groupSessions(sessions, dimension).map((groupedSessions) => buildGroupRow(groupedSessions.key, groupedSessions.sessions, idleCutoffMs, windowInterval))
|
|
2004
|
+
rows: groupSessions(sessions, dimension).map((groupedSessions) => buildGroupRow(groupedSessions.key, groupedSessions.sessions, idleCutoffMs, windowInterval, windowInterval.end))
|
|
1391
2005
|
}));
|
|
1392
2006
|
}
|
|
1393
|
-
function buildGroupRow(key, sessions, idleCutoffMs, windowInterval) {
|
|
1394
|
-
const metrics = clipActivityMetricsToWindow(buildActivityMetrics(sessions, idleCutoffMs), windowInterval);
|
|
2007
|
+
function buildGroupRow(key, sessions, idleCutoffMs, windowInterval, observedAt) {
|
|
2008
|
+
const metrics = clipActivityMetricsToWindow(buildActivityMetrics(sessions, idleCutoffMs, observedAt), windowInterval);
|
|
1395
2009
|
const tokenTotals = sumTokenTotals(sessions, windowInterval);
|
|
1396
2010
|
return {
|
|
1397
2011
|
key,
|
|
@@ -1434,44 +2048,42 @@ function clipActivityMetricsToWindow(metrics, windowInterval) {
|
|
|
1434
2048
|
const directActivityBlocks = intersectTimeIntervals(metrics.directActivityBlocks, [windowInterval]);
|
|
1435
2049
|
const agentCoverageBlocks = intersectTimeIntervals(metrics.agentCoverageBlocks, [windowInterval]);
|
|
1436
2050
|
const agentOnlyBlocks = intersectTimeIntervals(metrics.agentOnlyBlocks, [windowInterval]);
|
|
1437
|
-
const
|
|
2051
|
+
const perAgentTaskBlocks = metrics.perAgentTaskBlocks.map((taskBlocks) => intersectTimeIntervals(taskBlocks, [windowInterval]));
|
|
1438
2052
|
return {
|
|
1439
2053
|
strictEngagementBlocks,
|
|
1440
2054
|
directActivityBlocks,
|
|
1441
2055
|
agentCoverageBlocks,
|
|
1442
2056
|
agentOnlyBlocks,
|
|
1443
|
-
|
|
2057
|
+
perAgentTaskBlocks,
|
|
1444
2058
|
strictEngagementMs: sumTimeIntervalsMs(strictEngagementBlocks),
|
|
1445
2059
|
directActivityMs: sumTimeIntervalsMs(directActivityBlocks),
|
|
1446
2060
|
agentCoverageMs: sumTimeIntervalsMs(agentCoverageBlocks),
|
|
1447
2061
|
agentOnlyMs: sumTimeIntervalsMs(agentOnlyBlocks),
|
|
1448
|
-
cumulativeAgentMs:
|
|
1449
|
-
peakConcurrentAgents: peakConcurrency(
|
|
2062
|
+
cumulativeAgentMs: perAgentTaskBlocks.reduce((totalDurationMs, taskBlocks) => totalDurationMs + sumTimeIntervalsMs(taskBlocks), 0),
|
|
2063
|
+
peakConcurrentAgents: peakConcurrency(perAgentTaskBlocks)
|
|
1450
2064
|
};
|
|
1451
2065
|
}
|
|
1452
2066
|
|
|
1453
2067
|
// src/reporting/render-summary-report.ts
|
|
1454
2068
|
var summaryBarWidth = 18;
|
|
1455
|
-
function renderSummaryReport(report, options, hourlyReport) {
|
|
1456
|
-
return options.shareMode ? renderShareSummaryReport(report, options, hourlyReport) : renderFullSummaryReport(report, options, hourlyReport);
|
|
2069
|
+
function renderSummaryReport(report, options, hourlyReport, bestPlaque = null) {
|
|
2070
|
+
return options.shareMode ? renderShareSummaryReport(report, options, hourlyReport, bestPlaque) : renderFullSummaryReport(report, options, hourlyReport, bestPlaque);
|
|
1457
2071
|
}
|
|
1458
|
-
function renderFullSummaryReport(report, options, hourlyReport) {
|
|
2072
|
+
function renderFullSummaryReport(report, options, hourlyReport, bestPlaque = null) {
|
|
1459
2073
|
const lines = [];
|
|
1460
2074
|
const requestedMetrics = report.metrics;
|
|
1461
2075
|
const actualComparisonMetrics = report.comparisonMetrics;
|
|
1462
2076
|
const windowDurationMs = report.window.end.getTime() - report.window.start.getTime();
|
|
1463
|
-
const headerLines =
|
|
1464
|
-
formatTimeRange(report.window.start, report.window.end, report.window),
|
|
1465
|
-
`${report.sessionCounts.total} sessions · ${formatDurationHours(requestedMetrics.strictEngagementMs)} focused · ${formatCompactInteger(report.tokenTotals.practicalBurn)} tokens`,
|
|
1466
|
-
...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
|
|
1467
|
-
];
|
|
2077
|
+
const headerLines = buildSummaryHeaderLines(report, hourlyReport);
|
|
1468
2078
|
const panelLines = renderPanel(`idletime • ${report.window.label}`, headerLines, options);
|
|
1469
2079
|
const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
|
|
1470
2080
|
const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
|
|
1471
|
-
lines.push(...buildLogoSection(logoSectionWidth, options));
|
|
2081
|
+
lines.push(...buildLogoSection(logoSectionWidth, options, bestPlaque));
|
|
1472
2082
|
lines.push("");
|
|
1473
2083
|
lines.push(...panelLines);
|
|
1474
2084
|
if (hourlyReport) {
|
|
2085
|
+
lines.push("");
|
|
2086
|
+
lines.push(...buildAgentSection(hourlyReport, options));
|
|
1475
2087
|
lines.push("");
|
|
1476
2088
|
lines.push(...buildRhythmSection(hourlyReport, options));
|
|
1477
2089
|
}
|
|
@@ -1519,20 +2131,18 @@ function renderFullSummaryReport(report, options, hourlyReport) {
|
|
|
1519
2131
|
return lines.join(`
|
|
1520
2132
|
`);
|
|
1521
2133
|
}
|
|
1522
|
-
function renderShareSummaryReport(report, options, hourlyReport) {
|
|
2134
|
+
function renderShareSummaryReport(report, options, hourlyReport, bestPlaque = null) {
|
|
1523
2135
|
const lines = [];
|
|
1524
|
-
const headerLines =
|
|
1525
|
-
formatTimeRange(report.window.start, report.window.end, report.window),
|
|
1526
|
-
`${report.sessionCounts.total} sessions · ${formatDurationHours(report.metrics.strictEngagementMs)} focused · ${formatCompactInteger(report.tokenTotals.practicalBurn)} tokens`,
|
|
1527
|
-
...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
|
|
1528
|
-
];
|
|
2136
|
+
const headerLines = buildSummaryHeaderLines(report, hourlyReport);
|
|
1529
2137
|
const panelLines = renderPanel(`idletime • ${report.window.label}`, headerLines, options);
|
|
1530
2138
|
const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
|
|
1531
2139
|
const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
|
|
1532
|
-
lines.push(...buildLogoSection(logoSectionWidth, options));
|
|
2140
|
+
lines.push(...buildLogoSection(logoSectionWidth, options, bestPlaque));
|
|
1533
2141
|
lines.push("");
|
|
1534
2142
|
lines.push(...panelLines);
|
|
1535
2143
|
if (hourlyReport) {
|
|
2144
|
+
lines.push("");
|
|
2145
|
+
lines.push(...buildAgentSection(hourlyReport, options));
|
|
1536
2146
|
lines.push("");
|
|
1537
2147
|
lines.push(...buildRhythmSection(hourlyReport, options));
|
|
1538
2148
|
}
|
|
@@ -1563,6 +2173,93 @@ function formatAppliedFilters(report) {
|
|
|
1563
2173
|
}
|
|
1564
2174
|
return appliedFilters;
|
|
1565
2175
|
}
|
|
2176
|
+
function buildSummaryHeaderLines(report, hourlyReport) {
|
|
2177
|
+
if (!hourlyReport) {
|
|
2178
|
+
return [
|
|
2179
|
+
formatTimeRange(report.window.start, report.window.end, report.window),
|
|
2180
|
+
`${report.sessionCounts.total} sessions · ${formatDurationHours(report.metrics.strictEngagementMs)} focused · ${formatCompactInteger(report.tokenTotals.practicalBurn)} tokens`,
|
|
2181
|
+
...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
|
|
2182
|
+
];
|
|
2183
|
+
}
|
|
2184
|
+
return [
|
|
2185
|
+
buildPostureLine(report, hourlyReport),
|
|
2186
|
+
buildBiggestStoryLine(report, hourlyReport),
|
|
2187
|
+
buildSupportFactsLine(report),
|
|
2188
|
+
...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
|
|
2189
|
+
];
|
|
2190
|
+
}
|
|
2191
|
+
function buildPostureLine(report, hourlyReport) {
|
|
2192
|
+
const directActivityMs = Math.max(report.metrics.directActivityMs, 1);
|
|
2193
|
+
const focusRatio = report.metrics.strictEngagementMs / directActivityMs;
|
|
2194
|
+
const agentCoverageRatio = report.metrics.agentCoverageMs / directActivityMs;
|
|
2195
|
+
const quietRatio = sumQuietMs(hourlyReport) / Math.max(1, report.window.end.getTime() - report.window.start.getTime());
|
|
2196
|
+
const posture = quietRatio >= 0.45 ? "Fragmented day" : agentCoverageRatio >= 0.75 && focusRatio < 0.65 ? "Mostly orchestrating" : focusRatio >= 0.8 ? "Mostly in the loop" : report.metrics.peakConcurrentAgents >= 6 ? "Heavy agent day" : "Balanced day";
|
|
2197
|
+
return `${posture}: ${formatDurationHours(report.metrics.strictEngagementMs)} focused, ${formatDurationHours(report.metrics.agentCoverageMs)} agent live`;
|
|
2198
|
+
}
|
|
2199
|
+
function buildBiggestStoryLine(report, hourlyReport) {
|
|
2200
|
+
const longestQuietRun = findLongestQuietRun(hourlyReport);
|
|
2201
|
+
const peakBurnBucket = hourlyReport.buckets.reduce((currentPeak, bucket) => bucket.practicalBurn > currentPeak.practicalBurn ? bucket : currentPeak, hourlyReport.buckets[0]);
|
|
2202
|
+
const quietPhrase = longestQuietRun.durationMs >= 2 * 3600000 ? `long quiet stretch ${describeDayPeriod(longestQuietRun.start, report)}` : "steady rhythm overall";
|
|
2203
|
+
return `Biggest story: ${quietPhrase}, big burn ${describeDayPeriod(peakBurnBucket.start, report)}`;
|
|
2204
|
+
}
|
|
2205
|
+
function buildSupportFactsLine(report) {
|
|
2206
|
+
return `${report.sessionCounts.direct} direct / ${report.sessionCounts.subagent} subagent • ${report.metrics.peakConcurrentAgents} peak • ${formatCompactInteger(report.tokenTotals.practicalBurn)} burn`;
|
|
2207
|
+
}
|
|
2208
|
+
function sumQuietMs(hourlyReport) {
|
|
2209
|
+
return hourlyReport.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs), 0);
|
|
2210
|
+
}
|
|
2211
|
+
function findLongestQuietRun(hourlyReport) {
|
|
2212
|
+
let longestQuietRun = {
|
|
2213
|
+
durationMs: 0,
|
|
2214
|
+
start: hourlyReport.buckets[0]?.start ?? new Date(0)
|
|
2215
|
+
};
|
|
2216
|
+
let currentStart = null;
|
|
2217
|
+
let currentDurationMs = 0;
|
|
2218
|
+
for (const bucket of hourlyReport.buckets) {
|
|
2219
|
+
const quietMs = Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs);
|
|
2220
|
+
const isQuietBucket = quietMs >= 30 * 60000;
|
|
2221
|
+
if (isQuietBucket) {
|
|
2222
|
+
currentStart ??= bucket.start;
|
|
2223
|
+
currentDurationMs += quietMs;
|
|
2224
|
+
continue;
|
|
2225
|
+
}
|
|
2226
|
+
if (currentStart && currentDurationMs > longestQuietRun.durationMs) {
|
|
2227
|
+
longestQuietRun = {
|
|
2228
|
+
durationMs: currentDurationMs,
|
|
2229
|
+
start: currentStart
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
currentStart = null;
|
|
2233
|
+
currentDurationMs = 0;
|
|
2234
|
+
}
|
|
2235
|
+
if (currentStart && currentDurationMs > longestQuietRun.durationMs) {
|
|
2236
|
+
longestQuietRun = {
|
|
2237
|
+
durationMs: currentDurationMs,
|
|
2238
|
+
start: currentStart
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
if (longestQuietRun.durationMs > 0) {
|
|
2242
|
+
return longestQuietRun;
|
|
2243
|
+
}
|
|
2244
|
+
const quietestBucket = hourlyReport.buckets.reduce((currentQuietest, bucket) => {
|
|
2245
|
+
const quietMs = Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs);
|
|
2246
|
+
return quietMs > currentQuietest.durationMs ? { durationMs: quietMs, start: bucket.start } : currentQuietest;
|
|
2247
|
+
}, longestQuietRun);
|
|
2248
|
+
return quietestBucket;
|
|
2249
|
+
}
|
|
2250
|
+
function describeDayPeriod(timestamp, report) {
|
|
2251
|
+
const hourOfDay = Number.parseInt(formatHourOfDay(timestamp, report.window), 10);
|
|
2252
|
+
if (hourOfDay >= 21 || hourOfDay < 5) {
|
|
2253
|
+
return "overnight";
|
|
2254
|
+
}
|
|
2255
|
+
if (hourOfDay < 12) {
|
|
2256
|
+
return "this morning";
|
|
2257
|
+
}
|
|
2258
|
+
if (hourOfDay < 17) {
|
|
2259
|
+
return "this afternoon";
|
|
2260
|
+
}
|
|
2261
|
+
return "this evening";
|
|
2262
|
+
}
|
|
1566
2263
|
function formatDurationLabel(durationMs) {
|
|
1567
2264
|
return `${Math.round(durationMs / 60000)}m`;
|
|
1568
2265
|
}
|
|
@@ -1574,12 +2271,21 @@ function renderSnapshotRow(label, primaryText, detailText, role, options) {
|
|
|
1574
2271
|
}
|
|
1575
2272
|
|
|
1576
2273
|
// src/cli/run-last24h-command.ts
|
|
1577
|
-
async function
|
|
1578
|
-
const window = resolveTrailingReportWindow({
|
|
1579
|
-
|
|
2274
|
+
async function buildLast24hCommandResult(command, options = {}) {
|
|
2275
|
+
const window = resolveTrailingReportWindow({
|
|
2276
|
+
durationMs: command.hourlyWindowMs,
|
|
2277
|
+
now: options.now
|
|
2278
|
+
});
|
|
2279
|
+
const bestLedgerPromise = readBestLedger({ stateDirectory: options.stateDirectory });
|
|
2280
|
+
const sessionsPromise = readCodexSessions({
|
|
1580
2281
|
windowStart: window.start,
|
|
1581
|
-
windowEnd: window.end
|
|
2282
|
+
windowEnd: window.end,
|
|
2283
|
+
sessionRootDirectory: options.sessionRootDirectory
|
|
1582
2284
|
});
|
|
2285
|
+
const [bestLedger, sessions] = await Promise.all([
|
|
2286
|
+
bestLedgerPromise,
|
|
2287
|
+
sessionsPromise
|
|
2288
|
+
]);
|
|
1583
2289
|
const summaryReport = buildSummaryReport(sessions, {
|
|
1584
2290
|
filters: command.filters,
|
|
1585
2291
|
groupBy: command.groupBy,
|
|
@@ -1593,39 +2299,1066 @@ async function runLast24hCommand(command) {
|
|
|
1593
2299
|
wakeWindow: command.wakeWindow,
|
|
1594
2300
|
window
|
|
1595
2301
|
});
|
|
1596
|
-
return
|
|
2302
|
+
return {
|
|
2303
|
+
bestLedger,
|
|
2304
|
+
hourlyReport,
|
|
2305
|
+
summaryReport
|
|
2306
|
+
};
|
|
1597
2307
|
}
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
const window = resolveTodayReportWindow();
|
|
1602
|
-
const sessions = await readCodexSessions({
|
|
1603
|
-
windowStart: window.start,
|
|
1604
|
-
windowEnd: window.end
|
|
1605
|
-
});
|
|
1606
|
-
return renderSummaryReport(buildSummaryReport(sessions, {
|
|
1607
|
-
filters: command.filters,
|
|
1608
|
-
groupBy: command.groupBy,
|
|
1609
|
-
idleCutoffMs: command.idleCutoffMs,
|
|
1610
|
-
wakeWindow: command.wakeWindow,
|
|
1611
|
-
window
|
|
1612
|
-
}), createRenderOptions(command.shareMode));
|
|
2308
|
+
async function runLast24hCommand(command, options = {}) {
|
|
2309
|
+
const commandResult = await buildLast24hCommandResult(command, options);
|
|
2310
|
+
return renderSummaryReport(commandResult.summaryReport, createRenderOptions(command.shareMode), commandResult.hourlyReport, commandResult.bestLedger ? buildBestPlaque(commandResult.bestLedger) : null);
|
|
1613
2311
|
}
|
|
1614
2312
|
|
|
1615
|
-
// src/
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
2313
|
+
// src/reporting/build-live-report.ts
|
|
2314
|
+
var doneRecentWindowMs = 15 * 60000;
|
|
2315
|
+
var directSessionWarmMs = 15 * 60000;
|
|
2316
|
+
var recentConcurrencyBucketCount = 15;
|
|
2317
|
+
var recentConcurrencyBucketMs = 60000;
|
|
2318
|
+
var waitingOnUserWarmMs = 30 * 60000;
|
|
2319
|
+
function buildLiveReport(sessions, query) {
|
|
2320
|
+
const observedAt = query.observedAt ?? new Date;
|
|
2321
|
+
const appliedFilters = { ...query.filters };
|
|
2322
|
+
const filteredSessions = filterSessions(sessions, appliedFilters);
|
|
2323
|
+
const liveTaskWindows = filteredSessions.flatMap((session) => session.taskWindows);
|
|
2324
|
+
const runningTaskWindows = liveTaskWindows.filter((taskWindow) => isTaskWindowRunning(taskWindow, observedAt));
|
|
2325
|
+
const waitingOnUserThreads = resolveWaitingOnUserThreads(filteredSessions, observedAt);
|
|
2326
|
+
const childTaskWindows = filteredSessions.filter((session) => session.kind === "subagent").flatMap((session) => session.taskWindows);
|
|
2327
|
+
const recentWindowStart = new Date(observedAt.getTime() - doneRecentWindowMs);
|
|
2328
|
+
return {
|
|
2329
|
+
appliedFilters,
|
|
2330
|
+
doneRecentCount: liveTaskWindows.filter((taskWindow) => isTaskWindowCompletedBetween(taskWindow, recentWindowStart, observedAt)).length,
|
|
2331
|
+
doneRecentWindowMs,
|
|
2332
|
+
doneThisTurnCount: countDoneThisTurn(filteredSessions, childTaskWindows, observedAt),
|
|
2333
|
+
observedAt,
|
|
2334
|
+
peakTodayCount: peakConcurrency([
|
|
2335
|
+
collectTaskIntervalsForWindow(liveTaskWindows, observedAt, startOfLocalDay2(observedAt), observedAt)
|
|
2336
|
+
]),
|
|
2337
|
+
recentConcurrencyValues: buildRecentConcurrencyValues(liveTaskWindows, observedAt),
|
|
2338
|
+
runningCount: runningTaskWindows.length,
|
|
2339
|
+
runningLocations: buildRunningLocations(runningTaskWindows),
|
|
2340
|
+
waitingThreads: waitingOnUserThreads,
|
|
2341
|
+
waitingOnUserCount: waitingOnUserThreads.length,
|
|
2342
|
+
waitingOnUserLocations: buildWaitingOnUserLocations(waitingOnUserThreads),
|
|
2343
|
+
scope: appliedFilters.workspaceOnlyPrefix ? "workspace" : "global",
|
|
2344
|
+
workspacePrefix: appliedFilters.workspaceOnlyPrefix
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
function buildRunningLocations(runningTaskWindows) {
|
|
2348
|
+
const runningLocations = new Map;
|
|
2349
|
+
for (const taskWindow of runningTaskWindows) {
|
|
2350
|
+
runningLocations.set(taskWindow.cwd, (runningLocations.get(taskWindow.cwd) ?? 0) + 1);
|
|
1621
2351
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
2352
|
+
return [...runningLocations.entries()].map(([cwd, runningCount]) => ({ cwd, runningCount })).sort((leftLocation, rightLocation) => rightLocation.runningCount - leftLocation.runningCount || leftLocation.cwd.localeCompare(rightLocation.cwd));
|
|
2353
|
+
}
|
|
2354
|
+
function buildWaitingOnUserLocations(waitingOnUserThreads) {
|
|
2355
|
+
const waitingLocations = new Map;
|
|
2356
|
+
for (const waitingThread of waitingOnUserThreads) {
|
|
2357
|
+
waitingLocations.set(waitingThread.cwd, (waitingLocations.get(waitingThread.cwd) ?? 0) + 1);
|
|
1625
2358
|
}
|
|
1626
|
-
|
|
2359
|
+
return [...waitingLocations.entries()].map(([cwd, waitingCount]) => ({ cwd, waitingCount })).sort((leftLocation, rightLocation) => rightLocation.waitingCount - leftLocation.waitingCount || leftLocation.cwd.localeCompare(rightLocation.cwd));
|
|
2360
|
+
}
|
|
2361
|
+
function buildRecentConcurrencyValues(taskWindows, observedAt) {
|
|
2362
|
+
const values = [];
|
|
2363
|
+
for (let bucketIndex = recentConcurrencyBucketCount - 1;bucketIndex >= 0; bucketIndex -= 1) {
|
|
2364
|
+
const bucketStart = new Date(observedAt.getTime() - (bucketIndex + 1) * recentConcurrencyBucketMs);
|
|
2365
|
+
const bucketEnd = new Date(bucketStart.getTime() + recentConcurrencyBucketMs);
|
|
2366
|
+
values.push(peakConcurrency([
|
|
2367
|
+
collectTaskIntervalsForWindow(taskWindows, observedAt, bucketStart, bucketEnd)
|
|
2368
|
+
]));
|
|
2369
|
+
}
|
|
2370
|
+
return values;
|
|
2371
|
+
}
|
|
2372
|
+
function collectTaskIntervalsForWindow(taskWindows, observedAt, windowStart, windowEnd) {
|
|
2373
|
+
return taskWindows.flatMap((taskWindow) => {
|
|
2374
|
+
const taskWindowInterval = buildTaskWindowInterval(taskWindow, observedAt);
|
|
2375
|
+
if (!taskWindowInterval) {
|
|
2376
|
+
return [];
|
|
2377
|
+
}
|
|
2378
|
+
const clippedStart = new Date(Math.max(taskWindowInterval.start.getTime(), windowStart.getTime()));
|
|
2379
|
+
const clippedEnd = new Date(Math.min(taskWindowInterval.end.getTime(), windowEnd.getTime()));
|
|
2380
|
+
if (clippedStart.getTime() >= clippedEnd.getTime()) {
|
|
2381
|
+
return [];
|
|
2382
|
+
}
|
|
2383
|
+
return [{ start: clippedStart, end: clippedEnd }];
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
function countDoneThisTurn(sessions, subagentTaskWindows, observedAt) {
|
|
2387
|
+
const rootTurnAnchor = resolveRootTurnAnchor(sessions, observedAt);
|
|
2388
|
+
if (!rootTurnAnchor) {
|
|
2389
|
+
return 0;
|
|
2390
|
+
}
|
|
2391
|
+
return subagentTaskWindows.filter((taskWindow) => {
|
|
2392
|
+
if (taskWindow.parentSessionId !== rootTurnAnchor.sessionId || !taskWindow.completedAt) {
|
|
2393
|
+
return false;
|
|
2394
|
+
}
|
|
2395
|
+
return taskWindow.completedAt.getTime() > rootTurnAnchor.startedAt.getTime() && taskWindow.completedAt.getTime() <= observedAt.getTime();
|
|
2396
|
+
}).length;
|
|
2397
|
+
}
|
|
2398
|
+
function resolveRootTurnAnchor(sessions, observedAt) {
|
|
2399
|
+
const warmCutoff = observedAt.getTime() - directSessionWarmMs;
|
|
2400
|
+
const warmDirectSessions = sessions.filter((session) => session.kind === "direct").filter((session) => session.lastTimestamp.getTime() >= warmCutoff).filter((session) => session.userMessageTimestamps.length > 0);
|
|
2401
|
+
if (warmDirectSessions.length === 0) {
|
|
2402
|
+
return null;
|
|
2403
|
+
}
|
|
2404
|
+
const activeRootSession = warmDirectSessions.reduce((latestSession, session) => session.lastTimestamp.getTime() > latestSession.lastTimestamp.getTime() ? session : latestSession);
|
|
2405
|
+
const latestUserMessageTimestamp = activeRootSession.userMessageTimestamps[activeRootSession.userMessageTimestamps.length - 1] ?? null;
|
|
2406
|
+
if (!latestUserMessageTimestamp) {
|
|
2407
|
+
return null;
|
|
2408
|
+
}
|
|
2409
|
+
return {
|
|
2410
|
+
sessionId: activeRootSession.sessionId,
|
|
2411
|
+
startedAt: latestUserMessageTimestamp
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
function resolveWaitingOnUserThreads(sessions, observedAt) {
|
|
2415
|
+
const warmCutoff = observedAt.getTime() - waitingOnUserWarmMs;
|
|
2416
|
+
return sessions.flatMap((session) => {
|
|
2417
|
+
if (session.kind !== "direct" || session.taskWindows.length === 0) {
|
|
2418
|
+
return [];
|
|
2419
|
+
}
|
|
2420
|
+
const latestTaskWindow = session.taskWindows.reduce((latestWindow, taskWindow) => taskWindow.startedAt.getTime() > latestWindow.startedAt.getTime() ? taskWindow : latestWindow);
|
|
2421
|
+
const latestUserMessageTimestamp = session.userMessageTimestamps[session.userMessageTimestamps.length - 1] ?? null;
|
|
2422
|
+
if (latestTaskWindow.completedAt === null || !latestUserMessageTimestamp || latestTaskWindow.completedAt.getTime() <= latestUserMessageTimestamp.getTime()) {
|
|
2423
|
+
return [];
|
|
2424
|
+
}
|
|
2425
|
+
if (latestTaskWindow.completedAt.getTime() < warmCutoff) {
|
|
2426
|
+
return [];
|
|
2427
|
+
}
|
|
2428
|
+
return [
|
|
2429
|
+
{
|
|
2430
|
+
cwd: session.cwd,
|
|
2431
|
+
sessionId: session.sessionId,
|
|
2432
|
+
waitDurationMs: observedAt.getTime() - latestTaskWindow.completedAt.getTime(),
|
|
2433
|
+
waitingSince: latestTaskWindow.completedAt
|
|
2434
|
+
}
|
|
2435
|
+
];
|
|
2436
|
+
}).sort((leftThread, rightThread) => rightThread.waitDurationMs - leftThread.waitDurationMs || leftThread.waitingSince.getTime() - rightThread.waitingSince.getTime());
|
|
2437
|
+
}
|
|
2438
|
+
function startOfLocalDay2(timestamp) {
|
|
2439
|
+
return new Date(timestamp.getFullYear(), timestamp.getMonth(), timestamp.getDate(), 0, 0, 0, 0);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// src/reporting/render-live-report.ts
|
|
2443
|
+
import { basename } from "node:path";
|
|
2444
|
+
var bigDigitGlyphs = {
|
|
2445
|
+
"0": ["█▀█", "█ █", "▀▀▀"],
|
|
2446
|
+
"1": [" ▄█", " █", "▄▄█"],
|
|
2447
|
+
"2": ["█▀█", " ▄▀", "█▄▄"],
|
|
2448
|
+
"3": ["█▀█", " ▀▄", "█▄█"],
|
|
2449
|
+
"4": ["█ █", "█▄█", " █"],
|
|
2450
|
+
"5": ["█▀▀", "▀▀▄", "▄▄█"],
|
|
2451
|
+
"6": ["█▀▀", "█▀█", "█▄█"],
|
|
2452
|
+
"7": ["█▀█", " █", " █"],
|
|
2453
|
+
"8": ["█▀█", "█▄█", "█▄█"],
|
|
2454
|
+
"9": ["█▀█", "█▄█", " █"]
|
|
2455
|
+
};
|
|
2456
|
+
function renderLiveReport(report, options) {
|
|
2457
|
+
const headerLines = renderPanel("idletime live", [
|
|
2458
|
+
buildScopeLine(report),
|
|
2459
|
+
`observed ${formatTimestamp(report.observedAt, { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone })} • refresh 5s`,
|
|
2460
|
+
buildContextLine(report)
|
|
2461
|
+
], options);
|
|
2462
|
+
const panelWidth = measureVisibleTextWidth(headerLines[0] ?? "");
|
|
2463
|
+
const lines = [];
|
|
2464
|
+
lines.push(...buildLogoSection(panelWidth, options));
|
|
2465
|
+
lines.push("");
|
|
2466
|
+
lines.push(...headerLines);
|
|
2467
|
+
lines.push("");
|
|
2468
|
+
lines.push(...buildScoreboardSection(report, options));
|
|
2469
|
+
return lines.join(`
|
|
2470
|
+
`);
|
|
2471
|
+
}
|
|
2472
|
+
function renderLiveErrorReport(workspacePrefix, error, options) {
|
|
2473
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2474
|
+
const panelLines = renderPanel("idletime live", [
|
|
2475
|
+
workspacePrefix ? `scope workspace • ${shortenPath(workspacePrefix, 40)}` : "scope global",
|
|
2476
|
+
"live refresh failed; retrying in 5s",
|
|
2477
|
+
errorMessage
|
|
2478
|
+
], options);
|
|
2479
|
+
const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
|
|
2480
|
+
return [...buildLogoSection(panelWidth, options), "", ...panelLines].join(`
|
|
2481
|
+
`);
|
|
2482
|
+
}
|
|
2483
|
+
function buildScopeLine(report) {
|
|
2484
|
+
return report.scope === "workspace" && report.workspacePrefix ? `scope workspace • ${shortenPath(report.workspacePrefix, 40)}` : "scope global";
|
|
2485
|
+
}
|
|
2486
|
+
function buildContextLine(report) {
|
|
2487
|
+
const filterParts = [];
|
|
2488
|
+
if (report.appliedFilters.model) {
|
|
2489
|
+
filterParts.push(`model ${report.appliedFilters.model}`);
|
|
2490
|
+
}
|
|
2491
|
+
if (report.appliedFilters.reasoningEffort) {
|
|
2492
|
+
filterParts.push(`effort ${report.appliedFilters.reasoningEffort}`);
|
|
2493
|
+
}
|
|
2494
|
+
if (report.appliedFilters.sessionKind) {
|
|
2495
|
+
filterParts.push(`kind ${report.appliedFilters.sessionKind}`);
|
|
2496
|
+
}
|
|
2497
|
+
return filterParts.length > 0 ? filterParts.join(" • ") : "all sessions";
|
|
2498
|
+
}
|
|
2499
|
+
function renderBigDigits(value) {
|
|
2500
|
+
const rows = ["", "", ""];
|
|
2501
|
+
for (const character of value) {
|
|
2502
|
+
const glyph = bigDigitGlyphs[character] ?? [" ", " ? ", " "];
|
|
2503
|
+
rows[0] += `${glyph[0]} `;
|
|
2504
|
+
rows[1] += `${glyph[1]} `;
|
|
2505
|
+
rows[2] += `${glyph[2]} `;
|
|
2506
|
+
}
|
|
2507
|
+
return rows;
|
|
2508
|
+
}
|
|
2509
|
+
function buildScoreboardSection(report, options) {
|
|
2510
|
+
const waitingLabel = "waiting on you";
|
|
2511
|
+
const runningLabel = "running";
|
|
2512
|
+
const waitingDigits = renderBigDigits(report.waitingOnUserCount.toString());
|
|
2513
|
+
const runningDigits = renderBigDigits(report.runningCount.toString());
|
|
2514
|
+
const leftColumnWidth = Math.max(waitingLabel.length, ...waitingDigits.map((line) => line.length));
|
|
2515
|
+
const rightColumnWidth = Math.max(runningLabel.length, ...runningDigits.map((line) => line.length));
|
|
2516
|
+
const columnGap = " ";
|
|
2517
|
+
const lines = [
|
|
2518
|
+
joinScoreboardColumns(waitingLabel, runningLabel, leftColumnWidth, rightColumnWidth, options),
|
|
2519
|
+
...waitingDigits.map((line, lineIndex) => joinScoreboardColumns(line, runningDigits[lineIndex] ?? "", leftColumnWidth, rightColumnWidth, options)),
|
|
2520
|
+
"",
|
|
2521
|
+
`${paint(" recent ", "muted", options)}${paint(buildSparkline(report.recentConcurrencyValues), "agent", options)}`,
|
|
2522
|
+
...buildRunningLocationLines(report, options),
|
|
2523
|
+
...buildWaitingLocationLines(report, options),
|
|
2524
|
+
...buildWaitingThreadLines(report, options),
|
|
2525
|
+
`${paint(" this turn ", "muted", options)}${paint(`${report.doneThisTurnCount} done`, "value", options)}`,
|
|
2526
|
+
`${paint(" today peak ", "muted", options)}${paint(`${report.peakTodayCount} concurrent`, "value", options)}`
|
|
2527
|
+
];
|
|
2528
|
+
function joinScoreboardColumns(leftValue, rightValue, leftWidth, rightWidth, renderOptions) {
|
|
2529
|
+
return ` ${paint(padRight(leftValue, leftWidth), "agent", renderOptions)}${columnGap}${paint(padRight(rightValue, rightWidth), "value", renderOptions)}`;
|
|
2530
|
+
}
|
|
2531
|
+
return lines;
|
|
2532
|
+
}
|
|
2533
|
+
function buildRunningLocationLines(report, options) {
|
|
2534
|
+
if (report.runningLocations.length === 0) {
|
|
2535
|
+
return [
|
|
2536
|
+
`${paint(" running at ", "muted", options)}${paint("no active tasks", "muted", options)}`
|
|
2537
|
+
];
|
|
2538
|
+
}
|
|
2539
|
+
const visibleLocations = report.runningLocations.slice(0, 3);
|
|
2540
|
+
const lines = visibleLocations.map((location, locationIndex) => {
|
|
2541
|
+
const label = locationIndex === 0 ? " running at " : " ";
|
|
2542
|
+
return `${paint(label, "muted", options)}${paint(`${location.runningCount} ${formatLocationLabel(location.cwd)}`, "value", options)}`;
|
|
2543
|
+
});
|
|
2544
|
+
const hiddenLocationCount = report.runningLocations.length - visibleLocations.length;
|
|
2545
|
+
if (hiddenLocationCount > 0) {
|
|
2546
|
+
lines.push(`${paint(" ", "muted", options)}${paint(`+${hiddenLocationCount} more`, "muted", options)}`);
|
|
2547
|
+
}
|
|
2548
|
+
return lines;
|
|
2549
|
+
}
|
|
2550
|
+
function buildWaitingLocationLines(report, options) {
|
|
2551
|
+
if (report.waitingOnUserLocations.length === 0) {
|
|
2552
|
+
return [
|
|
2553
|
+
`${paint(" waiting at ", "muted", options)}${paint("nothing waiting", "muted", options)}`
|
|
2554
|
+
];
|
|
2555
|
+
}
|
|
2556
|
+
const visibleLocations = report.waitingOnUserLocations.slice(0, 3);
|
|
2557
|
+
const lines = visibleLocations.map((location, locationIndex) => {
|
|
2558
|
+
const label = locationIndex === 0 ? " waiting at " : " ";
|
|
2559
|
+
return `${paint(label, "muted", options)}${paint(`${location.waitingCount} ${formatLocationLabel(location.cwd)}`, "value", options)}`;
|
|
2560
|
+
});
|
|
2561
|
+
const hiddenLocationCount = report.waitingOnUserLocations.length - visibleLocations.length;
|
|
2562
|
+
if (hiddenLocationCount > 0) {
|
|
2563
|
+
lines.push(`${paint(" ", "muted", options)}${paint(`+${hiddenLocationCount} more`, "muted", options)}`);
|
|
2564
|
+
}
|
|
2565
|
+
return lines;
|
|
2566
|
+
}
|
|
2567
|
+
function buildWaitingThreadLines(report, options) {
|
|
2568
|
+
if (report.waitingThreads.length === 0) {
|
|
2569
|
+
return [];
|
|
2570
|
+
}
|
|
2571
|
+
return report.waitingThreads.slice(0, 3).map((waitingThread, waitingIndex) => {
|
|
2572
|
+
const label = waitingIndex === 0 ? " top waiting " : " ";
|
|
2573
|
+
return `${paint(label, "muted", options)}${paint(`${formatLocationLabel(waitingThread.cwd)} • ${formatDurationCompact(waitingThread.waitDurationMs)} • ${formatThreadLabel(waitingThread.sessionId)}`, "value", options)}`;
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
function formatLocationLabel(cwd) {
|
|
2577
|
+
if (cwd.endsWith("/.agents")) {
|
|
2578
|
+
return "~/.agents";
|
|
2579
|
+
}
|
|
2580
|
+
return basename(cwd) || shortenPath(cwd, 24);
|
|
2581
|
+
}
|
|
2582
|
+
function formatThreadLabel(sessionId) {
|
|
2583
|
+
return sessionId.slice(-6);
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// src/cli/run-live-command.ts
|
|
2587
|
+
var liveRefreshIntervalMs = 5000;
|
|
2588
|
+
var enterLiveScreenSequence = "\x1B[?1049h\x1B[2J\x1B[H\x1B[?25l";
|
|
2589
|
+
var exitLiveScreenSequence = "\x1B[0m\x1B[?25h\x1B[?1049l";
|
|
2590
|
+
async function runLiveCommand(command) {
|
|
2591
|
+
const renderOptions = createRenderOptions(false);
|
|
2592
|
+
if (!process.stdout.isTTY) {
|
|
2593
|
+
const liveReport = await takeLiveSnapshot(command);
|
|
2594
|
+
console.log(renderLiveReport(liveReport, renderOptions));
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2597
|
+
let shouldStop = false;
|
|
2598
|
+
let previousFrameLineCount = 0;
|
|
2599
|
+
const canCaptureInput = process.stdin.isTTY && typeof process.stdin.setRawMode === "function";
|
|
2600
|
+
const stopLiveLoop = () => {
|
|
2601
|
+
shouldStop = true;
|
|
2602
|
+
};
|
|
2603
|
+
const handleInput = (input) => {
|
|
2604
|
+
const inputText = typeof input === "string" ? input : input.toString("utf8");
|
|
2605
|
+
if (inputText.includes("\x03") || inputText.toLowerCase().includes("q")) {
|
|
2606
|
+
stopLiveLoop();
|
|
2607
|
+
}
|
|
2608
|
+
};
|
|
2609
|
+
process.on("SIGINT", stopLiveLoop);
|
|
2610
|
+
process.on("SIGTERM", stopLiveLoop);
|
|
2611
|
+
if (canCaptureInput) {
|
|
2612
|
+
process.stdin.setRawMode(true);
|
|
2613
|
+
process.stdin.resume();
|
|
2614
|
+
process.stdin.on("data", handleInput);
|
|
2615
|
+
}
|
|
2616
|
+
process.stdout.write(enterLiveScreenSequence);
|
|
2617
|
+
try {
|
|
2618
|
+
while (!shouldStop) {
|
|
2619
|
+
const observedAt = new Date;
|
|
2620
|
+
try {
|
|
2621
|
+
const liveReport = await takeLiveSnapshot(command, {
|
|
2622
|
+
observedAt
|
|
2623
|
+
});
|
|
2624
|
+
previousFrameLineCount = drawFrame(renderLiveReport(liveReport, renderOptions), previousFrameLineCount);
|
|
2625
|
+
} catch (error) {
|
|
2626
|
+
previousFrameLineCount = drawFrame(renderLiveErrorReport(command.filters.workspaceOnlyPrefix, error, renderOptions), previousFrameLineCount);
|
|
2627
|
+
}
|
|
2628
|
+
if (shouldStop) {
|
|
2629
|
+
break;
|
|
2630
|
+
}
|
|
2631
|
+
await waitForNextRefresh();
|
|
2632
|
+
}
|
|
2633
|
+
} finally {
|
|
2634
|
+
process.off("SIGINT", stopLiveLoop);
|
|
2635
|
+
process.off("SIGTERM", stopLiveLoop);
|
|
2636
|
+
if (canCaptureInput) {
|
|
2637
|
+
process.stdin.off("data", handleInput);
|
|
2638
|
+
process.stdin.setRawMode(false);
|
|
2639
|
+
process.stdin.pause();
|
|
2640
|
+
}
|
|
2641
|
+
process.stdout.write(exitLiveScreenSequence);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
async function takeLiveSnapshot(command, options = {}) {
|
|
2645
|
+
const observedAt = options.observedAt ?? new Date;
|
|
2646
|
+
const recentWindow = resolveTrailingReportWindow({
|
|
2647
|
+
durationMs: 24 * 60 * 60 * 1000,
|
|
2648
|
+
now: observedAt
|
|
2649
|
+
});
|
|
2650
|
+
const sessions = await readCodexSessions({
|
|
2651
|
+
windowStart: recentWindow.start,
|
|
2652
|
+
windowEnd: observedAt,
|
|
2653
|
+
sessionRootDirectory: options.sessionRootDirectory
|
|
2654
|
+
});
|
|
2655
|
+
return buildLiveReport(sessions, {
|
|
2656
|
+
filters: command.filters,
|
|
2657
|
+
observedAt
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
function drawFrame(frameText, previousFrameLineCount) {
|
|
2661
|
+
const frameLines = frameText.split(`
|
|
2662
|
+
`);
|
|
2663
|
+
process.stdout.write("\x1B[H");
|
|
2664
|
+
for (const [lineIndex, line] of frameLines.entries()) {
|
|
2665
|
+
if (lineIndex > 0) {
|
|
2666
|
+
process.stdout.write(`
|
|
2667
|
+
\r`);
|
|
2668
|
+
}
|
|
2669
|
+
process.stdout.write("\x1B[2K");
|
|
2670
|
+
process.stdout.write(line);
|
|
2671
|
+
}
|
|
2672
|
+
for (let lineIndex = frameLines.length;lineIndex < previousFrameLineCount; lineIndex += 1) {
|
|
2673
|
+
process.stdout.write(`
|
|
2674
|
+
\r\x1B[2K`);
|
|
2675
|
+
}
|
|
2676
|
+
return frameLines.length;
|
|
2677
|
+
}
|
|
2678
|
+
function waitForNextRefresh() {
|
|
2679
|
+
return new Promise((resolve) => {
|
|
2680
|
+
setTimeout(resolve, liveRefreshIntervalMs);
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
// src/best-metrics/notification-delivery.ts
|
|
2685
|
+
import { execFile } from "node:child_process";
|
|
2686
|
+
import { existsSync } from "node:fs";
|
|
2687
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
2688
|
+
import { promisify } from "node:util";
|
|
2689
|
+
var execFileAsync = promisify(execFile);
|
|
2690
|
+
async function deliverLocalNotifications(notifications, options = {}) {
|
|
2691
|
+
const platform = options.platform ?? process.platform;
|
|
2692
|
+
if (platform !== "darwin" || notifications.length === 0) {
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
const notifier = options.notifier ?? sendMacOsNotification;
|
|
2696
|
+
for (const notification of notifications) {
|
|
2697
|
+
try {
|
|
2698
|
+
await notifier(notification);
|
|
2699
|
+
} catch {
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
async function sendMacOsNotification(notification) {
|
|
2705
|
+
const notificationIconPath = resolveNotificationIconPath();
|
|
2706
|
+
try {
|
|
2707
|
+
await execFileAsync("terminal-notifier", [
|
|
2708
|
+
"-title",
|
|
2709
|
+
notification.title,
|
|
2710
|
+
"-message",
|
|
2711
|
+
notification.body,
|
|
2712
|
+
...notificationIconPath ? ["-appIcon", pathToFileURL(notificationIconPath).href] : []
|
|
2713
|
+
]);
|
|
2714
|
+
return;
|
|
2715
|
+
} catch (error) {
|
|
2716
|
+
if (!isCommandMissingError(error)) {
|
|
2717
|
+
throw error;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
await execFileAsync("osascript", [
|
|
2721
|
+
"-e",
|
|
2722
|
+
`display notification "${escapeAppleScriptText(notification.body)}" with title "${escapeAppleScriptText(notification.title)}"`
|
|
2723
|
+
]);
|
|
2724
|
+
}
|
|
2725
|
+
function escapeAppleScriptText(value) {
|
|
2726
|
+
return value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"");
|
|
2727
|
+
}
|
|
2728
|
+
function resolveNotificationIconPath() {
|
|
2729
|
+
const candidatePaths = [
|
|
2730
|
+
fileURLToPath(new URL("../../assets/idle-time-notification-icon.png", import.meta.url)),
|
|
2731
|
+
fileURLToPath(new URL("../assets/idle-time-notification-icon.png", import.meta.url))
|
|
2732
|
+
];
|
|
2733
|
+
return candidatePaths.find((candidatePath) => existsSync(candidatePath)) ?? null;
|
|
2734
|
+
}
|
|
2735
|
+
function isCommandMissingError(error) {
|
|
2736
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
// src/best-metrics/near-best-notifications.ts
|
|
2740
|
+
import { mkdir, readFile as readFile3, rename, writeFile } from "node:fs/promises";
|
|
2741
|
+
import { homedir as homedir3 } from "node:os";
|
|
2742
|
+
import { join as join3 } from "node:path";
|
|
2743
|
+
var nearBestNotificationStateFileName = "near-best-notifications-v1.json";
|
|
2744
|
+
var nearBestNotificationVersion = 1;
|
|
2745
|
+
async function notifyNearBestMetrics(currentMetrics, ledger, options = {}) {
|
|
2746
|
+
const now = options.now ?? new Date;
|
|
2747
|
+
const state = await ensureNearBestNotificationState(options);
|
|
2748
|
+
if (!state.nearBestEnabled) {
|
|
2749
|
+
return [];
|
|
2750
|
+
}
|
|
2751
|
+
const metricsToNotify = buildNearBestMetricKeys(currentMetrics, ledger, state, now);
|
|
2752
|
+
if (metricsToNotify.length === 0) {
|
|
2753
|
+
return [];
|
|
2754
|
+
}
|
|
2755
|
+
const nextState = {
|
|
2756
|
+
...state,
|
|
2757
|
+
lastNotifiedAt: {
|
|
2758
|
+
...state.lastNotifiedAt,
|
|
2759
|
+
...Object.fromEntries(metricsToNotify.map((metric) => [metric, now]))
|
|
2760
|
+
}
|
|
2761
|
+
};
|
|
2762
|
+
await writeNearBestNotificationState(nextState, options);
|
|
2763
|
+
await deliverLocalNotifications(metricsToNotify.map((metric) => buildNearBestNotification(metric, currentMetrics[metric], ledger[metric]?.value ?? 0)), options);
|
|
2764
|
+
return metricsToNotify;
|
|
2765
|
+
}
|
|
2766
|
+
async function ensureNearBestNotificationState(options) {
|
|
2767
|
+
const existingState = await readNearBestNotificationState(options);
|
|
2768
|
+
if (existingState) {
|
|
2769
|
+
return existingState;
|
|
2770
|
+
}
|
|
2771
|
+
const defaultState = createDefaultNearBestNotificationState();
|
|
2772
|
+
await writeNearBestNotificationState(defaultState, options);
|
|
2773
|
+
return defaultState;
|
|
2774
|
+
}
|
|
2775
|
+
async function readNearBestNotificationState(options) {
|
|
2776
|
+
try {
|
|
2777
|
+
const rawStateText = await readFile3(resolveNearBestNotificationStatePath(options), "utf8");
|
|
2778
|
+
return parseNearBestNotificationState(JSON.parse(rawStateText));
|
|
2779
|
+
} catch (error) {
|
|
2780
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
2781
|
+
return null;
|
|
2782
|
+
}
|
|
2783
|
+
throw error;
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
function parseNearBestNotificationState(value) {
|
|
2787
|
+
const stateRecord = expectObject(value, "nearBestNotificationState");
|
|
2788
|
+
const version = readNumber(stateRecord, "version", "nearBestNotificationState");
|
|
2789
|
+
if (version !== nearBestNotificationVersion) {
|
|
2790
|
+
throw new Error(`nearBestNotificationState.version must be ${nearBestNotificationVersion}.`);
|
|
2791
|
+
}
|
|
2792
|
+
const lastNotifiedAtRecord = expectObject(stateRecord.lastNotifiedAt, "nearBestNotificationState.lastNotifiedAt");
|
|
2793
|
+
return {
|
|
2794
|
+
version,
|
|
2795
|
+
nearBestEnabled: Boolean(stateRecord.nearBestEnabled),
|
|
2796
|
+
thresholdRatio: readNumber(stateRecord, "thresholdRatio", "nearBestNotificationState"),
|
|
2797
|
+
cooldownMs: readNumber(stateRecord, "cooldownMs", "nearBestNotificationState"),
|
|
2798
|
+
lastNotifiedAt: {
|
|
2799
|
+
bestConcurrentAgents: readOptionalIsoTimestamp(lastNotifiedAtRecord.bestConcurrentAgents, "nearBestNotificationState.lastNotifiedAt.bestConcurrentAgents"),
|
|
2800
|
+
best24hRawBurn: readOptionalIsoTimestamp(lastNotifiedAtRecord.best24hRawBurn, "nearBestNotificationState.lastNotifiedAt.best24hRawBurn"),
|
|
2801
|
+
best24hAgentSumMs: readOptionalIsoTimestamp(lastNotifiedAtRecord.best24hAgentSumMs, "nearBestNotificationState.lastNotifiedAt.best24hAgentSumMs")
|
|
2802
|
+
}
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
async function writeNearBestNotificationState(state, options) {
|
|
2806
|
+
const statePath = resolveNearBestNotificationStatePath(options);
|
|
2807
|
+
const stateDirectory = options.stateDirectory ?? join3(homedir3(), ".idletime");
|
|
2808
|
+
await mkdir(stateDirectory, { recursive: true });
|
|
2809
|
+
const temporaryPath = join3(stateDirectory, `.near-best-notifications.${process.pid}.${Date.now()}.tmp`);
|
|
2810
|
+
await writeFile(temporaryPath, `${JSON.stringify({
|
|
2811
|
+
version: state.version,
|
|
2812
|
+
nearBestEnabled: state.nearBestEnabled,
|
|
2813
|
+
thresholdRatio: state.thresholdRatio,
|
|
2814
|
+
cooldownMs: state.cooldownMs,
|
|
2815
|
+
lastNotifiedAt: {
|
|
2816
|
+
bestConcurrentAgents: state.lastNotifiedAt.bestConcurrentAgents?.toISOString() ?? null,
|
|
2817
|
+
best24hRawBurn: state.lastNotifiedAt.best24hRawBurn?.toISOString() ?? null,
|
|
2818
|
+
best24hAgentSumMs: state.lastNotifiedAt.best24hAgentSumMs?.toISOString() ?? null
|
|
2819
|
+
}
|
|
2820
|
+
}, null, 2)}
|
|
2821
|
+
`, "utf8");
|
|
2822
|
+
await rename(temporaryPath, statePath);
|
|
2823
|
+
}
|
|
2824
|
+
function buildNearBestMetricKeys(currentMetrics, ledger, state, now) {
|
|
2825
|
+
return [
|
|
2826
|
+
"bestConcurrentAgents",
|
|
2827
|
+
"best24hRawBurn",
|
|
2828
|
+
"best24hAgentSumMs"
|
|
2829
|
+
].filter((metric) => {
|
|
2830
|
+
const bestValue = ledger[metric]?.value ?? 0;
|
|
2831
|
+
if (bestValue <= 0) {
|
|
2832
|
+
return false;
|
|
2833
|
+
}
|
|
2834
|
+
const currentValue = currentMetrics[metric];
|
|
2835
|
+
if (currentValue <= 0 || currentValue >= bestValue) {
|
|
2836
|
+
return false;
|
|
2837
|
+
}
|
|
2838
|
+
if (currentValue / bestValue < state.thresholdRatio) {
|
|
2839
|
+
return false;
|
|
2840
|
+
}
|
|
2841
|
+
const lastNotifiedAt = state.lastNotifiedAt[metric];
|
|
2842
|
+
return lastNotifiedAt === null || now.getTime() - lastNotifiedAt.getTime() >= state.cooldownMs;
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
function buildNearBestNotification(metric, currentValue, bestValue) {
|
|
2846
|
+
return {
|
|
2847
|
+
title: metric === "bestConcurrentAgents" ? "Close to best concurrent agents" : metric === "best24hRawBurn" ? "Close to best 24hr raw burn" : "Close to best agent sum",
|
|
2848
|
+
body: metric === "bestConcurrentAgents" ? `${formatInteger2(currentValue)} of ${formatInteger2(bestValue)} concurrent agents` : metric === "best24hRawBurn" ? `${formatCompactInteger2(currentValue)} of ${formatCompactInteger2(bestValue)} 24hr raw burn` : `${formatAgentSumHours2(currentValue)} of ${formatAgentSumHours2(bestValue)} agent sum`
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
function createDefaultNearBestNotificationState() {
|
|
2852
|
+
return {
|
|
2853
|
+
version: nearBestNotificationVersion,
|
|
2854
|
+
nearBestEnabled: false,
|
|
2855
|
+
thresholdRatio: 0.97,
|
|
2856
|
+
cooldownMs: 24 * 60 * 60 * 1000,
|
|
2857
|
+
lastNotifiedAt: {
|
|
2858
|
+
bestConcurrentAgents: null,
|
|
2859
|
+
best24hRawBurn: null,
|
|
2860
|
+
best24hAgentSumMs: null
|
|
2861
|
+
}
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
function resolveNearBestNotificationStatePath(options) {
|
|
2865
|
+
return join3(options.stateDirectory ?? join3(homedir3(), ".idletime"), nearBestNotificationStateFileName);
|
|
2866
|
+
}
|
|
2867
|
+
function readOptionalIsoTimestamp(value, label) {
|
|
2868
|
+
if (value === null || value === undefined) {
|
|
2869
|
+
return null;
|
|
2870
|
+
}
|
|
2871
|
+
return readIsoTimestamp(value, label);
|
|
2872
|
+
}
|
|
2873
|
+
function formatAgentSumHours2(durationMs) {
|
|
2874
|
+
const hours = durationMs / 3600000;
|
|
2875
|
+
return hours >= 10 ? Math.round(hours).toString() : (Math.round(hours * 10) / 10).toString();
|
|
2876
|
+
}
|
|
2877
|
+
function formatCompactInteger2(value) {
|
|
2878
|
+
return new Intl.NumberFormat("en-US", {
|
|
2879
|
+
notation: "compact",
|
|
2880
|
+
maximumFractionDigits: 1
|
|
2881
|
+
}).format(Math.round(value)).toUpperCase();
|
|
2882
|
+
}
|
|
2883
|
+
function formatInteger2(value) {
|
|
2884
|
+
return new Intl.NumberFormat("en-US").format(Math.round(value));
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// src/best-metrics/notify-best-events.ts
|
|
2888
|
+
async function notifyBestEvents(bestEvents, options = {}) {
|
|
2889
|
+
await deliverLocalNotifications(bestEvents.map((bestEvent) => buildBestEventNotification(bestEvent)), options);
|
|
2890
|
+
}
|
|
2891
|
+
function buildBestEventNotification(bestEvent) {
|
|
2892
|
+
return {
|
|
2893
|
+
title: resolveNotificationTitle(bestEvent.metric),
|
|
2894
|
+
body: resolveNotificationBody(bestEvent)
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
function resolveNotificationTitle(bestMetricKey) {
|
|
2898
|
+
return bestMetricKey === "bestConcurrentAgents" ? "New best concurrent agents" : bestMetricKey === "best24hRawBurn" ? "New best 24hr raw burn" : "New best agent sum";
|
|
2899
|
+
}
|
|
2900
|
+
function resolveNotificationBody(bestEvent) {
|
|
2901
|
+
return bestEvent.metric === "bestConcurrentAgents" ? `${formatInteger3(bestEvent.value)} concurrent agents` : bestEvent.metric === "best24hRawBurn" ? `${formatCompactInteger3(bestEvent.value)} 24hr raw burn` : `${formatAgentSumHours3(bestEvent.value)} agent sum`;
|
|
2902
|
+
}
|
|
2903
|
+
function formatAgentSumHours3(durationMs) {
|
|
2904
|
+
const hours = durationMs / 3600000;
|
|
2905
|
+
return hours >= 10 ? Math.round(hours).toString() : (Math.round(hours * 10) / 10).toString();
|
|
2906
|
+
}
|
|
2907
|
+
function formatCompactInteger3(value) {
|
|
2908
|
+
return new Intl.NumberFormat("en-US", {
|
|
2909
|
+
notation: "compact",
|
|
2910
|
+
maximumFractionDigits: 1
|
|
2911
|
+
}).format(Math.round(value)).toUpperCase();
|
|
2912
|
+
}
|
|
2913
|
+
function formatInteger3(value) {
|
|
2914
|
+
return new Intl.NumberFormat("en-US").format(Math.round(value));
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
// src/best-metrics/build-current-best-metrics.ts
|
|
2918
|
+
function buildCurrentBestMetricValues(sessions, options = {}) {
|
|
2919
|
+
const idleCutoffMs = options.idleCutoffMs ?? defaultBestMetricsIdleCutoffMs;
|
|
2920
|
+
const now = options.now ?? new Date;
|
|
2921
|
+
const activityMetrics = buildActivityMetrics(sessions, idleCutoffMs, now);
|
|
2922
|
+
const currentWindow = {
|
|
2923
|
+
start: new Date(now.getTime() - rollingWindowDurationMs),
|
|
2924
|
+
end: now
|
|
2925
|
+
};
|
|
2926
|
+
return {
|
|
2927
|
+
bestConcurrentAgents: countLiveSubagents(activityMetrics.perAgentTaskBlocks, now),
|
|
2928
|
+
best24hRawBurn: sessions.reduce((rawBurnTotal, session) => rawBurnTotal + buildTokenDeltaPoints(session.tokenPoints).filter((tokenDeltaPoint) => tokenDeltaPoint.timestamp.getTime() >= currentWindow.start.getTime() && tokenDeltaPoint.timestamp.getTime() <= currentWindow.end.getTime()).reduce((sessionTotal, tokenDeltaPoint) => sessionTotal + tokenDeltaPoint.deltaUsage.totalTokens, 0), 0),
|
|
2929
|
+
best24hAgentSumMs: measureOverlapMs(activityMetrics.perAgentTaskBlocks.flatMap((taskBlocks) => taskBlocks), currentWindow)
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
function countLiveSubagents(intervalGroups, now) {
|
|
2933
|
+
return intervalGroups.reduce((liveCount, intervalGroup) => liveCount + Number(intervalGroup.some((interval) => interval.start.getTime() <= now.getTime() && interval.end.getTime() > now.getTime())), 0);
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
// src/best-metrics/append-best-events.ts
|
|
2937
|
+
import { appendFile, mkdir as mkdir2 } from "node:fs/promises";
|
|
2938
|
+
import { homedir as homedir4 } from "node:os";
|
|
2939
|
+
import { join as join4 } from "node:path";
|
|
2940
|
+
var bestEventsFileName = "best-events.ndjson";
|
|
2941
|
+
async function appendBestEvents(bestEvents, options = {}) {
|
|
2942
|
+
if (bestEvents.length === 0) {
|
|
2943
|
+
return;
|
|
2944
|
+
}
|
|
2945
|
+
const stateDirectory = resolveBestStateDirectory2(options);
|
|
2946
|
+
await mkdir2(stateDirectory, { recursive: true });
|
|
2947
|
+
await appendFile(join4(stateDirectory, bestEventsFileName), `${bestEvents.map(serializeBestEvent).join(`
|
|
2948
|
+
`)}
|
|
2949
|
+
`, "utf8");
|
|
2950
|
+
}
|
|
2951
|
+
function serializeBestEvent(bestEvent) {
|
|
2952
|
+
return JSON.stringify({
|
|
2953
|
+
metric: bestEvent.metric,
|
|
2954
|
+
previousValue: bestEvent.previousValue,
|
|
2955
|
+
value: bestEvent.value,
|
|
2956
|
+
observedAt: bestEvent.observedAt.toISOString(),
|
|
2957
|
+
windowStart: bestEvent.windowStart.toISOString(),
|
|
2958
|
+
windowEnd: bestEvent.windowEnd.toISOString(),
|
|
2959
|
+
version: bestEvent.version
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
function resolveBestStateDirectory2(options) {
|
|
2963
|
+
return options.stateDirectory ?? join4(homedir4(), ".idletime");
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
// src/best-metrics/build-rolling-24h-windows.ts
|
|
2967
|
+
function findBestRollingWindowTotal(weightedPoints) {
|
|
2968
|
+
const sortedPoints = weightedPoints.filter((point) => point.value > 0).slice().sort((leftPoint, rightPoint) => leftPoint.timestamp.getTime() - rightPoint.timestamp.getTime());
|
|
2969
|
+
if (sortedPoints.length === 0) {
|
|
2970
|
+
return null;
|
|
2971
|
+
}
|
|
2972
|
+
let bestValue = 0;
|
|
2973
|
+
let bestTimestampMs = 0;
|
|
2974
|
+
let currentTotal = 0;
|
|
2975
|
+
let leftIndex = 0;
|
|
2976
|
+
for (let rightIndex = 0;rightIndex < sortedPoints.length; rightIndex += 1) {
|
|
2977
|
+
const rightPoint = sortedPoints[rightIndex];
|
|
2978
|
+
currentTotal += rightPoint.value;
|
|
2979
|
+
while (rightPoint.timestamp.getTime() - sortedPoints[leftIndex].timestamp.getTime() > rollingWindowDurationMs) {
|
|
2980
|
+
currentTotal -= sortedPoints[leftIndex].value;
|
|
2981
|
+
leftIndex += 1;
|
|
2982
|
+
}
|
|
2983
|
+
if (currentTotal > bestValue) {
|
|
2984
|
+
bestValue = currentTotal;
|
|
2985
|
+
bestTimestampMs = rightPoint.timestamp.getTime();
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
if (bestValue === 0) {
|
|
2989
|
+
return null;
|
|
2990
|
+
}
|
|
2991
|
+
return createRollingRecord(bestValue, bestTimestampMs);
|
|
2992
|
+
}
|
|
2993
|
+
function findBestRollingWindowOverlap(intervals) {
|
|
2994
|
+
const slopeChanges = intervals.flatMap(buildSlopeChanges);
|
|
2995
|
+
if (slopeChanges.length === 0) {
|
|
2996
|
+
return null;
|
|
2997
|
+
}
|
|
2998
|
+
slopeChanges.sort((leftChange, rightChange) => leftChange.timestampMs - rightChange.timestampMs);
|
|
2999
|
+
let bestTimestampMs = 0;
|
|
3000
|
+
let bestValue = 0;
|
|
3001
|
+
let currentSlope = 0;
|
|
3002
|
+
let currentValue = 0;
|
|
3003
|
+
let previousTimestampMs = slopeChanges[0].timestampMs;
|
|
3004
|
+
let index = 0;
|
|
3005
|
+
while (index < slopeChanges.length) {
|
|
3006
|
+
const timestampMs = slopeChanges[index].timestampMs;
|
|
3007
|
+
currentValue += currentSlope * (timestampMs - previousTimestampMs);
|
|
3008
|
+
if (currentValue > bestValue) {
|
|
3009
|
+
bestValue = currentValue;
|
|
3010
|
+
bestTimestampMs = timestampMs;
|
|
3011
|
+
}
|
|
3012
|
+
while (index < slopeChanges.length && slopeChanges[index].timestampMs === timestampMs) {
|
|
3013
|
+
currentSlope += slopeChanges[index].deltaSlope;
|
|
3014
|
+
index += 1;
|
|
3015
|
+
}
|
|
3016
|
+
previousTimestampMs = timestampMs;
|
|
3017
|
+
}
|
|
3018
|
+
if (bestValue === 0) {
|
|
3019
|
+
return null;
|
|
3020
|
+
}
|
|
3021
|
+
return createRollingRecord(bestValue, bestTimestampMs);
|
|
3022
|
+
}
|
|
3023
|
+
function buildSlopeChanges(interval) {
|
|
3024
|
+
const startMs = interval.start.getTime();
|
|
3025
|
+
const endMs = interval.end.getTime();
|
|
3026
|
+
if (endMs <= startMs) {
|
|
3027
|
+
return [];
|
|
3028
|
+
}
|
|
3029
|
+
return [
|
|
3030
|
+
{ timestampMs: startMs, deltaSlope: 1 },
|
|
3031
|
+
{ timestampMs: endMs, deltaSlope: -1 },
|
|
3032
|
+
{ timestampMs: startMs + rollingWindowDurationMs, deltaSlope: -1 },
|
|
3033
|
+
{ timestampMs: endMs + rollingWindowDurationMs, deltaSlope: 1 }
|
|
3034
|
+
];
|
|
3035
|
+
}
|
|
3036
|
+
function createRollingRecord(value, observedAtMs) {
|
|
3037
|
+
return {
|
|
3038
|
+
value,
|
|
3039
|
+
observedAt: new Date(observedAtMs),
|
|
3040
|
+
windowStart: new Date(observedAtMs - rollingWindowDurationMs),
|
|
3041
|
+
windowEnd: new Date(observedAtMs)
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
// src/best-metrics/build-best-metrics.ts
|
|
3046
|
+
function buildBestMetricCandidates(sessions, options = {}) {
|
|
3047
|
+
const idleCutoffMs = options.idleCutoffMs ?? defaultBestMetricsIdleCutoffMs;
|
|
3048
|
+
const activityMetrics = buildActivityMetrics(sessions, idleCutoffMs);
|
|
3049
|
+
return {
|
|
3050
|
+
bestConcurrentAgents: findBestConcurrentAgents(activityMetrics.perAgentTaskBlocks),
|
|
3051
|
+
best24hRawBurn: findBestRollingWindowTotal(sessions.flatMap((session) => buildTokenDeltaPoints(session.tokenPoints).map((tokenDeltaPoint) => ({
|
|
3052
|
+
timestamp: tokenDeltaPoint.timestamp,
|
|
3053
|
+
value: tokenDeltaPoint.deltaUsage.totalTokens
|
|
3054
|
+
})))),
|
|
3055
|
+
best24hAgentSumMs: findBestRollingWindowOverlap(activityMetrics.perAgentTaskBlocks.flatMap((taskBlocks) => taskBlocks))
|
|
3056
|
+
};
|
|
3057
|
+
}
|
|
3058
|
+
function findBestConcurrentAgents(intervalGroups) {
|
|
3059
|
+
const concurrencyEdges = intervalGroups.flatMap((intervalGroup) => intervalGroup.flatMap((interval) => [
|
|
3060
|
+
{ timestampMs: interval.start.getTime(), delta: 1 },
|
|
3061
|
+
{ timestampMs: interval.end.getTime(), delta: -1 }
|
|
3062
|
+
]));
|
|
3063
|
+
if (concurrencyEdges.length === 0) {
|
|
3064
|
+
return null;
|
|
3065
|
+
}
|
|
3066
|
+
concurrencyEdges.sort((leftEdge, rightEdge) => leftEdge.timestampMs - rightEdge.timestampMs);
|
|
3067
|
+
let activeCount = 0;
|
|
3068
|
+
let bestRecord = null;
|
|
3069
|
+
let index = 0;
|
|
3070
|
+
while (index < concurrencyEdges.length) {
|
|
3071
|
+
const timestampMs = concurrencyEdges[index].timestampMs;
|
|
3072
|
+
while (index < concurrencyEdges.length && concurrencyEdges[index].timestampMs === timestampMs) {
|
|
3073
|
+
activeCount += concurrencyEdges[index].delta;
|
|
3074
|
+
index += 1;
|
|
3075
|
+
}
|
|
3076
|
+
const nextTimestampMs = concurrencyEdges[index]?.timestampMs ?? timestampMs;
|
|
3077
|
+
if (nextTimestampMs <= timestampMs || activeCount <= 0) {
|
|
3078
|
+
continue;
|
|
3079
|
+
}
|
|
3080
|
+
if (!bestRecord || activeCount > bestRecord.value) {
|
|
3081
|
+
bestRecord = {
|
|
3082
|
+
value: activeCount,
|
|
3083
|
+
observedAt: new Date(timestampMs),
|
|
3084
|
+
windowStart: new Date(timestampMs),
|
|
3085
|
+
windowEnd: new Date(nextTimestampMs)
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
return bestRecord;
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
// src/best-metrics/read-all-codex-sessions.ts
|
|
3093
|
+
import { readdir as readdir2 } from "node:fs/promises";
|
|
3094
|
+
import { homedir as homedir5 } from "node:os";
|
|
3095
|
+
import { join as join5 } from "node:path";
|
|
3096
|
+
var defaultSessionRootDirectory2 = join5(homedir5(), ".codex", "sessions");
|
|
3097
|
+
async function readAllCodexSessions(options = {}) {
|
|
3098
|
+
const sessionRootDirectory = options.sessionRootDirectory ?? defaultSessionRootDirectory2;
|
|
3099
|
+
const sessionFiles = await listAllSessionFiles(sessionRootDirectory);
|
|
3100
|
+
const parsedSessionResults = await Promise.allSettled(sessionFiles.map((sessionFilePath) => parseCodexSession(sessionFilePath)));
|
|
3101
|
+
const parsedSessions = parsedSessionResults.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
|
|
3102
|
+
return parsedSessions.sort((leftSession, rightSession) => leftSession.firstTimestamp.getTime() - rightSession.firstTimestamp.getTime());
|
|
3103
|
+
}
|
|
3104
|
+
async function listAllSessionFiles(rootDirectory) {
|
|
3105
|
+
const pendingDirectories = [rootDirectory];
|
|
3106
|
+
const sessionFiles = [];
|
|
3107
|
+
while (pendingDirectories.length > 0) {
|
|
3108
|
+
const currentDirectory = pendingDirectories.pop();
|
|
3109
|
+
const directoryEntries = await readDirectoryEntries2(currentDirectory);
|
|
3110
|
+
for (const directoryEntry of directoryEntries) {
|
|
3111
|
+
const entryPath = join5(currentDirectory, directoryEntry.name);
|
|
3112
|
+
if (directoryEntry.isDirectory()) {
|
|
3113
|
+
pendingDirectories.push(entryPath);
|
|
3114
|
+
continue;
|
|
3115
|
+
}
|
|
3116
|
+
if (directoryEntry.isFile() && directoryEntry.name.endsWith(".jsonl")) {
|
|
3117
|
+
sessionFiles.push(entryPath);
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
return sessionFiles.sort();
|
|
3122
|
+
}
|
|
3123
|
+
async function readDirectoryEntries2(directoryPath) {
|
|
3124
|
+
try {
|
|
3125
|
+
return await readdir2(directoryPath, { withFileTypes: true });
|
|
3126
|
+
} catch (error) {
|
|
3127
|
+
if (isMissingDirectoryError2(error)) {
|
|
3128
|
+
return [];
|
|
3129
|
+
}
|
|
3130
|
+
throw error;
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
function isMissingDirectoryError2(error) {
|
|
3134
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
// src/best-metrics/write-best-ledger.ts
|
|
3138
|
+
import { mkdir as mkdir3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
3139
|
+
import { join as join6 } from "node:path";
|
|
3140
|
+
async function writeBestLedger(ledger, options = {}) {
|
|
3141
|
+
const ledgerPath = resolveBestLedgerPath(options);
|
|
3142
|
+
const stateDirectory = options.stateDirectory ?? ledgerPath.slice(0, ledgerPath.lastIndexOf("/"));
|
|
3143
|
+
await mkdir3(stateDirectory, { recursive: true });
|
|
3144
|
+
const temporaryPath = join6(stateDirectory, `.bests-v1.${process.pid}.${Date.now()}.tmp`);
|
|
3145
|
+
await writeFile2(temporaryPath, serializeBestLedger(ledger), "utf8");
|
|
3146
|
+
await rename2(temporaryPath, ledgerPath);
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
// src/best-metrics/refresh-best-metrics.ts
|
|
3150
|
+
async function refreshBestMetrics(options = {}) {
|
|
3151
|
+
const refreshedAt = options.now ?? new Date;
|
|
3152
|
+
const existingLedger = await readBestLedger(options);
|
|
3153
|
+
const sessions = await readAllCodexSessions({
|
|
3154
|
+
sessionRootDirectory: options.sessionRootDirectory
|
|
3155
|
+
});
|
|
3156
|
+
const bestMetricCandidates = buildBestMetricCandidates(sessions);
|
|
3157
|
+
const currentMetrics = buildCurrentBestMetricValues(sessions, {
|
|
3158
|
+
now: refreshedAt
|
|
3159
|
+
});
|
|
3160
|
+
if (!existingLedger) {
|
|
3161
|
+
const bootstrappedLedger = {
|
|
3162
|
+
version: bestMetricsLedgerVersion,
|
|
3163
|
+
initializedAt: refreshedAt,
|
|
3164
|
+
lastScannedAt: refreshedAt,
|
|
3165
|
+
...bestMetricCandidates
|
|
3166
|
+
};
|
|
3167
|
+
await writeBestLedger(bootstrappedLedger, options);
|
|
3168
|
+
return {
|
|
3169
|
+
currentMetrics,
|
|
3170
|
+
ledger: bootstrappedLedger,
|
|
3171
|
+
newBestEvents: [],
|
|
3172
|
+
refreshMode: "bootstrap"
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
const newBestEvents = buildNewBestEvents(existingLedger, bestMetricCandidates);
|
|
3176
|
+
const refreshedLedger = {
|
|
3177
|
+
...existingLedger,
|
|
3178
|
+
lastScannedAt: refreshedAt,
|
|
3179
|
+
...mergeBestMetricCandidates(existingLedger, bestMetricCandidates)
|
|
3180
|
+
};
|
|
3181
|
+
await writeBestLedger(refreshedLedger, options);
|
|
3182
|
+
await appendBestEvents(newBestEvents, options);
|
|
3183
|
+
return {
|
|
3184
|
+
currentMetrics,
|
|
3185
|
+
ledger: refreshedLedger,
|
|
3186
|
+
newBestEvents,
|
|
3187
|
+
refreshMode: "refresh"
|
|
3188
|
+
};
|
|
3189
|
+
}
|
|
3190
|
+
function mergeBestMetricCandidates(currentLedger, candidateLedger) {
|
|
3191
|
+
return {
|
|
3192
|
+
bestConcurrentAgents: pickBetterRecord(currentLedger.bestConcurrentAgents, candidateLedger.bestConcurrentAgents),
|
|
3193
|
+
best24hRawBurn: pickBetterRecord(currentLedger.best24hRawBurn, candidateLedger.best24hRawBurn),
|
|
3194
|
+
best24hAgentSumMs: pickBetterRecord(currentLedger.best24hAgentSumMs, candidateLedger.best24hAgentSumMs)
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
function pickBetterRecord(currentRecord, candidateRecord) {
|
|
3198
|
+
if (!candidateRecord) {
|
|
3199
|
+
return currentRecord;
|
|
3200
|
+
}
|
|
3201
|
+
if (!currentRecord || candidateRecord.value > currentRecord.value) {
|
|
3202
|
+
return candidateRecord;
|
|
3203
|
+
}
|
|
3204
|
+
return currentRecord;
|
|
3205
|
+
}
|
|
3206
|
+
function buildNewBestEvents(currentLedger, candidateLedger) {
|
|
3207
|
+
return [
|
|
3208
|
+
buildNewBestEvent("bestConcurrentAgents", currentLedger.bestConcurrentAgents, candidateLedger.bestConcurrentAgents),
|
|
3209
|
+
buildNewBestEvent("best24hRawBurn", currentLedger.best24hRawBurn, candidateLedger.best24hRawBurn),
|
|
3210
|
+
buildNewBestEvent("best24hAgentSumMs", currentLedger.best24hAgentSumMs, candidateLedger.best24hAgentSumMs)
|
|
3211
|
+
].flatMap((bestEvent) => bestEvent ? [bestEvent] : []);
|
|
3212
|
+
}
|
|
3213
|
+
function buildNewBestEvent(metric, currentRecord, candidateRecord) {
|
|
3214
|
+
if (!candidateRecord || currentRecord !== null && candidateRecord.value <= currentRecord.value) {
|
|
3215
|
+
return null;
|
|
3216
|
+
}
|
|
3217
|
+
return {
|
|
3218
|
+
metric,
|
|
3219
|
+
previousValue: currentRecord?.value ?? null,
|
|
3220
|
+
value: candidateRecord.value,
|
|
3221
|
+
observedAt: candidateRecord.observedAt,
|
|
3222
|
+
windowStart: candidateRecord.windowStart,
|
|
3223
|
+
windowEnd: candidateRecord.windowEnd,
|
|
3224
|
+
version: bestMetricsLedgerVersion
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
// src/cli/run-refresh-bests-command.ts
|
|
3229
|
+
async function runRefreshBestsCommand(options = {}) {
|
|
3230
|
+
const refreshedBestMetrics = await refreshBestMetrics(options);
|
|
3231
|
+
await notifyBestEvents(refreshedBestMetrics.newBestEvents, options);
|
|
3232
|
+
await notifyNearBestMetrics(refreshedBestMetrics.currentMetrics, refreshedBestMetrics.ledger, options);
|
|
3233
|
+
return [
|
|
3234
|
+
"BEST metrics refreshed",
|
|
3235
|
+
`mode: ${refreshedBestMetrics.refreshMode}`,
|
|
3236
|
+
`new bests: ${refreshedBestMetrics.newBestEvents.length}`,
|
|
3237
|
+
`last scanned: ${refreshedBestMetrics.ledger.lastScannedAt.toISOString()}`
|
|
3238
|
+
].join(`
|
|
3239
|
+
`);
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
// src/cli/run-today-command.ts
|
|
3243
|
+
async function buildTodayCommandResult(command, options = {}) {
|
|
3244
|
+
const window = resolveTodayReportWindow({ now: options.now });
|
|
3245
|
+
const bestLedgerPromise = readBestLedger({ stateDirectory: options.stateDirectory });
|
|
3246
|
+
const sessionsPromise = readCodexSessions({
|
|
3247
|
+
windowStart: window.start,
|
|
3248
|
+
windowEnd: window.end,
|
|
3249
|
+
sessionRootDirectory: options.sessionRootDirectory
|
|
3250
|
+
});
|
|
3251
|
+
const [bestLedger, sessions] = await Promise.all([
|
|
3252
|
+
bestLedgerPromise,
|
|
3253
|
+
sessionsPromise
|
|
3254
|
+
]);
|
|
3255
|
+
return {
|
|
3256
|
+
bestLedger,
|
|
3257
|
+
summaryReport: buildSummaryReport(sessions, {
|
|
3258
|
+
filters: command.filters,
|
|
3259
|
+
groupBy: command.groupBy,
|
|
3260
|
+
idleCutoffMs: command.idleCutoffMs,
|
|
3261
|
+
wakeWindow: command.wakeWindow,
|
|
3262
|
+
window
|
|
3263
|
+
})
|
|
3264
|
+
};
|
|
3265
|
+
}
|
|
3266
|
+
async function runTodayCommand(command, options = {}) {
|
|
3267
|
+
const commandResult = await buildTodayCommandResult(command, options);
|
|
3268
|
+
return renderSummaryReport(commandResult.summaryReport, createRenderOptions(command.shareMode), undefined, commandResult.bestLedger ? buildBestPlaque(commandResult.bestLedger) : null);
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
// src/cli/run-idletime.ts
|
|
3272
|
+
async function runIdletimeCli(argv) {
|
|
3273
|
+
const command = parseIdletimeCommand(argv);
|
|
3274
|
+
if (command.helpRequested) {
|
|
3275
|
+
console.log(renderHelpText());
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
if (command.versionRequested) {
|
|
3279
|
+
console.log(package_default.version);
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
if (command.commandName === "refresh-bests") {
|
|
3283
|
+
console.log(await runRefreshBestsCommand());
|
|
3284
|
+
return;
|
|
3285
|
+
}
|
|
3286
|
+
if (command.outputFormat === "json") {
|
|
3287
|
+
console.log(await buildJsonOutput(command));
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
if (command.commandName === "live") {
|
|
3291
|
+
await runLiveCommand(command);
|
|
3292
|
+
return;
|
|
3293
|
+
}
|
|
3294
|
+
const output = command.commandName === "hourly" ? await runHourlyCommand(command) : command.commandName === "today" ? await runTodayCommand(command) : await runLast24hCommand(command);
|
|
1627
3295
|
console.log(output);
|
|
1628
3296
|
}
|
|
3297
|
+
async function buildJsonOutput(command) {
|
|
3298
|
+
const generatedAt = new Date;
|
|
3299
|
+
if (command.commandName === "live") {
|
|
3300
|
+
const liveSnapshot = await takeLiveSnapshot(command, {
|
|
3301
|
+
observedAt: generatedAt
|
|
3302
|
+
});
|
|
3303
|
+
return serializeLiveSnapshot({
|
|
3304
|
+
command: buildJsonLiveSnapshotCommand(command),
|
|
3305
|
+
generatedAt,
|
|
3306
|
+
liveReport: liveSnapshot
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
if (command.commandName === "hourly") {
|
|
3310
|
+
const commandResult2 = await buildHourlyCommandResult(command, {
|
|
3311
|
+
now: generatedAt
|
|
3312
|
+
});
|
|
3313
|
+
return serializeHourlySnapshot({
|
|
3314
|
+
command: buildJsonHourlySnapshotCommand(command),
|
|
3315
|
+
generatedAt,
|
|
3316
|
+
hourlyReport: commandResult2.hourlyReport
|
|
3317
|
+
});
|
|
3318
|
+
}
|
|
3319
|
+
if (command.commandName === "today") {
|
|
3320
|
+
const commandResult2 = await buildTodayCommandResult(command, {
|
|
3321
|
+
now: generatedAt
|
|
3322
|
+
});
|
|
3323
|
+
return serializeSummarySnapshot({
|
|
3324
|
+
command: buildJsonSummarySnapshotCommand(command),
|
|
3325
|
+
generatedAt,
|
|
3326
|
+
hourlyReport: null,
|
|
3327
|
+
mode: "today",
|
|
3328
|
+
summaryReport: commandResult2.summaryReport
|
|
3329
|
+
});
|
|
3330
|
+
}
|
|
3331
|
+
const commandResult = await buildLast24hCommandResult(command, {
|
|
3332
|
+
now: generatedAt
|
|
3333
|
+
});
|
|
3334
|
+
return serializeSummarySnapshot({
|
|
3335
|
+
command: buildJsonSummarySnapshotCommand(command),
|
|
3336
|
+
generatedAt,
|
|
3337
|
+
hourlyReport: commandResult.hourlyReport,
|
|
3338
|
+
mode: "last24h",
|
|
3339
|
+
summaryReport: commandResult.summaryReport
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
function buildJsonSummarySnapshotCommand(command) {
|
|
3343
|
+
return {
|
|
3344
|
+
idleCutoffMs: command.idleCutoffMs,
|
|
3345
|
+
filters: { ...command.filters },
|
|
3346
|
+
groupBy: [...command.groupBy],
|
|
3347
|
+
wakeWindow: command.wakeWindow ? { ...command.wakeWindow } : null
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
function buildJsonHourlySnapshotCommand(command) {
|
|
3351
|
+
return {
|
|
3352
|
+
idleCutoffMs: command.idleCutoffMs,
|
|
3353
|
+
filters: { ...command.filters },
|
|
3354
|
+
wakeWindow: command.wakeWindow ? { ...command.wakeWindow } : null
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
function buildJsonLiveSnapshotCommand(command) {
|
|
3358
|
+
return {
|
|
3359
|
+
filters: { ...command.filters }
|
|
3360
|
+
};
|
|
3361
|
+
}
|
|
1629
3362
|
|
|
1630
3363
|
// src/cli/idletime-bin.ts
|
|
1631
3364
|
await runIdletimeCli(process.argv.slice(2));
|