deepline 0.3.121 → 0.3.123

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.
@@ -177,6 +177,345 @@ export type DlqApplyResult = {
177
177
  readonly verificationRequired: boolean;
178
178
  };
179
179
 
180
+ /**
181
+ * Queue-item control is deliberately a separate port from an inventory. A
182
+ * count and a bounded index description are not permission to manufacture a
183
+ * row scan: owners register this reader only when they can return exact item
184
+ * evidence without widening their data-access boundary.
185
+ */
186
+ export const QUEUE_ITEM_CONTROL_CONTRACT_VERSION = 1 as const;
187
+
188
+ /** The common state vocabulary exposed by every queue connector. */
189
+ export const QUEUE_ITEM_STATES = [
190
+ 'ready',
191
+ 'delayed',
192
+ 'running',
193
+ 'held',
194
+ 'quarantined',
195
+ 'archived',
196
+ 'completed',
197
+ 'cancelled',
198
+ 'unknown',
199
+ ] as const;
200
+
201
+ export type QueueItemState = (typeof QUEUE_ITEM_STATES)[number];
202
+
203
+ export type QueueItemAction =
204
+ | 'archive'
205
+ | 'quarantine'
206
+ | 'retry'
207
+ | 'hold'
208
+ | 'resume'
209
+ | 'cancel'
210
+ | 'update';
211
+
212
+ export const QUEUE_ITEM_ACTIONS = [
213
+ 'archive',
214
+ 'quarantine',
215
+ 'retry',
216
+ 'hold',
217
+ 'resume',
218
+ 'cancel',
219
+ 'update',
220
+ ] as const satisfies readonly QueueItemAction[];
221
+
222
+ export type QueueItemActionAvailability = {
223
+ readonly action: QueueItemAction;
224
+ readonly allowed: boolean;
225
+ readonly reason?: string;
226
+ };
227
+
228
+ export type QueueItemFieldType =
229
+ | 'string'
230
+ | 'number'
231
+ | 'boolean'
232
+ | 'datetime'
233
+ | 'json';
234
+
235
+ export type QueueItemFieldDefinition = {
236
+ readonly name: string;
237
+ readonly type: QueueItemFieldType;
238
+ readonly required: boolean;
239
+ readonly mutable: boolean;
240
+ readonly description?: string;
241
+ };
242
+
243
+ export type QueueItemLegacyAction = Exclude<DlqPathAction, 'inspect'>;
244
+ export type QueueItemActionInput = QueueItemAction | QueueItemLegacyAction;
245
+
246
+ export type QueueItemLastError = {
247
+ readonly code: string;
248
+ readonly message: string;
249
+ };
250
+
251
+ /** Names used in the API documentation and in connector implementations. */
252
+ export type ActionAvailability = QueueItemActionAvailability;
253
+ export type FieldDefinition = QueueItemFieldDefinition;
254
+
255
+ /** A normalized item returned by an owner connector. `nativeState` is always
256
+ * retained so an operator can distinguish a connector mapping from the
257
+ * backend's actual state. */
258
+ export type QueueItem = {
259
+ readonly queueId: string;
260
+ readonly id: string;
261
+ /** Opaque source revision used for compare-and-set mutation. */
262
+ readonly revision: string;
263
+ readonly observedAt: string;
264
+ readonly state: QueueItemState;
265
+ readonly nativeState: string;
266
+ readonly lastError?: QueueItemLastError;
267
+ readonly actions: readonly ActionAvailability[];
268
+ readonly editableFields: readonly FieldDefinition[];
269
+ readonly createdAt?: string;
270
+ readonly updatedAt?: string;
271
+ readonly summary?: string;
272
+ };
273
+
274
+ /** Keep old DLQ verbs working at the boundary while exposing a smaller,
275
+ * operator-readable control vocabulary. */
276
+ export function normalizeQueueItemAction(
277
+ action: QueueItemActionInput,
278
+ ): QueueItemAction {
279
+ const aliases: Readonly<Record<QueueItemLegacyAction, QueueItemAction>> = {
280
+ redrive: 'retry',
281
+ discard: 'archive',
282
+ mark_done: 'archive',
283
+ handoff: 'resume',
284
+ recover: 'retry',
285
+ reconcile: 'update',
286
+ replay: 'retry',
287
+ retry: 'retry',
288
+ quarantine: 'quarantine',
289
+ release: 'cancel',
290
+ settle: 'resume',
291
+ };
292
+ return action in aliases
293
+ ? aliases[action as QueueItemLegacyAction]
294
+ : (action as QueueItemAction);
295
+ }
296
+
297
+ /**
298
+ * Legacy bounded evidence kept for callers that only consume the original
299
+ * `id/state/revision` projection. New connectors must return `QueueItem`.
300
+ */
301
+ export type QueueItemEvidence = {
302
+ readonly id: string;
303
+ readonly state: string;
304
+ readonly revision: string;
305
+ /** Additive fields understood by newer readers; old producers may omit them. */
306
+ readonly nativeState?: string;
307
+ readonly lastError?: QueueItemLastError;
308
+ readonly actions?: readonly QueueItemActionAvailability[];
309
+ readonly editableFields?: readonly QueueItemFieldDefinition[];
310
+ readonly createdAt?: number;
311
+ readonly updatedAt?: number;
312
+ readonly summary?: string;
313
+ };
314
+
315
+ export type QueueItemUnavailable = {
316
+ readonly code: 'item_reader_unavailable';
317
+ readonly reason: string;
318
+ };
319
+
320
+ export type QueueItemList = {
321
+ readonly contractVersion: typeof QUEUE_ITEM_CONTROL_CONTRACT_VERSION;
322
+ readonly queueId: string;
323
+ readonly environment: string;
324
+ readonly observedAt: number;
325
+ readonly items: readonly QueueItem[];
326
+ readonly nextCursor: string | null;
327
+ readonly limit: number;
328
+ readonly cursor: string | null;
329
+ readonly unavailable?: QueueItemUnavailable;
330
+ };
331
+
332
+ export type QueueItemLookup =
333
+ | { readonly kind: 'found'; readonly item: QueueItem }
334
+ | { readonly kind: 'not_found' }
335
+ | {
336
+ readonly kind: 'unavailable';
337
+ readonly unavailable: QueueItemUnavailable;
338
+ };
339
+
340
+ export type QueueItemActionRequest = {
341
+ readonly action: QueueItemAction;
342
+ readonly sourceAction?: QueueItemActionInput;
343
+ readonly idempotencyKey: string;
344
+ readonly expectedRevision: string;
345
+ readonly reason: string;
346
+ /** Connector-declared mutable fields for the `update` action only. */
347
+ readonly fields?: Readonly<Record<string, unknown>>;
348
+ /** Authenticated platform-admin identity forwarded to the owner connector. */
349
+ readonly actorId?: string;
350
+ };
351
+
352
+ export type QueueItemActionOperation = {
353
+ readonly contractVersion: typeof QUEUE_ITEM_CONTROL_CONTRACT_VERSION;
354
+ readonly operationId: string;
355
+ readonly queueId: string;
356
+ readonly itemId: string;
357
+ readonly environment: string;
358
+ readonly action: QueueItemAction;
359
+ readonly idempotencyKey: string;
360
+ readonly expectedRevision: string;
361
+ /** `pending` means the owner accepted the request but has not yet verified
362
+ * the native transition. `confirmed` is the only successful terminal
363
+ * status. The rejected/stale statuses preserve the existing safe boundary. */
364
+ readonly status: 'pending' | 'confirmed' | 'rejected' | 'stale' | 'failed';
365
+ readonly requestedAt?: string;
366
+ readonly confirmedAt?: string;
367
+ readonly replacementItemId?: string;
368
+ readonly item?: QueueItem;
369
+ readonly error?: QueueItemLastError;
370
+ readonly audit: {
371
+ readonly recorded: boolean;
372
+ readonly reason: string;
373
+ };
374
+ /** Existing owner command, when the catalog has one. This is guidance only;
375
+ * callers must still invoke its own authenticated, audited surface. */
376
+ readonly ownerCommand?: string;
377
+ };
378
+
379
+ export type QueueItemActionStatus =
380
+ | 'pending'
381
+ | 'confirmed'
382
+ | 'accepted'
383
+ | 'rejected'
384
+ | 'stale'
385
+ | 'failed'
386
+ | 'not_found'
387
+ | 'unavailable';
388
+
389
+ /**
390
+ * Connector result for a requested transition. `confirmed` means the owner
391
+ * verified the resulting native state; `pending` means the owner durably
392
+ * accepted it but has not yet confirmed that state. `accepted` remains as a
393
+ * compatibility spelling for older connectors and must not be treated as
394
+ * confirmed by the shared control service.
395
+ */
396
+ export type QueueItemActionResult = {
397
+ readonly contractVersion: typeof QUEUE_ITEM_CONTROL_CONTRACT_VERSION;
398
+ readonly operationId: string;
399
+ readonly queueId: string;
400
+ readonly itemId: string;
401
+ readonly environment: string;
402
+ readonly action: QueueItemAction;
403
+ readonly idempotencyKey: string;
404
+ readonly expectedRevision: string;
405
+ readonly status: QueueItemActionStatus;
406
+ readonly item?: QueueItem;
407
+ readonly replacementItemId?: string;
408
+ readonly reason?: string;
409
+ readonly audit: {
410
+ readonly recorded: boolean;
411
+ readonly auditId?: string;
412
+ };
413
+ };
414
+
415
+ export type QueueItemConnectorContext = {
416
+ readonly queueId: string;
417
+ readonly environment: string;
418
+ readonly now: number;
419
+ readonly limit: number;
420
+ readonly cursor?: string;
421
+ readonly actorId?: string;
422
+ readonly operationId?: string;
423
+ };
424
+
425
+ /**
426
+ * Owner implementation port. The connector owns native reads, state mapping,
427
+ * lease/CAS checks, and outcome verification. The shared control service only
428
+ * authenticates, validates, journals idempotency, and returns this result.
429
+ */
430
+ export type QueueItemConnector = {
431
+ readonly id: string;
432
+ readonly queueId: string;
433
+ readonly source: string;
434
+ readonly mapNativeState: (nativeState: string) => QueueItemState;
435
+ readonly list: (context: QueueItemConnectorContext) => Promise<QueueItemList>;
436
+ readonly get: (
437
+ context: QueueItemConnectorContext,
438
+ itemId: string,
439
+ ) => Promise<QueueItemLookup>;
440
+ readonly requestAction: (
441
+ context: QueueItemConnectorContext,
442
+ itemId: string,
443
+ request: QueueItemActionRequest,
444
+ ) => Promise<QueueItemActionOperation>;
445
+ };
446
+
447
+ /** Conservative shared fallback for an unrecognised native state. */
448
+ export function normalizeQueueItemState(nativeState: string): QueueItemState {
449
+ const state = nativeState.trim().toLowerCase();
450
+ if ((QUEUE_ITEM_STATES as readonly string[]).includes(state)) {
451
+ return state as QueueItemState;
452
+ }
453
+ if (
454
+ state === 'in_flight' ||
455
+ state === 'in-flight' ||
456
+ state === 'processing'
457
+ ) {
458
+ return 'running';
459
+ }
460
+ if (
461
+ state === 'dead_lettered' ||
462
+ state === 'dead-lettered' ||
463
+ state === 'dlq'
464
+ ) {
465
+ return 'quarantined';
466
+ }
467
+ if (state === 'done' || state === 'succeeded' || state === 'success') {
468
+ return 'completed';
469
+ }
470
+ if (state === 'canceled' || state === 'aborted') return 'cancelled';
471
+ return 'unknown';
472
+ }
473
+
474
+ export function unavailableQueueItemList(input: {
475
+ queueId: string;
476
+ environment: string;
477
+ observedAt: number;
478
+ reason: string;
479
+ limit?: number;
480
+ cursor?: string | null;
481
+ }): QueueItemList {
482
+ return {
483
+ contractVersion: QUEUE_ITEM_CONTROL_CONTRACT_VERSION,
484
+ queueId: input.queueId,
485
+ environment: input.environment,
486
+ observedAt: input.observedAt,
487
+ items: [],
488
+ nextCursor: null,
489
+ limit: input.limit ?? 50,
490
+ cursor: input.cursor ?? null,
491
+ unavailable: { code: 'item_reader_unavailable', reason: input.reason },
492
+ };
493
+ }
494
+
495
+ /** A deterministic request identity is safe to show before an owner has a
496
+ * durable operation store. It is explicitly not represented as an audit. */
497
+ export function queueItemOperationId(input: {
498
+ queueId: string;
499
+ itemId: string;
500
+ action: QueueItemAction;
501
+ idempotencyKey: string;
502
+ }): string {
503
+ const value = [
504
+ input.queueId,
505
+ input.itemId,
506
+ input.action,
507
+ input.idempotencyKey,
508
+ ]
509
+ .map((part) => part.trim())
510
+ .join(':');
511
+ let hash = 2166136261;
512
+ for (let index = 0; index < value.length; index += 1) {
513
+ hash ^= value.charCodeAt(index);
514
+ hash = Math.imul(hash, 16777619);
515
+ }
516
+ return `queue-item-${(hash >>> 0).toString(36)}`;
517
+ }
518
+
180
519
  /** Every backend—including Absurd—must implement this port at its boundary. */
181
520
  export type DlqAdapter = {
182
521
  readonly id: string;
@@ -0,0 +1,314 @@
1
+ import {
2
+ normalizeQueueItemState,
3
+ queueItemOperationId,
4
+ unavailableQueueItemList,
5
+ type QueueItemActionOperation,
6
+ type QueueItemActionRequest,
7
+ type QueueItemConnector,
8
+ type QueueItemConnectorContext,
9
+ type QueueItemList,
10
+ type QueueItemLookup,
11
+ type QueueItemState,
12
+ } from './dlq';
13
+ import { OPERATIONS_QUEUE_CATALOG } from './queue-health';
14
+
15
+ /** Native source families supported by the shared control-plane port. */
16
+ export type QueueConnectorBackend =
17
+ | 'convex'
18
+ | 'workpool'
19
+ | 'postgres-outbox'
20
+ | 'absurd'
21
+ | 'provider';
22
+
23
+ export type QueueItemConnectorRegistration = {
24
+ readonly backend: QueueConnectorBackend;
25
+ readonly connector: QueueItemConnector;
26
+ };
27
+
28
+ export type QueueItemConnectorRegistryOptions = {
29
+ /** Registration is intentionally injectable for tests and owner wiring. */
30
+ readonly registrations?: readonly QueueItemConnectorRegistration[];
31
+ };
32
+
33
+ export type QueueItemConnectorRegisterOptions = {
34
+ /** Only an unavailable placeholder may be replaced at startup. */
35
+ readonly replaceUnavailable?: boolean;
36
+ };
37
+
38
+ /**
39
+ * The registry is the routing boundary: the control service looks up a queue
40
+ * by its stable public id and never switches on a native table or provider.
41
+ * Duplicate registrations are rejected so a new owner cannot silently shadow
42
+ * an existing mutation implementation.
43
+ */
44
+ export class QueueItemConnectorRegistry {
45
+ private readonly registrations = new Map<
46
+ string,
47
+ QueueItemConnectorRegistration
48
+ >();
49
+
50
+ constructor(options: QueueItemConnectorRegistryOptions = {}) {
51
+ for (const registration of options.registrations ?? []) {
52
+ this.register(registration);
53
+ }
54
+ }
55
+
56
+ register(
57
+ registration: QueueItemConnectorRegistration,
58
+ options: QueueItemConnectorRegisterOptions = {},
59
+ ): this {
60
+ const { backend, connector } = registration;
61
+ if (!backend) throw new Error('Queue connector backend is required.');
62
+ if (!connector.queueId.trim())
63
+ throw new Error('Queue connector queueId is required.');
64
+ if (!connector.id.trim())
65
+ throw new Error('Queue connector id is required.');
66
+ const existing = this.registrations.get(connector.queueId);
67
+ if (existing && !options.replaceUnavailable) {
68
+ throw new Error(
69
+ `A queue connector is already registered for ${connector.queueId}.`,
70
+ );
71
+ }
72
+ if (
73
+ existing &&
74
+ options.replaceUnavailable &&
75
+ !existing.connector.id.endsWith('-unavailable')
76
+ ) {
77
+ throw new Error(
78
+ `Queue connector ${connector.queueId} is already owned by ${existing.connector.id}; replacement is not allowed.`,
79
+ );
80
+ }
81
+ this.registrations.set(connector.queueId, registration);
82
+ return this;
83
+ }
84
+
85
+ get(queueId: string): QueueItemConnectorRegistration | undefined {
86
+ return this.registrations.get(queueId);
87
+ }
88
+
89
+ has(queueId: string): boolean {
90
+ return this.registrations.has(queueId);
91
+ }
92
+
93
+ list(): readonly QueueItemConnectorRegistration[] {
94
+ return [...this.registrations.values()];
95
+ }
96
+ }
97
+
98
+ type UnavailableConnectorOptions = {
99
+ readonly queueId: string;
100
+ readonly backend: QueueConnectorBackend;
101
+ readonly source: string;
102
+ readonly id?: string;
103
+ readonly reason?: string;
104
+ };
105
+
106
+ const unavailableReason = (options: UnavailableConnectorOptions): string =>
107
+ options.reason ??
108
+ `The ${options.backend} owner has not registered a bounded item reader and fenced mutation adapter for ${options.queueId}.`;
109
+
110
+ /**
111
+ * Safe connector shell used until an owner supplies source-owned reads and
112
+ * transactions. It is deliberately observable and routable, but cannot
113
+ * claim an item exists or that a transition was applied.
114
+ */
115
+ export function createUnavailableQueueItemConnector(
116
+ options: UnavailableConnectorOptions,
117
+ ): QueueItemConnectorRegistration {
118
+ const reason = unavailableReason(options);
119
+ const connector: QueueItemConnector = {
120
+ id: options.id ?? `${options.backend}-${options.queueId}-unavailable`,
121
+ queueId: options.queueId,
122
+ source: options.source,
123
+ mapNativeState: (nativeState: string): QueueItemState =>
124
+ normalizeQueueItemState(nativeState),
125
+ async list(context: QueueItemConnectorContext): Promise<QueueItemList> {
126
+ return unavailableQueueItemList({
127
+ queueId: options.queueId,
128
+ environment: context.environment,
129
+ observedAt: context.now,
130
+ reason,
131
+ limit: context.limit,
132
+ cursor: context.cursor,
133
+ });
134
+ },
135
+ async get(
136
+ _context: QueueItemConnectorContext,
137
+ _itemId: string,
138
+ ): Promise<QueueItemLookup> {
139
+ return {
140
+ kind: 'unavailable',
141
+ unavailable: { code: 'item_reader_unavailable', reason },
142
+ };
143
+ },
144
+ async requestAction(
145
+ context: QueueItemConnectorContext,
146
+ itemId: string,
147
+ request: QueueItemActionRequest,
148
+ ): Promise<QueueItemActionOperation> {
149
+ const operationId =
150
+ context.operationId ??
151
+ queueItemOperationId({
152
+ queueId: options.queueId,
153
+ itemId,
154
+ action: request.action,
155
+ idempotencyKey: request.idempotencyKey,
156
+ });
157
+ return {
158
+ contractVersion: 1,
159
+ operationId,
160
+ queueId: options.queueId,
161
+ itemId,
162
+ environment: context.environment,
163
+ action: request.action,
164
+ idempotencyKey: request.idempotencyKey,
165
+ expectedRevision: request.expectedRevision,
166
+ status: 'rejected',
167
+ audit: { recorded: false, reason },
168
+ };
169
+ },
170
+ };
171
+ return { backend: options.backend, connector };
172
+ }
173
+
174
+ type BackendConnectorOptions = Omit<
175
+ UnavailableConnectorOptions,
176
+ 'backend' | 'source'
177
+ > & {
178
+ readonly source?: string;
179
+ readonly reason?: string;
180
+ };
181
+
182
+ export function createConvexQueueItemConnector(
183
+ options: BackendConnectorOptions,
184
+ ): QueueItemConnectorRegistration {
185
+ return createUnavailableQueueItemConnector({
186
+ ...options,
187
+ backend: 'convex',
188
+ source: options.source ?? 'convex',
189
+ });
190
+ }
191
+
192
+ export function createWorkpoolQueueItemConnector(
193
+ options: BackendConnectorOptions,
194
+ ): QueueItemConnectorRegistration {
195
+ return createUnavailableQueueItemConnector({
196
+ ...options,
197
+ backend: 'workpool',
198
+ source: options.source ?? 'convex-workpool',
199
+ });
200
+ }
201
+
202
+ export function createPostgresOutboxQueueItemConnector(
203
+ options: BackendConnectorOptions,
204
+ ): QueueItemConnectorRegistration {
205
+ return createUnavailableQueueItemConnector({
206
+ ...options,
207
+ backend: 'postgres-outbox',
208
+ source: options.source ?? 'postgres-outbox',
209
+ });
210
+ }
211
+
212
+ export function createAbsurdQueueItemConnector(
213
+ options: BackendConnectorOptions,
214
+ ): QueueItemConnectorRegistration {
215
+ return createUnavailableQueueItemConnector({
216
+ ...options,
217
+ backend: 'absurd',
218
+ source: options.source ?? 'absurd',
219
+ });
220
+ }
221
+
222
+ export function createProviderQueueItemConnector(
223
+ options: BackendConnectorOptions,
224
+ ): QueueItemConnectorRegistration {
225
+ return createUnavailableQueueItemConnector({
226
+ ...options,
227
+ backend: 'provider',
228
+ source: options.source ?? 'provider',
229
+ });
230
+ }
231
+
232
+ /** Explicit owner mapping. Unknown queues must fail registration instead of
233
+ * silently falling back to a connector with potentially wrong semantics. */
234
+ export const QUEUE_CONNECTOR_BACKENDS: Readonly<
235
+ Record<string, QueueConnectorBackend>
236
+ > = {
237
+ 'play-runtime-tasks': 'absurd',
238
+ 'run-ledger-projector-outbox': 'postgres-outbox',
239
+ 'compute-billing-jobs': 'postgres-outbox',
240
+ 'play-webhook-receipts': 'convex',
241
+ 'workflow-cron-bindings': 'convex',
242
+ 'play-cron-bindings': 'convex',
243
+ 'ingestion-jobs': 'provider',
244
+ 'monitor-fleet-convergence': 'convex',
245
+ 'monitor-deployment-jobs': 'workpool',
246
+ 'monitor-batch-operations': 'workpool',
247
+ 'provider-monitor-reconciliation': 'provider',
248
+ 'signal-radar-inbound-dlq': 'provider',
249
+ 'charge-reconciliation-backlog': 'postgres-outbox',
250
+ 'workspace-fast-settlement': 'convex',
251
+ 'metronome-usage-delivery': 'postgres-outbox',
252
+ 'billing-operations': 'postgres-outbox',
253
+ 'stripe-financial-inbox': 'provider',
254
+ 'async-usage-recovery': 'postgres-outbox',
255
+ 'notification-deliveries': 'workpool',
256
+ 'usage-rollup-fold': 'postgres-outbox',
257
+ 'play-deployment-jobs': 'workpool',
258
+ 'github-runner-wait': 'provider',
259
+ 'sandbox-cleanup-jobs': 'postgres-outbox',
260
+ };
261
+
262
+ function defaultRegistration(
263
+ queueId: string,
264
+ backend: QueueConnectorBackend,
265
+ ): QueueItemConnectorRegistration {
266
+ const options = {
267
+ queueId,
268
+ source: `${backend}:unavailable`,
269
+ };
270
+ if (backend === 'convex') return createConvexQueueItemConnector(options);
271
+ if (backend === 'workpool') return createWorkpoolQueueItemConnector(options);
272
+ if (backend === 'postgres-outbox')
273
+ return createPostgresOutboxQueueItemConnector(options);
274
+ if (backend === 'absurd') return createAbsurdQueueItemConnector(options);
275
+ return createProviderQueueItemConnector(options);
276
+ }
277
+
278
+ export function createDefaultQueueItemConnectorRegistry(): QueueItemConnectorRegistry {
279
+ const catalogIds = new Set(OPERATIONS_QUEUE_CATALOG.map((entry) => entry.id));
280
+ const mappedIds = Object.keys(QUEUE_CONNECTOR_BACKENDS);
281
+ const missing = [...catalogIds].filter(
282
+ (queueId) => QUEUE_CONNECTOR_BACKENDS[queueId] === undefined,
283
+ );
284
+ const extra = mappedIds.filter((queueId) => !catalogIds.has(queueId));
285
+ if (missing.length > 0 || extra.length > 0) {
286
+ throw new Error(
287
+ `Queue connector mapping drift: missing=${missing.join(',') || 'none'} extra=${extra.join(',') || 'none'}`,
288
+ );
289
+ }
290
+ return new QueueItemConnectorRegistry({
291
+ registrations: OPERATIONS_QUEUE_CATALOG.map((entry) =>
292
+ defaultRegistration(entry.id, QUEUE_CONNECTOR_BACKENDS[entry.id]!),
293
+ ),
294
+ });
295
+ }
296
+
297
+ /** Process-local default used by the admin control service. Owner modules can
298
+ * replace an unavailable registration during startup with
299
+ * `register(ownerRegistration, { replaceUnavailable: true })`; there is no
300
+ * fallback that mutates source data. */
301
+ export const defaultQueueItemConnectorRegistry =
302
+ createDefaultQueueItemConnectorRegistry();
303
+
304
+ /**
305
+ * Owner startup hook. A source module should register its connector before
306
+ * serving admin requests, replacing only the matching unavailable placeholder.
307
+ */
308
+ export function registerQueueItemConnector(
309
+ registration: QueueItemConnectorRegistration,
310
+ ): void {
311
+ defaultQueueItemConnectorRegistry.register(registration, {
312
+ replaceUnavailable: true,
313
+ });
314
+ }
@@ -655,7 +655,7 @@
655
655
  "id": "convex.provider-monitor-credit-recovery",
656
656
  "name": "Provider monitor credit-balance recovery",
657
657
  "scheduler": "convex_scheduled_action",
658
- "cadence": "runAfter(0) after a non-admitting balance becomes positive",
658
+ "cadence": "runAfter(0) after a non-admitting balance becomes positive (legacy queued delivery)",
659
659
  "timezone": "UTC",
660
660
  "source": "packages/convex/scheduledJobsNode.ts",
661
661
  "locator": "recoverProviderMonitorsAfterCreditBalanceChange",
@@ -669,6 +669,24 @@
669
669
  "failureAlert": "convex_action_wrapper",
670
670
  "domainReportRequired": true
671
671
  },
672
+ {
673
+ "id": "convex.credit-blocked-automation-recovery",
674
+ "name": "Credit-blocked automation recovery",
675
+ "scheduler": "convex_scheduled_action",
676
+ "cadence": "runAfter(0) after any positive balance movement; Monitor recovery only at the non-admitting-to-positive crossing",
677
+ "timezone": "UTC",
678
+ "source": "packages/convex/scheduledJobsNode.ts",
679
+ "locator": "recoverCreditBlockedAutomationsAfterCreditBalanceChange",
680
+ "owner": "billing",
681
+ "criticality": "critical",
682
+ "purpose": "Recover scheduled Plays and provider Monitors blocked by insufficient credits after a positive balance movement.",
683
+ "successDefinition": "Eligible credit-blocked automations are resumed or a valid no-op is recorded; ineligible resources remain unchanged.",
684
+ "failureDefinition": "Recovery intent cannot be durably recorded or an eligible automation cannot be rescheduled or reactivated.",
685
+ "startGraceMs": 120000,
686
+ "runTimeoutMs": 1800000,
687
+ "failureAlert": "convex_action_wrapper",
688
+ "domainReportRequired": true
689
+ },
672
690
  {
673
691
  "id": "inngest.workflow-run-watchdog",
674
692
  "name": "Workflow run watchdog",