idletime 0.1.3 → 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.
Files changed (3) hide show
  1. package/README.md +70 -3
  2. package/dist/idletime.js +1820 -864
  3. package/package.json +1 -1
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.3",
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",
@@ -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);
@@ -1256,6 +1694,27 @@ function clamp(value, minValue, maxValue) {
1256
1694
  return Math.min(maxValue, Math.max(minValue, value));
1257
1695
  }
1258
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
+
1259
1718
  // src/reporting/render-layout.ts
1260
1719
  function buildPanel(title, lines) {
1261
1720
  const innerWidth = Math.max(56, title.length + 4, ...lines.map((line) => line.length));
@@ -1282,8 +1741,17 @@ function renderSectionTitle(title, options) {
1282
1741
  ];
1283
1742
  }
1284
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
+
1285
1754
  // src/reporting/render-rhythm-section.ts
1286
- var groupSize = 4;
1287
1755
  function buildRhythmSection(report, options) {
1288
1756
  const quietValues = report.buckets.map((bucket) => Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs));
1289
1757
  const idleValues = report.hasWakeWindow ? report.buckets.map((bucket) => bucket.awakeIdleMs) : quietValues;
@@ -1291,7 +1759,7 @@ function buildRhythmSection(report, options) {
1291
1759
  const idleTotal = formatDurationCompact(idleValues.reduce((totalDurationMs, idleDurationMs) => totalDurationMs + idleDurationMs, 0));
1292
1760
  const lines = [
1293
1761
  ...renderSectionTitle("24h Rhythm", options),
1294
- paint(` hours ${buildHourMarkerLine(report)}`, "muted", options),
1762
+ paint(` time ${buildTimeAxisLine(report)}`, "muted", options),
1295
1763
  renderRhythmRow("focus", buildGroupedTrack(buildSparkline(report.buckets.map((bucket) => bucket.engagedMs))), formatDurationCompact(report.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + bucket.engagedMs, 0)), "focus", options),
1296
1764
  renderRhythmRow("active", buildGroupedTrack(buildSparkline(report.buckets.map((bucket) => bucket.directActivityMs))), formatDurationCompact(report.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + bucket.directActivityMs, 0)), "active", options)
1297
1765
  ];
@@ -1299,24 +1767,6 @@ function buildRhythmSection(report, options) {
1299
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));
1300
1768
  return lines;
1301
1769
  }
1302
- function buildGroupedTrack(text) {
1303
- const groups = [];
1304
- for (let i = 0;i < text.length; i += groupSize) {
1305
- groups.push(text.slice(i, i + groupSize));
1306
- }
1307
- return groups.join("│");
1308
- }
1309
- function buildHourMarkerLine(report) {
1310
- const markerGroups = [];
1311
- for (let index = 0;index < report.buckets.length; index += groupSize) {
1312
- const bucket = report.buckets[index];
1313
- if (!bucket) {
1314
- continue;
1315
- }
1316
- markerGroups.push(padRight(formatHourOfDay(bucket.start, report.window), Math.min(groupSize, report.buckets.length - index)));
1317
- }
1318
- return markerGroups.join("│");
1319
- }
1320
1770
  function renderRhythmRow(label, sparkline, totalText, role, options) {
1321
1771
  return `${paint(` ${padRight(label, 6)}`, role, options)} ${paint(sparkline, role, options)} ${paint(totalText, "value", options)}`;
1322
1772
  }
@@ -1353,6 +1803,8 @@ function renderFullHourlyReport(report, options) {
1353
1803
  lines.push("");
1354
1804
  lines.push(...panelLines);
1355
1805
  lines.push("");
1806
+ lines.push(...buildAgentSection(report, options));
1807
+ lines.push("");
1356
1808
  lines.push(...buildRhythmSection(report, options));
1357
1809
  lines.push("");
1358
1810
  lines.push(...buildSpikeSection(report, options));
@@ -1382,6 +1834,8 @@ function renderShareHourlyReport(report, options) {
1382
1834
  lines.push("");
1383
1835
  lines.push(...panelLines);
1384
1836
  lines.push("");
1837
+ lines.push(...buildAgentSection(report, options));
1838
+ lines.push("");
1385
1839
  lines.push(...buildRhythmSection(report, options));
1386
1840
  lines.push("");
1387
1841
  lines.push(...buildSpikeSection(report, options));
@@ -1408,985 +1862,1410 @@ function buildFilterLine(report) {
1408
1862
  }
1409
1863
 
1410
1864
  // src/cli/run-hourly-command.ts
1411
- async function runHourlyCommand(command) {
1412
- 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
+ });
1413
1870
  const sessions = await readCodexSessions({
1414
1871
  windowStart: window.start,
1415
- windowEnd: window.end
1872
+ windowEnd: window.end,
1873
+ sessionRootDirectory: options.sessionRootDirectory
1416
1874
  });
1417
- return renderHourlyReport(buildHourlyReport(sessions, {
1418
- filters: command.filters,
1419
- idleCutoffMs: command.idleCutoffMs,
1420
- wakeWindow: command.wakeWindow,
1421
- window
1422
- }), 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
+ };
1423
1883
  }
1424
-
1425
- // src/best-metrics/notification-delivery.ts
1426
- import { execFile } from "node:child_process";
1427
- import { existsSync } from "node:fs";
1428
- import { fileURLToPath, pathToFileURL } from "node:url";
1429
- import { promisify } from "node:util";
1430
- var execFileAsync = promisify(execFile);
1431
- async function deliverLocalNotifications(notifications, options = {}) {
1432
- const platform = options.platform ?? process.platform;
1433
- if (platform !== "darwin" || notifications.length === 0) {
1434
- return;
1435
- }
1436
- const notifier = options.notifier ?? sendMacOsNotification;
1437
- for (const notification of notifications) {
1438
- try {
1439
- await notifier(notification);
1440
- } catch {
1441
- return;
1442
- }
1443
- }
1884
+ async function runHourlyCommand(command, options = {}) {
1885
+ const commandResult = await buildHourlyCommandResult(command, options);
1886
+ return renderHourlyReport(commandResult.hourlyReport, createRenderOptions(command.shareMode));
1444
1887
  }
1445
- async function sendMacOsNotification(notification) {
1446
- const notificationIconPath = resolveNotificationIconPath();
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 = {}) {
1447
1902
  try {
1448
- await execFileAsync("terminal-notifier", [
1449
- "-title",
1450
- notification.title,
1451
- "-message",
1452
- notification.body,
1453
- ...notificationIconPath ? ["-appIcon", pathToFileURL(notificationIconPath).href] : []
1454
- ]);
1455
- return;
1903
+ const rawLedgerText = await readFile2(resolveBestLedgerPath(options), "utf8");
1904
+ return parseBestLedger(JSON.parse(rawLedgerText));
1456
1905
  } catch (error) {
1457
- if (!isCommandMissingError(error)) {
1458
- throw error;
1906
+ if (isMissingFileError(error)) {
1907
+ return null;
1459
1908
  }
1909
+ throw error;
1460
1910
  }
1461
- await execFileAsync("osascript", [
1462
- "-e",
1463
- `display notification "${escapeAppleScriptText(notification.body)}" with title "${escapeAppleScriptText(notification.title)}"`
1464
- ]);
1465
1911
  }
1466
- function escapeAppleScriptText(value) {
1467
- return value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"");
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
+ };
1468
1926
  }
1469
- function resolveNotificationIconPath() {
1470
- const candidatePaths = [
1471
- fileURLToPath(new URL("../../assets/idle-time-notification-icon.png", import.meta.url)),
1472
- fileURLToPath(new URL("../assets/idle-time-notification-icon.png", import.meta.url))
1473
- ];
1474
- return candidatePaths.find((candidatePath) => existsSync(candidatePath)) ?? null;
1927
+ function resolveBestLedgerPath(options = {}) {
1928
+ return join2(resolveBestStateDirectory(options), bestLedgerFileName);
1475
1929
  }
1476
- function isCommandMissingError(error) {
1477
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
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
+ `;
1478
1941
  }
1479
-
1480
- // src/best-metrics/near-best-notifications.ts
1481
- import { mkdir, readFile as readFile2, rename, writeFile } from "node:fs/promises";
1482
- import { homedir as homedir2 } from "node:os";
1483
- import { join as join2 } from "node:path";
1484
- var nearBestNotificationStateFileName = "near-best-notifications-v1.json";
1485
- var nearBestNotificationVersion = 1;
1486
- async function notifyNearBestMetrics(currentMetrics, ledger, options = {}) {
1487
- const now = options.now ?? new Date;
1488
- const state = await ensureNearBestNotificationState(options);
1489
- if (!state.nearBestEnabled) {
1490
- return [];
1491
- }
1492
- const metricsToNotify = buildNearBestMetricKeys(currentMetrics, ledger, state, now);
1493
- if (metricsToNotify.length === 0) {
1494
- return [];
1942
+ function parseBestMetricRecord(value, label) {
1943
+ if (value === null || value === undefined) {
1944
+ return null;
1495
1945
  }
1496
- const nextState = {
1497
- ...state,
1498
- lastNotifiedAt: {
1499
- ...state.lastNotifiedAt,
1500
- ...Object.fromEntries(metricsToNotify.map((metric) => [metric, now]))
1501
- }
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`)
1502
1952
  };
1503
- await writeNearBestNotificationState(nextState, options);
1504
- await deliverLocalNotifications(metricsToNotify.map((metric) => buildNearBestNotification(metric, currentMetrics[metric], ledger[metric]?.value ?? 0)), options);
1505
- return metricsToNotify;
1506
- }
1507
- async function ensureNearBestNotificationState(options) {
1508
- const existingState = await readNearBestNotificationState(options);
1509
- if (existingState) {
1510
- return existingState;
1511
- }
1512
- const defaultState = createDefaultNearBestNotificationState();
1513
- await writeNearBestNotificationState(defaultState, options);
1514
- return defaultState;
1515
- }
1516
- async function readNearBestNotificationState(options) {
1517
- try {
1518
- const rawStateText = await readFile2(resolveNearBestNotificationStatePath(options), "utf8");
1519
- return parseNearBestNotificationState(JSON.parse(rawStateText));
1520
- } catch (error) {
1521
- if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
1522
- return null;
1523
- }
1524
- throw error;
1525
- }
1526
1953
  }
1527
- function parseNearBestNotificationState(value) {
1528
- const stateRecord = expectObject(value, "nearBestNotificationState");
1529
- const version = readNumber(stateRecord, "version", "nearBestNotificationState");
1530
- if (version !== nearBestNotificationVersion) {
1531
- throw new Error(`nearBestNotificationState.version must be ${nearBestNotificationVersion}.`);
1954
+ function serializeBestMetricRecord(record) {
1955
+ if (!record) {
1956
+ return null;
1532
1957
  }
1533
- const lastNotifiedAtRecord = expectObject(stateRecord.lastNotifiedAt, "nearBestNotificationState.lastNotifiedAt");
1534
1958
  return {
1535
- version,
1536
- nearBestEnabled: Boolean(stateRecord.nearBestEnabled),
1537
- thresholdRatio: readNumber(stateRecord, "thresholdRatio", "nearBestNotificationState"),
1538
- cooldownMs: readNumber(stateRecord, "cooldownMs", "nearBestNotificationState"),
1539
- lastNotifiedAt: {
1540
- bestConcurrentAgents: readOptionalIsoTimestamp(lastNotifiedAtRecord.bestConcurrentAgents, "nearBestNotificationState.lastNotifiedAt.bestConcurrentAgents"),
1541
- best24hRawBurn: readOptionalIsoTimestamp(lastNotifiedAtRecord.best24hRawBurn, "nearBestNotificationState.lastNotifiedAt.best24hRawBurn"),
1542
- best24hAgentSumMs: readOptionalIsoTimestamp(lastNotifiedAtRecord.best24hAgentSumMs, "nearBestNotificationState.lastNotifiedAt.best24hAgentSumMs")
1543
- }
1959
+ value: record.value,
1960
+ observedAt: record.observedAt.toISOString(),
1961
+ windowStart: record.windowStart.toISOString(),
1962
+ windowEnd: record.windowEnd.toISOString()
1544
1963
  };
1545
1964
  }
1546
- async function writeNearBestNotificationState(state, options) {
1547
- const statePath = resolveNearBestNotificationStatePath(options);
1548
- const stateDirectory = options.stateDirectory ?? join2(homedir2(), ".idletime");
1549
- await mkdir(stateDirectory, { recursive: true });
1550
- const temporaryPath = join2(stateDirectory, `.near-best-notifications.${process.pid}.${Date.now()}.tmp`);
1551
- await writeFile(temporaryPath, `${JSON.stringify({
1552
- version: state.version,
1553
- nearBestEnabled: state.nearBestEnabled,
1554
- thresholdRatio: state.thresholdRatio,
1555
- cooldownMs: state.cooldownMs,
1556
- lastNotifiedAt: {
1557
- bestConcurrentAgents: state.lastNotifiedAt.bestConcurrentAgents?.toISOString() ?? null,
1558
- best24hRawBurn: state.lastNotifiedAt.best24hRawBurn?.toISOString() ?? null,
1559
- best24hAgentSumMs: state.lastNotifiedAt.best24hAgentSumMs?.toISOString() ?? null
1560
- }
1561
- }, null, 2)}
1562
- `, "utf8");
1563
- await rename(temporaryPath, statePath);
1965
+ function resolveBestStateDirectory(options) {
1966
+ return options.stateDirectory ?? join2(homedir2(), ".idletime");
1564
1967
  }
1565
- function buildNearBestMetricKeys(currentMetrics, ledger, state, now) {
1566
- return [
1567
- "bestConcurrentAgents",
1568
- "best24hRawBurn",
1569
- "best24hAgentSumMs"
1570
- ].filter((metric) => {
1571
- const bestValue = ledger[metric]?.value ?? 0;
1572
- if (bestValue <= 0) {
1573
- return false;
1574
- }
1575
- const currentValue = currentMetrics[metric];
1576
- if (currentValue <= 0 || currentValue >= bestValue) {
1577
- return false;
1578
- }
1579
- if (currentValue / bestValue < state.thresholdRatio) {
1580
- return false;
1581
- }
1582
- const lastNotifiedAt = state.lastNotifiedAt[metric];
1583
- return lastNotifiedAt === null || now.getTime() - lastNotifiedAt.getTime() >= state.cooldownMs;
1584
- });
1968
+ function isMissingFileError(error) {
1969
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1585
1970
  }
1586
- function buildNearBestNotification(metric, currentValue, bestValue) {
1971
+
1972
+ // src/reporting/build-summary-report.ts
1973
+ function buildSummaryReport(sessions, query) {
1974
+ const filteredSessions = filterSessions(sessions, query.filters);
1975
+ const windowInterval = {
1976
+ start: query.window.start,
1977
+ end: query.window.end
1978
+ };
1979
+ const metrics = clipActivityMetricsToWindow(buildActivityMetrics(filteredSessions, query.idleCutoffMs, query.window.end), windowInterval);
1980
+ const comparisonCutoffMs = parseDurationToMs("30m");
1981
+ const sessionCounts = {
1982
+ total: filteredSessions.length,
1983
+ direct: filteredSessions.filter((session) => session.kind === "direct").length,
1984
+ subagent: filteredSessions.filter((session) => session.kind === "subagent").length
1985
+ };
1587
1986
  return {
1588
- title: metric === "bestConcurrentAgents" ? "Close to best concurrent agents" : metric === "best24hRawBurn" ? "Close to best 24hr raw burn" : "Close to best agent sum",
1589
- 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`
1987
+ activityWindow: resolveActivityWindow(filteredSessions, windowInterval),
1988
+ appliedFilters: query.filters,
1989
+ comparisonCutoffMs,
1990
+ comparisonMetrics: query.idleCutoffMs === comparisonCutoffMs ? metrics : clipActivityMetricsToWindow(buildActivityMetrics(filteredSessions, comparisonCutoffMs, query.window.end), windowInterval),
1991
+ directTokenTotals: sumTokenTotals(filteredSessions.filter((session) => session.kind === "direct"), windowInterval),
1992
+ groupBreakdowns: buildGroupBreakdowns(filteredSessions, query.groupBy, query.idleCutoffMs, windowInterval),
1993
+ idleCutoffMs: query.idleCutoffMs,
1994
+ metrics,
1995
+ sessionCounts,
1996
+ tokenTotals: sumTokenTotals(filteredSessions, windowInterval),
1997
+ wakeSummary: query.wakeWindow ? summarizeWakeWindow(query.wakeWindow, query.window, metrics) : null,
1998
+ window: query.window
1590
1999
  };
1591
2000
  }
1592
- function createDefaultNearBestNotificationState() {
2001
+ function buildGroupBreakdowns(sessions, dimensions, idleCutoffMs, windowInterval) {
2002
+ return dimensions.map((dimension) => ({
2003
+ dimension,
2004
+ rows: groupSessions(sessions, dimension).map((groupedSessions) => buildGroupRow(groupedSessions.key, groupedSessions.sessions, idleCutoffMs, windowInterval, windowInterval.end))
2005
+ }));
2006
+ }
2007
+ function buildGroupRow(key, sessions, idleCutoffMs, windowInterval, observedAt) {
2008
+ const metrics = clipActivityMetricsToWindow(buildActivityMetrics(sessions, idleCutoffMs, observedAt), windowInterval);
2009
+ const tokenTotals = sumTokenTotals(sessions, windowInterval);
1593
2010
  return {
1594
- version: nearBestNotificationVersion,
1595
- nearBestEnabled: false,
1596
- thresholdRatio: 0.97,
1597
- cooldownMs: 24 * 60 * 60 * 1000,
1598
- lastNotifiedAt: {
1599
- bestConcurrentAgents: null,
1600
- best24hRawBurn: null,
1601
- best24hAgentSumMs: null
1602
- }
2011
+ key,
2012
+ sessionCount: sessions.length,
2013
+ directActivityMs: metrics.directActivityMs,
2014
+ agentCoverageMs: metrics.agentCoverageMs,
2015
+ cumulativeAgentMs: metrics.cumulativeAgentMs,
2016
+ practicalBurn: tokenTotals.practicalBurn,
2017
+ rawTotalTokens: tokenTotals.rawTotalTokens
1603
2018
  };
1604
2019
  }
1605
- function resolveNearBestNotificationStatePath(options) {
1606
- return join2(options.stateDirectory ?? join2(homedir2(), ".idletime"), nearBestNotificationStateFileName);
2020
+ function sumTokenTotals(sessions, windowInterval) {
2021
+ return sessions.reduce((tokenTotals, session) => {
2022
+ const sessionWindowTotals = buildTokenDeltaPoints(session.tokenPoints).filter((tokenDeltaPoint) => tokenDeltaPoint.timestamp.getTime() >= windowInterval.start.getTime() && tokenDeltaPoint.timestamp.getTime() <= windowInterval.end.getTime()).reduce((sessionTotals, tokenDeltaPoint) => ({
2023
+ practicalBurn: sessionTotals.practicalBurn + tokenDeltaPoint.deltaUsage.practicalBurn,
2024
+ rawTotalTokens: sessionTotals.rawTotalTokens + tokenDeltaPoint.deltaUsage.totalTokens
2025
+ }), { practicalBurn: 0, rawTotalTokens: 0 });
2026
+ return {
2027
+ practicalBurn: tokenTotals.practicalBurn + sessionWindowTotals.practicalBurn,
2028
+ rawTotalTokens: tokenTotals.rawTotalTokens + sessionWindowTotals.rawTotalTokens
2029
+ };
2030
+ }, {
2031
+ practicalBurn: 0,
2032
+ rawTotalTokens: 0
2033
+ });
1607
2034
  }
1608
- function readOptionalIsoTimestamp(value, label) {
1609
- if (value === null || value === undefined) {
2035
+ function resolveActivityWindow(sessions, windowInterval) {
2036
+ if (sessions.length === 0) {
1610
2037
  return null;
1611
2038
  }
1612
- return readIsoTimestamp(value, label);
1613
- }
1614
- function formatAgentSumHours2(durationMs) {
1615
- const hours = durationMs / 3600000;
1616
- return hours >= 10 ? Math.round(hours).toString() : (Math.round(hours * 10) / 10).toString();
1617
- }
1618
- function formatCompactInteger2(value) {
1619
- return new Intl.NumberFormat("en-US", {
1620
- notation: "compact",
1621
- maximumFractionDigits: 1
1622
- }).format(Math.round(value)).toUpperCase();
1623
- }
1624
- function formatInteger2(value) {
1625
- return new Intl.NumberFormat("en-US").format(Math.round(value));
1626
- }
1627
-
1628
- // src/best-metrics/notify-best-events.ts
1629
- async function notifyBestEvents(bestEvents, options = {}) {
1630
- await deliverLocalNotifications(bestEvents.map((bestEvent) => buildBestEventNotification(bestEvent)), options);
1631
- }
1632
- function buildBestEventNotification(bestEvent) {
2039
+ const firstTimestamp = sessions.reduce((earliestTimestamp, session) => session.firstTimestamp.getTime() < earliestTimestamp.getTime() ? session.firstTimestamp : earliestTimestamp, sessions[0].firstTimestamp);
2040
+ const lastTimestamp = sessions.reduce((latestTimestamp, session) => session.lastTimestamp.getTime() > latestTimestamp.getTime() ? session.lastTimestamp : latestTimestamp, sessions[0].lastTimestamp);
1633
2041
  return {
1634
- title: resolveNotificationTitle(bestEvent.metric),
1635
- body: resolveNotificationBody(bestEvent)
2042
+ start: new Date(Math.max(firstTimestamp.getTime(), windowInterval.start.getTime())),
2043
+ end: new Date(Math.min(lastTimestamp.getTime(), windowInterval.end.getTime()))
1636
2044
  };
1637
2045
  }
1638
- function resolveNotificationTitle(bestMetricKey) {
1639
- return bestMetricKey === "bestConcurrentAgents" ? "New best concurrent agents" : bestMetricKey === "best24hRawBurn" ? "New best 24hr raw burn" : "New best agent sum";
1640
- }
1641
- function resolveNotificationBody(bestEvent) {
1642
- return bestEvent.metric === "bestConcurrentAgents" ? `${formatInteger3(bestEvent.value)} concurrent agents` : bestEvent.metric === "best24hRawBurn" ? `${formatCompactInteger3(bestEvent.value)} 24hr raw burn` : `${formatAgentSumHours3(bestEvent.value)} agent sum`;
1643
- }
1644
- function formatAgentSumHours3(durationMs) {
1645
- const hours = durationMs / 3600000;
1646
- return hours >= 10 ? Math.round(hours).toString() : (Math.round(hours * 10) / 10).toString();
1647
- }
1648
- function formatCompactInteger3(value) {
1649
- return new Intl.NumberFormat("en-US", {
1650
- notation: "compact",
1651
- maximumFractionDigits: 1
1652
- }).format(Math.round(value)).toUpperCase();
1653
- }
1654
- function formatInteger3(value) {
1655
- return new Intl.NumberFormat("en-US").format(Math.round(value));
1656
- }
1657
-
1658
- // src/best-metrics/types.ts
1659
- var bestMetricsLedgerVersion = 1;
1660
- var rollingWindowDurationMs = 24 * 60 * 60 * 1000;
1661
- var defaultBestMetricsIdleCutoffMs = 15 * 60 * 1000;
1662
-
1663
- // src/best-metrics/build-current-best-metrics.ts
1664
- function buildCurrentBestMetricValues(sessions, options = {}) {
1665
- const idleCutoffMs = options.idleCutoffMs ?? defaultBestMetricsIdleCutoffMs;
1666
- const now = options.now ?? new Date;
1667
- const activityMetrics = buildActivityMetrics(sessions, idleCutoffMs);
1668
- const currentWindow = {
1669
- start: new Date(now.getTime() - rollingWindowDurationMs),
1670
- end: now
1671
- };
2046
+ function clipActivityMetricsToWindow(metrics, windowInterval) {
2047
+ const strictEngagementBlocks = intersectTimeIntervals(metrics.strictEngagementBlocks, [windowInterval]);
2048
+ const directActivityBlocks = intersectTimeIntervals(metrics.directActivityBlocks, [windowInterval]);
2049
+ const agentCoverageBlocks = intersectTimeIntervals(metrics.agentCoverageBlocks, [windowInterval]);
2050
+ const agentOnlyBlocks = intersectTimeIntervals(metrics.agentOnlyBlocks, [windowInterval]);
2051
+ const perAgentTaskBlocks = metrics.perAgentTaskBlocks.map((taskBlocks) => intersectTimeIntervals(taskBlocks, [windowInterval]));
1672
2052
  return {
1673
- bestConcurrentAgents: countLiveSubagents(activityMetrics.perSubagentBlocks, now),
1674
- 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),
1675
- best24hAgentSumMs: measureOverlapMs(activityMetrics.perSubagentBlocks.flatMap((sessionBlocks) => sessionBlocks), currentWindow)
2053
+ strictEngagementBlocks,
2054
+ directActivityBlocks,
2055
+ agentCoverageBlocks,
2056
+ agentOnlyBlocks,
2057
+ perAgentTaskBlocks,
2058
+ strictEngagementMs: sumTimeIntervalsMs(strictEngagementBlocks),
2059
+ directActivityMs: sumTimeIntervalsMs(directActivityBlocks),
2060
+ agentCoverageMs: sumTimeIntervalsMs(agentCoverageBlocks),
2061
+ agentOnlyMs: sumTimeIntervalsMs(agentOnlyBlocks),
2062
+ cumulativeAgentMs: perAgentTaskBlocks.reduce((totalDurationMs, taskBlocks) => totalDurationMs + sumTimeIntervalsMs(taskBlocks), 0),
2063
+ peakConcurrentAgents: peakConcurrency(perAgentTaskBlocks)
1676
2064
  };
1677
2065
  }
1678
- function countLiveSubagents(intervalGroups, now) {
1679
- return intervalGroups.reduce((liveCount, intervalGroup) => liveCount + Number(intervalGroup.some((interval) => interval.start.getTime() <= now.getTime() && interval.end.getTime() > now.getTime())), 0);
1680
- }
1681
2066
 
1682
- // src/best-metrics/append-best-events.ts
1683
- import { appendFile, mkdir as mkdir2 } from "node:fs/promises";
1684
- import { homedir as homedir3 } from "node:os";
1685
- import { join as join3 } from "node:path";
1686
- var bestEventsFileName = "best-events.ndjson";
1687
- async function appendBestEvents(bestEvents, options = {}) {
1688
- if (bestEvents.length === 0) {
1689
- return;
2067
+ // src/reporting/render-summary-report.ts
2068
+ var summaryBarWidth = 18;
2069
+ function renderSummaryReport(report, options, hourlyReport, bestPlaque = null) {
2070
+ return options.shareMode ? renderShareSummaryReport(report, options, hourlyReport, bestPlaque) : renderFullSummaryReport(report, options, hourlyReport, bestPlaque);
2071
+ }
2072
+ function renderFullSummaryReport(report, options, hourlyReport, bestPlaque = null) {
2073
+ const lines = [];
2074
+ const requestedMetrics = report.metrics;
2075
+ const actualComparisonMetrics = report.comparisonMetrics;
2076
+ const windowDurationMs = report.window.end.getTime() - report.window.start.getTime();
2077
+ const headerLines = buildSummaryHeaderLines(report, hourlyReport);
2078
+ const panelLines = renderPanel(`idletime • ${report.window.label}`, headerLines, options);
2079
+ const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
2080
+ const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
2081
+ lines.push(...buildLogoSection(logoSectionWidth, options, bestPlaque));
2082
+ lines.push("");
2083
+ lines.push(...panelLines);
2084
+ if (hourlyReport) {
2085
+ lines.push("");
2086
+ lines.push(...buildAgentSection(hourlyReport, options));
2087
+ lines.push("");
2088
+ lines.push(...buildRhythmSection(hourlyReport, options));
1690
2089
  }
1691
- const stateDirectory = resolveBestStateDirectory(options);
1692
- await mkdir2(stateDirectory, { recursive: true });
1693
- await appendFile(join3(stateDirectory, bestEventsFileName), `${bestEvents.map(serializeBestEvent).join(`
1694
- `)}
1695
- `, "utf8");
2090
+ lines.push("");
2091
+ lines.push(...renderSectionTitle("Activity", options));
2092
+ lines.push(renderMetricRow("strict", requestedMetrics.strictEngagementMs, windowDurationMs, formatDurationHours(requestedMetrics.strictEngagementMs), `${formatSignedDurationHours(actualComparisonMetrics.strictEngagementMs - requestedMetrics.strictEngagementMs)} at ${formatDurationLabel(report.comparisonCutoffMs)}`, "█", "focus", options));
2093
+ lines.push(renderMetricRow("direct", requestedMetrics.directActivityMs, windowDurationMs, formatDurationHours(requestedMetrics.directActivityMs), `${formatSignedDurationHours(actualComparisonMetrics.directActivityMs - requestedMetrics.directActivityMs)} at ${formatDurationLabel(report.comparisonCutoffMs)}`, "▓", "active", options));
2094
+ lines.push(renderMetricRow("agent live", requestedMetrics.agentCoverageMs, windowDurationMs, formatDurationHours(requestedMetrics.agentCoverageMs), "coverage", "▒", "agent", options));
2095
+ lines.push(renderMetricRow("agent sum", requestedMetrics.cumulativeAgentMs, Math.max(windowDurationMs, requestedMetrics.cumulativeAgentMs), formatDurationHours(requestedMetrics.cumulativeAgentMs), `peak ${requestedMetrics.peakConcurrentAgents} concurrent`, "▚", "agent", options));
2096
+ lines.push(`${paint(padRight(" session mix", 14), "muted", options)} ${paint(buildSplitBar([
2097
+ {
2098
+ filledCharacter: "█",
2099
+ value: report.sessionCounts.direct
2100
+ },
2101
+ {
2102
+ filledCharacter: "▓",
2103
+ value: report.sessionCounts.subagent
2104
+ }
2105
+ ], summaryBarWidth), "active", options)} ${paint(`${report.sessionCounts.direct} direct / ${report.sessionCounts.subagent} subagent`, "value", options)}`);
2106
+ lines.push("");
2107
+ lines.push(...renderSectionTitle("Tokens", options));
2108
+ const maxBurnValue = Math.max(report.tokenTotals.practicalBurn, report.directTokenTotals.practicalBurn);
2109
+ const maxRawValue = Math.max(report.tokenTotals.rawTotalTokens, report.directTokenTotals.rawTotalTokens);
2110
+ lines.push(renderMetricRow("practical burn", report.tokenTotals.practicalBurn, maxBurnValue, formatCompactInteger(report.tokenTotals.practicalBurn), `${formatPercentage(report.tokenTotals.practicalBurn / report.tokenTotals.rawTotalTokens)} of raw`, "█", "burn", options, "burn"));
2111
+ lines.push(renderMetricRow("all raw", report.tokenTotals.rawTotalTokens, maxRawValue, formatCompactInteger(report.tokenTotals.rawTotalTokens), `${formatInteger(report.tokenTotals.rawTotalTokens)} total`, "█", "raw", options, "raw"));
2112
+ lines.push(renderMetricRow("direct burn", report.directTokenTotals.practicalBurn, maxBurnValue, formatCompactInteger(report.directTokenTotals.practicalBurn), `${formatPercentage(report.directTokenTotals.practicalBurn / report.tokenTotals.practicalBurn)} of burn`, "▒", "burn", options, "burn"));
2113
+ lines.push(renderMetricRow("direct raw", report.directTokenTotals.rawTotalTokens, maxRawValue, formatCompactInteger(report.directTokenTotals.rawTotalTokens), `${formatPercentage(report.directTokenTotals.rawTotalTokens / report.tokenTotals.rawTotalTokens)} of raw`, "▒", "raw", options, "raw"));
2114
+ if (report.wakeSummary) {
2115
+ lines.push("");
2116
+ lines.push(...renderSectionTitle("Wake Window", options));
2117
+ lines.push(renderMetricRow("direct awake", report.wakeSummary.directActivityMs, report.wakeSummary.wakeDurationMs, formatDurationClock(report.wakeSummary.directActivityMs), `of ${formatDurationClock(report.wakeSummary.wakeDurationMs)} wake`, "▓", "active", options));
2118
+ lines.push(renderMetricRow("strict awake", report.wakeSummary.strictEngagementMs, report.wakeSummary.wakeDurationMs, formatDurationClock(report.wakeSummary.strictEngagementMs), "engaged", "█", "focus", options));
2119
+ lines.push(renderMetricRow("agent awake", report.wakeSummary.agentOnlyMs, report.wakeSummary.wakeDurationMs, formatDurationClock(report.wakeSummary.agentOnlyMs), "agent-only", "▒", "agent", options));
2120
+ lines.push(renderMetricRow("awake idle", report.wakeSummary.awakeIdleMs, report.wakeSummary.wakeDurationMs, formatDurationClock(report.wakeSummary.awakeIdleMs), `${formatPercentage(report.wakeSummary.awakeIdlePercentage)} idle`, "░", "idle", options));
2121
+ lines.push(`${paint(padRight(" longest gap", 14), "muted", options)} ${paint(formatDurationClock(report.wakeSummary.longestIdleGapMs), "value", options)} ${dim("largest quiet stretch", options)}`);
2122
+ }
2123
+ for (const groupBreakdown of report.groupBreakdowns) {
2124
+ lines.push("");
2125
+ lines.push(...renderSectionTitle(groupBreakdown.dimension === "model" ? "Model Breakdown" : "Effort Breakdown", options));
2126
+ const maxBreakdownBurn = Math.max(...groupBreakdown.rows.map((row) => row.practicalBurn), 0);
2127
+ for (const row of groupBreakdown.rows) {
2128
+ lines.push(`${paint(padRight(` ${row.key}`, 20), "muted", options)} ${paint(buildBar(row.practicalBurn, maxBreakdownBurn, 14, "█"), "burn", options)} ${paint(padRight(formatCompactInteger(row.practicalBurn), 6), "value", options)} ${dim("burn", options)} ${paint(padRight(formatDurationCompact(row.directActivityMs), 5), "active", options)} ${dim("direct", options)} ${paint(padRight(formatDurationCompact(row.agentCoverageMs), 5), "agent", options)} ${dim("live", options)} ${paint(`${row.sessionCount} s`, "value", options)}`);
2129
+ }
2130
+ }
2131
+ return lines.join(`
2132
+ `);
1696
2133
  }
1697
- function serializeBestEvent(bestEvent) {
1698
- return JSON.stringify({
1699
- metric: bestEvent.metric,
1700
- previousValue: bestEvent.previousValue,
1701
- value: bestEvent.value,
1702
- observedAt: bestEvent.observedAt.toISOString(),
1703
- windowStart: bestEvent.windowStart.toISOString(),
1704
- windowEnd: bestEvent.windowEnd.toISOString(),
1705
- version: bestEvent.version
2134
+ function renderShareSummaryReport(report, options, hourlyReport, bestPlaque = null) {
2135
+ const lines = [];
2136
+ const headerLines = buildSummaryHeaderLines(report, hourlyReport);
2137
+ const panelLines = renderPanel(`idletime • ${report.window.label}`, headerLines, options);
2138
+ const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
2139
+ const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
2140
+ lines.push(...buildLogoSection(logoSectionWidth, options, bestPlaque));
2141
+ lines.push("");
2142
+ lines.push(...panelLines);
2143
+ if (hourlyReport) {
2144
+ lines.push("");
2145
+ lines.push(...buildAgentSection(hourlyReport, options));
2146
+ lines.push("");
2147
+ lines.push(...buildRhythmSection(hourlyReport, options));
2148
+ }
2149
+ lines.push("");
2150
+ lines.push(...renderSectionTitle("Snapshot", options));
2151
+ lines.push(renderSnapshotRow("focus", formatDurationHours(report.metrics.strictEngagementMs), "focused time", "focus", options));
2152
+ lines.push(renderSnapshotRow("active", formatDurationHours(report.metrics.directActivityMs), "direct-session movement", "active", options));
2153
+ lines.push(renderSnapshotRow(report.wakeSummary ? "idle" : "quiet", report.wakeSummary ? formatDurationClock(report.wakeSummary.awakeIdleMs) : hourlyReport ? formatDurationCompact(hourlyReport.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs), 0)) : "n/a", report.wakeSummary ? "awake idle" : "quiet hours", "idle", options));
2154
+ lines.push(renderSnapshotRow("burn", formatCompactInteger(report.tokenTotals.practicalBurn), `${formatPercentage(report.tokenTotals.practicalBurn / report.tokenTotals.rawTotalTokens)} of raw`, "burn", options));
2155
+ lines.push(renderSnapshotRow("agents", `${report.metrics.peakConcurrentAgents} peak`, `${formatDurationHours(report.metrics.cumulativeAgentMs)} cumulative`, "agent", options));
2156
+ lines.push(renderSnapshotRow("sessions", `${report.sessionCounts.total}`, `${report.sessionCounts.direct} direct / ${report.sessionCounts.subagent} subagent`, "value", options));
2157
+ return lines.join(`
2158
+ `);
2159
+ }
2160
+ function formatAppliedFilters(report) {
2161
+ const appliedFilters = [];
2162
+ if (report.appliedFilters.workspaceOnlyPrefix) {
2163
+ appliedFilters.push(`workspace=${shortenPath(report.appliedFilters.workspaceOnlyPrefix, 48)}`);
2164
+ }
2165
+ if (report.appliedFilters.sessionKind) {
2166
+ appliedFilters.push(`kind=${report.appliedFilters.sessionKind}`);
2167
+ }
2168
+ if (report.appliedFilters.model) {
2169
+ appliedFilters.push(`model=${report.appliedFilters.model}`);
2170
+ }
2171
+ if (report.appliedFilters.reasoningEffort) {
2172
+ appliedFilters.push(`effort=${report.appliedFilters.reasoningEffort}`);
2173
+ }
2174
+ return appliedFilters;
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
+ }
2263
+ function formatDurationLabel(durationMs) {
2264
+ return `${Math.round(durationMs / 60000)}m`;
2265
+ }
2266
+ function renderMetricRow(label, value, maxValue, primaryText, detailText, filledCharacter, role, options, valueRole = "value") {
2267
+ return `${paint(padRight(` ${label}`, 14), "muted", options)} ${paint(buildBar(value, maxValue, summaryBarWidth, filledCharacter), role, options)} ${paint(padRight(primaryText, 7), valueRole, options)} ${dim(detailText, options)}`;
2268
+ }
2269
+ function renderSnapshotRow(label, primaryText, detailText, role, options) {
2270
+ return `${paint(padRight(` ${label}`, 12), role, options)} ${paint(padRight(primaryText, 10), "value", options)} ${dim(detailText, options)}`;
2271
+ }
2272
+
2273
+ // src/cli/run-last24h-command.ts
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({
2281
+ windowStart: window.start,
2282
+ windowEnd: window.end,
2283
+ sessionRootDirectory: options.sessionRootDirectory
2284
+ });
2285
+ const [bestLedger, sessions] = await Promise.all([
2286
+ bestLedgerPromise,
2287
+ sessionsPromise
2288
+ ]);
2289
+ const summaryReport = buildSummaryReport(sessions, {
2290
+ filters: command.filters,
2291
+ groupBy: command.groupBy,
2292
+ idleCutoffMs: command.idleCutoffMs,
2293
+ wakeWindow: command.wakeWindow,
2294
+ window
2295
+ });
2296
+ const hourlyReport = buildHourlyReport(sessions, {
2297
+ filters: command.filters,
2298
+ idleCutoffMs: command.idleCutoffMs,
2299
+ wakeWindow: command.wakeWindow,
2300
+ window
1706
2301
  });
2302
+ return {
2303
+ bestLedger,
2304
+ hourlyReport,
2305
+ summaryReport
2306
+ };
1707
2307
  }
1708
- function resolveBestStateDirectory(options) {
1709
- return options.stateDirectory ?? join3(homedir3(), ".idletime");
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);
1710
2311
  }
1711
2312
 
1712
- // src/best-metrics/build-rolling-24h-windows.ts
1713
- function findBestRollingWindowTotal(weightedPoints) {
1714
- const sortedPoints = weightedPoints.filter((point) => point.value > 0).slice().sort((leftPoint, rightPoint) => leftPoint.timestamp.getTime() - rightPoint.timestamp.getTime());
1715
- if (sortedPoints.length === 0) {
1716
- return null;
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);
1717
2351
  }
1718
- let bestValue = 0;
1719
- let bestTimestampMs = 0;
1720
- let currentTotal = 0;
1721
- let leftIndex = 0;
1722
- for (let rightIndex = 0;rightIndex < sortedPoints.length; rightIndex += 1) {
1723
- const rightPoint = sortedPoints[rightIndex];
1724
- currentTotal += rightPoint.value;
1725
- while (rightPoint.timestamp.getTime() - sortedPoints[leftIndex].timestamp.getTime() > rollingWindowDurationMs) {
1726
- currentTotal -= sortedPoints[leftIndex].value;
1727
- leftIndex += 1;
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);
2358
+ }
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 [];
1728
2377
  }
1729
- if (currentTotal > bestValue) {
1730
- bestValue = currentTotal;
1731
- bestTimestampMs = rightPoint.timestamp.getTime();
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 [];
1732
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;
1733
2390
  }
1734
- if (bestValue === 0) {
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) {
1735
2402
  return null;
1736
2403
  }
1737
- return createRollingRecord(bestValue, bestTimestampMs);
1738
- }
1739
- function findBestRollingWindowOverlap(intervals) {
1740
- const slopeChanges = intervals.flatMap(buildSlopeChanges);
1741
- if (slopeChanges.length === 0) {
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) {
1742
2407
  return null;
1743
2408
  }
1744
- slopeChanges.sort((leftChange, rightChange) => leftChange.timestampMs - rightChange.timestampMs);
1745
- let bestTimestampMs = 0;
1746
- let bestValue = 0;
1747
- let currentSlope = 0;
1748
- let currentValue = 0;
1749
- let previousTimestampMs = slopeChanges[0].timestampMs;
1750
- let index = 0;
1751
- while (index < slopeChanges.length) {
1752
- const timestampMs = slopeChanges[index].timestampMs;
1753
- currentValue += currentSlope * (timestampMs - previousTimestampMs);
1754
- if (currentValue > bestValue) {
1755
- bestValue = currentValue;
1756
- bestTimestampMs = timestampMs;
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 [];
1757
2419
  }
1758
- while (index < slopeChanges.length && slopeChanges[index].timestampMs === timestampMs) {
1759
- currentSlope += slopeChanges[index].deltaSlope;
1760
- index += 1;
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 [];
1761
2424
  }
1762
- previousTimestampMs = timestampMs;
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
+ ];
1763
2538
  }
1764
- if (bestValue === 0) {
1765
- return null;
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)}`);
1766
2547
  }
1767
- return createRollingRecord(bestValue, bestTimestampMs);
2548
+ return lines;
1768
2549
  }
1769
- function buildSlopeChanges(interval) {
1770
- const startMs = interval.start.getTime();
1771
- const endMs = interval.end.getTime();
1772
- if (endMs <= startMs) {
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) {
1773
2569
  return [];
1774
2570
  }
1775
- return [
1776
- { timestampMs: startMs, deltaSlope: 1 },
1777
- { timestampMs: endMs, deltaSlope: -1 },
1778
- { timestampMs: startMs + rollingWindowDurationMs, deltaSlope: -1 },
1779
- { timestampMs: endMs + rollingWindowDurationMs, deltaSlope: 1 }
1780
- ];
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
+ });
1781
2575
  }
1782
- function createRollingRecord(value, observedAtMs) {
1783
- return {
1784
- value,
1785
- observedAt: new Date(observedAtMs),
1786
- windowStart: new Date(observedAtMs - rollingWindowDurationMs),
1787
- windowEnd: new Date(observedAtMs)
1788
- };
2576
+ function formatLocationLabel(cwd) {
2577
+ if (cwd.endsWith("/.agents")) {
2578
+ return "~/.agents";
2579
+ }
2580
+ return basename(cwd) || shortenPath(cwd, 24);
1789
2581
  }
1790
-
1791
- // src/best-metrics/build-best-metrics.ts
1792
- function buildBestMetricCandidates(sessions, options = {}) {
1793
- const idleCutoffMs = options.idleCutoffMs ?? defaultBestMetricsIdleCutoffMs;
1794
- const activityMetrics = buildActivityMetrics(sessions, idleCutoffMs);
1795
- return {
1796
- bestConcurrentAgents: findBestConcurrentAgents(activityMetrics.perSubagentBlocks),
1797
- best24hRawBurn: findBestRollingWindowTotal(sessions.flatMap((session) => buildTokenDeltaPoints(session.tokenPoints).map((tokenDeltaPoint) => ({
1798
- timestamp: tokenDeltaPoint.timestamp,
1799
- value: tokenDeltaPoint.deltaUsage.totalTokens
1800
- })))),
1801
- best24hAgentSumMs: findBestRollingWindowOverlap(activityMetrics.perSubagentBlocks.flatMap((sessionBlocks) => sessionBlocks))
1802
- };
2582
+ function formatThreadLabel(sessionId) {
2583
+ return sessionId.slice(-6);
1803
2584
  }
1804
- function findBestConcurrentAgents(intervalGroups) {
1805
- const concurrencyEdges = intervalGroups.flatMap((intervalGroup) => intervalGroup.flatMap((interval) => [
1806
- { timestampMs: interval.start.getTime(), delta: 1 },
1807
- { timestampMs: interval.end.getTime(), delta: -1 }
1808
- ]));
1809
- if (concurrencyEdges.length === 0) {
1810
- return null;
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;
1811
2596
  }
1812
- concurrencyEdges.sort((leftEdge, rightEdge) => leftEdge.timestampMs - rightEdge.timestampMs);
1813
- let activeCount = 0;
1814
- let bestRecord = null;
1815
- let index = 0;
1816
- while (index < concurrencyEdges.length) {
1817
- const timestampMs = concurrencyEdges[index].timestampMs;
1818
- while (index < concurrencyEdges.length && concurrencyEdges[index].timestampMs === timestampMs) {
1819
- activeCount += concurrencyEdges[index].delta;
1820
- index += 1;
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();
1821
2607
  }
1822
- const nextTimestampMs = concurrencyEdges[index]?.timestampMs ?? timestampMs;
1823
- if (nextTimestampMs <= timestampMs || activeCount <= 0) {
1824
- continue;
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();
1825
2632
  }
1826
- if (!bestRecord || activeCount > bestRecord.value) {
1827
- bestRecord = {
1828
- value: activeCount,
1829
- observedAt: new Date(timestampMs),
1830
- windowStart: new Date(timestampMs),
1831
- windowEnd: new Date(nextTimestampMs)
1832
- };
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();
1833
2640
  }
2641
+ process.stdout.write(exitLiveScreenSequence);
1834
2642
  }
1835
- return bestRecord;
1836
2643
  }
1837
-
1838
- // src/best-metrics/read-all-codex-sessions.ts
1839
- import { readdir as readdir2 } from "node:fs/promises";
1840
- import { homedir as homedir4 } from "node:os";
1841
- import { join as join4 } from "node:path";
1842
- var defaultSessionRootDirectory2 = join4(homedir4(), ".codex", "sessions");
1843
- async function readAllCodexSessions(options = {}) {
1844
- const sessionRootDirectory = options.sessionRootDirectory ?? defaultSessionRootDirectory2;
1845
- const sessionFiles = await listAllSessionFiles(sessionRootDirectory);
1846
- const parsedSessionResults = await Promise.allSettled(sessionFiles.map((sessionFilePath) => parseCodexSession(sessionFilePath)));
1847
- const parsedSessions = parsedSessionResults.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
1848
- return parsedSessions.sort((leftSession, rightSession) => leftSession.firstTimestamp.getTime() - rightSession.firstTimestamp.getTime());
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
+ });
1849
2659
  }
1850
- async function listAllSessionFiles(rootDirectory) {
1851
- const pendingDirectories = [rootDirectory];
1852
- const sessionFiles = [];
1853
- while (pendingDirectories.length > 0) {
1854
- const currentDirectory = pendingDirectories.pop();
1855
- const directoryEntries = await readDirectoryEntries2(currentDirectory);
1856
- for (const directoryEntry of directoryEntries) {
1857
- const entryPath = join4(currentDirectory, directoryEntry.name);
1858
- if (directoryEntry.isDirectory()) {
1859
- pendingDirectories.push(entryPath);
1860
- continue;
1861
- }
1862
- if (directoryEntry.isFile() && directoryEntry.name.endsWith(".jsonl")) {
1863
- sessionFiles.push(entryPath);
1864
- }
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`);
1865
2668
  }
2669
+ process.stdout.write("\x1B[2K");
2670
+ process.stdout.write(line);
1866
2671
  }
1867
- return sessionFiles.sort();
1868
- }
1869
- async function readDirectoryEntries2(directoryPath) {
1870
- try {
1871
- return await readdir2(directoryPath, { withFileTypes: true });
1872
- } catch (error) {
1873
- if (isMissingDirectoryError2(error)) {
1874
- return [];
1875
- }
1876
- throw error;
2672
+ for (let lineIndex = frameLines.length;lineIndex < previousFrameLineCount; lineIndex += 1) {
2673
+ process.stdout.write(`
2674
+ \r\x1B[2K`);
1877
2675
  }
2676
+ return frameLines.length;
1878
2677
  }
1879
- function isMissingDirectoryError2(error) {
1880
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2678
+ function waitForNextRefresh() {
2679
+ return new Promise((resolve) => {
2680
+ setTimeout(resolve, liveRefreshIntervalMs);
2681
+ });
1881
2682
  }
1882
2683
 
1883
- // src/best-metrics/read-best-ledger.ts
1884
- import { readFile as readFile3 } from "node:fs/promises";
1885
- import { homedir as homedir5 } from "node:os";
1886
- import { join as join5 } from "node:path";
1887
- var bestLedgerFileName = "bests-v1.json";
1888
- async function readBestLedger(options = {}) {
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();
1889
2706
  try {
1890
- const rawLedgerText = await readFile3(resolveBestLedgerPath(options), "utf8");
1891
- return parseBestLedger(JSON.parse(rawLedgerText));
2707
+ await execFileAsync("terminal-notifier", [
2708
+ "-title",
2709
+ notification.title,
2710
+ "-message",
2711
+ notification.body,
2712
+ ...notificationIconPath ? ["-appIcon", pathToFileURL(notificationIconPath).href] : []
2713
+ ]);
2714
+ return;
1892
2715
  } catch (error) {
1893
- if (isMissingFileError(error)) {
1894
- return null;
2716
+ if (!isCommandMissingError(error)) {
2717
+ throw error;
1895
2718
  }
1896
- throw error;
1897
2719
  }
2720
+ await execFileAsync("osascript", [
2721
+ "-e",
2722
+ `display notification "${escapeAppleScriptText(notification.body)}" with title "${escapeAppleScriptText(notification.title)}"`
2723
+ ]);
1898
2724
  }
1899
- function parseBestLedger(value) {
1900
- const ledgerRecord = expectObject(value, "bestMetricsLedger");
1901
- const version = readNumber(ledgerRecord, "version", "bestMetricsLedger");
1902
- if (version !== bestMetricsLedgerVersion) {
1903
- throw new Error(`bestMetricsLedger.version must be ${bestMetricsLedgerVersion}.`);
1904
- }
1905
- return {
1906
- version: bestMetricsLedgerVersion,
1907
- initializedAt: readIsoTimestamp(ledgerRecord.initializedAt, "bestMetricsLedger.initializedAt"),
1908
- lastScannedAt: readIsoTimestamp(ledgerRecord.lastScannedAt, "bestMetricsLedger.lastScannedAt"),
1909
- bestConcurrentAgents: parseBestMetricRecord(ledgerRecord.bestConcurrentAgents, "bestMetricsLedger.bestConcurrentAgents"),
1910
- best24hRawBurn: parseBestMetricRecord(ledgerRecord.best24hRawBurn, "bestMetricsLedger.best24hRawBurn"),
1911
- best24hAgentSumMs: parseBestMetricRecord(ledgerRecord.best24hAgentSumMs, "bestMetricsLedger.best24hAgentSumMs")
1912
- };
2725
+ function escapeAppleScriptText(value) {
2726
+ return value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"");
1913
2727
  }
1914
- function resolveBestLedgerPath(options = {}) {
1915
- return join5(resolveBestStateDirectory2(options), bestLedgerFileName);
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;
1916
2734
  }
1917
- function serializeBestLedger(ledger) {
1918
- const serializedLedger = {
1919
- version: ledger.version,
1920
- initializedAt: ledger.initializedAt.toISOString(),
1921
- lastScannedAt: ledger.lastScannedAt.toISOString(),
1922
- bestConcurrentAgents: serializeBestMetricRecord(ledger.bestConcurrentAgents),
1923
- best24hRawBurn: serializeBestMetricRecord(ledger.best24hRawBurn),
1924
- best24hAgentSumMs: serializeBestMetricRecord(ledger.best24hAgentSumMs)
1925
- };
1926
- return `${JSON.stringify(serializedLedger, null, 2)}
1927
- `;
2735
+ function isCommandMissingError(error) {
2736
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1928
2737
  }
1929
- function parseBestMetricRecord(value, label) {
1930
- if (value === null || value === undefined) {
1931
- return null;
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 [];
1932
2750
  }
1933
- const record = expectObject(value, label);
1934
- return {
1935
- value: readNumber(record, "value", label),
1936
- observedAt: readIsoTimestamp(record.observedAt, `${label}.observedAt`),
1937
- windowStart: readIsoTimestamp(record.windowStart, `${label}.windowStart`),
1938
- windowEnd: readIsoTimestamp(record.windowEnd, `${label}.windowEnd`)
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
+ }
1939
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
+ }
1940
2785
  }
1941
- function serializeBestMetricRecord(record) {
1942
- if (!record) {
1943
- return null;
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}.`);
1944
2791
  }
2792
+ const lastNotifiedAtRecord = expectObject(stateRecord.lastNotifiedAt, "nearBestNotificationState.lastNotifiedAt");
1945
2793
  return {
1946
- value: record.value,
1947
- observedAt: record.observedAt.toISOString(),
1948
- windowStart: record.windowStart.toISOString(),
1949
- windowEnd: record.windowEnd.toISOString()
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
+ }
1950
2803
  };
1951
2804
  }
1952
- function resolveBestStateDirectory2(options) {
1953
- return options.stateDirectory ?? join5(homedir5(), ".idletime");
1954
- }
1955
- function isMissingFileError(error) {
1956
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1957
- }
1958
-
1959
- // src/best-metrics/write-best-ledger.ts
1960
- import { mkdir as mkdir3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
1961
- import { join as join6 } from "node:path";
1962
- async function writeBestLedger(ledger, options = {}) {
1963
- const ledgerPath = resolveBestLedgerPath(options);
1964
- const stateDirectory = options.stateDirectory ?? ledgerPath.slice(0, ledgerPath.lastIndexOf("/"));
1965
- await mkdir3(stateDirectory, { recursive: true });
1966
- const temporaryPath = join6(stateDirectory, `.bests-v1.${process.pid}.${Date.now()}.tmp`);
1967
- await writeFile2(temporaryPath, serializeBestLedger(ledger), "utf8");
1968
- await rename2(temporaryPath, ledgerPath);
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);
1969
2823
  }
1970
-
1971
- // src/best-metrics/refresh-best-metrics.ts
1972
- async function refreshBestMetrics(options = {}) {
1973
- const refreshedAt = options.now ?? new Date;
1974
- const existingLedger = await readBestLedger(options);
1975
- const sessions = await readAllCodexSessions({
1976
- sessionRootDirectory: options.sessionRootDirectory
1977
- });
1978
- const bestMetricCandidates = buildBestMetricCandidates(sessions);
1979
- const currentMetrics = buildCurrentBestMetricValues(sessions, {
1980
- now: refreshedAt
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;
1981
2843
  });
1982
- if (!existingLedger) {
1983
- const bootstrappedLedger = {
1984
- version: bestMetricsLedgerVersion,
1985
- initializedAt: refreshedAt,
1986
- lastScannedAt: refreshedAt,
1987
- ...bestMetricCandidates
1988
- };
1989
- await writeBestLedger(bootstrappedLedger, options);
1990
- return {
1991
- currentMetrics,
1992
- ledger: bootstrappedLedger,
1993
- newBestEvents: [],
1994
- refreshMode: "bootstrap"
1995
- };
1996
- }
1997
- const newBestEvents = buildNewBestEvents(existingLedger, bestMetricCandidates);
1998
- const refreshedLedger = {
1999
- ...existingLedger,
2000
- lastScannedAt: refreshedAt,
2001
- ...mergeBestMetricCandidates(existingLedger, bestMetricCandidates)
2002
- };
2003
- await writeBestLedger(refreshedLedger, options);
2004
- await appendBestEvents(newBestEvents, options);
2844
+ }
2845
+ function buildNearBestNotification(metric, currentValue, bestValue) {
2005
2846
  return {
2006
- currentMetrics,
2007
- ledger: refreshedLedger,
2008
- newBestEvents,
2009
- refreshMode: "refresh"
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`
2010
2849
  };
2011
2850
  }
2012
- function mergeBestMetricCandidates(currentLedger, candidateLedger) {
2851
+ function createDefaultNearBestNotificationState() {
2013
2852
  return {
2014
- bestConcurrentAgents: pickBetterRecord(currentLedger.bestConcurrentAgents, candidateLedger.bestConcurrentAgents),
2015
- best24hRawBurn: pickBetterRecord(currentLedger.best24hRawBurn, candidateLedger.best24hRawBurn),
2016
- best24hAgentSumMs: pickBetterRecord(currentLedger.best24hAgentSumMs, candidateLedger.best24hAgentSumMs)
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
+ }
2017
2862
  };
2018
2863
  }
2019
- function pickBetterRecord(currentRecord, candidateRecord) {
2020
- if (!candidateRecord) {
2021
- return currentRecord;
2022
- }
2023
- if (!currentRecord || candidateRecord.value > currentRecord.value) {
2024
- return candidateRecord;
2025
- }
2026
- return currentRecord;
2027
- }
2028
- function buildNewBestEvents(currentLedger, candidateLedger) {
2029
- return [
2030
- buildNewBestEvent("bestConcurrentAgents", currentLedger.bestConcurrentAgents, candidateLedger.bestConcurrentAgents),
2031
- buildNewBestEvent("best24hRawBurn", currentLedger.best24hRawBurn, candidateLedger.best24hRawBurn),
2032
- buildNewBestEvent("best24hAgentSumMs", currentLedger.best24hAgentSumMs, candidateLedger.best24hAgentSumMs)
2033
- ].flatMap((bestEvent) => bestEvent ? [bestEvent] : []);
2864
+ function resolveNearBestNotificationStatePath(options) {
2865
+ return join3(options.stateDirectory ?? join3(homedir3(), ".idletime"), nearBestNotificationStateFileName);
2034
2866
  }
2035
- function buildNewBestEvent(metric, currentRecord, candidateRecord) {
2036
- if (!candidateRecord || currentRecord !== null && candidateRecord.value <= currentRecord.value) {
2867
+ function readOptionalIsoTimestamp(value, label) {
2868
+ if (value === null || value === undefined) {
2037
2869
  return null;
2038
2870
  }
2039
- return {
2040
- metric,
2041
- previousValue: currentRecord?.value ?? null,
2042
- value: candidateRecord.value,
2043
- observedAt: candidateRecord.observedAt,
2044
- windowStart: candidateRecord.windowStart,
2045
- windowEnd: candidateRecord.windowEnd,
2046
- version: bestMetricsLedgerVersion
2047
- };
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));
2048
2885
  }
2049
2886
 
2050
- // src/reporting/build-summary-report.ts
2051
- function buildSummaryReport(sessions, query) {
2052
- const filteredSessions = filterSessions(sessions, query.filters);
2053
- const windowInterval = {
2054
- start: query.window.start,
2055
- end: query.window.end
2056
- };
2057
- const metrics = clipActivityMetricsToWindow(buildActivityMetrics(filteredSessions, query.idleCutoffMs), windowInterval);
2058
- const comparisonCutoffMs = parseDurationToMs("30m");
2059
- const sessionCounts = {
2060
- total: filteredSessions.length,
2061
- direct: filteredSessions.filter((session) => session.kind === "direct").length,
2062
- subagent: filteredSessions.filter((session) => session.kind === "subagent").length
2063
- };
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) {
2064
2892
  return {
2065
- activityWindow: resolveActivityWindow(filteredSessions, windowInterval),
2066
- appliedFilters: query.filters,
2067
- comparisonCutoffMs,
2068
- comparisonMetrics: query.idleCutoffMs === comparisonCutoffMs ? metrics : clipActivityMetricsToWindow(buildActivityMetrics(filteredSessions, comparisonCutoffMs), windowInterval),
2069
- directTokenTotals: sumTokenTotals(filteredSessions.filter((session) => session.kind === "direct"), windowInterval),
2070
- groupBreakdowns: buildGroupBreakdowns(filteredSessions, query.groupBy, query.idleCutoffMs, windowInterval),
2071
- idleCutoffMs: query.idleCutoffMs,
2072
- metrics,
2073
- sessionCounts,
2074
- tokenTotals: sumTokenTotals(filteredSessions, windowInterval),
2075
- wakeSummary: query.wakeWindow ? summarizeWakeWindow(query.wakeWindow, query.window, metrics) : null,
2076
- window: query.window
2893
+ title: resolveNotificationTitle(bestEvent.metric),
2894
+ body: resolveNotificationBody(bestEvent)
2077
2895
  };
2078
2896
  }
2079
- function buildGroupBreakdowns(sessions, dimensions, idleCutoffMs, windowInterval) {
2080
- return dimensions.map((dimension) => ({
2081
- dimension,
2082
- rows: groupSessions(sessions, dimension).map((groupedSessions) => buildGroupRow(groupedSessions.key, groupedSessions.sessions, idleCutoffMs, windowInterval))
2083
- }));
2897
+ function resolveNotificationTitle(bestMetricKey) {
2898
+ return bestMetricKey === "bestConcurrentAgents" ? "New best concurrent agents" : bestMetricKey === "best24hRawBurn" ? "New best 24hr raw burn" : "New best agent sum";
2084
2899
  }
2085
- function buildGroupRow(key, sessions, idleCutoffMs, windowInterval) {
2086
- const metrics = clipActivityMetricsToWindow(buildActivityMetrics(sessions, idleCutoffMs), windowInterval);
2087
- const tokenTotals = sumTokenTotals(sessions, windowInterval);
2088
- return {
2089
- key,
2090
- sessionCount: sessions.length,
2091
- directActivityMs: metrics.directActivityMs,
2092
- agentCoverageMs: metrics.agentCoverageMs,
2093
- cumulativeAgentMs: metrics.cumulativeAgentMs,
2094
- practicalBurn: tokenTotals.practicalBurn,
2095
- rawTotalTokens: tokenTotals.rawTotalTokens
2096
- };
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`;
2097
2902
  }
2098
- function sumTokenTotals(sessions, windowInterval) {
2099
- return sessions.reduce((tokenTotals, session) => {
2100
- const sessionWindowTotals = buildTokenDeltaPoints(session.tokenPoints).filter((tokenDeltaPoint) => tokenDeltaPoint.timestamp.getTime() >= windowInterval.start.getTime() && tokenDeltaPoint.timestamp.getTime() <= windowInterval.end.getTime()).reduce((sessionTotals, tokenDeltaPoint) => ({
2101
- practicalBurn: sessionTotals.practicalBurn + tokenDeltaPoint.deltaUsage.practicalBurn,
2102
- rawTotalTokens: sessionTotals.rawTotalTokens + tokenDeltaPoint.deltaUsage.totalTokens
2103
- }), { practicalBurn: 0, rawTotalTokens: 0 });
2104
- return {
2105
- practicalBurn: tokenTotals.practicalBurn + sessionWindowTotals.practicalBurn,
2106
- rawTotalTokens: tokenTotals.rawTotalTokens + sessionWindowTotals.rawTotalTokens
2107
- };
2108
- }, {
2109
- practicalBurn: 0,
2110
- rawTotalTokens: 0
2111
- });
2903
+ function formatAgentSumHours3(durationMs) {
2904
+ const hours = durationMs / 3600000;
2905
+ return hours >= 10 ? Math.round(hours).toString() : (Math.round(hours * 10) / 10).toString();
2112
2906
  }
2113
- function resolveActivityWindow(sessions, windowInterval) {
2114
- if (sessions.length === 0) {
2115
- return null;
2116
- }
2117
- const firstTimestamp = sessions.reduce((earliestTimestamp, session) => session.firstTimestamp.getTime() < earliestTimestamp.getTime() ? session.firstTimestamp : earliestTimestamp, sessions[0].firstTimestamp);
2118
- const lastTimestamp = sessions.reduce((latestTimestamp, session) => session.lastTimestamp.getTime() > latestTimestamp.getTime() ? session.lastTimestamp : latestTimestamp, sessions[0].lastTimestamp);
2119
- return {
2120
- start: new Date(Math.max(firstTimestamp.getTime(), windowInterval.start.getTime())),
2121
- end: new Date(Math.min(lastTimestamp.getTime(), windowInterval.end.getTime()))
2122
- };
2907
+ function formatCompactInteger3(value) {
2908
+ return new Intl.NumberFormat("en-US", {
2909
+ notation: "compact",
2910
+ maximumFractionDigits: 1
2911
+ }).format(Math.round(value)).toUpperCase();
2123
2912
  }
2124
- function clipActivityMetricsToWindow(metrics, windowInterval) {
2125
- const strictEngagementBlocks = intersectTimeIntervals(metrics.strictEngagementBlocks, [windowInterval]);
2126
- const directActivityBlocks = intersectTimeIntervals(metrics.directActivityBlocks, [windowInterval]);
2127
- const agentCoverageBlocks = intersectTimeIntervals(metrics.agentCoverageBlocks, [windowInterval]);
2128
- const agentOnlyBlocks = intersectTimeIntervals(metrics.agentOnlyBlocks, [windowInterval]);
2129
- const perSubagentBlocks = metrics.perSubagentBlocks.map((sessionBlocks) => intersectTimeIntervals(sessionBlocks, [windowInterval]));
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
+ };
2130
2926
  return {
2131
- strictEngagementBlocks,
2132
- directActivityBlocks,
2133
- agentCoverageBlocks,
2134
- agentOnlyBlocks,
2135
- perSubagentBlocks,
2136
- strictEngagementMs: sumTimeIntervalsMs(strictEngagementBlocks),
2137
- directActivityMs: sumTimeIntervalsMs(directActivityBlocks),
2138
- agentCoverageMs: sumTimeIntervalsMs(agentCoverageBlocks),
2139
- agentOnlyMs: sumTimeIntervalsMs(agentOnlyBlocks),
2140
- cumulativeAgentMs: perSubagentBlocks.reduce((totalDurationMs, sessionBlocks) => totalDurationMs + sumTimeIntervalsMs(sessionBlocks), 0),
2141
- peakConcurrentAgents: peakConcurrency(perSubagentBlocks)
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)
2142
2930
  };
2143
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
+ }
2144
2935
 
2145
- // src/reporting/render-summary-report.ts
2146
- var summaryBarWidth = 18;
2147
- function renderSummaryReport(report, options, hourlyReport, bestPlaque = null) {
2148
- return options.shareMode ? renderShareSummaryReport(report, options, hourlyReport, bestPlaque) : renderFullSummaryReport(report, options, hourlyReport, bestPlaque);
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");
2149
2950
  }
2150
- function renderFullSummaryReport(report, options, hourlyReport, bestPlaque = null) {
2151
- const lines = [];
2152
- const requestedMetrics = report.metrics;
2153
- const actualComparisonMetrics = report.comparisonMetrics;
2154
- const windowDurationMs = report.window.end.getTime() - report.window.start.getTime();
2155
- const headerLines = buildSummaryHeaderLines(report, hourlyReport);
2156
- const panelLines = renderPanel(`idletime • ${report.window.label}`, headerLines, options);
2157
- const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
2158
- const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
2159
- lines.push(...buildLogoSection(logoSectionWidth, options, bestPlaque));
2160
- lines.push("");
2161
- lines.push(...panelLines);
2162
- if (hourlyReport) {
2163
- lines.push("");
2164
- lines.push(...buildRhythmSection(hourlyReport, options));
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;
2165
2971
  }
2166
- lines.push("");
2167
- lines.push(...renderSectionTitle("Activity", options));
2168
- lines.push(renderMetricRow("strict", requestedMetrics.strictEngagementMs, windowDurationMs, formatDurationHours(requestedMetrics.strictEngagementMs), `${formatSignedDurationHours(actualComparisonMetrics.strictEngagementMs - requestedMetrics.strictEngagementMs)} at ${formatDurationLabel(report.comparisonCutoffMs)}`, "█", "focus", options));
2169
- lines.push(renderMetricRow("direct", requestedMetrics.directActivityMs, windowDurationMs, formatDurationHours(requestedMetrics.directActivityMs), `${formatSignedDurationHours(actualComparisonMetrics.directActivityMs - requestedMetrics.directActivityMs)} at ${formatDurationLabel(report.comparisonCutoffMs)}`, "▓", "active", options));
2170
- lines.push(renderMetricRow("agent live", requestedMetrics.agentCoverageMs, windowDurationMs, formatDurationHours(requestedMetrics.agentCoverageMs), "coverage", "▒", "agent", options));
2171
- lines.push(renderMetricRow("agent sum", requestedMetrics.cumulativeAgentMs, Math.max(windowDurationMs, requestedMetrics.cumulativeAgentMs), formatDurationHours(requestedMetrics.cumulativeAgentMs), `peak ${requestedMetrics.peakConcurrentAgents} concurrent`, "▚", "agent", options));
2172
- lines.push(`${paint(padRight(" session mix", 14), "muted", options)} ${paint(buildSplitBar([
2173
- {
2174
- filledCharacter: "█",
2175
- value: report.sessionCounts.direct
2176
- },
2177
- {
2178
- filledCharacter: "▓",
2179
- value: report.sessionCounts.subagent
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;
2180
2982
  }
2181
- ], summaryBarWidth), "active", options)} ${paint(`${report.sessionCounts.direct} direct / ${report.sessionCounts.subagent} subagent`, "value", options)}`);
2182
- lines.push("");
2183
- lines.push(...renderSectionTitle("Tokens", options));
2184
- const maxBurnValue = Math.max(report.tokenTotals.practicalBurn, report.directTokenTotals.practicalBurn);
2185
- const maxRawValue = Math.max(report.tokenTotals.rawTotalTokens, report.directTokenTotals.rawTotalTokens);
2186
- lines.push(renderMetricRow("practical burn", report.tokenTotals.practicalBurn, maxBurnValue, formatCompactInteger(report.tokenTotals.practicalBurn), `${formatPercentage(report.tokenTotals.practicalBurn / report.tokenTotals.rawTotalTokens)} of raw`, "█", "burn", options, "burn"));
2187
- lines.push(renderMetricRow("all raw", report.tokenTotals.rawTotalTokens, maxRawValue, formatCompactInteger(report.tokenTotals.rawTotalTokens), `${formatInteger(report.tokenTotals.rawTotalTokens)} total`, "█", "raw", options, "raw"));
2188
- lines.push(renderMetricRow("direct burn", report.directTokenTotals.practicalBurn, maxBurnValue, formatCompactInteger(report.directTokenTotals.practicalBurn), `${formatPercentage(report.directTokenTotals.practicalBurn / report.tokenTotals.practicalBurn)} of burn`, "▒", "burn", options, "burn"));
2189
- lines.push(renderMetricRow("direct raw", report.directTokenTotals.rawTotalTokens, maxRawValue, formatCompactInteger(report.directTokenTotals.rawTotalTokens), `${formatPercentage(report.directTokenTotals.rawTotalTokens / report.tokenTotals.rawTotalTokens)} of raw`, "▒", "raw", options, "raw"));
2190
- if (report.wakeSummary) {
2191
- lines.push("");
2192
- lines.push(...renderSectionTitle("Wake Window", options));
2193
- lines.push(renderMetricRow("direct awake", report.wakeSummary.directActivityMs, report.wakeSummary.wakeDurationMs, formatDurationClock(report.wakeSummary.directActivityMs), `of ${formatDurationClock(report.wakeSummary.wakeDurationMs)} wake`, "▓", "active", options));
2194
- lines.push(renderMetricRow("strict awake", report.wakeSummary.strictEngagementMs, report.wakeSummary.wakeDurationMs, formatDurationClock(report.wakeSummary.strictEngagementMs), "engaged", "█", "focus", options));
2195
- lines.push(renderMetricRow("agent awake", report.wakeSummary.agentOnlyMs, report.wakeSummary.wakeDurationMs, formatDurationClock(report.wakeSummary.agentOnlyMs), "agent-only", "▒", "agent", options));
2196
- lines.push(renderMetricRow("awake idle", report.wakeSummary.awakeIdleMs, report.wakeSummary.wakeDurationMs, formatDurationClock(report.wakeSummary.awakeIdleMs), `${formatPercentage(report.wakeSummary.awakeIdlePercentage)} idle`, "░", "idle", options));
2197
- lines.push(`${paint(padRight(" longest gap", 14), "muted", options)} ${paint(formatDurationClock(report.wakeSummary.longestIdleGapMs), "value", options)} ${dim("largest quiet stretch", options)}`);
2198
- }
2199
- for (const groupBreakdown of report.groupBreakdowns) {
2200
- lines.push("");
2201
- lines.push(...renderSectionTitle(groupBreakdown.dimension === "model" ? "Model Breakdown" : "Effort Breakdown", options));
2202
- const maxBreakdownBurn = Math.max(...groupBreakdown.rows.map((row) => row.practicalBurn), 0);
2203
- for (const row of groupBreakdown.rows) {
2204
- lines.push(`${paint(padRight(` ${row.key}`, 20), "muted", options)} ${paint(buildBar(row.practicalBurn, maxBreakdownBurn, 14, "█"), "burn", options)} ${paint(padRight(formatCompactInteger(row.practicalBurn), 6), "value", options)} ${dim("burn", options)} ${paint(padRight(formatDurationCompact(row.directActivityMs), 5), "active", options)} ${dim("direct", options)} ${paint(padRight(formatDurationCompact(row.agentCoverageMs), 5), "agent", options)} ${dim("live", options)} ${paint(`${row.sessionCount} s`, "value", options)}`);
2983
+ if (currentTotal > bestValue) {
2984
+ bestValue = currentTotal;
2985
+ bestTimestampMs = rightPoint.timestamp.getTime();
2205
2986
  }
2206
2987
  }
2207
- return lines.join(`
2208
- `);
2209
- }
2210
- function renderShareSummaryReport(report, options, hourlyReport, bestPlaque = null) {
2211
- const lines = [];
2212
- const headerLines = buildSummaryHeaderLines(report, hourlyReport);
2213
- const panelLines = renderPanel(`idletime • ${report.window.label}`, headerLines, options);
2214
- const panelWidth = measureVisibleTextWidth(panelLines[0] ?? "");
2215
- const logoSectionWidth = resolveLogoSectionWidth(panelWidth, options);
2216
- lines.push(...buildLogoSection(logoSectionWidth, options, bestPlaque));
2217
- lines.push("");
2218
- lines.push(...panelLines);
2219
- if (hourlyReport) {
2220
- lines.push("");
2221
- lines.push(...buildRhythmSection(hourlyReport, options));
2988
+ if (bestValue === 0) {
2989
+ return null;
2222
2990
  }
2223
- lines.push("");
2224
- lines.push(...renderSectionTitle("Snapshot", options));
2225
- lines.push(renderSnapshotRow("focus", formatDurationHours(report.metrics.strictEngagementMs), "focused time", "focus", options));
2226
- lines.push(renderSnapshotRow("active", formatDurationHours(report.metrics.directActivityMs), "direct-session movement", "active", options));
2227
- lines.push(renderSnapshotRow(report.wakeSummary ? "idle" : "quiet", report.wakeSummary ? formatDurationClock(report.wakeSummary.awakeIdleMs) : hourlyReport ? formatDurationCompact(hourlyReport.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs), 0)) : "n/a", report.wakeSummary ? "awake idle" : "quiet hours", "idle", options));
2228
- lines.push(renderSnapshotRow("burn", formatCompactInteger(report.tokenTotals.practicalBurn), `${formatPercentage(report.tokenTotals.practicalBurn / report.tokenTotals.rawTotalTokens)} of raw`, "burn", options));
2229
- lines.push(renderSnapshotRow("agents", `${report.metrics.peakConcurrentAgents} peak`, `${formatDurationHours(report.metrics.cumulativeAgentMs)} cumulative`, "agent", options));
2230
- lines.push(renderSnapshotRow("sessions", `${report.sessionCounts.total}`, `${report.sessionCounts.direct} direct / ${report.sessionCounts.subagent} subagent`, "value", options));
2231
- return lines.join(`
2232
- `);
2991
+ return createRollingRecord(bestValue, bestTimestampMs);
2233
2992
  }
2234
- function formatAppliedFilters(report) {
2235
- const appliedFilters = [];
2236
- if (report.appliedFilters.workspaceOnlyPrefix) {
2237
- appliedFilters.push(`workspace=${shortenPath(report.appliedFilters.workspaceOnlyPrefix, 48)}`);
2238
- }
2239
- if (report.appliedFilters.sessionKind) {
2240
- appliedFilters.push(`kind=${report.appliedFilters.sessionKind}`);
2993
+ function findBestRollingWindowOverlap(intervals) {
2994
+ const slopeChanges = intervals.flatMap(buildSlopeChanges);
2995
+ if (slopeChanges.length === 0) {
2996
+ return null;
2241
2997
  }
2242
- if (report.appliedFilters.model) {
2243
- appliedFilters.push(`model=${report.appliedFilters.model}`);
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;
2244
3017
  }
2245
- if (report.appliedFilters.reasoningEffort) {
2246
- appliedFilters.push(`effort=${report.appliedFilters.reasoningEffort}`);
3018
+ if (bestValue === 0) {
3019
+ return null;
2247
3020
  }
2248
- return appliedFilters;
3021
+ return createRollingRecord(bestValue, bestTimestampMs);
2249
3022
  }
2250
- function buildSummaryHeaderLines(report, hourlyReport) {
2251
- if (!hourlyReport) {
2252
- return [
2253
- formatTimeRange(report.window.start, report.window.end, report.window),
2254
- `${report.sessionCounts.total} sessions · ${formatDurationHours(report.metrics.strictEngagementMs)} focused · ${formatCompactInteger(report.tokenTotals.practicalBurn)} tokens`,
2255
- ...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
2256
- ];
3023
+ function buildSlopeChanges(interval) {
3024
+ const startMs = interval.start.getTime();
3025
+ const endMs = interval.end.getTime();
3026
+ if (endMs <= startMs) {
3027
+ return [];
2257
3028
  }
2258
3029
  return [
2259
- buildPostureLine(report, hourlyReport),
2260
- buildBiggestStoryLine(report, hourlyReport),
2261
- buildSupportFactsLine(report),
2262
- ...formatAppliedFilters(report).map((filter) => `filter ${filter}`)
3030
+ { timestampMs: startMs, deltaSlope: 1 },
3031
+ { timestampMs: endMs, deltaSlope: -1 },
3032
+ { timestampMs: startMs + rollingWindowDurationMs, deltaSlope: -1 },
3033
+ { timestampMs: endMs + rollingWindowDurationMs, deltaSlope: 1 }
2263
3034
  ];
2264
3035
  }
2265
- function buildPostureLine(report, hourlyReport) {
2266
- const directActivityMs = Math.max(report.metrics.directActivityMs, 1);
2267
- const focusRatio = report.metrics.strictEngagementMs / directActivityMs;
2268
- const agentCoverageRatio = report.metrics.agentCoverageMs / directActivityMs;
2269
- const quietRatio = sumQuietMs(hourlyReport) / Math.max(1, report.window.end.getTime() - report.window.start.getTime());
2270
- 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";
2271
- return `${posture}: ${formatDurationHours(report.metrics.strictEngagementMs)} focused, ${formatDurationHours(report.metrics.agentCoverageMs)} agent live`;
2272
- }
2273
- function buildBiggestStoryLine(report, hourlyReport) {
2274
- const longestQuietRun = findLongestQuietRun(hourlyReport);
2275
- const peakBurnBucket = hourlyReport.buckets.reduce((currentPeak, bucket) => bucket.practicalBurn > currentPeak.practicalBurn ? bucket : currentPeak, hourlyReport.buckets[0]);
2276
- const quietPhrase = longestQuietRun.durationMs >= 2 * 3600000 ? `long quiet stretch ${describeDayPeriod(longestQuietRun.start, report)}` : "steady rhythm overall";
2277
- return `Biggest story: ${quietPhrase}, big burn ${describeDayPeriod(peakBurnBucket.start, report)}`;
2278
- }
2279
- function buildSupportFactsLine(report) {
2280
- return `${report.sessionCounts.direct} direct / ${report.sessionCounts.subagent} subagent • ${report.metrics.peakConcurrentAgents} peak • ${formatCompactInteger(report.tokenTotals.practicalBurn)} burn`;
2281
- }
2282
- function sumQuietMs(hourlyReport) {
2283
- return hourlyReport.buckets.reduce((totalDurationMs, bucket) => totalDurationMs + Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs), 0);
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
+ };
2284
3043
  }
2285
- function findLongestQuietRun(hourlyReport) {
2286
- let longestQuietRun = {
2287
- durationMs: 0,
2288
- start: hourlyReport.buckets[0]?.start ?? new Date(0)
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))
2289
3056
  };
2290
- let currentStart = null;
2291
- let currentDurationMs = 0;
2292
- for (const bucket of hourlyReport.buckets) {
2293
- const quietMs = Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs);
2294
- const isQuietBucket = quietMs >= 30 * 60000;
2295
- if (isQuietBucket) {
2296
- currentStart ??= bucket.start;
2297
- currentDurationMs += quietMs;
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) {
2298
3078
  continue;
2299
3079
  }
2300
- if (currentStart && currentDurationMs > longestQuietRun.durationMs) {
2301
- longestQuietRun = {
2302
- durationMs: currentDurationMs,
2303
- start: currentStart
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)
2304
3086
  };
2305
3087
  }
2306
- currentStart = null;
2307
- currentDurationMs = 0;
2308
3088
  }
2309
- if (currentStart && currentDurationMs > longestQuietRun.durationMs) {
2310
- longestQuietRun = {
2311
- durationMs: currentDurationMs,
2312
- start: currentStart
2313
- };
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
+ }
2314
3120
  }
2315
- if (longestQuietRun.durationMs > 0) {
2316
- return longestQuietRun;
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;
2317
3131
  }
2318
- const quietestBucket = hourlyReport.buckets.reduce((currentQuietest, bucket) => {
2319
- const quietMs = Math.max(0, bucket.end.getTime() - bucket.start.getTime() - bucket.directActivityMs - bucket.agentOnlyMs);
2320
- return quietMs > currentQuietest.durationMs ? { durationMs: quietMs, start: bucket.start } : currentQuietest;
2321
- }, longestQuietRun);
2322
- return quietestBucket;
2323
3132
  }
2324
- function describeDayPeriod(timestamp, report) {
2325
- const hourOfDay = Number.parseInt(formatHourOfDay(timestamp, report.window), 10);
2326
- if (hourOfDay >= 21 || hourOfDay < 5) {
2327
- return "overnight";
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
+ };
2328
3174
  }
2329
- if (hourOfDay < 12) {
2330
- return "this morning";
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;
2331
3200
  }
2332
- if (hourOfDay < 17) {
2333
- return "this afternoon";
3201
+ if (!currentRecord || candidateRecord.value > currentRecord.value) {
3202
+ return candidateRecord;
2334
3203
  }
2335
- return "this evening";
2336
- }
2337
- function formatDurationLabel(durationMs) {
2338
- return `${Math.round(durationMs / 60000)}m`;
3204
+ return currentRecord;
2339
3205
  }
2340
- function renderMetricRow(label, value, maxValue, primaryText, detailText, filledCharacter, role, options, valueRole = "value") {
2341
- return `${paint(padRight(` ${label}`, 14), "muted", options)} ${paint(buildBar(value, maxValue, summaryBarWidth, filledCharacter), role, options)} ${paint(padRight(primaryText, 7), valueRole, options)} ${dim(detailText, options)}`;
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] : []);
2342
3212
  }
2343
- function renderSnapshotRow(label, primaryText, detailText, role, options) {
2344
- return `${paint(padRight(` ${label}`, 12), role, options)} ${paint(padRight(primaryText, 10), "value", options)} ${dim(detailText, options)}`;
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
+ };
2345
3226
  }
2346
3227
 
2347
- // src/cli/run-last24h-command.ts
2348
- async function runLast24hCommand(command) {
2349
- const window = resolveTrailingReportWindow({ durationMs: command.hourlyWindowMs });
2350
- const bestMetrics = await refreshBestMetrics();
2351
- await notifyBestEvents(bestMetrics.newBestEvents);
2352
- await notifyNearBestMetrics(bestMetrics.currentMetrics, bestMetrics.ledger);
2353
- const sessions = await readCodexSessions({
2354
- windowStart: window.start,
2355
- windowEnd: window.end
2356
- });
2357
- const summaryReport = buildSummaryReport(sessions, {
2358
- filters: command.filters,
2359
- groupBy: command.groupBy,
2360
- idleCutoffMs: command.idleCutoffMs,
2361
- wakeWindow: command.wakeWindow,
2362
- window
2363
- });
2364
- const hourlyReport = buildHourlyReport(sessions, {
2365
- filters: command.filters,
2366
- idleCutoffMs: command.idleCutoffMs,
2367
- wakeWindow: command.wakeWindow,
2368
- window
2369
- });
2370
- return renderSummaryReport(summaryReport, createRenderOptions(command.shareMode), hourlyReport, buildBestPlaque(bestMetrics.ledger));
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
+ `);
2371
3240
  }
2372
3241
 
2373
3242
  // src/cli/run-today-command.ts
2374
- async function runTodayCommand(command) {
2375
- const window = resolveTodayReportWindow();
2376
- const bestMetrics = await refreshBestMetrics();
2377
- await notifyBestEvents(bestMetrics.newBestEvents);
2378
- await notifyNearBestMetrics(bestMetrics.currentMetrics, bestMetrics.ledger);
2379
- const sessions = await readCodexSessions({
3243
+ async function buildTodayCommandResult(command, options = {}) {
3244
+ const window = resolveTodayReportWindow({ now: options.now });
3245
+ const bestLedgerPromise = readBestLedger({ stateDirectory: options.stateDirectory });
3246
+ const sessionsPromise = readCodexSessions({
2380
3247
  windowStart: window.start,
2381
- windowEnd: window.end
3248
+ windowEnd: window.end,
3249
+ sessionRootDirectory: options.sessionRootDirectory
2382
3250
  });
2383
- return renderSummaryReport(buildSummaryReport(sessions, {
2384
- filters: command.filters,
2385
- groupBy: command.groupBy,
2386
- idleCutoffMs: command.idleCutoffMs,
2387
- wakeWindow: command.wakeWindow,
2388
- window
2389
- }), createRenderOptions(command.shareMode), undefined, buildBestPlaque(bestMetrics.ledger));
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);
2390
3269
  }
2391
3270
 
2392
3271
  // src/cli/run-idletime.ts
@@ -2400,9 +3279,86 @@ async function runIdletimeCli(argv) {
2400
3279
  console.log(package_default.version);
2401
3280
  return;
2402
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
+ }
2403
3294
  const output = command.commandName === "hourly" ? await runHourlyCommand(command) : command.commandName === "today" ? await runTodayCommand(command) : await runLast24hCommand(command);
2404
3295
  console.log(output);
2405
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
+ }
2406
3362
 
2407
3363
  // src/cli/idletime-bin.ts
2408
3364
  await runIdletimeCli(process.argv.slice(2));