@cronwatch/sdk 0.6.1 → 0.8.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 (112) hide show
  1. package/README.md +2 -0
  2. package/dist/anthropic.cjs +13 -2
  3. package/dist/anthropic.cjs.map +1 -1
  4. package/dist/anthropic.d.cts +1 -1
  5. package/dist/anthropic.d.ts +1 -1
  6. package/dist/anthropic.js +13 -2
  7. package/dist/anthropic.js.map +1 -1
  8. package/dist/bugsnag.cjs +8 -1
  9. package/dist/bugsnag.cjs.map +1 -1
  10. package/dist/bugsnag.d.cts +1 -1
  11. package/dist/bugsnag.d.ts +1 -1
  12. package/dist/bugsnag.js +8 -1
  13. package/dist/bugsnag.js.map +1 -1
  14. package/dist/{client-B7bqnkI5.d.cts → client-B5hM4l-D.d.ts} +5 -2
  15. package/dist/{client-pmqH3apS.d.ts → client-Di5HAWQz.d.cts} +5 -2
  16. package/dist/d1.cjs +4 -1
  17. package/dist/d1.cjs.map +1 -1
  18. package/dist/d1.d.cts +1 -1
  19. package/dist/d1.d.ts +1 -1
  20. package/dist/d1.js +4 -1
  21. package/dist/d1.js.map +1 -1
  22. package/dist/datadog.cjs.map +1 -1
  23. package/dist/datadog.d.cts +1 -1
  24. package/dist/datadog.d.ts +1 -1
  25. package/dist/datadog.js.map +1 -1
  26. package/dist/discord.cjs.map +1 -1
  27. package/dist/discord.d.cts +1 -1
  28. package/dist/discord.d.ts +1 -1
  29. package/dist/discord.js.map +1 -1
  30. package/dist/{email-_H0faCh3.d.ts → email-Ciq7GRn4.d.ts} +1 -1
  31. package/dist/{email-BJ-z_epD.d.cts → email-DX6gkd5H.d.cts} +1 -1
  32. package/dist/honeybadger.cjs +8 -1
  33. package/dist/honeybadger.cjs.map +1 -1
  34. package/dist/honeybadger.d.cts +1 -1
  35. package/dist/honeybadger.d.ts +1 -1
  36. package/dist/honeybadger.js +8 -1
  37. package/dist/honeybadger.js.map +1 -1
  38. package/dist/index.cjs +98 -22
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +4 -4
  41. package/dist/index.d.ts +4 -4
  42. package/dist/index.js +98 -22
  43. package/dist/index.js.map +1 -1
  44. package/dist/mailgun.cjs.map +1 -1
  45. package/dist/mailgun.d.cts +2 -2
  46. package/dist/mailgun.d.ts +2 -2
  47. package/dist/mailgun.js.map +1 -1
  48. package/dist/newrelic.cjs.map +1 -1
  49. package/dist/newrelic.d.cts +1 -1
  50. package/dist/newrelic.d.ts +1 -1
  51. package/dist/newrelic.js.map +1 -1
  52. package/dist/pg-cron.cjs +8 -1
  53. package/dist/pg-cron.cjs.map +1 -1
  54. package/dist/pg-cron.d.cts +2 -2
  55. package/dist/pg-cron.d.ts +2 -2
  56. package/dist/pg-cron.js +8 -1
  57. package/dist/pg-cron.js.map +1 -1
  58. package/dist/postgres.cjs +6 -1
  59. package/dist/postgres.cjs.map +1 -1
  60. package/dist/postgres.d.cts +1 -1
  61. package/dist/postgres.d.ts +1 -1
  62. package/dist/postgres.js +6 -1
  63. package/dist/postgres.js.map +1 -1
  64. package/dist/postmark.cjs.map +1 -1
  65. package/dist/postmark.d.cts +2 -2
  66. package/dist/postmark.d.ts +2 -2
  67. package/dist/postmark.js.map +1 -1
  68. package/dist/resend.cjs.map +1 -1
  69. package/dist/resend.d.cts +2 -2
  70. package/dist/resend.d.ts +2 -2
  71. package/dist/resend.js.map +1 -1
  72. package/dist/rollbar.cjs +8 -1
  73. package/dist/rollbar.cjs.map +1 -1
  74. package/dist/rollbar.d.cts +1 -1
  75. package/dist/rollbar.d.ts +1 -1
  76. package/dist/rollbar.js +8 -1
  77. package/dist/rollbar.js.map +1 -1
  78. package/dist/sendgrid.cjs.map +1 -1
  79. package/dist/sendgrid.d.cts +2 -2
  80. package/dist/sendgrid.d.ts +2 -2
  81. package/dist/sendgrid.js.map +1 -1
  82. package/dist/sentry.cjs +8 -1
  83. package/dist/sentry.cjs.map +1 -1
  84. package/dist/sentry.d.cts +1 -1
  85. package/dist/sentry.d.ts +1 -1
  86. package/dist/sentry.js +8 -1
  87. package/dist/sentry.js.map +1 -1
  88. package/dist/ses.cjs.map +1 -1
  89. package/dist/ses.d.cts +2 -2
  90. package/dist/ses.d.ts +2 -2
  91. package/dist/ses.js.map +1 -1
  92. package/dist/slack.cjs.map +1 -1
  93. package/dist/slack.d.cts +1 -1
  94. package/dist/slack.d.ts +1 -1
  95. package/dist/slack.js.map +1 -1
  96. package/dist/sqlite.cjs +4 -1
  97. package/dist/sqlite.cjs.map +1 -1
  98. package/dist/sqlite.d.cts +1 -1
  99. package/dist/sqlite.d.ts +1 -1
  100. package/dist/sqlite.js +4 -1
  101. package/dist/sqlite.js.map +1 -1
  102. package/dist/twilio.cjs.map +1 -1
  103. package/dist/twilio.d.cts +1 -1
  104. package/dist/twilio.d.ts +1 -1
  105. package/dist/twilio.js.map +1 -1
  106. package/dist/{types-Ddq1MUPL.d.cts → types-BjBbCGeJ.d.cts} +4 -1
  107. package/dist/{types-Ddq1MUPL.d.ts → types-BjBbCGeJ.d.ts} +4 -1
  108. package/dist/webhook.cjs.map +1 -1
  109. package/dist/webhook.d.cts +1 -1
  110. package/dist/webhook.d.ts +1 -1
  111. package/dist/webhook.js.map +1 -1
  112. package/package.json +1 -1
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { C as CronWatchOptions, a as CronWatch } from './client-B7bqnkI5.cjs';
2
- export { F as FetchHandler, H as HandlerFn, b as HandlerOptions, J as JobContext, c as JobFn, d as JobHandle, R as RecordRunOptions, e as Routes, f as RoutesOptions, g as RunHandle, h as RunOutcome, S as Source, i as SourceHost, j as StartOptions, k as consoleChannel, l as createRoutes, m as custom } from './client-B7bqnkI5.cjs';
3
- import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-Ddq1MUPL.cjs';
4
- export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, J as JobDefinition, h as JobHealth, i as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as StoredJob, T as TriageContext, n as TriageFn } from './types-Ddq1MUPL.cjs';
1
+ import { C as CronWatchOptions, a as CronWatch } from './client-Di5HAWQz.cjs';
2
+ export { F as FetchHandler, H as HandlerFn, b as HandlerOptions, J as JobContext, c as JobFn, d as JobHandle, R as RecordRunOptions, e as Routes, f as RoutesOptions, g as RunHandle, h as RunOutcome, S as Source, i as SourceHost, j as StartOptions, k as consoleChannel, l as createRoutes, m as custom } from './client-Di5HAWQz.cjs';
3
+ import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-BjBbCGeJ.cjs';
4
+ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, J as JobDefinition, h as JobHealth, i as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as StoredJob, T as TriageContext, n as TriageFn } from './types-BjBbCGeJ.cjs';
5
5
 
6
6
  /**
7
7
  * Keeps everything in process memory. The default when no store is given,
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { C as CronWatchOptions, a as CronWatch } from './client-pmqH3apS.js';
2
- export { F as FetchHandler, H as HandlerFn, b as HandlerOptions, J as JobContext, c as JobFn, d as JobHandle, R as RecordRunOptions, e as Routes, f as RoutesOptions, g as RunHandle, h as RunOutcome, S as Source, i as SourceHost, j as StartOptions, k as consoleChannel, l as createRoutes, m as custom } from './client-pmqH3apS.js';
3
- import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-Ddq1MUPL.js';
4
- export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, J as JobDefinition, h as JobHealth, i as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as StoredJob, T as TriageContext, n as TriageFn } from './types-Ddq1MUPL.js';
1
+ import { C as CronWatchOptions, a as CronWatch } from './client-B5hM4l-D.js';
2
+ export { F as FetchHandler, H as HandlerFn, b as HandlerOptions, J as JobContext, c as JobFn, d as JobHandle, R as RecordRunOptions, e as Routes, f as RoutesOptions, g as RunHandle, h as RunOutcome, S as Source, i as SourceHost, j as StartOptions, k as consoleChannel, l as createRoutes, m as custom } from './client-B5hM4l-D.js';
3
+ import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-BjBbCGeJ.js';
4
+ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, J as JobDefinition, h as JobHealth, i as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as StoredJob, T as TriageContext, n as TriageFn } from './types-BjBbCGeJ.js';
5
5
 
6
6
  /**
7
7
  * Keeps everything in process memory. The default when no store is given,
package/dist/index.js CHANGED
@@ -9,11 +9,14 @@ var UNIT_MS = {
9
9
  d: 864e5,
10
10
  w: 6048e5
11
11
  };
12
+ var MAX_LENGTH = 64;
13
+ var QUOTED = 32;
12
14
  function parseDuration(value, label = "duration") {
13
15
  if (typeof value === "number") {
14
16
  if (!Number.isFinite(value) || value < 0) throw new Error(`${label} must be a non-negative number of milliseconds`);
15
17
  return value;
16
18
  }
19
+ if (value.length > MAX_LENGTH) tooLong(value, label);
17
20
  const text = value.trim().toLowerCase();
18
21
  if (text === "") throw new Error(`${label} is empty`);
19
22
  const re = /(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)/g;
@@ -29,6 +32,16 @@ function parseDuration(value, label = "duration") {
29
32
  }
30
33
  return Math.round(total);
31
34
  }
35
+ function tooLong(value, label) {
36
+ let count = 0;
37
+ let head = "";
38
+ for (const char of value) {
39
+ if (count < QUOTED) head += char;
40
+ if (++count > MAX_LENGTH) {
41
+ throw new Error(`${label} "${head}..." is too long for a duration (more than ${MAX_LENGTH} characters)`);
42
+ }
43
+ }
44
+ }
32
45
  function formatDuration(ms) {
33
46
  if (!Number.isFinite(ms)) return "?";
34
47
  if (ms < 1e3) return `${Math.round(ms)}ms`;
@@ -52,6 +65,14 @@ function formatRelative(at, now) {
52
65
  if (abs < 5e3) return text;
53
66
  return diff < 0 ? `${text} ago` : `in ${text}`;
54
67
  }
68
+ var FIRST_DATE_MS = -621355968e5;
69
+ var LAST_DATE_MS = 253402300799999;
70
+ function isoTime(at) {
71
+ return at >= FIRST_DATE_MS && at <= LAST_DATE_MS ? new Date(at).toISOString() : null;
72
+ }
73
+ function beyondDates(at) {
74
+ return at > LAST_DATE_MS ? "after 9999-12-31 23:59:59 UTC" : "before 0001-01-01 00:00:00 UTC";
75
+ }
55
76
 
56
77
  // src/env.ts
57
78
  function readEnv(name) {
@@ -89,15 +110,36 @@ function parseSchedule(schedule, timezone) {
89
110
  cache.set(key, parsed);
90
111
  return parsed;
91
112
  }
113
+ var CYCLE_MS = 146097 * 864e5;
114
+ var CRONER_FIRST_MS = Date.UTC(400, 0, 1);
115
+ var CRONER_LAST_MS = Date.UTC(2800, 0, 1);
116
+ function runsAfter(cron, n, from) {
117
+ let shift = 0;
118
+ if (from < CRONER_FIRST_MS) shift = Math.ceil((CRONER_FIRST_MS - from) / CYCLE_MS) * CYCLE_MS;
119
+ else if (from >= CRONER_LAST_MS) shift = -(Math.floor((from - CRONER_LAST_MS) / CYCLE_MS) + 1) * CYCLE_MS;
120
+ const out = [];
121
+ for (const date of cron.nextRuns(n, new Date(from + shift))) {
122
+ const t = date.getTime() - shift;
123
+ if (t > LAST_DATE_MS) break;
124
+ out.push(t);
125
+ }
126
+ return out;
127
+ }
128
+ function countFrom(from) {
129
+ if (from >= LAST_DATE_MS) return null;
130
+ return from >= FIRST_DATE_MS ? from : FIRST_DATE_MS - 1;
131
+ }
92
132
  function fireAfter(parsed, from) {
93
133
  const cron = crons.get(parsed);
94
134
  if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
95
- let probe = from;
135
+ const start = countFrom(from);
136
+ if (start === null) return null;
137
+ let probe = start;
96
138
  for (let attempt = 0; attempt < 4; attempt++) {
97
- const runs = cron.nextRuns(8, new Date(probe));
139
+ const runs = runsAfter(cron, 8, probe);
98
140
  if (runs.length === 0) return null;
99
- const found = runs.find((d) => d.getTime() > from);
100
- if (found) return found.getTime();
141
+ const found = runs.find((t) => t > start);
142
+ if (found !== void 0) return found;
101
143
  probe += 36e5;
102
144
  }
103
145
  return null;
@@ -106,20 +148,21 @@ function firesBetween(parsed, from, to, limit) {
106
148
  const cron = crons.get(parsed);
107
149
  if (!cron) throw new Error(`schedule "${parsed.source}" was not made by parseSchedule`);
108
150
  const out = [];
109
- let probe = from;
110
- let last = from;
151
+ const start = countFrom(from);
152
+ if (start === null) return out;
153
+ let probe = start;
154
+ let last = start;
111
155
  for (let guard = 0; guard < 1e3; guard++) {
112
- const batch = cron.nextRuns(Math.min(limit + 1 - out.length, 24), new Date(probe));
156
+ const batch = runsAfter(cron, Math.min(limit + 1 - out.length, 24), probe);
113
157
  if (batch.length === 0) return out;
114
- for (const date of batch) {
115
- const t = date.getTime();
158
+ for (const t of batch) {
116
159
  if (t <= last) continue;
117
160
  if (t > to) return out;
118
161
  out.push(t);
119
162
  last = t;
120
163
  if (out.length > limit) return null;
121
164
  }
122
- const end = batch[batch.length - 1].getTime();
165
+ const end = batch[batch.length - 1];
123
166
  probe = end > probe ? end : probe + 36e5;
124
167
  }
125
168
  return out;
@@ -155,6 +198,7 @@ function runCovers(startedAt, dueAt, followingAt = null) {
155
198
  }
156
199
  function inSpringForwardGap(parsed, startedAt, fireAt) {
157
200
  const LOOKBACK = 3 * 36e5;
201
+ if (fireAt - LOOKBACK < FIRST_DATE_MS) return false;
158
202
  const after2 = utcOffset(fireAt, parsed.timezone);
159
203
  const before = utcOffset(fireAt - LOOKBACK, parsed.timezone);
160
204
  const gap = after2 - before;
@@ -191,8 +235,10 @@ function utcOffset(at, timezone) {
191
235
  for (const part of format.formatToParts(new Date(at))) {
192
236
  if (part.type !== "literal") parts[part.type] = Number(part.value);
193
237
  }
194
- const wall = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
195
- return wall - Math.floor(at / 1e3) * 1e3;
238
+ const wall = /* @__PURE__ */ new Date(0);
239
+ wall.setUTCFullYear(parts.year, parts.month - 1, parts.day);
240
+ wall.setUTCHours(parts.hour, parts.minute, parts.second);
241
+ return wall.getTime() - Math.floor(at / 1e3) * 1e3;
196
242
  }
197
243
 
198
244
  // src/stats.ts
@@ -215,6 +261,15 @@ var DEFAULT_TIMEOUT_MS = 60 * 6e4;
215
261
  var SLOW_FLOOR_MS = 1e4;
216
262
  var BASELINE_MIN_RUNS = 5;
217
263
  var BASELINE_WINDOW = 20;
264
+ var MAX_DURATION_MS = Number.MAX_SAFE_INTEGER;
265
+ function runDuration(startedAt, finishedAt) {
266
+ const ms = finishedAt - startedAt;
267
+ return ms > 0 ? Math.min(ms, MAX_DURATION_MS) : 0;
268
+ }
269
+ function stateVersion(state) {
270
+ const version = state?.version;
271
+ return typeof version === "number" && Number.isSafeInteger(version) && version >= 0 ? version + 0 : 0;
272
+ }
218
273
  function emptyState(job) {
219
274
  return { job, open: {}, consecutiveFailures: 0, silencedUntil: null, lastAlertAt: null, pendingRecovery: [], undelivered: [] };
220
275
  }
@@ -425,7 +480,9 @@ function summary(stored, recent, state, nextExpectedAt, health) {
425
480
  // src/format.ts
426
481
  function when(at, now) {
427
482
  if (at === null || at === void 0) return "never";
428
- return `${new Date(at).toISOString().replace("T", " ").slice(0, 19)} UTC (${formatRelative(at, now)})`;
483
+ const iso = isoTime(at);
484
+ if (iso === null) return beyondDates(at);
485
+ return `${iso.replace("T", " ").slice(0, 19)} UTC (${formatRelative(at, now)})`;
429
486
  }
430
487
  function firstLines(text, n) {
431
488
  if (!text) return "";
@@ -821,9 +878,13 @@ function dayLabel(t) {
821
878
  return `${WEEKDAYS[d.getUTCDay()]} ${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]}`;
822
879
  }
823
880
  function when2(t, now) {
881
+ if (t > LAST_DATE_MS) return "after 31 Dec 9999 23:59";
882
+ if (!(t >= FIRST_DATE_MS)) return "before 1 Jan 0001 00:00";
824
883
  if (Math.floor(t / DAY) === Math.floor(now / DAY)) return clock(t);
825
884
  const d = new Date(t);
826
- return `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]} ${clock(t)}`;
885
+ const year = d.getUTCFullYear();
886
+ const other = year === new Date(now).getUTCFullYear() ? "" : ` ${String(year).padStart(4, "0")}`;
887
+ return `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]}${other} ${clock(t)}`;
827
888
  }
828
889
  function parsedSchedule(job) {
829
890
  if (!job.definition.schedule) return null;
@@ -1314,7 +1375,8 @@ function sparkline(runs) {
1314
1375
  }
1315
1376
  function stamp(at, now) {
1316
1377
  if (at === null) return `<span class="muted">never</span>`;
1317
- const iso = new Date(at).toISOString();
1378
+ const iso = isoTime(at);
1379
+ if (iso === null) return `<span class="nowrap">${beyondDates(at)}</span>`;
1318
1380
  return `<time class="nowrap" datetime="${iso}" title="${iso.replace("T", " ").slice(0, 19)} UTC">${h(formatRelative(at, now))}</time>`;
1319
1381
  }
1320
1382
  function healthFigures(jobs) {
@@ -1480,7 +1542,20 @@ function developmentToken() {
1480
1542
  return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1481
1543
  }
1482
1544
  function developmentSignInLine(origin, base, token) {
1483
- return `[cronwatch] CRONWATCH_TOKEN is not set, so this development server made a token for the dashboard. Sign in: ${origin}${base}/?token=${token}`;
1545
+ const intro = "[cronwatch] CRONWATCH_TOKEN is not set, so this development server made a token for the dashboard. Sign in: ";
1546
+ return origin === null ? `${intro}${base}/?token=${token} on this server (the first request's host is not local, so the link leaves it out)` : `${intro}${origin}${base}/?token=${token}`;
1547
+ }
1548
+ function isLoopbackOrigin(origin) {
1549
+ let host;
1550
+ try {
1551
+ host = new URL(origin).hostname.toLowerCase();
1552
+ } catch {
1553
+ return false;
1554
+ }
1555
+ if (host === "localhost" || host.endsWith(".localhost")) return true;
1556
+ if (host === "[::1]") return true;
1557
+ const octets = /^127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
1558
+ return octets !== null && octets.slice(1).every((o) => Number(o) <= 255);
1484
1559
  }
1485
1560
  function safeDecode(value) {
1486
1561
  try {
@@ -1585,7 +1660,8 @@ function createRoutes(cw, options = {}) {
1585
1660
  const publicOrigin = originOf(request, url);
1586
1661
  if (generated && !announced) {
1587
1662
  announced = true;
1588
- console.info(developmentSignInLine(publicOrigin, base, token));
1663
+ const shown = fixedOrigin ?? (isLoopbackOrigin(publicOrigin) ? publicOrigin : null);
1664
+ console.info(developmentSignInLine(shown, base, token));
1589
1665
  }
1590
1666
  if (method === "GET" || method === "HEAD") {
1591
1667
  if (path === "/offline") {
@@ -1833,7 +1909,7 @@ function memory() {
1833
1909
  states.set(state.job, clone(state));
1834
1910
  },
1835
1911
  async compareAndSetState(state, expectedVersion) {
1836
- if ((states.get(state.job)?.version ?? 0) !== expectedVersion) return false;
1912
+ if (stateVersion(states.get(state.job)) !== expectedVersion) return false;
1837
1913
  states.set(state.job, clone(state));
1838
1914
  return true;
1839
1915
  },
@@ -2103,7 +2179,7 @@ var CronWatch = class {
2103
2179
  const current = await this.readState(job);
2104
2180
  const { state, result } = await change(current);
2105
2181
  if (sameState(state, current)) return { state: current, result };
2106
- const version = current.version ?? 0;
2182
+ const version = stateVersion(current);
2107
2183
  const next = { ...state, version: version + 1 };
2108
2184
  if (await this.writeState(next, version)) return { state: next, result };
2109
2185
  if (attempt >= STATE_ATTEMPTS) {
@@ -2163,7 +2239,7 @@ var CronWatch = class {
2163
2239
  }
2164
2240
  const finishedAt = this.now();
2165
2241
  run.finishedAt = finishedAt;
2166
- run.durationMs = Math.max(0, finishedAt - startedAt);
2242
+ run.durationMs = runDuration(startedAt, finishedAt);
2167
2243
  run.metrics = recorder.metrics();
2168
2244
  run.output = recorder.output() ?? (typeof result === "string" ? capOutput(result) : null);
2169
2245
  this.conclude(definition, run, result, error, threw, recorder.expectText() ?? (typeof result === "string" ? result : null));
@@ -2397,7 +2473,7 @@ ${id}`;
2397
2473
  ...from,
2398
2474
  status: "running",
2399
2475
  finishedAt,
2400
- durationMs: Math.max(0, finishedAt - from.startedAt),
2476
+ durationMs: runDuration(from.startedAt, finishedAt),
2401
2477
  error: null,
2402
2478
  output: joinOutput(from.output, added),
2403
2479
  metrics: { ...from.metrics, ...recorder.metrics() }
@@ -2586,7 +2662,7 @@ ${id}`;
2586
2662
  const timeout = timeoutMs(definition);
2587
2663
  run.status = "timeout";
2588
2664
  run.finishedAt = now;
2589
- run.durationMs = now - run.startedAt;
2665
+ run.durationMs = runDuration(run.startedAt, now);
2590
2666
  run.error = `Still running after ${formatDuration(timeout)}; marked as timed out`;
2591
2667
  if (!await this.writeRunIf(run, ["running"])) continue;
2592
2668
  alerts.push(...await this.finishRun(definition, run, now));