deepline 0.3.258 → 0.3.259
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/api-error.ts +45 -0
- package/dist/bundling-sources/sdk/src/client.ts +98 -459
- package/dist/bundling-sources/sdk/src/http.ts +1 -34
- package/dist/bundling-sources/sdk/src/index.ts +8 -0
- package/dist/bundling-sources/sdk/src/plays/csv-data-input.ts +139 -0
- package/dist/bundling-sources/sdk/src/plays/run-request.ts +855 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/runs/overview.ts +45 -2
- package/dist/bundling-sources/sdk/src/types.ts +5 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +42 -10
- package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +23 -0
- package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-admission-outcome.ts +23 -5
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-execution-state.ts +845 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +106 -6
- package/dist/bundling-sources/shared_libs/play-runtime/run-observation.ts +440 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +35 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +66 -7
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +0 -29
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-transition.ts +47 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +475 -36
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +78 -11
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +550 -42
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +181 -8
- package/dist/bundling-sources/shared_libs/play-runtime/step-lifecycle-tracker.ts +0 -18
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +80 -7
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +37 -2
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +148 -18
- package/dist/bundling-sources/shared_libs/plays/cascade-falloff.ts +86 -0
- package/dist/bundling-sources/shared_libs/plays/docflow-unreached-reason.ts +611 -0
- package/dist/bundling-sources/shared_libs/plays/docflow.ts +21 -0
- package/dist/bundling-sources/shared_libs/plays/play-exports.ts +28 -5
- package/dist/bundling-sources/shared_libs/plays/settings-contract.ts +92 -10
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +5 -0
- package/dist/bundling-sources/shared_libs/plays/trigger-config-contract.ts +222 -0
- package/dist/bundling-sources/shared_libs/plays/trigger-config-route.ts +15 -0
- package/dist/cli/index.js +4957 -771
- package/dist/cli/index.mjs +4957 -771
- package/dist/client-B5XbKrrs.d.mts +6564 -0
- package/dist/client-C9Y49yya.d.ts +6564 -0
- package/dist/{compiler-manifest-C4mdRJOP.d.mts → compiler-manifest-CUuh8C3b.d.mts} +14 -1
- package/dist/{compiler-manifest-C4mdRJOP.d.ts → compiler-manifest-CUuh8C3b.d.ts} +14 -1
- package/dist/index.d.mts +7 -6275
- package/dist/index.d.ts +7 -6275
- package/dist/index.js +360 -326
- package/dist/index.mjs +360 -326
- package/dist/install-integrity.json +21 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +117 -18
- package/dist/plays/csv-data-input.d.mts +37 -0
- package/dist/plays/csv-data-input.d.ts +37 -0
- package/dist/plays/csv-data-input.js +3135 -0
- package/dist/plays/csv-data-input.mjs +3108 -0
- package/dist/plays/run-request.d.mts +6 -0
- package/dist/plays/run-request.d.ts +6 -0
- package/dist/plays/run-request.js +664 -0
- package/dist/plays/run-request.mjs +614 -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 +12 -1
|
@@ -14,7 +14,10 @@ import {
|
|
|
14
14
|
readPostgresSchedulerOutboxNextDueAt,
|
|
15
15
|
type PostgresSchedulerOptions,
|
|
16
16
|
type PostgresSchedulerOutboxProjectionResult,
|
|
17
|
+
type PostgresSchedulerQueryClient,
|
|
18
|
+
type RunLedgerOutboxDeliveredRow,
|
|
17
19
|
type RunLedgerOutboxHeadPriority,
|
|
20
|
+
type RunLedgerOutboxTokenAuthority,
|
|
18
21
|
} from './postgres';
|
|
19
22
|
import {
|
|
20
23
|
acquirePgNotifyHub,
|
|
@@ -95,9 +98,22 @@ export type RunLedgerOutboxRecoveryController = {
|
|
|
95
98
|
wake: (reason?: string) => void;
|
|
96
99
|
/** Whether the optional direct LISTEN hint is currently usable. */
|
|
97
100
|
notifyState: () => PgNotifyHubState | 'unavailable';
|
|
101
|
+
/**
|
|
102
|
+
* Liveness facts for the drain loop itself, independent of whether the
|
|
103
|
+
* app control plane is reachable. A projection retry is a settled dispatch;
|
|
104
|
+
* only a dispatch that never settles makes the drain look stuck.
|
|
105
|
+
*/
|
|
106
|
+
drainHealth: () => RunLedgerOutboxDrainHealth;
|
|
98
107
|
close: () => Promise<void>;
|
|
99
108
|
};
|
|
100
109
|
|
|
110
|
+
export type RunLedgerOutboxDrainHealth = {
|
|
111
|
+
inFlightDispatches: number;
|
|
112
|
+
/** Start of the oldest dispatch still in flight, or null when idle. */
|
|
113
|
+
oldestInFlightStartedAt: number | null;
|
|
114
|
+
lastDispatchSettledAt: number | null;
|
|
115
|
+
};
|
|
116
|
+
|
|
101
117
|
function boundedFailureDelayMs(failures: number): number {
|
|
102
118
|
const exponent = Math.min(Math.max(0, failures - 1), 4);
|
|
103
119
|
return Math.min(
|
|
@@ -139,14 +155,26 @@ export function startRunLedgerOutboxRecovery(
|
|
|
139
155
|
let unsubscribeNotify: (() => void) | undefined;
|
|
140
156
|
let unsubscribeState: (() => void) | undefined;
|
|
141
157
|
let requestDrain: (reason: string) => void = () => undefined;
|
|
158
|
+
const inFlightStartedAt = new Map<number, number>();
|
|
159
|
+
let nextDispatchId = 0;
|
|
160
|
+
let lastDispatchSettledAt: number | null = null;
|
|
142
161
|
let admissionTurns = 0;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
162
|
+
const dispatchOnce =
|
|
163
|
+
async (): Promise<PostgresSchedulerOutboxProjectionResult> => {
|
|
164
|
+
const oldestReady =
|
|
165
|
+
admissionTurns === ADMISSION_TURNS_PER_OLDEST_READY_TURN;
|
|
166
|
+
admissionTurns = oldestReady ? 0 : admissionTurns + 1;
|
|
167
|
+
const id = nextDispatchId++;
|
|
168
|
+
inFlightStartedAt.set(id, now());
|
|
169
|
+
try {
|
|
170
|
+
return await deps.dispatchOnce(
|
|
171
|
+
oldestReady ? 'oldest_ready' : 'admission_first',
|
|
172
|
+
);
|
|
173
|
+
} finally {
|
|
174
|
+
inFlightStartedAt.delete(id);
|
|
175
|
+
lastDispatchSettledAt = now();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
150
178
|
|
|
151
179
|
const clearWakeTimer = () => {
|
|
152
180
|
if (!timer) return;
|
|
@@ -392,6 +420,17 @@ export function startRunLedgerOutboxRecovery(
|
|
|
392
420
|
return {
|
|
393
421
|
wake: (reason = 'external_safety_wake') => requestDrain(reason),
|
|
394
422
|
notifyState: () => hubLease?.hub.state() ?? 'unavailable',
|
|
423
|
+
drainHealth: () => {
|
|
424
|
+
let oldest: number | null = null;
|
|
425
|
+
for (const startedAt of inFlightStartedAt.values()) {
|
|
426
|
+
if (oldest === null || startedAt < oldest) oldest = startedAt;
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
inFlightDispatches: inFlightStartedAt.size,
|
|
430
|
+
oldestInFlightStartedAt: oldest,
|
|
431
|
+
lastDispatchSettledAt,
|
|
432
|
+
};
|
|
433
|
+
},
|
|
395
434
|
close: async () => {
|
|
396
435
|
if (closed) return;
|
|
397
436
|
closed = true;
|
|
@@ -406,6 +445,130 @@ export function startRunLedgerOutboxRecovery(
|
|
|
406
445
|
};
|
|
407
446
|
}
|
|
408
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Latency budget for the inline `run.created` attempt made by an admitting
|
|
450
|
+
* process after its Runtime Postgres commit. The whole attempt (claim, token,
|
|
451
|
+
* append, acknowledgement) must settle or be abandoned inside this bound so an
|
|
452
|
+
* admission response is never held longer than this for projection.
|
|
453
|
+
*/
|
|
454
|
+
export const RUN_CREATED_INLINE_DELIVERY_DEADLINE_MS = 1_500;
|
|
455
|
+
/**
|
|
456
|
+
* The inline attempt's lease is the scheduler minimum. If the admitting
|
|
457
|
+
* process is frozen or killed mid-attempt, the durable projector can reclaim
|
|
458
|
+
* the row after this many seconds instead of the projector's default lease.
|
|
459
|
+
*/
|
|
460
|
+
const RUN_CREATED_INLINE_DELIVERY_LEASE_SECONDS = 5;
|
|
461
|
+
|
|
462
|
+
export type RunCreatedInlineDeliveryOutcome =
|
|
463
|
+
| { status: 'delivered'; durationMs: number }
|
|
464
|
+
/** Already delivered, leased by the projector, not yet due, or not the head. */
|
|
465
|
+
| { status: 'not_claimed'; durationMs: number }
|
|
466
|
+
/** Claimed but not acknowledged; the durable projector owns the retry. */
|
|
467
|
+
| { status: 'retrying' | 'blocked'; durationMs: number }
|
|
468
|
+
/**
|
|
469
|
+
* Lease lost mid-attempt and another deliverer acknowledged the row. The
|
|
470
|
+
* shared idempotency key made any duplicate ledger append a no-op.
|
|
471
|
+
*/
|
|
472
|
+
| { status: 'superseded'; durationMs: number }
|
|
473
|
+
| { status: 'timed_out'; durationMs: number }
|
|
474
|
+
| { status: 'error'; durationMs: number; error: string };
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* One bounded, idempotent attempt to deliver exactly `run-created:<runId>`
|
|
478
|
+
* from the admitting process, right after the admission commit.
|
|
479
|
+
*
|
|
480
|
+
* ADR-0001: "An inline projection attempt is a latency optimization only."
|
|
481
|
+
* This uses the projector's own single-row path
|
|
482
|
+
* (`dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope`): the same
|
|
483
|
+
* `FOR UPDATE ... SKIP LOCKED` run claim and outbox lease, the same Run Ledger
|
|
484
|
+
* Projection Contract idempotency key, and the same lease-fenced
|
|
485
|
+
* acknowledgement. Concurrent inline and projector delivery therefore produce
|
|
486
|
+
* one ledger admission and one `delivered_at`. The projector remains the
|
|
487
|
+
* durable owner: every non-delivered outcome leaves the row pending for it.
|
|
488
|
+
*
|
|
489
|
+
* Never throws and never exceeds `deadlineMs`; a deadline abandons the
|
|
490
|
+
* append and releases its lease without charging a retry.
|
|
491
|
+
*/
|
|
492
|
+
export async function deliverRunCreatedOutboxEventInline(input: {
|
|
493
|
+
runId: string;
|
|
494
|
+
withClient: <T>(
|
|
495
|
+
operation: (client: PostgresSchedulerQueryClient) => Promise<T>,
|
|
496
|
+
) => Promise<T>;
|
|
497
|
+
schedulerOptions?: Pick<PostgresSchedulerOptions, 'schema'>;
|
|
498
|
+
tokenAuthority: RunLedgerOutboxTokenAuthority;
|
|
499
|
+
deadlineMs?: number;
|
|
500
|
+
now?: () => number;
|
|
501
|
+
}): Promise<RunCreatedInlineDeliveryOutcome> {
|
|
502
|
+
const now = input.now ?? Date.now;
|
|
503
|
+
const startedAt = now();
|
|
504
|
+
const elapsed = () => Math.max(0, now() - startedAt);
|
|
505
|
+
const deadlineMs = Math.max(
|
|
506
|
+
1,
|
|
507
|
+
Math.min(
|
|
508
|
+
RUN_CREATED_INLINE_DELIVERY_DEADLINE_MS,
|
|
509
|
+
input.deadlineMs ?? RUN_CREATED_INLINE_DELIVERY_DEADLINE_MS,
|
|
510
|
+
),
|
|
511
|
+
);
|
|
512
|
+
const runId = input.runId.trim();
|
|
513
|
+
if (!runId) {
|
|
514
|
+
return { status: 'error', durationMs: 0, error: 'missing run id' };
|
|
515
|
+
}
|
|
516
|
+
const controller = new AbortController();
|
|
517
|
+
const attempt = dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
518
|
+
input.withClient,
|
|
519
|
+
{
|
|
520
|
+
runId,
|
|
521
|
+
headEventId: `run-created:${runId}`,
|
|
522
|
+
limit: 1,
|
|
523
|
+
leaseSeconds: RUN_CREATED_INLINE_DELIVERY_LEASE_SECONDS,
|
|
524
|
+
tokenAuthority: input.tokenAuthority,
|
|
525
|
+
signal: controller.signal,
|
|
526
|
+
},
|
|
527
|
+
input.schedulerOptions ?? {},
|
|
528
|
+
).then(
|
|
529
|
+
(result): RunCreatedInlineDeliveryOutcome => {
|
|
530
|
+
if (result.attempted === 0) {
|
|
531
|
+
return { status: 'not_claimed', durationMs: elapsed() };
|
|
532
|
+
}
|
|
533
|
+
if (result.projected > 0) {
|
|
534
|
+
return { status: 'delivered', durationMs: elapsed() };
|
|
535
|
+
}
|
|
536
|
+
if (controller.signal.aborted) {
|
|
537
|
+
return { status: 'timed_out', durationMs: elapsed() };
|
|
538
|
+
}
|
|
539
|
+
if (result.failed === 0 && result.retrying === 0) {
|
|
540
|
+
return { status: 'superseded', durationMs: elapsed() };
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
status: result.failed > 0 ? 'blocked' : 'retrying',
|
|
544
|
+
durationMs: elapsed(),
|
|
545
|
+
};
|
|
546
|
+
},
|
|
547
|
+
(error: unknown): RunCreatedInlineDeliveryOutcome => ({
|
|
548
|
+
status: 'error',
|
|
549
|
+
durationMs: elapsed(),
|
|
550
|
+
error: error instanceof Error ? error.message : String(error),
|
|
551
|
+
}),
|
|
552
|
+
);
|
|
553
|
+
let deadline: ReturnType<typeof setTimeout> | undefined;
|
|
554
|
+
const timedOut = new Promise<RunCreatedInlineDeliveryOutcome>((resolve) => {
|
|
555
|
+
deadline = setTimeout(() => {
|
|
556
|
+
// Abort the append; the attempt's own tail (a short, lease-fenced
|
|
557
|
+
// release) completes in the background and cannot fail the caller.
|
|
558
|
+
controller.abort(
|
|
559
|
+
new Error('Inline run.created delivery deadline elapsed.'),
|
|
560
|
+
);
|
|
561
|
+
resolve({ status: 'timed_out', durationMs: elapsed() });
|
|
562
|
+
}, deadlineMs);
|
|
563
|
+
deadline.unref?.();
|
|
564
|
+
});
|
|
565
|
+
try {
|
|
566
|
+
return await Promise.race([attempt, timedOut]);
|
|
567
|
+
} finally {
|
|
568
|
+
if (deadline) clearTimeout(deadline);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
409
572
|
/**
|
|
410
573
|
* Production wiring shared by each stateless Fly projector replica. Every
|
|
411
574
|
* replica uses gateway authority because Fly holds no signing key; leased
|
|
@@ -416,6 +579,8 @@ export function startRunLedgerOutboxRecoveryController(input: {
|
|
|
416
579
|
schedulerOptions?: Pick<PostgresSchedulerOptions, 'schema'>;
|
|
417
580
|
/** Direct LISTEN is a latency hint and may be disabled for isolated schemas. */
|
|
418
581
|
useDirectNotify?: boolean;
|
|
582
|
+
/** Observation hook for acknowledged rows (lag metrics). */
|
|
583
|
+
onDelivered?: (deliveries: readonly RunLedgerOutboxDeliveredRow[]) => void;
|
|
419
584
|
}): RunLedgerOutboxRecoveryController {
|
|
420
585
|
const schedulerOptions = input.schedulerOptions ?? {};
|
|
421
586
|
return startRunLedgerOutboxRecovery({
|
|
@@ -431,7 +596,15 @@ export function startRunLedgerOutboxRecoveryController(input: {
|
|
|
431
596
|
dispatchOnce: (headPriority) =>
|
|
432
597
|
dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
|
|
433
598
|
(operation) => withRuntimeSchedulerClient(input.poolOptions, operation),
|
|
434
|
-
{
|
|
599
|
+
{
|
|
600
|
+
tokenAuthority: 'gateway',
|
|
601
|
+
limit: 8,
|
|
602
|
+
headPriority,
|
|
603
|
+
// The lease holder delivers a run's terminal committed during its
|
|
604
|
+
// append, instead of leaving it for the next wave.
|
|
605
|
+
continueThroughTerminal: true,
|
|
606
|
+
...(input.onDelivered ? { onDelivered: input.onDelivered } : {}),
|
|
607
|
+
},
|
|
435
608
|
schedulerOptions,
|
|
436
609
|
),
|
|
437
610
|
// `dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope` releases each
|
|
@@ -192,24 +192,6 @@ export class PlayStepLifecycleTracker {
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
markStartedFailed(at = this.now(), error?: string): void {
|
|
196
|
-
for (let index = this.nodes.length - 1; index >= 0; index -= 1) {
|
|
197
|
-
const node = this.nodes[index]!;
|
|
198
|
-
const existing = this.getProgress()[node.nodeId];
|
|
199
|
-
if (existing?.startedAt && !existing.completedAt) {
|
|
200
|
-
this.failedNodeIds.add(node.nodeId);
|
|
201
|
-
this.emit({
|
|
202
|
-
nodeId: node.nodeId,
|
|
203
|
-
type: node.type,
|
|
204
|
-
transition: 'failed',
|
|
205
|
-
at,
|
|
206
|
-
...(error?.trim() ? { error: error.trim() } : {}),
|
|
207
|
-
});
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
195
|
private completeStartedNonMapNodes(at: number): void {
|
|
214
196
|
for (const node of this.nodes) {
|
|
215
197
|
if (node.type === 'dataset') {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isHardBillingToolHttpError,
|
|
3
|
+
isHtmlErrorBody,
|
|
3
4
|
normalizeToolHttpErrorMessage,
|
|
4
5
|
readExplicitToolHttpRetryability,
|
|
5
6
|
type ToolHttpError,
|
|
@@ -145,6 +146,7 @@ export type ToolExecuteHttpRetryDecision = {
|
|
|
145
146
|
| 'gateway_invocation_in_progress'
|
|
146
147
|
| 'customer_db_storage_unavailable'
|
|
147
148
|
| 'retry_safe_transient_5xx'
|
|
149
|
+
| 'edge_gateway_transient'
|
|
148
150
|
| 'unsafe_transient_5xx'
|
|
149
151
|
| 'hard_billing_error'
|
|
150
152
|
| 'non_retryable_status';
|
|
@@ -170,6 +172,7 @@ export type ToolExecuteHttpFailureAttemptTracker = {
|
|
|
170
172
|
status: number;
|
|
171
173
|
bodyText?: string;
|
|
172
174
|
transientHttpRetrySafe?: boolean;
|
|
175
|
+
executionFenced?: boolean;
|
|
173
176
|
}): number;
|
|
174
177
|
};
|
|
175
178
|
|
|
@@ -264,6 +267,41 @@ function isProviderPacingWaitResponse(input: {
|
|
|
264
267
|
);
|
|
265
268
|
}
|
|
266
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Edge statuses (Cloudflare 521/522/523/525/526/530) that mean the edge never
|
|
272
|
+
* reached the Deepline gateway: no provider request can have been dispatched.
|
|
273
|
+
* 530 is Cloudflare's origin-unreachable page (1033 "Tunnel error": no
|
|
274
|
+
* connector serves the hostname, or the origin cannot be resolved) — the
|
|
275
|
+
* request had nowhere to go. A tunnel connector restart surfaced it on a dev
|
|
276
|
+
* gateway and, unretried, it failed the row on its first attempt.
|
|
277
|
+
*/
|
|
278
|
+
const EDGE_ORIGIN_UNREACHED_STATUSES: ReadonlySet<number> = new Set([
|
|
279
|
+
521, 522, 523, 525, 526, 530,
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* A 5xx whose body is the HTML error page of the edge in front of the Deepline
|
|
284
|
+
* gateway (a Cloudflare 524 origin timeout, a 502/503/504 proxy page) rather
|
|
285
|
+
* than a Deepline JSON envelope: the gateway produced no answer of its own, so
|
|
286
|
+
* the provider outcome is unknown or the request never arrived.
|
|
287
|
+
*/
|
|
288
|
+
export function isEdgeGatewayFailureResponse(input: {
|
|
289
|
+
status: number;
|
|
290
|
+
bodyText: string;
|
|
291
|
+
}): boolean {
|
|
292
|
+
return (
|
|
293
|
+
input.status >= 500 && input.status < 600 && isHtmlErrorBody(input.bodyText)
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Exponential backoff for edge gateway failures: 1 s, 2 s, 4 s, capped. */
|
|
298
|
+
function edgeGatewayRetryDelayMs(attempt: number): number {
|
|
299
|
+
return Math.min(
|
|
300
|
+
TOOL_EXECUTE_RETRY_DELAY_MAX_MS,
|
|
301
|
+
TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS * 2 ** Math.max(0, attempt - 1),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
267
305
|
function idempotencyInProgressRetryDelayMs(attempt: number): number {
|
|
268
306
|
return (
|
|
269
307
|
TOOL_EXECUTE_IDEMPOTENCY_IN_PROGRESS_RETRY_DELAYS_MS[
|
|
@@ -303,6 +341,8 @@ function decideToolExecuteHttpRetry(input: {
|
|
|
303
341
|
hardBillingFailure?: boolean;
|
|
304
342
|
hasRetryAfterHeader?: boolean;
|
|
305
343
|
transientHttpRetrySafe?: boolean;
|
|
344
|
+
edgeGatewayFailure?: boolean;
|
|
345
|
+
executionFenced?: boolean;
|
|
306
346
|
explicitRetryable?: boolean;
|
|
307
347
|
}): ToolExecuteHttpRetryDecision {
|
|
308
348
|
if (input.hardBillingFailure) {
|
|
@@ -380,6 +420,23 @@ function decideToolExecuteHttpRetry(input: {
|
|
|
380
420
|
reason: 'non_retryable_status',
|
|
381
421
|
};
|
|
382
422
|
}
|
|
423
|
+
// An edge failure in front of the gateway is transient. Repeating it is
|
|
424
|
+
// safe when the edge never reached the gateway, when the call runs under
|
|
425
|
+
// the gateway's durable execution fence (a replay returns the recorded
|
|
426
|
+
// outcome and never re-dispatches), or when the operation is declared
|
|
427
|
+
// transient-HTTP safe. Otherwise it stays a row failure, like any 5xx.
|
|
428
|
+
if (
|
|
429
|
+
input.edgeGatewayFailure &&
|
|
430
|
+
(EDGE_ORIGIN_UNREACHED_STATUSES.has(input.status) ||
|
|
431
|
+
input.executionFenced === true ||
|
|
432
|
+
input.transientHttpRetrySafe === true)
|
|
433
|
+
) {
|
|
434
|
+
return {
|
|
435
|
+
retryable: true,
|
|
436
|
+
attemptCap: TOOL_EXECUTE_TRANSIENT_HTTP_MAX_ATTEMPTS,
|
|
437
|
+
reason: 'edge_gateway_transient',
|
|
438
|
+
};
|
|
439
|
+
}
|
|
383
440
|
if (!input.transientHttpRetrySafe) {
|
|
384
441
|
return {
|
|
385
442
|
retryable: false,
|
|
@@ -413,6 +470,7 @@ export function createToolExecuteHttpFailureAttemptTracker(): ToolExecuteHttpFai
|
|
|
413
470
|
gateway_invocation_in_progress: 0,
|
|
414
471
|
customer_db_storage_unavailable: 0,
|
|
415
472
|
retry_safe_transient_5xx: 0,
|
|
473
|
+
edge_gateway_transient: 0,
|
|
416
474
|
unsafe_transient_5xx: 0,
|
|
417
475
|
hard_billing_error: 0,
|
|
418
476
|
non_retryable_status: 0,
|
|
@@ -449,6 +507,11 @@ export function createToolExecuteHttpFailureAttemptTracker(): ToolExecuteHttpFai
|
|
|
449
507
|
}),
|
|
450
508
|
hasRetryAfterHeader: true,
|
|
451
509
|
transientHttpRetrySafe: input.transientHttpRetrySafe === true,
|
|
510
|
+
edgeGatewayFailure: isEdgeGatewayFailureResponse({
|
|
511
|
+
status: input.status,
|
|
512
|
+
bodyText: input.bodyText ?? '',
|
|
513
|
+
}),
|
|
514
|
+
executionFenced: input.executionFenced === true,
|
|
452
515
|
explicitRetryable: readExplicitToolHttpRetryability({
|
|
453
516
|
toolId: input.toolId,
|
|
454
517
|
bodyText: input.bodyText ?? '',
|
|
@@ -493,6 +556,8 @@ export function classifyToolExecuteHttpFailure(input: {
|
|
|
493
556
|
schemaVersion?: ToolExecutionErrorSchemaVersion;
|
|
494
557
|
retryAfterHeader?: string | null;
|
|
495
558
|
transientHttpRetrySafe?: boolean;
|
|
559
|
+
/** The call runs under the gateway's durable execution fence. */
|
|
560
|
+
executionFenced?: boolean;
|
|
496
561
|
providerLatencyMs?: number;
|
|
497
562
|
nowMs?: number;
|
|
498
563
|
}): ToolExecuteHttpFailureOutcome {
|
|
@@ -508,6 +573,8 @@ export function classifyToolExecuteHttpFailure(input: {
|
|
|
508
573
|
const concurrencyBackpressure = isConcurrencyBackpressureResponse(input);
|
|
509
574
|
const runtimeSchedulerCapacity = isRuntimeSchedulerCapacityResponse(input);
|
|
510
575
|
const providerPacingWait = isProviderPacingWaitResponse(input);
|
|
576
|
+
const edgeGatewayFailure = isEdgeGatewayFailureResponse(input);
|
|
577
|
+
const executionFenced = input.executionFenced === true;
|
|
511
578
|
const explicitRetryable = readExplicitToolHttpRetryability(input);
|
|
512
579
|
const initialRetryDecision = decideToolExecuteHttpRetry({
|
|
513
580
|
status: input.status,
|
|
@@ -519,6 +586,8 @@ export function classifyToolExecuteHttpFailure(input: {
|
|
|
519
586
|
providerPacingWait,
|
|
520
587
|
hasRetryAfterHeader,
|
|
521
588
|
transientHttpRetrySafe,
|
|
589
|
+
edgeGatewayFailure,
|
|
590
|
+
executionFenced,
|
|
522
591
|
explicitRetryable,
|
|
523
592
|
});
|
|
524
593
|
const providerPacingWaitBody = providerPacingWait
|
|
@@ -583,6 +652,8 @@ export function classifyToolExecuteHttpFailure(input: {
|
|
|
583
652
|
hardBillingFailure,
|
|
584
653
|
hasRetryAfterHeader,
|
|
585
654
|
transientHttpRetrySafe,
|
|
655
|
+
edgeGatewayFailure,
|
|
656
|
+
executionFenced,
|
|
586
657
|
explicitRetryable,
|
|
587
658
|
});
|
|
588
659
|
const shouldRetry =
|
|
@@ -636,15 +707,17 @@ export function classifyToolExecuteHttpFailure(input: {
|
|
|
636
707
|
TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS * input.attempt,
|
|
637
708
|
),
|
|
638
709
|
)
|
|
639
|
-
: retryDecision.reason === '
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
: retryDecision.reason === 'gateway_invocation_in_progress' &&
|
|
710
|
+
: retryDecision.reason === 'edge_gateway_transient'
|
|
711
|
+
? edgeGatewayRetryDelayMs(input.attempt)
|
|
712
|
+
: retryDecision.reason === 'idempotency_in_progress' &&
|
|
643
713
|
!hasRetryAfterHeader
|
|
644
714
|
? idempotencyInProgressRetryDelayMs(input.attempt)
|
|
645
|
-
:
|
|
646
|
-
|
|
647
|
-
|
|
715
|
+
: retryDecision.reason === 'gateway_invocation_in_progress' &&
|
|
716
|
+
!hasRetryAfterHeader
|
|
717
|
+
? idempotencyInProgressRetryDelayMs(input.attempt)
|
|
718
|
+
: retryAfterMs > 0
|
|
719
|
+
? Math.min(TOOL_EXECUTE_RETRY_DELAY_MAX_MS, retryAfterMs)
|
|
720
|
+
: TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS;
|
|
648
721
|
return {
|
|
649
722
|
...retryDecision,
|
|
650
723
|
error,
|
|
@@ -322,12 +322,47 @@ function formatInsufficientCreditsMessage(input: {
|
|
|
322
322
|
return `Workspace balance ${balance} < required ${required} for ${operation}.${addSuffix}`;
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
/** An HTML document, as an edge proxy (Cloudflare, Vercel) serves for a 5xx. */
|
|
326
|
+
export function isHtmlErrorBody(bodyText: string): boolean {
|
|
327
|
+
return /^\s*(?:<!doctype\s+html|<html[\s>]|<head[\s>]|<body[\s>])/i.test(
|
|
328
|
+
bodyText,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** The Cloudflare Ray ID an edge error page carries, when it has one. */
|
|
333
|
+
export function edgeErrorPageRayId(bodyText: string): string | null {
|
|
334
|
+
const match = /Ray ID:?\s*(?:<[^>]*>\s*)*([0-9a-f]{8,32})/i.exec(bodyText);
|
|
335
|
+
return match?.[1] ?? null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* One line for an edge HTML error page: its title and Ray ID, never the
|
|
340
|
+
* document. A raw page is kilobytes of markup that floods CLI output and logs.
|
|
341
|
+
*/
|
|
342
|
+
export function summarizeHtmlErrorBody(bodyText: string): string {
|
|
343
|
+
const title = /<title[^>]*>([\s\S]{0,500}?)<\/title>/i
|
|
344
|
+
.exec(bodyText)?.[1]
|
|
345
|
+
?.replace(/<[^>]*>/g, ' ')
|
|
346
|
+
.replace(/\s+/g, ' ')
|
|
347
|
+
.trim();
|
|
348
|
+
const rayId = edgeErrorPageRayId(bodyText);
|
|
349
|
+
return [
|
|
350
|
+
`gateway HTML error page${title ? `: ${title.slice(0, 160)}` : ''}`,
|
|
351
|
+
rayId ? `(ray ${rayId})` : null,
|
|
352
|
+
]
|
|
353
|
+
.filter(Boolean)
|
|
354
|
+
.join(' ');
|
|
355
|
+
}
|
|
356
|
+
|
|
325
357
|
function formatPublicToolErrorPayload(input: {
|
|
326
358
|
parsed: Record<string, unknown> | null;
|
|
327
359
|
bodyText: string;
|
|
328
360
|
}): string {
|
|
329
361
|
if (!input.parsed) {
|
|
330
|
-
|
|
362
|
+
if (isHtmlErrorBody(input.bodyText)) {
|
|
363
|
+
return summarizeHtmlErrorBody(input.bodyText);
|
|
364
|
+
}
|
|
365
|
+
return input.bodyText.replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
331
366
|
}
|
|
332
367
|
|
|
333
368
|
const selected: Record<string, unknown> = {};
|
|
@@ -461,7 +496,7 @@ export function normalizeToolHttpErrorMessage(input: {
|
|
|
461
496
|
getStringField(parsed, 'request_id') ??
|
|
462
497
|
getStringField(parsed, 'requestId') ??
|
|
463
498
|
hydratedFailure?.requestId ??
|
|
464
|
-
null,
|
|
499
|
+
(parsed ? null : edgeErrorPageRayId(input.bodyText)),
|
|
465
500
|
retryAfterMs: hydratedFailure?.retryAfterMs ?? input.retryAfterMs ?? null,
|
|
466
501
|
networkKind:
|
|
467
502
|
hydratedFailure?.networkKind ??
|