deepline 0.3.120 → 0.3.122
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.
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/monitors/billing/credit-recovery.ts +77 -0
- package/dist/bundling-sources/shared_libs/monitors/package.json +2 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +19 -1
- package/dist/bundling-sources/shared_libs/observability/dlq.ts +339 -0
- package/dist/bundling-sources/shared_libs/observability/queue-item-connectors.ts +314 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +19 -1
- package/dist/bundling-sources/shared_libs/observability/telemetry.ts +5 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +36 -4
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +96 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +10 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/install-integrity.json +2 -0
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
|
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
|
|
|
200
200
|
// getters keep their established compatibility behavior.
|
|
201
201
|
// 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
|
|
202
202
|
// 0.3.90 is the first deliberately versioned SDK release for API v3.
|
|
203
|
-
version: '0.3.
|
|
203
|
+
version: '0.3.122',
|
|
204
204
|
updateSummary:
|
|
205
205
|
'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
|
|
206
206
|
packageCapabilities: {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { parseStoredMonitorBillingState } from './state';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Only these event-time billing denials are healed by a restored Deepline
|
|
5
|
+
* balance. A monthly spending cap and an opaque billing failure deliberately
|
|
6
|
+
* remain explicit-recovery states.
|
|
7
|
+
*/
|
|
8
|
+
export function isCreditExhaustionSuspensionReason(
|
|
9
|
+
reason: string | undefined,
|
|
10
|
+
): boolean {
|
|
11
|
+
return (
|
|
12
|
+
reason === 'insufficient_credits' ||
|
|
13
|
+
reason === 'insufficient_credits_after_provider_event'
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A billing suspension is not itself proof that TamRadar accepted the delete.
|
|
19
|
+
* Keep this test beside the recovery selector so every automatic path rejects
|
|
20
|
+
* an unfinished cleanup, including a retired binding that still needs work.
|
|
21
|
+
*/
|
|
22
|
+
export function hasConfirmedProviderMonitorUpstreamCleanup(
|
|
23
|
+
providerBinding: unknown,
|
|
24
|
+
): boolean {
|
|
25
|
+
if (!providerBinding || typeof providerBinding !== 'object') return false;
|
|
26
|
+
const binding = providerBinding as Record<string, unknown>;
|
|
27
|
+
const suspension = binding.billing_suspension;
|
|
28
|
+
if (!suspension || typeof suspension !== 'object') return false;
|
|
29
|
+
const upstreamDisabledAt = (suspension as Record<string, unknown>)
|
|
30
|
+
.upstream_disabled_at;
|
|
31
|
+
if (typeof upstreamDisabledAt !== 'string' || !upstreamDisabledAt.length) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const retiredBindings = binding.retired_bindings;
|
|
35
|
+
if (!Array.isArray(retiredBindings) || retiredBindings.length === 0) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
const cleanup = binding.cleanup;
|
|
39
|
+
const cleanupStatus =
|
|
40
|
+
cleanup && typeof cleanup === 'object'
|
|
41
|
+
? (cleanup as Record<string, unknown>).status
|
|
42
|
+
: null;
|
|
43
|
+
return cleanupStatus === 'old_cleanup_deleted';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Narrow selector for direct event-time suspensions that credit restoration
|
|
48
|
+
* may re-provision. It intentionally does not include policy-only pauses,
|
|
49
|
+
* manual intent, spending-cap denials, or cleanup still in flight.
|
|
50
|
+
*/
|
|
51
|
+
export function isCreditRestorableDirectMonitorSuspension(input: {
|
|
52
|
+
fleetId?: string;
|
|
53
|
+
status: string;
|
|
54
|
+
desired?: 'active' | 'inactive';
|
|
55
|
+
deactivationReason?: string;
|
|
56
|
+
billingState: unknown;
|
|
57
|
+
providerBinding: unknown;
|
|
58
|
+
}): boolean {
|
|
59
|
+
if (input.fleetId !== undefined || input.status !== 'disabled') return false;
|
|
60
|
+
const billingState = parseStoredMonitorBillingState(input.billingState);
|
|
61
|
+
if (
|
|
62
|
+
billingState.state !== 'billing_suspended' ||
|
|
63
|
+
!billingState.reactivation_required ||
|
|
64
|
+
!isCreditExhaustionSuspensionReason(billingState.suspension_reason) ||
|
|
65
|
+
!hasConfirmedProviderMonitorUpstreamCleanup(input.providerBinding)
|
|
66
|
+
) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
// An inactive desired state with no policy provenance is the customer's
|
|
70
|
+
// current choice. Historical policy rows can be identified exactly by the
|
|
71
|
+
// closed control-plane reason; an already-active desired state is a prior
|
|
72
|
+
// restoration intent that still requires the normal reactivation lifecycle.
|
|
73
|
+
return (
|
|
74
|
+
input.desired !== 'inactive' ||
|
|
75
|
+
input.deactivationReason === 'insufficient_balance'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"./analytics-public.js": "./analytics-public.ts",
|
|
12
12
|
"./billing/charges": "./billing/charges.ts",
|
|
13
13
|
"./billing/charges.js": "./billing/charges.ts",
|
|
14
|
+
"./billing/credit-recovery": "./billing/credit-recovery.ts",
|
|
15
|
+
"./billing/credit-recovery.js": "./billing/credit-recovery.ts",
|
|
14
16
|
"./billing/policy-builders": "./billing/policy-builders.ts",
|
|
15
17
|
"./billing/policy-builders.js": "./billing/policy-builders.ts",
|
|
16
18
|
"./billing/pricing": "./billing/pricing.ts",
|
|
@@ -65,6 +65,11 @@ export type ProviderMonitorBatchGlobalAuditScope = {
|
|
|
65
65
|
monitorKey?: string;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
/** Internal-only scope for credit-restoration reactivation. */
|
|
69
|
+
export type ProviderMonitorBatchCreditRecoveryScope = {
|
|
70
|
+
kind: 'credit_recovery';
|
|
71
|
+
};
|
|
72
|
+
|
|
68
73
|
export type ProviderMonitorBatchScope =
|
|
69
74
|
| { kind: 'keys'; monitorKeys: readonly string[] }
|
|
70
75
|
| { kind: 'target'; target: ProviderMonitorBatchTarget }
|
|
@@ -76,7 +81,8 @@ export type ProviderMonitorBatchScope =
|
|
|
76
81
|
generation: number;
|
|
77
82
|
intent: 'sync' | 'start' | 'stop';
|
|
78
83
|
}
|
|
79
|
-
| ProviderMonitorBatchGlobalAuditScope
|
|
84
|
+
| ProviderMonitorBatchGlobalAuditScope
|
|
85
|
+
| ProviderMonitorBatchCreditRecoveryScope;
|
|
80
86
|
|
|
81
87
|
export type ProviderMonitorBatchOperationRequest = {
|
|
82
88
|
kind: ProviderMonitorBatchOperationKind;
|
|
@@ -389,6 +395,18 @@ export function validateProviderMonitorBatchOperationRequest(
|
|
|
389
395
|
...(callbackBaseUrl === undefined ? {} : { callbackBaseUrl }),
|
|
390
396
|
};
|
|
391
397
|
}
|
|
398
|
+
if (scope.kind === 'credit_recovery') {
|
|
399
|
+
if (request.kind !== 'reactivate') {
|
|
400
|
+
throw new Error(
|
|
401
|
+
'A credit-recovery scope is only valid for reactivation.',
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
...request,
|
|
406
|
+
scope: { kind: 'credit_recovery' },
|
|
407
|
+
...(callbackBaseUrl === undefined ? {} : { callbackBaseUrl }),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
392
410
|
throw new Error(
|
|
393
411
|
'A monitor batch operation requires exactly one supported scope.',
|
|
394
412
|
);
|
|
@@ -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",
|
|
@@ -31,6 +31,9 @@ export type TelemetryContext = {
|
|
|
31
31
|
workflowOrigin?: string | null;
|
|
32
32
|
playName?: string | null;
|
|
33
33
|
machineId?: string | null;
|
|
34
|
+
gatewayRole?: string | null;
|
|
35
|
+
/** Stable identity for one in-process gateway handler lifecycle. */
|
|
36
|
+
gatewayLifecycleId?: string | null;
|
|
34
37
|
};
|
|
35
38
|
|
|
36
39
|
export type TelemetryLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
@@ -182,6 +185,8 @@ const RESERVED_FIELDS = new Set([
|
|
|
182
185
|
'workflowOrigin',
|
|
183
186
|
'playName',
|
|
184
187
|
'machineId',
|
|
188
|
+
'gatewayRole',
|
|
189
|
+
'gatewayLifecycleId',
|
|
185
190
|
]);
|
|
186
191
|
|
|
187
192
|
function errorTriage(fields: TelemetryFields | undefined): TelemetryTriage {
|
|
@@ -110,6 +110,7 @@ import {
|
|
|
110
110
|
type ToolResponseView,
|
|
111
111
|
} from '../plays/tool-response-contract';
|
|
112
112
|
import {
|
|
113
|
+
AllAdmittedRowsFailedError,
|
|
113
114
|
isProviderExhaustedFailure,
|
|
114
115
|
ProviderExhaustedError,
|
|
115
116
|
} from './run-failure';
|
|
@@ -1529,10 +1530,13 @@ type FieldMapRunResult = {
|
|
|
1529
1530
|
failedRows: PersistableMapRow[];
|
|
1530
1531
|
completedRowCount: number;
|
|
1531
1532
|
failedRowCount: number;
|
|
1533
|
+
failureSamples: Array<{ rowLabel: string; error: string }>;
|
|
1532
1534
|
previewRows: Record<string, unknown>[];
|
|
1533
1535
|
retainedRowsComplete: boolean;
|
|
1534
1536
|
};
|
|
1535
1537
|
|
|
1538
|
+
const MAP_FAILURE_SAMPLE_LIMIT = 3;
|
|
1539
|
+
|
|
1536
1540
|
type RuntimeMapRowPersistence = {
|
|
1537
1541
|
persistRows: (rows: PersistableMapRow[]) => {
|
|
1538
1542
|
admitted: Promise<void>;
|
|
@@ -6804,6 +6808,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
6804
6808
|
let failedCount = 0;
|
|
6805
6809
|
let duplicateReuseCount = 0;
|
|
6806
6810
|
let executedCount = 0;
|
|
6811
|
+
const failureSamples: Array<{ rowLabel: string; error: string }> = [];
|
|
6812
|
+
const failureSampleErrors = new Set<string>();
|
|
6807
6813
|
const previewRows: Record<string, unknown>[] = [];
|
|
6808
6814
|
// A bounded dataset may be materialized by authored code immediately
|
|
6809
6815
|
// after run(). Keep the rows produced by this exact awaited execution so
|
|
@@ -7179,6 +7185,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
7179
7185
|
executedCount += rowsToExecute.length;
|
|
7180
7186
|
successfulCount += mapResult.completedRowCount;
|
|
7181
7187
|
failedCount += mapResult.failedRowCount;
|
|
7188
|
+
for (const sample of mapResult.failureSamples) {
|
|
7189
|
+
if (
|
|
7190
|
+
failureSamples.length >= MAP_FAILURE_SAMPLE_LIMIT ||
|
|
7191
|
+
failureSampleErrors.has(sample.error)
|
|
7192
|
+
) {
|
|
7193
|
+
continue;
|
|
7194
|
+
}
|
|
7195
|
+
failureSampleErrors.add(sample.error);
|
|
7196
|
+
failureSamples.push(sample);
|
|
7197
|
+
}
|
|
7182
7198
|
processedCount += rowsToExecute.length;
|
|
7183
7199
|
page.rows.length = 0;
|
|
7184
7200
|
}
|
|
@@ -7216,10 +7232,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
7216
7232
|
totalRows: totalInputCount,
|
|
7217
7233
|
at: Date.now(),
|
|
7218
7234
|
});
|
|
7219
|
-
throw new
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7235
|
+
throw new AllAdmittedRowsFailedError({
|
|
7236
|
+
mapName: normalizedMapNamespace,
|
|
7237
|
+
failedCount,
|
|
7238
|
+
totalInputCount,
|
|
7239
|
+
samples: failureSamples,
|
|
7240
|
+
});
|
|
7223
7241
|
}
|
|
7224
7242
|
|
|
7225
7243
|
this.setMapFrame({
|
|
@@ -8105,6 +8123,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
8105
8123
|
const failFastOnRowError = runtimeOptions?.onRowError === 'fail';
|
|
8106
8124
|
const completedRowsToPersist: PersistableMapRow[] = [];
|
|
8107
8125
|
const failedRowsToPersist: PersistableMapRow[] = [];
|
|
8126
|
+
const failureSamples: Array<{ rowLabel: string; error: string }> = [];
|
|
8127
|
+
const failureSampleErrors = new Set<string>();
|
|
8108
8128
|
const previewRows: Record<string, unknown>[] = [];
|
|
8109
8129
|
let completedRowCount = 0;
|
|
8110
8130
|
let failedRowCount = 0;
|
|
@@ -8368,6 +8388,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
8368
8388
|
failedRows: [],
|
|
8369
8389
|
completedRowCount: results.length,
|
|
8370
8390
|
failedRowCount: 0,
|
|
8391
|
+
failureSamples: [],
|
|
8371
8392
|
previewRows: results.slice(0, 5),
|
|
8372
8393
|
retainedRowsComplete: true,
|
|
8373
8394
|
};
|
|
@@ -8573,6 +8594,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
8573
8594
|
error: formattedError,
|
|
8574
8595
|
});
|
|
8575
8596
|
failedRowCount += 1;
|
|
8597
|
+
if (
|
|
8598
|
+
failureSamples.length < MAP_FAILURE_SAMPLE_LIMIT &&
|
|
8599
|
+
!failureSampleErrors.has(formattedError)
|
|
8600
|
+
) {
|
|
8601
|
+
failureSampleErrors.add(formattedError);
|
|
8602
|
+
failureSamples.push({
|
|
8603
|
+
rowLabel: failedMapRowLogLabel(failedRow),
|
|
8604
|
+
error: formattedError,
|
|
8605
|
+
});
|
|
8606
|
+
}
|
|
8576
8607
|
retainPersistedRow(failedRow, failedRowsToPersist);
|
|
8577
8608
|
this.emitScopedRowUpdate(rowKey, normalizedTableNamespace, {
|
|
8578
8609
|
rowId: idx,
|
|
@@ -8985,6 +9016,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
8985
9016
|
failedRows: failedRowsToPersist,
|
|
8986
9017
|
completedRowCount,
|
|
8987
9018
|
failedRowCount,
|
|
9019
|
+
failureSamples,
|
|
8988
9020
|
previewRows,
|
|
8989
9021
|
retainedRowsComplete,
|
|
8990
9022
|
};
|
|
@@ -58,6 +58,93 @@ export const MODAL_PAYLOAD_UPLOAD_TIMEOUT_MESSAGE =
|
|
|
58
58
|
|
|
59
59
|
export const WORKSPACE_STORAGE_NOT_READY_CODE = 'WORKSPACE_STORAGE_NOT_READY';
|
|
60
60
|
|
|
61
|
+
export const ALL_ADMITTED_ROWS_FAILED_CODE = 'ALL_ADMITTED_ROWS_FAILED';
|
|
62
|
+
|
|
63
|
+
export type AllAdmittedRowsFailedSample = {
|
|
64
|
+
rowLabel: string;
|
|
65
|
+
error: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const ALL_ADMITTED_ROWS_FAILED_SAMPLE_LIMIT = 3;
|
|
69
|
+
const ALL_ADMITTED_ROWS_FAILED_SAMPLE_TEXT_LIMIT = 500;
|
|
70
|
+
|
|
71
|
+
function boundedAllAdmittedRowsFailedSample(value: string): string {
|
|
72
|
+
const normalized = value.trim();
|
|
73
|
+
if (normalized.length <= ALL_ADMITTED_ROWS_FAILED_SAMPLE_TEXT_LIMIT) {
|
|
74
|
+
return normalized;
|
|
75
|
+
}
|
|
76
|
+
return `${normalized.slice(0, ALL_ADMITTED_ROWS_FAILED_SAMPLE_TEXT_LIMIT - 1)}…`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function allAdmittedRowsFailedMessage(input: {
|
|
80
|
+
mapName: string;
|
|
81
|
+
failedCount: number;
|
|
82
|
+
totalInputCount: number;
|
|
83
|
+
samples: readonly AllAdmittedRowsFailedSample[];
|
|
84
|
+
}): string {
|
|
85
|
+
const representativeFailures = input.samples
|
|
86
|
+
.slice(0, ALL_ADMITTED_ROWS_FAILED_SAMPLE_LIMIT)
|
|
87
|
+
.map(
|
|
88
|
+
(sample) =>
|
|
89
|
+
`${boundedAllAdmittedRowsFailedSample(sample.rowLabel)}: ${boundedAllAdmittedRowsFailedSample(sample.error)}`,
|
|
90
|
+
)
|
|
91
|
+
.join('; ');
|
|
92
|
+
const omittedCount = Math.max(
|
|
93
|
+
0,
|
|
94
|
+
input.failedCount -
|
|
95
|
+
Math.min(input.samples.length, ALL_ADMITTED_ROWS_FAILED_SAMPLE_LIMIT),
|
|
96
|
+
);
|
|
97
|
+
const omittedSuffix =
|
|
98
|
+
omittedCount > 0 ? `; ${omittedCount} additional failure(s) omitted` : '';
|
|
99
|
+
return (
|
|
100
|
+
`ctx.dataset("${input.mapName}") produced no successful rows ` +
|
|
101
|
+
`(${input.failedCount}/${input.totalInputCount} admitted rows failed). ` +
|
|
102
|
+
(representativeFailures
|
|
103
|
+
? `Representative failures: ${representativeFailures}${omittedSuffix}. `
|
|
104
|
+
: '') +
|
|
105
|
+
'Failed rows were persisted for review and may be retried on a later run.'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class AllAdmittedRowsFailedError extends Error {
|
|
110
|
+
readonly code = ALL_ADMITTED_ROWS_FAILED_CODE;
|
|
111
|
+
|
|
112
|
+
constructor(input: {
|
|
113
|
+
mapName: string;
|
|
114
|
+
failedCount: number;
|
|
115
|
+
totalInputCount: number;
|
|
116
|
+
samples?: readonly AllAdmittedRowsFailedSample[];
|
|
117
|
+
}) {
|
|
118
|
+
super(
|
|
119
|
+
`${ALL_ADMITTED_ROWS_FAILED_CODE}: ${allAdmittedRowsFailedMessage({
|
|
120
|
+
...input,
|
|
121
|
+
samples: input.samples ?? [],
|
|
122
|
+
})}`,
|
|
123
|
+
);
|
|
124
|
+
this.name = 'AllAdmittedRowsFailedError';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function isAllAdmittedRowsFailedFailure(error: unknown): boolean {
|
|
129
|
+
if (error instanceof AllAdmittedRowsFailedError) return true;
|
|
130
|
+
const code =
|
|
131
|
+
error && typeof error === 'object'
|
|
132
|
+
? (error as { code?: unknown }).code
|
|
133
|
+
: null;
|
|
134
|
+
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
135
|
+
return (
|
|
136
|
+
code === ALL_ADMITTED_ROWS_FAILED_CODE ||
|
|
137
|
+
new RegExp(`^${ALL_ADMITTED_ROWS_FAILED_CODE}:\\s*`).test(message)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function allAdmittedRowsFailedPublicMessage(cause: string): string {
|
|
142
|
+
return cause.replace(
|
|
143
|
+
new RegExp(`^${ALL_ADMITTED_ROWS_FAILED_CODE}:\\s*`),
|
|
144
|
+
'',
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
61
148
|
// User-facing message. Contains NO SQL, role names, database names, or hosts by
|
|
62
149
|
// construction, so it is safe past redactRuntimeInternalError (which only strips
|
|
63
150
|
// postgres-URI passwords). The raw pg error goes in the Error `cause` for server
|
|
@@ -323,6 +410,15 @@ function toErrorText(error: unknown): string {
|
|
|
323
410
|
export function normalizePlayRunFailure(error: unknown): PlayRunFailureDetails {
|
|
324
411
|
const rawCause = toErrorText(error);
|
|
325
412
|
const cause = boundedFailureText(rawCause);
|
|
413
|
+
if (isAllAdmittedRowsFailedFailure(error)) {
|
|
414
|
+
return {
|
|
415
|
+
code: ALL_ADMITTED_ROWS_FAILED_CODE,
|
|
416
|
+
phase: 'runtime',
|
|
417
|
+
message: allAdmittedRowsFailedPublicMessage(cause),
|
|
418
|
+
retryable: false,
|
|
419
|
+
cause,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
326
422
|
if (
|
|
327
423
|
error instanceof ToolExecutionError &&
|
|
328
424
|
error.origin === 'deepline' &&
|
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts
CHANGED
|
@@ -2043,6 +2043,16 @@ async function prepareAbsurdAttempt(input: {
|
|
|
2043
2043
|
exists: projection.exists,
|
|
2044
2044
|
blocked: projection.blocked,
|
|
2045
2045
|
});
|
|
2046
|
+
console.info('[runtime.scheduler.projection_deferred]', {
|
|
2047
|
+
runId: input.runId,
|
|
2048
|
+
eventId,
|
|
2049
|
+
phase: 'run_created_projection_gate',
|
|
2050
|
+
status: 'deferred',
|
|
2051
|
+
exists: projection.exists,
|
|
2052
|
+
blocked: projection.blocked,
|
|
2053
|
+
delaySeconds: projection.blocked ? 5 : 1,
|
|
2054
|
+
attemptAllocated: false,
|
|
2055
|
+
});
|
|
2046
2056
|
throw new DeferTask(
|
|
2047
2057
|
`Run-created projection is not yet delivered for ${input.runId}.`,
|
|
2048
2058
|
projection.blocked ? 5 : 1,
|
package/dist/cli/index.js
CHANGED
|
@@ -3068,7 +3068,7 @@ var SDK_RELEASE = {
|
|
|
3068
3068
|
// getters keep their established compatibility behavior.
|
|
3069
3069
|
// 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
|
|
3070
3070
|
// 0.3.90 is the first deliberately versioned SDK release for API v3.
|
|
3071
|
-
version: "0.3.
|
|
3071
|
+
version: "0.3.122",
|
|
3072
3072
|
updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
|
|
3073
3073
|
packageCapabilities: {
|
|
3074
3074
|
updatePreferences: 1
|
package/dist/cli/index.mjs
CHANGED
|
@@ -3063,7 +3063,7 @@ var SDK_RELEASE = {
|
|
|
3063
3063
|
// getters keep their established compatibility behavior.
|
|
3064
3064
|
// 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
|
|
3065
3065
|
// 0.3.90 is the first deliberately versioned SDK release for API v3.
|
|
3066
|
-
version: "0.3.
|
|
3066
|
+
version: "0.3.122",
|
|
3067
3067
|
updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
|
|
3068
3068
|
packageCapabilities: {
|
|
3069
3069
|
updatePreferences: 1
|
package/dist/index.js
CHANGED
|
@@ -863,7 +863,7 @@ var SDK_RELEASE = {
|
|
|
863
863
|
// getters keep their established compatibility behavior.
|
|
864
864
|
// 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
|
|
865
865
|
// 0.3.90 is the first deliberately versioned SDK release for API v3.
|
|
866
|
-
version: "0.3.
|
|
866
|
+
version: "0.3.122",
|
|
867
867
|
updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
|
|
868
868
|
packageCapabilities: {
|
|
869
869
|
updatePreferences: 1
|
package/dist/index.mjs
CHANGED
|
@@ -768,7 +768,7 @@ var SDK_RELEASE = {
|
|
|
768
768
|
// getters keep their established compatibility behavior.
|
|
769
769
|
// 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
|
|
770
770
|
// 0.3.90 is the first deliberately versioned SDK release for API v3.
|
|
771
|
-
version: "0.3.
|
|
771
|
+
version: "0.3.122",
|
|
772
772
|
updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
|
|
773
773
|
packageCapabilities: {
|
|
774
774
|
updatePreferences: 1
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"dist/bundling-sources/shared_libs/monitors/analytics-install-guides.ts",
|
|
36
36
|
"dist/bundling-sources/shared_libs/monitors/analytics-public.ts",
|
|
37
37
|
"dist/bundling-sources/shared_libs/monitors/billing/charges.ts",
|
|
38
|
+
"dist/bundling-sources/shared_libs/monitors/billing/credit-recovery.ts",
|
|
38
39
|
"dist/bundling-sources/shared_libs/monitors/billing/policy-builders.ts",
|
|
39
40
|
"dist/bundling-sources/shared_libs/monitors/billing/pricing.ts",
|
|
40
41
|
"dist/bundling-sources/shared_libs/monitors/billing/public-pricing.ts",
|
|
@@ -93,6 +94,7 @@
|
|
|
93
94
|
"dist/bundling-sources/shared_libs/observability/log-metadata.ts",
|
|
94
95
|
"dist/bundling-sources/shared_libs/observability/node-tracing.ts",
|
|
95
96
|
"dist/bundling-sources/shared_libs/observability/queue-health.ts",
|
|
97
|
+
"dist/bundling-sources/shared_libs/observability/queue-item-connectors.ts",
|
|
96
98
|
"dist/bundling-sources/shared_libs/observability/redaction.ts",
|
|
97
99
|
"dist/bundling-sources/shared_libs/observability/request-context.ts",
|
|
98
100
|
"dist/bundling-sources/shared_libs/observability/scheduled-job-errors.ts",
|
package/dist/release.d.mts
CHANGED
|
@@ -149,7 +149,7 @@ type SdkRelease = {
|
|
|
149
149
|
supportPolicy: SdkSupportPolicy;
|
|
150
150
|
};
|
|
151
151
|
declare const SDK_RELEASE: {
|
|
152
|
-
readonly version: "0.3.
|
|
152
|
+
readonly version: "0.3.122";
|
|
153
153
|
readonly updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.";
|
|
154
154
|
readonly packageCapabilities: {
|
|
155
155
|
readonly updatePreferences: 1;
|
package/dist/release.d.ts
CHANGED
|
@@ -149,7 +149,7 @@ type SdkRelease = {
|
|
|
149
149
|
supportPolicy: SdkSupportPolicy;
|
|
150
150
|
};
|
|
151
151
|
declare const SDK_RELEASE: {
|
|
152
|
-
readonly version: "0.3.
|
|
152
|
+
readonly version: "0.3.122";
|
|
153
153
|
readonly updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.";
|
|
154
154
|
readonly packageCapabilities: {
|
|
155
155
|
readonly updatePreferences: 1;
|
package/dist/release.js
CHANGED
|
@@ -74,7 +74,7 @@ var SDK_RELEASE = {
|
|
|
74
74
|
// getters keep their established compatibility behavior.
|
|
75
75
|
// 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
|
|
76
76
|
// 0.3.90 is the first deliberately versioned SDK release for API v3.
|
|
77
|
-
version: "0.3.
|
|
77
|
+
version: "0.3.122",
|
|
78
78
|
updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
|
|
79
79
|
packageCapabilities: {
|
|
80
80
|
updatePreferences: 1
|
package/dist/release.mjs
CHANGED
|
@@ -48,7 +48,7 @@ var SDK_RELEASE = {
|
|
|
48
48
|
// getters keep their established compatibility behavior.
|
|
49
49
|
// 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
|
|
50
50
|
// 0.3.90 is the first deliberately versioned SDK release for API v3.
|
|
51
|
-
version: "0.3.
|
|
51
|
+
version: "0.3.122",
|
|
52
52
|
updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
|
|
53
53
|
packageCapabilities: {
|
|
54
54
|
updatePreferences: 1
|