deepline 0.3.115 → 0.3.117

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 (28) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +356 -12
  2. package/dist/bundling-sources/sdk/src/index.ts +12 -1
  3. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  4. package/dist/bundling-sources/shared_libs/monitors/billing/state.ts +36 -6
  5. package/dist/bundling-sources/shared_libs/monitors/billing/types.ts +0 -1
  6. package/dist/bundling-sources/shared_libs/monitors/deactivation-reasons.ts +6 -3
  7. package/dist/bundling-sources/shared_libs/monitors/fleets/verdict.ts +52 -15
  8. package/dist/bundling-sources/shared_libs/monitors/monitor-job-state.ts +44 -0
  9. package/dist/bundling-sources/shared_libs/monitors/package.json +2 -0
  10. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +76 -3
  11. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-state.ts +81 -6
  12. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-status.ts +26 -10
  13. package/dist/bundling-sources/shared_libs/observability/queue-health.ts +128 -0
  14. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +18 -0
  15. package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +1 -0
  16. package/dist/bundling-sources/shared_libs/plays/share/static-pipeline-summary.ts +2 -2
  17. package/dist/cli/index.js +413 -68
  18. package/dist/cli/index.mjs +413 -68
  19. package/dist/index.d.mts +155 -13
  20. package/dist/index.d.ts +155 -13
  21. package/dist/index.js +137 -2
  22. package/dist/index.mjs +134 -2
  23. package/dist/install-integrity.json +1 -0
  24. package/dist/release.d.mts +1 -1
  25. package/dist/release.d.ts +1 -1
  26. package/dist/release.js +1 -1
  27. package/dist/release.mjs +1 -1
  28. package/package.json +1 -1
@@ -8,23 +8,32 @@
8
8
  * the fact; this is the single reading of them.
9
9
  */
10
10
 
11
- export type FleetConvergenceCounts = {
12
- pending: number;
13
- submitting: number;
14
- rate_limited: number;
15
- settled: number;
16
- blocked: number;
17
- failed: number;
18
- };
11
+ import type { ProviderMonitorCustomerState } from '@deepline/monitors/provider-monitor-state';
12
+
13
+ export const FLEET_CONVERGENCE_STATES = [
14
+ 'pending',
15
+ 'submitting',
16
+ 'rate_limited',
17
+ 'settled',
18
+ 'blocked',
19
+ 'failed',
20
+ ] as const;
21
+
22
+ export type FleetConvergenceState = (typeof FLEET_CONVERGENCE_STATES)[number];
23
+
24
+ export type FleetConvergenceCounts = Record<FleetConvergenceState, number>;
19
25
 
20
26
  /** The lifecycle a fleet row records. */
21
- export type FleetLifecycleStatus =
22
- | 'active'
23
- | 'deactivating'
24
- | 'deactivated'
25
- | 'billing_suspending'
26
- | 'billing_suspended'
27
- | 'billing_reactivating';
27
+ export const FLEET_LIFECYCLE_STATUSES = [
28
+ 'active',
29
+ 'deactivating',
30
+ 'deactivated',
31
+ 'billing_suspending',
32
+ 'billing_suspended',
33
+ 'billing_reactivating',
34
+ ] as const;
35
+
36
+ export type FleetLifecycleStatus = (typeof FLEET_LIFECYCLE_STATUSES)[number];
28
37
 
29
38
  /** What a caller is told the fleet is doing. */
30
39
  export type FleetStatusVerdict =
@@ -37,6 +46,34 @@ export type FleetStatusVerdict =
37
46
  | 'billing_suspended'
38
47
  | 'billing_reactivating';
39
48
 
49
+ /**
50
+ * The Fleet's stable customer state. Convergence and lifecycle transitions are
51
+ * reported by the durable Fleet operation, not by growing this union.
52
+ */
53
+ export function fleetCustomerState(input: {
54
+ lifecycle: FleetLifecycleStatus;
55
+ verdict: FleetStatusVerdict;
56
+ convergedGeneration: number | null;
57
+ }): ProviderMonitorCustomerState {
58
+ switch (input.lifecycle) {
59
+ case 'billing_suspending':
60
+ case 'billing_suspended':
61
+ case 'billing_reactivating':
62
+ return 'billing_suspended';
63
+ case 'deactivating':
64
+ case 'deactivated':
65
+ return 'disabled';
66
+ case 'active':
67
+ // A new Fleet is not active merely because its desired lifecycle says
68
+ // so. It becomes active after one generation has been provider-proven.
69
+ // A later convergence may keep serving that last proven generation.
70
+ return input.verdict !== 'degraded' &&
71
+ (input.verdict === 'converged' || input.convergedGeneration !== null)
72
+ ? 'active'
73
+ : 'disabled';
74
+ }
75
+ }
76
+
40
77
  const ZERO_FLEET_COUNTS: FleetConvergenceCounts = {
41
78
  pending: 0,
42
79
  submitting: 0,
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared vocabulary for the public Monitor Job projection.
3
+ *
4
+ * A job reports progress toward provider truth. None of these values are a
5
+ * monitor lifecycle state, and no cron/admin path may invent another job
6
+ * vocabulary. Convex validators repeat these literals at the storage ABI
7
+ * boundary; the contract tests keep that deliberate mirror aligned.
8
+ */
9
+ export const MONITOR_JOB_STATES = [
10
+ 'queued',
11
+ 'running',
12
+ 'cancelling',
13
+ 'succeeded',
14
+ 'failed',
15
+ 'cancelled',
16
+ ] as const;
17
+
18
+ export type MonitorJobState = (typeof MONITOR_JOB_STATES)[number];
19
+
20
+ export const MONITOR_JOB_PHASES = [
21
+ 'queued',
22
+ 'provider_create',
23
+ 'provider_readback',
24
+ 'reconciliation',
25
+ 'completed',
26
+ ] as const;
27
+
28
+ export type MonitorJobPhase = (typeof MONITOR_JOB_PHASES)[number];
29
+
30
+ export const MONITOR_JOB_RECONCILIATION_STATES = [
31
+ 'pending',
32
+ 'aligned',
33
+ 'partial',
34
+ 'mismatched',
35
+ ] as const;
36
+
37
+ export type MonitorJobReconciliationState =
38
+ (typeof MONITOR_JOB_RECONCILIATION_STATES)[number];
39
+
40
+ export const TERMINAL_MONITOR_JOB_STATES = [
41
+ 'succeeded',
42
+ 'failed',
43
+ 'cancelled',
44
+ ] as const satisfies readonly MonitorJobState[];
@@ -53,6 +53,8 @@
53
53
  "./lifecycle-notifications.js": "./lifecycle-notifications.ts",
54
54
  "./monitor-fleet-limits": "./monitor-fleet-limits.ts",
55
55
  "./monitor-fleet-limits.js": "./monitor-fleet-limits.ts",
56
+ "./monitor-job-state": "./monitor-job-state.ts",
57
+ "./monitor-job-state.js": "./monitor-job-state.ts",
56
58
  "./org-monitor-limits": "./org-monitor-limits.ts",
57
59
  "./org-monitor-limits.js": "./org-monitor-limits.ts",
58
60
  "./provider-callback-verification": "./provider-callback-verification.ts",
@@ -21,6 +21,10 @@ export const PROVIDER_MONITOR_BATCH_OPERATION_KINDS = [
21
21
  // operation is the durable, observable hand-off to the fenced Fleet
22
22
  // executor; it is not a second lifecycle verb or a raw provider create.
23
23
  'converge',
24
+ // A named Batch owns a bounded mixed plan (sync/start/stop) under one
25
+ // durable operation. It is intentionally distinct from manifest deploy:
26
+ // omission/removal semantics belong to the Batch desired-set owner.
27
+ 'named_batch',
24
28
  ] as const;
25
29
 
26
30
  export type ProviderMonitorBatchOperationKind =
@@ -66,12 +70,25 @@ export type ProviderMonitorBatchScope =
66
70
  | { kind: 'target'; target: ProviderMonitorBatchTarget }
67
71
  | { kind: 'fleet'; fleetId: string }
68
72
  | ProviderMonitorBatchManifestScope
73
+ | {
74
+ kind: 'named_batch';
75
+ batchId: string;
76
+ generation: number;
77
+ intent: 'sync' | 'start' | 'stop';
78
+ }
69
79
  | ProviderMonitorBatchGlobalAuditScope;
70
80
 
71
81
  export type ProviderMonitorBatchOperationRequest = {
72
82
  kind: ProviderMonitorBatchOperationKind;
73
83
  scope: ProviderMonitorBatchScope;
74
84
  dryRun: boolean;
85
+ /**
86
+ * Optional ownership mode for exact customer lifecycle requests. Omitted
87
+ * means the system/policy runner may operate the selected definitions. The
88
+ * field is deliberately additive so older operation rows and callers keep
89
+ * their original fingerprint and behavior.
90
+ */
91
+ control?: 'customer_exact' | 'system';
75
92
  /**
76
93
  * The application deployment that accepted this operation. A provider
77
94
  * callback is part of the create side effect, so a continuation must retain
@@ -93,6 +110,17 @@ export type ProviderMonitorBatchOperationState =
93
110
  | 'failed'
94
111
  | 'cancelled';
95
112
 
113
+ /** Per-monitor evidence written by a durable batch operation. */
114
+ export const PROVIDER_MONITOR_BATCH_RESULT_STATES = [
115
+ 'done',
116
+ 'retry',
117
+ 'blocked',
118
+ 'superseded',
119
+ ] as const;
120
+
121
+ export type ProviderMonitorBatchResultState =
122
+ (typeof PROVIDER_MONITOR_BATCH_RESULT_STATES)[number];
123
+
96
124
  const IDEMPOTENCY_KEY = /^[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/;
97
125
 
98
126
  function canonicalJson(value: unknown): string {
@@ -176,14 +204,40 @@ export function validateProviderMonitorBatchOperationRequest(
176
204
  if (!isProviderMonitorBatchOperationKind(request.kind)) {
177
205
  throw new Error(`Unknown monitor batch operation '${request.kind}'.`);
178
206
  }
207
+ if (
208
+ request.control !== undefined &&
209
+ request.control !== 'customer_exact' &&
210
+ request.control !== 'system'
211
+ ) {
212
+ throw new Error(`Unknown monitor batch control '${request.control}'.`);
213
+ }
179
214
  const callbackBaseUrl = normalizeCallbackBaseUrl(request.callbackBaseUrl);
180
- if (callbackBaseUrl !== undefined && request.kind !== 'deploy') {
181
- throw new Error('callbackBaseUrl is only valid for a batch deploy.');
215
+ if (
216
+ callbackBaseUrl !== undefined &&
217
+ request.kind !== 'deploy' &&
218
+ request.kind !== 'named_batch'
219
+ ) {
220
+ throw new Error(
221
+ 'callbackBaseUrl is only valid for a batch deploy or named Batch sync.',
222
+ );
182
223
  }
183
224
  if (!request.scope || typeof request.scope !== 'object') {
184
225
  throw new Error('A monitor batch operation requires exactly one scope.');
185
226
  }
186
227
  const scope = request.scope;
228
+ if (request.kind === 'named_batch' && scope.kind !== 'named_batch') {
229
+ throw new Error('A named_batch operation requires a named Batch scope.');
230
+ }
231
+ if (
232
+ callbackBaseUrl !== undefined &&
233
+ request.kind === 'named_batch' &&
234
+ scope.kind === 'named_batch' &&
235
+ scope.intent !== 'sync'
236
+ ) {
237
+ throw new Error(
238
+ 'callbackBaseUrl is only valid for a named Batch sync operation.',
239
+ );
240
+ }
187
241
  if (scope.kind === 'keys') {
188
242
  const keys = [...new Set(scope.monitorKeys.map((key) => key.trim()))];
189
243
  if (
@@ -294,6 +348,23 @@ export function validateProviderMonitorBatchOperationRequest(
294
348
  ...(callbackBaseUrl === undefined ? {} : { callbackBaseUrl }),
295
349
  };
296
350
  }
351
+ if (scope.kind === 'named_batch') {
352
+ const batchId = scope.batchId.trim();
353
+ if (!batchId || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(batchId)) {
354
+ throw new Error('A named Batch scope requires a valid batch id.');
355
+ }
356
+ if (!Number.isSafeInteger(scope.generation) || scope.generation < 1) {
357
+ throw new Error('A named Batch scope requires a positive generation.');
358
+ }
359
+ if (request.kind !== 'named_batch') {
360
+ throw new Error('A named Batch scope is only valid for named_batch.');
361
+ }
362
+ return {
363
+ ...request,
364
+ scope: { ...scope, batchId },
365
+ ...(callbackBaseUrl === undefined ? {} : { callbackBaseUrl }),
366
+ };
367
+ }
297
368
  if (scope.kind === 'global_audit') {
298
369
  const runId = scope.runId.trim();
299
370
  const monitorKey = scope.monitorKey?.trim();
@@ -304,7 +375,9 @@ export function validateProviderMonitorBatchOperationRequest(
304
375
  throw new Error('A global-audit scope is only valid for repair.');
305
376
  }
306
377
  if (scope.monitorKey !== undefined && !monitorKey) {
307
- throw new Error('A global-audit monitor key, when supplied, must be non-empty.');
378
+ throw new Error(
379
+ 'A global-audit monitor key, when supplied, must be non-empty.',
380
+ );
308
381
  }
309
382
  return {
310
383
  ...request,
@@ -1,9 +1,22 @@
1
1
  /**
2
- * The customer-visible state of a monitor. Provider work and audit evidence
3
- * intentionally live on separate axes; neither may add a third value here.
2
+ * The complete customer-visible lifecycle of a monitor.
3
+ *
4
+ * Provider work, retries, and audit evidence intentionally live on separate
5
+ * axes. They must never become additional values in this union.
6
+ */
7
+ export type ProviderMonitorCustomerState =
8
+ | 'active'
9
+ | 'billing_suspended'
10
+ | 'disabled';
11
+
12
+ /**
13
+ * The old two-value desired-state type. Keep it for control-plane callers and
14
+ * the deprecated `status` response alias while clients migrate to `state`.
4
15
  */
5
16
  export type ProviderMonitorState = 'active' | 'inactive';
6
17
 
18
+ export type ProviderMonitorStatusAlias = ProviderMonitorState;
19
+
7
20
  declare const providerMonitorResourceIdBrand: unique symbol;
8
21
 
9
22
  /**
@@ -64,7 +77,9 @@ export function providerMonitorOperationalState(input: {
64
77
  desired: ProviderMonitorState;
65
78
  providerResourceId: string | null | undefined;
66
79
  }): OperationalProviderMonitorState {
67
- const providerResourceId = providerMonitorResourceId(input.providerResourceId);
80
+ const providerResourceId = providerMonitorResourceId(
81
+ input.providerResourceId,
82
+ );
68
83
  if (input.desired === 'active' && providerResourceId !== null) {
69
84
  return {
70
85
  state: 'active',
@@ -81,6 +96,62 @@ export function providerMonitorOperationalState(input: {
81
96
  };
82
97
  }
83
98
 
99
+ /**
100
+ * Project untrusted persisted billing state onto the only billing lifecycle
101
+ * values that are meaningful to a customer. Cleanup progress is a job detail;
102
+ * an unresolved billing admission is unavailable, not a new public state.
103
+ */
104
+ export function providerMonitorCustomerStateForBillingState(
105
+ billingState: unknown,
106
+ ): ProviderMonitorCustomerState {
107
+ switch (billingState) {
108
+ case 'active':
109
+ return 'active';
110
+ case 'billing_suspended':
111
+ case 'upstream_disable_pending':
112
+ case 'upstream_disable_failed':
113
+ return 'billing_suspended';
114
+ case 'disabled':
115
+ case 'billing_pending':
116
+ case 'preflight_denied':
117
+ default:
118
+ return 'disabled';
119
+ }
120
+ }
121
+
122
+ /**
123
+ * The single public monitor-state projection. A billing suspension owns the
124
+ * off state. Otherwise `active` is operational proof: a committed active
125
+ * lifecycle and a non-empty provider resource binding. Internal billing
126
+ * bookkeeping such as an unresolved usage charge must not make a usable
127
+ * provider resource look disabled.
128
+ */
129
+ export function providerMonitorCustomerState(input: {
130
+ lifecycleStatus?: string | null;
131
+ billingState?: unknown;
132
+ providerResourceId?: string | null | undefined;
133
+ }): ProviderMonitorCustomerState {
134
+ const billingState = providerMonitorCustomerStateForBillingState(
135
+ input.billingState,
136
+ );
137
+ if (billingState === 'billing_suspended') return billingState;
138
+
139
+ const providerResourceId = providerMonitorResourceId(
140
+ input.providerResourceId,
141
+ );
142
+ if (input.lifecycleStatus === 'active' && providerResourceId !== null) {
143
+ return 'active';
144
+ }
145
+ return 'disabled';
146
+ }
147
+
148
+ /** Deprecated compatibility projection for the old `status`/`active` keys. */
149
+ export function providerMonitorStatusAlias(
150
+ state: ProviderMonitorCustomerState,
151
+ ): ProviderMonitorStatusAlias {
152
+ return state === 'active' ? 'active' : 'inactive';
153
+ }
154
+
84
155
  /**
85
156
  * Runtime boundary for code that is about to publish or administer an active
86
157
  * resource. Its return type is the same proof exposed to callers: an active
@@ -102,9 +173,13 @@ export function requireActiveProviderMonitorBinding(input: {
102
173
  }
103
174
 
104
175
  /**
105
- * Translate the historical control-record lifecycle label at the sole
106
- * compatibility boundary. New code persists `desired` and returns this type;
107
- * this mapping exists only while the online backfill drains existing rows.
176
+ * Recover desired intent from a historical control-record lifecycle label at
177
+ * the sole compatibility boundary. This does not prove that a monitor is
178
+ * operationally active: customer projections must still require a committed
179
+ * provider resource through {@link providerMonitorCustomerState}.
180
+ *
181
+ * New code persists `desired`; this mapping exists only while the online
182
+ * backfill drains existing rows.
108
183
  */
109
184
  export function providerMonitorStateForLegacyStatus(
110
185
  status: string | undefined,
@@ -1,10 +1,26 @@
1
- export type ProviderMonitorStatus =
2
- | 'replacement_pending'
3
- | 'replacement_active_old_cleanup_pending'
4
- | 'replacement_cleanup_failed'
5
- | 'active'
6
- | 'disabled'
7
- | 'deleted'
8
- | 'deleting'
9
- | 'error'
10
- | 'pending_create';
1
+ /** Current values allowed in providerMonitorDefinitions.status. */
2
+ export const PROVIDER_MONITOR_PERSISTED_STATUSES = [
3
+ 'active',
4
+ 'disabled',
5
+ 'deleted',
6
+ 'deleting',
7
+ 'error',
8
+ 'pending_create',
9
+ ] as const;
10
+
11
+ /** Compatibility-only inputs normalized by current writers before storage. */
12
+ export const PROVIDER_MONITOR_LEGACY_REPLACEMENT_STATUSES = [
13
+ 'replacement_pending',
14
+ 'replacement_active_old_cleanup_pending',
15
+ 'replacement_cleanup_failed',
16
+ ] as const;
17
+
18
+ export const PROVIDER_MONITOR_STATUSES = [
19
+ ...PROVIDER_MONITOR_LEGACY_REPLACEMENT_STATUSES,
20
+ ...PROVIDER_MONITOR_PERSISTED_STATUSES,
21
+ ] as const;
22
+
23
+ export type ProviderMonitorStatus = (typeof PROVIDER_MONITOR_STATUSES)[number];
24
+
25
+ export type PersistedProviderMonitorStatus =
26
+ (typeof PROVIDER_MONITOR_PERSISTED_STATUSES)[number];
@@ -456,6 +456,26 @@ export type QueueHealth = {
456
456
  readonly worstAgeRatio?: number;
457
457
  };
458
458
 
459
+ /**
460
+ * Small operator-facing state set for the dashboard and admin CLI.
461
+ *
462
+ * This is deliberately derived from the source-owned depth buckets. It is
463
+ * not a replacement for the queue's durable state machine and therefore does
464
+ * not create a new persisted state or a new retry policy.
465
+ */
466
+ export type QueueOperatorState =
467
+ | 'draining'
468
+ | 'stalled'
469
+ | 'held'
470
+ | 'quarantined'
471
+ | 'clear'
472
+ | 'unknown';
473
+
474
+ export type QueueOperatorStateSnapshot = {
475
+ readonly state: QueueOperatorState;
476
+ readonly reason: string;
477
+ };
478
+
459
479
  export const QUEUE_HEALTH_TELEMETRY_EVENT = 'operations.queue.health' as const;
460
480
  export const QUEUE_HEALTH_TELEMETRY_SCHEMA_VERSION = 1 as const;
461
481
  const UNOBSERVED_SIGNAL_REPEAT_MS = 5 * 60_000;
@@ -475,6 +495,8 @@ export type QueueHealthSignal = {
475
495
  readonly adapter: string;
476
496
  readonly observedAt: number;
477
497
  readonly health: QueueHealth;
498
+ readonly operatorState: QueueOperatorState;
499
+ readonly operatorReason: string;
478
500
  readonly previousHealth?: QueueHealth;
479
501
  readonly warningDefinition: string;
480
502
  readonly failureDefinition: string;
@@ -503,6 +525,11 @@ export function deriveQueueHealthSignal(input: {
503
525
  }): QueueHealthSignal | null {
504
526
  const now = input.now ?? input.observation.observedAt;
505
527
  const health = deriveQueueHealth(input.definition, input.observation, now);
528
+ const operator = deriveQueueOperatorState(
529
+ input.definition,
530
+ input.observation,
531
+ now,
532
+ );
506
533
  const previousHealth = input.previousObservation
507
534
  ? deriveQueueHealth(input.definition, input.previousObservation, now)
508
535
  : undefined;
@@ -546,6 +573,8 @@ export function deriveQueueHealthSignal(input: {
546
573
  adapter: input.observation.adapter,
547
574
  observedAt: input.observation.observedAt,
548
575
  health,
576
+ operatorState: operator.state,
577
+ operatorReason: operator.reason,
549
578
  ...(previousHealth === undefined ? {} : { previousHealth }),
550
579
  warningDefinition: input.definition.warningDefinition,
551
580
  failureDefinition: input.definition.failureDefinition,
@@ -694,6 +723,105 @@ export function deriveQueueHealth(
694
723
  };
695
724
  }
696
725
 
726
+ /**
727
+ * Collapse the detailed queue buckets into a small, actionable operator
728
+ * vocabulary. The precedence is intentional: quarantined and held work must
729
+ * not be hidden by a large ready count, while an old ready-only queue is
730
+ * called stalled only when no in-flight work is observed.
731
+ */
732
+ export function deriveQueueOperatorState(
733
+ definition: QueueDefinition,
734
+ observation: QueueObservation | undefined,
735
+ now = Date.now(),
736
+ ): QueueOperatorStateSnapshot {
737
+ const health = deriveQueueHealth(definition, observation, now);
738
+ if (
739
+ observation === undefined ||
740
+ health.status === 'unobserved' ||
741
+ health.reason === 'invalid_observation'
742
+ ) {
743
+ return {
744
+ state: 'unknown',
745
+ reason: 'Queue observation is missing or stale.',
746
+ };
747
+ }
748
+
749
+ const ready = measuredValue(observation.depth.ready);
750
+ const delayed = measuredValue(observation.depth.delayed);
751
+ const inFlight = measuredValue(observation.depth.inFlight);
752
+ const blocked = measuredValue(observation.depth.blocked);
753
+ const deadLettered = measuredValue(observation.depth.deadLettered);
754
+ const oldestReadyMs = measuredValue(observation.oldestAgeMs.ready);
755
+
756
+ if (deadLettered !== undefined && deadLettered > 0) {
757
+ return {
758
+ state: 'quarantined',
759
+ reason: 'Dead-lettered work requires an owner recovery path.',
760
+ };
761
+ }
762
+ if (blocked !== undefined && blocked > 0) {
763
+ return {
764
+ state: 'held',
765
+ reason: 'Blocked work is intentionally held for owner review.',
766
+ };
767
+ }
768
+
769
+ if (
770
+ Object.values(observation.depth).some(
771
+ (measurement) =>
772
+ measurement.kind === 'lower_bound' && measurement.value === 0,
773
+ )
774
+ ) {
775
+ return {
776
+ state: 'unknown',
777
+ reason: 'A bounded read cannot prove that the queue is clear.',
778
+ };
779
+ }
780
+ if (
781
+ Object.values(observation.depth).some(
782
+ (measurement) => measurement.kind === 'unavailable',
783
+ )
784
+ ) {
785
+ return {
786
+ state: 'unknown',
787
+ reason: 'A depth bucket is unavailable; queue state cannot be proven.',
788
+ };
789
+ }
790
+
791
+ if (
792
+ ready !== undefined &&
793
+ ready > 0 &&
794
+ inFlight === 0 &&
795
+ oldestReadyMs !== undefined &&
796
+ oldestReadyMs >= definition.ageSlo.failMs
797
+ ) {
798
+ return {
799
+ state: 'stalled',
800
+ reason:
801
+ 'Ready work is past its failure age SLO and no in-flight work is observed.',
802
+ };
803
+ }
804
+
805
+ if (
806
+ (ready !== undefined && ready > 0) ||
807
+ (delayed !== undefined && delayed > 0) ||
808
+ (inFlight !== undefined && inFlight > 0)
809
+ ) {
810
+ return {
811
+ state: 'draining',
812
+ reason:
813
+ health.reason === 'age_breached'
814
+ ? 'Work is active, but its oldest item is past the age SLO.'
815
+ : 'Work is eligible, scheduled, or currently in flight.',
816
+ };
817
+ }
818
+
819
+ return {
820
+ state: 'clear',
821
+ reason: 'No active, held, or quarantined work is observed.',
822
+ };
823
+ }
824
+
697
825
  // Legacy v1 snapshot helpers remain exported for the webhook/cron collectors
698
826
  // during the v1 -> v2 observability rollout. They are intentionally isolated
699
827
  // from the scheduler-neutral v2 contract above.
@@ -975,6 +975,24 @@
975
975
  "failureAlert": "convex_action_wrapper",
976
976
  "domainReportRequired": true
977
977
  },
978
+ {
979
+ "id": "convex.monitor-provider-repair-recovery-tick",
980
+ "name": "Monitor provider repair recovery tick",
981
+ "scheduler": "convex_cron",
982
+ "cadence": "every 60s",
983
+ "timezone": "UTC",
984
+ "source": "packages/convex/crons.ts",
985
+ "locator": "monitor provider repair recovery tick",
986
+ "owner": "monitors",
987
+ "criticality": "critical",
988
+ "purpose": "Reclaim and wake durable provider monitor repair operations without delaying ordinary Fleet convergence.",
989
+ "successDefinition": "Due repair operations are re-issued or durably accounted; no due repairs is a valid no-op.",
990
+ "failureDefinition": "A repair lease or continuation is stranded, or its recovery state cannot persist.",
991
+ "startGraceMs": 120000,
992
+ "runTimeoutMs": 1800000,
993
+ "failureAlert": "convex_action_wrapper",
994
+ "domainReportRequired": true
995
+ },
978
996
  {
979
997
  "id": "convex.recover-play-webhook-receipts",
980
998
  "name": "Recover Play webhook receipts",
@@ -15,6 +15,7 @@ export const MANUALLY_RUNNABLE_CONVEX_CRON_JOB_IDS = [
15
15
  'convex.cleanup-expired-domain-health-cache',
16
16
  'convex.salesforce-oauth-refresh-maintenance',
17
17
  'convex.monitor-fleet-convergence-tick',
18
+ 'convex.monitor-provider-repair-recovery-tick',
18
19
  'convex.monitor-deployment-job-tick',
19
20
  'convex.provider-monitor-global-audit-daily',
20
21
  'convex.recover-notification-delivery-work',
@@ -328,8 +328,8 @@ function previewDetailFor(substep: PlayStaticSubstep): string | null {
328
328
  ? `${substep.steps.length} fallbacks`
329
329
  : 'first match wins';
330
330
  case 'step_suite': {
331
- const count = stepSuiteToolIds(substep).length;
332
- return count > 0 ? `${count} providers, first match wins` : null;
331
+ const count = effectiveStepCount(substep);
332
+ return `${count} step${count === 1 ? '' : 's'}`;
333
333
  }
334
334
  case 'control_flow': {
335
335
  const count = substep.steps.reduce(