deepline 0.3.391 → 0.3.393

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 (154) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +156 -24
  2. package/dist/bundling-sources/sdk/src/compat.ts +9 -0
  3. package/dist/bundling-sources/sdk/src/http.ts +5 -1
  4. package/dist/bundling-sources/sdk/src/index.ts +5 -0
  5. package/dist/bundling-sources/sdk/src/plays/csv-data-input.ts +3 -0
  6. package/dist/bundling-sources/sdk/src/plays/run-request.ts +182 -6
  7. package/dist/bundling-sources/sdk/src/release.ts +4 -1
  8. package/dist/bundling-sources/sdk/src/types.ts +15 -0
  9. package/dist/bundling-sources/shared_libs/integration-contracts/request-warnings.ts +64 -0
  10. package/dist/bundling-sources/shared_libs/play-data-plane/runtime-sheet-storage-contract.ts +315 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/async-operation.ts +41 -8
  12. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +418 -44
  13. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +36 -3
  14. package/dist/bundling-sources/shared_libs/play-runtime/durable-invocation-fence.ts +36 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +11 -3
  16. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +1 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +19 -1
  18. package/dist/bundling-sources/shared_libs/play-runtime/run-identity.ts +26 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +158 -10
  20. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +235 -2
  21. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +92 -58
  22. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +7 -1
  23. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +30 -7
  24. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +1161 -316
  25. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +28 -15
  26. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +24 -3
  27. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +31 -0
  28. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +37 -4
  29. package/dist/bundling-sources/shared_libs/plays/dataset.ts +172 -79
  30. package/dist/bundling-sources/shared_libs/plays/extension-bridge.ts +57 -0
  31. package/dist/bundling-sources/shared_libs/plays/run-summary-types.ts +1 -0
  32. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +173 -1
  33. package/dist/cli/{auth-6QET7NFP.mjs → auth-5PXHA232.mjs} +4 -4
  34. package/dist/cli/auth-P3MVDTXM.js +39 -0
  35. package/dist/cli/{billing-KJAGTCFR.js → billing-JFB2XLSO.js} +101 -74
  36. package/dist/cli/{billing-HAQ2VDQX.mjs → billing-Z2N7ZAGM.mjs} +32 -5
  37. package/dist/cli/{chunk-556BMGEB.js → chunk-2HAD745V.js} +242 -183
  38. package/dist/cli/{chunk-HFTOOZGN.mjs → chunk-2LGC4EFT.mjs} +3 -3
  39. package/dist/cli/{chunk-QCWIORKR.mjs → chunk-4PT2VA3A.mjs} +2 -2
  40. package/dist/cli/{chunk-KA7X75HC.js → chunk-4XX6443T.js} +176 -87
  41. package/dist/cli/{chunk-YBZY3EZF.mjs → chunk-5KWKZIQR.mjs} +4 -4
  42. package/dist/cli/{chunk-VH4B5OYM.mjs → chunk-5PRVMXKB.mjs} +27 -2
  43. package/dist/cli/{chunk-DSWSEHRX.mjs → chunk-6I73X254.mjs} +1 -1
  44. package/dist/cli/{chunk-6L4P7Z6C.mjs → chunk-7HR7BGZL.mjs} +13 -3
  45. package/dist/cli/{chunk-4S6X74A3.js → chunk-AARX5L4B.js} +2 -2
  46. package/dist/cli/{chunk-MHLCMJ2E.js → chunk-CDFH2DYL.js} +20 -19
  47. package/dist/cli/{chunk-PFPP4BKS.mjs → chunk-FQ6VHPW4.mjs} +2 -2
  48. package/dist/cli/{chunk-5JLQPVNX.js → chunk-GSK6NHVM.js} +1 -0
  49. package/dist/cli/{chunk-TLHF54VX.mjs → chunk-HLEESTCH.mjs} +1 -1
  50. package/dist/cli/{chunk-EDWJY6T4.mjs → chunk-HQXL5526.mjs} +1 -0
  51. package/dist/cli/{chunk-Q6EOHNRH.mjs → chunk-JI2B4RZP.mjs} +119 -6
  52. package/dist/cli/{chunk-I3MKXASV.mjs → chunk-KIYU2TGL.mjs} +2 -2
  53. package/dist/cli/{chunk-Y6VOGHF2.mjs → chunk-KZZC33PY.mjs} +0 -181
  54. package/dist/cli/{chunk-XBPLTHGB.js → chunk-L2SYLGRP.js} +8 -8
  55. package/dist/cli/{chunk-K3ODOHOI.mjs → chunk-LMMEAMA7.mjs} +3 -2
  56. package/dist/cli/{chunk-KWF4HJF7.mjs → chunk-LZPCDB7U.mjs} +3 -2
  57. package/dist/cli/{chunk-I7EVBLTW.js → chunk-M7Y7JYZX.js} +32 -7
  58. package/dist/cli/{chunk-QGKPTBRR.js → chunk-MS6GUPBB.js} +3 -3
  59. package/dist/cli/{chunk-ME2DZAHC.js → chunk-OXNOAD6Y.js} +26 -26
  60. package/dist/cli/{chunk-XEV5LMS2.js → chunk-PTDECJGP.js} +50 -50
  61. package/dist/cli/{chunk-5O4B6AUN.js → chunk-PWSWAN7Q.js} +25 -2
  62. package/dist/cli/{chunk-DRZFTRZJ.mjs → chunk-Q26LUSQ2.mjs} +79 -20
  63. package/dist/cli/{chunk-3VN4FTE3.js → chunk-RDXFGPJU.js} +1 -182
  64. package/dist/cli/{chunk-7E66A24V.mjs → chunk-RKMB3N7K.mjs} +25 -2
  65. package/dist/cli/{chunk-WLODHQ7S.js → chunk-RWHWF5ZJ.js} +3 -2
  66. package/dist/cli/{chunk-H3DCZOXR.mjs → chunk-STNYIMIV.mjs} +97 -8
  67. package/dist/cli/chunk-UBKA33KY.mjs +310 -0
  68. package/dist/cli/{chunk-PAIMOGCM.js → chunk-UHNDVD3P.js} +125 -12
  69. package/dist/cli/{chunk-H2ER7QWR.mjs → chunk-UOGZUA24.mjs} +180 -18
  70. package/dist/cli/{chunk-RTH26NF3.js → chunk-VIQJIX2P.js} +73 -63
  71. package/dist/cli/{chunk-ED42XCOW.js → chunk-VIWE6WRK.js} +629 -467
  72. package/dist/cli/{chunk-RA2SDTSX.js → chunk-W4WOMXR3.js} +15 -15
  73. package/dist/cli/chunk-XDVT3GOM.js +310 -0
  74. package/dist/cli/{chunk-UOSKJIXE.js → chunk-Z3OOZM5L.js} +8 -8
  75. package/dist/cli/{csv-V7UHOBFE.mjs → csv-KJWZHKCH.mjs} +3 -3
  76. package/dist/cli/{csv-2WSF5TEM.js → csv-P3Z3YE7H.js} +12 -12
  77. package/dist/cli/{db-TD375E4N.mjs → db-NCM4CQIS.mjs} +6 -5
  78. package/dist/cli/{db-42RL4FZD.js → db-NUZGJNQC.js} +31 -30
  79. package/dist/cli/{describe-GPUKAWOZ.mjs → describe-WT6MM2GO.mjs} +18 -17
  80. package/dist/cli/{describe-7WFCBZUY.js → describe-YGSEN5WC.js} +25 -24
  81. package/dist/cli/{executions-LGVG3FPH.js → executions-PCOLHN7Q.js} +8 -7
  82. package/dist/cli/{executions-M7O63JNA.mjs → executions-XS2IPJ4W.mjs} +6 -5
  83. package/dist/cli/{feedback-PZZA2M4S.js → feedback-4MSH7TFU.js} +7 -7
  84. package/dist/cli/{feedback-6PBSKVB2.mjs → feedback-CCLTS5SV.mjs} +3 -3
  85. package/dist/cli/{git-2VIFM5DI.mjs → git-K2DHPMPC.mjs} +4 -4
  86. package/dist/cli/git-RNM45FX4.js +23 -0
  87. package/dist/cli/index.js +96 -92
  88. package/dist/cli/index.mjs +46 -42
  89. package/dist/cli/{legacy-noops-DPUBZI7N.mjs → legacy-noops-B5S4AR3P.mjs} +4 -4
  90. package/dist/cli/{legacy-noops-IQ6QTZNZ.js → legacy-noops-GPNOPFXV.js} +7 -7
  91. package/dist/cli/{monitors-KBZVQG2U.mjs → monitors-JLMAIPRG.mjs} +6 -5
  92. package/dist/cli/{monitors-6GWAZFYV.js → monitors-ZAWD7NSN.js} +87 -86
  93. package/dist/cli/{org-JKGTHXOZ.js → org-P2KN4CES.js} +66 -65
  94. package/dist/cli/{org-CP3VSJBX.mjs → org-T3JTHP4G.mjs} +6 -5
  95. package/dist/cli/{play-DD2ME4L4.mjs → play-NK6BLRGL.mjs} +12 -11
  96. package/dist/cli/play-PDXR6JFB.js +158 -0
  97. package/dist/cli/{providers-JUNOFKI7.mjs → providers-3NQRA7IT.mjs} +6 -5
  98. package/dist/cli/{providers-P34QYYG6.js → providers-5QRZN3DT.js} +9 -8
  99. package/dist/cli/{quickstart-N6SWOK7E.mjs → quickstart-PIA2JGPV.mjs} +3 -3
  100. package/dist/cli/{quickstart-UL3DEFA4.js → quickstart-R7NUZGNW.js} +5 -5
  101. package/dist/cli/run-identity-G257GQED.js +25 -0
  102. package/dist/cli/run-identity-MRNCEGV2.mjs +25 -0
  103. package/dist/cli/{search-SC7I56HX.mjs → search-AIDIIGLY.mjs} +7 -6
  104. package/dist/cli/search-QRWL7MFM.js +27 -0
  105. package/dist/cli/{secrets-6ZJE7ZPM.js → secrets-BT3PGMWN.js} +15 -14
  106. package/dist/cli/{secrets-RTARNYF5.mjs → secrets-OQNHL45O.mjs} +6 -5
  107. package/dist/cli/{sessions-6IZ2XHGW.js → sessions-EYXWOSWK.js} +5 -5
  108. package/dist/cli/{sessions-7RKV2T3O.mjs → sessions-SF7WJPJP.mjs} +4 -4
  109. package/dist/cli/{settings-YX3XJDXE.js → settings-BA3GHBOD.js} +76 -75
  110. package/dist/cli/{settings-V6PXA6HY.mjs → settings-Y57JTFDD.mjs} +9 -8
  111. package/dist/cli/{setup-V2O4UJNT.mjs → setup-7GR5GBHA.mjs} +6 -6
  112. package/dist/cli/{setup-CQ5VRNNU.js → setup-RERXYC66.js} +41 -41
  113. package/dist/cli/{skills-VHJZWEHA.mjs → skills-SJWIQAV5.mjs} +4 -4
  114. package/dist/cli/skills-V7EY75HB.js +28 -0
  115. package/dist/cli/tools-H5HLBCHH.js +41 -0
  116. package/dist/cli/{tools-OD5WKEWY.mjs → tools-ZF3JXPNL.mjs} +10 -9
  117. package/dist/cli/update-472HA3LT.js +28 -0
  118. package/dist/cli/{update-CRMNOMJ3.mjs → update-FFFYQAOJ.mjs} +6 -6
  119. package/dist/cli/{workflow-XHORSD5T.js → workflow-SZJVQH4T.js} +17 -16
  120. package/dist/cli/{workflow-EVDYNCDD.mjs → workflow-XVFEEGAS.mjs} +7 -6
  121. package/dist/{client-DpzZi4fU.d.mts → client-DU5dtHDy.d.mts} +157 -4
  122. package/dist/{client-BhK9kix9.d.ts → client-mCE88hC4.d.ts} +157 -4
  123. package/dist/{compiler-manifest-CY2v8ab0.d.ts → compiler-manifest-DE4RdAiY.d.mts} +61 -1
  124. package/dist/{compiler-manifest-CY2v8ab0.d.mts → compiler-manifest-DE4RdAiY.d.ts} +61 -1
  125. package/dist/{index-B9fBZH1g.d.mts → index-BI6nWgaV.d.mts} +1 -1
  126. package/dist/{index-BZ6uzZJl.d.ts → index-Bi-f7HFT.d.ts} +1 -1
  127. package/dist/index.d.mts +5 -5
  128. package/dist/index.d.ts +5 -5
  129. package/dist/index.js +120 -29
  130. package/dist/index.mjs +111 -20
  131. package/dist/install-integrity.json +94 -88
  132. package/dist/plays/bundle-play-file.d.mts +3 -3
  133. package/dist/plays/bundle-play-file.d.ts +3 -3
  134. package/dist/plays/bundle-play-file.mjs +2 -2
  135. package/dist/plays/csv-data-input.d.mts +2 -0
  136. package/dist/plays/csv-data-input.d.ts +2 -0
  137. package/dist/plays/csv-data-input.js +2 -2
  138. package/dist/plays/csv-data-input.mjs +1 -1
  139. package/dist/plays/run-request.d.mts +2 -2
  140. package/dist/plays/run-request.d.ts +2 -2
  141. package/dist/plays/run-request.js +5 -2
  142. package/dist/plays/run-request.mjs +4 -1
  143. package/dist/release.d.mts +4 -1
  144. package/dist/release.d.ts +4 -1
  145. package/dist/release.js +2 -2
  146. package/dist/release.mjs +1 -1
  147. package/package.json +1 -1
  148. package/dist/cli/auth-6ZUH42ZA.js +0 -39
  149. package/dist/cli/git-ZSJD3BID.js +0 -23
  150. package/dist/cli/play-G5GVMIBN.js +0 -157
  151. package/dist/cli/search-W4GXWTOM.js +0 -26
  152. package/dist/cli/skills-LBZPJTPI.js +0 -28
  153. package/dist/cli/tools-TQNM473H.js +0 -40
  154. package/dist/cli/update-QY6GAK6B.js +0 -28
@@ -46,6 +46,11 @@ import {
46
46
  } from '../play-data-plane/sheet-column-storage';
47
47
  import {
48
48
  RUNTIME_SHEET_CELL_META_COLUMN,
49
+ RUNTIME_SHEET_RUN_MEMBERSHIP_TABLE,
50
+ RUNTIME_SHEET_RUN_MEMBERSHIP_WRITES_ENABLED,
51
+ appendRuntimeSheetRunRowMembershipBatch,
52
+ isRuntimeSheetRunMembershipStorageAvailable,
53
+ type RuntimeSheetRunRowMembership,
49
54
  runtimeSheetResultFreshnessSql,
50
55
  runtimeSheetResultAdmissionSql,
51
56
  } from '../play-data-plane/runtime-sheet-storage-contract';
@@ -66,6 +71,7 @@ import type {
66
71
  } from './db-session';
67
72
  import {
68
73
  derivePlayRowIdentity,
74
+ stableStringify,
69
75
  normalizePlayNameForSheet,
70
76
  normalizeTableNamespace,
71
77
  } from '../plays/row-identity';
@@ -503,6 +509,8 @@ export type PrepareRuntimeSheetResult = {
503
509
  attemptExpiresAt?: string;
504
510
  attemptSeq?: number;
505
511
  writeVersion?: number;
512
+ /** Optional reader-first capture capability; absent retains legacy behavior. */
513
+ membershipCapture?: boolean;
506
514
  };
507
515
 
508
516
  export type RuntimeSheetTiming = {
@@ -645,6 +653,10 @@ type RuntimeApiActionRequest =
645
653
  action: keyof typeof RUNTIME_SHEET_ADMISSION_CAPABILITIES;
646
654
  input: Parameters<typeof startRuntimeSheetDataset>[1];
647
655
  }
656
+ | {
657
+ action: 'runtime_sheet_finish_membership';
658
+ input: Parameters<typeof finishRuntimeSheetRunMembership>[1];
659
+ }
648
660
  | {
649
661
  action: 'runtime_sheet_complete_map_rows';
650
662
  input: Parameters<typeof completeRuntimeMapRows>[1];
@@ -2872,6 +2884,151 @@ async function registerRuntimeDataset(
2872
2884
  );
2873
2885
  }
2874
2886
 
2887
+ async function runtimeSheetMembershipCaptureAvailable(
2888
+ client: RuntimeQueryClient,
2889
+ session: RuntimePostgresSession,
2890
+ ): Promise<boolean> {
2891
+ if (!RUNTIME_SHEET_RUN_MEMBERSHIP_WRITES_ENABLED) return false;
2892
+ return isRuntimeSheetRunMembershipStorageAvailable(
2893
+ async <Row>(sql: string, params?: readonly unknown[]) => {
2894
+ const result = await client.query(sql, params ? [...params] : undefined);
2895
+ return { rows: result.rows as Row[] };
2896
+ },
2897
+ session.postgres.schema,
2898
+ true,
2899
+ );
2900
+ }
2901
+
2902
+ async function persistRuntimeSheetMembershipBatch(
2903
+ client: RuntimeQueryClient,
2904
+ session: RuntimePostgresSession,
2905
+ rows: Array<Omit<RuntimeSheetRunRowMembership, 'recordedAt'>>,
2906
+ ) {
2907
+ await appendRuntimeSheetRunRowMembershipBatch(
2908
+ async <Row>(sql: string, params?: readonly unknown[]) => {
2909
+ const result = await client.query(sql, params ? [...params] : undefined);
2910
+ return { rows: result.rows as Row[] };
2911
+ },
2912
+ rows,
2913
+ session.postgres.schema,
2914
+ );
2915
+ }
2916
+
2917
+ async function captureRuntimeSheetTerminalMembership(
2918
+ client: RuntimeQueryClient,
2919
+ session: RuntimePostgresSession,
2920
+ input: {
2921
+ runId: string;
2922
+ tableNamespace: string;
2923
+ sheetContract: PlaySheetContract;
2924
+ keys: string[];
2925
+ },
2926
+ ): Promise<void> {
2927
+ if (input.keys.length === 0) return;
2928
+ const datasetId = createRuntimeDatasetId(
2929
+ session.playName,
2930
+ input.tableNamespace,
2931
+ );
2932
+ // Read canonical values AFTER the fenced mutation, inside its transaction.
2933
+ // A stale submitted body must never become a fabricated historical result.
2934
+ const result = await client.query<Record<string, unknown>>(
2935
+ `SELECT target.*, admission.input_digest AS membership_input_digest, admission.source_ref AS membership_source_ref
2936
+ FROM ${sheetTable(session)} target
2937
+ JOIN ${fqRuntimeTable(session, RUNTIME_SHEET_RUN_MEMBERSHIP_TABLE)} admission
2938
+ ON admission.run_id = $1 AND admission.dataset_id = $2 AND admission.row_key = target._key AND admission.phase = 'admitted'
2939
+ WHERE target._key = ANY($3::text[]) AND target._run_id = $1 AND target._status IN ('enriched', 'failed')`,
2940
+ [input.runId, datasetId, input.keys],
2941
+ );
2942
+ const snapshots = result.rows.map((raw) => {
2943
+ const attemptSeq =
2944
+ raw._attempt_seq == null ? null : Number(raw._attempt_seq);
2945
+ const rowVersion = Number(raw._version);
2946
+ const data = mapRuntimePostgresRow({
2947
+ raw,
2948
+ sheetContract: input.sheetContract,
2949
+ }).data;
2950
+ delete data[DEEPLINE_CELL_META_FIELD];
2951
+ return {
2952
+ runId: input.runId,
2953
+ datasetId,
2954
+ rowKey: String(raw._key),
2955
+ phase: 'terminal' as const,
2956
+ generation: `attempt:${attemptSeq ?? 'legacy'}:version:${rowVersion}`,
2957
+ attemptSeq,
2958
+ rowVersion,
2959
+ inputIndex: raw._input_index == null ? null : Number(raw._input_index),
2960
+ inputDigest: String(raw.membership_input_digest),
2961
+ status: raw._status as 'enriched' | 'failed',
2962
+ data,
2963
+ source: {
2964
+ ...(raw.membership_source_ref as Record<string, unknown>),
2965
+ ...(raw._error == null ? {} : { rowError: String(raw._error) }),
2966
+ },
2967
+ };
2968
+ });
2969
+ await persistRuntimeSheetMembershipBatch(client, session, snapshots);
2970
+ }
2971
+
2972
+ /** Named completion is called only after admission advertised capture. */
2973
+ export async function finishRuntimeSheetRunMembership(
2974
+ context: RuntimeApiContext & { playName: string },
2975
+ input: {
2976
+ runId: string;
2977
+ tableNamespace: string;
2978
+ sheetContract: PlaySheetContract;
2979
+ expectedRows: number;
2980
+ },
2981
+ ): Promise<{ captured: boolean; complete: boolean }> {
2982
+ if (!Number.isSafeInteger(input.expectedRows) || input.expectedRows < 0)
2983
+ throw new Error('Membership expectedRows must be a non-negative integer.');
2984
+ if (context.dbSessionStrategy === 'gateway_only')
2985
+ return postRuntimeApi(context, {
2986
+ action: 'runtime_sheet_finish_membership',
2987
+ input,
2988
+ });
2989
+ const session = requireRuntimePostgresSession(
2990
+ await getRuntimeDbSession(context, {
2991
+ tableNamespace: input.tableNamespace,
2992
+ logicalTable: 'sheet_rows',
2993
+ operations: ['rows.read', 'rows.upsert'],
2994
+ limits: {},
2995
+ }),
2996
+ );
2997
+ return withRuntimeSheetQueryClient(
2998
+ context,
2999
+ session,
3000
+ {
3001
+ playName: context.playName,
3002
+ tableNamespace: input.tableNamespace,
3003
+ sheetContract: input.sheetContract,
3004
+ transactional: true,
3005
+ },
3006
+ async (client) => {
3007
+ if (!(await runtimeSheetMembershipCaptureAvailable(client, session)))
3008
+ return { captured: false, complete: false };
3009
+ const datasetId = createRuntimeDatasetId(
3010
+ session.playName,
3011
+ input.tableNamespace,
3012
+ );
3013
+ const result = await client.query<{ complete: boolean }>(
3014
+ `UPDATE ${runDatasetCatalogTable(session)} catalog
3015
+ SET membership_expected_rows = $3,
3016
+ membership_completed_at = CASE WHEN
3017
+ (SELECT count(*) FROM ${fqRuntimeTable(session, RUNTIME_SHEET_RUN_MEMBERSHIP_TABLE)} WHERE run_id = $1 AND dataset_id = $2 AND phase = 'admitted') = $3
3018
+ AND (SELECT count(DISTINCT row_key) FROM ${fqRuntimeTable(session, RUNTIME_SHEET_RUN_MEMBERSHIP_TABLE)} WHERE run_id = $1 AND dataset_id = $2 AND phase = 'terminal') = $3
3019
+ THEN coalesce(catalog.membership_completed_at, now()) ELSE NULL END
3020
+ WHERE run_id = $1 AND dataset_id = $2 AND membership_started_at IS NOT NULL
3021
+ RETURNING membership_completed_at IS NOT NULL AS complete`,
3022
+ [input.runId, datasetId, input.expectedRows],
3023
+ );
3024
+ return {
3025
+ captured: result.rows.length === 1,
3026
+ complete: result.rows[0]?.complete === true,
3027
+ };
3028
+ },
3029
+ );
3030
+ }
3031
+
2875
3032
  function workReceiptTable(session: RuntimePostgresSession): string {
2876
3033
  return fqRuntimeTable(
2877
3034
  session,
@@ -6877,7 +7034,11 @@ export async function startRuntimeSheetDataset(
6877
7034
  const normalizeStartedAt = Date.now();
6878
7035
  const uniqueRows = new Map<
6879
7036
  string,
6880
- { row: Record<string, unknown>; inputIndex: number | null }
7037
+ {
7038
+ row: Record<string, unknown>;
7039
+ inputIndex: number | null;
7040
+ source: Record<string, unknown>;
7041
+ }
6881
7042
  >();
6882
7043
  for (const row of input.rows) {
6883
7044
  // Materializes projected CSV aliases as visible cells and drops internal
@@ -6890,6 +7051,13 @@ export async function startRuntimeSheetDataset(
6890
7051
  if (key && !uniqueRows.has(key)) {
6891
7052
  uniqueRows.set(key, {
6892
7053
  row: sanitizePostgresJsonValue(cleanedRow),
7054
+ source:
7055
+ row.__deeplineSourceRow && typeof row.__deeplineSourceRow === 'object'
7056
+ ? (sanitizePostgresJsonValue(row.__deeplineSourceRow) as Record<
7057
+ string,
7058
+ unknown
7059
+ >)
7060
+ : {},
6893
7061
  inputIndex: normalizeRuntimeMapInputIndex(
6894
7062
  row[MAP_ROW_OUTCOME_RUNTIME_FIELDS.inputIndex],
6895
7063
  ),
@@ -6901,6 +7069,7 @@ export async function startRuntimeSheetDataset(
6901
7069
  ([key, entry], inputIndex) => ({
6902
7070
  key,
6903
7071
  row: entry.row,
7072
+ source: entry.source,
6904
7073
  inputIndex: entry.inputIndex ?? inputOffset + inputIndex,
6905
7074
  }),
6906
7075
  );
@@ -7135,6 +7304,60 @@ export async function startRuntimeSheetDataset(
7135
7304
  force: input.force === true,
7136
7305
  ...prepared,
7137
7306
  });
7307
+ const membershipCapture = await runtimeSheetMembershipCaptureAvailable(
7308
+ client,
7309
+ session,
7310
+ );
7311
+ if (membershipCapture) {
7312
+ const dispositions = new Set(
7313
+ prepared.rowDispositions
7314
+ .filter(
7315
+ (row) =>
7316
+ row.disposition === 'pending' ||
7317
+ row.disposition === 'completed',
7318
+ )
7319
+ .map((row) => row.key),
7320
+ );
7321
+ const admitted = rowEntries.filter((entry) =>
7322
+ dispositions.has(entry.key),
7323
+ );
7324
+ await client.query(
7325
+ `UPDATE ${runDatasetCatalogTable(session)} SET membership_started_at = coalesce(membership_started_at, now()) WHERE run_id = $1 AND dataset_id = $2`,
7326
+ [
7327
+ input.runId,
7328
+ createRuntimeDatasetId(session.playName, input.tableNamespace),
7329
+ ],
7330
+ );
7331
+ await persistRuntimeSheetMembershipBatch(
7332
+ client,
7333
+ session,
7334
+ admitted.map((entry) => ({
7335
+ runId: input.runId,
7336
+ datasetId: createRuntimeDatasetId(
7337
+ session.playName,
7338
+ input.tableNamespace,
7339
+ ),
7340
+ rowKey: entry.key,
7341
+ phase: 'admitted',
7342
+ generation: 'admitted',
7343
+ attemptSeq: null,
7344
+ rowVersion: null,
7345
+ inputIndex: entry.inputIndex,
7346
+ inputDigest: createHash('sha256')
7347
+ .update(stableStringify(entry.row))
7348
+ .digest('hex'),
7349
+ status: 'pending',
7350
+ data: entry.row,
7351
+ source: entry.source,
7352
+ })),
7353
+ );
7354
+ await captureRuntimeSheetTerminalMembership(client, session, {
7355
+ runId: input.runId,
7356
+ tableNamespace: input.tableNamespace,
7357
+ sheetContract: input.sheetContract,
7358
+ keys: admitted.map((entry) => entry.key),
7359
+ });
7360
+ }
7138
7361
  timings.push({
7139
7362
  phase: 'build_result',
7140
7363
  ms: Date.now() - buildStartedAt,
@@ -7146,6 +7369,7 @@ export async function startRuntimeSheetDataset(
7146
7369
  });
7147
7370
  return {
7148
7371
  ...built,
7372
+ membershipCapture,
7149
7373
  attemptExpiresAt,
7150
7374
  attemptSeq,
7151
7375
  writeVersion,
@@ -8982,11 +9206,20 @@ export async function completeRuntimeMapRows(
8982
9206
  }),
8983
9207
  )
8984
9208
  : { updated: 0, fencedRows: [] };
8985
- return classifyRuntimeMapRowsWrite({
9209
+ const classified = classifyRuntimeMapRowsWrite({
8986
9210
  submittedKeys: uniqueRows.keys(),
8987
9211
  updated: completed.updated + failed.updated,
8988
9212
  fencedRows: [...completed.fencedRows, ...failed.fencedRows],
8989
9213
  });
9214
+ if (await runtimeSheetMembershipCaptureAvailable(client, session)) {
9215
+ await captureRuntimeSheetTerminalMembership(client, session, {
9216
+ runId: input.runId,
9217
+ tableNamespace: input.tableNamespace,
9218
+ sheetContract,
9219
+ keys: classified.committedKeys,
9220
+ });
9221
+ }
9222
+ return classified;
8990
9223
  },
8991
9224
  )
8992
9225
  .then((result) => {
@@ -178,6 +178,9 @@ import {
178
178
  readPostgresSchedulerRunnerLiveness,
179
179
  readPostgresSchedulerRuntimeResourcesForRun,
180
180
  isPostgresSchedulerRunnerProgressAttemptCurrent,
181
+ appendPostgresRunLedgerEvents,
182
+ readPostgresSchedulerRunnerTerminalReceivedAtMs,
183
+ claimPostgresSchedulerWaitingSuspensionAttemptForFailure,
181
184
  requestComputeBillingDispatchToken,
182
185
  blockPostgresSchedulerCronSchedule,
183
186
  PostgresSchedulerCronScheduleConfigurationError,
@@ -233,6 +236,7 @@ import {
233
236
  import { runWithAbsurdSchedulerQuerySignal } from './absurd-pool';
234
237
  import {
235
238
  buildFailedTerminalSettledProgressEvents,
239
+ collectGatewayRunnerTerminalProgressLedgerEvents,
236
240
  createPostgresSchedulerProgressReporter,
237
241
  type PostgresSchedulerProgressReporter,
238
242
  } from './postgres-progress';
@@ -3261,11 +3265,57 @@ async function executeAssignedAbsurdAttempt(input: {
3261
3265
  terminalLogProducerAttempt: input.terminalLogProducerAttempt,
3262
3266
  }),
3263
3267
  });
3264
- // A real suspension closes an invocation's counter segment before its
3265
- // wait or handoff. Detached terminal recovery can replay the retained
3266
- // result and its idempotent final segment after a crash. A detached_runner
3267
- // placeholder still owns a live runner; preserve its park-first recovery.
3268
- if (suspension.kind !== 'detached_runner') {
3268
+ const retainedDetachedSuspension =
3269
+ input.fromDetachedRunner && suspension.kind !== 'detached_runner';
3270
+ let finalProgressRetained = false;
3271
+ if (retainedDetachedSuspension) {
3272
+ await runWithAbsurdExecutionLeaseHeartbeat({
3273
+ ctx,
3274
+ runId,
3275
+ operation: () =>
3276
+ deps.runWithClient(async (client) => {
3277
+ const runnerAttempt =
3278
+ input.terminalLogProducerAttempt ?? generation;
3279
+ const acceptedAt =
3280
+ await readPostgresSchedulerRunnerTerminalReceivedAtMs(
3281
+ client,
3282
+ { runId, attempt: runnerAttempt },
3283
+ options,
3284
+ );
3285
+ if (acceptedAt !== null) {
3286
+ const events =
3287
+ await collectGatewayRunnerTerminalProgressLedgerEvents({
3288
+ launch,
3289
+ attempt: runnerAttempt,
3290
+ result: input.runnerResult,
3291
+ events: [],
3292
+ liveLogOffset: 0,
3293
+ suspensionAcceptedAtMs: acceptedAt,
3294
+ });
3295
+ if (events.length > 0) {
3296
+ await appendPostgresRunLedgerEvents(
3297
+ client,
3298
+ { orgId: launch.orgId, runId, events },
3299
+ options,
3300
+ );
3301
+ }
3302
+ finalProgressRetained = true;
3303
+ }
3304
+ // The pushed result proves this exact attempt's runner exited.
3305
+ // Persist its provider-scoped obligation before publishing a wait
3306
+ // that can outlive this worker. Only the cleanup lease deletes it.
3307
+ await enqueuePostgresSchedulerSandboxCleanupForAttempt(
3308
+ client,
3309
+ { runId, attempt: runnerAttempt },
3310
+ options,
3311
+ );
3312
+ }),
3313
+ });
3314
+ }
3315
+ // Local and historical results still require final publication before
3316
+ // advancing their checkpoint. Otherwise all final facts and cleanup are
3317
+ // durable, so progress transport cannot hide the typed waiting state.
3318
+ if (suspension.kind !== 'detached_runner' && !finalProgressRetained) {
3269
3319
  await finalizeSuspensionProgress();
3270
3320
  }
3271
3321
  const outcome = await createPlayRunAttemptSuspensionJob({
@@ -3284,6 +3334,9 @@ async function executeAssignedAbsurdAttempt(input: {
3284
3334
  options,
3285
3335
  transition: 'waiting',
3286
3336
  });
3337
+ if (finalProgressRetained) {
3338
+ await finalizeSuspensionProgress();
3339
+ }
3287
3340
  },
3288
3341
  resumed: async (lifecycle) =>
3289
3342
  await projectLifecycleOutbox({
@@ -3695,9 +3748,6 @@ async function executeAssignedAbsurdAttempt(input: {
3695
3748
  ),
3696
3749
  ),
3697
3750
  inspectDetachedRunner: deps.inspectDetachedRunner,
3698
- deleteRuntimeSandbox: deps.deleteRuntimeSandbox,
3699
- deleteRuntimeSandboxRef: deps.deleteRuntimeSandboxRef,
3700
- sandboxGcTimeoutMs: deps.sandboxGcTimeoutMs,
3701
3751
  }),
3702
3752
  });
3703
3753
  } catch (error) {
@@ -3800,9 +3850,6 @@ async function executeAssignedAbsurdAttempt(input: {
3800
3850
  inspectDetachedRunner: deps.inspectDetachedRunner,
3801
3851
  readDetachedRunnerRuntimeCompletion:
3802
3852
  deps.readDetachedRunnerRuntimeCompletion,
3803
- deleteRuntimeSandbox: deps.deleteRuntimeSandbox,
3804
- deleteRuntimeSandboxRef: deps.deleteRuntimeSandboxRef,
3805
- sandboxGcTimeoutMs: deps.sandboxGcTimeoutMs,
3806
3853
  });
3807
3854
  } else {
3808
3855
  if (backendExecutions > 0) {
@@ -4824,10 +4871,9 @@ export async function enforceRuntimeTrafficPolicyBeforeSandboxStart(input: {
4824
4871
  * terminal transaction;
4825
4872
  * - thrown outcomes -> `releaseAbsurdAttemptRuntimeState` in the caller's
4826
4873
  * catch (B4/FIX 3);
4827
- * - SUSPENDED results -> HERE, immediately: the runner already exited, and
4828
- * the follow-up play park (sleep/HITL) can last hours — waiting for
4829
- * the eventual terminal sweep would leak a dead sandbox for that whole
4830
- * window.
4874
+ * - suspended results -> the existing durable cleanup controller, after
4875
+ * `resolveRunnerSuspension` persists its obligation before typed waiting.
4876
+ * Provider deletion never delays publishing the user's wait.
4831
4877
  */
4832
4878
  async function resolveDetachedRunnerOutcome(input: {
4833
4879
  runId: string;
@@ -4845,9 +4891,6 @@ async function resolveDetachedRunnerOutcome(input: {
4845
4891
  } | null>;
4846
4892
  inspectDetachedRunner?: AbsurdRuntimeWorkerDeps['inspectDetachedRunner'];
4847
4893
  readDetachedRunnerRuntimeCompletion?: AbsurdRuntimeWorkerDeps['readDetachedRunnerRuntimeCompletion'];
4848
- deleteRuntimeSandbox: AbsurdRuntimeWorkerDeps['deleteRuntimeSandbox'];
4849
- deleteRuntimeSandboxRef?: AbsurdRuntimeWorkerDeps['deleteRuntimeSandboxRef'];
4850
- sandboxGcTimeoutMs?: number;
4851
4894
  }): Promise<PlayRunnerResult> {
4852
4895
  const { suspension } = input;
4853
4896
  const runtimeLimitSeconds =
@@ -4873,40 +4916,10 @@ async function resolveDetachedRunnerOutcome(input: {
4873
4916
  }),
4874
4917
  );
4875
4918
  };
4876
- const gcSandboxIfSuspended = async (result: PlayRunnerResult) => {
4877
- if (result.status !== 'suspended') return;
4878
- try {
4879
- const deleteResult = await deleteRuntimeSandboxBounded<
4880
- boolean | RuntimeSandboxDeleteOutcome
4881
- >({
4882
- deleteRuntimeSandbox: input.deleteRuntimeSandboxRef
4883
- ? () =>
4884
- input.deleteRuntimeSandboxRef!(
4885
- runtimeSandboxRefFromSuspension(suspension),
4886
- )
4887
- : input.deleteRuntimeSandbox,
4888
- sandboxId: suspension.sandboxId,
4889
- timeoutMs: input.sandboxGcTimeoutMs ?? ABSURD_SANDBOX_GC_TIMEOUT_MS,
4890
- });
4891
- requireRuntimeSandboxDeleted(
4892
- runtimeSandboxRefFromSuspension(suspension),
4893
- deleteResult,
4894
- );
4895
- } catch (error) {
4896
- console.warn('[absurd-runtime.worker] detached_sandbox_gc_failed', {
4897
- runId: input.runId,
4898
- runnerAttempt: suspension.runnerAttempt,
4899
- sandboxId: suspension.sandboxId,
4900
- stage: 'suspended_result',
4901
- error: error instanceof Error ? error.message : String(error),
4902
- });
4903
- }
4904
- };
4905
4919
  const settle = async (
4906
4920
  result: PlayRunnerResult,
4907
4921
  ): Promise<PlayRunnerResult> => {
4908
4922
  await recordWorkCompletion(result);
4909
- await gcSandboxIfSuspended(result);
4910
4923
  return result;
4911
4924
  };
4912
4925
  const settleFirstWriteWinner = async (
@@ -5548,6 +5561,7 @@ async function salvageAbsurdFinalAttemptTerminal(input: {
5548
5561
  ) {
5549
5562
  try {
5550
5563
  result = await input.deps.runWithClient(async (client) => {
5564
+ let protectWaitingSuspensionFailure = false;
5551
5565
  // A detached-runner wake keeps its attempt `waiting` until setup
5552
5566
  // succeeds. Take that same fence to `running` (the overdue-ceiling
5553
5567
  // claim's transition) so the failure terminal can be written. When
@@ -5555,22 +5569,42 @@ async function salvageAbsurdFinalAttemptTerminal(input: {
5555
5569
  // fence or pushed terminal owns it — the ordinary fenced path below
5556
5570
  // decides.
5557
5571
  if (input.detachedRecovery) {
5558
- await claimPostgresSchedulerWaitingDetachedAttemptForTerminal(
5559
- client,
5560
- {
5561
- runId: input.runId,
5562
- attempt: input.generation,
5563
- leaseToken: input.leaseToken,
5564
- },
5565
- input.options,
5566
- );
5572
+ const detachedClaimed =
5573
+ await claimPostgresSchedulerWaitingDetachedAttemptForTerminal(
5574
+ client,
5575
+ {
5576
+ runId: input.runId,
5577
+ attempt: input.generation,
5578
+ leaseToken: input.leaseToken,
5579
+ },
5580
+ input.options,
5581
+ );
5582
+ if (!detachedClaimed) {
5583
+ const preparation =
5584
+ await claimPostgresSchedulerWaitingSuspensionAttemptForFailure(
5585
+ client,
5586
+ {
5587
+ runId: input.runId,
5588
+ attempt: input.generation,
5589
+ leaseToken: input.leaseToken,
5590
+ },
5591
+ input.options,
5592
+ );
5593
+ if (preparation === 'fence_lost')
5594
+ return { status: 'idle' as const };
5595
+ protectWaitingSuspensionFailure = preparation === 'prepared';
5596
+ }
5567
5597
  }
5568
5598
  return await executePostgresSchedulerClaim(
5569
5599
  client,
5570
5600
  claim,
5571
5601
  async () => failedResult,
5572
5602
  input.detachedRecovery
5573
- ? { ...input.options, preserveRunnerTerminal: true }
5603
+ ? {
5604
+ ...input.options,
5605
+ preserveRunnerTerminal: true,
5606
+ protectWaitingSuspensionFailure,
5607
+ }
5574
5608
  : input.options,
5575
5609
  );
5576
5610
  });
@@ -1086,7 +1086,13 @@ export async function withRuntimeSchedulerClient<T>(
1086
1086
  try {
1087
1087
  const queryResult = await poolClient.query(sql, params);
1088
1088
  throwIfSchedulerAborted(signal);
1089
- return { rows: queryResult.rows };
1089
+ // Simple-protocol transactions return one result per command.
1090
+ // Keep the same rows contract for callers of either protocol.
1091
+ return {
1092
+ rows: Array.isArray(queryResult)
1093
+ ? queryResult.flatMap((result) => result.rows)
1094
+ : queryResult.rows,
1095
+ };
1090
1096
  } catch (error) {
1091
1097
  // node-postgres' query timeout and an AbortSignal reject the
1092
1098
  // promise, but the server may still be processing the SQL.
@@ -39,6 +39,7 @@ import { runtimeWorkCountersFromStats } from '../runtime-work-counters';
39
39
  import {
40
40
  buildPlayRunLedgerEventsFromStatusPatch,
41
41
  createEmptyPlayRunLedgerSnapshot,
42
+ buildTerminalLogReplayEvents,
42
43
  type PlayMapRowActivitySource,
43
44
  type PlayRunLedgerEvent,
44
45
  } from '../run-ledger';
@@ -1902,8 +1903,9 @@ export type DirectWorkerProgressReporter = PostgresSchedulerProgressReporter;
1902
1903
  * network boundary. The returned events are stored in the same Postgres
1903
1904
  * terminal outbox row as `run.completed`, so the projector can append the
1904
1905
  * final step/log/activity facts and the terminal fact in one ordered Convex
1905
- * mutation. This is the completed-run fast path; failed/suspended runs retain
1906
- * the compatibility projection until their parked worker resumes.
1906
+ * mutation. A worker can also retain a suspended runner's complete final
1907
+ * progress before advancing its checkpoint, using the immutable acceptance
1908
+ * timestamp for the same outbox identity across crash or response-loss replay.
1907
1909
  */
1908
1910
  export async function collectGatewayRunnerTerminalProgressLedgerEvents(input: {
1909
1911
  launch: PlaySchedulerSubmitInput;
@@ -1911,14 +1913,17 @@ export async function collectGatewayRunnerTerminalProgressLedgerEvents(input: {
1911
1913
  result: PlayRunnerResult;
1912
1914
  events: unknown[];
1913
1915
  liveLogOffset: number;
1916
+ suspensionAcceptedAtMs?: number;
1914
1917
  }): Promise<PlayRunLedgerEvent[]> {
1915
1918
  const captured: { update?: RuntimeStatusUpdate } = {};
1916
- const reporter = createDirectWorkerProgressReporter({
1919
+ const retainedSuspension = input.suspensionAcceptedAtMs !== undefined;
1920
+ const reporter = createPostgresSchedulerProgressReporter({
1917
1921
  launch: input.launch,
1918
1922
  producerAttempt: input.attempt,
1919
1923
  initialLiveLogTotalCount: input.liveLogOffset,
1920
1924
  isCurrentAttempt: () => true,
1921
1925
  attemptAlreadyVerified: true,
1926
+ durableLogMode: retainedSuspension ? 'progress_reporter' : 'direct_worker',
1922
1927
  publishStatusUpdate: async (update) => {
1923
1928
  captured.update = update;
1924
1929
  },
@@ -1929,6 +1934,7 @@ export async function collectGatewayRunnerTerminalProgressLedgerEvents(input: {
1929
1934
  input.launch.contractSnapshot,
1930
1935
  );
1931
1936
  if (
1937
+ !retainedSuspension &&
1932
1938
  pipeline?.docflow?.nodes.some(
1933
1939
  (node) => node.kind === 'decision' && !node.sketch,
1934
1940
  )
@@ -1977,11 +1983,14 @@ export async function collectGatewayRunnerTerminalProgressLedgerEvents(input: {
1977
1983
  await reporter.finalize(input.result, {
1978
1984
  status: 'running',
1979
1985
  required: true,
1980
- skipTerminalLogReplay: true,
1986
+ skipTerminalLogReplay: !retainedSuspension,
1987
+ replayAllTerminalLogs: retainedSuspension,
1981
1988
  terminalLogProducerAttempt: input.attempt,
1982
1989
  });
1983
1990
  const finalUpdate = captured.update;
1984
1991
  if (!finalUpdate) return [];
1992
+ const checkpointAt =
1993
+ input.suspensionAcceptedAtMs ?? finalUpdate.lastCheckpointAt;
1985
1994
 
1986
1995
  const liveLogs = finalUpdate.liveLogs ?? [];
1987
1996
  const liveLogTotalCount =
@@ -1990,7 +1999,7 @@ export async function collectGatewayRunnerTerminalProgressLedgerEvents(input: {
1990
1999
  patch: {
1991
2000
  runId: input.launch.runId,
1992
2001
  status: 'running',
1993
- lastCheckpointAt: finalUpdate.lastCheckpointAt,
2002
+ lastCheckpointAt: checkpointAt,
1994
2003
  liveNodeProgress: finalUpdate.liveNodeProgress,
1995
2004
  liveLogs,
1996
2005
  liveLogContexts: finalUpdate.liveLogContexts,
@@ -2011,7 +2020,7 @@ export async function collectGatewayRunnerTerminalProgressLedgerEvents(input: {
2011
2020
  type: RUN_EVENT_KIND.workCounters,
2012
2021
  runId: input.launch.runId,
2013
2022
  source: 'worker',
2014
- occurredAt: finalUpdate.lastCheckpointAt ?? Date.now(),
2023
+ occurredAt: checkpointAt ?? Date.now(),
2015
2024
  ...finalUpdate.workCounters,
2016
2025
  });
2017
2026
  }
@@ -2045,5 +2054,19 @@ export async function collectGatewayRunnerTerminalProgressLedgerEvents(input: {
2045
2054
  occurredAt: observation.observedAt,
2046
2055
  observation,
2047
2056
  }));
2048
- return [...progressEvents, ...datasetEvents, ...activityEvents];
2057
+ const replay = finalUpdate.terminalLogReplay;
2058
+ const terminalLogEvents = replay
2059
+ ? buildTerminalLogReplayEvents({
2060
+ runId: input.launch.runId,
2061
+ ...replay,
2062
+ occurredAt: checkpointAt ?? undefined,
2063
+ liveLogTotalCount: replay.totalCount,
2064
+ })
2065
+ : [];
2066
+ return [
2067
+ ...progressEvents,
2068
+ ...datasetEvents,
2069
+ ...activityEvents,
2070
+ ...terminalLogEvents,
2071
+ ];
2049
2072
  }