deepline 0.1.199 → 0.1.201

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 (72) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +5 -1
  2. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +67 -18
  3. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/README.md +1 -1
  4. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +16 -0
  5. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +40 -9
  6. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +108 -39
  7. package/dist/bundling-sources/sdk/src/http.ts +5 -2
  8. package/dist/bundling-sources/sdk/src/play.ts +1 -1
  9. package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +7 -0
  10. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  11. package/dist/bundling-sources/sdk/src/worker-play-entry.ts +11 -55
  12. package/dist/bundling-sources/shared_libs/play-runtime/adaptive-tool-dispatcher.ts +355 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +172 -73
  14. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1794 -377
  15. package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +3 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +67 -4
  17. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +161 -39
  18. package/dist/bundling-sources/shared_libs/play-runtime/governor/adaptive-admission.ts +218 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +1 -1
  20. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +115 -25
  21. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +17 -17
  22. package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +22 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +2 -1
  24. package/dist/bundling-sources/shared_libs/play-runtime/live-events.ts +0 -1
  25. package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +265 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +135 -0
  27. package/dist/bundling-sources/shared_libs/play-runtime/play-input.ts +17 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/play-latency-trace.ts +0 -3
  29. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +10 -1
  30. package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +9 -15
  31. package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +262 -0
  32. package/dist/bundling-sources/shared_libs/play-runtime/receipt-heartbeat-supervisor.ts +64 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +76 -8
  34. package/dist/bundling-sources/shared_libs/play-runtime/receipt-state-sink.ts +59 -0
  35. package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +3 -2
  36. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +269 -0
  37. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +45 -3
  38. package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +0 -1
  39. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +433 -0
  40. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +227 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +882 -0
  42. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +573 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/bundle.ts +264 -0
  44. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/index.ts +33 -0
  45. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runner-events.ts +35 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +53 -0
  47. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +0 -1
  48. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +561 -105
  49. package/dist/bundling-sources/shared_libs/play-runtime/runtime-constants.ts +12 -0
  50. package/dist/bundling-sources/shared_libs/play-runtime/runtime-infra-mode.ts +23 -0
  51. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +23 -2
  52. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +16 -24
  53. package/dist/bundling-sources/shared_libs/play-runtime/serial-task-queue.ts +26 -0
  54. package/dist/bundling-sources/shared_libs/play-runtime/submit-limits.ts +1 -1
  55. package/dist/bundling-sources/shared_libs/play-runtime/vercel-protection.ts +81 -0
  56. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +54 -9
  57. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +17 -3
  58. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +0 -20
  59. package/dist/bundling-sources/shared_libs/plays/dataset.ts +105 -4
  60. package/dist/bundling-sources/shared_libs/plays/row-identity.ts +4 -11
  61. package/dist/cli/index.js +167 -54
  62. package/dist/cli/index.mjs +167 -54
  63. package/dist/index.d.mts +1 -1
  64. package/dist/index.d.ts +1 -1
  65. package/dist/index.js +8 -8
  66. package/dist/index.mjs +8 -8
  67. package/dist/plays/bundle-play-file.mjs +15 -20
  68. package/package.json +1 -1
  69. package/dist/bundling-sources/shared_libs/play-runtime/hatchet-cold-execution-diagnosis.ts +0 -321
  70. package/dist/bundling-sources/shared_libs/play-runtime/hatchet-cold-execution-target.ts +0 -158
  71. package/dist/bundling-sources/shared_libs/temporal/constants.ts +0 -39
  72. package/dist/bundling-sources/shared_libs/temporal/preview-config.ts +0 -150
@@ -66,10 +66,10 @@ import {
66
66
  import { RECEIPT_STATUS_CODE, receiptStatusFromCode } from './receipt-status';
67
67
  import {
68
68
  workReceiptClaimableStatusCodes,
69
+ workReceiptActiveOwnerPredicateSql,
69
70
  workReceiptClaimConflictPredicateSql,
70
71
  workReceiptHeartbeatPredicateSql,
71
72
  workReceiptReleaseOwnerPredicateSql,
72
- workReceiptRunningOwnerPredicateSql,
73
73
  } from './receipt-sql';
74
74
  import {
75
75
  activeRuntimeSheetAttemptFenceSql,
@@ -105,6 +105,7 @@ import {
105
105
  PLAY_RUNTIME_TEST_FAULT_HEADER,
106
106
  type RuntimeTestFaultName,
107
107
  } from './test-runtime-seams';
108
+ import { vercelProtectionBypassHeader } from './vercel-protection';
108
109
 
109
110
  type RuntimeApiContext = {
110
111
  baseUrl?: string | null;
@@ -302,10 +303,11 @@ const RUNTIME_DB_SESSION_RENEWAL_WINDOW_MS = 60_000;
302
303
  const RUNTIME_API_RETRY_DELAYS_MS = [
303
304
  250, 500, 1_000, 2_000, 4_000, 8_000, 8_000,
304
305
  ] as const;
305
- const RUNTIME_API_MAX_ATTEMPTS = RUNTIME_API_RETRY_DELAYS_MS.length + 1;
306
+ const RUNTIME_API_RESOLVE_PLAY_RETRY_DELAYS_MS = [
307
+ 250, 500, 1_000, 2_000, 4_000, 8_000, 8_000, 15_000, 15_000, 30_000,
308
+ ] as const;
306
309
  const RUNTIME_API_DEFAULT_RETRY_AFTER_MS = 2_000;
307
310
  const RUNTIME_API_REQUEST_TIMEOUT_MS = 30_000;
308
- const vercelProtectionCookieCache = new Map<string, Promise<string | null>>();
309
311
  const RUNTIME_POSTGRES_PREWARM_MAX_ATTEMPTS = 4;
310
312
  const RUNTIME_POSTGRES_PREWARM_RETRY_DELAYS_MS = [250, 750, 1_500] as const;
311
313
  const RUNTIME_POSTGRES_CONNECT_MAX_ATTEMPTS = 4;
@@ -315,11 +317,12 @@ const RUNTIME_WORK_RECEIPT_QUERY_RETRY_DELAYS_MS = [250, 750] as const;
315
317
  // Runtime DB sessions are minted against the pooled tenant endpoint. A healthy
316
318
  // connect is sub-second; spending minutes on one sandbox dial only hides a
317
319
  // broken route and stalls the whole play. Keep retries bounded and loud.
318
- const RUNTIME_POSTGRES_CONNECT_TIMEOUT_MS = 5_000;
320
+ const RUNTIME_POSTGRES_CONNECT_TIMEOUT_MS = 10_000;
319
321
  // Daytona executes one play per runner process. This is the bounded data-plane
320
322
  // budget for that one run's sheet reads/writes, receipts, and terminal/export
321
323
  // reads over the PgBouncer URL; it is not a scheduler/control-plane fan-out.
322
324
  const RUNTIME_POSTGRES_POOL_MAX_CONNECTIONS = 8;
325
+ const RECEIPT_STATUS_QUEUED = RECEIPT_STATUS_CODE.queued;
323
326
  const RECEIPT_STATUS_PENDING = RECEIPT_STATUS_CODE.pending;
324
327
  const RECEIPT_STATUS_RUNNING = RECEIPT_STATUS_CODE.running;
325
328
  const RECEIPT_STATUS_COMPLETED = RECEIPT_STATUS_CODE.completed;
@@ -456,20 +459,12 @@ function resolveRuntimeApiUrl(context: RuntimeApiContext): string {
456
459
  if (!baseUrl) {
457
460
  throw new Error('Runner runtime API requires a baseUrl.');
458
461
  }
459
- const url = new URL(
460
- `${baseUrl.replace(/\/$/, '')}${PLAY_RUNTIME_API_COMPAT_PATH}`,
461
- );
462
- const bypassToken = context.vercelProtectionBypassToken?.trim();
463
- if (bypassToken) {
464
- url.searchParams.set('x-vercel-set-bypass-cookie', 'true');
465
- url.searchParams.set('x-vercel-protection-bypass', bypassToken);
466
- }
467
- return url.toString();
462
+ return `${baseUrl.replace(/\/$/, '')}${PLAY_RUNTIME_API_COMPAT_PATH}`;
468
463
  }
469
464
 
470
465
  function resolveRuntimeApiHeaders(
471
466
  context: RuntimeApiContext,
472
- vercelProtectionCookie?: string | null,
467
+ vercelHeaders: Record<string, string>,
473
468
  ): Record<string, string> {
474
469
  const token = context.executorToken?.trim();
475
470
  if (!token) {
@@ -479,61 +474,16 @@ function resolveRuntimeApiHeaders(
479
474
  'content-type': 'application/json',
480
475
  authorization: `Bearer ${token}`,
481
476
  [PLAY_RUNTIME_CONTRACT_HEADER]: String(PLAY_RUNTIME_CONTRACT),
482
- ...(context.vercelProtectionBypassToken
483
- ? { 'x-vercel-protection-bypass': context.vercelProtectionBypassToken }
484
- : {}),
477
+ ...vercelHeaders,
485
478
  ...(context.runtimeTestFaultHeader?.trim()
486
479
  ? {
487
480
  [PLAY_RUNTIME_TEST_FAULT_HEADER]:
488
481
  context.runtimeTestFaultHeader.trim(),
489
482
  }
490
483
  : {}),
491
- ...(vercelProtectionCookie ? { cookie: vercelProtectionCookie } : {}),
492
484
  };
493
485
  }
494
486
 
495
- function setCookieHeaders(headers: Headers): string[] {
496
- const getter = (headers as Headers & { getSetCookie?: () => string[] })
497
- .getSetCookie;
498
- if (typeof getter === 'function') {
499
- return getter.call(headers);
500
- }
501
- const single = headers.get('set-cookie');
502
- return single ? [single] : [];
503
- }
504
-
505
- function cookieHeaderFromSetCookie(headers: Headers): string | null {
506
- const cookies = setCookieHeaders(headers)
507
- .flatMap((header) => header.split(/,(?=\s*[^;,=]+=[^;,]+)/g))
508
- .map((header) => header.split(';', 1)[0]?.trim() ?? '')
509
- .filter(Boolean);
510
- return cookies.length > 0 ? cookies.join('; ') : null;
511
- }
512
-
513
- function resolveVercelProtectionCookie(
514
- context: RuntimeApiContext,
515
- ): Promise<string | null> {
516
- const bypassToken = context.vercelProtectionBypassToken?.trim();
517
- const baseUrl = context.baseUrl?.trim().replace(/\/$/, '');
518
- if (!bypassToken || !baseUrl) return Promise.resolve(null);
519
-
520
- const cacheKey = `${baseUrl}\n${bypassToken}`;
521
- const cached = vercelProtectionCookieCache.get(cacheKey);
522
- if (cached) return cached;
523
-
524
- const promise = (async () => {
525
- const url = new URL(`${baseUrl}/api/v2/health`);
526
- url.searchParams.set('x-vercel-set-bypass-cookie', 'true');
527
- url.searchParams.set('x-vercel-protection-bypass', bypassToken);
528
- const response = await fetch(url.toString(), {
529
- headers: { 'x-vercel-protection-bypass': bypassToken },
530
- }).catch(() => null);
531
- return response ? cookieHeaderFromSetCookie(response.headers) : null;
532
- })();
533
- vercelProtectionCookieCache.set(cacheKey, promise);
534
- return promise;
535
- }
536
-
537
487
  function normalizeRuntimeUserEmail(
538
488
  value: string | null | undefined,
539
489
  ): string | null {
@@ -546,8 +496,11 @@ async function postRuntimeApi<TResponse>(
546
496
  body: RuntimeApiActionRequest,
547
497
  ): Promise<TResponse> {
548
498
  const url = resolveRuntimeApiUrl(context);
549
- const vercelProtectionCookie = await resolveVercelProtectionCookie(context);
550
- for (let attempt = 1; attempt <= RUNTIME_API_MAX_ATTEMPTS; attempt += 1) {
499
+ const vercelHeaders = vercelProtectionBypassHeader(
500
+ context.vercelProtectionBypassToken,
501
+ );
502
+ const maxAttempts = runtimeApiMaxAttempts(body.action);
503
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
551
504
  let response: Response;
552
505
  const abortController = new AbortController();
553
506
  const timeout = setTimeout(
@@ -557,13 +510,13 @@ async function postRuntimeApi<TResponse>(
557
510
  try {
558
511
  response = await fetch(url, {
559
512
  method: 'POST',
560
- headers: resolveRuntimeApiHeaders(context, vercelProtectionCookie),
513
+ headers: resolveRuntimeApiHeaders(context, vercelHeaders),
561
514
  body: JSON.stringify(body),
562
515
  signal: abortController.signal,
563
516
  });
564
517
  } catch (error) {
565
- if (attempt < RUNTIME_API_MAX_ATTEMPTS) {
566
- await sleepRuntimeApiRetry(attempt);
518
+ if (attempt < maxAttempts) {
519
+ await sleepRuntimeApiRetry(body.action, attempt);
567
520
  continue;
568
521
  }
569
522
  const message = error instanceof Error ? error.message : String(error);
@@ -605,8 +558,8 @@ async function postRuntimeApi<TResponse>(
605
558
  isTransientRuntimeApiErrorMessage(
606
559
  `${typeof parsed?.error === 'string' ? parsed.error : ''}\n${details ?? ''}`,
607
560
  ));
608
- if (shouldRetryRuntimeResponse && attempt < RUNTIME_API_MAX_ATTEMPTS) {
609
- await sleepRuntimeApiRetry(attempt, retryAfterMs);
561
+ if (shouldRetryRuntimeResponse && attempt < maxAttempts) {
562
+ await sleepRuntimeApiRetry(body.action, attempt, retryAfterMs);
610
563
  continue;
611
564
  }
612
565
 
@@ -625,11 +578,12 @@ async function postRuntimeApi<TResponse>(
625
578
  }
626
579
 
627
580
  function runtimeApiRetryDelayMs(
581
+ action: RuntimeApiActionRequest['action'],
628
582
  attempt: number,
629
583
  retryAfterMs?: number,
630
584
  ): number {
631
585
  const configuredDelay =
632
- RUNTIME_API_RETRY_DELAYS_MS[attempt - 1] ??
586
+ runtimeApiRetryDelaysForAction(action)[attempt - 1] ??
633
587
  RUNTIME_API_DEFAULT_RETRY_AFTER_MS;
634
588
  return retryAfterMs === undefined
635
589
  ? configuredDelay
@@ -637,14 +591,29 @@ function runtimeApiRetryDelayMs(
637
591
  }
638
592
 
639
593
  async function sleepRuntimeApiRetry(
594
+ action: RuntimeApiActionRequest['action'],
640
595
  attempt: number,
641
596
  retryAfterMs?: number,
642
597
  ): Promise<void> {
643
598
  await new Promise((resolve) =>
644
- setTimeout(resolve, runtimeApiRetryDelayMs(attempt, retryAfterMs)),
599
+ setTimeout(resolve, runtimeApiRetryDelayMs(action, attempt, retryAfterMs)),
645
600
  );
646
601
  }
647
602
 
603
+ function runtimeApiRetryDelaysForAction(
604
+ action: RuntimeApiActionRequest['action'],
605
+ ): readonly number[] {
606
+ return action === 'resolve_play'
607
+ ? RUNTIME_API_RESOLVE_PLAY_RETRY_DELAYS_MS
608
+ : RUNTIME_API_RETRY_DELAYS_MS;
609
+ }
610
+
611
+ function runtimeApiMaxAttempts(
612
+ action: RuntimeApiActionRequest['action'],
613
+ ): number {
614
+ return runtimeApiRetryDelaysForAction(action).length + 1;
615
+ }
616
+
648
617
  function isTransientRuntimeApiErrorMessage(message: string): boolean {
649
618
  return /timeout exceeded when trying to connect|timed out|fetch failed|ECONNRESET|ECONNREFUSED|UND_ERR_CONNECT_TIMEOUT|requested endpoint could not be found, or you don't have access/i.test(
650
619
  message,
@@ -757,6 +726,75 @@ async function isRuntimeSheetSchemaReady(
757
726
  }
758
727
  }
759
728
 
729
+ /**
730
+ * Report exactly what the scoped runtime role can see for the sheet table, from
731
+ * the same connection the readiness probe uses. When ensure_sheet reports the
732
+ * table as provisioned but the runner probe still returns not-ready, the cause
733
+ * is almost always that the runtime role lacks a privilege on the freshly
734
+ * created table (so information_schema.columns hides the attempt columns) or a
735
+ * schema/table-name mismatch. This turns the previously blind "still not ready"
736
+ * error into an actionable one that names the concrete missing piece.
737
+ */
738
+ async function readRuntimeSheetSchemaDiagnostic(
739
+ session: RuntimePostgresSession,
740
+ ): Promise<Record<string, unknown>> {
741
+ const query = async (
742
+ client: RuntimeQueryClient,
743
+ ): Promise<Record<string, unknown>> => {
744
+ const visibleColumns = await client.query<{ column_name: string }>(
745
+ `
746
+ SELECT column_name
747
+ FROM information_schema.columns
748
+ WHERE table_schema = $1
749
+ AND table_name = $2
750
+ ORDER BY ordinal_position
751
+ `,
752
+ [session.postgres.schema, session.postgres.sheetTable],
753
+ );
754
+ const privileges = await client.query<Record<string, unknown>>(
755
+ `
756
+ SELECT
757
+ current_user AS probe_role,
758
+ to_regclass($3) IS NOT NULL AS regclass_visible,
759
+ has_table_privilege(current_user, $3, 'SELECT') AS can_select,
760
+ has_table_privilege(current_user, $3, 'INSERT') AS can_insert,
761
+ has_table_privilege(current_user, $3, 'UPDATE') AS can_update
762
+ `,
763
+ [
764
+ session.postgres.schema,
765
+ session.postgres.sheetTable,
766
+ `${quoteIdentifier(session.postgres.schema)}.${quoteIdentifier(session.postgres.sheetTable)}`,
767
+ ],
768
+ );
769
+ const visible = new Set(visibleColumns.rows.map((row) => row.column_name));
770
+ return {
771
+ schema: session.postgres.schema,
772
+ table: session.postgres.sheetTable,
773
+ probeRole: privileges.rows[0]?.probe_role ?? null,
774
+ regclassVisible: privileges.rows[0]?.regclass_visible ?? null,
775
+ canSelect: privileges.rows[0]?.can_select ?? null,
776
+ canInsert: privileges.rows[0]?.can_insert ?? null,
777
+ canUpdate: privileges.rows[0]?.can_update ?? null,
778
+ visibleColumnCount: visible.size,
779
+ missingAttemptColumns: RUNTIME_SHEET_ATTEMPT_COLUMNS.filter(
780
+ (column) => !visible.has(column),
781
+ ),
782
+ };
783
+ };
784
+ try {
785
+ if (isRuntimeOneShotQueryFactoryRegistered()) {
786
+ return await withRuntimeOneShotPostgres(session, query);
787
+ }
788
+ return await withRuntimePostgres(session, query);
789
+ } catch (error) {
790
+ return {
791
+ schema: session.postgres.schema,
792
+ table: session.postgres.sheetTable,
793
+ diagnosticError: error instanceof Error ? error.message : String(error),
794
+ };
795
+ }
796
+ }
797
+
760
798
  async function waitForRuntimeSheetSchemaReadyAfterEnsure(
761
799
  session: RuntimePostgresSession,
762
800
  input: {
@@ -870,7 +908,7 @@ async function ensureRuntimeSheetForPreloadedSession(
870
908
  phase: 'ensure_sheet_for_preloaded_session',
871
909
  ms: Date.now() - ensureStartedAt,
872
910
  });
873
- const readyAfterEnsure = await waitForRuntimeSheetSchemaReadyAfterEnsure(
911
+ let readyAfterEnsure = await waitForRuntimeSheetSchemaReadyAfterEnsure(
874
912
  input.session,
875
913
  {
876
914
  sheetContract: input.sheetContract,
@@ -878,8 +916,39 @@ async function ensureRuntimeSheetForPreloadedSession(
878
916
  },
879
917
  );
880
918
  if (!readyAfterEnsure) {
919
+ // ensure_sheet provisions the sheet as the tenant admin role, but the
920
+ // preloaded session probes as the scoped runtime role. When the tenant
921
+ // role contract was only partially applied (e.g. role-membership grants
922
+ // skipped on a data plane where the owner cannot administer roles), the
923
+ // runtime role never inherits access to the freshly created table, so its
924
+ // information_schema view hides the attempt columns and the probe stays
925
+ // not-ready even though the table exists. Re-apply the storage grants
926
+ // through the same self-heal the pre-ensure probe uses, then re-check
927
+ // once. This is a grant repair, not a blind retry: if the runtime role
928
+ // still cannot see the sheet, we fail loudly below with the concrete
929
+ // runtime-role view.
930
+ const repairStartedAt = Date.now();
931
+ await repairRuntimeStorageGrants(context, {
932
+ playName: context.playName,
933
+ });
934
+ input.timings?.push({
935
+ phase: 'repair_storage_grants_after_preloaded_session_ensure',
936
+ ms: Date.now() - repairStartedAt,
937
+ retried: true,
938
+ });
939
+ readyAfterEnsure = await waitForRuntimeSheetSchemaReadyAfterEnsure(
940
+ input.session,
941
+ {
942
+ sheetContract: input.sheetContract,
943
+ timings: input.timings,
944
+ },
945
+ );
946
+ }
947
+ if (!readyAfterEnsure) {
948
+ const diagnostic = await readRuntimeSheetSchemaDiagnostic(input.session);
881
949
  throw new Error(
882
- `Runtime sheet schema for ctx.dataset("${input.tableNamespace}") is still not ready after ensure_sheet.`,
950
+ `Runtime sheet schema for ctx.dataset("${input.tableNamespace}") is still not ready after ensure_sheet. ` +
951
+ `Runtime-role view: ${JSON.stringify(diagnostic)}`,
883
952
  );
884
953
  }
885
954
  })();
@@ -1303,9 +1372,13 @@ async function prewarmRuntimePostgresSession(
1303
1372
  attempt += 1
1304
1373
  ) {
1305
1374
  try {
1306
- await withRuntimePostgres(session, async (client) => {
1307
- await client.query('SELECT 1');
1308
- });
1375
+ await withRuntimePostgres(
1376
+ session,
1377
+ async (client) => {
1378
+ await client.query('SELECT 1');
1379
+ },
1380
+ { maxConnectAttempts: 1 },
1381
+ );
1309
1382
  return;
1310
1383
  } catch (error) {
1311
1384
  if (
@@ -1454,17 +1527,17 @@ async function resetRuntimePostgresPool(postgresUrl: string): Promise<void> {
1454
1527
  async function withRuntimePostgres<T>(
1455
1528
  session: RuntimePostgresSession,
1456
1529
  fn: (client: RuntimePoolClient) => Promise<T>,
1457
- options: { cachePool?: boolean } = {},
1530
+ options: { cachePool?: boolean; maxConnectAttempts?: number } = {},
1458
1531
  ): Promise<T> {
1459
1532
  let client: RuntimePoolClient | null = null;
1460
1533
  let requestLocalPool: RuntimePool | null = null;
1461
1534
  const cachePool =
1462
1535
  (options.cachePool ?? true) && canReuseRuntimePostgresPoolsAcrossRequests();
1463
- for (
1464
- let attempt = 1;
1465
- attempt <= RUNTIME_POSTGRES_CONNECT_MAX_ATTEMPTS;
1466
- attempt += 1
1467
- ) {
1536
+ const maxConnectAttempts =
1537
+ options.maxConnectAttempts === undefined
1538
+ ? RUNTIME_POSTGRES_CONNECT_MAX_ATTEMPTS
1539
+ : Math.max(1, Math.floor(options.maxConnectAttempts));
1540
+ for (let attempt = 1; attempt <= maxConnectAttempts; attempt += 1) {
1468
1541
  try {
1469
1542
  const pool = getPostgresPool(session.postgresUrl, cachePool);
1470
1543
  if (!cachePool) {
@@ -1484,7 +1557,7 @@ async function withRuntimePostgres<T>(
1484
1557
  requestLocalPool = null;
1485
1558
  }
1486
1559
  if (
1487
- attempt >= RUNTIME_POSTGRES_CONNECT_MAX_ATTEMPTS ||
1560
+ attempt >= maxConnectAttempts ||
1488
1561
  !isTransientRuntimePostgresConnectionError(error)
1489
1562
  ) {
1490
1563
  throw error;
@@ -2658,6 +2731,8 @@ async function prepareRuntimeSheetDatasetRows(
2658
2731
  '$9::timestamptz',
2659
2732
  '$10::integer',
2660
2733
  );
2734
+ const targetOlderForeignAttemptOwnerSql =
2735
+ 'coalesce(target._attempt_owner_run_id, target._run_id) IS DISTINCT FROM $8::text AND COALESCE(target._attempt_seq, 0) < $10::integer';
2661
2736
  const existingAttemptFenceSql = activeRuntimeSheetAttemptFenceSql(
2662
2737
  'existing',
2663
2738
  '$7::text',
@@ -2665,6 +2740,8 @@ async function prepareRuntimeSheetDatasetRows(
2665
2740
  '$9::timestamptz',
2666
2741
  '$10::integer',
2667
2742
  );
2743
+ const existingForeignAttemptOwnerSql =
2744
+ 'coalesce(existing._attempt_owner_run_id, existing._run_id) IS DISTINCT FROM $8::text';
2668
2745
  const targetNewerTerminalRowSql = newerTerminalRuntimeSheetRowSql(
2669
2746
  'target',
2670
2747
  '$9::timestamptz',
@@ -2776,10 +2853,14 @@ async function prepareRuntimeSheetDatasetRows(
2776
2853
  WHERE target._key = input_rows._key
2777
2854
  AND target._status IN ('pending', 'running', 'failed')
2778
2855
  AND NOT (${targetNewerTerminalRowSql})
2779
- AND (
2780
- target._status = 'failed'
2781
- OR ${targetAttemptFenceSql}
2782
- )
2856
+ AND (
2857
+ ${targetAttemptFenceSql}
2858
+ OR ${targetOlderForeignAttemptOwnerSql}
2859
+ OR (
2860
+ $11::boolean
2861
+ AND target._status = 'failed'
2862
+ )
2863
+ )
2783
2864
  AND (
2784
2865
  target._run_id IS DISTINCT FROM $4::text
2785
2866
  OR target._input_index IS DISTINCT FROM input_rows._input_index
@@ -2808,10 +2889,17 @@ async function prepareRuntimeSheetDatasetRows(
2808
2889
  WHERE (
2809
2890
  existing._status IN ('pending', 'running', 'failed')
2810
2891
  AND NOT (${existingNewerTerminalRowSql})
2811
- AND (
2812
- existing._status = 'failed'
2813
- OR ${existingAttemptFenceSql}
2814
- )
2892
+ AND (
2893
+ ${existingAttemptFenceSql}
2894
+ OR (
2895
+ ${existingForeignAttemptOwnerSql}
2896
+ AND COALESCE(existing._attempt_seq, 0) < $10::integer
2897
+ )
2898
+ OR (
2899
+ $11::boolean
2900
+ AND existing._status = 'failed'
2901
+ )
2902
+ )
2815
2903
  )
2816
2904
  OR (
2817
2905
  existing._status = 'enriched'
@@ -3110,6 +3198,8 @@ async function markRuntimeRowsPendingForRecompute(
3110
3198
  '$7::timestamptz',
3111
3199
  '$8::integer',
3112
3200
  );
3201
+ const targetOlderForeignAttemptOwnerSql =
3202
+ 'coalesce(target._attempt_owner_run_id, target._run_id) IS DISTINCT FROM $6::text AND COALESCE(target._attempt_seq, 0) < $8::integer';
3113
3203
  const targetWritableEnrichedAttemptSql =
3114
3204
  writableEnrichedRuntimeSheetAttemptSql(
3115
3205
  'target',
@@ -3138,7 +3228,18 @@ async function markRuntimeRowsPendingForRecompute(
3138
3228
  FROM target_rows
3139
3229
  WHERE target._key = target_rows._key
3140
3230
  AND (
3141
- ${targetAttemptFenceSql}
3231
+ (
3232
+ target._status IN ('pending', 'running', 'failed')
3233
+ AND (
3234
+ ${targetAttemptFenceSql}
3235
+ OR ${targetOlderForeignAttemptOwnerSql}
3236
+ OR (
3237
+ $10::boolean
3238
+ AND target._status = 'failed'
3239
+ )
3240
+ )
3241
+ )
3242
+ OR ${targetAttemptFenceSql}
3142
3243
  OR (
3143
3244
  $10::boolean
3144
3245
  AND target._status = 'enriched'
@@ -3450,6 +3551,10 @@ export async function claimRuntimeWorkReceipt(
3450
3551
  input.leaseTtlMs,
3451
3552
  PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
3452
3553
  );
3554
+ const claimStatus =
3555
+ input.leaseAware === true
3556
+ ? RECEIPT_STATUS_QUEUED
3557
+ : RECEIPT_STATUS_RUNNING;
3453
3558
  const claimableStatuses = workReceiptClaimableStatusCodes({
3454
3559
  forceRefresh: input.forceRefresh,
3455
3560
  forceFailedRefresh: input.forceFailedRefresh,
@@ -3457,7 +3562,13 @@ export async function claimRuntimeWorkReceipt(
3457
3562
  });
3458
3563
  const { rows } = await client.query<Record<string, unknown>>(
3459
3564
  `
3460
- WITH claimed AS (
3565
+ WITH existing AS MATERIALIZED (
3566
+ SELECT status
3567
+ FROM ${workReceiptTable(session)}
3568
+ WHERE k = decode($1, 'hex')
3569
+ FOR UPDATE
3570
+ ),
3571
+ claimed AS (
3461
3572
  INSERT INTO ${workReceiptTable(session)} (
3462
3573
  k,
3463
3574
  status,
@@ -3500,6 +3611,7 @@ export async function claimRuntimeWorkReceipt(
3500
3611
  claimableStatusesSql: '$5',
3501
3612
  claimantRunIdSql: '$3',
3502
3613
  claimantRunAttemptSql: '$9',
3614
+ reclaimRunningSql: '$11',
3503
3615
  forceRefreshSql: '$8',
3504
3616
  forceFailedRefreshSql: '$10',
3505
3617
  })}
@@ -3525,25 +3637,39 @@ export async function claimRuntimeWorkReceipt(
3525
3637
  lease_owner_run_id,
3526
3638
  lease_owner_attempt,
3527
3639
  lease_expires_at,
3528
- updated_at
3640
+ updated_at,
3641
+ EXISTS (
3642
+ SELECT 1
3643
+ FROM existing
3644
+ WHERE status = $12::smallint
3645
+ ) AS refreshed_failed_receipt
3529
3646
  FROM claimed
3530
3647
  `,
3531
3648
  [
3532
3649
  workReceiptKeyHex(input.key),
3533
- RECEIPT_STATUS_RUNNING,
3650
+ claimStatus,
3534
3651
  input.runId,
3535
- RECEIPT_STATUS_RUNNING,
3652
+ claimStatus,
3536
3653
  claimableStatuses,
3537
3654
  leaseId,
3538
3655
  leaseTtlMs,
3539
3656
  input.forceRefresh === true,
3540
3657
  runAttempt,
3541
3658
  input.forceFailedRefresh === true,
3659
+ input.reclaimRunning === true,
3660
+ RECEIPT_STATUS_CODE.failed,
3542
3661
  ],
3543
3662
  );
3544
- const claimed = rows[0] ? mapRuntimeWorkReceiptRow(rows[0]) : null;
3663
+ const claimedRow = rows[0] ?? null;
3664
+ const claimed = claimedRow ? mapRuntimeWorkReceiptRow(claimedRow) : null;
3545
3665
  if (claimed) {
3546
- return { disposition: 'claimed', receipt: claimed };
3666
+ return {
3667
+ disposition: 'claimed',
3668
+ receipt: claimed,
3669
+ wasFailed:
3670
+ claimedRow.refreshed_failed_receipt === true ||
3671
+ claimedRow.refreshed_failed_receipt === 'true',
3672
+ };
3547
3673
  }
3548
3674
 
3549
3675
  const latest = await readRuntimeWorkReceipt(client, session, input.key);
@@ -3577,7 +3703,11 @@ function runtimeWorkReceiptClaimDispositionForBlockedReceipt(input: {
3577
3703
  if (receipt.status === 'failed') {
3578
3704
  return { disposition: 'failed', receipt };
3579
3705
  }
3580
- if (receipt.status === 'pending' || receipt.status === 'running') {
3706
+ if (
3707
+ receipt.status === 'queued' ||
3708
+ receipt.status === 'pending' ||
3709
+ receipt.status === 'running'
3710
+ ) {
3581
3711
  const ownerRunId = receipt.leaseOwnerRunId ?? receipt.runId ?? null;
3582
3712
  const leaseExpiresAtMs =
3583
3713
  typeof receipt.leaseExpiresAt === 'string'
@@ -3628,6 +3758,10 @@ export async function claimRuntimeWorkReceipts(
3628
3758
  const leaseIds = keys.map(() =>
3629
3759
  input.leaseAware === true ? newRuntimeWorkReceiptLeaseId() : null,
3630
3760
  );
3761
+ const claimStatus =
3762
+ input.leaseAware === true
3763
+ ? RECEIPT_STATUS_QUEUED
3764
+ : RECEIPT_STATUS_RUNNING;
3631
3765
  const runAttempt = normalizeRuntimeRunAttempt(input.runAttempt);
3632
3766
  const leaseTtlMs = normalizeRuntimeLeaseTtlMs(
3633
3767
  input.leaseTtlMs,
@@ -3697,6 +3831,7 @@ export async function claimRuntimeWorkReceipts(
3697
3831
  claimableStatusesSql: '$6',
3698
3832
  claimantRunIdSql: '$4::text',
3699
3833
  claimantRunAttemptSql: '$9',
3834
+ reclaimRunningSql: '$11',
3700
3835
  forceRefreshSql: '$8',
3701
3836
  forceFailedRefreshSql: '$10',
3702
3837
  })}
@@ -3783,14 +3918,15 @@ export async function claimRuntimeWorkReceipts(
3783
3918
  [
3784
3919
  keyHexes,
3785
3920
  leaseIds,
3786
- RECEIPT_STATUS_RUNNING,
3921
+ claimStatus,
3787
3922
  input.runId,
3788
- RECEIPT_STATUS_RUNNING,
3923
+ claimStatus,
3789
3924
  reclaimStatuses,
3790
3925
  leaseTtlMs,
3791
3926
  input.forceRefresh === true,
3792
3927
  runAttempt,
3793
3928
  input.forceFailedRefresh === true,
3929
+ input.reclaimRunning === true,
3794
3930
  ],
3795
3931
  );
3796
3932
  return rows.map((row) => {
@@ -3811,6 +3947,317 @@ export async function claimRuntimeWorkReceipts(
3811
3947
  );
3812
3948
  }
3813
3949
 
3950
+ export async function markRuntimeWorkReceiptRunning(
3951
+ context: RuntimeApiContext,
3952
+ input: {
3953
+ playName: string;
3954
+ runId: string;
3955
+ key: string;
3956
+ runAttempt?: number | null;
3957
+ leaseId?: string | null;
3958
+ leaseTtlMs?: number | null;
3959
+ },
3960
+ ): Promise<WorkReceipt | null> {
3961
+ const session = await getRuntimeWorkReceiptSession(context, {
3962
+ playName: input.playName,
3963
+ key: input.key,
3964
+ });
3965
+ return await withRuntimeWorkReceiptClient(
3966
+ context,
3967
+ session,
3968
+ async (client) => {
3969
+ const leaseId = input.leaseId?.trim() || null;
3970
+ const runAttempt = normalizeRuntimeRunAttempt(input.runAttempt);
3971
+ const leaseTtlMs = normalizeRuntimeLeaseTtlMs(
3972
+ input.leaseTtlMs,
3973
+ PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
3974
+ );
3975
+ const { rows } = await client.query<Record<string, unknown>>(
3976
+ `
3977
+ WITH running AS (
3978
+ UPDATE ${workReceiptTable(session)}
3979
+ SET status = $2::smallint,
3980
+ lease_expires_at = CASE
3981
+ WHEN lease_id IS NULL THEN NULL
3982
+ ELSE now() + ($6::double precision * interval '1 millisecond')
3983
+ END,
3984
+ updated_at = now()
3985
+ WHERE k = decode($1, 'hex')
3986
+ AND ${workReceiptActiveOwnerPredicateSql({
3987
+ receiptTable: workReceiptTable(session),
3988
+ ownerRunIdSql: '$3',
3989
+ ownerRunAttemptSql: '$5',
3990
+ leaseIdSql: '$4::text',
3991
+ })}
3992
+ RETURNING convert_from(k, 'UTF8') AS k,
3993
+ status,
3994
+ output,
3995
+ error,
3996
+ failure_kind,
3997
+ run_id,
3998
+ lease_id,
3999
+ lease_owner_run_id,
4000
+ lease_owner_attempt,
4001
+ lease_expires_at,
4002
+ updated_at
4003
+ )
4004
+ SELECT k, status, output, error, failure_kind, run_id, lease_id, lease_owner_run_id, lease_owner_attempt, lease_expires_at, updated_at
4005
+ FROM running
4006
+ `,
4007
+ [
4008
+ workReceiptKeyHex(input.key),
4009
+ RECEIPT_STATUS_RUNNING,
4010
+ input.runId,
4011
+ leaseId,
4012
+ runAttempt,
4013
+ leaseTtlMs,
4014
+ ],
4015
+ );
4016
+ return rows[0] ? mapRuntimeWorkReceiptRow(rows[0]) : null;
4017
+ },
4018
+ );
4019
+ }
4020
+
4021
+ export async function markRuntimeWorkReceiptsRunning(
4022
+ context: RuntimeApiContext,
4023
+ input: {
4024
+ playName: string;
4025
+ receipts: Array<{
4026
+ runId: string;
4027
+ key: string;
4028
+ leaseId?: string | null;
4029
+ runAttempt?: number | null;
4030
+ }>;
4031
+ leaseTtlMs?: number | null;
4032
+ },
4033
+ ): Promise<Array<WorkReceipt | null>> {
4034
+ const receipts = input.receipts.filter((receipt) => receipt.key.trim());
4035
+ if (receipts.length === 0) return [];
4036
+ const leaseTtlMs = normalizeRuntimeLeaseTtlMs(
4037
+ input.leaseTtlMs,
4038
+ PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
4039
+ );
4040
+ const session = await getRuntimeWorkReceiptSessionForKeys(context, {
4041
+ playName: input.playName,
4042
+ keys: receipts.map((receipt) => receipt.key),
4043
+ });
4044
+ return await withRuntimeWorkReceiptClient(
4045
+ context,
4046
+ session,
4047
+ async (client) => {
4048
+ const { rows } = await client.query<Record<string, unknown>>(
4049
+ `
4050
+ WITH inputs AS (
4051
+ SELECT key_values.key_hex,
4052
+ run_values.run_id,
4053
+ lease_values.lease_id,
4054
+ attempt_values.run_attempt,
4055
+ key_values.ord
4056
+ FROM unnest($1::text[]) WITH ORDINALITY AS key_values(key_hex, ord)
4057
+ JOIN unnest($2::text[]) WITH ORDINALITY AS run_values(run_id, ord)
4058
+ ON run_values.ord = key_values.ord
4059
+ JOIN unnest($3::text[]) WITH ORDINALITY AS lease_values(lease_id, ord)
4060
+ ON lease_values.ord = key_values.ord
4061
+ JOIN unnest($4::integer[]) WITH ORDINALITY AS attempt_values(run_attempt, ord)
4062
+ ON attempt_values.ord = key_values.ord
4063
+ ),
4064
+ running AS (
4065
+ UPDATE ${workReceiptTable(session)} AS target
4066
+ SET status = $5::smallint,
4067
+ lease_expires_at = CASE
4068
+ WHEN target.lease_id IS NULL THEN NULL
4069
+ ELSE now() + ($6::double precision * interval '1 millisecond')
4070
+ END,
4071
+ updated_at = now()
4072
+ FROM inputs
4073
+ WHERE target.k = decode(inputs.key_hex, 'hex')
4074
+ AND ${workReceiptActiveOwnerPredicateSql({
4075
+ receiptTable: 'target',
4076
+ ownerRunIdSql: 'inputs.run_id',
4077
+ ownerRunAttemptSql: 'inputs.run_attempt',
4078
+ leaseIdSql: 'inputs.lease_id',
4079
+ })}
4080
+ RETURNING target.k,
4081
+ target.status,
4082
+ target.output,
4083
+ target.error,
4084
+ target.failure_kind,
4085
+ target.run_id,
4086
+ target.lease_id,
4087
+ target.lease_owner_run_id,
4088
+ target.lease_owner_attempt,
4089
+ target.lease_expires_at,
4090
+ target.updated_at,
4091
+ inputs.ord
4092
+ ),
4093
+ returned AS (
4094
+ SELECT running.k,
4095
+ running.status,
4096
+ running.output,
4097
+ running.error,
4098
+ running.failure_kind,
4099
+ running.run_id,
4100
+ running.lease_id,
4101
+ running.lease_owner_run_id,
4102
+ running.lease_owner_attempt,
4103
+ running.lease_expires_at,
4104
+ running.updated_at,
4105
+ inputs.ord
4106
+ FROM inputs
4107
+ LEFT JOIN running ON running.ord = inputs.ord
4108
+ )
4109
+ SELECT convert_from(returned.k, 'UTF8') AS k,
4110
+ returned.status,
4111
+ returned.output,
4112
+ returned.error,
4113
+ returned.failure_kind,
4114
+ returned.run_id,
4115
+ returned.lease_id,
4116
+ returned.lease_owner_run_id,
4117
+ returned.lease_owner_attempt,
4118
+ returned.lease_expires_at,
4119
+ returned.updated_at
4120
+ FROM returned
4121
+ ORDER BY returned.ord
4122
+ `,
4123
+ [
4124
+ receipts.map((receipt) => workReceiptKeyHex(receipt.key)),
4125
+ receipts.map((receipt) => receipt.runId),
4126
+ receipts.map((receipt) => receipt.leaseId?.trim() || null),
4127
+ receipts.map((receipt) =>
4128
+ normalizeRuntimeRunAttempt(receipt.runAttempt),
4129
+ ),
4130
+ RECEIPT_STATUS_RUNNING,
4131
+ leaseTtlMs,
4132
+ ],
4133
+ );
4134
+ return rows.map((row) =>
4135
+ row.k == null ? null : mapRuntimeWorkReceiptRow(row),
4136
+ );
4137
+ },
4138
+ );
4139
+ }
4140
+
4141
+ export async function markRuntimeWorkReceiptsQueued(
4142
+ context: RuntimeApiContext,
4143
+ input: {
4144
+ playName: string;
4145
+ receipts: Array<{
4146
+ runId: string;
4147
+ key: string;
4148
+ leaseId?: string | null;
4149
+ runAttempt?: number | null;
4150
+ }>;
4151
+ leaseTtlMs?: number | null;
4152
+ },
4153
+ ): Promise<Array<WorkReceipt | null>> {
4154
+ const receipts = input.receipts.filter((receipt) => receipt.key.trim());
4155
+ if (receipts.length === 0) return [];
4156
+ const leaseTtlMs = normalizeRuntimeLeaseTtlMs(
4157
+ input.leaseTtlMs,
4158
+ PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
4159
+ );
4160
+ const session = await getRuntimeWorkReceiptSessionForKeys(context, {
4161
+ playName: input.playName,
4162
+ keys: receipts.map((receipt) => receipt.key),
4163
+ });
4164
+ return await withRuntimeWorkReceiptClient(
4165
+ context,
4166
+ session,
4167
+ async (client) => {
4168
+ const { rows } = await client.query<Record<string, unknown>>(
4169
+ `
4170
+ WITH inputs AS (
4171
+ SELECT key_values.key_hex,
4172
+ run_values.run_id,
4173
+ lease_values.lease_id,
4174
+ attempt_values.run_attempt,
4175
+ key_values.ord
4176
+ FROM unnest($1::text[]) WITH ORDINALITY AS key_values(key_hex, ord)
4177
+ JOIN unnest($2::text[]) WITH ORDINALITY AS run_values(run_id, ord)
4178
+ ON run_values.ord = key_values.ord
4179
+ JOIN unnest($3::text[]) WITH ORDINALITY AS lease_values(lease_id, ord)
4180
+ ON lease_values.ord = key_values.ord
4181
+ JOIN unnest($4::integer[]) WITH ORDINALITY AS attempt_values(run_attempt, ord)
4182
+ ON attempt_values.ord = key_values.ord
4183
+ ),
4184
+ queued AS (
4185
+ UPDATE ${workReceiptTable(session)} AS target
4186
+ SET status = $5::smallint,
4187
+ lease_expires_at = CASE
4188
+ WHEN target.lease_id IS NULL THEN NULL
4189
+ ELSE now() + ($6::double precision * interval '1 millisecond')
4190
+ END,
4191
+ updated_at = now()
4192
+ FROM inputs
4193
+ WHERE target.k = decode(inputs.key_hex, 'hex')
4194
+ AND ${workReceiptActiveOwnerPredicateSql({
4195
+ receiptTable: 'target',
4196
+ ownerRunIdSql: 'inputs.run_id',
4197
+ ownerRunAttemptSql: 'inputs.run_attempt',
4198
+ leaseIdSql: 'inputs.lease_id',
4199
+ })}
4200
+ RETURNING target.k,
4201
+ target.status,
4202
+ target.output,
4203
+ target.error,
4204
+ target.failure_kind,
4205
+ target.run_id,
4206
+ target.lease_id,
4207
+ target.lease_owner_run_id,
4208
+ target.lease_owner_attempt,
4209
+ target.lease_expires_at,
4210
+ target.updated_at,
4211
+ inputs.ord
4212
+ ),
4213
+ returned AS (
4214
+ SELECT queued.k,
4215
+ queued.status,
4216
+ queued.output,
4217
+ queued.error,
4218
+ queued.failure_kind,
4219
+ queued.run_id,
4220
+ queued.lease_id,
4221
+ queued.lease_owner_run_id,
4222
+ queued.lease_owner_attempt,
4223
+ queued.lease_expires_at,
4224
+ queued.updated_at,
4225
+ inputs.ord
4226
+ FROM inputs
4227
+ LEFT JOIN queued ON queued.ord = inputs.ord
4228
+ )
4229
+ SELECT convert_from(returned.k, 'UTF8') AS k,
4230
+ returned.status,
4231
+ returned.output,
4232
+ returned.error,
4233
+ returned.failure_kind,
4234
+ returned.run_id,
4235
+ returned.lease_id,
4236
+ returned.lease_owner_run_id,
4237
+ returned.lease_owner_attempt,
4238
+ returned.lease_expires_at,
4239
+ returned.updated_at
4240
+ FROM returned
4241
+ ORDER BY returned.ord
4242
+ `,
4243
+ [
4244
+ receipts.map((receipt) => workReceiptKeyHex(receipt.key)),
4245
+ receipts.map((receipt) => receipt.runId),
4246
+ receipts.map((receipt) => receipt.leaseId?.trim() || null),
4247
+ receipts.map((receipt) =>
4248
+ normalizeRuntimeRunAttempt(receipt.runAttempt),
4249
+ ),
4250
+ RECEIPT_STATUS_QUEUED,
4251
+ leaseTtlMs,
4252
+ ],
4253
+ );
4254
+ return rows.map((row) =>
4255
+ row.k == null ? null : mapRuntimeWorkReceiptRow(row),
4256
+ );
4257
+ },
4258
+ );
4259
+ }
4260
+
3814
4261
  export async function completeRuntimeWorkReceipt(
3815
4262
  context: RuntimeApiContext,
3816
4263
  input: {
@@ -3876,7 +4323,7 @@ export async function completeRuntimeWorkReceipt(
3876
4323
  lease_expires_at = NULL,
3877
4324
  updated_at = now()
3878
4325
  WHERE k = decode($1, 'hex')
3879
- AND ${workReceiptRunningOwnerPredicateSql({
4326
+ AND ${workReceiptActiveOwnerPredicateSql({
3880
4327
  receiptTable: workReceiptTable(session),
3881
4328
  ownerRunIdSql: '$4',
3882
4329
  ownerRunAttemptSql: '$6',
@@ -4012,7 +4459,7 @@ export async function completeRuntimeWorkReceipts(
4012
4459
  updated_at = now()
4013
4460
  FROM inputs
4014
4461
  WHERE target.k = decode(inputs.key_hex, 'hex')
4015
- AND ${workReceiptRunningOwnerPredicateSql({
4462
+ AND ${workReceiptActiveOwnerPredicateSql({
4016
4463
  receiptTable: 'target',
4017
4464
  ownerRunIdSql: 'inputs.run_id',
4018
4465
  ownerRunAttemptSql: 'inputs.run_attempt',
@@ -4120,7 +4567,7 @@ export async function failRuntimeWorkReceipt(
4120
4567
  lease_expires_at = NULL,
4121
4568
  updated_at = now()
4122
4569
  WHERE k = decode($1, 'hex')
4123
- AND ${workReceiptRunningOwnerPredicateSql({
4570
+ AND ${workReceiptActiveOwnerPredicateSql({
4124
4571
  receiptTable: workReceiptTable(session),
4125
4572
  ownerRunIdSql: '$4',
4126
4573
  ownerRunAttemptSql: '$7',
@@ -4190,7 +4637,7 @@ export async function releaseRuntimeWorkReceipt(
4190
4637
  lease_expires_at = NULL,
4191
4638
  updated_at = now()
4192
4639
  WHERE k = decode($1, 'hex')
4193
- AND ${workReceiptRunningOwnerPredicateSql({
4640
+ AND ${workReceiptActiveOwnerPredicateSql({
4194
4641
  receiptTable: workReceiptTable(session),
4195
4642
  ownerRunIdSql: '$3',
4196
4643
  ownerRunAttemptSql: '$5',
@@ -4283,7 +4730,7 @@ export async function failRuntimeWorkReceipts(
4283
4730
  updated_at = now()
4284
4731
  FROM inputs
4285
4732
  WHERE target.k = decode(inputs.key_hex, 'hex')
4286
- AND ${workReceiptRunningOwnerPredicateSql({
4733
+ AND ${workReceiptActiveOwnerPredicateSql({
4287
4734
  receiptTable: 'target',
4288
4735
  ownerRunIdSql: 'inputs.run_id',
4289
4736
  ownerRunAttemptSql: 'inputs.run_attempt',
@@ -4493,7 +4940,7 @@ export async function skipRuntimeWorkReceipt(
4493
4940
  lease_expires_at = NULL,
4494
4941
  updated_at = now()
4495
4942
  WHERE k = decode($1, 'hex')
4496
- AND ${workReceiptRunningOwnerPredicateSql({
4943
+ AND ${workReceiptActiveOwnerPredicateSql({
4497
4944
  receiptTable: workReceiptTable(session),
4498
4945
  ownerRunIdSql: '$4',
4499
4946
  ownerRunAttemptSql: '$6',
@@ -5122,6 +5569,13 @@ async function completeRuntimeMapRowChunks(
5122
5569
  '$11::timestamptz',
5123
5570
  '$12',
5124
5571
  );
5572
+ const writableNonTerminalAttemptSql = `(
5573
+ target._status <> 'enriched'
5574
+ AND (
5575
+ ${activeRuntimeSheetAttemptFenceSql('target', '$9::text', '$10::text', '$11::timestamptz', '$12::integer')}
5576
+ OR coalesce(target._attempt_owner_run_id, target._run_id) IS DISTINCT FROM $10::text
5577
+ )
5578
+ )`;
5125
5579
  const sameOwnerTerminalEpochSql = sameOwnerTerminalAttemptEpochSql(
5126
5580
  'target',
5127
5581
  '$10::text',
@@ -5174,6 +5628,7 @@ async function completeRuntimeMapRowChunks(
5174
5628
  AND NOT (${sameOwnerTerminalEpochSql})
5175
5629
  AND (
5176
5630
  ($9::text IS NULL AND target._run_id = $5::text)
5631
+ OR ($9::text IS NOT NULL AND ${writableNonTerminalAttemptSql})
5177
5632
  OR ($9::text IS NOT NULL AND ${activeRuntimeSheetAttemptFenceSql('target', '$9::text', '$10::text', '$11::timestamptz', '$12::integer')})
5178
5633
  OR ($9::text IS NOT NULL AND ${writableEnrichedAttemptSql})
5179
5634
  )
@@ -5208,6 +5663,7 @@ async function completeRuntimeMapRowChunks(
5208
5663
  (${newerTerminalRuntimeSheetRowSql('target', '$11::timestamptz', '$12::integer', '$9::text', '$10::text')})
5209
5664
  OR NOT (
5210
5665
  ($9::text IS NULL AND target._run_id = $5::text)
5666
+ OR ($9::text IS NOT NULL AND ${writableNonTerminalAttemptSql})
5211
5667
  OR ($9::text IS NOT NULL AND ${activeRuntimeSheetAttemptFenceSql('target', '$9::text', '$10::text', '$11::timestamptz', '$12::integer')})
5212
5668
  OR ($9::text IS NOT NULL AND ${writableEnrichedAttemptSql})
5213
5669
  )