@ellipsis-dev/sdk 0.15.0 → 0.16.0
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/chunk-RMPAMUAE.js +208 -0
- package/dist/index.d.ts +2 -2
- package/dist/store/index.d.ts +45 -2
- package/dist/store/index.js +320 -4
- package/dist/stream/index.d.ts +1 -1
- package/dist/stream/index.js +18 -189
- package/dist/{types-DtVn9Yj3.d.ts → types-DB1hOyQE.d.ts} +120 -49
- package/package.json +1 -1
- package/schema/frames.schema.json +176 -67
- package/schema/openapi.v1.json +168 -74
package/dist/store/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SESSION_STREAM_PROTOCOL_VERSION
|
|
3
|
+
} from "../chunk-RMPAMUAE.js";
|
|
4
|
+
|
|
1
5
|
// src/store/lifecycle.ts
|
|
2
6
|
function sandboxOutputStep(payload) {
|
|
3
7
|
if (typeof payload.step === "string" && payload.step) return payload.step;
|
|
@@ -366,6 +370,8 @@ function recordToItems(record, keyBase, options = {}) {
|
|
|
366
370
|
const text = lifecycleText(record.record_type, record.payload);
|
|
367
371
|
return text ? [{ key: keyBase, kind: "notice", text, spaceBefore: true }] : [];
|
|
368
372
|
}
|
|
373
|
+
default:
|
|
374
|
+
return [];
|
|
369
375
|
}
|
|
370
376
|
}
|
|
371
377
|
function isConnectVisibleRecord(record) {
|
|
@@ -481,7 +487,8 @@ function groupRecordsToChatTurns(records) {
|
|
|
481
487
|
durationMs: null,
|
|
482
488
|
costUsd: null,
|
|
483
489
|
tokens: null,
|
|
484
|
-
resumed: pendingResume
|
|
490
|
+
resumed: pendingResume,
|
|
491
|
+
isError: false
|
|
485
492
|
});
|
|
486
493
|
pendingResume = false;
|
|
487
494
|
openIdx = turns.length - 1;
|
|
@@ -547,16 +554,18 @@ function groupRecordsToChatTurns(records) {
|
|
|
547
554
|
durationMs: null,
|
|
548
555
|
costUsd: null,
|
|
549
556
|
tokens: null,
|
|
550
|
-
resumed: false
|
|
557
|
+
resumed: false,
|
|
558
|
+
isError: false
|
|
551
559
|
});
|
|
552
560
|
continue;
|
|
553
561
|
}
|
|
554
562
|
if (record.record_format === "codex_jsonl@1") {
|
|
555
563
|
codexEventIntoTurns(record, record.payload, {
|
|
556
564
|
openAgentTurn,
|
|
557
|
-
closeTurn: (completedAt) => {
|
|
565
|
+
closeTurn: (completedAt, isError) => {
|
|
558
566
|
if (openIdx >= 0) {
|
|
559
567
|
turns[openIdx].completedAt = completedAt;
|
|
568
|
+
if (isError) turns[openIdx].isError = true;
|
|
560
569
|
openIdx = -1;
|
|
561
570
|
}
|
|
562
571
|
},
|
|
@@ -579,6 +588,7 @@ function groupRecordsToChatTurns(records) {
|
|
|
579
588
|
if (typeof data.cost_usd === "number") {
|
|
580
589
|
turns[openIdx].costUsd = data.cost_usd;
|
|
581
590
|
}
|
|
591
|
+
if (data.is_error) turns[openIdx].isError = true;
|
|
582
592
|
openIdx = -1;
|
|
583
593
|
}
|
|
584
594
|
continue;
|
|
@@ -590,6 +600,7 @@ function groupRecordsToChatTurns(records) {
|
|
|
590
600
|
openIdx = -1;
|
|
591
601
|
turns.push({
|
|
592
602
|
resumed: false,
|
|
603
|
+
isError: false,
|
|
593
604
|
key: nextKey(record.id),
|
|
594
605
|
role: "user",
|
|
595
606
|
nodes: [
|
|
@@ -670,7 +681,7 @@ function groupRecordsToChatTurns(records) {
|
|
|
670
681
|
}
|
|
671
682
|
function codexEventIntoTurns(record, event, ops) {
|
|
672
683
|
if (event.type === "turn.completed" || event.type === "turn.failed") {
|
|
673
|
-
ops.closeTurn(record.created_at);
|
|
684
|
+
ops.closeTurn(record.created_at, event.type === "turn.failed");
|
|
674
685
|
return;
|
|
675
686
|
}
|
|
676
687
|
if (event.type === "error") {
|
|
@@ -768,6 +779,289 @@ function codexEventIntoTurns(record, event, ops) {
|
|
|
768
779
|
}
|
|
769
780
|
}
|
|
770
781
|
|
|
782
|
+
// src/store/derive.ts
|
|
783
|
+
function recordSlice(records) {
|
|
784
|
+
return records;
|
|
785
|
+
}
|
|
786
|
+
function humanDuration(seconds) {
|
|
787
|
+
const clamped = Math.max(0, seconds);
|
|
788
|
+
if (clamped === 0) return "0s";
|
|
789
|
+
if (clamped < 1) return `${Math.round(clamped * 1e3)}ms`;
|
|
790
|
+
if (clamped < 5) {
|
|
791
|
+
const s2 = clamped.toFixed(1);
|
|
792
|
+
return s2.endsWith(".0") ? `${Math.round(clamped)}s` : `${s2}s`;
|
|
793
|
+
}
|
|
794
|
+
const total = Math.round(clamped);
|
|
795
|
+
const h = Math.floor(total / 3600);
|
|
796
|
+
const m = Math.floor(total % 3600 / 60);
|
|
797
|
+
const s = total % 60;
|
|
798
|
+
const bits = [];
|
|
799
|
+
if (h > 0) bits.push(`${h}h`);
|
|
800
|
+
if (m > 0) bits.push(`${m}m`);
|
|
801
|
+
if (s > 0 || bits.length === 0) bits.push(`${s}s`);
|
|
802
|
+
return bits.join(" ");
|
|
803
|
+
}
|
|
804
|
+
function sessionLogText(recordType, payload) {
|
|
805
|
+
const p = payload;
|
|
806
|
+
switch (recordType) {
|
|
807
|
+
case "session_idle":
|
|
808
|
+
return "Session asleep";
|
|
809
|
+
case "session_starting": {
|
|
810
|
+
const wake = typeof p.wake_index === "number" ? p.wake_index : 0;
|
|
811
|
+
const attempt = typeof p.attempt === "number" ? p.attempt : 0;
|
|
812
|
+
if (attempt > 0) return "Restarting the sandbox after a transient error\u2026";
|
|
813
|
+
return wake > 0 ? "Waking the session\u2026" : null;
|
|
814
|
+
}
|
|
815
|
+
case "session_retrying":
|
|
816
|
+
return typeof p.reason === "string" && p.reason ? `Retrying \xB7 ${p.reason}` : "Retrying after a transient error\u2026";
|
|
817
|
+
case "session_resumed":
|
|
818
|
+
return "Session awake";
|
|
819
|
+
case "session_cancelled": {
|
|
820
|
+
const reason = typeof p.reason === "string" && p.reason ? ` \xB7 ${p.reason}` : "";
|
|
821
|
+
return `Session cancelled${reason}`;
|
|
822
|
+
}
|
|
823
|
+
default:
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
function awaitingAgentPhase(records) {
|
|
828
|
+
let inFlight = false;
|
|
829
|
+
let sawAgent = false;
|
|
830
|
+
for (const r of records) {
|
|
831
|
+
if (r.source !== "lifecycle") {
|
|
832
|
+
sawAgent = true;
|
|
833
|
+
} else {
|
|
834
|
+
if (r.record_type === "turn_started") inFlight = true;
|
|
835
|
+
else if (r.record_type === "turn_completed" || r.record_type === "turn_failed") {
|
|
836
|
+
inFlight = false;
|
|
837
|
+
} else if (r.record_type === "session_starting" || r.record_type === "session_retrying") {
|
|
838
|
+
inFlight = false;
|
|
839
|
+
sawAgent = false;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (!inFlight) return null;
|
|
844
|
+
return sawAgent ? "turn" : "boot";
|
|
845
|
+
}
|
|
846
|
+
function deliveredUnechoedSends(records) {
|
|
847
|
+
const received = /* @__PURE__ */ new Map();
|
|
848
|
+
const delivered = /* @__PURE__ */ new Map();
|
|
849
|
+
const failedTurns = /* @__PURE__ */ new Set();
|
|
850
|
+
const echoed = /* @__PURE__ */ new Set();
|
|
851
|
+
for (const r of records) {
|
|
852
|
+
if (r.session_message_id != null) echoed.add(r.session_message_id);
|
|
853
|
+
if (r.source !== "lifecycle") continue;
|
|
854
|
+
if (r.record_type === "turn_failed") {
|
|
855
|
+
if (typeof r.payload.turn_id === "string")
|
|
856
|
+
failedTurns.add(r.payload.turn_id);
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const id = typeof r.payload.message_id === "string" ? r.payload.message_id : null;
|
|
860
|
+
if (!id) continue;
|
|
861
|
+
if (r.record_type === "message_received") {
|
|
862
|
+
if (!received.has(id))
|
|
863
|
+
received.set(
|
|
864
|
+
id,
|
|
865
|
+
typeof r.payload.body === "string" ? r.payload.body : ""
|
|
866
|
+
);
|
|
867
|
+
} else if (r.record_type === "message_delivered") {
|
|
868
|
+
delivered.set(
|
|
869
|
+
id,
|
|
870
|
+
typeof r.payload.turn_id === "string" ? r.payload.turn_id : ""
|
|
871
|
+
);
|
|
872
|
+
} else if (r.record_type === "message_requeued") delivered.delete(id);
|
|
873
|
+
}
|
|
874
|
+
const out = [];
|
|
875
|
+
for (const [id, body] of received) {
|
|
876
|
+
const turnId = delivered.get(id);
|
|
877
|
+
if (turnId === void 0 || echoed.has(id)) continue;
|
|
878
|
+
out.push({ id, body, cancelled: failedTurns.has(turnId) });
|
|
879
|
+
}
|
|
880
|
+
return out;
|
|
881
|
+
}
|
|
882
|
+
function msLabel(ms) {
|
|
883
|
+
if (typeof ms !== "number" || !isFinite(ms) || ms < 0) return null;
|
|
884
|
+
return humanDuration(ms / 1e3);
|
|
885
|
+
}
|
|
886
|
+
function imageStepLabel(step) {
|
|
887
|
+
switch (step) {
|
|
888
|
+
case "build":
|
|
889
|
+
return "Building image";
|
|
890
|
+
case "container":
|
|
891
|
+
return "Starting container";
|
|
892
|
+
case "smoke":
|
|
893
|
+
return "Smoke check";
|
|
894
|
+
default:
|
|
895
|
+
return step;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function hookPhrase(step) {
|
|
899
|
+
switch (step) {
|
|
900
|
+
case "setup":
|
|
901
|
+
case "image.setup":
|
|
902
|
+
return "Building image";
|
|
903
|
+
case "clone":
|
|
904
|
+
return "Fetching repositories";
|
|
905
|
+
case "post_start":
|
|
906
|
+
return "Post-start setup";
|
|
907
|
+
case "post_clone":
|
|
908
|
+
return "Post-clone setup";
|
|
909
|
+
default:
|
|
910
|
+
return step;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function stepLabel(phase, step) {
|
|
914
|
+
if (step) {
|
|
915
|
+
if (phase === "hooks") return hookPhrase(step);
|
|
916
|
+
if (phase === "image") return imageStepLabel(step);
|
|
917
|
+
return step;
|
|
918
|
+
}
|
|
919
|
+
return sandboxPhaseLabel(phase);
|
|
920
|
+
}
|
|
921
|
+
function deriveSandboxState(records, minFeedSeq) {
|
|
922
|
+
let seen = false;
|
|
923
|
+
let headline = "Starting cloud agent\u2026";
|
|
924
|
+
let done = false;
|
|
925
|
+
let sandboxDone = false;
|
|
926
|
+
let readySeconds = null;
|
|
927
|
+
let configName = null;
|
|
928
|
+
let configCommitSha = null;
|
|
929
|
+
let log = [];
|
|
930
|
+
let open = /* @__PURE__ */ new Map();
|
|
931
|
+
const push = (record, kind, text) => {
|
|
932
|
+
const entry = { key: `${record.feed_seq}:${log.length}`, kind, text };
|
|
933
|
+
log.push(entry);
|
|
934
|
+
return entry;
|
|
935
|
+
};
|
|
936
|
+
const reset = () => {
|
|
937
|
+
log = [];
|
|
938
|
+
open = /* @__PURE__ */ new Map();
|
|
939
|
+
sandboxDone = false;
|
|
940
|
+
};
|
|
941
|
+
for (const record of records) {
|
|
942
|
+
if (record.feed_seq <= minFeedSeq || record.source !== "lifecycle")
|
|
943
|
+
continue;
|
|
944
|
+
const p = record.payload;
|
|
945
|
+
switch (record.record_type) {
|
|
946
|
+
case "session_scheduled": {
|
|
947
|
+
seen = true;
|
|
948
|
+
headline = "Session scheduled\u2026";
|
|
949
|
+
done = false;
|
|
950
|
+
configName = typeof p.config_name === "string" && p.config_name ? p.config_name : null;
|
|
951
|
+
configCommitSha = typeof p.config_commit_sha === "string" && p.config_commit_sha ? p.config_commit_sha : null;
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
case "session_starting":
|
|
955
|
+
case "session_retrying": {
|
|
956
|
+
seen = true;
|
|
957
|
+
const text = lifecycleText(record.record_type, p);
|
|
958
|
+
headline = !text || text === "Session starting\u2026" ? "Starting cloud agent\u2026" : text;
|
|
959
|
+
done = false;
|
|
960
|
+
readySeconds = null;
|
|
961
|
+
reset();
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
case "session_resumed":
|
|
965
|
+
case "session_idle": {
|
|
966
|
+
seen = true;
|
|
967
|
+
done = true;
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
case "sandbox_starting": {
|
|
971
|
+
seen = true;
|
|
972
|
+
reset();
|
|
973
|
+
push(record, "step", "Starting sandbox\u2026");
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
case "sandbox_phase": {
|
|
977
|
+
seen = true;
|
|
978
|
+
const phase = typeof p.phase === "string" && p.phase ? p.phase : "setup";
|
|
979
|
+
const step = typeof p.step === "string" && p.step ? p.step : null;
|
|
980
|
+
const key = step ? `${phase}:${step}` : phase;
|
|
981
|
+
const label = stepLabel(phase, step);
|
|
982
|
+
if (p.status === "completed" || p.status === "failed") {
|
|
983
|
+
const detail = p.detail && typeof p.detail === "object" ? p.detail : {};
|
|
984
|
+
const tier = cacheTierLabel(detail.cache_tier);
|
|
985
|
+
const dur = msLabel(p.duration_ms);
|
|
986
|
+
const failed = p.status === "failed";
|
|
987
|
+
const base = failed ? `${label} failed` : label;
|
|
988
|
+
const text = [
|
|
989
|
+
base,
|
|
990
|
+
...tier ? [tier] : [],
|
|
991
|
+
...dur ? [dur] : []
|
|
992
|
+
].join(" \xB7 ");
|
|
993
|
+
const line = open.get(key);
|
|
994
|
+
if (line) {
|
|
995
|
+
line.kind = failed ? "failed" : "done";
|
|
996
|
+
line.text = text;
|
|
997
|
+
open.delete(key);
|
|
998
|
+
} else {
|
|
999
|
+
push(record, failed ? "failed" : "done", text);
|
|
1000
|
+
}
|
|
1001
|
+
} else if (!open.has(key)) {
|
|
1002
|
+
open.set(key, push(record, "step", `${label}\u2026`));
|
|
1003
|
+
}
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
case "sandbox_output": {
|
|
1007
|
+
seen = true;
|
|
1008
|
+
for (const l of sandboxOutputLines(p)) push(record, "output", l);
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
case "sandbox_ready": {
|
|
1012
|
+
seen = true;
|
|
1013
|
+
for (const [, line] of open) line.kind = "done";
|
|
1014
|
+
open = /* @__PURE__ */ new Map();
|
|
1015
|
+
const timings = p.phase_timings && typeof p.phase_timings === "object" ? Object.values(p.phase_timings) : [];
|
|
1016
|
+
const totalSeconds = timings.reduce(
|
|
1017
|
+
(acc, v) => typeof v === "number" && isFinite(v) ? acc + v : acc,
|
|
1018
|
+
0
|
|
1019
|
+
);
|
|
1020
|
+
const tier = cacheTierLabel(p.cache_tier);
|
|
1021
|
+
push(
|
|
1022
|
+
record,
|
|
1023
|
+
"done",
|
|
1024
|
+
[
|
|
1025
|
+
"Sandbox ready",
|
|
1026
|
+
...tier ? [tier] : [],
|
|
1027
|
+
...totalSeconds > 0 ? [humanDuration(totalSeconds)] : []
|
|
1028
|
+
].join(" \xB7 ")
|
|
1029
|
+
);
|
|
1030
|
+
sandboxDone = true;
|
|
1031
|
+
readySeconds = totalSeconds > 0 ? totalSeconds : null;
|
|
1032
|
+
done = true;
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
default:
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
const full = configName ? [
|
|
1040
|
+
{
|
|
1041
|
+
key: "config",
|
|
1042
|
+
kind: "done",
|
|
1043
|
+
text: `Using ${configName}${configCommitSha ? ` @ ${configCommitSha.slice(0, 7)}` : ""}`
|
|
1044
|
+
},
|
|
1045
|
+
...log
|
|
1046
|
+
] : log;
|
|
1047
|
+
return seen ? {
|
|
1048
|
+
headline,
|
|
1049
|
+
done,
|
|
1050
|
+
readySeconds,
|
|
1051
|
+
sandboxDone,
|
|
1052
|
+
configName,
|
|
1053
|
+
configCommitSha,
|
|
1054
|
+
log: full
|
|
1055
|
+
} : null;
|
|
1056
|
+
}
|
|
1057
|
+
function sandboxSummary(sandbox) {
|
|
1058
|
+
const seconds = sandbox?.readySeconds ?? null;
|
|
1059
|
+
return seconds ? `Sandbox ready in ${humanDuration(seconds)}` : "Sandbox started";
|
|
1060
|
+
}
|
|
1061
|
+
function lastLines(log, max) {
|
|
1062
|
+
return log.length <= max ? [...log] : log.slice(log.length - max);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
771
1065
|
// src/store/index.ts
|
|
772
1066
|
var TERMINAL_SESSION_STATUSES = /* @__PURE__ */ new Set([
|
|
773
1067
|
"completed",
|
|
@@ -781,6 +1075,18 @@ function isConversationOver(session) {
|
|
|
781
1075
|
}
|
|
782
1076
|
return TERMINAL_SESSION_STATUSES.has(session.status);
|
|
783
1077
|
}
|
|
1078
|
+
function seedTranscriptStore(store, seed) {
|
|
1079
|
+
store.ingest({
|
|
1080
|
+
type: "snapshot",
|
|
1081
|
+
protocol: SESSION_STREAM_PROTOCOL_VERSION,
|
|
1082
|
+
earliest_feed_seq: seed.earliestFeedSeq ?? null,
|
|
1083
|
+
session: seed.session,
|
|
1084
|
+
messages: [...seed.messages ?? []]
|
|
1085
|
+
});
|
|
1086
|
+
const ordered = [...seed.records].sort((a, b) => a.feed_seq - b.feed_seq);
|
|
1087
|
+
if (ordered.length)
|
|
1088
|
+
store.ingest({ type: "records_append", records: ordered });
|
|
1089
|
+
}
|
|
784
1090
|
var EMPTY_SNAPSHOT = {
|
|
785
1091
|
session: null,
|
|
786
1092
|
records: [],
|
|
@@ -961,28 +1267,38 @@ var SessionTranscriptStore = class {
|
|
|
961
1267
|
};
|
|
962
1268
|
export {
|
|
963
1269
|
SessionTranscriptStore,
|
|
1270
|
+
awaitingAgentPhase,
|
|
964
1271
|
cacheTierLabel,
|
|
965
1272
|
clampLines,
|
|
966
1273
|
codexChangedPaths,
|
|
967
1274
|
codexEventToItems,
|
|
968
1275
|
codexMcpToolName,
|
|
969
1276
|
collapseToolRuns,
|
|
1277
|
+
deliveredUnechoedSends,
|
|
1278
|
+
deriveSandboxState,
|
|
970
1279
|
emptySessionTranscriptSnapshot,
|
|
971
1280
|
eventToItems,
|
|
972
1281
|
foldCosts,
|
|
973
1282
|
formatDuration,
|
|
974
1283
|
groupRecordsToChatTurns,
|
|
1284
|
+
hookPhrase,
|
|
1285
|
+
humanDuration,
|
|
975
1286
|
isConnectVisibleRecord,
|
|
976
1287
|
isConversationOver,
|
|
1288
|
+
lastLines,
|
|
977
1289
|
lifecycleText,
|
|
978
1290
|
oneLine,
|
|
979
1291
|
pendingToolCalls,
|
|
1292
|
+
recordSlice,
|
|
980
1293
|
recordToItems,
|
|
981
1294
|
resultCostUsd,
|
|
982
1295
|
sandboxOutputLine,
|
|
983
1296
|
sandboxOutputLines,
|
|
984
1297
|
sandboxOutputStep,
|
|
985
1298
|
sandboxPhaseLabel,
|
|
1299
|
+
sandboxSummary,
|
|
1300
|
+
seedTranscriptStore,
|
|
1301
|
+
sessionLogText,
|
|
986
1302
|
statusActivityText,
|
|
987
1303
|
summarizeToolInput,
|
|
988
1304
|
toolResultText
|
package/dist/stream/index.d.ts
CHANGED
package/dist/stream/index.js
CHANGED
|
@@ -1,192 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
};
|
|
19
|
-
var StreamAuthError = class extends Error {
|
|
20
|
-
constructor(message) {
|
|
21
|
-
super(message);
|
|
22
|
-
this.name = "StreamAuthError";
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
function streamQuery(afterSeq) {
|
|
26
|
-
const base = `protocol=${SESSION_STREAM_PROTOCOL_VERSION}`;
|
|
27
|
-
return afterSeq > 0 ? `${base}&after_seq=${afterSeq}` : base;
|
|
28
|
-
}
|
|
29
|
-
var HEARTBEAT_TIMEOUT_MS = 45e3;
|
|
30
|
-
var DEFAULT_MAX_RECONNECTS = 5;
|
|
31
|
-
function classifyCloseCode(code) {
|
|
32
|
-
switch (code) {
|
|
33
|
-
case WS_CLOSE_NORMAL:
|
|
34
|
-
return "normal";
|
|
35
|
-
case WS_CLOSE_AUTH_FAILED:
|
|
36
|
-
case 4401:
|
|
37
|
-
// dashboard ticket door: bad/expired ticket
|
|
38
|
-
case 4403:
|
|
39
|
-
return "auth";
|
|
40
|
-
case WS_CLOSE_UNSUPPORTED_PROTOCOL_VERSION:
|
|
41
|
-
case WS_CLOSE_NO_PROTOCOL:
|
|
42
|
-
return "unsupported";
|
|
43
|
-
default:
|
|
44
|
-
return "retry";
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
function nextReconnectDelayMs(attempt) {
|
|
48
|
-
const base = 500;
|
|
49
|
-
const max = 8e3;
|
|
50
|
-
return Math.min(max, base * 2 ** Math.max(0, attempt - 1));
|
|
51
|
-
}
|
|
52
|
-
function decideReconnect(params) {
|
|
53
|
-
const { closeKind, everReceivedFrame, attempt, maxReconnects } = params;
|
|
54
|
-
if (closeKind === "auth") return { action: "fail-auth" };
|
|
55
|
-
if (closeKind === "unsupported") return { action: "fallback" };
|
|
56
|
-
const cap = everReceivedFrame ? maxReconnects : Math.min(2, maxReconnects);
|
|
57
|
-
if (attempt >= cap) return { action: "fallback" };
|
|
58
|
-
return { action: "reconnect", delayMs: nextReconnectDelayMs(attempt) };
|
|
59
|
-
}
|
|
60
|
-
function connectOnce(sock, emit, signal) {
|
|
61
|
-
return new Promise((resolve) => {
|
|
62
|
-
let settled = false;
|
|
63
|
-
let heartbeat;
|
|
64
|
-
const finish = (result) => {
|
|
65
|
-
if (settled) return;
|
|
66
|
-
settled = true;
|
|
67
|
-
if (heartbeat) clearTimeout(heartbeat);
|
|
68
|
-
if (signal) signal.removeEventListener("abort", onAbort);
|
|
69
|
-
sock.close();
|
|
70
|
-
resolve(result);
|
|
71
|
-
};
|
|
72
|
-
const onAbort = () => finish({ kind: "aborted" });
|
|
73
|
-
const bumpHeartbeat = () => {
|
|
74
|
-
if (heartbeat) clearTimeout(heartbeat);
|
|
75
|
-
heartbeat = setTimeout(
|
|
76
|
-
() => finish({ kind: "error", err: new Error("heartbeat timeout") }),
|
|
77
|
-
HEARTBEAT_TIMEOUT_MS
|
|
78
|
-
);
|
|
79
|
-
};
|
|
80
|
-
if (signal) {
|
|
81
|
-
if (signal.aborted) {
|
|
82
|
-
finish({ kind: "aborted" });
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
signal.addEventListener("abort", onAbort);
|
|
86
|
-
}
|
|
87
|
-
sock.onOpen(() => bumpHeartbeat());
|
|
88
|
-
sock.onMessage((data) => {
|
|
89
|
-
bumpHeartbeat();
|
|
90
|
-
let frame;
|
|
91
|
-
try {
|
|
92
|
-
frame = JSON.parse(data);
|
|
93
|
-
} catch {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
emit(frame);
|
|
97
|
-
if (frame.type === "done") {
|
|
98
|
-
finish({ kind: "done" });
|
|
99
|
-
} else if (frame.type === "error") {
|
|
100
|
-
finish({
|
|
101
|
-
kind: "frameError",
|
|
102
|
-
message: frame.message ?? "stream error"
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
sock.onClose((code) => finish({ kind: "closed", code }));
|
|
107
|
-
sock.onError((err) => finish({ kind: "error", err }));
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
function sleep(ms, signal) {
|
|
111
|
-
return new Promise((resolve) => {
|
|
112
|
-
const timer = setTimeout(resolve, ms);
|
|
113
|
-
signal?.addEventListener(
|
|
114
|
-
"abort",
|
|
115
|
-
() => {
|
|
116
|
-
clearTimeout(timer);
|
|
117
|
-
resolve();
|
|
118
|
-
},
|
|
119
|
-
{ once: true }
|
|
120
|
-
);
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
async function streamSession(opts) {
|
|
124
|
-
const maxReconnects = opts.maxReconnects ?? DEFAULT_MAX_RECONNECTS;
|
|
125
|
-
let afterSeq = opts.afterSeq ?? 0;
|
|
126
|
-
let everReceivedFrame = false;
|
|
127
|
-
let attempt = 0;
|
|
128
|
-
let lastStatusWord = "";
|
|
129
|
-
let lastExitStatus = null;
|
|
130
|
-
const emit = (frame) => {
|
|
131
|
-
everReceivedFrame = true;
|
|
132
|
-
attempt = 0;
|
|
133
|
-
if (frame.type === "records_append") {
|
|
134
|
-
const records = frame.records;
|
|
135
|
-
for (const record of records) {
|
|
136
|
-
if (typeof record.feed_seq === "number") {
|
|
137
|
-
afterSeq = Math.max(afterSeq, record.feed_seq);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
} else if (frame.type === "snapshot" || frame.type === "session") {
|
|
141
|
-
const session = frame.session;
|
|
142
|
-
lastStatusWord = sessionStatusWord(session);
|
|
143
|
-
lastExitStatus = session.exit_status ?? null;
|
|
144
|
-
}
|
|
145
|
-
opts.onFrame(frame);
|
|
146
|
-
};
|
|
147
|
-
for (; ; ) {
|
|
148
|
-
if (opts.signal?.aborted) return { type: "aborted" };
|
|
149
|
-
let res;
|
|
150
|
-
try {
|
|
151
|
-
const sock = await opts.openSocket({
|
|
152
|
-
sessionId: opts.sessionId,
|
|
153
|
-
afterSeq,
|
|
154
|
-
query: streamQuery(afterSeq)
|
|
155
|
-
});
|
|
156
|
-
res = await connectOnce(sock, emit, opts.signal);
|
|
157
|
-
} catch (err) {
|
|
158
|
-
res = {
|
|
159
|
-
kind: "error",
|
|
160
|
-
err: err instanceof Error ? err : new Error(String(err))
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
if (res.kind === "done") {
|
|
164
|
-
return {
|
|
165
|
-
type: "done",
|
|
166
|
-
status: lastStatusWord,
|
|
167
|
-
exitStatus: lastExitStatus
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
if (res.kind === "frameError")
|
|
171
|
-
return { type: "error", message: res.message };
|
|
172
|
-
if (res.kind === "aborted") return { type: "aborted" };
|
|
173
|
-
attempt++;
|
|
174
|
-
const decision = decideReconnect({
|
|
175
|
-
closeKind: res.kind === "closed" ? classifyCloseCode(res.code) : void 0,
|
|
176
|
-
everReceivedFrame,
|
|
177
|
-
attempt,
|
|
178
|
-
maxReconnects
|
|
179
|
-
});
|
|
180
|
-
if (decision.action === "fail-auth") {
|
|
181
|
-
throw new StreamAuthError("not authorized to stream this session");
|
|
182
|
-
}
|
|
183
|
-
if (decision.action === "fallback") {
|
|
184
|
-
const why = res.kind === "error" ? res.err.message : `stream closed (code ${res.code})`;
|
|
185
|
-
throw new StreamUnavailableError(why);
|
|
186
|
-
}
|
|
187
|
-
await sleep(decision.delayMs ?? 0, opts.signal);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
SESSION_STREAM_PROTOCOL_VERSION,
|
|
3
|
+
StreamAuthError,
|
|
4
|
+
StreamUnavailableError,
|
|
5
|
+
WS_CLOSE_AUTH_FAILED,
|
|
6
|
+
WS_CLOSE_GOING_AWAY,
|
|
7
|
+
WS_CLOSE_NORMAL,
|
|
8
|
+
WS_CLOSE_NO_PROTOCOL,
|
|
9
|
+
WS_CLOSE_OVER_CAPACITY,
|
|
10
|
+
WS_CLOSE_SERVER_ERROR,
|
|
11
|
+
WS_CLOSE_UNSUPPORTED_PROTOCOL_VERSION,
|
|
12
|
+
classifyCloseCode,
|
|
13
|
+
decideReconnect,
|
|
14
|
+
nextReconnectDelayMs,
|
|
15
|
+
sessionStatusWord,
|
|
16
|
+
streamQuery,
|
|
17
|
+
streamSession
|
|
18
|
+
} from "../chunk-RMPAMUAE.js";
|
|
190
19
|
export {
|
|
191
20
|
SESSION_STREAM_PROTOCOL_VERSION,
|
|
192
21
|
StreamAuthError,
|