deepline 0.3.201 → 0.3.202
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.
- package/dist/bundling-sources/sdk/src/client.ts +62 -1
- package/dist/bundling-sources/sdk/src/http.ts +55 -6
- package/dist/bundling-sources/sdk/src/index.ts +1 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +4 -2
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +460 -96
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +11 -1
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts +6 -2
- package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +401 -2
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +458 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-no-progress.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +25 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +119 -171
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-work-counters.ts +37 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +16 -7
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +136 -25
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +64 -15
- package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +5 -5
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/result-receipt-policy.ts +46 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +14 -5
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +5 -1
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +3 -9
- package/dist/cli/index.js +343 -39
- package/dist/cli/index.mjs +346 -41
- package/dist/index.d.mts +129 -1
- package/dist/index.d.ts +129 -1
- package/dist/index.js +64 -7
- package/dist/index.mjs +64 -7
- package/dist/install-integrity.json +3 -0
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- 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
|
-
|
|
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
|
-
|
|
1643
|
+
succeededRows ?? 0,
|
|
1630
1644
|
),
|
|
1631
|
-
|
|
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
|