@greatwait/contracts 0.3.0 → 0.3.2

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 (104) hide show
  1. package/dist/attempt-diagnostics.d.ts +1 -1
  2. package/dist/attempt-diagnostics.js +13 -1
  3. package/dist/attempt-search.d.ts +2 -0
  4. package/dist/attempt-search.js +22 -19
  5. package/dist/authority.js +9 -9
  6. package/dist/billing.d.ts +46 -0
  7. package/dist/billing.js +29 -0
  8. package/dist/callback-safety.js +5 -1
  9. package/dist/configuration.d.ts +6 -2
  10. package/dist/configuration.js +93 -52
  11. package/dist/delivery.d.ts +13 -1
  12. package/dist/delivery.js +2 -0
  13. package/dist/effective-limits.d.ts +4 -4
  14. package/dist/endpoints.d.ts +29 -0
  15. package/dist/endpoints.js +57 -26
  16. package/dist/forecast.d.ts +2 -2
  17. package/dist/http-request.d.ts +12 -2
  18. package/dist/http-request.js +56 -24
  19. package/dist/identifiers.js +20 -20
  20. package/dist/index.d.ts +8 -7
  21. package/dist/index.js +8 -7
  22. package/dist/mcp.js +1 -0
  23. package/dist/operational-config.d.ts +174 -22
  24. package/dist/operational-config.js +213 -79
  25. package/dist/operations.d.ts +334 -52
  26. package/dist/operations.js +195 -34
  27. package/dist/project-notifications.d.ts +35 -9
  28. package/dist/project-notifications.js +43 -12
  29. package/dist/protocol.d.ts +28 -0
  30. package/dist/protocol.js +56 -33
  31. package/dist/schedule-selection.js +2 -2
  32. package/dist/scheduling-records.d.ts +106 -16
  33. package/dist/scheduling-records.js +36 -12
  34. package/dist/scheduling.d.ts +200 -43
  35. package/dist/scheduling.js +280 -131
  36. package/dist/service-mode.d.ts +88 -0
  37. package/dist/service-mode.js +130 -0
  38. package/dist/signatures.d.ts +6 -6
  39. package/dist/signatures.js +24 -24
  40. package/dist/stored-http.d.ts +18 -0
  41. package/dist/stored-http.js +106 -0
  42. package/dist/tenancy.d.ts +54 -30
  43. package/dist/tenancy.js +35 -17
  44. package/dist/time.js +7 -7
  45. package/dist/timing.d.ts +39 -0
  46. package/dist/timing.js +51 -0
  47. package/dist/zod-values.d.ts +1 -1
  48. package/dist/zod-values.js +1 -1
  49. package/dist/zod.d.ts +1 -1
  50. package/dist/zod.js +1 -1
  51. package/generated/v1/examples/billing.noticesdismiss.request.json +3 -0
  52. package/generated/v1/examples/billing.noticesdismiss.response.json +6 -0
  53. package/generated/v1/examples/billing.reconcile.response.json +12 -0
  54. package/generated/v1/examples/billing.summary.response.json +12 -0
  55. package/generated/v1/examples/executions.list.response.json +1 -0
  56. package/generated/v1/examples/limits.read.response.json +1 -1
  57. package/generated/v1/examples/projectnotifications.rotateSigningKey.response.json +1 -0
  58. package/generated/v1/examples/projects.list.response.json +0 -6
  59. package/generated/v1/examples/projects.read.response.json +14 -0
  60. package/generated/v1/examples/schedules.list.response.json +1 -3
  61. package/generated/v1/examples/schedules.summary.response.json +174 -0
  62. package/generated/v1/examples/timing.summary.response.json +33 -1
  63. package/generated/v1/manifest.json +79 -2
  64. package/generated/v1/openapi.json +4084 -2023
  65. package/generated/v1/policy-matrix.json +22 -1
  66. package/generated/v1/schemas/attempts.list.response.json +10 -0
  67. package/generated/v1/schemas/attempts.read.response.json +10 -0
  68. package/generated/v1/schemas/attempts.search.response.json +4 -0
  69. package/generated/v1/schemas/billing.notices.query.json +4 -0
  70. package/generated/v1/schemas/billing.noticesdismiss.request.json +12 -0
  71. package/generated/v1/schemas/billing.noticesdismiss.response.json +22 -0
  72. package/generated/v1/schemas/billing.reconcile.response.json +43 -0
  73. package/generated/v1/schemas/billing.summary.response.json +43 -0
  74. package/generated/v1/schemas/endpoints.list.query.json +15 -0
  75. package/generated/v1/schemas/executions.list.response.json +4 -0
  76. package/generated/v1/schemas/occurrences.list.response.json +4 -0
  77. package/generated/v1/schemas/occurrences.read.response.json +4 -0
  78. package/generated/v1/schemas/occurrences.redeliver.response.json +10 -0
  79. package/generated/v1/schemas/projectnotifications.read.response.json +4 -0
  80. package/generated/v1/schemas/projectnotifications.rotateSigningKey.response.json +4 -0
  81. package/generated/v1/schemas/projectnotifications.update.request.json +41 -17
  82. package/generated/v1/schemas/projectnotifications.update.response.json +4 -0
  83. package/generated/v1/schemas/projects.list.response.json +0 -27
  84. package/generated/v1/schemas/projects.read.response-headers.json +11 -0
  85. package/generated/v1/schemas/projects.read.response.json +79 -0
  86. package/generated/v1/schemas/schedules.archive.response.json +8 -0
  87. package/generated/v1/schemas/schedules.cancel.response.json +8 -0
  88. package/generated/v1/schemas/schedules.create.request.json +1 -1
  89. package/generated/v1/schemas/schedules.create.response.json +8 -0
  90. package/generated/v1/schemas/schedules.list.query.json +6 -0
  91. package/generated/v1/schemas/schedules.list.response.json +4 -0
  92. package/generated/v1/schemas/schedules.pause.response.json +8 -0
  93. package/generated/v1/schemas/schedules.read.response.json +8 -0
  94. package/generated/v1/schemas/schedules.replace.request.json +1 -1
  95. package/generated/v1/schemas/schedules.replace.response.json +8 -0
  96. package/generated/v1/schemas/schedules.resume.response.json +8 -0
  97. package/generated/v1/schemas/schedules.summary.query.json +11 -0
  98. package/generated/v1/schemas/schedules.summary.response.json +144 -0
  99. package/generated/v1/schemas/timing.summary.response.json +69 -0
  100. package/generated/v1/snippets/billing.noticesdismiss.json +6 -0
  101. package/generated/v1/snippets/projectnotifications.rotateSigningKey.json +1 -1
  102. package/generated/v1/snippets/projects.read.json +6 -0
  103. package/generated/v1/snippets/schedules.summary.json +6 -0
  104. package/package.json +1 -1
@@ -3,11 +3,13 @@ import { executionSelectionSchema, normalizeExecutionSelection, } from "./execut
3
3
  import { httpRequestSchema, publicHttpRequestSchema } from "./http-request.js";
4
4
  import { FIXED_INTERVAL_MIN_SECONDS, FIXED_INTERVAL_MAX_SECONDS, MAX_INLINE_PAYLOAD_BYTES, } from "./configuration-bounds.js";
5
5
  import { z } from "./zod.js";
6
+ import { SERVICE_MODE_POLICY } from "./operational-config.js";
7
+ import { REQUESTED_LATENCY_CLASSES, oneOffSpacingMs, resolveService, } from "./service-mode.js";
6
8
  import { externalIdentifierSchema, scheduleTagsSchema, } from "./host-correlation.js";
7
9
  import { attemptIdSchema, deliveryIdSchema, endpointIdSchema, occurrenceIdSchema, scheduleIdSchema, } from "./identifiers.js";
8
10
  import { CALLBACK_ATTEMPT_DIAGNOSTICS } from "./delivery.js";
9
11
  import { callbackUrlSchema, signingSecretNoticeSchema } from "./endpoints.js";
10
- import { pageRequestSchema, pageSchema, versionSchema, } from "./protocol.js";
12
+ import { pageRequestSchema, pageSchema, projectedFreshnessSchema, versionSchema, } from "./protocol.js";
11
13
  import { callbackTriggerSchema, } from "./signatures.js";
12
14
  import { durationSecondsSchema, timeZoneSchema, timestampSchema } from "./time.js";
13
15
  /**
@@ -51,18 +53,18 @@ export const SCHEDULE_STATUSES = [
51
53
  "cancelled",
52
54
  "completed",
53
55
  ];
54
- export const scheduleStatusSchema = z.enum(SCHEDULE_STATUSES);
56
+ export const scheduleStatusSchema = /* @__PURE__ */ (() => z.enum(SCHEDULE_STATUSES))();
55
57
  /** A recurring schedule pauses after this many failed runs; "never" disables it. */
56
58
  export const DEFAULT_AUTO_PAUSE_AFTER_FAILURES = 5;
57
- export const autoPauseAfterFailuresSchema = z
59
+ export const autoPauseAfterFailuresSchema = /* @__PURE__ */ (() => z
58
60
  .union([z.number().int().min(1).max(100), z.literal("never")])
59
- .describe('Consecutive failed runs before automatic pause: 1–100 or "never". Defaults to 5 on create; omission on replace preserves the setting. One-shot schedules are unaffected.');
60
- export const autoPauseReasonSchema = z.object({
61
+ .describe('Consecutive failed runs before automatic pause: 1–100 or "never". Defaults to 5 on create; omission on replace preserves the setting. One-shot schedules are unaffected.'))();
62
+ export const autoPauseReasonSchema = /* @__PURE__ */ (() => z.object({
61
63
  failedRuns: z.number().int().positive(),
62
64
  status: z.number().int().min(100).max(599).optional(),
63
65
  diagnostic: z.string().max(500),
64
66
  pausedAt: timestampSchema,
65
- });
67
+ }))();
66
68
  /**
67
69
  * The statuses a schedule never leaves, and from which it never fires again on
68
70
  * its own.
@@ -103,7 +105,14 @@ export function isTerminalScheduleStatus(status) {
103
105
  * anything to anybody. `realtime` gets preference without starving `bulk`.
104
106
  */
105
107
  export const LATENCY_CLASSES = ["realtime", "bulk"];
106
- export const latencyClassSchema = z.enum(LATENCY_CLASSES);
108
+ export const latencyClassSchema = /* @__PURE__ */ (() => z.enum(LATENCY_CLASSES))();
109
+ /**
110
+ * The service mode a request asks for (W12-02, FXO-3171): `auto`, or one of
111
+ * the concrete classes. Omission means `auto`. A read reports the concrete
112
+ * class an occurrence resolved to as `latencyClass`, and what was asked for as
113
+ * `requestedLatencyClass`; see `service-mode.ts` for the resolution.
114
+ */
115
+ export const requestedLatencyClassSchema = /* @__PURE__ */ (() => z.enum(REQUESTED_LATENCY_CLASSES))();
107
116
  /**
108
117
  * What happens when an occurrence reaches its first attempt after its planned
109
118
  * instant.
@@ -118,7 +127,7 @@ export const latencyClassSchema = z.enum(LATENCY_CLASSES);
118
127
  * it started under.
119
128
  */
120
129
  export const LATENESS_POLICIES = ["skipAhead", "catchUp"];
121
- export const latenessPolicySchema = z.enum(LATENESS_POLICIES);
130
+ export const latenessPolicySchema = /* @__PURE__ */ (() => z.enum(LATENESS_POLICIES))();
122
131
  /** The inherited one-shot staleness window: twenty-four hours. */
123
132
  export const DEFAULT_ONE_SHOT_MAX_STALENESS_SECONDS = 86_400;
124
133
  /**
@@ -129,7 +138,7 @@ export const DEFAULT_ONE_SHOT_MAX_STALENESS_SECONDS = 86_400;
129
138
  * staleness bound uses the recurrence's own default: twenty-four hours for a
130
139
  * one-shot, and one recurrence interval for a recurring schedule.
131
140
  */
132
- export const scheduleLatenessRequestSchema = z
141
+ export const scheduleLatenessRequestSchema = /* @__PURE__ */ (() => z
133
142
  .object({
134
143
  maxStalenessSeconds: durationSecondsSchema
135
144
  .positive("must be at least 1 second")
@@ -137,7 +146,7 @@ export const scheduleLatenessRequestSchema = z
137
146
  .optional(),
138
147
  policy: latenessPolicySchema.optional(),
139
148
  })
140
- .strict();
149
+ .strict())();
141
150
  /**
142
151
  * Lateness settings as a schedule reads back.
143
152
  *
@@ -145,9 +154,9 @@ export const scheduleLatenessRequestSchema = z
145
154
  * remains optional because it records a per-schedule override, not a duplicated
146
155
  * copy of a default that can be derived from the recurrence.
147
156
  */
148
- export const scheduleLatenessSchema = scheduleLatenessRequestSchema.extend({
157
+ export const scheduleLatenessSchema = /* @__PURE__ */ (() => scheduleLatenessRequestSchema.extend({
149
158
  policy: latenessPolicySchema,
150
- });
159
+ }))();
151
160
  /* -------------------------------------------------------------------------- */
152
161
  /* Delivery duration */
153
162
  /* -------------------------------------------------------------------------- */
@@ -256,12 +265,12 @@ export const RESPONSE_DELIVERY_MAX_DURATION_SECONDS = 1_800;
256
265
  * a relation between two fields), and above
257
266
  * {@link FREE_DELIVERY_DURATION_SECONDS} it needs a plan that allows it.
258
267
  */
259
- export const deliveryDurationSecondsSchema = z
268
+ export const deliveryDurationSecondsSchema = /* @__PURE__ */ (() => z
260
269
  .number()
261
270
  .int("must be a whole number of seconds")
262
271
  .positive("must be at least 1 second")
263
272
  .max(MAX_DELIVERY_DURATION_SECONDS, `must be at most ${String(MAX_DELIVERY_DURATION_SECONDS)} seconds`)
264
- .describe("How long the receiver may take to answer, in seconds");
273
+ .describe("How long the receiver may take to answer, in seconds"))();
265
274
  /** Whether a duration is past the point where it needs a plan that allows it. */
266
275
  export function isLongRunningDuration(seconds) {
267
276
  return seconds > FREE_DELIVERY_DURATION_SECONDS;
@@ -291,7 +300,7 @@ export function isLongRunningDuration(seconds) {
291
300
  * untouched, and so is what a rung costs.
292
301
  */
293
302
  export const DELIVERY_COMPLETION_MODES = ["response", "callback"];
294
- export const deliveryCompletionModeSchema = z.enum(DELIVERY_COMPLETION_MODES);
303
+ export const deliveryCompletionModeSchema = /* @__PURE__ */ (() => z.enum(DELIVERY_COMPLETION_MODES))();
295
304
  /**
296
305
  * What to do when a recurring schedule comes due and its previous run has not
297
306
  * finished.
@@ -313,7 +322,7 @@ export const deliveryCompletionModeSchema = z.enum(DELIVERY_COMPLETION_MODES);
313
322
  * - `queue` — run it when the previous one finishes.
314
323
  */
315
324
  export const DELIVERY_OVERLAP_POLICIES = ["skip", "queue"];
316
- export const deliveryOverlapPolicySchema = z.enum(DELIVERY_OVERLAP_POLICIES);
325
+ export const deliveryOverlapPolicySchema = /* @__PURE__ */ (() => z.enum(DELIVERY_OVERLAP_POLICIES))();
317
326
  export const DEFAULT_DELIVERY_OVERLAP_POLICY = "skip";
318
327
  /**
319
328
  * A definition generation. Starts at 1 and increases on every replacement.
@@ -324,10 +333,7 @@ export const DEFAULT_DELIVERY_OVERLAP_POLICY = "skip";
324
333
  * would make pausing and resuming a schedule invalidate occurrences that are
325
334
  * still perfectly valid.
326
335
  */
327
- export const generationSchema = z
328
- .number()
329
- .int("must be a whole number")
330
- .positive("must be at least 1");
336
+ export const generationSchema = /* @__PURE__ */ (() => z.number().int("must be a whole number").positive("must be at least 1"))();
331
337
  /**
332
338
  * A customer-supplied fairness key.
333
339
  *
@@ -339,11 +345,11 @@ export const generationSchema = z
339
345
  * another Durable Object namespace.
340
346
  */
341
347
  export const PARTITION_KEY_MAX_LENGTH = 64;
342
- export const partitionKeySchema = z
348
+ export const partitionKeySchema = /* @__PURE__ */ (() => z
343
349
  .string()
344
350
  .min(1, "must not be empty")
345
351
  .max(PARTITION_KEY_MAX_LENGTH, `must be at most ${String(PARTITION_KEY_MAX_LENGTH)} characters`)
346
- .regex(/^[A-Za-z0-9._:-]+$/u, "must use only letters, digits, dot, underscore, colon, or hyphen");
352
+ .regex(/^[A-Za-z0-9._:-]+$/u, "must use only letters, digits, dot, underscore, colon, or hyphen"))();
347
353
  /* -------------------------------------------------------------------------- */
348
354
  /* Occurrence state */
349
355
  /* -------------------------------------------------------------------------- */
@@ -369,11 +375,11 @@ export { OCCURRENCE_STATES, occurrenceStateSchema, } from "./occurrence-states.j
369
375
  * the same thing. The date range is half-open, like every other time window in
370
376
  * this contract, so adjacent requests neither overlap nor leave a gap.
371
377
  */
372
- export const projectOccurrenceListRequestSchema = pageRequestSchema
378
+ export const projectOccurrenceListRequestSchema = /* @__PURE__ */ (() => pageRequestSchema
373
379
  .extend(executionSelectionSchema.shape)
374
380
  .refine((request) => request.from === undefined ||
375
381
  request.to === undefined ||
376
- Date.parse(request.from) < Date.parse(request.to), { message: "must be later than from", path: ["to"] });
382
+ Date.parse(request.from) < Date.parse(request.to), { message: "must be later than from", path: ["to"] }))();
377
383
  const PROJECT_OCCURRENCE_LIST_PARAMETERS = [
378
384
  "cursor",
379
385
  "limit",
@@ -468,9 +474,9 @@ export const ATTEMPT_STATES = [
468
474
  "terminal_failure",
469
475
  "ambiguous",
470
476
  ];
471
- export const attemptStateSchema = z.enum(ATTEMPT_STATES);
477
+ export const attemptStateSchema = /* @__PURE__ */ (() => z.enum(ATTEMPT_STATES))();
472
478
  /** Safe diagnostic codes produced by the W3-03 delivery classifier. */
473
- export const attemptDiagnosticSchema = z.enum(CALLBACK_ATTEMPT_DIAGNOSTICS);
479
+ export const attemptDiagnosticSchema = /* @__PURE__ */ (() => z.enum(CALLBACK_ATTEMPT_DIAGNOSTICS))();
474
480
  /**
475
481
  * Bounded next-action hints for an attempt timeline.
476
482
  *
@@ -499,7 +505,7 @@ export const ATTEMPT_REMEDIATIONS = [
499
505
  export const ATTEMPT_RESPONSE_HEADER_MAX_LENGTH = 256;
500
506
  /** How much of a failed receiver's body is published, in characters. */
501
507
  export const ATTEMPT_FAILURE_SNIPPET_MAX_LENGTH = 4_096;
502
- export const attemptRemediationSchema = z.enum(ATTEMPT_REMEDIATIONS);
508
+ export const attemptRemediationSchema = /* @__PURE__ */ (() => z.enum(ATTEMPT_REMEDIATIONS))();
503
509
  /** Derive the one safe next-action code a public attempt may expose. */
504
510
  export function remediationForAttempt(diagnostic) {
505
511
  switch (diagnostic) {
@@ -615,13 +621,11 @@ export const SCHEDULING_BOUNDS = {
615
621
  * was the copied scheduler's `MAX_INTERVAL_SECONDS`, carried over on the
616
622
  * belief that "a Durable Object alarm is not arbitrarily far in the future".
617
623
  * Cloudflare documents no maximum distance for `setAlarm()`, and the belief
618
- * predates the PostgreSQL authority besides: the durable fact is a row, and
619
- * the alarm is only what wakes a bucket to read it.
624
+ * predates the PostgreSQL authority besides: the durable fact is a row.
620
625
  *
621
- * What makes a distant alarm safe is not the alarm. `TimerBucketRepairer`
622
- * asks PostgreSQL for occurrences whose instant has passed and re-signals
623
- * their bucket, on committed truth, never asking whether an alarm was armed
624
- * — so an alarm that is somehow lost costs lateness rather than the run.
626
+ * Since W13-03 there is no alarm at all. The Node runtime polls
627
+ * `core.occurrence` for rows whose `due_at` has passed, so a year-distant
628
+ * occurrence is the same row read on the same poll as a minute-distant one.
625
629
  *
626
630
  * Widening a bound is additive by `classifyChange` and narrowing it is
627
631
  * breaking, so this direction is the safe one to move in.
@@ -692,17 +696,15 @@ export const SCHEDULING_BOUNDS = {
692
696
  */
693
697
  payloadBytes: MAX_INLINE_PAYLOAD_BYTES,
694
698
  /**
695
- * How soon a one-shot has to be for `realtime` to be the class it gets by
696
- * default, in seconds.
697
- *
698
- * Two minutes, which is the copied `validate.ts`'s own threshold kept rather
699
- * than a number invented beside it: upstream classifies an `at` or a `delay`
700
- * inside 120 seconds as realtime and everything else as bulk, on the
701
- * reasoning that a wait that short is a human waiting on a screen. A caller
702
- * who disagrees says so — `latencyClass` on the request is what makes this a
703
- * default rather than a rule.
699
+ * How soon a one-shot has to be for `auto` to resolve it to `realtime`, in
700
+ * seconds, exclusive: a one-shot planned less than a minute after it was
701
+ * accepted is realtime, and one planned exactly a minute out is bulk
702
+ * (W12-02, FXO-3171; owner decision 2026-10-14). The single owner is
703
+ * `SERVICE_MODE_POLICY.autoRealtimeBelowMs`. A caller who disagrees says so
704
+ * — `latencyClass` on the request is what makes this a default rather than a
705
+ * rule.
704
706
  */
705
- realtimeOneShotSeconds: 120,
707
+ realtimeOneShotSeconds: SERVICE_MODE_POLICY.autoRealtimeBelowMs / 1000,
706
708
  };
707
709
  /* -------------------------------------------------------------------------- */
708
710
  /* Recurrence */
@@ -735,12 +737,12 @@ export const CRON_EXPRESSION_MAX_LENGTH = 120;
735
737
  * fire on a second boundary is refused by the partition, because a minute is
736
738
  * the granularity both arms of recurrence promise.
737
739
  */
738
- export const cronExpressionSchema = z
740
+ export const cronExpressionSchema = /* @__PURE__ */ (() => z
739
741
  .string()
740
742
  .min(1, "must not be empty")
741
743
  .max(CRON_EXPRESSION_MAX_LENGTH, `must be at most ${String(CRON_EXPRESSION_MAX_LENGTH)} characters`)
742
744
  .refine((expression) => expression.startsWith("@") ||
743
- [5, 6].includes(expression.trim().split(/\s+/u).length), { message: "must be five cron fields, six fields, or an @-nickname" });
745
+ [5, 6].includes(expression.trim().split(/\s+/u).length), { message: "must be five cron fields, six fields, or an @-nickname" }))();
744
746
  /**
745
747
  * How a schedule recurs, as a caller reads it back.
746
748
  *
@@ -757,7 +759,7 @@ export const cronExpressionSchema = z
757
759
  * on that arm rather than optional, so a read always says which wall clock a
758
760
  * cron schedule is on instead of leaving the caller to assume UTC.
759
761
  */
760
- export const recurrenceSchema = z.discriminatedUnion("kind", [
762
+ export const recurrenceSchema = /* @__PURE__ */ (() => z.discriminatedUnion("kind", [
761
763
  /**
762
764
  * A one-shot, which carries no instant of its own.
763
765
  *
@@ -779,7 +781,7 @@ export const recurrenceSchema = z.discriminatedUnion("kind", [
779
781
  kind: z.literal("cron"),
780
782
  timeZone: timeZoneSchema,
781
783
  }),
782
- ]);
784
+ ]))();
783
785
  /** The customer-safe default for a schedule's recurrence kind. */
784
786
  export function defaultLatenessPolicyForRecurrence(kind) {
785
787
  return kind === "at" ? "catchUp" : "skipAhead";
@@ -807,12 +809,12 @@ export function defaultLatenessPolicyForRecurrence(kind) {
807
809
  */
808
810
  /** The most a schedule's name may hold; the generated one is cut to fit it. */
809
811
  export const SCHEDULE_NAME_MAX_LENGTH = 120;
810
- export const scheduleNameSchema = z
812
+ export const scheduleNameSchema = /* @__PURE__ */ (() => z
811
813
  .string()
812
814
  .trim()
813
815
  .min(1, "must not be blank")
814
- .max(SCHEDULE_NAME_MAX_LENGTH, `must be at most ${String(SCHEDULE_NAME_MAX_LENGTH)} characters`);
815
- export const scheduleSchema = z.object({
816
+ .max(SCHEDULE_NAME_MAX_LENGTH, `must be at most ${String(SCHEDULE_NAME_MAX_LENGTH)} characters`))();
817
+ export const scheduleSchema = /* @__PURE__ */ (() => z.object({
816
818
  autoPauseAfterFailures: autoPauseAfterFailuresSchema.optional(),
817
819
  consecutiveFailedRuns: z.number().int().nonnegative().optional(),
818
820
  autoPauseReason: autoPauseReasonSchema.nullable().optional(),
@@ -884,7 +886,18 @@ export const scheduleSchema = z.object({
884
886
  endpointUrl: z.string().url().optional(),
885
887
  generation: generationSchema,
886
888
  id: scheduleIdSchema,
889
+ /**
890
+ * The concrete class this schedule's occurrences are served and charged
891
+ * as: what `auto` resolved to, or the class the caller named.
892
+ */
887
893
  latencyClass: latencyClassSchema,
894
+ /**
895
+ * The mode the caller asked for (W12-02): `auto` when the request named
896
+ * none. Absent on a schedule accepted before the mode was recorded; its
897
+ * `latencyClass` is what it was accepted as, and a replacement that leaves
898
+ * its timing alone keeps it.
899
+ */
900
+ requestedLatencyClass: requestedLatencyClassSchema.optional(),
888
901
  /**
889
902
  * What this schedule does with an occurrence that reaches its first attempt
890
903
  * late. Present on every schedule so a caller never has to duplicate the
@@ -937,7 +950,7 @@ export const scheduleSchema = z.object({
937
950
  status: scheduleStatusSchema,
938
951
  updatedAt: timestampSchema,
939
952
  version: versionSchema,
940
- });
953
+ }))();
941
954
  /**
942
955
  * A page of schedules.
943
956
  *
@@ -948,7 +961,77 @@ export const scheduleSchema = z.object({
948
961
  * listing back through every partition it spans. Detail is where the occurrence
949
962
  * is authoritative, and detail is one call away.
950
963
  */
951
- export const schedulePageSchema = pageSchema(scheduleSchema);
964
+ export const schedulePageSchema = /* @__PURE__ */ (() => pageSchema(scheduleSchema))();
965
+ /** Local days a schedule summary covers, today included (W9-80, FXO-3446). */
966
+ export const SCHEDULE_SUMMARY_DAYS = 30;
967
+ /** Auto-paused schedules a summary names beside their count. */
968
+ export const SCHEDULE_SUMMARY_PAUSED_SAMPLE = 5;
969
+ /** The oldest a served schedule summary may be, in seconds. */
970
+ export const SCHEDULE_SUMMARY_MAX_AGE_SECONDS = 300;
971
+ const localDateSchema = /* @__PURE__ */ (() => z
972
+ .string()
973
+ .regex(/^\d{4}-\d{2}-\d{2}$/u, "must be a calendar date, YYYY-MM-DD"))();
974
+ /** A schedule summary request: the reader's time zone for the day buckets. */
975
+ export const scheduleSummaryRequestSchema = /* @__PURE__ */ (() => z.object({
976
+ timeZone: timeZoneSchema
977
+ .default("UTC")
978
+ .describe("IANA time zone the per-day buckets are counted in; defaults to UTC."),
979
+ }))();
980
+ /**
981
+ * Whole-project schedule counts for the caller's project and environment
982
+ * (W9-80, FXO-3446).
983
+ *
984
+ * **Exact as of `freshness.asOf`, and at most five minutes old.** The counts
985
+ * read every retained, unarchived schedule in one statement, which is too
986
+ * costly to repeat on every view of a large project, so an answer is computed
987
+ * on read and reused for up to {@link SCHEDULE_SUMMARY_MAX_AGE_SECONDS}
988
+ * seconds per project, environment and time zone. `freshness` is always
989
+ * `projected`: `asOf` is when the counts were taken and `lagSeconds` how long
990
+ * ago that was, so a client can say "as of 2 minutes ago" rather than present
991
+ * the numbers as live. Counts cover retained schedules only: retention removes
992
+ * cancelled schedules and one-offs that ran once their history horizon passes.
993
+ */
994
+ export const scheduleSummarySchema = /* @__PURE__ */ (() => z.object({
995
+ counts: z.object({
996
+ active: z.number().int().nonnegative(),
997
+ paused: z.number().int().nonnegative(),
998
+ completed: z.number().int().nonnegative(),
999
+ cancelled: z.number().int().nonnegative(),
1000
+ }),
1001
+ /**
1002
+ * Paused schedules whose newest run paused them after repeated failures,
1003
+ * and up to five of them (id, name and why), most recently paused first.
1004
+ * Read a schedule, or list `status=paused`, for the rest.
1005
+ */
1006
+ pausedAfterFailures: z.object({
1007
+ count: z.number().int().nonnegative(),
1008
+ schedules: z
1009
+ .array(z.object({
1010
+ id: scheduleIdSchema,
1011
+ name: z.string().optional(),
1012
+ autoPauseReason: autoPauseReasonSchema,
1013
+ }))
1014
+ .max(SCHEDULE_SUMMARY_PAUSED_SAMPLE),
1015
+ }),
1016
+ /**
1017
+ * One entry per local day of the window, oldest first, zero days
1018
+ * included: schedules created that day, and schedules that finished that
1019
+ * day (completed by their final run, or cancelled).
1020
+ */
1021
+ days: z
1022
+ .array(z.object({
1023
+ date: localDateSchema,
1024
+ created: z.number().int().nonnegative(),
1025
+ finished: z.number().int().nonnegative(),
1026
+ }))
1027
+ .length(SCHEDULE_SUMMARY_DAYS),
1028
+ window: z.object({
1029
+ from: localDateSchema,
1030
+ to: localDateSchema,
1031
+ timeZone: timeZoneSchema,
1032
+ }),
1033
+ freshness: projectedFreshnessSchema,
1034
+ }))();
952
1035
  function hasCompleteTimingEvidence(value) {
953
1036
  return ((value.firstAttemptStartedAt === undefined) ===
954
1037
  (value.firstAttemptLatenessMs === undefined) &&
@@ -973,7 +1056,12 @@ const timingEvidenceShape = {
973
1056
  .optional(),
974
1057
  timingEligibilityReason: z.string().min(1).max(128).optional(),
975
1058
  /** Policy pinned when trusted first-attempt timing evidence is recorded. */
976
- timingPolicyVersion: z.string().min(1).max(64).optional(),
1059
+ timingPolicyVersion: z
1060
+ .string()
1061
+ .min(1)
1062
+ .max(64)
1063
+ .optional()
1064
+ .describe("Timing policy pinned with trusted first-outbound evidence. Banded occurrences use timing-v2-service-bands (realtime 1000ms, short bulk 10000ms, bulk 60000ms from plannedFor); legacy occurrences without a band retain their recorded policy, including payg-v1, and 5000ms/15000ms thresholds. Absent timing evidence means unknown."),
977
1065
  };
978
1066
  /**
979
1067
  * One concrete occurrence of a schedule.
@@ -983,7 +1071,7 @@ const timingEvidenceShape = {
983
1071
  * numbering starts again. Together `(schedule, generation, sequence)` is what
984
1072
  * makes an occurrence identifiable without depending on when it was written.
985
1073
  */
986
- export const occurrenceSchema = z
1074
+ export const occurrenceSchema = /* @__PURE__ */ (() => z
987
1075
  .object({
988
1076
  attemptCount: z.number().int().nonnegative(),
989
1077
  ...timingEvidenceShape,
@@ -992,6 +1080,13 @@ export const occurrenceSchema = z
992
1080
  /** When the occurrence was planned to fire. Never earlier than this. */
993
1081
  plannedFor: timestampSchema,
994
1082
  schedule: scheduleIdSchema,
1083
+ /**
1084
+ * The schedule's name, on the project executions listing
1085
+ * (`executions.list`) only, read from the same schedule rows that page
1086
+ * already joins — so a list of runs can say whose they are without a
1087
+ * schedule read per row (W9-80). Absent elsewhere.
1088
+ */
1089
+ scheduleName: z.string().optional(),
995
1090
  sequence: z.number().int().positive(),
996
1091
  /** Present exactly when the occurrence is in a terminal state. */
997
1092
  settledAt: timestampSchema.optional(),
@@ -1014,7 +1109,7 @@ export const occurrenceSchema = z
1014
1109
  .refine(hasConsistentTimingEligibility, {
1015
1110
  message: "timingEligibilityReason is required for a blocked population and absent otherwise",
1016
1111
  path: ["timingEligibilityReason"],
1017
- });
1112
+ }))();
1018
1113
  /**
1019
1114
  * One delivery attempt against one occurrence.
1020
1115
  *
@@ -1039,11 +1134,11 @@ export const occurrenceSchema = z
1039
1134
  * can look up in their own logs. Cookies, `Authorization` and
1040
1135
  * `WWW-Authenticate` have no representation here at all.
1041
1136
  */
1042
- export const attemptResponseHeaderSchema = z.object({
1137
+ export const attemptResponseHeaderSchema = /* @__PURE__ */ (() => z.object({
1043
1138
  /** Lowercase, from the published allowlist. */
1044
1139
  name: z.string().min(1).max(64),
1045
1140
  value: z.string().max(ATTEMPT_RESPONSE_HEADER_MAX_LENGTH),
1046
- });
1141
+ }))();
1047
1142
  /**
1048
1143
  * What a failing receiver said, bounded and access-controlled.
1049
1144
  *
@@ -1057,14 +1152,14 @@ export const attemptResponseHeaderSchema = z.object({
1057
1152
  * that is exactly the bound and a body that was cut at the bound are different
1058
1153
  * facts and only one of them should show a customer an ellipsis.
1059
1154
  */
1060
- export const attemptFailureSnippetSchema = z.object({
1155
+ export const attemptFailureSnippetSchema = /* @__PURE__ */ (() => z.object({
1061
1156
  /** The media type it was captured from. Not customer content. */
1062
1157
  contentType: z.string().min(1).max(128),
1063
1158
  text: z.string().max(ATTEMPT_FAILURE_SNIPPET_MAX_LENGTH),
1064
1159
  truncated: z.boolean(),
1065
- });
1160
+ }))();
1066
1161
  /** Stored diagnostic content, available only under the attempt's verified authority. */
1067
- export const attemptCapturedBodySchema = z.object({
1162
+ export const attemptCapturedBodySchema = /* @__PURE__ */ (() => z.object({
1068
1163
  contentType: z.string(),
1069
1164
  encoding: z.enum(["utf8", "base64"]),
1070
1165
  // Up to 10 KiB received bytes; credential masking can expand the representation.
@@ -1074,27 +1169,46 @@ export const attemptCapturedBodySchema = z.object({
1074
1169
  truncated: z.boolean(),
1075
1170
  redacted: z.boolean().optional(),
1076
1171
  incomplete: z.boolean().optional(),
1077
- });
1078
- export const attemptResponseSchema = z.object({
1172
+ }))();
1173
+ export const attemptResponseSchema = /* @__PURE__ */ (() => z.object({
1079
1174
  statusText: z.string(),
1080
1175
  headers: z.array(z.object({ name: z.string(), value: z.string() })),
1081
1176
  bodyState: z.enum(["captured", "empty", "expired", "not_captured"]),
1082
1177
  body: attemptCapturedBodySchema.optional(),
1083
- });
1084
- export const attemptRequestSchema = z.object({
1178
+ }))();
1179
+ export const attemptRequestSchema = /* @__PURE__ */ (() => z.object({
1085
1180
  method: z.enum(["GET", "POST", "PUT"]),
1086
1181
  url: z.string(),
1087
1182
  headers: z.array(z.object({ name: z.string(), value: z.string() })),
1088
1183
  body: z.string(),
1089
- });
1090
- export const attemptSchema = z
1091
- .object({
1184
+ }))();
1185
+ /** Shared validators for attempt search summaries and full attempt details. */
1186
+ export const attemptSearchSummaryShape = /* @__PURE__ */ (() => ({
1092
1187
  attemptNumber: z.number().int().positive(),
1093
- /** Present exactly when the attempt has been classified. */
1094
1188
  classifiedAt: timestampSchema.optional(),
1189
+ diagnostic: attemptDiagnosticSchema.optional(),
1190
+ durationMs: z
1191
+ .number()
1192
+ .int()
1193
+ .nonnegative()
1194
+ .max(ATTEMPT_DURATION_MAX_MS)
1195
+ .optional(),
1196
+ id: attemptIdSchema,
1197
+ occurrence: occurrenceIdSchema,
1198
+ remediation: attemptRemediationSchema.optional(),
1199
+ responseStatus: z.number().int().min(100).max(599).optional(),
1200
+ startedAt: timestampSchema,
1201
+ state: attemptStateSchema.optional(),
1202
+ trigger: attemptTriggerSchema,
1203
+ }))();
1204
+ export const attemptSchema = /* @__PURE__ */ (() => z
1205
+ .object({
1206
+ attemptNumber: attemptSearchSummaryShape.attemptNumber,
1207
+ /** Present exactly when the attempt has been classified. */
1208
+ classifiedAt: attemptSearchSummaryShape.classifiedAt,
1095
1209
  delivery: deliveryIdSchema,
1096
1210
  /** Safe classifier code; absent when durable evidence predates diagnostics. */
1097
- diagnostic: attemptDiagnosticSchema.optional(),
1211
+ diagnostic: attemptSearchSummaryShape.diagnostic,
1098
1212
  /**
1099
1213
  * Instrumented re-drives of the same dispatching occurrence under a new
1100
1214
  * dispatch epoch. Includes deliberate admission deferrals; this is not a
@@ -1102,14 +1216,9 @@ export const attemptSchema = z
1102
1216
  */
1103
1217
  dispatchRecoveries: z.int().nonnegative().optional(),
1104
1218
  /** Executor-observed elapsed time; absent when no durable timing survived. */
1105
- durationMs: z
1106
- .number()
1107
- .int()
1108
- .nonnegative()
1109
- .max(ATTEMPT_DURATION_MAX_MS)
1110
- .optional(),
1219
+ durationMs: attemptSearchSummaryShape.durationMs,
1111
1220
  ...timingEvidenceShape,
1112
- id: attemptIdSchema,
1221
+ id: attemptSearchSummaryShape.id,
1113
1222
  /**
1114
1223
  * Instrumented send authorizations granted after a later local alarm
1115
1224
  * replayed the original wake receipt. Preserves the attempt and delivery
@@ -1118,7 +1227,7 @@ export const attemptSchema = z
1118
1227
  * older responses.
1119
1228
  */
1120
1229
  localReceiptRecoveries: z.int().nonnegative().optional(),
1121
- occurrence: occurrenceIdSchema,
1230
+ occurrence: attemptSearchSummaryShape.occurrence,
1122
1231
  /**
1123
1232
  * How many times the watchdog took this attempt's send back and sent it
1124
1233
  * again: the send was authorized, and its outcome was never recorded
@@ -1129,7 +1238,7 @@ export const attemptSchema = z
1129
1238
  */
1130
1239
  rescues: z.int().positive().optional(),
1131
1240
  /** Stable next-action code paired with a diagnostic when one is known. */
1132
- remediation: attemptRemediationSchema.optional(),
1241
+ remediation: attemptSearchSummaryShape.remediation,
1133
1242
  /**
1134
1243
  * Compatibility text prefix, up to 4 KiB, derived from the retained
1135
1244
  * response when its media type is supported by legacy clients. Use
@@ -1140,13 +1249,29 @@ export const attemptSchema = z
1140
1249
  response: attemptResponseSchema.optional(),
1141
1250
  request: attemptRequestSchema.optional(),
1142
1251
  requestState: z.enum(["captured", "expired", "not_captured"]).optional(),
1252
+ /**
1253
+ * Why a successful attempt's request cannot be shown, when
1254
+ * `requestState` is `not_captured` for that reason. A successful
1255
+ * attempt's request is rebuilt from the schedule's payload and its
1256
+ * endpoint, so it is shown only while neither has changed since the run
1257
+ * and the payload still exists; it is never shown from a later version.
1258
+ * Absent otherwise.
1259
+ */
1260
+ requestUnavailable: z
1261
+ .enum([
1262
+ "schedule_changed",
1263
+ "endpoint_changed",
1264
+ "endpoint_deleted",
1265
+ "payload_deleted",
1266
+ ])
1267
+ .optional(),
1143
1268
  /** The fixed diagnostic-header allowlist, when a response carried any. */
1144
1269
  responseHeaders: z.array(attemptResponseHeaderSchema).max(8).optional(),
1145
1270
  /** Absent when no response was received at all. */
1146
- responseStatus: z.number().int().min(100).max(599).optional(),
1147
- startedAt: timestampSchema,
1148
- state: attemptStateSchema.optional(),
1149
- trigger: attemptTriggerSchema,
1271
+ responseStatus: attemptSearchSummaryShape.responseStatus,
1272
+ startedAt: attemptSearchSummaryShape.startedAt,
1273
+ state: attemptSearchSummaryShape.state,
1274
+ trigger: attemptSearchSummaryShape.trigger,
1150
1275
  })
1151
1276
  // Classification and its timestamp arrive together or not at all — the same
1152
1277
  // pairing the partition's CHECK constraint enforces. Independently optional
@@ -1185,30 +1310,30 @@ export const attemptSchema = z
1185
1310
  .refine(hasConsistentTimingEligibility, {
1186
1311
  message: "timingEligibilityReason is required for a blocked population and absent otherwise",
1187
1312
  path: ["timingEligibilityReason"],
1188
- });
1313
+ }))();
1189
1314
  /** An authoritative page over one schedule's retained occurrences. */
1190
- export const occurrencePageSchema = pageSchema(occurrenceSchema).extend({
1315
+ export const occurrencePageSchema = /* @__PURE__ */ (() => pageSchema(occurrenceSchema).extend({
1191
1316
  freshness: z.object({ kind: z.literal("authoritative") }),
1192
- });
1317
+ }))();
1193
1318
  /** A project-wide occurrence page and its plan's normal history boundary. */
1194
- export const projectOccurrencePageSchema = pageSchema(occurrenceSchema).extend({
1319
+ export const projectOccurrencePageSchema = /* @__PURE__ */ (() => pageSchema(occurrenceSchema).extend({
1195
1320
  freshness: z.object({ kind: z.literal("authoritative") }),
1196
1321
  retention: z.object({ from: timestampSchema }),
1197
- });
1322
+ }))();
1198
1323
  /** An authoritative occurrence detail response. */
1199
- export const occurrenceViewSchema = z.object({
1324
+ export const occurrenceViewSchema = /* @__PURE__ */ (() => z.object({
1200
1325
  freshness: z.object({ kind: z.literal("authoritative") }),
1201
1326
  occurrence: occurrenceSchema,
1202
- });
1327
+ }))();
1203
1328
  /** An authoritative page over one retained occurrence's immutable attempts. */
1204
- export const attemptPageSchema = pageSchema(attemptSchema).extend({
1329
+ export const attemptPageSchema = /* @__PURE__ */ (() => pageSchema(attemptSchema).extend({
1205
1330
  freshness: z.object({ kind: z.literal("authoritative") }),
1206
- });
1331
+ }))();
1207
1332
  /** An authoritative attempt detail or redelivery response. */
1208
- export const attemptViewSchema = z.object({
1333
+ export const attemptViewSchema = /* @__PURE__ */ (() => z.object({
1209
1334
  attempt: attemptSchema,
1210
1335
  freshness: z.object({ kind: z.literal("authoritative") }),
1211
- });
1336
+ }))();
1212
1337
  /**
1213
1338
  * A schedule together with the occurrence that is next to happen.
1214
1339
  *
@@ -1217,19 +1342,19 @@ export const attemptViewSchema = z.object({
1217
1342
  * that has to make a second request to answer it will cache the first answer and
1218
1343
  * be wrong.
1219
1344
  */
1220
- export const scheduleViewSchema = z.object({
1345
+ export const scheduleViewSchema = /* @__PURE__ */ (() => z.object({
1221
1346
  occurrence: occurrenceSchema,
1222
1347
  schedule: scheduleSchema,
1223
- });
1348
+ }))();
1224
1349
  /**
1225
1350
  * What a schedule create or replace answers: the schedule view, and — when the
1226
1351
  * request named its destination by `url` — where that endpoint's signing
1227
1352
  * secret is (W9-44). Present only then, because only then can the caller have
1228
1353
  * an endpoint whose secret it never held.
1229
1354
  */
1230
- export const scheduleWrittenViewSchema = scheduleViewSchema.extend({
1355
+ export const scheduleWrittenViewSchema = /* @__PURE__ */ (() => scheduleViewSchema.extend({
1231
1356
  endpointSigningSecret: signingSecretNoticeSchema.optional(),
1232
- });
1357
+ }))();
1233
1358
  /**
1234
1359
  * The four ways a caller may say when a schedule happens.
1235
1360
  *
@@ -1259,7 +1384,7 @@ export const RECURRENCE_REQUEST_FIELDS = [
1259
1384
  * `ambiguous_schedule`, and a scheduler that silently picks one of two instants
1260
1385
  * a customer named is the worst kind of helpful.
1261
1386
  */
1262
- export const scheduleRequestSchema = z
1387
+ export const scheduleRequestSchema = /* @__PURE__ */ (() => z
1263
1388
  .object({
1264
1389
  autoPauseAfterFailures: autoPauseAfterFailuresSchema.optional(),
1265
1390
  http: httpRequestSchema.optional(),
@@ -1361,20 +1486,27 @@ export const scheduleRequestSchema = z
1361
1486
  */
1362
1487
  url: callbackUrlSchema().optional(),
1363
1488
  /**
1364
- * How promptly this schedule's occurrences expect to be served.
1489
+ * How promptly this schedule's occurrences expect to be served: `auto`,
1490
+ * `bulk` or `realtime` (W12-02, FXO-3171).
1365
1491
  *
1366
- * Optional, and omitting it is the ordinary case: the default is derived
1367
- * from how the schedule recurs (`deriveLatencyClass`). Stating it is for
1492
+ * Omitting it means `auto`, the ordinary case. `auto` makes a one-shot
1493
+ * planned less than a minute after acceptance `realtime` and everything
1494
+ * else `bulk`; a recurrence is classified by the gap between its runs, so
1495
+ * it is `bulk`. Naming `realtime` or `bulk` always gets that class — for
1368
1496
  * the caller whose nightly cron really is a human waiting, or whose
1369
- * thirty-second one-shot really is a campaign — the derivation is a good
1370
- * guess about intent and cannot be a correct one.
1497
+ * thirty-second one-shot really is a campaign.
1498
+ *
1499
+ * Bulk work becomes eligible on a 5-second boundary when its runs are
1500
+ * under five minutes apart (or it is due within five minutes) and on a
1501
+ * 50-second boundary otherwise; realtime on a 250-millisecond boundary.
1502
+ * The occurrence's `plannedFor` is unchanged. Each occurrence is charged
1503
+ * as the class it resolved to.
1371
1504
  *
1372
- * It buys preference, not a promise. `realtime` is served ahead of `bulk`
1373
- * within the same partition; it does not raise the partition's rate, so a
1374
- * caller who marks everything `realtime` has re-labelled their work rather
1375
- * than obtained more of the service.
1505
+ * On a replacement, omission keeps the schedule's mode. Changing the
1506
+ * timing resolves again under that mode; changing anything else keeps
1507
+ * the class and price the schedule was accepted under.
1376
1508
  */
1377
- latencyClass: latencyClassSchema.optional(),
1509
+ latencyClass: requestedLatencyClassSchema.optional(),
1378
1510
  /**
1379
1511
  * What to do when an occurrence reaches its first attempt late.
1380
1512
  *
@@ -1493,7 +1625,7 @@ export const scheduleRequestSchema = z
1493
1625
  path: ["delivery", "completion"],
1494
1626
  });
1495
1627
  }
1496
- });
1628
+ }))();
1497
1629
  /** Resolve the lateness settings a create or replacement request runs under. */
1498
1630
  export function deriveLatenessSettings(request) {
1499
1631
  const recurrenceKind = request.cron !== undefined
@@ -1529,29 +1661,46 @@ export function deriveDeliverySettings(request) {
1529
1661
  onOverlap: request.delivery?.onOverlap ?? DEFAULT_DELIVERY_OVERLAP_POLICY,
1530
1662
  };
1531
1663
  }
1664
+ /**
1665
+ * The create request as its idempotency fingerprint reads it (W12-02).
1666
+ *
1667
+ * An omitted `latencyClass` means `auto`, so the two spellings are one request
1668
+ * and must fingerprint alike. They are made alike by writing the default the
1669
+ * way the request has always been able to write it — by omission — so a
1670
+ * receipt issued before `auto` existed still recognizes its own retry.
1671
+ */
1672
+ export function fingerprintedScheduleRequest(request) {
1673
+ if (request.latencyClass !== "auto")
1674
+ return request;
1675
+ const { latencyClass: _auto, ...rest } = request;
1676
+ void _auto;
1677
+ return rest;
1678
+ }
1679
+ /**
1680
+ * The concrete class a create request resolves to at `nowMs`, its acceptance
1681
+ * time. Here so REST, MCP and the dashboard's preview reach the same answer
1682
+ * for the same body; `service-mode.ts` owns the rule.
1683
+ *
1684
+ * A recurrence is classified by the gap between runs, at least a minute for
1685
+ * anything this surface accepts, so `auto` makes it `bulk` without evaluating
1686
+ * a cron expression here. `nowMs` is a parameter so the boundary can be asked
1687
+ * about one millisecond either side of it.
1688
+ */
1532
1689
  export function deriveLatencyClass(request, nowMs) {
1533
- if (request.latencyClass !== undefined) {
1534
- return request.latencyClass;
1535
- }
1690
+ const requested = request.latencyClass ?? "auto";
1536
1691
  if (request.cron !== undefined || request.everySeconds !== undefined) {
1537
- return "bulk";
1538
- }
1539
- const threshold = SCHEDULING_BOUNDS.realtimeOneShotSeconds * 1000;
1540
- if (request.delaySeconds !== undefined) {
1541
- return request.delaySeconds * 1000 <= threshold ? "realtime" : "bulk";
1542
- }
1543
- if (request.at === undefined) {
1544
- // No recurrence at all, which the schema refuses before this is reached.
1545
- // Answered rather than thrown so a caller of this function on an unvalidated
1546
- // body gets the conservative class instead of an exception.
1547
- return "bulk";
1548
- }
1549
- const atMs = Date.parse(request.at);
1550
- if (Number.isNaN(atMs)) {
1551
- return "bulk";
1692
+ return resolveService(requested, request.everySeconds === undefined
1693
+ ? SCHEDULING_BOUNDS.everySecondsMin * 1000
1694
+ : request.everySeconds * 1000).latencyClass;
1552
1695
  }
1553
- // An instant already in the past is as immediate as an instant gets, so the
1554
- // comparison is against the delay from now and not against its absolute
1555
- // value: `at` one second ago is realtime, and `at` a year out is not.
1556
- return atMs - nowMs <= threshold ? "realtime" : "bulk";
1696
+ const atMs = request.delaySeconds !== undefined
1697
+ ? nowMs + request.delaySeconds * 1000
1698
+ : request.at === undefined
1699
+ ? Number.NaN
1700
+ : Date.parse(request.at);
1701
+ // No instant at all, which the schema refuses before this is reached, is
1702
+ // answered conservatively rather than thrown.
1703
+ return Number.isNaN(atMs)
1704
+ ? resolveService(requested, Infinity).latencyClass
1705
+ : resolveService(requested, oneOffSpacingMs(nowMs, atMs)).latencyClass;
1557
1706
  }