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/dist/idletime.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "idletime",
5
- version: "0.1.2",
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: readString(sessionMetaPayload, "id", "session_meta.payload"),
1134
+ sessionId,
714
1135
  sourceFilePath,
715
- cwd: readString(sessionMetaPayload, "cwd", "session_meta.payload"),
716
- kind: classifySessionKind(sessionMetaPayload.source),
717
- forkedFromSessionId: readOptionalString(sessionMetaPayload, "forked_from_id"),
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 perSubagentBlocks = subagentSessions.map((session) => buildActivityBlocks(session.eventTimestamps, idleCutoffMs));
824
- const agentCoverageBlocks = buildActivityBlocks(subagentSessions.flatMap((session) => session.eventTimestamps), idleCutoffMs);
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
- perSubagentBlocks,
1261
+ perAgentTaskBlocks,
832
1262
  strictEngagementMs: sumTimeIntervalsMs(strictEngagementBlocks),
833
1263
  directActivityMs: sumTimeIntervalsMs(directActivityBlocks),
834
1264
  agentCoverageMs: sumTimeIntervalsMs(agentCoverageBlocks),
835
1265
  agentOnlyMs: sumTimeIntervalsMs(agentOnlyBlocks),
836
- cumulativeAgentMs: perSubagentBlocks.reduce((totalDurationMs, sessionBlocks) => totalDurationMs + sumTimeIntervalsMs(sessionBlocks), 0),
837
- peakConcurrentAgents: peakConcurrency(perSubagentBlocks)
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.perSubagentBlocks.map((sessionBlocks) => clipIntervals(sessionBlocks, bucketInterval))),
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;236;239;148",
1067
- active: "1;38;2;208;219;96",
1068
- agent: "1;38;2;166;182;77",
1069
- idle: "1;38;2;138;150;66",
1070
- burn: "1;38;2;228;209;92",
1071
- raw: "1;38;2;188;172;80",
1072
- frame: "1;38;2;149;158;56",
1073
- heading: "1;38;2;249;246;212",
1074
- muted: "38;2;142;145;96",
1075
- value: "1;38;2;242;236;179"
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 wordmarkStyle = `${baseBackgroundStyle};1;38;2;247;245;204`;
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: 48, green: 58, blue: 18 },
1124
- { red: 86, green: 96, blue: 24 },
1125
- { red: 128, green: 138, blue: 30 },
1126
- { red: 176, green: 188, blue: 40 },
1127
- { red: 220, green: 228, blue: 78 }
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 currentSegmentWidth = 0;
1640
+ let currentSegment = "";
1150
1641
  for (let columnIndex = 0;columnIndex < width; columnIndex += 1) {
1151
- const style = getPatternCellStyle(getPatternIntensity(width, rowIndex, columnIndex));
1642
+ const overlayCharacter = overlayCharacters[columnIndex];
1643
+ const style = overlayCharacter === null ? cellStyles[columnIndex] : `${cellStyles[columnIndex]};${plaqueTextStyle}`;
1152
1644
  if (style === currentStyle) {
1153
- currentSegmentWidth += 1;
1645
+ currentSegment += overlayCharacter ?? " ";
1154
1646
  continue;
1155
1647
  }
1156
- if (currentSegmentWidth > 0) {
1157
- patternTail += paintAnsi(" ".repeat(currentSegmentWidth), currentStyle, options);
1648
+ if (currentSegment.length > 0) {
1649
+ patternTail += paintAnsi(currentSegment, currentStyle, options);
1158
1650
  }
1159
1651
  currentStyle = style;
1160
- currentSegmentWidth = 1;
1652
+ currentSegment = overlayCharacter ?? " ";
1161
1653
  }
1162
- if (currentSegmentWidth > 0) {
1163
- patternTail += paintAnsi(" ".repeat(currentSegmentWidth), currentStyle, options);
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(` hours ${buildHourMarkerLine(report)}`, "muted", options),
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 runHourlyCommand(command) {
1345
- const window = resolveTrailingReportWindow({ durationMs: command.hourlyWindowMs });
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 renderHourlyReport(buildHourlyReport(sessions, {
1351
- filters: command.filters,
1352
- idleCutoffMs: command.idleCutoffMs,
1353
- wakeWindow: command.wakeWindow,
1354
- window
1355
- }), createRenderOptions(command.shareMode));
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 perSubagentBlocks = metrics.perSubagentBlocks.map((sessionBlocks) => intersectTimeIntervals(sessionBlocks, [windowInterval]));
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
- perSubagentBlocks,
2057
+ perAgentTaskBlocks,
1444
2058
  strictEngagementMs: sumTimeIntervalsMs(strictEngagementBlocks),
1445
2059
  directActivityMs: sumTimeIntervalsMs(directActivityBlocks),
1446
2060
  agentCoverageMs: sumTimeIntervalsMs(agentCoverageBlocks),
1447
2061
  agentOnlyMs: sumTimeIntervalsMs(agentOnlyBlocks),
1448
- cumulativeAgentMs: perSubagentBlocks.reduce((totalDurationMs, sessionBlocks) => totalDurationMs + sumTimeIntervalsMs(sessionBlocks), 0),
1449
- peakConcurrentAgents: peakConcurrency(perSubagentBlocks)
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 runLast24hCommand(command) {
1578
- const window = resolveTrailingReportWindow({ durationMs: command.hourlyWindowMs });
1579
- const sessions = await readCodexSessions({
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 renderSummaryReport(summaryReport, createRenderOptions(command.shareMode), hourlyReport);
2302
+ return {
2303
+ bestLedger,
2304
+ hourlyReport,
2305
+ summaryReport
2306
+ };
1597
2307
  }
1598
-
1599
- // src/cli/run-today-command.ts
1600
- async function runTodayCommand(command) {
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/cli/run-idletime.ts
1616
- async function runIdletimeCli(argv) {
1617
- const command = parseIdletimeCommand(argv);
1618
- if (command.helpRequested) {
1619
- console.log(renderHelpText());
1620
- return;
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
- if (command.versionRequested) {
1623
- console.log(package_default.version);
1624
- return;
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
- const output = command.commandName === "hourly" ? await runHourlyCommand(command) : command.commandName === "today" ? await runTodayCommand(command) : await runLast24hCommand(command);
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));