deepline 0.3.104 → 0.3.106

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.
@@ -18,6 +18,7 @@ export const MANUALLY_RUNNABLE_CONVEX_CRON_JOB_IDS = [
18
18
  'convex.monitor-deployment-job-tick',
19
19
  'convex.provider-monitor-global-audit-daily',
20
20
  'convex.recover-notification-delivery-work',
21
+ 'convex.observe-operational-queue-health',
21
22
  'convex.cleanup-expired-notification-audit-records',
22
23
  'convex.fail-stale-play-deployments',
23
24
  ] as const;
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Scheduler-neutral scheduled-work contract.
3
+ *
4
+ * This is deliberately separate from scheduled-work.ts. That module is the
5
+ * v1 wire contract and is consumed by existing adapters. New adapters should
6
+ * use this module: all domain counters and declared condition codes are
7
+ * required, and no result-shape inference is performed.
8
+ */
9
+
10
+ export const SCHEDULED_WORK_CONTRACT_VERSION = 2 as const;
11
+
12
+ const SCHEDULED_WORK_BOILERPLATE = [
13
+ 'publish its declared domain outcome',
14
+ 'Declared work completes with valid accounting',
15
+ 'the trigger is missed, execution is stuck, reporting is invalid',
16
+ ] as const;
17
+
18
+ export type ScheduledCatalogEntry = {
19
+ readonly id: string;
20
+ readonly name: string;
21
+ readonly scheduler: string;
22
+ readonly cadence: string;
23
+ readonly timezone: string;
24
+ readonly source: string;
25
+ readonly locator: string;
26
+ readonly owner: string;
27
+ readonly criticality: string;
28
+ readonly purpose: string;
29
+ readonly successDefinition: string;
30
+ readonly failureDefinition: string;
31
+ /** Every root must report a concrete domain result, not only scheduler liveness. */
32
+ readonly domainReportRequired: true;
33
+ readonly startGraceMs: number;
34
+ readonly runTimeoutMs: number;
35
+ readonly failureAlert: string;
36
+ };
37
+
38
+ export function validateScheduledWorkCatalog(
39
+ catalog: readonly ScheduledCatalogEntry[],
40
+ ): void {
41
+ const ids = new Set<string>();
42
+ for (const entry of catalog) {
43
+ for (const field of [
44
+ 'id',
45
+ 'name',
46
+ 'scheduler',
47
+ 'cadence',
48
+ 'timezone',
49
+ 'source',
50
+ 'locator',
51
+ 'owner',
52
+ 'criticality',
53
+ 'purpose',
54
+ 'successDefinition',
55
+ 'failureDefinition',
56
+ 'failureAlert',
57
+ ] as const)
58
+ requireText(
59
+ entry[field],
60
+ `catalog entry ${entry.id || '<unknown>'}: ${field}`,
61
+ );
62
+ if (entry.domainReportRequired !== true) {
63
+ throw new Error(`${entry.id}: domainReportRequired must be true`);
64
+ }
65
+ for (const field of [
66
+ 'purpose',
67
+ 'successDefinition',
68
+ 'failureDefinition',
69
+ ] as const) {
70
+ if (
71
+ SCHEDULED_WORK_BOILERPLATE.some((phrase) =>
72
+ entry[field].includes(phrase),
73
+ )
74
+ ) {
75
+ throw new Error(
76
+ `${entry.id}: ${field} contains scheduled-work boilerplate`,
77
+ );
78
+ }
79
+ }
80
+ if (ids.has(entry.id))
81
+ throw new Error(`duplicate scheduled-work id: ${entry.id}`);
82
+ ids.add(entry.id);
83
+ if (!Number.isFinite(entry.startGraceMs) || entry.startGraceMs < 0) {
84
+ throw new Error(
85
+ `${entry.id}: startGraceMs must be finite and non-negative`,
86
+ );
87
+ }
88
+ if (!Number.isFinite(entry.runTimeoutMs) || entry.runTimeoutMs <= 0) {
89
+ throw new Error(`${entry.id}: runTimeoutMs must be finite and positive`);
90
+ }
91
+ }
92
+ }
93
+
94
+ export type Measured<T> =
95
+ | { readonly kind: 'exact'; readonly value: T }
96
+ | { readonly kind: 'lower_bound'; readonly value: T }
97
+ | { readonly kind: 'not_applicable'; readonly reason: string }
98
+ | { readonly kind: 'unavailable'; readonly reason: string };
99
+
100
+ export function exact<T>(value: T): Measured<T> {
101
+ return { kind: 'exact', value };
102
+ }
103
+
104
+ export function lowerBound<T>(value: T): Measured<T> {
105
+ return { kind: 'lower_bound', value };
106
+ }
107
+
108
+ export function notApplicable<T = never>(reason: string): Measured<T> {
109
+ return { kind: 'not_applicable', reason };
110
+ }
111
+
112
+ export function unavailable<T = never>(reason: string): Measured<T> {
113
+ return { kind: 'unavailable', reason };
114
+ }
115
+
116
+ export function measuredValue<T>(measurement: Measured<T>): T | undefined {
117
+ return measurement.kind === 'exact' || measurement.kind === 'lower_bound'
118
+ ? measurement.value
119
+ : undefined;
120
+ }
121
+
122
+ export type ScheduledConditionSeverity = 'warning' | 'failure';
123
+ export type ScheduledConditionOwner = 'customer' | 'provider' | 'deepline';
124
+
125
+ export type ScheduledCondition = {
126
+ readonly severity: ScheduledConditionSeverity;
127
+ readonly owner: ScheduledConditionOwner;
128
+ readonly meaning: string;
129
+ };
130
+
131
+ export type ScheduledTrigger =
132
+ | {
133
+ readonly kind: 'periodic';
134
+ readonly cadence: string;
135
+ readonly startGraceMs: number;
136
+ }
137
+ | { readonly kind: 'on_demand' };
138
+
139
+ export type ScheduledWorkDefinition<
140
+ W extends string = string,
141
+ F extends string = string,
142
+ > = {
143
+ readonly id: string;
144
+ readonly owner: string;
145
+ readonly purpose: string;
146
+ readonly unit: string;
147
+ readonly trigger: ScheduledTrigger;
148
+ readonly runTimeoutMs: number;
149
+ readonly conditions: Readonly<Record<W | F, ScheduledCondition>>;
150
+ /** Type-only witnesses make the intended condition sets visible to TS. */
151
+ readonly warningCodes?: readonly W[];
152
+ readonly failureCodes?: readonly F[];
153
+ };
154
+
155
+ export type ScheduledWorkReport<
156
+ W extends string = string,
157
+ F extends string = string,
158
+ > = {
159
+ readonly examined: number;
160
+ readonly actionable: number;
161
+ readonly dispositions: {
162
+ readonly succeeded: number;
163
+ readonly skipped: number;
164
+ readonly deferred: number;
165
+ readonly failures: Readonly<Record<F, number>>;
166
+ };
167
+ readonly warnings: Readonly<Record<W, number>>;
168
+ readonly failures: Readonly<Record<F, number>>;
169
+ readonly remaining: Measured<number>;
170
+ readonly noop?: { readonly reason: string };
171
+ };
172
+
173
+ type WarningCodes<C extends Record<string, ScheduledCondition>> = {
174
+ [K in keyof C]: C[K] extends { readonly severity: 'warning' } ? K : never;
175
+ }[keyof C] &
176
+ string;
177
+
178
+ type FailureCodes<C extends Record<string, ScheduledCondition>> = {
179
+ [K in keyof C]: C[K] extends { readonly severity: 'failure' } ? K : never;
180
+ }[keyof C] &
181
+ string;
182
+
183
+ export type ScheduledWorkOccurrence = {
184
+ readonly occurrenceId: string;
185
+ readonly invocationId?: string;
186
+ readonly expectedAt: number;
187
+ readonly startedAt?: number;
188
+ readonly heartbeatAt?: number;
189
+ readonly completedAt?: number;
190
+ readonly outcome?: 'succeeded' | 'warning' | 'failed';
191
+ };
192
+
193
+ export type ScheduledWorkHealthStatus =
194
+ | 'waiting'
195
+ | 'running'
196
+ | 'healthy'
197
+ | 'warning'
198
+ | 'failed'
199
+ | 'missed_trigger'
200
+ | 'stuck'
201
+ | 'on_demand'
202
+ | 'unobserved';
203
+
204
+ export type ScheduledWorkHealth = {
205
+ readonly status: ScheduledWorkHealthStatus;
206
+ readonly reason:
207
+ | 'before_start_deadline'
208
+ | 'started'
209
+ | 'completed'
210
+ | 'completed_with_warnings'
211
+ | 'completed_with_failures'
212
+ | 'missed_trigger'
213
+ | 'runtime_timeout'
214
+ | 'heartbeat_timeout'
215
+ | 'on_demand_idle'
216
+ | 'missing_observation';
217
+ };
218
+
219
+ export function defineScheduledWork<
220
+ const C extends Readonly<Record<string, ScheduledCondition>>,
221
+ >(
222
+ definition: Omit<
223
+ ScheduledWorkDefinition<WarningCodes<C>, FailureCodes<C>>,
224
+ 'conditions'
225
+ > & { readonly conditions: C },
226
+ ) {
227
+ validateScheduledWorkDefinition(definition);
228
+ return {
229
+ definition,
230
+ report(
231
+ report: ScheduledWorkReport<WarningCodes<C>, FailureCodes<C>>,
232
+ ): ScheduledWorkReport<WarningCodes<C>, FailureCodes<C>> {
233
+ validateScheduledWorkReport(definition, report);
234
+ return report;
235
+ },
236
+ } as const;
237
+ }
238
+
239
+ export function validateScheduledWorkDefinition(
240
+ definition: ScheduledWorkDefinition,
241
+ ): void {
242
+ requireText(definition.id, 'id');
243
+ requireText(definition.owner, 'owner');
244
+ requireText(definition.purpose, 'purpose');
245
+ requireText(definition.unit, 'unit');
246
+ if (
247
+ !Number.isFinite(definition.runTimeoutMs) ||
248
+ definition.runTimeoutMs <= 0
249
+ ) {
250
+ throw new Error(`${definition.id}: runTimeoutMs must be positive`);
251
+ }
252
+ if (definition.trigger.kind === 'periodic') {
253
+ requireText(definition.trigger.cadence, `${definition.id}: cadence`);
254
+ if (
255
+ !Number.isFinite(definition.trigger.startGraceMs) ||
256
+ definition.trigger.startGraceMs < 0
257
+ ) {
258
+ throw new Error(`${definition.id}: startGraceMs must be non-negative`);
259
+ }
260
+ }
261
+ const codes = Object.entries(definition.conditions);
262
+ if (codes.length === 0) {
263
+ throw new Error(`${definition.id}: declare at least one condition code`);
264
+ }
265
+ const codeSet = new Set<string>();
266
+ for (const [code, condition] of codes) {
267
+ requireText(code, `${definition.id}: condition code`);
268
+ if (codeSet.has(code))
269
+ throw new Error(`${definition.id}: duplicate condition ${code}`);
270
+ codeSet.add(code);
271
+ requireText(condition.meaning, `${definition.id}: ${code}.meaning`);
272
+ }
273
+ }
274
+
275
+ export function validateScheduledWorkReport<W extends string, F extends string>(
276
+ definition: ScheduledWorkDefinition<W, F>,
277
+ report: ScheduledWorkReport<W, F>,
278
+ ): void {
279
+ for (const [name, value] of [
280
+ ['examined', report.examined],
281
+ ['actionable', report.actionable],
282
+ ['succeeded', report.dispositions.succeeded],
283
+ ['skipped', report.dispositions.skipped],
284
+ ['deferred', report.dispositions.deferred],
285
+ ] as const)
286
+ requireCount(definition.id, name, value);
287
+ if (report.actionable > report.examined) {
288
+ throw new Error(`${definition.id}: actionable cannot exceed examined`);
289
+ }
290
+ const conditionCodes = Object.keys(definition.conditions);
291
+ for (const code of conditionCodes) {
292
+ const condition = (
293
+ definition.conditions as Readonly<Record<string, ScheduledCondition>>
294
+ )[code]!;
295
+ const value =
296
+ condition.severity === 'warning'
297
+ ? report.warnings[code as W]
298
+ : report.failures[code as F];
299
+ requireCount(definition.id, `${code} condition`, value);
300
+ }
301
+ for (const [code, value] of Object.entries(report.dispositions.failures)) {
302
+ requireCount(definition.id, `disposition ${code}`, value as number);
303
+ }
304
+ if (
305
+ report.remaining.kind === 'exact' ||
306
+ report.remaining.kind === 'lower_bound'
307
+ ) {
308
+ requireCount(definition.id, 'remaining', report.remaining.value);
309
+ } else
310
+ requireText(report.remaining.reason, `${definition.id}: remaining.reason`);
311
+ const totals =
312
+ report.dispositions.succeeded +
313
+ report.dispositions.skipped +
314
+ report.dispositions.deferred +
315
+ (Object.values(report.dispositions.failures) as number[]).reduce(
316
+ (sum, n) => sum + n,
317
+ 0,
318
+ );
319
+ if (totals > report.actionable) {
320
+ throw new Error(`${definition.id}: dispositions exceed actionable work`);
321
+ }
322
+ if (report.actionable === 0 && !report.noop) {
323
+ throw new Error(
324
+ `${definition.id}: zero actionable work requires noop.reason`,
325
+ );
326
+ }
327
+ if (report.noop)
328
+ requireText(report.noop.reason, `${definition.id}: noop.reason`);
329
+ }
330
+
331
+ export function deriveScheduledWorkHealth(
332
+ definition: ScheduledWorkDefinition,
333
+ occurrence: ScheduledWorkOccurrence | undefined,
334
+ now = Date.now(),
335
+ ): ScheduledWorkHealth {
336
+ if (definition.trigger.kind === 'on_demand' && !occurrence) {
337
+ return { status: 'on_demand', reason: 'on_demand_idle' };
338
+ }
339
+ if (!occurrence)
340
+ return { status: 'unobserved', reason: 'missing_observation' };
341
+ const startDeadline =
342
+ occurrence.expectedAt +
343
+ (definition.trigger.kind === 'periodic'
344
+ ? definition.trigger.startGraceMs
345
+ : 0);
346
+ if (!occurrence.startedAt) {
347
+ return now <= startDeadline
348
+ ? { status: 'waiting', reason: 'before_start_deadline' }
349
+ : { status: 'missed_trigger', reason: 'missed_trigger' };
350
+ }
351
+ if (occurrence.completedAt !== undefined) {
352
+ return occurrence.outcome === 'failed'
353
+ ? { status: 'failed', reason: 'completed_with_failures' }
354
+ : occurrence.outcome === 'warning'
355
+ ? { status: 'warning', reason: 'completed_with_warnings' }
356
+ : { status: 'healthy', reason: 'completed' };
357
+ }
358
+ const lastAlive = occurrence.heartbeatAt ?? occurrence.startedAt;
359
+ const timeoutAt = occurrence.startedAt + definition.runTimeoutMs;
360
+ if (now > timeoutAt) return { status: 'stuck', reason: 'runtime_timeout' };
361
+ if (now - lastAlive > Math.min(definition.runTimeoutMs, 5 * 60_000)) {
362
+ return { status: 'stuck', reason: 'heartbeat_timeout' };
363
+ }
364
+ return { status: 'running', reason: 'started' };
365
+ }
366
+
367
+ function requireText(value: string | undefined, label: string): void {
368
+ if (typeof value !== 'string' || value.trim() === '')
369
+ throw new Error(`${label} is required`);
370
+ }
371
+
372
+ function requireCount(
373
+ id: string,
374
+ label: string,
375
+ value: number | undefined,
376
+ ): void {
377
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
378
+ throw new Error(`${id}: ${label} must be a finite non-negative number`);
379
+ }
380
+ }
@@ -32,6 +32,13 @@ export type PlayLaunchQueuePolicy = {
32
32
  sandboxCapacityMaxWaitSeconds: number | null;
33
33
  };
34
34
 
35
+ /**
36
+ * CLI/API launches are user-visible requests. Give a short capacity window so
37
+ * a temporary spike can clear, but never let an interactive run appear to
38
+ * succeed and then start charging much later without an explicit retry.
39
+ */
40
+ export const API_LAUNCH_CAPACITY_WAIT_SECONDS = 60;
41
+
35
42
  export function shouldTerminalizeSandboxCapacityRejection(input: {
36
43
  triggerSource?: PlayLaunchTriggerSource | null;
37
44
  submittedAtMs?: number | null;
@@ -68,13 +75,15 @@ export function resolvePlayLaunchQueuePolicy(
68
75
  case 'api':
69
76
  return {
70
77
  triggerSource: 'api',
71
- // A person waiting in the CLI/UI should get the next slot in their
72
- // own organization rather than sitting behind that org's automation.
73
- // The bounded deadline gives the caller a typed terminal outcome
74
- // instead of an indefinite watch.
78
+ // API/CLI callers need a bounded, user-visible outcome. A brief
79
+ // capacity window absorbs normal spikes; after that the run is
80
+ // terminalized with a typed queue-expired/capacity result rather than
81
+ // starting unexpectedly much later. The priority still lets manual
82
+ // work win the next slot within its organization without bypassing
83
+ // cross-org fairness.
75
84
  priority: 100,
76
- maxDelaySeconds: 10,
77
- sandboxCapacityMaxWaitSeconds: 0,
85
+ maxDelaySeconds: API_LAUNCH_CAPACITY_WAIT_SECONDS,
86
+ sandboxCapacityMaxWaitSeconds: API_LAUNCH_CAPACITY_WAIT_SECONDS,
78
87
  };
79
88
  case 'webhook':
80
89
  return {
@@ -82,8 +91,7 @@ export function resolvePlayLaunchQueuePolicy(
82
91
  priority: 0,
83
92
  // A 202 webhook response is a durable delivery promise. Capacity is
84
93
  // not a delivery failure: retain it until it executes, is explicitly
85
- // cancelled, or an operator terminalizes it. Interactive API launches
86
- // remain separately bounded above.
94
+ // cancelled, or an operator terminalizes it.
87
95
  maxDelaySeconds: null,
88
96
  sandboxCapacityMaxWaitSeconds: null,
89
97
  };
@@ -91,15 +99,21 @@ export function resolvePlayLaunchQueuePolicy(
91
99
  return {
92
100
  triggerSource: 'cron',
93
101
  priority: -10,
94
- maxDelaySeconds: 30 * 60,
95
- sandboxCapacityMaxWaitSeconds: 30 * 60,
102
+ // Scheduled automation is a durable customer intent. A capacity SEV
103
+ // must not silently discard a scheduled run; recovery/alerting owns
104
+ // platform-wide exhaustion and operators can cancel explicitly.
105
+ maxDelaySeconds: null,
106
+ sandboxCapacityMaxWaitSeconds: null,
96
107
  };
97
108
  case 'sql_listener':
98
109
  return {
99
110
  triggerSource: 'sql_listener',
100
111
  priority: -10,
101
- maxDelaySeconds: 30 * 60,
102
- sandboxCapacityMaxWaitSeconds: 30 * 60,
112
+ // SQL-listener automation has the same durable intent contract as
113
+ // cron. Capacity pressure is recoverable state, not a user-visible
114
+ // queue expiry.
115
+ maxDelaySeconds: null,
116
+ sandboxCapacityMaxWaitSeconds: null,
103
117
  };
104
118
  case 'async_child':
105
119
  return {
@@ -322,9 +322,9 @@ const HEARTBEAT_CLAIM_TIMEOUT_SECONDS = RUNTIME_CLAIM_LEASE_SECONDS;
322
322
  export const ABSURD_IDLE_POLL_INTERVAL_SECONDS = 30;
323
323
  /**
324
324
  * A direct LISTEN session is an optional latency optimization. When it is
325
- * unavailable, durable polling must still discover manual/API work before its
326
- * 10-second admission deadline. Keep enough margin for a pooled query, claim,
327
- * and scheduler receipt write.
325
+ * unavailable, durable polling should still discover newly accepted work
326
+ * promptly. Queue admission has source-specific deadlines, so a missed poll
327
+ * may add latency but must not invent a terminal result outside that policy.
328
328
  */
329
329
  export const ABSURD_WAKE_UNAVAILABLE_POLL_INTERVAL_MS = 1_000;
330
330
  /** Heartbeat throttle: keep the lease well ahead of expiry (leaseTimeout / 3). */