taskchef 7.26.0 → 7.27.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.
@@ -59,6 +59,12 @@ export interface UsageProjection {
59
59
  updatedAt?: string | null;
60
60
  }
61
61
 
62
+ export interface ReportedWorkSummary {
63
+ terminalTurns: number;
64
+ validTurns: number;
65
+ totalMilliseconds: number | null;
66
+ }
67
+
62
68
  export interface Task {
63
69
  schemaVersion?: number;
64
70
  id: string;
@@ -78,6 +84,7 @@ export interface Task {
78
84
  latestTurn: TaskTurn | null;
79
85
  turns?: TaskTurn[];
80
86
  results?: TaskResult[];
87
+ reportedWork?: ReportedWorkSummary;
81
88
  usage?: UsageProjection | null;
82
89
  relatedGitHubLinks?: GitHubLink[];
83
90
  relatedGitHubLinksTruncated?: boolean;
package/src/dashboard.js CHANGED
@@ -27,6 +27,7 @@ import { taskGitHubProjection } from "./dashboard/github-links.js";
27
27
  import { CODEX_CHAT_ARCHIVE_ENABLED } from "./dashboard/state.js";
28
28
  import { createUsageTracker } from "./usage-tracker.js";
29
29
  import { readUsageStore, usageStorePath } from "./usage.js";
30
+ import { reportedWorkSummary } from "./reported-work.js";
30
31
  import {
31
32
  MAX_DASHBOARD_SESSION_PIDS,
32
33
  MAX_TRANSFERRED_DASHBOARD_SESSION_PIDS,
@@ -229,6 +230,7 @@ function taskListProjection(task) {
229
230
  const { turns: _turns, results: _results, ...projection } = {
230
231
  ...task,
231
232
  ...taskGitHubProjection(task),
233
+ reportedWork: reportedWorkSummary(task.turns ?? []),
232
234
  };
233
235
  return projection;
234
236
  }
@@ -0,0 +1,61 @@
1
+ const LIFECYCLE_TIMESTAMP_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(Z|[+-]\d{2}:\d{2})$/;
2
+
3
+ export function durationBetween(startedAt, endedAt) {
4
+ const start = parseLifecycleTimestamp(startedAt);
5
+ const end = parseLifecycleTimestamp(endedAt);
6
+ if (start === null || end === null) return null;
7
+ const duration = end - start;
8
+ return duration >= 0 ? duration : null;
9
+ }
10
+
11
+ export function terminalTurnDuration(turn) {
12
+ const duration = durationBetween(turn.startedAt, turn.result?.updatedAt);
13
+ return duration === 0 ? null : duration;
14
+ }
15
+
16
+ export function reportedWorkSummary(turns) {
17
+ const terminalTurns = turns.filter((turn) => turn.result !== null);
18
+ const durations = terminalTurns
19
+ .map(terminalTurnDuration)
20
+ .filter((duration) => duration !== null);
21
+ const totalMilliseconds = durations.length === 0
22
+ ? terminalTurns.length === 0 ? 0 : null
23
+ : durations.reduce((total, duration) => total + duration, 0);
24
+ return Object.freeze({
25
+ terminalTurns: terminalTurns.length,
26
+ validTurns: durations.length,
27
+ totalMilliseconds: totalMilliseconds === null || Number.isSafeInteger(totalMilliseconds)
28
+ ? totalMilliseconds
29
+ : null,
30
+ });
31
+ }
32
+
33
+ function parseLifecycleTimestamp(value) {
34
+ if (typeof value !== "string") return null;
35
+ const match = value.match(LIFECYCLE_TIMESTAMP_PATTERN);
36
+ if (!match) return null;
37
+ const [, yearText, monthText, dayText, hourText, minuteText, secondText, , zone] = match;
38
+ const year = Number(yearText);
39
+ const month = Number(monthText);
40
+ const day = Number(dayText);
41
+ const hour = Number(hourText);
42
+ const minute = Number(minuteText);
43
+ const second = Number(secondText);
44
+ const zoneHour = zone === "Z" ? 0 : Number(zone.slice(1, 3));
45
+ const zoneMinute = zone === "Z" ? 0 : Number(zone.slice(4, 6));
46
+ const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
47
+ const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
48
+ if (
49
+ month < 1
50
+ || month > 12
51
+ || day < 1
52
+ || day > daysInMonth[month - 1]
53
+ || hour > 23
54
+ || minute > 59
55
+ || second > 59
56
+ || zoneHour > 23
57
+ || zoneMinute > 59
58
+ ) return null;
59
+ const parsed = Date.parse(value);
60
+ return Number.isFinite(parsed) ? parsed : null;
61
+ }