deepline 0.3.201 → 0.3.203

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 (41) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +62 -1
  2. package/dist/bundling-sources/sdk/src/http.ts +55 -6
  3. package/dist/bundling-sources/sdk/src/index.ts +1 -0
  4. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  5. package/dist/bundling-sources/sdk/src/types.ts +8 -0
  6. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +4 -2
  7. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +460 -96
  8. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +11 -1
  9. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts +6 -2
  10. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +401 -2
  11. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +458 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/provider-no-progress.ts +40 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +7 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +25 -3
  15. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +1 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +119 -171
  17. package/dist/bundling-sources/shared_libs/play-runtime/runtime-work-counters.ts +37 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +238 -204
  19. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +16 -7
  20. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +136 -25
  21. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +21 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +64 -15
  23. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +5 -5
  24. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/result-receipt-policy.ts +46 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +14 -5
  27. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +5 -1
  28. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +3 -9
  29. package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +1 -1
  30. package/dist/cli/index.js +362 -55
  31. package/dist/cli/index.mjs +365 -57
  32. package/dist/index.d.mts +129 -1
  33. package/dist/index.d.ts +129 -1
  34. package/dist/index.js +64 -7
  35. package/dist/index.mjs +64 -7
  36. package/dist/install-integrity.json +3 -0
  37. package/dist/release.d.mts +1 -1
  38. package/dist/release.d.ts +1 -1
  39. package/dist/release.js +1 -1
  40. package/dist/release.mjs +1 -1
  41. package/package.json +1 -1
@@ -390,6 +390,8 @@ export type LedgerTerminalResultRef = {
390
390
  * fetching the complete scheduler result.
391
391
  */
392
392
  preview?: BoundedRunListOutputPreview;
393
+ /** Default detail preview derived while the exact terminal return is in memory. */
394
+ responsePreview?: LedgerStoredRunOutputPreview;
393
395
  /** Failure ownership retained when the full failed result is out of line. */
394
396
  failure?: LedgerTerminalFailureSummary;
395
397
  };
@@ -471,6 +473,452 @@ export type BoundedRunListOutputPreview = {
471
473
  truncated: boolean;
472
474
  };
473
475
 
476
+ export const RUN_RESPONSE_PREVIEW_LIMITS = {
477
+ maxRows: 5,
478
+ maxBytes: 8_000,
479
+ maxDepth: 4,
480
+ maxStringLength: 300,
481
+ } as const;
482
+
483
+ function jsonSize(value: unknown): number {
484
+ return JSON.stringify(value).length;
485
+ }
486
+
487
+ export function buildRunDatasetPreview(
488
+ rows: unknown[],
489
+ totalRows: number | undefined,
490
+ ) {
491
+ const previewRows: unknown[] = [];
492
+ let truncated = rows.length > RUN_RESPONSE_PREVIEW_LIMITS.maxRows;
493
+ for (const row of rows.slice(0, RUN_RESPONSE_PREVIEW_LIMITS.maxRows)) {
494
+ const compacted = compactRunOutputPreviewValue(row, 0);
495
+ truncated ||= compacted.truncated;
496
+ const nextRows = [...previewRows, compacted.value];
497
+ if (jsonSize(nextRows) > RUN_RESPONSE_PREVIEW_LIMITS.maxBytes) {
498
+ truncated = true;
499
+ break;
500
+ }
501
+ previewRows.push(compacted.value);
502
+ }
503
+ if (typeof totalRows === 'number' && totalRows > previewRows.length) {
504
+ truncated = true;
505
+ }
506
+ return {
507
+ rows: previewRows,
508
+ ...(typeof totalRows === 'number' ? { totalRows } : {}),
509
+ truncated,
510
+ limits: RUN_RESPONSE_PREVIEW_LIMITS,
511
+ };
512
+ }
513
+
514
+ const RUN_OUTPUT_VALUE_PREVIEW_LIMITS = {
515
+ maxDepth: 4,
516
+ maxStringLength: 300,
517
+ } as const;
518
+
519
+ export type BoundedRunOutputValuePreview = {
520
+ kind: 'value';
521
+ path: string;
522
+ value: unknown;
523
+ truncated?: true;
524
+ totalItems?: number;
525
+ };
526
+
527
+ export type BoundedRunDatasetOutputPreview = {
528
+ kind: 'dataset';
529
+ path: string;
530
+ dataset: string;
531
+ datasetId?: string;
532
+ tableNamespace?: string;
533
+ rowCount?: number;
534
+ datasetKind?: string;
535
+ runtimeSheetBacked?: true;
536
+ preview?: ReturnType<typeof buildRunDatasetPreview>;
537
+ access?: Record<string, unknown>;
538
+ };
539
+
540
+ export type BoundedRunOutputPreview = {
541
+ outputs: Record<
542
+ string,
543
+ BoundedRunOutputValuePreview | BoundedRunDatasetOutputPreview
544
+ >;
545
+ previewTruncated: boolean;
546
+ omittedFields: number;
547
+ };
548
+
549
+ type LedgerStoredRunOutputValuePreview = Omit<
550
+ BoundedRunOutputValuePreview,
551
+ 'value'
552
+ > &
553
+ (
554
+ | { value: unknown; valueFromListPreview?: never }
555
+ | { value?: never; valueFromListPreview: true }
556
+ );
557
+
558
+ export type LedgerStoredRunOutputPreview = Omit<
559
+ BoundedRunOutputPreview,
560
+ 'outputs'
561
+ > & {
562
+ outputs: Record<
563
+ string,
564
+ LedgerStoredRunOutputValuePreview | BoundedRunDatasetOutputPreview
565
+ >;
566
+ };
567
+
568
+ function trimString(value: string): { value: string; truncated: boolean } {
569
+ const compact = value.replace(/\s+/g, ' ').trim();
570
+ if (compact.length <= RUN_OUTPUT_VALUE_PREVIEW_LIMITS.maxStringLength) {
571
+ return { value: compact, truncated: compact !== value };
572
+ }
573
+ return {
574
+ value: `${compact.slice(0, RUN_OUTPUT_VALUE_PREVIEW_LIMITS.maxStringLength)}...`,
575
+ truncated: true,
576
+ };
577
+ }
578
+
579
+ export function compactRunOutputPreviewValue(
580
+ value: unknown,
581
+ depth: number,
582
+ ): { value: unknown; truncated: boolean } {
583
+ if (
584
+ value == null ||
585
+ typeof value === 'number' ||
586
+ typeof value === 'boolean'
587
+ ) {
588
+ return { value, truncated: false };
589
+ }
590
+ if (typeof value === 'string') {
591
+ return trimString(value);
592
+ }
593
+ if (depth >= RUN_OUTPUT_VALUE_PREVIEW_LIMITS.maxDepth) {
594
+ return {
595
+ value: Array.isArray(value) ? '[Array]' : '[Object]',
596
+ truncated: true,
597
+ };
598
+ }
599
+ if (Array.isArray(value)) {
600
+ let truncated = false;
601
+ const items = value.slice(0, 20).map((item) => {
602
+ const compacted = compactRunOutputPreviewValue(item, depth + 1);
603
+ truncated ||= compacted.truncated;
604
+ return compacted.value;
605
+ });
606
+ return {
607
+ value: items,
608
+ truncated: truncated || value.length > items.length,
609
+ };
610
+ }
611
+ if (!isPlainObject(value)) {
612
+ return { value: String(value), truncated: true };
613
+ }
614
+ let truncated = false;
615
+ const out: Record<string, unknown> = Object.create(null) as Record<
616
+ string,
617
+ unknown
618
+ >;
619
+ const entries = Object.entries(value);
620
+ for (const [key, child] of entries.slice(0, 40)) {
621
+ const compacted = compactRunOutputPreviewValue(child, depth + 1);
622
+ truncated ||= compacted.truncated;
623
+ out[key] = compacted.value;
624
+ }
625
+ return {
626
+ value: out,
627
+ truncated: truncated || entries.length > Object.keys(out).length,
628
+ };
629
+ }
630
+
631
+ /** Stable returned-dataset aliases shared by writer and default package reader. */
632
+ export function runOutputKeyForDataset(
633
+ path: string,
634
+ tableNamespace: string | undefined,
635
+ used: Set<string>,
636
+ ): string {
637
+ const parts = path.split('.');
638
+ const raw =
639
+ parts[0] === 'result' && parts.length > 1
640
+ ? parts.at(-1)
641
+ : (tableNamespace ?? 'dataset');
642
+ const base =
643
+ String(raw || 'dataset')
644
+ .replace(/[^A-Za-z0-9_-]+/g, '_')
645
+ .replace(/^_+|_+$/g, '') || 'dataset';
646
+ let key = base;
647
+ let index = 2;
648
+ while (used.has(key)) key = `${base}_${index++}`;
649
+ used.add(key);
650
+ return key;
651
+ }
652
+
653
+ /** Derive the existing default value-output preview once, at terminal persistence. */
654
+ export function buildBoundedRunOutputPreview(
655
+ value: Record<string, unknown>,
656
+ ): BoundedRunOutputPreview {
657
+ const outputs: BoundedRunOutputPreview['outputs'] = Object.create(null);
658
+ let bytes = 2;
659
+ let fields = 0;
660
+ let previewTruncated = false;
661
+ let omittedFields = 0;
662
+ const add = (
663
+ key: string,
664
+ output: BoundedRunOutputPreview['outputs'][string],
665
+ ) => {
666
+ const fieldBytes = runListPreviewFieldBytes(key, output, fields > 0);
667
+ // Unsafe names/nesting cannot poison the live ledger. Omission is explicit;
668
+ // the original value remains in the separately stored full terminal result.
669
+ if (
670
+ !isSafeRunListPreviewFieldName(key) ||
671
+ inspectConvexLedgerPayload(output) ||
672
+ fields >= 200 ||
673
+ bytes + fieldBytes > 32_000
674
+ ) {
675
+ previewTruncated = true;
676
+ omittedFields += 1;
677
+ return;
678
+ }
679
+ outputs[key] = output;
680
+ bytes += fieldBytes;
681
+ fields += 1;
682
+ };
683
+ const used = new Set<string>();
684
+ const visitDatasets = (
685
+ candidate: unknown,
686
+ returnPath: string,
687
+ depth: number,
688
+ ) => {
689
+ if (depth > 8 || candidate == null) return;
690
+ if (Array.isArray(candidate)) {
691
+ candidate.forEach((child, index) =>
692
+ visitDatasets(child, `${returnPath}[${index}]`, depth + 1),
693
+ );
694
+ return;
695
+ }
696
+ if (!isPlainObject(candidate)) return;
697
+ if (candidate.kind === 'dataset') {
698
+ const string = (key: string) =>
699
+ typeof candidate[key] === 'string' && candidate[key].trim()
700
+ ? candidate[key].trim()
701
+ : undefined;
702
+ const datasetId = string('datasetId');
703
+ const tableNamespace = string('tableNamespace');
704
+ const datasetKind = string('datasetKind');
705
+ const count = candidate.count ?? candidate.rowCount;
706
+ const rowCount =
707
+ typeof count === 'number' && Number.isFinite(count) ? count : undefined;
708
+ if (
709
+ !tableNamespace &&
710
+ !datasetKind &&
711
+ rowCount === undefined &&
712
+ (!Array.isArray(candidate.preview) || candidate.preview.length === 0)
713
+ )
714
+ return;
715
+ const backing = isPlainObject(candidate.backing)
716
+ ? candidate.backing
717
+ : null;
718
+ const sheet = isPlainObject(backing?.sheet) ? backing.sheet : null;
719
+ const runtimeSheetBacked =
720
+ backing?.storage === 'neon_sheet' &&
721
+ typeof sheet?.tableNamespace === 'string';
722
+ const sourceAccess = isPlainObject(candidate.access)
723
+ ? candidate.access
724
+ : null;
725
+ let access: Record<string, unknown> | undefined;
726
+ if (
727
+ sourceAccess &&
728
+ typeof sourceAccess.path === 'string' &&
729
+ typeof sourceAccess.tableNamespace === 'string' &&
730
+ typeof sourceAccess.sqlQuery === 'string'
731
+ ) {
732
+ access = {};
733
+ for (const field of [
734
+ 'path',
735
+ 'tableNamespace',
736
+ 'sqlSchemaName',
737
+ 'sqlTableName',
738
+ 'sqlQualifiedTableName',
739
+ 'currentTableSqlQuery',
740
+ 'queryCurrentTableCommand',
741
+ 'sqlQuery',
742
+ 'queryDatasetCommand',
743
+ 'slowExportAsCsvCommand',
744
+ 'cliCommand',
745
+ 'toolCommand',
746
+ 'note',
747
+ ]) {
748
+ const fieldValue = sourceAccess[field];
749
+ if (typeof fieldValue === 'string' || fieldValue === null)
750
+ access[field] = fieldValue;
751
+ }
752
+ const api = isPlainObject(sourceAccess.api) ? sourceAccess.api : null;
753
+ const body = isPlainObject(api?.body) ? api.body : null;
754
+ if (
755
+ api &&
756
+ body &&
757
+ typeof api.method === 'string' &&
758
+ typeof api.path === 'string' &&
759
+ typeof body.sql === 'string' &&
760
+ typeof body.max_rows === 'number'
761
+ ) {
762
+ access.api = {
763
+ method: api.method,
764
+ path: api.path,
765
+ body: { sql: body.sql, max_rows: body.max_rows },
766
+ };
767
+ }
768
+ }
769
+ const key = runOutputKeyForDataset(returnPath, tableNamespace, used);
770
+ add(key, {
771
+ kind: 'dataset',
772
+ path: returnPath,
773
+ dataset: string('path') ?? returnPath,
774
+ ...(datasetId ? { datasetId } : {}),
775
+ ...(tableNamespace ? { tableNamespace } : {}),
776
+ ...(datasetKind ? { datasetKind } : {}),
777
+ ...(rowCount !== undefined ? { rowCount } : {}),
778
+ ...(runtimeSheetBacked ? { runtimeSheetBacked: true } : {}),
779
+ ...(Array.isArray(candidate.preview)
780
+ ? { preview: buildRunDatasetPreview(candidate.preview, rowCount) }
781
+ : {}),
782
+ ...(access ? { access } : {}),
783
+ });
784
+ return;
785
+ }
786
+ for (const [key, child] of Object.entries(candidate)) {
787
+ if (key !== 'preview')
788
+ visitDatasets(child, `${returnPath}.${key}`, depth + 1);
789
+ }
790
+ };
791
+ visitDatasets(value, 'result', 0);
792
+ for (const [key, child] of Object.entries(value)) {
793
+ if (
794
+ key.startsWith('_') ||
795
+ used.has(key) ||
796
+ (isPlainObject(child) && child.kind === 'dataset')
797
+ )
798
+ continue;
799
+ const compacted = compactRunOutputPreviewValue(child, 0);
800
+ previewTruncated ||= compacted.truncated;
801
+ add(key, {
802
+ kind: 'value',
803
+ path: `result.${key}`,
804
+ value: compacted.value,
805
+ ...(compacted.truncated ? { truncated: true } : {}),
806
+ ...(Array.isArray(child) ? { totalItems: child.length } : {}),
807
+ });
808
+ }
809
+ return { outputs, previewTruncated, omittedFields };
810
+ }
811
+
812
+ /**
813
+ * Reuse the existing run-list copy for exact duplicate scalar previews. The
814
+ * terminal reference carries both projections for old and current readers,
815
+ * so duplicating the same bounded value needlessly inflates its ledger event.
816
+ */
817
+ export function reuseRunListValuesInOutputPreview(
818
+ responsePreview: BoundedRunOutputPreview,
819
+ listPreview: BoundedRunListOutputPreview,
820
+ ): LedgerStoredRunOutputPreview {
821
+ const outputs: LedgerStoredRunOutputPreview['outputs'] = Object.create(null);
822
+ for (const [key, output] of Object.entries(responsePreview.outputs)) {
823
+ if (
824
+ output.kind !== 'value' ||
825
+ !Object.prototype.hasOwnProperty.call(listPreview.output, key)
826
+ ) {
827
+ outputs[key] = output;
828
+ continue;
829
+ }
830
+ let matches = false;
831
+ try {
832
+ matches =
833
+ JSON.stringify(output.value) ===
834
+ JSON.stringify(listPreview.output[key]);
835
+ } catch {
836
+ // Non-JSON preview values remain self-contained in the detail projection.
837
+ }
838
+ outputs[key] = matches
839
+ ? {
840
+ kind: 'value',
841
+ path: output.path,
842
+ valueFromListPreview: true,
843
+ ...(output.truncated ? { truncated: true as const } : {}),
844
+ ...(output.totalItems !== undefined
845
+ ? { totalItems: output.totalItems }
846
+ : {}),
847
+ }
848
+ : output;
849
+ }
850
+ return { ...responsePreview, outputs };
851
+ }
852
+
853
+ /** Resolve compact value references before handing previews to response code. */
854
+ export function resolveRunOutputPreviewValues(
855
+ responsePreview: LedgerStoredRunOutputPreview,
856
+ listPreview: BoundedRunListOutputPreview,
857
+ ): BoundedRunOutputPreview {
858
+ const outputs: BoundedRunOutputPreview['outputs'] = Object.create(null);
859
+ for (const [key, output] of Object.entries(responsePreview.outputs)) {
860
+ if (output.kind !== 'value') {
861
+ outputs[key] = output;
862
+ continue;
863
+ }
864
+ // Read stored JSON through a runtime view. With strictNullChecks disabled,
865
+ // TypeScript collapses the optional-never member of the stored union after
866
+ // discriminant narrowing, even though these optional fields are persisted.
867
+ const storedValue = output as unknown as {
868
+ path: string;
869
+ value?: unknown;
870
+ valueFromListPreview?: unknown;
871
+ truncated?: true;
872
+ totalItems?: number;
873
+ };
874
+ const hasValueProperty = Object.prototype.hasOwnProperty.call(
875
+ storedValue,
876
+ 'value',
877
+ );
878
+ const hasReuseMarker = Object.prototype.hasOwnProperty.call(
879
+ storedValue,
880
+ 'valueFromListPreview',
881
+ );
882
+ const value = storedValue.value;
883
+ const reuseMarker = storedValue.valueFromListPreview;
884
+ if (reuseMarker === true) {
885
+ if (hasValueProperty) {
886
+ throw new Error(
887
+ `Terminal output preview entry has both a value and a run-list reference: ${key}`,
888
+ );
889
+ }
890
+ if (!Object.prototype.hasOwnProperty.call(listPreview.output, key)) {
891
+ throw new Error(
892
+ `Terminal output preview reference is missing its run-list value: ${key}`,
893
+ );
894
+ }
895
+ outputs[key] = {
896
+ kind: 'value',
897
+ path: storedValue.path,
898
+ value: listPreview.output[key],
899
+ ...(storedValue.truncated ? { truncated: true as const } : {}),
900
+ ...(storedValue.totalItems !== undefined
901
+ ? { totalItems: storedValue.totalItems }
902
+ : {}),
903
+ };
904
+ continue;
905
+ }
906
+ if (hasReuseMarker || !hasValueProperty || value === undefined) {
907
+ throw new Error(`Terminal output preview entry is malformed: ${key}`);
908
+ }
909
+ outputs[key] = {
910
+ kind: 'value',
911
+ path: storedValue.path,
912
+ value,
913
+ ...(storedValue.truncated ? { truncated: true as const } : {}),
914
+ ...(storedValue.totalItems !== undefined
915
+ ? { totalItems: storedValue.totalItems }
916
+ : {}),
917
+ };
918
+ }
919
+ return { ...responsePreview, outputs };
920
+ }
921
+
474
922
  function compactRunListPreviewString(value: string): {
475
923
  value: string;
476
924
  truncated: boolean;
@@ -751,6 +1199,7 @@ function ledgerTerminalResultRef(
751
1199
  content?: 'full';
752
1200
  bytes?: number;
753
1201
  preview?: BoundedRunListOutputPreview;
1202
+ responsePreview?: LedgerStoredRunOutputPreview;
754
1203
  failure?: LedgerTerminalFailureSummary;
755
1204
  } = {},
756
1205
  ): LedgerTerminalResultRef {
@@ -763,6 +1212,9 @@ function ledgerTerminalResultRef(
763
1212
  projection: 'out_of_line',
764
1213
  ...(options.content ? { content: options.content } : {}),
765
1214
  ...(options.preview ? { preview: options.preview } : {}),
1215
+ ...(options.responsePreview
1216
+ ? { responsePreview: options.responsePreview }
1217
+ : {}),
766
1218
  ...(options.failure ? { failure: options.failure } : {}),
767
1219
  warning: options.content
768
1220
  ? LEDGER_TERMINAL_RESULT_STORED_WARNING
@@ -853,9 +1305,14 @@ export function ledgerTerminalResultRefForValue(
853
1305
  // customer payload again merely to populate reference metadata.
854
1306
  }
855
1307
  let preview: BoundedRunListOutputPreview | undefined;
1308
+ let responsePreview: LedgerStoredRunOutputPreview | undefined;
856
1309
  if (options.content && isPlainObject(value)) {
857
1310
  try {
858
1311
  preview = buildBoundedRunListOutputPreview(value);
1312
+ responsePreview = reuseRunListValuesInOutputPreview(
1313
+ buildBoundedRunOutputPreview(value),
1314
+ preview,
1315
+ );
859
1316
  } catch {
860
1317
  // The terminal reference is the durable fallback. A hostile/non-JSON
861
1318
  // value must not turn optional list-preview derivation into a terminal
@@ -866,6 +1323,7 @@ export function ledgerTerminalResultRefForValue(
866
1323
  ...options,
867
1324
  bytes,
868
1325
  preview,
1326
+ responsePreview,
869
1327
  failure: terminalFailureSummary(value),
870
1328
  });
871
1329
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * A provider hold is queueing while progress remains possible. A run that has
3
+ * received no successful response from this credential for ten minutes must
4
+ * settle its affected work as repairable instead of occupying durable waits
5
+ * indefinitely. The start time lives in the Play checkpoint, across sandboxes.
6
+ */
7
+ export const PROVIDER_NO_PROGRESS_MAX_MS = 10 * 60_000;
8
+
9
+ export function observeProviderNoProgress(input: {
10
+ startedAtMsByScope: Record<string, number>;
11
+ scope: string;
12
+ retryAtMs: number;
13
+ nowMs: number;
14
+ }): { exhausted: boolean; startedAtMs: number; deadlineMs: number } {
15
+ const saved = input.startedAtMsByScope[input.scope];
16
+ const startedAtMs =
17
+ Number.isFinite(saved) && saved >= 0 && saved <= input.nowMs
18
+ ? saved
19
+ : input.nowMs;
20
+ input.startedAtMsByScope[input.scope] = startedAtMs;
21
+ const deadlineMs = startedAtMs + PROVIDER_NO_PROGRESS_MAX_MS;
22
+ return {
23
+ exhausted: input.nowMs >= deadlineMs || input.retryAtMs > deadlineMs,
24
+ startedAtMs,
25
+ deadlineMs,
26
+ };
27
+ }
28
+
29
+ export function providerNoProgressExpired(input: {
30
+ startedAtMsByScope: Record<string, number> | undefined;
31
+ scope: string;
32
+ nowMs: number;
33
+ }): boolean {
34
+ const startedAtMs = input.startedAtMsByScope?.[input.scope];
35
+ return (
36
+ typeof startedAtMs === 'number' &&
37
+ Number.isFinite(startedAtMs) &&
38
+ input.nowMs >= startedAtMs + PROVIDER_NO_PROGRESS_MAX_MS
39
+ );
40
+ }
@@ -38,6 +38,13 @@ export function workReceiptClaimConflictPredicateSql(input: {
38
38
  const reclaimRunningSql = input.reclaimRunningSql ?? 'false';
39
39
  const forceFailedRefreshSql = input.forceFailedRefreshSql ?? 'false';
40
40
  return `${table}.status = ANY(${input.claimableStatusesSql}::smallint[])
41
+ AND NOT (
42
+ ${table}.status IN (
43
+ ${RECEIPT_STATUS_CODE.completed}::smallint,
44
+ ${RECEIPT_STATUS_CODE.skipped}::smallint
45
+ )
46
+ AND ${table}.run_id = ${input.claimantRunIdSql}
47
+ )
41
48
  AND (
42
49
  (
43
50
  ${input.forceRefreshSql}::boolean
@@ -106,9 +106,13 @@ export type PlayRunLedgerDatasetSnapshot = {
106
106
  tableNamespace: string;
107
107
  phase: 'registered' | 'available' | 'failed';
108
108
  persistedRows: number;
109
+ persistedRowsObserved?: boolean;
109
110
  succeededRows: number;
111
+ succeededRowsObserved?: boolean;
110
112
  failedRows: number;
113
+ failedRowsObserved?: boolean;
111
114
  complete: boolean;
115
+ completeObserved?: boolean;
112
116
  /**
113
117
  * Dataset-grain row birth (ADR 0019). Absent on datasets registered before
114
118
  * the contract and on any registration that did not state one — never
@@ -620,9 +624,13 @@ export function normalizePlayRunLedgerSnapshot(
620
624
  tableNamespace: optionalString(rawDataset.tableNamespace) ?? datasetId,
621
625
  phase,
622
626
  persistedRows: Math.max(0, finiteNumber(rawDataset.persistedRows) ?? 0),
627
+ persistedRowsObserved: rawDataset.persistedRowsObserved === true,
623
628
  succeededRows: Math.max(0, finiteNumber(rawDataset.succeededRows) ?? 0),
629
+ succeededRowsObserved: rawDataset.succeededRowsObserved === true,
624
630
  failedRows: Math.max(0, finiteNumber(rawDataset.failedRows) ?? 0),
631
+ failedRowsObserved: rawDataset.failedRowsObserved === true,
625
632
  complete: rawDataset.complete === true,
633
+ completeObserved: rawDataset.completeObserved === true,
626
634
  // ADR 0019 birth survives the snapshot round trip. A partial record is
627
635
  // dropped rather than half-admitted, so a dataset either states where its
628
636
  // rows came from or says nothing.
@@ -1611,6 +1619,9 @@ export function reducePlayRunLedgerEvent(
1611
1619
  }
1612
1620
  case 'dataset.lifecycle': {
1613
1621
  const current = base.datasetsById[event.datasetId];
1622
+ const persistedRows = optionalFiniteNumber(event.persistedRows);
1623
+ const succeededRows = optionalFiniteNumber(event.succeededRows);
1624
+ const failedRows = optionalFiniteNumber(event.failedRows);
1614
1625
  const mergedBornFrom = mergeDatasetBornFrom(
1615
1626
  current?.bornFrom,
1616
1627
  event.bornFrom,
@@ -1622,14 +1633,25 @@ export function reducePlayRunLedgerEvent(
1622
1633
  phase: event.phase,
1623
1634
  persistedRows: Math.max(
1624
1635
  current?.persistedRows ?? 0,
1625
- event.persistedRows ?? 0,
1636
+ persistedRows ?? 0,
1626
1637
  ),
1638
+ persistedRowsObserved:
1639
+ current?.persistedRowsObserved === true ||
1640
+ (persistedRows !== undefined && persistedRows >= 0),
1627
1641
  succeededRows: Math.max(
1628
1642
  current?.succeededRows ?? 0,
1629
- event.succeededRows ?? 0,
1643
+ succeededRows ?? 0,
1630
1644
  ),
1631
- failedRows: Math.max(current?.failedRows ?? 0, event.failedRows ?? 0),
1645
+ succeededRowsObserved:
1646
+ current?.succeededRowsObserved === true ||
1647
+ (succeededRows !== undefined && succeededRows >= 0),
1648
+ failedRows: Math.max(current?.failedRows ?? 0, failedRows ?? 0),
1649
+ failedRowsObserved:
1650
+ current?.failedRowsObserved === true ||
1651
+ (failedRows !== undefined && failedRows >= 0),
1632
1652
  complete: current?.complete === true || event.complete === true,
1653
+ completeObserved:
1654
+ current?.completeObserved === true || event.complete !== undefined,
1633
1655
  // Birth is stated at registration. A later phase event carries no birth
1634
1656
  // record and must not erase the one that was witnessed; a paged
1635
1657
  // dataset re-registers with a larger admitted-row count.
@@ -2080,6 +2080,7 @@ async function run(
2080
2080
  const ctx = createPlayContext({
2081
2081
  artifactRuntimeContract,
2082
2082
  rowAdmissionSignal: options?.rowAdmissionSignal,
2083
+ executionSignal: persistenceSignal,
2083
2084
  executorToken: config.context.executorToken,
2084
2085
  baseUrl: config.context.baseUrl,
2085
2086
  // Runtime control stays on baseUrl (the receipt gateway); provider calls