deepline 0.1.91 → 0.1.94

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/index.mjs CHANGED
@@ -179,10 +179,10 @@ import { join as join2 } from "path";
179
179
 
180
180
  // src/release.ts
181
181
  var SDK_RELEASE = {
182
- version: "0.1.91",
182
+ version: "0.1.94",
183
183
  apiContract: "2026-06-dataset-column-cell-stale-hard-cutover",
184
184
  supportPolicy: {
185
- latest: "0.1.91",
185
+ latest: "0.1.94",
186
186
  minimumSupported: "0.1.53",
187
187
  deprecatedBelow: "0.1.53"
188
188
  }
@@ -537,6 +537,807 @@ function sleep(ms) {
537
537
  return new Promise((resolve2) => setTimeout(resolve2, ms));
538
538
  }
539
539
 
540
+ // src/stream-reconnect.ts
541
+ var STREAM_RECONNECT_BASE_DELAY_MS = 500;
542
+ var STREAM_RECONNECT_MAX_DELAY_MS = 15e3;
543
+ var STREAM_HEALTHY_CONNECTION_MS = 3e4;
544
+ function streamReconnectDelayMs(attempt) {
545
+ const cappedExponentialMs = Math.min(
546
+ STREAM_RECONNECT_MAX_DELAY_MS,
547
+ STREAM_RECONNECT_BASE_DELAY_MS * 2 ** Math.max(0, attempt)
548
+ );
549
+ return Math.max(1, Math.floor(Math.random() * (cappedExponentialMs + 1)));
550
+ }
551
+ function isTransientPlayStreamError(error) {
552
+ if (error instanceof DeeplineError && typeof error.statusCode === "number") {
553
+ return error.statusCode >= 500 && error.statusCode < 600;
554
+ }
555
+ const text = error instanceof Error ? error.message : String(error);
556
+ return /auth validation backend timed out|fetch failed|eaddrnotavail|econnreset|etimedout|eai_again|socket hang up/i.test(
557
+ text
558
+ );
559
+ }
560
+
561
+ // ../shared_libs/play-runtime/live-events.ts
562
+ function resolveTimingWindow(input) {
563
+ const startedAt = typeof input.startedAt === "number" && Number.isFinite(input.startedAt) ? input.startedAt : null;
564
+ const completedAt = typeof input.completedAt === "number" && Number.isFinite(input.completedAt) ? input.completedAt : null;
565
+ const updatedAt = typeof input.updatedAt === "number" && Number.isFinite(input.updatedAt) ? input.updatedAt : null;
566
+ return {
567
+ startedAt,
568
+ completedAt,
569
+ updatedAt
570
+ };
571
+ }
572
+
573
+ // ../shared_libs/play-runtime/run-ledger.ts
574
+ var LOG_TAIL_LIMIT = 100;
575
+ function isRecord(value) {
576
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
577
+ }
578
+ function finiteNumber(value) {
579
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
580
+ }
581
+ function optionalFiniteNumber(value) {
582
+ const normalized = finiteNumber(value);
583
+ return normalized === null ? void 0 : normalized;
584
+ }
585
+ function optionalString(value) {
586
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
587
+ }
588
+ function optionalNullableString(value) {
589
+ if (value === null) return null;
590
+ return optionalString(value);
591
+ }
592
+ function normalizePlayRunLedgerStatus(value) {
593
+ const normalized = String(value ?? "").trim().toLowerCase();
594
+ switch (normalized) {
595
+ case "queued":
596
+ case "pending":
597
+ return "queued";
598
+ case "running":
599
+ case "started":
600
+ return "running";
601
+ case "waiting":
602
+ return "waiting";
603
+ case "completed":
604
+ case "complete":
605
+ case "succeeded":
606
+ return "completed";
607
+ case "failed":
608
+ case "error":
609
+ return "failed";
610
+ case "cancelled":
611
+ case "canceled":
612
+ return "cancelled";
613
+ case "terminated":
614
+ return "terminated";
615
+ case "timed_out":
616
+ case "timeout":
617
+ return "timed_out";
618
+ default:
619
+ return "unknown";
620
+ }
621
+ }
622
+ function createEmptyPlayRunLedgerSnapshot(input) {
623
+ const status = normalizePlayRunLedgerStatus(input.status ?? "unknown");
624
+ const startedAt = finiteNumber(input.startedAt) ?? null;
625
+ const finishedAt = finiteNumber(input.finishedAt) ?? null;
626
+ return {
627
+ runId: input.runId,
628
+ playName: input.playName ?? null,
629
+ status,
630
+ error: optionalNullableString(input.error) ?? null,
631
+ createdAt: finiteNumber(input.createdAt) ?? null,
632
+ startedAt,
633
+ updatedAt: finiteNumber(input.updatedAt) ?? finishedAt ?? startedAt ?? finiteNumber(input.createdAt) ?? null,
634
+ finishedAt,
635
+ durationMs: startedAt !== null && finishedAt !== null ? Math.max(0, finishedAt - startedAt) : null,
636
+ orderedStepIds: [],
637
+ stepsById: {},
638
+ logTail: [],
639
+ totalLogCount: 0,
640
+ logsTruncated: false,
641
+ activeStepId: null,
642
+ activeArtifactTableNamespace: null,
643
+ resultTableNamespace: null
644
+ };
645
+ }
646
+ function normalizePlayRunLedgerSnapshot(value, fallback) {
647
+ if (!isRecord(value)) {
648
+ return createEmptyPlayRunLedgerSnapshot(fallback);
649
+ }
650
+ const orderedStepIds = Array.isArray(value.orderedStepIds) ? value.orderedStepIds.filter(
651
+ (entry) => typeof entry === "string" && Boolean(entry.trim())
652
+ ) : [];
653
+ const rawSteps = isRecord(value.stepsById) ? value.stepsById : {};
654
+ const stepsById = {};
655
+ for (const [stepId, rawStep] of Object.entries(rawSteps)) {
656
+ if (!stepId.trim() || !isRecord(rawStep)) continue;
657
+ const rawStatus = normalizeStepStatus(rawStep.status);
658
+ if (!rawStatus) continue;
659
+ const completedAt = finiteNumber(rawStep.completedAt);
660
+ const status = rawStatus === "running" && completedAt !== null ? "completed" : rawStatus;
661
+ const rawProgress = isRecord(rawStep.progress) ? rawStep.progress : null;
662
+ stepsById[stepId] = {
663
+ stepId,
664
+ label: optionalString(rawStep.label),
665
+ kind: optionalString(rawStep.kind),
666
+ status,
667
+ artifactTableNamespace: optionalNullableString(
668
+ rawStep.artifactTableNamespace
669
+ ),
670
+ startedAt: finiteNumber(rawStep.startedAt),
671
+ completedAt,
672
+ updatedAt: finiteNumber(rawStep.updatedAt),
673
+ progress: rawProgress ? normalizeStepProgress(rawProgress) : null
674
+ };
675
+ }
676
+ const createdAt = finiteNumber(value.createdAt) ?? fallback.createdAt ?? null;
677
+ const startedAt = finiteNumber(value.startedAt) ?? fallback.startedAt ?? null;
678
+ const finishedAt = finiteNumber(value.finishedAt) ?? fallback.finishedAt ?? null;
679
+ const updatedAt = finiteNumber(value.updatedAt) ?? fallback.updatedAt ?? finishedAt ?? startedAt ?? createdAt ?? null;
680
+ const error = Object.prototype.hasOwnProperty.call(value, "error") ? optionalNullableString(value.error) ?? null : fallback.error ?? null;
681
+ const rawTail = Array.isArray(value.logTail) ? value.logTail : value.logs;
682
+ const logTail = Array.isArray(rawTail) ? rawTail.filter((line) => typeof line === "string") : [];
683
+ return {
684
+ runId: optionalString(value.runId) ?? fallback.runId,
685
+ playName: optionalNullableString(value.playName) ?? fallback.playName ?? null,
686
+ status: normalizePlayRunLedgerStatus(value.status ?? fallback.status),
687
+ error,
688
+ createdAt,
689
+ startedAt,
690
+ updatedAt,
691
+ finishedAt,
692
+ durationMs: startedAt !== null && finishedAt !== null ? Math.max(0, finishedAt - startedAt) : finiteNumber(value.durationMs),
693
+ orderedStepIds: orderedStepIds.filter((stepId) => stepsById[stepId]),
694
+ stepsById,
695
+ logTail: logTail.slice(-LOG_TAIL_LIMIT),
696
+ // Snapshots persisted before totalLogCount existed only know the retained
697
+ // tail, so the best lower bound for the cumulative count is the tail size.
698
+ totalLogCount: Math.max(
699
+ finiteNumber(value.totalLogCount) ?? logTail.length,
700
+ logTail.length
701
+ ),
702
+ logsTruncated: value.logsTruncated === true,
703
+ activeStepId: optionalNullableString(value.activeStepId),
704
+ activeArtifactTableNamespace: optionalNullableString(
705
+ value.activeArtifactTableNamespace
706
+ ),
707
+ resultTableNamespace: optionalNullableString(value.resultTableNamespace),
708
+ resultSummary: value.resultSummary,
709
+ result: value.result
710
+ };
711
+ }
712
+ function normalizeStepStatus(value) {
713
+ const normalized = String(value ?? "").trim().toLowerCase();
714
+ if (normalized === "running" || normalized === "completed" || normalized === "failed" || normalized === "skipped") {
715
+ return normalized;
716
+ }
717
+ return null;
718
+ }
719
+ function normalizeStepProgress(value) {
720
+ return {
721
+ ...optionalFiniteNumber(value.completed) !== void 0 ? { completed: optionalFiniteNumber(value.completed) } : {},
722
+ ...optionalFiniteNumber(value.total) !== void 0 ? { total: optionalFiniteNumber(value.total) } : {},
723
+ ...optionalFiniteNumber(value.failed) !== void 0 ? { failed: optionalFiniteNumber(value.failed) } : {},
724
+ ...optionalString(value.message) ? { message: optionalString(value.message) } : {},
725
+ ...optionalNullableString(value.artifactTableNamespace) !== void 0 ? {
726
+ artifactTableNamespace: optionalNullableString(
727
+ value.artifactTableNamespace
728
+ )
729
+ } : {},
730
+ ...finiteNumber(value.updatedAt) !== null ? { updatedAt: finiteNumber(value.updatedAt) } : {}
731
+ };
732
+ }
733
+
734
+ // ../shared_libs/play-runtime/run-snapshot-stream.ts
735
+ function normalizePlayRunLiveStatus(value) {
736
+ const normalized = String(value ?? "").trim().toLowerCase();
737
+ switch (normalized) {
738
+ case "queued":
739
+ case "pending":
740
+ return "running";
741
+ case "running":
742
+ case "started":
743
+ return "running";
744
+ case "completed":
745
+ case "complete":
746
+ case "succeeded":
747
+ return "completed";
748
+ case "failed":
749
+ case "error":
750
+ return "failed";
751
+ case "cancelled":
752
+ case "canceled":
753
+ return "cancelled";
754
+ case "terminated":
755
+ return "terminated";
756
+ case "timed_out":
757
+ case "timeout":
758
+ return "timed_out";
759
+ default:
760
+ return "unknown";
761
+ }
762
+ }
763
+ function isTerminalPlayRunLiveStatus(status) {
764
+ return status === "completed" || status === "failed" || status === "cancelled" || status === "terminated" || status === "timed_out";
765
+ }
766
+ function buildSnapshotFromLedger(snapshot) {
767
+ const nodeStates = snapshot.orderedStepIds.map((stepId) => snapshot.stepsById[stepId]).filter((step) => Boolean(step)).map((step) => ({
768
+ nodeId: step.stepId,
769
+ status: step.status,
770
+ artifactTableNamespace: step.artifactTableNamespace ?? null,
771
+ progress: step.progress ? {
772
+ completed: step.progress.completed,
773
+ total: step.progress.total,
774
+ failed: step.progress.failed,
775
+ message: step.progress.message,
776
+ artifactTableNamespace: step.progress.artifactTableNamespace ?? step.artifactTableNamespace ?? null,
777
+ startedAt: step.startedAt ?? null,
778
+ completedAt: step.completedAt ?? null,
779
+ updatedAt: step.progress.updatedAt ?? step.updatedAt ?? null
780
+ } : null,
781
+ startedAt: step.startedAt ?? null,
782
+ completedAt: step.completedAt ?? null,
783
+ updatedAt: step.updatedAt ?? null
784
+ }));
785
+ return {
786
+ runId: snapshot.runId,
787
+ status: normalizePlayRunLiveStatus(snapshot.status),
788
+ updatedAt: snapshot.updatedAt ?? snapshot.finishedAt ?? snapshot.startedAt ?? null,
789
+ logs: snapshot.logTail,
790
+ totalLogCount: snapshot.totalLogCount,
791
+ ...snapshot.logsTruncated ? { logsTruncated: true } : {},
792
+ activeArtifactTableNamespace: snapshot.activeArtifactTableNamespace ?? null,
793
+ resultTableNamespace: snapshot.resultTableNamespace ?? null,
794
+ nodeStates,
795
+ activeNodeId: snapshot.activeStepId ?? null
796
+ };
797
+ }
798
+ function buildPlayRunStatusSnapshot(input) {
799
+ const ledgerSnapshot = normalizePlayRunLedgerSnapshot(input.run.runSnapshot, {
800
+ runId: input.run.workflowId,
801
+ playName: input.run.name ?? null,
802
+ status: input.run.status,
803
+ createdAt: input.run.createdAt ?? null,
804
+ startedAt: input.run.startedAt ?? null,
805
+ updatedAt: input.run.updatedAt ?? null,
806
+ finishedAt: input.run.finishedAt ?? null
807
+ });
808
+ return buildSnapshotFromLedger(ledgerSnapshot);
809
+ }
810
+ function makeRunStreamEvent(input) {
811
+ return {
812
+ ...input,
813
+ scope: "play",
814
+ at: input.at ?? (/* @__PURE__ */ new Date()).toISOString()
815
+ };
816
+ }
817
+ var EMPTY_PLAY_RUN_STREAM_DIFF_STATE = {
818
+ runSignature: "",
819
+ snapshotSignature: "",
820
+ stepStatusSignature: "",
821
+ stepProgressSignature: "",
822
+ lastLogSeq: 0
823
+ };
824
+ function getSnapshotCursor(snapshot) {
825
+ return String(snapshot.updatedAt ?? Date.now());
826
+ }
827
+ function getRunSignature(snapshot) {
828
+ return [snapshot.runId, snapshot.status, snapshot.updatedAt ?? 0].join(":");
829
+ }
830
+ function getStepStatusSignature(snapshot) {
831
+ return snapshot.nodeStates.map(
832
+ (state) => [
833
+ state.nodeId,
834
+ state.status,
835
+ state.artifactTableNamespace ?? "",
836
+ state.startedAt ?? "",
837
+ state.completedAt ?? "",
838
+ state.progress?.startedAt ?? "",
839
+ state.progress?.completedAt ?? "",
840
+ state.updatedAt ?? "",
841
+ state.progress?.updatedAt ?? ""
842
+ ].join(":")
843
+ ).join("|");
844
+ }
845
+ function getStepProgressSignature(snapshot) {
846
+ return snapshot.nodeStates.map(
847
+ (state) => [
848
+ state.nodeId,
849
+ state.progress?.completed ?? "",
850
+ state.progress?.total ?? "",
851
+ state.progress?.failed ?? "",
852
+ state.progress?.artifactTableNamespace ?? "",
853
+ state.progress?.startedAt ?? "",
854
+ state.progress?.completedAt ?? "",
855
+ state.progress?.updatedAt ?? "",
856
+ state.progress?.message ?? ""
857
+ ].join(":")
858
+ ).join("|");
859
+ }
860
+ function getSnapshotSignature(snapshot) {
861
+ return JSON.stringify(snapshot);
862
+ }
863
+ function resolvePlayRunLogGap(snapshot, lastLogSeq) {
864
+ if (snapshot.totalLogCount <= lastLogSeq) {
865
+ return null;
866
+ }
867
+ const tailFirstSeq = snapshot.totalLogCount - snapshot.logs.length + 1;
868
+ if (lastLogSeq + 1 >= tailFirstSeq) {
869
+ return null;
870
+ }
871
+ return {
872
+ missingCount: tailFirstSeq - 1 - lastLogSeq,
873
+ tailFirstSeq
874
+ };
875
+ }
876
+ function diffLogLines(input) {
877
+ const { logs, totalLogCount } = input.snapshot;
878
+ if (totalLogCount <= input.lastLogSeq) {
879
+ return { lines: [], lastLogSeq: input.lastLogSeq, firstSeq: null };
880
+ }
881
+ const tailFirstSeq = totalLogCount - logs.length + 1;
882
+ if (input.lastLogSeq + 1 >= tailFirstSeq) {
883
+ return {
884
+ lines: logs.slice(input.lastLogSeq + 1 - tailFirstSeq),
885
+ lastLogSeq: totalLogCount,
886
+ firstSeq: input.lastLogSeq + 1
887
+ };
888
+ }
889
+ const missingCount = tailFirstSeq - 1 - input.lastLogSeq;
890
+ return {
891
+ lines: [
892
+ `[stream] ${missingCount} log lines not retained in the live window; full logs via runs logs`,
893
+ ...logs
894
+ ],
895
+ lastLogSeq: totalLogCount,
896
+ firstSeq: null
897
+ };
898
+ }
899
+ function diffPlayRunStreamEvents(input) {
900
+ const { snapshot, streamId, previous } = input;
901
+ const cursor = getSnapshotCursor(snapshot);
902
+ const logDiff = diffLogLines({
903
+ snapshot,
904
+ lastLogSeq: previous.lastLogSeq
905
+ });
906
+ const next = {
907
+ runSignature: getRunSignature(snapshot),
908
+ stepStatusSignature: getStepStatusSignature(snapshot),
909
+ stepProgressSignature: getStepProgressSignature(snapshot),
910
+ snapshotSignature: getSnapshotSignature(snapshot),
911
+ lastLogSeq: logDiff.lastLogSeq
912
+ };
913
+ const events = [];
914
+ if (next.stepStatusSignature !== previous.stepStatusSignature) {
915
+ for (const state of snapshot.nodeStates) {
916
+ if (state.status === "idle") {
917
+ continue;
918
+ }
919
+ const persistedStartedAt = state.startedAt ?? state.progress?.startedAt ?? null;
920
+ const persistedCompletedAt = state.completedAt ?? state.progress?.completedAt ?? null;
921
+ events.push(
922
+ makeRunStreamEvent({
923
+ cursor,
924
+ streamId,
925
+ type: "play.step.status",
926
+ payload: {
927
+ runId: snapshot.runId,
928
+ stepId: state.nodeId,
929
+ status: state.status,
930
+ artifactTableNamespace: state.artifactTableNamespace ?? null,
931
+ ...resolveTimingWindow({
932
+ startedAt: persistedStartedAt,
933
+ completedAt: persistedCompletedAt,
934
+ updatedAt: state.updatedAt ?? state.progress?.updatedAt ?? snapshot.updatedAt ?? null
935
+ })
936
+ }
937
+ })
938
+ );
939
+ }
940
+ }
941
+ if (next.stepProgressSignature !== previous.stepProgressSignature) {
942
+ for (const state of snapshot.nodeStates) {
943
+ if (!state.progress) {
944
+ continue;
945
+ }
946
+ events.push(
947
+ makeRunStreamEvent({
948
+ cursor: String(
949
+ state.progress.updatedAt ?? snapshot.updatedAt ?? Date.now()
950
+ ),
951
+ streamId,
952
+ type: "play.step.progress",
953
+ payload: {
954
+ runId: snapshot.runId,
955
+ stepId: state.nodeId,
956
+ completed: state.progress.completed,
957
+ total: state.progress.total,
958
+ failed: state.progress.failed,
959
+ message: state.progress.message,
960
+ artifactTableNamespace: state.progress.artifactTableNamespace ?? state.artifactTableNamespace ?? null,
961
+ ...resolveTimingWindow({
962
+ startedAt: state.startedAt ?? state.progress.startedAt ?? null,
963
+ completedAt: state.completedAt ?? state.progress.completedAt ?? null,
964
+ updatedAt: state.progress.updatedAt ?? snapshot.updatedAt ?? null
965
+ })
966
+ }
967
+ })
968
+ );
969
+ }
970
+ }
971
+ if (logDiff.lines.length > 0) {
972
+ events.push(
973
+ makeRunStreamEvent({
974
+ cursor,
975
+ streamId,
976
+ type: "play.run.log",
977
+ payload: {
978
+ runId: snapshot.runId,
979
+ lines: logDiff.lines,
980
+ source: "worker",
981
+ ...logDiff.firstSeq !== null ? { firstSeq: logDiff.firstSeq } : {},
982
+ totalLogCount: snapshot.totalLogCount
983
+ }
984
+ })
985
+ );
986
+ }
987
+ if (next.snapshotSignature !== previous.snapshotSignature) {
988
+ const enrichedNodeStates = snapshot.nodeStates.map((state) => {
989
+ const timing = resolveTimingWindow({
990
+ startedAt: state.startedAt ?? state.progress?.startedAt ?? null,
991
+ completedAt: state.completedAt ?? state.progress?.completedAt ?? null,
992
+ updatedAt: state.updatedAt ?? state.progress?.updatedAt ?? snapshot.updatedAt ?? null
993
+ });
994
+ return {
995
+ ...state,
996
+ ...timing,
997
+ progress: state.progress ? {
998
+ ...state.progress,
999
+ ...resolveTimingWindow({
1000
+ startedAt: state.progress.startedAt ?? state.startedAt ?? null,
1001
+ completedAt: state.progress.completedAt ?? state.completedAt ?? null,
1002
+ updatedAt: state.progress.updatedAt ?? state.updatedAt ?? snapshot.updatedAt ?? null
1003
+ })
1004
+ } : state.progress
1005
+ };
1006
+ });
1007
+ events.push(
1008
+ makeRunStreamEvent({
1009
+ cursor,
1010
+ streamId,
1011
+ type: "play.run.snapshot",
1012
+ payload: { ...snapshot, nodeStates: enrichedNodeStates }
1013
+ })
1014
+ );
1015
+ }
1016
+ if (next.runSignature !== previous.runSignature) {
1017
+ events.push(
1018
+ makeRunStreamEvent({
1019
+ cursor,
1020
+ streamId,
1021
+ type: "play.run.status",
1022
+ payload: {
1023
+ runId: snapshot.runId,
1024
+ status: snapshot.status,
1025
+ updatedAt: snapshot.updatedAt
1026
+ }
1027
+ })
1028
+ );
1029
+ }
1030
+ return { events, next };
1031
+ }
1032
+
1033
+ // src/runs/observe-transport.ts
1034
+ var RunObserveTransportUnavailableError = class extends Error {
1035
+ constructor(message, reason) {
1036
+ super(message);
1037
+ this.reason = reason;
1038
+ this.name = "RunObserveTransportUnavailableError";
1039
+ }
1040
+ reason;
1041
+ };
1042
+ var OBSERVE_BOOTSTRAP_TIMEOUT_MS = 1e4;
1043
+ var OBSERVE_RECONNECT_NOTICE_MS = 1e4;
1044
+ var OBSERVE_STALE_WARNING_MS = 12e4;
1045
+ var OBSERVE_WATCHDOG_TICK_MS = 5e3;
1046
+ var GRANT_REFRESH_MARGIN_MS = 5 * 6e4;
1047
+ var BACKFILL_PAGE_LIMIT = 1e3;
1048
+ var BACKFILL_MAX_PAGES = 30;
1049
+ var OBSERVER_SNAPSHOT_QUERY = "runObservers:getPlayRunSnapshotForObserver";
1050
+ var OBSERVER_LOG_PAGE_QUERY = "runObservers:getRunLogPageForObserver";
1051
+ function errorText(error) {
1052
+ return error instanceof Error ? error.message : String(error);
1053
+ }
1054
+ async function mintRunObserveGrant(http, runId) {
1055
+ let response;
1056
+ try {
1057
+ response = await http.post(
1058
+ `/api/v2/runs/${encodeURIComponent(runId)}/observe-grant`,
1059
+ {}
1060
+ );
1061
+ } catch (error) {
1062
+ if (error instanceof DeeplineError) {
1063
+ if (error.statusCode === 401 || error.statusCode === 403) {
1064
+ throw error;
1065
+ }
1066
+ throw new RunObserveTransportUnavailableError(
1067
+ `observe-grant endpoint unavailable (${error.statusCode ?? "network"}): ${error.message}`,
1068
+ "grant_endpoint_unavailable"
1069
+ );
1070
+ }
1071
+ throw new RunObserveTransportUnavailableError(
1072
+ `observe-grant request failed: ${errorText(error)}`,
1073
+ "grant_request_failed"
1074
+ );
1075
+ }
1076
+ const grant = response;
1077
+ if (!grant || typeof grant.convexUrl !== "string" || !grant.convexUrl.trim() || typeof grant.token !== "string" || !grant.token.trim() || typeof grant.expiresAt !== "number") {
1078
+ throw new RunObserveTransportUnavailableError(
1079
+ "observe-grant endpoint returned an invalid grant payload.",
1080
+ "grant_payload_invalid"
1081
+ );
1082
+ }
1083
+ return grant;
1084
+ }
1085
+ async function backfillLogGap(input) {
1086
+ const lines = [];
1087
+ let cursor = input.lastLogSeq;
1088
+ for (let page = 0; page < BACKFILL_MAX_PAGES; page += 1) {
1089
+ if (cursor >= input.tailFirstSeq - 1) {
1090
+ break;
1091
+ }
1092
+ let logPage;
1093
+ try {
1094
+ logPage = await input.queryLogPage(
1095
+ cursor,
1096
+ Math.min(BACKFILL_PAGE_LIMIT, input.tailFirstSeq - 1 - cursor)
1097
+ );
1098
+ } catch {
1099
+ return null;
1100
+ }
1101
+ const entries = (logPage?.entries ?? []).filter(
1102
+ (entry) => entry.seq > cursor && entry.seq < input.tailFirstSeq
1103
+ );
1104
+ if (entries.length === 0) {
1105
+ break;
1106
+ }
1107
+ for (const entry of entries) {
1108
+ if (entry.seq !== cursor + 1) {
1109
+ return null;
1110
+ }
1111
+ lines.push(entry.line);
1112
+ cursor = entry.seq;
1113
+ }
1114
+ }
1115
+ if (cursor < input.tailFirstSeq - 1) {
1116
+ return null;
1117
+ }
1118
+ return lines;
1119
+ }
1120
+ async function* observeRunEvents(options) {
1121
+ const { http, runId } = options;
1122
+ let grant = await mintRunObserveGrant(http, runId);
1123
+ let convexBrowser;
1124
+ let convexServer;
1125
+ try {
1126
+ convexBrowser = await import("convex/browser");
1127
+ convexServer = await import("convex/server");
1128
+ } catch (error) {
1129
+ throw new RunObserveTransportUnavailableError(
1130
+ `convex client module unavailable: ${errorText(error)}`,
1131
+ "convex_module_unavailable"
1132
+ );
1133
+ }
1134
+ let webSocketConstructor;
1135
+ if (typeof WebSocket === "undefined") {
1136
+ try {
1137
+ const wsModuleName = "ws";
1138
+ const ws = await import(wsModuleName);
1139
+ webSocketConstructor = ws.default;
1140
+ } catch (error) {
1141
+ throw new RunObserveTransportUnavailableError(
1142
+ `no WebSocket implementation available: ${errorText(error)}`,
1143
+ "websocket_unavailable"
1144
+ );
1145
+ }
1146
+ }
1147
+ const snapshotQuery = convexServer.makeFunctionReference(
1148
+ OBSERVER_SNAPSHOT_QUERY
1149
+ );
1150
+ const logPageQuery = convexServer.makeFunctionReference(
1151
+ OBSERVER_LOG_PAGE_QUERY
1152
+ );
1153
+ const client = new convexBrowser.ConvexClient(grant.convexUrl, {
1154
+ ...webSocketConstructor ? { webSocketConstructor } : {},
1155
+ unsavedChangesWarning: false
1156
+ });
1157
+ const queue = [];
1158
+ let wake = null;
1159
+ const push = (item) => {
1160
+ queue.push(item);
1161
+ wake?.();
1162
+ wake = null;
1163
+ };
1164
+ let lastForcedRefreshAt = 0;
1165
+ client.setAuth(async ({ forceRefreshToken }) => {
1166
+ const now = Date.now();
1167
+ if (!forceRefreshToken && grant.expiresAt - now > GRANT_REFRESH_MARGIN_MS) {
1168
+ return grant.token;
1169
+ }
1170
+ if (forceRefreshToken && now - lastForcedRefreshAt < 5e3) {
1171
+ push({
1172
+ kind: "error",
1173
+ error: new DeeplineError(
1174
+ `Run observe grant for ${runId} was rejected after a re-mint. The server and Convex deployment disagree on the grant issuer/JWKS.`,
1175
+ 401,
1176
+ "RUN_OBSERVE_GRANT_REJECTED"
1177
+ )
1178
+ });
1179
+ return null;
1180
+ }
1181
+ if (forceRefreshToken) {
1182
+ lastForcedRefreshAt = now;
1183
+ }
1184
+ try {
1185
+ grant = await mintRunObserveGrant(http, runId);
1186
+ return grant.token;
1187
+ } catch (error) {
1188
+ push({ kind: "error", error });
1189
+ return null;
1190
+ }
1191
+ });
1192
+ const unsubscribe = client.onUpdate(
1193
+ snapshotQuery,
1194
+ { workflowId: runId },
1195
+ (run) => push({ kind: "run", run: run ?? null }),
1196
+ (error) => push({ kind: "error", error })
1197
+ );
1198
+ let lastUpdateAt = Date.now();
1199
+ let lastStatusTerminal = false;
1200
+ let disconnectedSince = null;
1201
+ let warnedReconnecting = false;
1202
+ let warnedStale = false;
1203
+ const watchdog = setInterval(() => {
1204
+ const now = Date.now();
1205
+ try {
1206
+ const connectionState = client.connectionState();
1207
+ if (connectionState.isWebSocketConnected) {
1208
+ disconnectedSince = null;
1209
+ warnedReconnecting = false;
1210
+ } else {
1211
+ disconnectedSince ??= now;
1212
+ if (!warnedReconnecting && now - disconnectedSince >= OBSERVE_RECONNECT_NOTICE_MS) {
1213
+ warnedReconnecting = true;
1214
+ options.onNotice?.(
1215
+ `[observe] connection lost; reconnecting to live run ${runId}\u2026`
1216
+ );
1217
+ }
1218
+ }
1219
+ } catch {
1220
+ }
1221
+ if (!lastStatusTerminal && !warnedStale && now - lastUpdateAt >= OBSERVE_STALE_WARNING_MS) {
1222
+ warnedStale = true;
1223
+ options.onNotice?.(
1224
+ `[observe] no live updates for ${Math.round((now - lastUpdateAt) / 1e3)}s; run ${runId} may be stalled (status checks continue)`
1225
+ );
1226
+ }
1227
+ }, OBSERVE_WATCHDOG_TICK_MS);
1228
+ watchdog.unref?.();
1229
+ const abortListener = () => push({
1230
+ kind: "error",
1231
+ error: new DeeplineError(
1232
+ "Run observation aborted.",
1233
+ void 0,
1234
+ "ABORTED"
1235
+ )
1236
+ });
1237
+ options.signal?.addEventListener("abort", abortListener, { once: true });
1238
+ let diffState = EMPTY_PLAY_RUN_STREAM_DIFF_STATE;
1239
+ const streamId = ["observe", runId].join(":");
1240
+ let sawFirstSnapshot = false;
1241
+ try {
1242
+ for (; ; ) {
1243
+ if (queue.length === 0) {
1244
+ const waitForItem = new Promise((resolve2) => {
1245
+ wake = resolve2;
1246
+ });
1247
+ if (!sawFirstSnapshot) {
1248
+ const timedOut = await Promise.race([
1249
+ waitForItem.then(() => false),
1250
+ new Promise(
1251
+ (resolve2) => setTimeout(() => resolve2(true), OBSERVE_BOOTSTRAP_TIMEOUT_MS)
1252
+ )
1253
+ ]);
1254
+ if (timedOut && queue.length === 0) {
1255
+ throw new RunObserveTransportUnavailableError(
1256
+ `no snapshot from Convex at ${grant.convexUrl} within ${OBSERVE_BOOTSTRAP_TIMEOUT_MS}ms`,
1257
+ "convex_unreachable"
1258
+ );
1259
+ }
1260
+ } else {
1261
+ await waitForItem;
1262
+ }
1263
+ continue;
1264
+ }
1265
+ const item = queue.shift();
1266
+ if (item.kind === "error") {
1267
+ if (options.signal?.aborted) {
1268
+ return;
1269
+ }
1270
+ throw item.error;
1271
+ }
1272
+ sawFirstSnapshot = true;
1273
+ lastUpdateAt = Date.now();
1274
+ warnedStale = false;
1275
+ if (item.run === null) {
1276
+ throw new DeeplineError(
1277
+ `Run ${runId} was not found (or is not visible to this grant).`,
1278
+ 404,
1279
+ "RUN_NOT_FOUND"
1280
+ );
1281
+ }
1282
+ const snapshot = buildPlayRunStatusSnapshot({ run: item.run });
1283
+ lastStatusTerminal = isTerminalPlayRunLiveStatus(snapshot.status);
1284
+ const gap = resolvePlayRunLogGap(snapshot, diffState.lastLogSeq);
1285
+ if (gap && diffState.lastLogSeq > 0) {
1286
+ const backfilled = await backfillLogGap({
1287
+ queryLogPage: (afterSeq, limit) => client.query(logPageQuery, {
1288
+ workflowId: runId,
1289
+ afterSeq,
1290
+ limit
1291
+ }),
1292
+ lastLogSeq: diffState.lastLogSeq,
1293
+ tailFirstSeq: gap.tailFirstSeq
1294
+ });
1295
+ if (backfilled && backfilled.length > 0) {
1296
+ yield {
1297
+ cursor: String(snapshot.updatedAt ?? Date.now()),
1298
+ streamId,
1299
+ scope: "play",
1300
+ type: "play.run.log",
1301
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1302
+ payload: {
1303
+ runId: snapshot.runId,
1304
+ lines: backfilled,
1305
+ source: "worker",
1306
+ firstSeq: diffState.lastLogSeq + 1,
1307
+ totalLogCount: snapshot.totalLogCount
1308
+ }
1309
+ };
1310
+ diffState = { ...diffState, lastLogSeq: gap.tailFirstSeq - 1 };
1311
+ }
1312
+ }
1313
+ const { events, next } = diffPlayRunStreamEvents({
1314
+ streamId,
1315
+ snapshot,
1316
+ previous: diffState
1317
+ });
1318
+ diffState = next;
1319
+ const ordered = [
1320
+ ...events.filter((event) => event.type === "play.run.snapshot"),
1321
+ ...events.filter((event) => event.type !== "play.run.snapshot")
1322
+ ];
1323
+ for (const event of ordered) {
1324
+ yield event;
1325
+ }
1326
+ if (lastStatusTerminal) {
1327
+ return;
1328
+ }
1329
+ }
1330
+ } finally {
1331
+ clearInterval(watchdog);
1332
+ options.signal?.removeEventListener("abort", abortListener);
1333
+ try {
1334
+ unsubscribe();
1335
+ } catch {
1336
+ }
1337
+ await client.close().catch(() => void 0);
1338
+ }
1339
+ }
1340
+
540
1341
  // src/client.ts
541
1342
  var TERMINAL_PLAY_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
542
1343
  var INCLUDE_TOOL_METADATA_HEADER = "x-deepline-include-tool-metadata";
@@ -555,7 +1356,8 @@ function isTransientCompileManifestError(error) {
555
1356
  message
556
1357
  );
557
1358
  }
558
- function isRecord(value) {
1359
+ var RUN_LOGS_PAGE_LIMIT = 1e3;
1360
+ function isRecord2(value) {
559
1361
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
560
1362
  }
561
1363
  function isPrebuiltPlayDescription(play) {
@@ -625,10 +1427,31 @@ function normalizeLiveStatus(value) {
625
1427
  }
626
1428
  return null;
627
1429
  }
1430
+ function appendPlayLiveLogLines(state, payload) {
1431
+ const lines = readStringArray(payload.lines);
1432
+ if (lines.length === 0) {
1433
+ return;
1434
+ }
1435
+ const firstSeq = typeof payload.firstSeq === "number" && Number.isFinite(payload.firstSeq) && payload.firstSeq >= 1 ? Math.trunc(payload.firstSeq) : null;
1436
+ if (firstSeq === null) {
1437
+ state.logs.push(...lines);
1438
+ const totalLogCount = typeof payload.totalLogCount === "number" && Number.isFinite(payload.totalLogCount) ? Math.trunc(payload.totalLogCount) : null;
1439
+ if (totalLogCount !== null) {
1440
+ state.lastLogSeq = Math.max(state.lastLogSeq, totalLogCount);
1441
+ }
1442
+ return;
1443
+ }
1444
+ const skip = Math.max(0, state.lastLogSeq + 1 - firstSeq);
1445
+ if (skip >= lines.length) {
1446
+ return;
1447
+ }
1448
+ state.logs.push(...lines.slice(skip));
1449
+ state.lastLogSeq = Math.max(state.lastLogSeq, firstSeq + lines.length - 1);
1450
+ }
628
1451
  function updatePlayLiveStatusState(state, event) {
629
1452
  const payload = getPlayLiveEventPayload(event);
630
1453
  if (event.type === "play.run.log") {
631
- state.logs.push(...readStringArray(payload.lines));
1454
+ appendPlayLiveLogLines(state, payload);
632
1455
  return null;
633
1456
  }
634
1457
  if (event.type !== "play.run.snapshot" && event.type !== "play.run.status" && event.type !== "play.run.final_status") {
@@ -636,12 +1459,14 @@ function updatePlayLiveStatusState(state, event) {
636
1459
  }
637
1460
  const runId = typeof payload.runId === "string" && payload.runId ? payload.runId : isPlayRunPackage(payload) ? payload.run.id : state.runId;
638
1461
  const status = normalizeLiveStatus(payload.status) ?? (isPlayRunPackage(payload) ? normalizeLiveStatus(payload.run.status) : null) ?? state.status;
639
- const progressPayload = isRecord(payload.progress) ? payload.progress : {};
640
- const payloadLogs = readStringArray(payload.logs);
641
- const progressLogs = readStringArray(progressPayload.logs);
642
- const logs = payloadLogs.length > 0 ? payloadLogs : progressLogs;
643
- if (logs.length > 0 || event.type === "play.run.snapshot" || event.type === "play.run.final_status" && !isPlayRunPackage(payload)) {
644
- state.logs = logs;
1462
+ const progressPayload = isRecord2(payload.progress) ? payload.progress : {};
1463
+ if (event.type === "play.run.final_status" && state.logs.length === 0 && state.lastLogSeq === 0) {
1464
+ const payloadLogs = readStringArray(payload.logs);
1465
+ const progressLogs = readStringArray(progressPayload.logs);
1466
+ const seedLogs = payloadLogs.length > 0 ? payloadLogs : progressLogs;
1467
+ if (seedLogs.length > 0) {
1468
+ state.logs = seedLogs;
1469
+ }
645
1470
  }
646
1471
  if ("result" in payload) {
647
1472
  state.result = payload.result;
@@ -735,9 +1560,9 @@ var DeeplineClient = class {
735
1560
  return fields.length > 0 ? { fields } : schema;
736
1561
  }
737
1562
  schemaMetadata(schema, key) {
738
- if (!isRecord(schema)) return null;
1563
+ if (!isRecord2(schema)) return null;
739
1564
  const value = schema[key];
740
- return isRecord(value) ? value : null;
1565
+ return isRecord2(value) ? value : null;
741
1566
  }
742
1567
  playRunCommand(play, options) {
743
1568
  const target = play.reference || play.name;
@@ -784,7 +1609,7 @@ var DeeplineClient = class {
784
1609
  aliases,
785
1610
  inputSchema: options?.compact ? this.compactSchema(play.inputSchema) : play.inputSchema ?? null,
786
1611
  outputSchema: options?.compact ? this.compactSchema(play.outputSchema) : play.outputSchema ?? null,
787
- staticPipeline: isRecord(play.staticPipeline) ? play.staticPipeline : isRecord(play.currentRevision?.staticPipeline) ? play.currentRevision.staticPipeline : isRecord(play.liveRevision?.staticPipeline) ? play.liveRevision.staticPipeline : null,
1612
+ staticPipeline: isRecord2(play.staticPipeline) ? play.staticPipeline : isRecord2(play.currentRevision?.staticPipeline) ? play.currentRevision.staticPipeline : isRecord2(play.liveRevision?.staticPipeline) ? play.liveRevision.staticPipeline : null,
788
1613
  ...csvInput ? { csvInput } : {},
789
1614
  ...rowOutputSchema ? { rowOutputSchema } : {},
790
1615
  runCommand,
@@ -1423,6 +2248,93 @@ var DeeplineClient = class {
1423
2248
  );
1424
2249
  return response.runs ?? [];
1425
2250
  }
2251
+ // ---------------------------------------------------------------------------
2252
+ // Legacy workflows (double-shipped). Thin pass-throughs over the live cloud
2253
+ // `/api/v2/workflows/*` API so the SDK CLI keeps existing cloud workflows
2254
+ // working while users migrate them to plays via `workflows transform`. Kept
2255
+ // intentionally minimal — workflows are a deprecated surface.
2256
+ // ---------------------------------------------------------------------------
2257
+ /** List the org's workflows. `GET /api/v2/workflows`. */
2258
+ async listWorkflows(options) {
2259
+ const params = new URLSearchParams();
2260
+ if (typeof options?.limit === "number") {
2261
+ params.set("limit", String(options.limit));
2262
+ }
2263
+ const query = params.size > 0 ? `?${params.toString()}` : "";
2264
+ return this.http.get(`/api/v2/workflows${query}`);
2265
+ }
2266
+ /**
2267
+ * Fetch a single workflow (including its published-revision config — the
2268
+ * input to `compileWorkflowConfigToPlay`). `GET /api/v2/workflows/:id`.
2269
+ */
2270
+ async getWorkflow(id) {
2271
+ return this.http.get(`/api/v2/workflows/${encodeURIComponent(id)}`);
2272
+ }
2273
+ /** Delete a workflow. `DELETE /api/v2/workflows/:id`. */
2274
+ async deleteWorkflow(id) {
2275
+ return this.http.delete(`/api/v2/workflows/${encodeURIComponent(id)}`);
2276
+ }
2277
+ /** Turn a workflow off. `POST /api/v2/workflows/:id/disable`. */
2278
+ async disableWorkflow(id) {
2279
+ return this.http.post(
2280
+ `/api/v2/workflows/${encodeURIComponent(id)}/disable`,
2281
+ {}
2282
+ );
2283
+ }
2284
+ /** Turn a workflow back on. `POST /api/v2/workflows/:id/enable`. */
2285
+ async enableWorkflow(id) {
2286
+ return this.http.post(
2287
+ `/api/v2/workflows/${encodeURIComponent(id)}/enable`,
2288
+ {}
2289
+ );
2290
+ }
2291
+ /** Create/update a workflow from config. `POST /api/v2/workflows/apply`. */
2292
+ async applyWorkflow(body) {
2293
+ return this.http.post("/api/v2/workflows/apply", body);
2294
+ }
2295
+ /** Validate a workflow config without saving. `POST /api/v2/workflows/lint`. */
2296
+ async lintWorkflow(body) {
2297
+ return this.http.post("/api/v2/workflows/lint", body);
2298
+ }
2299
+ /** Fetch live workflow request schemas. `GET /api/v2/workflows/schema`. */
2300
+ async getWorkflowSchema(subject) {
2301
+ const params = new URLSearchParams();
2302
+ if (subject) params.set("subject", subject);
2303
+ const query = params.size > 0 ? `?${params.toString()}` : "";
2304
+ return this.http.get(`/api/v2/workflows/schema${query}`);
2305
+ }
2306
+ /** Queue a workflow run. `POST /api/v2/workflows/call`. */
2307
+ async callWorkflow(body) {
2308
+ return this.http.post("/api/v2/workflows/call", body);
2309
+ }
2310
+ /** List a workflow's runs. `GET /api/v2/workflows/:id/runs`. */
2311
+ async listWorkflowRuns(id, options) {
2312
+ const params = new URLSearchParams();
2313
+ if (typeof options?.limit === "number") {
2314
+ params.set("limit", String(options.limit));
2315
+ }
2316
+ const query = params.size > 0 ? `?${params.toString()}` : "";
2317
+ return this.http.get(
2318
+ `/api/v2/workflows/${encodeURIComponent(id)}/runs${query}`
2319
+ );
2320
+ }
2321
+ /** Fetch one workflow run. `GET /api/v2/workflows/:id/runs/:runId`. */
2322
+ async getWorkflowRun(id, runId) {
2323
+ return this.http.get(
2324
+ `/api/v2/workflows/${encodeURIComponent(id)}/runs/${encodeURIComponent(
2325
+ runId
2326
+ )}`
2327
+ );
2328
+ }
2329
+ /** Cancel a workflow run. `POST /api/v2/workflows/:id/runs/:runId/cancel`. */
2330
+ async cancelWorkflowRun(id, runId) {
2331
+ return this.http.post(
2332
+ `/api/v2/workflows/${encodeURIComponent(id)}/runs/${encodeURIComponent(
2333
+ runId
2334
+ )}/cancel`,
2335
+ {}
2336
+ );
2337
+ }
1426
2338
  /**
1427
2339
  * Get a run by id using the public runs resource model.
1428
2340
  *
@@ -1468,43 +2380,140 @@ var DeeplineClient = class {
1468
2380
  );
1469
2381
  return response.runs ?? [];
1470
2382
  }
1471
- /** Read the canonical run stream and return the latest run snapshot. */
1472
- async tailRun(runId, options) {
2383
+ /**
2384
+ * Observe one run's live events through the Convex Run Snapshot
2385
+ * subscription transport (ADR-0008). Yields the same `play.*` event
2386
+ * envelopes as {@link streamPlayRunEvents} and ends after the terminal
2387
+ * snapshot. Throws {@link RunObserveTransportUnavailableError} when this
2388
+ * server cannot serve the transport (older server, unconfigured grants, or
2389
+ * unreachable Convex) — callers fall back to the SSE stream with a notice.
2390
+ */
2391
+ observeRunEvents(runId, options) {
2392
+ return observeRunEvents({
2393
+ http: this.http,
2394
+ runId,
2395
+ signal: options?.signal,
2396
+ onNotice: options?.onNotice
2397
+ });
2398
+ }
2399
+ /**
2400
+ * Tail one run through the subscription transport until terminal, then
2401
+ * return one durable REST status read (the final Run Response Package).
2402
+ */
2403
+ async tailRunViaObserveTransport(runId, options) {
1473
2404
  const state = {
1474
2405
  runId,
1475
2406
  status: "running",
1476
2407
  logs: [],
2408
+ lastLogSeq: 0,
1477
2409
  latest: null
1478
2410
  };
1479
- let terminal = false;
1480
- for await (const event of this.streamPlayRunEvents(runId, {
1481
- mode: "cli",
1482
- signal: options?.signal
2411
+ for await (const event of this.observeRunEvents(runId, {
2412
+ signal: options?.signal,
2413
+ onNotice: options?.onNotice
1483
2414
  })) {
1484
2415
  const status = updatePlayLiveStatusState(state, event);
1485
- if (!status) {
2416
+ if (!status || !TERMINAL_PLAY_STATUSES.has(status.status)) {
1486
2417
  continue;
1487
2418
  }
1488
- terminal = TERMINAL_PLAY_STATUSES.has(status.status);
1489
- if (terminal) {
1490
- break;
1491
- }
1492
- }
1493
- if (terminal && state.latest) {
1494
- return await this.getRunStatus(state.latest.runId || runId).catch(
2419
+ return await this.getRunStatus(status.runId || runId).catch(
1495
2420
  () => state.latest ?? playRunStatusFromState(state)
1496
2421
  );
1497
2422
  }
1498
- if (state.latest) {
1499
- return state.latest;
2423
+ if (options?.signal?.aborted) {
2424
+ throw new DeeplineError("Run observation aborted.", void 0, "ABORTED");
2425
+ }
2426
+ const refreshed = await this.getRunStatus(runId);
2427
+ if (TERMINAL_PLAY_STATUSES.has(refreshed.status)) {
2428
+ return refreshed;
1500
2429
  }
1501
2430
  throw new DeeplineError(
1502
- `Run stream for ${runId} ended before the initial snapshot.`,
2431
+ `Run observation for ${runId} ended before a terminal status.`,
1503
2432
  void 0,
1504
- "PLAY_RUN_STREAM_EMPTY",
1505
- { runId }
2433
+ "PLAY_LIVE_STREAM_ENDED"
1506
2434
  );
1507
2435
  }
2436
+ /**
2437
+ * Read the canonical run stream until a terminal run status is observed.
2438
+ *
2439
+ * Tries the Convex Run Snapshot subscription transport first (ADR-0008);
2440
+ * when the server cannot serve it (grant endpoint missing/unconfigured or
2441
+ * Convex unreachable) it falls back — with one `onNotice` message — to the
2442
+ * support-window SSE stream below.
2443
+ *
2444
+ * Server stream windows are finite: they end cleanly at the function
2445
+ * ceiling even while the run keeps executing. A window that ends (cleanly
2446
+ * or via transient network error) without a terminal event triggers one
2447
+ * durable-status re-check followed by a backed-off reconnect, so long runs
2448
+ * tail to completion. Abort via `options.signal` to stop waiting.
2449
+ */
2450
+ async tailRun(runId, options) {
2451
+ try {
2452
+ return await this.tailRunViaObserveTransport(runId, options);
2453
+ } catch (error) {
2454
+ if (!(error instanceof RunObserveTransportUnavailableError)) {
2455
+ throw error;
2456
+ }
2457
+ options?.onNotice?.(
2458
+ `[observe] live subscription unavailable (${error.reason}); falling back to SSE tail (support window, ADR-0008)`
2459
+ );
2460
+ }
2461
+ const state = {
2462
+ runId,
2463
+ status: "running",
2464
+ logs: [],
2465
+ lastLogSeq: 0,
2466
+ latest: null
2467
+ };
2468
+ let reconnectAttempt = 0;
2469
+ for (; ; ) {
2470
+ const connectedAt = Date.now();
2471
+ let sawEvent = false;
2472
+ let endedReason = "stream window ended before a terminal event";
2473
+ try {
2474
+ for await (const event of this.streamPlayRunEvents(runId, {
2475
+ mode: "cli",
2476
+ signal: options?.signal
2477
+ })) {
2478
+ sawEvent = true;
2479
+ const status = updatePlayLiveStatusState(state, event);
2480
+ if (!status || !TERMINAL_PLAY_STATUSES.has(status.status)) {
2481
+ continue;
2482
+ }
2483
+ return await this.getRunStatus(status.runId || runId).catch(
2484
+ () => state.latest ?? playRunStatusFromState(state)
2485
+ );
2486
+ }
2487
+ } catch (error) {
2488
+ if (options?.signal?.aborted || !isTransientPlayStreamError(error)) {
2489
+ throw error;
2490
+ }
2491
+ endedReason = error instanceof Error ? error.message : String(error);
2492
+ }
2493
+ let refreshed = null;
2494
+ try {
2495
+ refreshed = await this.getRunStatus(runId);
2496
+ } catch (error) {
2497
+ if (!isTransientPlayStreamError(error)) {
2498
+ throw error;
2499
+ }
2500
+ }
2501
+ if (refreshed && TERMINAL_PLAY_STATUSES.has(refreshed.status)) {
2502
+ return refreshed;
2503
+ }
2504
+ if (sawEvent || Date.now() - connectedAt >= STREAM_HEALTHY_CONNECTION_MS) {
2505
+ reconnectAttempt = 0;
2506
+ }
2507
+ const delayMs = streamReconnectDelayMs(reconnectAttempt);
2508
+ reconnectAttempt += 1;
2509
+ options?.onReconnect?.({
2510
+ attempt: reconnectAttempt,
2511
+ delayMs,
2512
+ reason: endedReason
2513
+ });
2514
+ await sleep2(delayMs);
2515
+ }
2516
+ }
1508
2517
  /**
1509
2518
  * Fetch persisted logs for a run using the public runs resource model.
1510
2519
  *
@@ -1515,19 +2524,40 @@ var DeeplineClient = class {
1515
2524
  * ```
1516
2525
  */
1517
2526
  async getRunLogs(runId, options) {
1518
- const status = await this.getRunStatus(runId, { full: true });
1519
- const logs = status.progress?.logs ?? [];
1520
- const limit = typeof options?.limit === "number" && Number.isFinite(options.limit) ? Math.max(0, Math.trunc(options.limit)) : 200;
1521
- const entries = logs.slice(Math.max(0, logs.length - limit));
2527
+ const limit = options?.all ? Number.MAX_SAFE_INTEGER : typeof options?.limit === "number" && Number.isFinite(options.limit) && options.limit > 0 ? Math.trunc(options.limit) : 200;
2528
+ const fetchPage = (afterSeq2, pageLimit) => this.http.get(
2529
+ `/api/v2/runs/${encodeURIComponent(runId)}/logs?afterSeq=${afterSeq2}&limit=${pageLimit}`
2530
+ );
2531
+ const probe = await fetchPage(0, 1);
2532
+ const lastStoredSeq = probe.lastStoredSeq;
2533
+ let afterSeq = options?.all ? 0 : Math.max(0, lastStoredSeq - limit);
2534
+ const entries = [];
2535
+ while (entries.length < limit) {
2536
+ const page = await fetchPage(
2537
+ afterSeq,
2538
+ Math.min(RUN_LOGS_PAGE_LIMIT, limit - entries.length)
2539
+ );
2540
+ if (page.entries.length === 0) {
2541
+ break;
2542
+ }
2543
+ entries.push(...page.entries);
2544
+ afterSeq = page.entries[page.entries.length - 1].seq;
2545
+ if (!page.hasMore) {
2546
+ break;
2547
+ }
2548
+ }
2549
+ const firstSequence = entries.length > 0 ? entries[0].seq : null;
2550
+ const lastSequence = entries.length > 0 ? entries[entries.length - 1].seq : null;
1522
2551
  return {
1523
- runId: status.runId,
1524
- totalCount: logs.length,
2552
+ runId: probe.runId,
2553
+ totalCount: probe.totalLogCount,
1525
2554
  returnedCount: entries.length,
1526
- firstSequence: logs.length === 0 ? null : logs.length - entries.length + 1,
1527
- lastSequence: logs.length === 0 ? null : logs.length,
1528
- truncated: logs.length > entries.length,
1529
- hasMore: logs.length > entries.length,
1530
- entries
2555
+ firstSequence,
2556
+ lastSequence,
2557
+ truncated: entries.length < probe.totalLogCount,
2558
+ hasMore: lastSequence !== null && lastSequence < lastStoredSeq,
2559
+ entries: entries.map((entry) => entry.line),
2560
+ ...probe.logsTruncated ? { logsTruncated: true } : {}
1531
2561
  };
1532
2562
  }
1533
2563
  /**
@@ -1818,6 +2848,7 @@ var DeeplineClient = class {
1818
2848
  runId: workflowId,
1819
2849
  status: "running",
1820
2850
  logs: [],
2851
+ lastLogSeq: 0,
1821
2852
  latest: null
1822
2853
  };
1823
2854
  if (options?.signal?.aborted) {
@@ -1971,41 +3002,6 @@ function formatPlayBootstrapFinderKindsForSentence() {
1971
3002
  }
1972
3003
 
1973
3004
  // ../shared_libs/play-runtime/email-status.ts
1974
- var DEFAULT_STATUS_MAP = {
1975
- verified: { status: "valid", verdict: "send", verified: true },
1976
- valid: { status: "valid", verdict: "send", verified: true },
1977
- deliverable: { status: "valid", verdict: "send", verified: true },
1978
- true: { status: "valid", verdict: "send", verified: true },
1979
- invalid: { status: "invalid", verdict: "drop", verified: false },
1980
- undeliverable: { status: "invalid", verdict: "drop", verified: false },
1981
- false: { status: "invalid", verdict: "drop", verified: false },
1982
- "catch-all": {
1983
- status: "catch_all",
1984
- verdict: "verify_next",
1985
- verified: false
1986
- },
1987
- catch_all: {
1988
- status: "catch_all",
1989
- verdict: "verify_next",
1990
- verified: false
1991
- },
1992
- valid_catch_all: {
1993
- status: "valid_catch_all",
1994
- verdict: "send_with_caution",
1995
- verified: true
1996
- },
1997
- accept_all: {
1998
- status: "catch_all",
1999
- verdict: "verify_next",
2000
- verified: false
2001
- },
2002
- unknown: { status: "unknown", verdict: "hold", verified: false },
2003
- unavailable: { status: "unknown", verdict: "hold", verified: false },
2004
- do_not_mail: { status: "do_not_mail", verdict: "drop", verified: false },
2005
- spamtrap: { status: "spamtrap", verdict: "drop", verified: false },
2006
- abuse: { status: "abuse", verdict: "drop", verified: false },
2007
- disposable: { status: "disposable", verdict: "drop", verified: false }
2008
- };
2009
3005
  function normalizeKey(value) {
2010
3006
  if (value == null) return null;
2011
3007
  if (typeof value === "boolean") return String(value);
@@ -2054,7 +3050,7 @@ function mxClass(mxProvider, mxRecord) {
2054
3050
  }
2055
3051
  function entryForStatus(key, map) {
2056
3052
  if (!key) return null;
2057
- return map?.[key] ?? DEFAULT_STATUS_MAP[key] ?? null;
3053
+ return map?.[key] ?? null;
2058
3054
  }
2059
3055
  function read(values, name) {
2060
3056
  return values[name];
@@ -2188,7 +3184,7 @@ var TARGET_FALLBACK_KEYS = {
2188
3184
  status: [/^email_status$/i, /^status$/i],
2189
3185
  email_status: [/^email_status$/i, /^status$/i]
2190
3186
  };
2191
- function isRecord2(value) {
3187
+ function isRecord3(value) {
2192
3188
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2193
3189
  }
2194
3190
  function toV2RawToolOutputPath(path) {
@@ -2248,7 +3244,7 @@ function valuesAtSegments(current, segments, path = []) {
2248
3244
  if (!Array.isArray(current)) return [];
2249
3245
  return valuesAtSegments(current[segment], rest, [...path, segment]);
2250
3246
  }
2251
- if (!isRecord2(current)) return [];
3247
+ if (!isRecord3(current)) return [];
2252
3248
  const directMatches = valuesAtSegments(current[segment], rest, [
2253
3249
  ...path,
2254
3250
  segment
@@ -2278,7 +3274,7 @@ function getValuesAtPath(root, path) {
2278
3274
  return valuesAtSegments(root, parsePath(path)).map((entry) => entry.value);
2279
3275
  }
2280
3276
  function toResultEnvelope(value) {
2281
- if (isRecord2(value) && "data" in value) {
3277
+ if (isRecord3(value) && "data" in value) {
2282
3278
  const envelope = { data: value.data };
2283
3279
  if ("meta" in value) envelope.meta = value.meta;
2284
3280
  return envelope;
@@ -2331,7 +3327,7 @@ function getAtPath(root, path) {
2331
3327
  current = current[segment];
2332
3328
  continue;
2333
3329
  }
2334
- if (!isRecord2(current)) return void 0;
3330
+ if (!isRecord3(current)) return void 0;
2335
3331
  current = current[segment];
2336
3332
  }
2337
3333
  return current;
@@ -2348,7 +3344,7 @@ function normalizeString(value) {
2348
3344
  }
2349
3345
  function normalizeRows(value) {
2350
3346
  if (!Array.isArray(value)) return null;
2351
- return value.map((entry) => isRecord2(entry) ? entry : { value: entry });
3347
+ return value.map((entry) => isRecord3(entry) ? entry : { value: entry });
2352
3348
  }
2353
3349
  function findFirstTargetByPath(result, paths) {
2354
3350
  for (const path of paths ?? []) {
@@ -2407,7 +3403,7 @@ function findFirstTargetByKey(result, target, depth = 0, path = []) {
2407
3403
  }
2408
3404
  return null;
2409
3405
  }
2410
- if (!isRecord2(result)) return null;
3406
+ if (!isRecord3(result)) return null;
2411
3407
  const patterns = TARGET_FALLBACK_KEYS[target] ?? [
2412
3408
  new RegExp(`^${target}$`, "i")
2413
3409
  ];
@@ -2467,7 +3463,7 @@ function normalizeJobChangeStatus(value) {
2467
3463
  function firstExperienceDate(value) {
2468
3464
  if (!Array.isArray(value)) return null;
2469
3465
  for (const entry of value) {
2470
- if (!isRecord2(entry)) continue;
3466
+ if (!isRecord3(entry)) continue;
2471
3467
  const date = normalizeString(
2472
3468
  entry.start_date ?? entry.started_at ?? entry.startDate
2473
3469
  );
@@ -2476,10 +3472,10 @@ function firstExperienceDate(value) {
2476
3472
  return null;
2477
3473
  }
2478
3474
  function normalizeJobChange(value) {
2479
- const record = isRecord2(value) ? value : {};
2480
- const nested = isRecord2(record.job_change) ? record.job_change : record;
2481
- const output = isRecord2(nested.output) ? nested.output : nested;
2482
- const person = isRecord2(output.person) ? output.person : {};
3475
+ const record = isRecord3(value) ? value : {};
3476
+ const nested = isRecord3(record.job_change) ? record.job_change : record;
3477
+ const output = isRecord3(nested.output) ? nested.output : nested;
3478
+ const person = isRecord3(output.person) ? output.person : {};
2483
3479
  const status = normalizeJobChangeStatus(
2484
3480
  output.status ?? output.job_change_status ?? output.job_changed ?? output.changed
2485
3481
  );
@@ -3047,11 +4043,11 @@ var Deepline = class {
3047
4043
  return new DeeplineContext(options);
3048
4044
  }
3049
4045
  };
3050
- function isRecord3(value) {
4046
+ function isRecord4(value) {
3051
4047
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3052
4048
  }
3053
4049
  function stringArrayRecord(value) {
3054
- if (!isRecord3(value)) return {};
4050
+ if (!isRecord4(value)) return {};
3055
4051
  return Object.fromEntries(
3056
4052
  Object.entries(value).map(([key, paths]) => [
3057
4053
  key,
@@ -3062,22 +4058,60 @@ function stringArrayRecord(value) {
3062
4058
  function stringArray(value) {
3063
4059
  return Array.isArray(value) ? value.map(String) : [];
3064
4060
  }
4061
+ function emailStatusExtractorConfig(value) {
4062
+ if (!isRecord4(value)) return void 0;
4063
+ const readPaths = (key) => {
4064
+ const paths = stringArray(value[key]).map((path) => path.trim()).filter(Boolean);
4065
+ return paths.length > 0 ? paths : void 0;
4066
+ };
4067
+ const provider = typeof value.provider === "string" && value.provider.trim() ? value.provider.trim() : null;
4068
+ if (!provider) return void 0;
4069
+ const config = { provider };
4070
+ for (const key of [
4071
+ "rawStatus",
4072
+ "rawScore",
4073
+ "valid",
4074
+ "deliverability",
4075
+ "catchAll",
4076
+ "mxProvider",
4077
+ "mxRecord",
4078
+ "fraudScore",
4079
+ "disposable",
4080
+ "roleBased",
4081
+ "freeEmail",
4082
+ "abuse",
4083
+ "spamtrap",
4084
+ "suspect"
4085
+ ]) {
4086
+ const paths = readPaths(key);
4087
+ if (paths) config[key] = paths;
4088
+ }
4089
+ if (isRecord4(value.statusMap)) {
4090
+ config.statusMap = value.statusMap;
4091
+ }
4092
+ if (Array.isArray(value.rules)) {
4093
+ config.rules = value.rules;
4094
+ }
4095
+ return config;
4096
+ }
3065
4097
  function extractorDescriptorRecord(value) {
3066
- if (!isRecord3(value)) return {};
4098
+ if (!isRecord4(value)) return {};
3067
4099
  return Object.fromEntries(
3068
4100
  Object.entries(value).flatMap(([key, descriptor]) => {
3069
- if (!isRecord3(descriptor)) return [];
4101
+ if (!isRecord4(descriptor)) return [];
3070
4102
  const paths = stringArray(descriptor.paths).map((path) => path.trim()).filter(Boolean);
3071
4103
  if (paths.length === 0) return [];
3072
4104
  const transforms = stringArray(descriptor.transforms).map((transform) => transform.trim()).filter(Boolean);
3073
4105
  const enumValues = stringArray(descriptor.enum).map((entry) => entry.trim()).filter(Boolean);
4106
+ const emailStatus = emailStatusExtractorConfig(descriptor.emailStatus);
3074
4107
  return [
3075
4108
  [
3076
4109
  key,
3077
4110
  {
3078
4111
  paths,
3079
4112
  ...transforms.length > 0 ? { transforms } : {},
3080
- ...enumValues.length > 0 ? { enum: enumValues } : {}
4113
+ ...enumValues.length > 0 ? { enum: enumValues } : {},
4114
+ ...emailStatus ? { emailStatus } : {}
3081
4115
  }
3082
4116
  ]
3083
4117
  ];
@@ -3087,14 +4121,14 @@ function extractorDescriptorRecord(value) {
3087
4121
  function toolExecutionEnvelopeToResult(fallbackToolId, response) {
3088
4122
  const raw = response.toolResponse?.raw ?? null;
3089
4123
  const meta = response.toolResponse?.meta;
3090
- const metadata = isRecord3(response._metadata) ? response._metadata.tool : null;
3091
- const toolMetadata = isRecord3(metadata) ? metadata : {};
4124
+ const metadata = isRecord4(response._metadata) ? response._metadata.tool : null;
4125
+ const toolMetadata = isRecord4(metadata) ? metadata : {};
3092
4126
  return createToolExecuteResult({
3093
4127
  status: typeof response.status === "string" ? response.status : "completed",
3094
4128
  jobId: typeof response.job_id === "string" ? response.job_id : void 0,
3095
4129
  result: {
3096
4130
  data: raw,
3097
- ...isRecord3(meta) ? { meta } : {}
4131
+ ...isRecord4(meta) ? { meta } : {}
3098
4132
  },
3099
4133
  metadata: {
3100
4134
  toolId: typeof toolMetadata.toolId === "string" ? toolMetadata.toolId : fallbackToolId,
@@ -3108,7 +4142,7 @@ function toolExecutionEnvelopeToResult(fallbackToolId, response) {
3108
4142
  cached: false,
3109
4143
  source: "live"
3110
4144
  },
3111
- meta: isRecord3(response.meta) ? response.meta : void 0
4145
+ meta: isRecord4(response.meta) ? response.meta : void 0
3112
4146
  });
3113
4147
  }
3114
4148
  function defineInput(schema) {
@@ -3409,6 +4443,7 @@ export {
3409
4443
  PLAY_BOOTSTRAP_TEMPLATES,
3410
4444
  PROD_URL,
3411
4445
  RateLimitError,
4446
+ RunObserveTransportUnavailableError,
3412
4447
  SDK_API_CONTRACT,
3413
4448
  SDK_VERSION,
3414
4449
  defineInput,