deepline 0.1.227 → 0.1.229

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 (46) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +76 -1842
  2. package/dist/bundling-sources/apps/play-runner-workers/src/dedup-do.ts +0 -113
  3. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +144 -1000
  4. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/ledger-event-batches.ts +81 -0
  5. package/dist/bundling-sources/apps/play-runner-workers/src/workflow-retry-state.ts +1 -50
  6. package/dist/bundling-sources/sdk/src/client.ts +24 -2
  7. package/dist/bundling-sources/sdk/src/play.ts +2 -2
  8. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  9. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +382 -31
  10. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +84 -79
  11. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +201 -705
  12. package/dist/bundling-sources/shared_libs/play-runtime/coordinator-headers.ts +3 -10
  13. package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +2 -2
  14. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +12 -47
  15. package/dist/bundling-sources/shared_libs/play-runtime/governor/block-reserving-budget-state-backend.ts +0 -2
  16. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +12 -200
  17. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +2 -17
  18. package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +3 -9
  19. package/dist/bundling-sources/shared_libs/play-runtime/play-call-execution.ts +6 -1
  20. package/dist/bundling-sources/shared_libs/play-runtime/play-latency-trace.ts +28 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/run-execution-scope.ts +16 -64
  22. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +26 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/run-lifecycle-policy.ts +5 -2
  24. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-paths.ts +0 -4
  25. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +81 -18
  26. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +0 -36
  27. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +3 -21
  28. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +9 -1
  29. package/dist/bundling-sources/shared_libs/play-runtime/transport-error-diagnostics.ts +63 -0
  30. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +2 -0
  31. package/dist/cli/index.js +78 -11
  32. package/dist/cli/index.mjs +78 -11
  33. package/dist/{compiler-manifest-BhgZ23A4.d.ts → compiler-manifest-CYcwzSOJ.d.mts} +2 -0
  34. package/dist/{compiler-manifest-BhgZ23A4.d.mts → compiler-manifest-CYcwzSOJ.d.ts} +2 -0
  35. package/dist/index.d.mts +3 -3
  36. package/dist/index.d.ts +3 -3
  37. package/dist/index.js +14 -5
  38. package/dist/index.mjs +14 -5
  39. package/dist/plays/bundle-play-file.d.mts +2 -2
  40. package/dist/plays/bundle-play-file.d.ts +2 -2
  41. package/package.json +1 -1
  42. package/dist/bundling-sources/apps/play-runner-workers/src/child-manifest-resolver.ts +0 -108
  43. package/dist/bundling-sources/apps/play-runner-workers/src/child-play-await.ts +0 -374
  44. package/dist/bundling-sources/apps/play-runner-workers/src/child-play-submit.ts +0 -203
  45. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-placement.ts +0 -14
  46. package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +0 -121
@@ -129,13 +129,10 @@ type RuntimeApiRequest =
129
129
  idempotencyKey?: string;
130
130
  }
131
131
  | {
132
- action: 'start_inline_child_run';
132
+ action: 'start_run';
133
133
  idempotencyKey?: string;
134
134
  playName: string;
135
135
  runId: string;
136
- parentRunId?: string | null;
137
- rootRunId?: string | null;
138
- workflowFamilyKey?: string | null;
139
136
  artifactStorageKey?: string | null;
140
137
  artifactHash?: string | null;
141
138
  graphHash?: string | null;
@@ -380,6 +377,8 @@ function applyRetryJitter(delayMs: number): number {
380
377
  return Math.round(half + Math.random() * half);
381
378
  }
382
379
  const APP_RUNTIME_API_DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
380
+ const APP_RUNTIME_RECEIPT_RETRY_TELEMETRY_TAG =
381
+ '[perf][worker.receipt_api.transport]';
383
382
  const RUN_STATUS_LEDGER_SNAPSHOT_CACHE_LIMIT = 1_000;
384
383
  const runStatusLedgerSnapshots = new Map<string, PlayRunLedgerSnapshot>();
385
384
  // Positional log cursor per run attempt: count of liveLogs lines already
@@ -454,7 +453,7 @@ async function appendRunEventBatchesViaAppRuntime(
454
453
  input: { playId: string; events: readonly PlayRunLedgerEvent[] },
455
454
  ): Promise<void> {
456
455
  for (const events of partitionRunEventsForAppRuntime(input.events)) {
457
- await appendRunEventsViaAppRuntime(context, {
456
+ await appendRunEventsViaAppRuntimeRaw(context, {
458
457
  playId: input.playId,
459
458
  events,
460
459
  });
@@ -504,6 +503,20 @@ function isRetryableAppRuntimeResponse(input: {
504
503
  if (!isRetryableAppRuntimeAction(input.action)) {
505
504
  return false;
506
505
  }
506
+ // The app runtime may explicitly classify an otherwise-client-error status
507
+ // as transient. Keep the structured delivery contract authoritative rather
508
+ // than reducing every 4xx to permanent at the worker boundary.
509
+ try {
510
+ const parsed = JSON.parse(input.body) as unknown;
511
+ if (
512
+ parsed &&
513
+ typeof parsed === 'object' &&
514
+ !Array.isArray(parsed) &&
515
+ (parsed as { retryable?: unknown }).retryable === true
516
+ ) {
517
+ return true;
518
+ }
519
+ } catch {}
507
520
  if (
508
521
  input.action === 'append_run_events' &&
509
522
  input.status === 500 &&
@@ -583,7 +596,7 @@ function appRuntimeErrorCode(response: Response, body: string): string | null {
583
596
  }
584
597
 
585
598
  export function isTransientAppRuntimeFailureBody(body: string): boolean {
586
- return /WorkerOverloaded|"\s*code\s*"\s*:\s*"\s*InternalServerError\s*"|Your request couldn't be completed\. Try again later\.|timeout exceeded when trying to connect|timed out|fetch failed|ECONNRESET|ECONNREFUSED|UND_ERR_CONNECT_TIMEOUT|tuple concurrently updated|RUNTIME_SCHEDULER_SATURATED|Runtime scheduler DB pool is saturated|Runtime scheduler DB circuit breaker is open|OptimisticConcurrencyControlFailure|changed while this mutation was being run|Documents read from or written to the .* table changed while this mutation/i.test(
599
+ return /WorkerOverloaded|"\s*code\s*"\s*:\s*"\s*InternalServerError\s*"|Your request couldn't be completed\. Try again later\.|timeout exceeded when trying to connect|timed out|fetch failed|ECONNRESET|ECONNREFUSED|UND_ERR_CONNECT_TIMEOUT|tuple concurrently updated|RUNTIME_SCHEDULER_(?:SATURATED|UNAVAILABLE)|Runtime scheduler DB pool is saturated|Runtime scheduler DB circuit breaker is open|OptimisticConcurrencyControlFailure|changed while this mutation was being run|Documents read from or written to the .* table changed while this mutation/i.test(
587
600
  body,
588
601
  );
589
602
  }
@@ -657,7 +670,7 @@ function isRetryableAppRuntimeAction(
657
670
  action === 'release_runtime_step_receipt' ||
658
671
  action === 'save_results' ||
659
672
  action === 'skip_runtime_step_receipt' ||
660
- action === 'start_inline_child_run'
673
+ action === 'start_run'
661
674
  );
662
675
  }
663
676
 
@@ -733,14 +746,146 @@ function sleep(ms: number): Promise<void> {
733
746
  return new Promise((resolve) => setTimeout(resolve, ms));
734
747
  }
735
748
 
749
+ type AppRuntimeRetryFailureKind =
750
+ | 'connection_refused'
751
+ | 'connection_reset'
752
+ | 'connection_timeout'
753
+ | 'dns'
754
+ | 'fetch_failed'
755
+ | 'http_error'
756
+ | 'request_timeout'
757
+ | 'response_body_error'
758
+ | 'response_body_timeout'
759
+ | 'retryable_http'
760
+ | 'transport_error';
761
+
762
+ type AppRuntimeRetryFailure = {
763
+ attempt: number;
764
+ failureKind: AppRuntimeRetryFailureKind;
765
+ attemptElapsedMs: number;
766
+ retryDelayMs: number;
767
+ httpStatus?: number;
768
+ };
769
+
770
+ type AppRuntimeRetryTelemetry = {
771
+ enabled: boolean;
772
+ startedAt: number;
773
+ retryCount: number;
774
+ retrySleepMs: number;
775
+ failures: AppRuntimeRetryFailure[];
776
+ };
777
+
778
+ function appRuntimeTransportFailureKind(
779
+ error: unknown,
780
+ ): AppRuntimeRetryFailureKind {
781
+ if (isRequestTimeoutAbort(error)) return 'request_timeout';
782
+ const message = error instanceof Error ? error.message : String(error ?? '');
783
+ if (/\b(?:ENOTFOUND|EAI_AGAIN)\b|getaddrinfo|\bdns\b/i.test(message)) {
784
+ return 'dns';
785
+ }
786
+ if (/\bECONNRESET\b|connection reset/i.test(message)) {
787
+ return 'connection_reset';
788
+ }
789
+ if (/\bECONNREFUSED\b|connection refused/i.test(message)) {
790
+ return 'connection_refused';
791
+ }
792
+ if (
793
+ /\bETIMEDOUT\b|UND_ERR_CONNECT_TIMEOUT|connection (?:timeout|timed out)/i.test(
794
+ message,
795
+ )
796
+ ) {
797
+ return 'connection_timeout';
798
+ }
799
+ return /fetch failed/i.test(message) ? 'fetch_failed' : 'transport_error';
800
+ }
801
+
802
+ function recordAppRuntimeRetryFailure(input: {
803
+ telemetry: AppRuntimeRetryTelemetry;
804
+ attempt: number;
805
+ failureKind: AppRuntimeRetryFailureKind;
806
+ attemptStartedAt: number;
807
+ retryDelayMs: number;
808
+ httpStatus?: number;
809
+ }): void {
810
+ if (!input.telemetry.enabled) return;
811
+ // Receipt actions have at most eight attempts. Keep the terminal eighth
812
+ // failure instead of truncating the causally important final observation.
813
+ if (input.telemetry.failures.length < 8) {
814
+ input.telemetry.failures.push({
815
+ attempt: input.attempt,
816
+ failureKind: input.failureKind,
817
+ attemptElapsedMs: Date.now() - input.attemptStartedAt,
818
+ retryDelayMs: input.retryDelayMs,
819
+ ...(input.httpStatus === undefined
820
+ ? {}
821
+ : { httpStatus: input.httpStatus }),
822
+ });
823
+ }
824
+ if (input.retryDelayMs > 0) {
825
+ input.telemetry.retryCount += 1;
826
+ input.telemetry.retrySleepMs += input.retryDelayMs;
827
+ }
828
+ }
829
+
830
+ function emitAppRuntimeRetrySummary(input: {
831
+ telemetry: AppRuntimeRetryTelemetry;
832
+ action: RuntimeApiRequest['action'];
833
+ outcome: 'failed_after_retry' | 'recovered_after_retry';
834
+ attempts: number;
835
+ maxAttempts: number;
836
+ finalAttemptStartedAt: number;
837
+ }): void {
838
+ if (!input.telemetry.enabled || input.telemetry.retryCount === 0) return;
839
+ const payload = {
840
+ action: input.action,
841
+ outcome: input.outcome,
842
+ attempts: input.attempts,
843
+ maxAttempts: input.maxAttempts,
844
+ totalElapsedMs: Date.now() - input.telemetry.startedAt,
845
+ retrySleepMs: input.telemetry.retrySleepMs,
846
+ finalAttemptElapsedMs: Date.now() - input.finalAttemptStartedAt,
847
+ failures: input.telemetry.failures,
848
+ };
849
+ // Observability must never alter receipt execution. The payload contains
850
+ // primitives from a fixed taxonomy only: no URL, token, body, raw error,
851
+ // run/play/org id, receipt key, input, or output.
852
+ try {
853
+ const serialized = JSON.stringify(payload);
854
+ if (input.outcome === 'failed_after_retry') {
855
+ console.warn(APP_RUNTIME_RECEIPT_RETRY_TELEMETRY_TAG, serialized);
856
+ } else {
857
+ console.info(APP_RUNTIME_RECEIPT_RETRY_TELEMETRY_TAG, serialized);
858
+ }
859
+ } catch {}
860
+ }
861
+
736
862
  async function retryAppRuntimeBodyTimeoutOrThrow(input: {
737
863
  action: RuntimeApiRequest['action'];
738
864
  attempt: number;
739
865
  maxAttempts: number;
740
866
  error: unknown;
867
+ telemetry: AppRuntimeRetryTelemetry;
868
+ attemptStartedAt: number;
869
+ httpStatus?: number;
741
870
  retryAfterMs?: number | null;
742
871
  }): Promise<void> {
743
872
  if (!isRequestTimeoutAbort(input.error)) {
873
+ recordAppRuntimeRetryFailure({
874
+ telemetry: input.telemetry,
875
+ attempt: input.attempt,
876
+ failureKind: 'response_body_error',
877
+ attemptStartedAt: input.attemptStartedAt,
878
+ retryDelayMs: 0,
879
+ httpStatus: input.httpStatus,
880
+ });
881
+ emitAppRuntimeRetrySummary({
882
+ telemetry: input.telemetry,
883
+ action: input.action,
884
+ outcome: 'failed_after_retry',
885
+ attempts: input.attempt,
886
+ maxAttempts: input.maxAttempts,
887
+ finalAttemptStartedAt: input.attemptStartedAt,
888
+ });
744
889
  throw input.error;
745
890
  }
746
891
  if (
@@ -750,14 +895,37 @@ async function retryAppRuntimeBodyTimeoutOrThrow(input: {
750
895
  error: input.error,
751
896
  })
752
897
  ) {
753
- await sleep(
754
- Math.max(
755
- applyRetryJitter(appRuntimeRetryDelayMs(input.action, input.attempt)),
756
- input.retryAfterMs ?? 0,
757
- ),
898
+ const retryDelayMs = Math.max(
899
+ applyRetryJitter(appRuntimeRetryDelayMs(input.action, input.attempt)),
900
+ input.retryAfterMs ?? 0,
758
901
  );
902
+ recordAppRuntimeRetryFailure({
903
+ telemetry: input.telemetry,
904
+ attempt: input.attempt,
905
+ failureKind: 'response_body_timeout',
906
+ attemptStartedAt: input.attemptStartedAt,
907
+ retryDelayMs,
908
+ httpStatus: input.httpStatus,
909
+ });
910
+ await sleep(retryDelayMs);
759
911
  return;
760
912
  }
913
+ recordAppRuntimeRetryFailure({
914
+ telemetry: input.telemetry,
915
+ attempt: input.attempt,
916
+ failureKind: 'response_body_timeout',
917
+ attemptStartedAt: input.attemptStartedAt,
918
+ retryDelayMs: 0,
919
+ httpStatus: input.httpStatus,
920
+ });
921
+ emitAppRuntimeRetrySummary({
922
+ telemetry: input.telemetry,
923
+ action: input.action,
924
+ outcome: 'failed_after_retry',
925
+ attempts: input.attempt,
926
+ maxAttempts: input.maxAttempts,
927
+ finalAttemptStartedAt: input.attemptStartedAt,
928
+ });
761
929
  throw new AppRuntimeApiTransportError({
762
930
  action: input.action,
763
931
  attempts: input.attempt,
@@ -786,6 +954,40 @@ export class AppRuntimeApiTransportError extends Error {
786
954
  }
787
955
  }
788
956
 
957
+ /**
958
+ * A non-2xx response from the app runtime boundary.
959
+ *
960
+ * Keep the response contract structured here so durable callers can decide
961
+ * whether to retry or block a delivery without parsing an error message.
962
+ */
963
+ export class AppRuntimeApiResponseError extends Error {
964
+ readonly status: number;
965
+ readonly code: string | null;
966
+ readonly requestId: string | null;
967
+ readonly retryable: boolean;
968
+
969
+ constructor(input: {
970
+ action: RuntimeApiRequest['action'];
971
+ status: number;
972
+ code?: string | null;
973
+ requestId?: string | null;
974
+ retryable: boolean;
975
+ detail: string;
976
+ }) {
977
+ super(
978
+ `App runtime API ${input.action} failed with status ${input.status}` +
979
+ `${input.code ? ` code=${input.code}` : ''}` +
980
+ `${input.requestId ? ` request_id=${input.requestId}` : ''}: ` +
981
+ input.detail,
982
+ );
983
+ this.name = 'AppRuntimeApiResponseError';
984
+ this.status = input.status;
985
+ this.code = input.code?.trim() || null;
986
+ this.requestId = input.requestId?.trim() || null;
987
+ this.retryable = input.retryable;
988
+ }
989
+ }
990
+
789
991
  function resolveAppRuntimeApiUrl(context: WorkerRuntimeApiContext): string {
790
992
  const baseUrl = context.baseUrl.trim().replace(/\/$/, '');
791
993
  return `${baseUrl}${PLAY_RUNTIME_API_COMPAT_PATH}`;
@@ -811,6 +1013,13 @@ async function postAppRuntimeApi<TResponse>(
811
1013
  });
812
1014
 
813
1015
  const maxAttempts = appRuntimeMaxAttempts(body.action);
1016
+ const retryTelemetry: AppRuntimeRetryTelemetry = {
1017
+ enabled: isRuntimeStepReceiptAction(body.action),
1018
+ startedAt: Date.now(),
1019
+ retryCount: 0,
1020
+ retrySleepMs: 0,
1021
+ failures: [],
1022
+ };
814
1023
  const requestTimeoutMs = Math.max(
815
1024
  1,
816
1025
  Math.floor(
@@ -819,6 +1028,7 @@ async function postAppRuntimeApi<TResponse>(
819
1028
  );
820
1029
 
821
1030
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1031
+ const attemptStartedAt = Date.now();
822
1032
  let response: Response;
823
1033
  try {
824
1034
  response = await runtimeFetch(resolveAppRuntimeApiUrl(context), {
@@ -843,11 +1053,34 @@ async function postAppRuntimeApi<TResponse>(
843
1053
  attempt < maxAttempts &&
844
1054
  isRetryableAppRuntimeFetchError({ action: body.action, error })
845
1055
  ) {
846
- await sleep(
847
- applyRetryJitter(appRuntimeRetryDelayMs(body.action, attempt)),
1056
+ const retryDelayMs = applyRetryJitter(
1057
+ appRuntimeRetryDelayMs(body.action, attempt),
848
1058
  );
1059
+ recordAppRuntimeRetryFailure({
1060
+ telemetry: retryTelemetry,
1061
+ attempt,
1062
+ failureKind: appRuntimeTransportFailureKind(error),
1063
+ attemptStartedAt,
1064
+ retryDelayMs,
1065
+ });
1066
+ await sleep(retryDelayMs);
849
1067
  continue;
850
1068
  }
1069
+ recordAppRuntimeRetryFailure({
1070
+ telemetry: retryTelemetry,
1071
+ attempt,
1072
+ failureKind: appRuntimeTransportFailureKind(error),
1073
+ attemptStartedAt,
1074
+ retryDelayMs: 0,
1075
+ });
1076
+ emitAppRuntimeRetrySummary({
1077
+ telemetry: retryTelemetry,
1078
+ action: body.action,
1079
+ outcome: 'failed_after_retry',
1080
+ attempts: attempt,
1081
+ maxAttempts,
1082
+ finalAttemptStartedAt: attemptStartedAt,
1083
+ });
851
1084
  throw new AppRuntimeApiTransportError({
852
1085
  action: body.action,
853
1086
  attempts: attempt,
@@ -856,13 +1089,25 @@ async function postAppRuntimeApi<TResponse>(
856
1089
  }
857
1090
  if (response.ok) {
858
1091
  try {
859
- return (await response.json()) as TResponse;
1092
+ const parsed = (await response.json()) as TResponse;
1093
+ emitAppRuntimeRetrySummary({
1094
+ telemetry: retryTelemetry,
1095
+ action: body.action,
1096
+ outcome: 'recovered_after_retry',
1097
+ attempts: attempt,
1098
+ maxAttempts,
1099
+ finalAttemptStartedAt: attemptStartedAt,
1100
+ });
1101
+ return parsed;
860
1102
  } catch (error) {
861
1103
  await retryAppRuntimeBodyTimeoutOrThrow({
862
1104
  action: body.action,
863
1105
  attempt,
864
1106
  maxAttempts,
865
1107
  error,
1108
+ telemetry: retryTelemetry,
1109
+ attemptStartedAt,
1110
+ httpStatus: response.status,
866
1111
  });
867
1112
  continue;
868
1113
  }
@@ -875,6 +1120,22 @@ async function postAppRuntimeApi<TResponse>(
875
1120
  isRequestTimeoutAbort(error) &&
876
1121
  !isRetryableAppRuntimeResponseStatus(response.status)
877
1122
  ) {
1123
+ recordAppRuntimeRetryFailure({
1124
+ telemetry: retryTelemetry,
1125
+ attempt,
1126
+ failureKind: 'response_body_timeout',
1127
+ attemptStartedAt,
1128
+ retryDelayMs: 0,
1129
+ httpStatus: response.status,
1130
+ });
1131
+ emitAppRuntimeRetrySummary({
1132
+ telemetry: retryTelemetry,
1133
+ action: body.action,
1134
+ outcome: 'failed_after_retry',
1135
+ attempts: attempt,
1136
+ maxAttempts,
1137
+ finalAttemptStartedAt: attemptStartedAt,
1138
+ });
878
1139
  throw new Error(
879
1140
  `App runtime API ${body.action} failed with status ${response.status}: response body timed out`,
880
1141
  { cause: error },
@@ -885,6 +1146,9 @@ async function postAppRuntimeApi<TResponse>(
885
1146
  attempt,
886
1147
  maxAttempts,
887
1148
  error,
1149
+ telemetry: retryTelemetry,
1150
+ attemptStartedAt,
1151
+ httpStatus: response.status,
888
1152
  retryAfterMs: appRuntimeRetryAfterMs(response, ''),
889
1153
  });
890
1154
  continue;
@@ -897,22 +1161,61 @@ async function postAppRuntimeApi<TResponse>(
897
1161
  body: responseText,
898
1162
  })
899
1163
  ) {
900
- await sleep(
901
- Math.max(
902
- applyRetryJitter(appRuntimeRetryDelayMs(body.action, attempt)),
903
- appRuntimeRetryAfterMs(response, responseText) ?? 0,
904
- ),
1164
+ const retryDelayMs = Math.max(
1165
+ applyRetryJitter(appRuntimeRetryDelayMs(body.action, attempt)),
1166
+ appRuntimeRetryAfterMs(response, responseText) ?? 0,
905
1167
  );
1168
+ recordAppRuntimeRetryFailure({
1169
+ telemetry: retryTelemetry,
1170
+ attempt,
1171
+ failureKind: 'retryable_http',
1172
+ attemptStartedAt,
1173
+ retryDelayMs,
1174
+ httpStatus: response.status,
1175
+ });
1176
+ await sleep(retryDelayMs);
906
1177
  continue;
907
1178
  }
1179
+ recordAppRuntimeRetryFailure({
1180
+ telemetry: retryTelemetry,
1181
+ attempt,
1182
+ failureKind: isRetryableAppRuntimeResponse({
1183
+ action: body.action,
1184
+ status: response.status,
1185
+ body: responseText,
1186
+ })
1187
+ ? 'retryable_http'
1188
+ : 'http_error',
1189
+ attemptStartedAt,
1190
+ retryDelayMs: 0,
1191
+ httpStatus: response.status,
1192
+ });
1193
+ emitAppRuntimeRetrySummary({
1194
+ telemetry: retryTelemetry,
1195
+ action: body.action,
1196
+ outcome: 'failed_after_retry',
1197
+ attempts: attempt,
1198
+ maxAttempts,
1199
+ finalAttemptStartedAt: attemptStartedAt,
1200
+ });
908
1201
  const code = appRuntimeErrorCode(response, responseText);
909
1202
  const requestId = response.headers.get('x-deepline-request-id')?.trim();
910
- throw new Error(
911
- `App runtime API ${body.action} failed with status ${response.status}` +
912
- `${code ? ` code=${code}` : ''}` +
913
- `${requestId ? ` request_id=${requestId}` : ''}: ` +
914
- summarizeAppRuntimeErrorBody(responseText),
915
- );
1203
+ throw new AppRuntimeApiResponseError({
1204
+ action: body.action,
1205
+ status: response.status,
1206
+ code,
1207
+ requestId,
1208
+ // Immediate request retries are intentionally selective, but durable
1209
+ // delivery must never classify an unrecognized upstream 5xx as poison.
1210
+ retryable:
1211
+ response.status >= 500 ||
1212
+ isRetryableAppRuntimeResponse({
1213
+ action: body.action,
1214
+ status: response.status,
1215
+ body: responseText,
1216
+ }),
1217
+ detail: summarizeAppRuntimeErrorBody(responseText),
1218
+ });
916
1219
  }
917
1220
 
918
1221
  throw new Error(`App runtime API ${body.action} failed after retries.`);
@@ -1239,7 +1542,7 @@ async function updateRunStatusViaAppRuntimeUnlocked(
1239
1542
  );
1240
1543
  }
1241
1544
 
1242
- export async function appendRunEventsViaAppRuntime(
1545
+ async function appendRunEventsViaAppRuntimeRaw(
1243
1546
  context: WorkerRuntimeApiContext,
1244
1547
  input: {
1245
1548
  playId: string;
@@ -1253,14 +1556,62 @@ export async function appendRunEventsViaAppRuntime(
1253
1556
  });
1254
1557
  }
1255
1558
 
1559
+ /**
1560
+ * Append Run Ledger events through the bounded transport used by every runtime
1561
+ * producer. Scheduler terminal outbox events can carry a complete log replay,
1562
+ * so this boundary must never forward an arbitrarily large event array or log
1563
+ * event directly to Convex.
1564
+ */
1565
+ export async function appendRunEventsViaAppRuntime(
1566
+ context: WorkerRuntimeApiContext,
1567
+ input: {
1568
+ playId: string;
1569
+ events: PlayRunLedgerEvent[];
1570
+ idempotencyKey?: string;
1571
+ },
1572
+ ): Promise<void> {
1573
+ const batches = partitionRunEventsForAppRuntime(input.events);
1574
+ if (batches.length <= 1) {
1575
+ await appendRunEventsViaAppRuntimeRaw(context, input);
1576
+ return;
1577
+ }
1578
+
1579
+ // Terminal idempotency keys record a completed delivery. Sending the same
1580
+ // key on an earlier log-only batch would make Convex drop the terminal batch,
1581
+ // so attach it to the batch that contains the final terminal event only.
1582
+ let terminalBatchIndex = -1;
1583
+ for (let index = batches.length - 1; index >= 0; index -= 1) {
1584
+ if (
1585
+ batches[index]!.some(
1586
+ (event) =>
1587
+ event.type === 'run.completed' ||
1588
+ event.type === 'run.failed' ||
1589
+ event.type === 'run.cancelled',
1590
+ )
1591
+ ) {
1592
+ terminalBatchIndex = index;
1593
+ break;
1594
+ }
1595
+ }
1596
+ const idempotencyBatchIndex =
1597
+ terminalBatchIndex >= 0 ? terminalBatchIndex : batches.length - 1;
1598
+
1599
+ for (let index = 0; index < batches.length; index += 1) {
1600
+ await appendRunEventsViaAppRuntimeRaw(context, {
1601
+ playId: input.playId,
1602
+ events: batches[index]!,
1603
+ ...(input.idempotencyKey && index === idempotencyBatchIndex
1604
+ ? { idempotencyKey: input.idempotencyKey }
1605
+ : {}),
1606
+ });
1607
+ }
1608
+ }
1609
+
1256
1610
  export async function startRunViaAppRuntime(
1257
1611
  context: WorkerRuntimeApiContext,
1258
1612
  input: {
1259
1613
  playName: string;
1260
1614
  runId: string;
1261
- parentRunId?: string | null;
1262
- rootRunId?: string | null;
1263
- workflowFamilyKey?: string | null;
1264
1615
  artifactStorageKey?: string | null;
1265
1616
  artifactHash?: string | null;
1266
1617
  graphHash?: string | null;
@@ -1275,7 +1626,7 @@ export async function startRunViaAppRuntime(
1275
1626
  },
1276
1627
  ): Promise<void> {
1277
1628
  await postAppRuntimeApi<{ ok: true }>(context, {
1278
- action: 'start_inline_child_run',
1629
+ action: 'start_run',
1279
1630
  ...input,
1280
1631
  });
1281
1632
  }