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.
Files changed (67) hide show
  1. package/dist/bundling-sources/sdk/src/api-error.ts +45 -0
  2. package/dist/bundling-sources/sdk/src/client.ts +98 -459
  3. package/dist/bundling-sources/sdk/src/http.ts +1 -34
  4. package/dist/bundling-sources/sdk/src/index.ts +8 -0
  5. package/dist/bundling-sources/sdk/src/plays/csv-data-input.ts +139 -0
  6. package/dist/bundling-sources/sdk/src/plays/run-request.ts +855 -0
  7. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  8. package/dist/bundling-sources/sdk/src/runs/overview.ts +45 -2
  9. package/dist/bundling-sources/sdk/src/types.ts +5 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +8 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +42 -10
  12. package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +3 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +23 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +3 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/play-admission-outcome.ts +23 -5
  16. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +1 -1
  17. package/dist/bundling-sources/shared_libs/play-runtime/run-execution-state.ts +845 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +106 -6
  19. package/dist/bundling-sources/shared_libs/play-runtime/run-observation.ts +440 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +35 -2
  21. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +66 -7
  22. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +0 -29
  23. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-transition.ts +47 -2
  24. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +6 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +475 -36
  26. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +78 -11
  27. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +550 -42
  28. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +181 -8
  29. package/dist/bundling-sources/shared_libs/play-runtime/step-lifecycle-tracker.ts +0 -18
  30. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +80 -7
  31. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +37 -2
  32. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +148 -18
  33. package/dist/bundling-sources/shared_libs/plays/cascade-falloff.ts +86 -0
  34. package/dist/bundling-sources/shared_libs/plays/docflow-unreached-reason.ts +611 -0
  35. package/dist/bundling-sources/shared_libs/plays/docflow.ts +21 -0
  36. package/dist/bundling-sources/shared_libs/plays/play-exports.ts +28 -5
  37. package/dist/bundling-sources/shared_libs/plays/settings-contract.ts +92 -10
  38. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +5 -0
  39. package/dist/bundling-sources/shared_libs/plays/trigger-config-contract.ts +222 -0
  40. package/dist/bundling-sources/shared_libs/plays/trigger-config-route.ts +15 -0
  41. package/dist/cli/index.js +4957 -771
  42. package/dist/cli/index.mjs +4957 -771
  43. package/dist/client-B5XbKrrs.d.mts +6564 -0
  44. package/dist/client-C9Y49yya.d.ts +6564 -0
  45. package/dist/{compiler-manifest-C4mdRJOP.d.mts → compiler-manifest-CUuh8C3b.d.mts} +14 -1
  46. package/dist/{compiler-manifest-C4mdRJOP.d.ts → compiler-manifest-CUuh8C3b.d.ts} +14 -1
  47. package/dist/index.d.mts +7 -6275
  48. package/dist/index.d.ts +7 -6275
  49. package/dist/index.js +360 -326
  50. package/dist/index.mjs +360 -326
  51. package/dist/install-integrity.json +21 -2
  52. package/dist/plays/bundle-play-file.d.mts +2 -2
  53. package/dist/plays/bundle-play-file.d.ts +2 -2
  54. package/dist/plays/bundle-play-file.mjs +117 -18
  55. package/dist/plays/csv-data-input.d.mts +37 -0
  56. package/dist/plays/csv-data-input.d.ts +37 -0
  57. package/dist/plays/csv-data-input.js +3135 -0
  58. package/dist/plays/csv-data-input.mjs +3108 -0
  59. package/dist/plays/run-request.d.mts +6 -0
  60. package/dist/plays/run-request.d.ts +6 -0
  61. package/dist/plays/run-request.js +664 -0
  62. package/dist/plays/run-request.mjs +614 -0
  63. package/dist/release.d.mts +1 -1
  64. package/dist/release.d.ts +1 -1
  65. package/dist/release.js +1 -1
  66. package/dist/release.mjs +1 -1
  67. 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
- const dispatchOnce = () => {
145
- const oldestReady =
146
- admissionTurns === ADMISSION_TURNS_PER_OLDEST_READY_TURN;
147
- admissionTurns = oldestReady ? 0 : admissionTurns + 1;
148
- return deps.dispatchOnce(oldestReady ? 'oldest_ready' : 'admission_first');
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
- { tokenAuthority: 'gateway', limit: 8, headPriority },
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 === 'idempotency_in_progress' &&
640
- !hasRetryAfterHeader
641
- ? idempotencyInProgressRetryDelayMs(input.attempt)
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
- : retryAfterMs > 0
646
- ? Math.min(TOOL_EXECUTE_RETRY_DELAY_MAX_MS, retryAfterMs)
647
- : TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS;
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
- return input.bodyText.slice(0, 500);
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 ??