deepline 0.1.211 → 0.1.212

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 (33) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +150 -11
  2. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/customer-console.ts +23 -0
  3. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +1 -1
  4. package/dist/bundling-sources/sdk/src/client.ts +118 -3
  5. package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
  6. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  7. package/dist/bundling-sources/sdk/src/types.ts +47 -0
  8. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +253 -30
  9. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +22 -20
  10. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +12 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/dataset-id.ts +10 -4
  12. package/dist/bundling-sources/shared_libs/play-runtime/db-session.ts +9 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +3 -5
  14. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +14 -2
  15. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +4 -2
  16. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +21 -1
  17. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +128 -6
  18. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +7 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +37 -5
  20. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +19 -10
  21. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +63 -7
  22. package/dist/bundling-sources/shared_libs/play-runtime/secret-capability.ts +18 -4
  23. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +29 -9
  24. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +22 -1
  25. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +79 -1
  26. package/dist/cli/index.js +637 -229
  27. package/dist/cli/index.mjs +637 -229
  28. package/dist/index.d.mts +76 -6
  29. package/dist/index.d.ts +76 -6
  30. package/dist/index.js +225 -43
  31. package/dist/index.mjs +225 -43
  32. package/dist/plays/bundle-play-file.mjs +65 -4
  33. package/package.json +1 -1
@@ -66,6 +66,22 @@ export type RuntimeStatusUpdate = {
66
66
  * positional cursor stay correct once the buffer has rotated.
67
67
  */
68
68
  liveLogTotalCount?: number;
69
+ /**
70
+ * Monotonic scheduler attempt that owns the positional log channel. A new
71
+ * attempt starts at offset zero without colliding with earlier attempts.
72
+ */
73
+ liveLogProducerAttempt?: number;
74
+ datasetLifecycleEvents?: Array<{
75
+ datasetId: string;
76
+ path: string;
77
+ tableNamespace: string;
78
+ phase: 'registered' | 'available' | 'failed';
79
+ persistedRows?: number;
80
+ succeededRows?: number;
81
+ failedRows?: number;
82
+ complete?: boolean;
83
+ at: number;
84
+ }>;
69
85
  /**
70
86
  * Explicit terminal-output replay for final runner logs when the caller must
71
87
  * keep `status` nonterminal. Direct-worker/Postgres progress finalizers do this so
@@ -75,6 +91,8 @@ export type RuntimeStatusUpdate = {
75
91
  terminalLogReplay?: {
76
92
  lines: string[];
77
93
  totalCount?: number;
94
+ dedupeMode?: 'semantic' | 'exact';
95
+ producerAttempt?: number;
78
96
  } | null;
79
97
  liveTimeline?: PlayRunTimelineEntry[];
80
98
  liveNodeProgress?: PlayVisualNodeProgressMap;
@@ -347,12 +365,84 @@ function applyRetryJitter(delayMs: number): number {
347
365
  const APP_RUNTIME_API_DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
348
366
  const RUN_STATUS_LEDGER_SNAPSHOT_CACHE_LIMIT = 1_000;
349
367
  const runStatusLedgerSnapshots = new Map<string, PlayRunLedgerSnapshot>();
350
- // Positional log cursor per run: count of liveLogs lines already forwarded to
351
- // the ledger from this process. Process-local on purpose a fresh process
352
- // re-sends from offset 0 and Run Log Stream ingestion skips the overlap
353
- // positionally (exactly-once on the channel, repeated identical lines kept).
368
+ // Positional log cursor per run attempt: count of liveLogs lines already
369
+ // forwarded from this process. A fresh process re-sends from offset 0; durable
370
+ // ingestion skips overlap within that same attempt.
354
371
  const runLogChannelSentCounts = new Map<string, number>();
355
372
  const runStatusUpdateChains = new Map<string, Promise<void>>();
373
+ const APP_RUNTIME_LOG_EVENT_MAX_JSON_BYTES = 128 * 1024;
374
+ const APP_RUNTIME_EVENT_BATCH_MAX_JSON_BYTES = 512 * 1024;
375
+ const appRuntimeTextEncoder = new TextEncoder();
376
+
377
+ function splitRunEventForAppRuntime(
378
+ event: PlayRunLedgerEvent,
379
+ ): PlayRunLedgerEvent[] {
380
+ if (event.type !== 'log.appended' || event.lines.length === 0) return [event];
381
+ const split: PlayRunLedgerEvent[] = [];
382
+ let index = 0;
383
+ while (index < event.lines.length) {
384
+ const start = index;
385
+ let bytes = 2;
386
+ while (index < event.lines.length) {
387
+ const lineBytes = appRuntimeTextEncoder.encode(
388
+ JSON.stringify(event.lines[index]!),
389
+ ).length;
390
+ if (
391
+ index > start &&
392
+ bytes + lineBytes + 1 > APP_RUNTIME_LOG_EVENT_MAX_JSON_BYTES
393
+ ) {
394
+ break;
395
+ }
396
+ bytes += lineBytes + 1;
397
+ index += 1;
398
+ }
399
+ split.push({
400
+ ...event,
401
+ lines: event.lines.slice(start, index),
402
+ ...(typeof event.channelOffset === 'number'
403
+ ? { channelOffset: event.channelOffset + start }
404
+ : {}),
405
+ });
406
+ }
407
+ return split;
408
+ }
409
+
410
+ export function partitionRunEventsForAppRuntime(
411
+ events: readonly PlayRunLedgerEvent[],
412
+ ): PlayRunLedgerEvent[][] {
413
+ const batches: PlayRunLedgerEvent[][] = [];
414
+ let current: PlayRunLedgerEvent[] = [];
415
+ let currentBytes = 2;
416
+ for (const event of events.flatMap(splitRunEventForAppRuntime)) {
417
+ const eventBytes = appRuntimeTextEncoder.encode(
418
+ JSON.stringify(event),
419
+ ).length;
420
+ if (
421
+ current.length > 0 &&
422
+ currentBytes + eventBytes + 1 > APP_RUNTIME_EVENT_BATCH_MAX_JSON_BYTES
423
+ ) {
424
+ batches.push(current);
425
+ current = [];
426
+ currentBytes = 2;
427
+ }
428
+ current.push(event);
429
+ currentBytes += eventBytes + 1;
430
+ }
431
+ if (current.length > 0) batches.push(current);
432
+ return batches;
433
+ }
434
+
435
+ async function appendRunEventBatchesViaAppRuntime(
436
+ context: WorkerRuntimeApiContext,
437
+ input: { playId: string; events: readonly PlayRunLedgerEvent[] },
438
+ ): Promise<void> {
439
+ for (const events of partitionRunEventsForAppRuntime(input.events)) {
440
+ await appendRunEventsViaAppRuntime(context, {
441
+ playId: input.playId,
442
+ events,
443
+ });
444
+ }
445
+ }
356
446
 
357
447
  function isTerminalRuntimeStatus(status: string): boolean {
358
448
  return (
@@ -364,6 +454,10 @@ function isTerminalRuntimeStatus(status: string): boolean {
364
454
  );
365
455
  }
366
456
 
457
+ function runLogChannelKey(runId: string, producerAttempt?: number): string {
458
+ return `${runId}:${producerAttempt ?? 'legacy'}`;
459
+ }
460
+
367
461
  function rememberRunStatusLedgerSnapshot(
368
462
  runId: string,
369
463
  snapshot: PlayRunLedgerSnapshot,
@@ -376,7 +470,11 @@ function rememberRunStatusLedgerSnapshot(
376
470
  const oldestRunId = runStatusLedgerSnapshots.keys().next().value;
377
471
  if (!oldestRunId) break;
378
472
  runStatusLedgerSnapshots.delete(oldestRunId);
379
- runLogChannelSentCounts.delete(oldestRunId);
473
+ for (const key of runLogChannelSentCounts.keys()) {
474
+ if (key.startsWith(`${oldestRunId}:`)) {
475
+ runLogChannelSentCounts.delete(key);
476
+ }
477
+ }
380
478
  runStatusUpdateChains.delete(oldestRunId);
381
479
  }
382
480
  }
@@ -399,12 +497,16 @@ function isRetryableAppRuntimeResponse(input: {
399
497
  if (input.status === 500 && isTransientAppRuntimeFailureBody(input.body)) {
400
498
  return true;
401
499
  }
500
+ return isRetryableAppRuntimeResponseStatus(input.status);
501
+ }
502
+
503
+ function isRetryableAppRuntimeResponseStatus(status: number): boolean {
402
504
  return (
403
- input.status === 429 ||
404
- (input.status >= 520 && input.status <= 524) ||
405
- input.status === 502 ||
406
- input.status === 503 ||
407
- input.status === 504
505
+ status === 429 ||
506
+ (status >= 520 && status <= 524) ||
507
+ status === 502 ||
508
+ status === 503 ||
509
+ status === 504
408
510
  );
409
511
  }
410
512
 
@@ -601,6 +703,38 @@ function sleep(ms: number): Promise<void> {
601
703
  return new Promise((resolve) => setTimeout(resolve, ms));
602
704
  }
603
705
 
706
+ async function retryAppRuntimeBodyTimeoutOrThrow(input: {
707
+ action: RuntimeApiRequest['action'];
708
+ attempt: number;
709
+ maxAttempts: number;
710
+ error: unknown;
711
+ retryAfterMs?: number | null;
712
+ }): Promise<void> {
713
+ if (!isRequestTimeoutAbort(input.error)) {
714
+ throw input.error;
715
+ }
716
+ if (
717
+ input.attempt < input.maxAttempts &&
718
+ isRetryableAppRuntimeFetchError({
719
+ action: input.action,
720
+ error: input.error,
721
+ })
722
+ ) {
723
+ await sleep(
724
+ Math.max(
725
+ applyRetryJitter(appRuntimeRetryDelayMs(input.action, input.attempt)),
726
+ input.retryAfterMs ?? 0,
727
+ ),
728
+ );
729
+ return;
730
+ }
731
+ throw new AppRuntimeApiTransportError({
732
+ action: input.action,
733
+ attempts: input.attempt,
734
+ cause: input.error,
735
+ });
736
+ }
737
+
604
738
  export class AppRuntimeApiTransportError extends Error {
605
739
  readonly action: RuntimeApiRequest['action'];
606
740
  readonly attempts: number;
@@ -691,9 +825,40 @@ async function postAppRuntimeApi<TResponse>(
691
825
  });
692
826
  }
693
827
  if (response.ok) {
694
- return (await response.json()) as TResponse;
828
+ try {
829
+ return (await response.json()) as TResponse;
830
+ } catch (error) {
831
+ await retryAppRuntimeBodyTimeoutOrThrow({
832
+ action: body.action,
833
+ attempt,
834
+ maxAttempts,
835
+ error,
836
+ });
837
+ continue;
838
+ }
839
+ }
840
+ let responseText: string;
841
+ try {
842
+ responseText = await response.text();
843
+ } catch (error) {
844
+ if (
845
+ isRequestTimeoutAbort(error) &&
846
+ !isRetryableAppRuntimeResponseStatus(response.status)
847
+ ) {
848
+ throw new Error(
849
+ `App runtime API ${body.action} failed with status ${response.status}: response body timed out`,
850
+ { cause: error },
851
+ );
852
+ }
853
+ await retryAppRuntimeBodyTimeoutOrThrow({
854
+ action: body.action,
855
+ attempt,
856
+ maxAttempts,
857
+ error,
858
+ retryAfterMs: appRuntimeRetryAfterMs(response, ''),
859
+ });
860
+ continue;
695
861
  }
696
- const responseText = await response.text();
697
862
  if (
698
863
  attempt < maxAttempts &&
699
864
  isRetryableAppRuntimeResponse({
@@ -917,7 +1082,9 @@ async function updateRunStatusViaAppRuntimeUnlocked(
917
1082
  // durable stream can recover any progress flush that was lost in transit.
918
1083
  const sentCount = isTerminalRuntimeStatus(update.status)
919
1084
  ? 0
920
- : (runLogChannelSentCounts.get(update.playId) ?? 0);
1085
+ : (runLogChannelSentCounts.get(
1086
+ runLogChannelKey(update.playId, update.liveLogProducerAttempt),
1087
+ ) ?? 0);
921
1088
  const logSlice = Array.isArray(update.liveLogs)
922
1089
  ? slicePositionalLogLines({
923
1090
  bufferLines: update.liveLogs,
@@ -932,6 +1099,7 @@ async function updateRunStatusViaAppRuntimeUnlocked(
932
1099
  ? {
933
1100
  lines: update.liveLogs,
934
1101
  totalCount: update.liveLogTotalCount,
1102
+ producerAttempt: update.liveLogProducerAttempt,
935
1103
  }
936
1104
  : null;
937
1105
  const statusPatchLogSlice = terminalLogReplay ? null : logSlice;
@@ -944,6 +1112,7 @@ async function updateRunStatusViaAppRuntimeUnlocked(
944
1112
  lastCheckpointAt: update.lastCheckpointAt ?? null,
945
1113
  liveLogs: statusPatchLogSlice?.lines ?? null,
946
1114
  liveLogsChannelOffset: statusPatchLogSlice?.channelOffset ?? null,
1115
+ liveLogsProducerAttempt: update.liveLogProducerAttempt ?? null,
947
1116
  liveNodeProgress: update.liveNodeProgress ?? null,
948
1117
  result: update.result,
949
1118
  },
@@ -951,33 +1120,87 @@ async function updateRunStatusViaAppRuntimeUnlocked(
951
1120
  now: update.lastCheckpointAt ?? Date.now(),
952
1121
  source: 'worker',
953
1122
  });
954
- if (terminalLogReplay && terminalLogReplay.lines.length > 0) {
955
- events.push(
956
- ...buildTerminalLogReplayEvents({
957
- runId: update.playId,
958
- source: 'worker',
959
- occurredAt: update.lastCheckpointAt ?? Date.now(),
960
- lines: terminalLogReplay.lines,
961
- liveLogTotalCount: terminalLogReplay.totalCount,
962
- }),
963
- );
1123
+ for (const event of update.datasetLifecycleEvents ?? []) {
1124
+ const current = previousSnapshot.datasetsById[event.datasetId];
1125
+ const changesSnapshot =
1126
+ !current ||
1127
+ current.path !== event.path ||
1128
+ current.tableNamespace !== event.tableNamespace ||
1129
+ current.phase !== event.phase ||
1130
+ (event.persistedRows ?? 0) > current.persistedRows ||
1131
+ (event.succeededRows ?? 0) > current.succeededRows ||
1132
+ (event.failedRows ?? 0) > current.failedRows ||
1133
+ (event.complete === true && current.complete !== true);
1134
+ if (!changesSnapshot) continue;
1135
+ events.push({
1136
+ type: 'dataset.lifecycle',
1137
+ runId: update.playId,
1138
+ source: 'worker',
1139
+ occurredAt: event.at,
1140
+ datasetId: event.datasetId,
1141
+ path: event.path,
1142
+ tableNamespace: event.tableNamespace,
1143
+ phase: event.phase,
1144
+ ...(event.persistedRows === undefined
1145
+ ? {}
1146
+ : { persistedRows: event.persistedRows }),
1147
+ ...(event.succeededRows === undefined
1148
+ ? {}
1149
+ : { succeededRows: event.succeededRows }),
1150
+ ...(event.failedRows === undefined
1151
+ ? {}
1152
+ : { failedRows: event.failedRows }),
1153
+ ...(event.complete === undefined ? {} : { complete: event.complete }),
1154
+ });
964
1155
  }
965
- if (events.length === 0) {
1156
+ const terminalLogEvents =
1157
+ terminalLogReplay && terminalLogReplay.lines.length > 0
1158
+ ? buildTerminalLogReplayEvents({
1159
+ runId: update.playId,
1160
+ source: 'worker',
1161
+ occurredAt: update.lastCheckpointAt ?? Date.now(),
1162
+ lines: terminalLogReplay.lines,
1163
+ liveLogTotalCount: terminalLogReplay.totalCount,
1164
+ dedupeMode: terminalLogReplay.dedupeMode,
1165
+ producerAttempt: terminalLogReplay.producerAttempt,
1166
+ })
1167
+ : [];
1168
+ if (events.length === 0 && terminalLogEvents.length === 0) {
966
1169
  return;
967
1170
  }
968
- await appendRunEventsViaAppRuntime(context, {
969
- playId: update.playId,
970
- events,
971
- });
1171
+ const combinedEvents = [...events, ...terminalLogEvents];
1172
+ if (partitionRunEventsForAppRuntime(combinedEvents).length === 1) {
1173
+ // Preserve the single-mutation fast path for ordinary bounded updates.
1174
+ await appendRunEventsViaAppRuntime(context, {
1175
+ playId: update.playId,
1176
+ events: combinedEvents,
1177
+ });
1178
+ } else {
1179
+ // Persist terminal lifecycle first so an oversized or interrupted
1180
+ // best-effort log heal can never prevent settlement. Each bounded log
1181
+ // batch is a separate mutation; retries are safe because ingestion owns
1182
+ // positional reconciliation.
1183
+ await appendRunEventBatchesViaAppRuntime(context, {
1184
+ playId: update.playId,
1185
+ events,
1186
+ });
1187
+ await appendRunEventBatchesViaAppRuntime(context, {
1188
+ playId: update.playId,
1189
+ events: terminalLogEvents,
1190
+ });
1191
+ }
972
1192
  if (statusPatchLogSlice) {
973
1193
  runLogChannelSentCounts.set(
974
- update.playId,
1194
+ runLogChannelKey(update.playId, update.liveLogProducerAttempt),
975
1195
  statusPatchLogSlice.channelOffset + statusPatchLogSlice.lines.length,
976
1196
  );
977
1197
  }
978
1198
  rememberRunStatusLedgerSnapshot(
979
1199
  update.playId,
980
- reducePlayRunLedgerEvents(previousSnapshot, events),
1200
+ reducePlayRunLedgerEvents(previousSnapshot, [
1201
+ ...events,
1202
+ ...terminalLogEvents,
1203
+ ]),
981
1204
  );
982
1205
  }
983
1206
 
@@ -106,6 +106,7 @@ import {
106
106
  parseToolExecuteAuthScopeChangedError,
107
107
  ToolExecuteAuthScopeChangedError,
108
108
  } from './tool-execute-retry-policy';
109
+ import { ToolHttpError } from './tool-http-errors';
109
110
  import {
110
111
  buildDurableCtxCallCacheKey,
111
112
  buildDurableRunPlayInvocationScope,
@@ -3486,7 +3487,8 @@ export class PlayContextImpl {
3486
3487
  );
3487
3488
  }
3488
3489
  const finalWrapped =
3489
- completed?.status === 'completed' || completed?.status === 'skipped'
3490
+ (completed?.status === 'completed' || completed?.status === 'skipped') &&
3491
+ completed.output !== undefined
3490
3492
  ? await this.wrapToolExecutionResult({
3491
3493
  toolId,
3492
3494
  status:
@@ -6319,11 +6321,6 @@ export class PlayContextImpl {
6319
6321
  semanticKey: toolRequestIdentity,
6320
6322
  force: toolCachePolicy.force,
6321
6323
  staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
6322
- markSkipped: () => {
6323
- this.log(
6324
- `ctx.tools.execute(${toolId}): no-op due completed receipt ${toolRequestIdentity} (label: ${normalizedKey})`,
6325
- );
6326
- },
6327
6324
  onClaimedResult: (output, receiptKey) =>
6328
6325
  markToolExecuteResultExecutionOutcome(output, {
6329
6326
  kind: 'live',
@@ -7277,11 +7274,6 @@ export class PlayContextImpl {
7277
7274
  egressSlot.release();
7278
7275
  }
7279
7276
  },
7280
- markSkipped: (output) => {
7281
- this.log(
7282
- `ctx.fetch(${output?.url ?? ''}): no-op due completed receipt`,
7283
- );
7284
- },
7285
7277
  },
7286
7278
  );
7287
7279
  }
@@ -7348,7 +7340,6 @@ export class PlayContextImpl {
7348
7340
  : options?.semanticKey,
7349
7341
  staleAfterSeconds: options?.staleAfterSeconds,
7350
7342
  markSkipped: (output) => {
7351
- this.log(`ctx.step(${normalizedKey}): no-op due completed receipt`);
7352
7343
  assertJsonSerializableStepOutput(normalizedKey, output);
7353
7344
  },
7354
7345
  execute: executeStep,
@@ -8500,6 +8491,8 @@ export class PlayContextImpl {
8500
8491
 
8501
8492
  while (true) {
8502
8493
  let response: Response;
8494
+ let providerCallStartedAt: number | null = null;
8495
+ let providerCallElapsedMs: number | null = null;
8503
8496
  const durableCallReceiptKey =
8504
8497
  options?.durableCallReceiptKey?.trim() || null;
8505
8498
  const executionAuthScopeDigest =
@@ -8533,7 +8526,6 @@ export class PlayContextImpl {
8533
8526
  }, timeoutMs)
8534
8527
  : null;
8535
8528
  try {
8536
- const providerCallStartedAt = Date.now();
8537
8529
  const ownershipStartedAt = Date.now();
8538
8530
  await options?.beforeProviderCall?.();
8539
8531
  if (runtimeReceiptReadTraceEnabled) {
@@ -8541,6 +8533,7 @@ export class PlayContextImpl {
8541
8533
  `[perf] tool call id=${toolId} phase=before_provider_ownership elapsed_ms=${Date.now() - ownershipStartedAt}`,
8542
8534
  );
8543
8535
  }
8536
+ providerCallStartedAt = Date.now();
8544
8537
  const heartbeatIntervalMs = hasReceiptHeartbeat
8545
8538
  ? runtimeLeaseHeartbeatIntervalFromExpiry({
8546
8539
  leaseExpiresAt: options?.receiptLeaseExpiresAt,
@@ -8640,10 +8633,11 @@ export class PlayContextImpl {
8640
8633
  `[perf] tool call id=${toolId} phase=integration_fetch_headers elapsed_ms=${Date.now() - integrationFetchStartedAt} status=${response.status}`,
8641
8634
  );
8642
8635
  }
8636
+ providerCallElapsedMs = Date.now() - providerCallStartedAt;
8643
8637
  this.resourceGovernor.observe({
8644
8638
  toolId,
8645
8639
  providerResourceKey: `tool:${toolId}`,
8646
- providerLatencyMs: Date.now() - providerCallStartedAt,
8640
+ providerLatencyMs: providerCallElapsedMs,
8647
8641
  providerSuccess: response.ok,
8648
8642
  provider429: response.status === 429,
8649
8643
  });
@@ -8654,16 +8648,18 @@ export class PlayContextImpl {
8654
8648
  if (heartbeatFailure) {
8655
8649
  throw heartbeatFailure;
8656
8650
  }
8657
- if (abortController?.signal.aborted) {
8658
- throw abortController.signal.reason instanceof Error
8651
+ const transportError = abortController?.signal.aborted
8652
+ ? abortController.signal.reason instanceof Error
8659
8653
  ? abortController.signal.reason
8660
8654
  : new Error(
8661
8655
  `Tool ${toolId} runtime API call timed out after ${timeoutMs}ms.`,
8662
- );
8663
- }
8656
+ )
8657
+ : error;
8664
8658
  transportAttempt += 1;
8665
8659
  const message =
8666
- error instanceof Error ? error.message : String(error);
8660
+ transportError instanceof Error
8661
+ ? transportError.message
8662
+ : String(transportError);
8667
8663
  if (transportAttempt < TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS) {
8668
8664
  await this.governor.chargeBudget('retry');
8669
8665
  const retryAfterMs =
@@ -8679,8 +8675,11 @@ export class PlayContextImpl {
8679
8675
  await this.sleepWithCheckpointHeartbeat(retryAfterMs);
8680
8676
  continue;
8681
8677
  }
8682
- throw new Error(
8678
+ throw new ToolHttpError(
8683
8679
  `Tool ${toolId} transport failed calling ${url} after ${transportAttempt}/${TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS} attempts: ${message}`,
8680
+ null,
8681
+ 0,
8682
+ 'repairable',
8684
8683
  );
8685
8684
  } finally {
8686
8685
  if (timeoutHandle) {
@@ -8711,6 +8710,9 @@ export class PlayContextImpl {
8711
8710
  bodyText: text,
8712
8711
  retryAfterHeader: response.headers.get('retry-after'),
8713
8712
  transientHttpRetrySafe: retrySafeTransientHttp,
8713
+ ...(providerCallElapsedMs !== null
8714
+ ? { providerLatencyMs: providerCallElapsedMs }
8715
+ : {}),
8714
8716
  });
8715
8717
  if (failure.backpressureDelayMs !== null) {
8716
8718
  // Feed the server-observed Retry-After back into the shared
@@ -380,6 +380,18 @@ export interface MapExecutionScope {
380
380
  }
381
381
 
382
382
  export type PlayExecutionEvent =
383
+ | {
384
+ type: 'dataset.lifecycle';
385
+ datasetId: string;
386
+ path: string;
387
+ tableNamespace: string;
388
+ phase: 'registered' | 'available' | 'failed';
389
+ persistedRows?: number;
390
+ succeededRows?: number;
391
+ failedRows?: number;
392
+ complete?: boolean;
393
+ at: number;
394
+ }
383
395
  | {
384
396
  type: 'map.preparing';
385
397
  mapInvocationId: string;
@@ -1,10 +1,16 @@
1
- import { createHash } from 'node:crypto';
1
+ import {
2
+ normalizePlayNameForSheet,
3
+ normalizeTableNamespace,
4
+ sha256Hex,
5
+ } from '../plays/row-identity';
2
6
 
3
7
  export function createRuntimeDatasetId(
4
8
  playName: string,
5
9
  tableNamespace: string,
6
10
  ): string {
7
- return createHash('sha1')
8
- .update(`${playName}:${tableNamespace}`)
9
- .digest('hex');
11
+ const normalizedPlayName = normalizePlayNameForSheet(playName);
12
+ const normalizedTableNamespace = normalizeTableNamespace(tableNamespace);
13
+ return sha256Hex(
14
+ `${normalizedPlayName}:${normalizedTableNamespace}`,
15
+ ).slice(0, 40);
10
16
  }
@@ -56,6 +56,7 @@ export type CreateDbSessionRequest = {
56
56
 
57
57
  export type CreateDbSessionResponse = {
58
58
  sessionId: string;
59
+ executionRole?: string;
59
60
  postgresUrl?: string;
60
61
  encryptedPostgresUrl?: EncryptedPostgresUrl;
61
62
  postgres?: {
@@ -398,6 +399,14 @@ export const createDbSessionResponseSchema =
398
399
  }
399
400
  return {
400
401
  sessionId: parseString(value.sessionId, 'sessionId'),
402
+ ...(value.executionRole !== undefined
403
+ ? {
404
+ executionRole: parseOptionalString(
405
+ value.executionRole,
406
+ 'executionRole',
407
+ ),
408
+ }
409
+ : {}),
401
410
  ...(value.postgresUrl !== undefined
402
411
  ? { postgresUrl: parseOptionalString(value.postgresUrl, 'postgresUrl') }
403
412
  : {}),
@@ -412,9 +412,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
412
412
  receipt: RuntimeStepReceipt,
413
413
  source: DurableReceiptRecoverySource = 'cache',
414
414
  ): Promise<T> => {
415
- input.log(
416
- `ctx.${input.operation}(${input.id}): recovered result from receipt`,
417
- );
415
+ input.log(`ctx.${input.operation}(${input.id}): reused completed work`);
418
416
  if (receipt.output === undefined) {
419
417
  return receipt.output as T;
420
418
  }
@@ -447,7 +445,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
447
445
  }
448
446
  if (reclaimed?.status === 'failed') {
449
447
  throw new Error(
450
- `ctx.${input.operation}(${input.id}): receipt is failed and could not be reclaimed: ${reclaimed.error ?? 'unknown error'}.`,
448
+ `ctx.${input.operation}(${input.id}): previous execution failed and cannot be reused: ${reclaimed.error ?? 'unknown error'}.`,
451
449
  );
452
450
  }
453
451
  if (
@@ -509,7 +507,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
509
507
  receipt: RuntimeStepReceipt,
510
508
  ): Promise<never> => {
511
509
  throw new Error(
512
- `ctx.${input.operation}(${input.id}): receipt is failed and could not be claimed: ${receipt.error ?? 'unknown error'}.`,
510
+ `ctx.${input.operation}(${input.id}): previous execution failed and cannot be reused: ${receipt.error ?? 'unknown error'}.`,
513
511
  );
514
512
  };
515
513
 
@@ -1,7 +1,10 @@
1
1
  import { createSecretRedactionContext } from './secret-redaction';
2
- import { RUNTIME_RECEIPT_OUTPUT_MAX_BYTES } from './output-size-limits';
2
+ import {
3
+ assertRuntimeReceiptOutputWithinLimit,
4
+ TERMINAL_RUN_RESULT_MAX_BYTES,
5
+ } from './output-size-limits';
3
6
 
4
- export const MAX_LEDGER_RESULT_PAYLOAD_BYTES = RUNTIME_RECEIPT_OUTPUT_MAX_BYTES;
7
+ export const MAX_LEDGER_RESULT_PAYLOAD_BYTES = TERMINAL_RUN_RESULT_MAX_BYTES;
5
8
  export const MAX_LEDGER_LOG_LINES_PER_EVENT = 500;
6
9
  export const MAX_LEDGER_LOG_LINE_LENGTH = 2_000;
7
10
 
@@ -21,6 +24,15 @@ export function sanitizeLedgerPayload(value: unknown): unknown {
21
24
  return ledgerIngressRedactor.redact(value);
22
25
  }
23
26
 
27
+ /** Secret-redact a receipt payload without coupling its 10 MiB cell limit to the ledger. */
28
+ export function sanitizeRuntimeReceiptPayload(value: unknown): unknown {
29
+ assertRuntimeReceiptOutputWithinLimit({
30
+ output: value,
31
+ path: 'runtime receipt output',
32
+ });
33
+ return ledgerIngressRedactor.redact(value);
34
+ }
35
+
24
36
  export function sanitizeLedgerLogLines(value: unknown): string[] {
25
37
  if (!Array.isArray(value)) return [];
26
38
  return value
@@ -3,8 +3,10 @@ const encoder = typeof TextEncoder === 'undefined' ? null : new TextEncoder();
3
3
  export const TERMINAL_RUN_RESULT_MAX_BYTES = 2_500_000;
4
4
  export const CUSTOMER_OUTPUT_VALUE_MAX_BYTES = 1 * 1024 * 1024;
5
5
  export const CUSTOMER_OUTPUT_TOTAL_MAX_BYTES = 2 * 1024 * 1024;
6
- export const RUNTIME_RECEIPT_OUTPUT_MAX_BYTES = 2 * 1024 * 1024;
7
- export const RUNTIME_RECEIPT_COMPLETION_BUFFER_MAX_BYTES = 16 * 1024 * 1024;
6
+ /** Inline Postgres receipt contract: one serialized output is at most 10 MiB. */
7
+ export const RUNTIME_RECEIPT_OUTPUT_MAX_BYTES = 10 * 1024 * 1024;
8
+ /** One completion request may contain multiple receipts up to 32 MiB total. */
9
+ export const RUNTIME_RECEIPT_COMPLETION_BUFFER_MAX_BYTES = 32 * 1024 * 1024;
8
10
 
9
11
  export class OutputTooLargeError extends Error {
10
12
  readonly code = 'OUTPUT_TOO_LARGE';
@@ -1,6 +1,26 @@
1
1
  import { RECEIPT_STATUS_CODE } from './receipt-status';
2
2
  import { workReceiptFailureKindCode } from './work-receipts';
3
3
 
4
+ export function workReceiptRepairableFailurePredicateSql(
5
+ receiptTable: string,
6
+ ): string {
7
+ return `(
8
+ ${receiptTable}.failure_kind = ${workReceiptFailureKindCode('repairable')}::smallint
9
+ OR (
10
+ ${receiptTable}.failure_kind = ${workReceiptFailureKindCode('terminal')}::smallint
11
+ AND ${receiptTable}.error IS NOT NULL
12
+ AND lower(${receiptTable}.error) NOT LIKE '%billing cap%'
13
+ AND lower(${receiptTable}.error) NOT LIKE '%insufficient credits%'
14
+ AND lower(${receiptTable}.error) NOT LIKE '%monthly billing limit%'
15
+ AND (
16
+ ${receiptTable}.error ~* '(^|[^0-9])5[0-9]{2}([^0-9]|$)'
17
+ OR lower(${receiptTable}.error) LIKE '%transport failed calling%'
18
+ OR lower(${receiptTable}.error) LIKE '%runtime api call timed out%'
19
+ )
20
+ )
21
+ )`;
22
+ }
23
+
4
24
  export function workReceiptClaimableStatusCodes(input: {
5
25
  forceRefresh?: boolean;
6
26
  forceFailedRefresh?: boolean;
@@ -47,7 +67,7 @@ export function workReceiptClaimConflictPredicateSql(input: {
47
67
  )
48
68
  OR ${table}.status <> ${RECEIPT_STATUS_CODE.failed}::smallint
49
69
  OR (
50
- ${table}.failure_kind = ${workReceiptFailureKindCode('repairable')}::smallint
70
+ ${workReceiptRepairableFailurePredicateSql(table)}
51
71
  AND (
52
72
  ${table}.run_id IS DISTINCT FROM ${input.claimantRunIdSql}
53
73
  OR COALESCE(${table}.lease_owner_attempt, 0) < ${claimantRunAttemptSql}::integer