@ellipsis-dev/sdk 0.34.0 → 0.35.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.
@@ -1,7 +1,3 @@
1
- import {
2
- SESSION_STREAM_PROTOCOL_VERSION
3
- } from "../chunk-6V7QTLLF.js";
4
-
5
1
  // src/store/claude.ts
6
2
  function claudePayload(record) {
7
3
  if (record.kind === "claude_sdk") return record.payload;
@@ -104,124 +100,6 @@ function codexAppServerContent(event) {
104
100
  }
105
101
  }
106
102
 
107
- // src/store/progress.ts
108
- function messageProgress(records) {
109
- const messages = /* @__PURE__ */ new Map();
110
- const pending = /* @__PURE__ */ new Map();
111
- const turns = /* @__PURE__ */ new Map();
112
- let ready = false;
113
- const getTurn = (id) => {
114
- let turn = turns.get(id);
115
- if (!turn) {
116
- turn = {
117
- timing: { id, startedAt: null, completedAt: null },
118
- phase: ready ? "working" : "starting",
119
- messages: /* @__PURE__ */ new Set()
120
- };
121
- turns.set(id, turn);
122
- }
123
- return turn;
124
- };
125
- const attach = (messageId, turnId) => {
126
- const message = messages.get(messageId);
127
- if (!message) return;
128
- const turn = getTurn(turnId);
129
- if (!turn.messages.has(messageId)) {
130
- turn.messages.add(messageId);
131
- message.turns.push(turn.timing);
132
- }
133
- pending.delete(messageId);
134
- message.phase = turn.phase;
135
- };
136
- const setPending = (turnId, phase) => {
137
- for (const [id, turn] of pending) {
138
- if (turnId == null || turn == null || turn === turnId)
139
- messages.get(id).phase = phase;
140
- }
141
- };
142
- for (const record of records) {
143
- if (record.kind === "platform") {
144
- const p = record.payload;
145
- const messageId = typeof p.message_id === "string" ? p.message_id : null;
146
- const turnId = typeof p.turn_id === "string" ? p.turn_id : record.turn_id;
147
- switch (record.record_type) {
148
- case "message_received":
149
- if (messageId && !messages.has(messageId)) {
150
- messages.set(messageId, { phase: "queued", turns: [] });
151
- if (turnId && turns.has(turnId)) attach(messageId, turnId);
152
- else pending.set(messageId, turnId ?? null);
153
- }
154
- break;
155
- case "message_delivered":
156
- if (messageId && turnId) attach(messageId, turnId);
157
- break;
158
- case "message_requeued":
159
- if (messageId && messages.has(messageId)) {
160
- const requeued = typeof p.requeued_turn_id === "string" ? p.requeued_turn_id : null;
161
- pending.set(messageId, requeued);
162
- messages.get(messageId).phase = "queued";
163
- }
164
- break;
165
- case "environment_phase":
166
- case "environment_output":
167
- ready = false;
168
- setPending(turnId ?? null, "preparing");
169
- break;
170
- case "environment_ready":
171
- ready = false;
172
- setPending(turnId ?? null, "starting");
173
- break;
174
- case "turn_started":
175
- if (turnId) {
176
- getTurn(turnId).timing.startedAt = record.created_at;
177
- for (const [id, turn] of pending) {
178
- if (turn === turnId) attach(id, turnId);
179
- }
180
- }
181
- break;
182
- case "turn_ended":
183
- if (turnId) {
184
- const turn = getTurn(turnId);
185
- turn.timing.completedAt = record.created_at;
186
- turn.phase = null;
187
- for (const id of turn.messages) {
188
- if (!pending.has(id)) messages.get(id).phase = null;
189
- }
190
- for (const [id, pendingTurn] of pending) {
191
- if (pendingTurn === turnId) {
192
- pending.delete(id);
193
- messages.get(id).phase = null;
194
- }
195
- }
196
- }
197
- break;
198
- case "session_closed":
199
- ready = false;
200
- for (const id of pending.keys()) messages.get(id).phase = null;
201
- break;
202
- }
203
- continue;
204
- }
205
- const claude = claudePayload(record);
206
- const agentStarted = record.kind === "codex_app_server" && record.payload.method === "turn/started" || record.kind === "codex" && record.payload.type === "turn.started" || claude != null && (claude.kind === "assistant" || claude.kind === "user" || claude.kind === "system" && claude.subtype === "init");
207
- if (agentStarted) {
208
- ready = true;
209
- if (record.turn_id) {
210
- const turn = getTurn(record.turn_id);
211
- if (turn.timing.completedAt == null) {
212
- turn.timing.startedAt ??= record.created_at;
213
- turn.phase = "working";
214
- for (const id of turn.messages) messages.get(id).phase = "working";
215
- }
216
- }
217
- }
218
- if (record.session_message_id && record.turn_id) {
219
- attach(record.session_message_id, record.turn_id);
220
- }
221
- }
222
- return messages;
223
- }
224
-
225
103
  // src/store/lifecycle.ts
226
104
  function environmentOutputStep(payload) {
227
105
  if (typeof payload.step === "string" && payload.step) return payload.step;
@@ -623,6 +501,55 @@ function recordToItems(record, keyBase, options = {}) {
623
501
  }
624
502
  ] : [];
625
503
  }
504
+ case "gemini": {
505
+ const content = geminiStreamContent(record.payload);
506
+ if (!content) return [];
507
+ if (content.kind === "end") {
508
+ return content.isError ? [
509
+ {
510
+ key: keyBase,
511
+ kind: "summary",
512
+ text: "turn ended with an error",
513
+ isError: true,
514
+ spaceBefore: true
515
+ }
516
+ ] : [];
517
+ }
518
+ if (content.kind === "tool") {
519
+ return [
520
+ {
521
+ key: keyBase,
522
+ kind: "tool",
523
+ gutter: "\u25CF",
524
+ text: content.name,
525
+ detail: content.summary ? `(${content.summary})` : void 0,
526
+ spaceBefore: true,
527
+ tool: { name: content.name, input: content.input ?? void 0 }
528
+ }
529
+ ];
530
+ }
531
+ if (content.kind === "tool_result") {
532
+ return [
533
+ {
534
+ key: keyBase,
535
+ kind: "tool_result",
536
+ gutter: "\u23BF",
537
+ text: content.text || "(completed)",
538
+ isError: content.isError,
539
+ spaceBefore: false
540
+ }
541
+ ];
542
+ }
543
+ return [
544
+ {
545
+ key: keyBase,
546
+ kind: content.kind,
547
+ text: content.text.trim(),
548
+ isError: content.kind === "error",
549
+ spaceBefore: true
550
+ }
551
+ ];
552
+ }
626
553
  case "platform": {
627
554
  const text2 = lifecycleText(record.record_type, record.payload);
628
555
  return text2 ? [{ key: keyBase, kind: "notice", text: text2, spaceBefore: true }] : [];
@@ -728,6 +655,155 @@ function statusActivityText(status) {
728
655
  }
729
656
  }
730
657
 
658
+ // src/store/geminiStream.ts
659
+ function geminiStreamContent(event) {
660
+ switch (event.type) {
661
+ case "message":
662
+ return event.role === "assistant" && !event.delta && event.content.trim() ? { kind: "assistant", text: event.content } : null;
663
+ case "tool_use": {
664
+ const input = event.parameters ?? null;
665
+ return {
666
+ kind: "tool",
667
+ toolId: event.tool_id,
668
+ name: event.tool_name,
669
+ input,
670
+ summary: oneLine(summarizeToolInput(event.tool_name, input ?? {}), 100)
671
+ };
672
+ }
673
+ case "tool_result":
674
+ return {
675
+ kind: "tool_result",
676
+ toolId: event.tool_id,
677
+ text: (event.output ?? event.error?.message ?? "").trim(),
678
+ isError: event.status === "error"
679
+ };
680
+ case "error":
681
+ return { kind: "error", text: event.message };
682
+ case "result":
683
+ return { kind: "end", isError: event.status === "error" };
684
+ default:
685
+ return null;
686
+ }
687
+ }
688
+
689
+ // src/store/progress.ts
690
+ function messageProgress(records) {
691
+ const messages = /* @__PURE__ */ new Map();
692
+ const pending = /* @__PURE__ */ new Map();
693
+ const turns = /* @__PURE__ */ new Map();
694
+ let ready = false;
695
+ const getTurn = (id) => {
696
+ let turn = turns.get(id);
697
+ if (!turn) {
698
+ turn = {
699
+ timing: { id, startedAt: null, completedAt: null },
700
+ phase: ready ? "working" : "starting",
701
+ messages: /* @__PURE__ */ new Set()
702
+ };
703
+ turns.set(id, turn);
704
+ }
705
+ return turn;
706
+ };
707
+ const attach = (messageId, turnId) => {
708
+ const message = messages.get(messageId);
709
+ if (!message) return;
710
+ const turn = getTurn(turnId);
711
+ if (!turn.messages.has(messageId)) {
712
+ turn.messages.add(messageId);
713
+ message.turns.push(turn.timing);
714
+ }
715
+ pending.delete(messageId);
716
+ message.phase = turn.phase;
717
+ };
718
+ const setPending = (turnId, phase) => {
719
+ for (const [id, turn] of pending) {
720
+ if (turnId == null || turn == null || turn === turnId)
721
+ messages.get(id).phase = phase;
722
+ }
723
+ };
724
+ for (const record of records) {
725
+ if (record.kind === "platform") {
726
+ const p = record.payload;
727
+ const messageId = typeof p.message_id === "string" ? p.message_id : null;
728
+ const turnId = typeof p.turn_id === "string" ? p.turn_id : record.turn_id;
729
+ switch (record.record_type) {
730
+ case "message_received":
731
+ if (messageId && !messages.has(messageId)) {
732
+ messages.set(messageId, { phase: "queued", turns: [] });
733
+ if (turnId && turns.has(turnId)) attach(messageId, turnId);
734
+ else pending.set(messageId, turnId ?? null);
735
+ }
736
+ break;
737
+ case "message_delivered":
738
+ if (messageId && turnId) attach(messageId, turnId);
739
+ break;
740
+ case "message_requeued":
741
+ if (messageId && messages.has(messageId)) {
742
+ const requeued = typeof p.requeued_turn_id === "string" ? p.requeued_turn_id : null;
743
+ pending.set(messageId, requeued);
744
+ messages.get(messageId).phase = "queued";
745
+ }
746
+ break;
747
+ case "environment_phase":
748
+ case "environment_output":
749
+ ready = false;
750
+ setPending(turnId ?? null, "preparing");
751
+ break;
752
+ case "environment_ready":
753
+ ready = false;
754
+ setPending(turnId ?? null, "starting");
755
+ break;
756
+ case "turn_started":
757
+ if (turnId) {
758
+ getTurn(turnId).timing.startedAt = record.created_at;
759
+ for (const [id, turn] of pending) {
760
+ if (turn === turnId) attach(id, turnId);
761
+ }
762
+ }
763
+ break;
764
+ case "turn_ended":
765
+ if (turnId) {
766
+ const turn = getTurn(turnId);
767
+ turn.timing.completedAt = record.created_at;
768
+ turn.phase = null;
769
+ for (const id of turn.messages) {
770
+ if (!pending.has(id)) messages.get(id).phase = null;
771
+ }
772
+ for (const [id, pendingTurn] of pending) {
773
+ if (pendingTurn === turnId) {
774
+ pending.delete(id);
775
+ messages.get(id).phase = null;
776
+ }
777
+ }
778
+ }
779
+ break;
780
+ case "session_closed":
781
+ ready = false;
782
+ for (const id of pending.keys()) messages.get(id).phase = null;
783
+ break;
784
+ }
785
+ continue;
786
+ }
787
+ const claude = claudePayload(record);
788
+ const agentStarted = record.kind === "codex_app_server" && record.payload.method === "turn/started" || record.kind === "codex" && record.payload.type === "turn.started" || record.kind === "gemini" && record.payload.type === "init" || claude != null && (claude.kind === "assistant" || claude.kind === "user" || claude.kind === "system" && claude.subtype === "init");
789
+ if (agentStarted) {
790
+ ready = true;
791
+ if (record.turn_id) {
792
+ const turn = getTurn(record.turn_id);
793
+ if (turn.timing.completedAt == null) {
794
+ turn.timing.startedAt ??= record.created_at;
795
+ turn.phase = "working";
796
+ for (const id of turn.messages) messages.get(id).phase = "working";
797
+ }
798
+ }
799
+ }
800
+ if (record.session_message_id && record.turn_id) {
801
+ attach(record.session_message_id, record.turn_id);
802
+ }
803
+ }
804
+ return messages;
805
+ }
806
+
731
807
  // src/store/chatTurns.ts
732
808
  var isInitEvent = (data) => data.kind === "system" && data.subtype === "init";
733
809
  var ENVIRONMENT_RECORD_TYPES = /* @__PURE__ */ new Set([
@@ -738,6 +814,7 @@ var ENVIRONMENT_RECORD_TYPES = /* @__PURE__ */ new Set([
738
814
  function groupRecordsToChatTurns(records, session) {
739
815
  const turns = [];
740
816
  const toolNodeByUseId = /* @__PURE__ */ new Map();
817
+ const openGeminiTools = /* @__PURE__ */ new Map();
741
818
  const userTurnByMessageId = /* @__PURE__ */ new Map();
742
819
  const turnByMessageId = /* @__PURE__ */ new Map();
743
820
  const unansweredTurnIds = /* @__PURE__ */ new Set();
@@ -897,6 +974,49 @@ function groupRecordsToChatTurns(records, session) {
897
974
  });
898
975
  continue;
899
976
  }
977
+ if (record.kind === "gemini") {
978
+ const content = geminiStreamContent(record.payload);
979
+ if (!content) continue;
980
+ if (content.kind === "end") {
981
+ if (openIdx >= 0) {
982
+ turns[openIdx].completedAt = record.created_at;
983
+ if (content.isError) turns[openIdx].isError = true;
984
+ openIdx = -1;
985
+ }
986
+ } else if (content.kind === "tool") {
987
+ const node = {
988
+ key: nextKey(record.id),
989
+ kind: "tool",
990
+ name: content.name,
991
+ input: content.input,
992
+ summary: content.summary,
993
+ result: null,
994
+ isError: false,
995
+ startedAt: record.created_at,
996
+ completedAt: null
997
+ };
998
+ openSessionTurn(record).nodes.push(node);
999
+ openGeminiTools.set(content.toolId, node);
1000
+ } else if (content.kind === "tool_result") {
1001
+ const node = openGeminiTools.get(content.toolId);
1002
+ if (node) {
1003
+ node.result = content.text;
1004
+ node.isError = content.isError;
1005
+ node.completedAt = record.created_at;
1006
+ openGeminiTools.delete(content.toolId);
1007
+ }
1008
+ } else {
1009
+ const turn2 = openSessionTurn(record);
1010
+ if (content.kind === "error") turn2.isError = true;
1011
+ turn2.nodes.push({
1012
+ key: nextKey(record.id),
1013
+ kind: "assistant",
1014
+ text: content.text.trim(),
1015
+ createdAt: record.created_at
1016
+ });
1017
+ }
1018
+ continue;
1019
+ }
900
1020
  if (record.kind === "codex_app_server") {
901
1021
  const content = codexAppServerContent(record.payload);
902
1022
  if (!content) continue;
@@ -1866,7 +1986,6 @@ function isConversationOver(session) {
1866
1986
  function seedTranscriptStore(store, seed) {
1867
1987
  store.ingest({
1868
1988
  type: "snapshot",
1869
- protocol: SESSION_STREAM_PROTOCOL_VERSION,
1870
1989
  earliest_feed_seq: seed.earliestFeedSeq ?? null,
1871
1990
  session: seed.session,
1872
1991
  messages: [...seed.messages ?? []]
@@ -1894,13 +2013,13 @@ var SessionTranscriptStore = class {
1894
2013
  snapshot = EMPTY_SNAPSHOT;
1895
2014
  listeners = /* @__PURE__ */ new Set();
1896
2015
  acknowledged = /* @__PURE__ */ new Set();
1897
- // The inbox projection (protocol v3): message_received/delivered/requeued
2016
+ // The inbox projection: message_received/delivered/requeued
1898
2017
  // RECORDS drive it; the snapshot seeds the pending set at connect. Keyed by
1899
2018
  // message id so a requeue can restore a message the delivered record
1900
2019
  // removed from the pending view.
1901
2020
  inbox = /* @__PURE__ */ new Map();
1902
2021
  // The resume cursor: the highest feed_seq received via records_append. Only
1903
- // records advance it (§3.4) — never messages/session frames. streamSession
2022
+ // records advance it — never messages/session frames. streamSession
1904
2023
  // tracks its own copy; this one seeds a NEW streamSession call (e.g. a
1905
2024
  // page-level reconnect after unmount) from already-held state.
1906
2025
  cursorSeq = 0;
@@ -1933,7 +2052,7 @@ var SessionTranscriptStore = class {
1933
2052
  // The current pending set, in feed order — what queued chips render.
1934
2053
  pendingMessages = () => [...this.inbox.values()].filter((m) => m.status === "pending").sort((a, b) => (a.feed_seq ?? 0) - (b.feed_seq ?? 0));
1935
2054
  // Fold one lifecycle record into the inbox projection; returns whether the
1936
- // pending view changed. Unknown fields/types are ignored (§3.6).
2055
+ // pending view changed. Unknown fields/types are ignored.
1937
2056
  applyMessageRecord = (record) => {
1938
2057
  if (record.kind !== "platform") return false;
1939
2058
  const payload = record.payload;
@@ -1984,7 +2103,7 @@ var SessionTranscriptStore = class {
1984
2103
  }
1985
2104
  };
1986
2105
  // Ingest one frame from the stream client. Unknown frame types stamp
1987
- // liveness and change nothing else (§3.6).
2106
+ // liveness and change nothing else.
1988
2107
  ingest = (rawFrame) => {
1989
2108
  const prev = this.snapshot;
1990
2109
  const next = {
@@ -1,10 +1,7 @@
1
- import { cS as StreamFrame, ca as Session } from '../types-sNAOvELB.js';
1
+ import { d2 as StreamFrame, cm as Session } from '../types-BrLpRRde.js';
2
2
 
3
- declare const SESSION_STREAM_PROTOCOL_VERSION = 7;
4
3
  declare const WS_CLOSE_NORMAL = 1000;
5
4
  declare const WS_CLOSE_GOING_AWAY = 1001;
6
- declare const WS_CLOSE_UNSUPPORTED_PROTOCOL_VERSION = 1002;
7
- declare const WS_CLOSE_NO_PROTOCOL = 1003;
8
5
  declare const WS_CLOSE_AUTH_FAILED = 1008;
9
6
  declare const WS_CLOSE_SERVER_ERROR = 1011;
10
7
  declare const WS_CLOSE_OVER_CAPACITY = 1013;
@@ -38,11 +35,11 @@ type OpenSocket = (args: {
38
35
  query: string;
39
36
  }) => StreamSocket | Promise<StreamSocket>;
40
37
  declare function streamQuery(afterSeq: number): string;
41
- type CloseKind = 'normal' | 'auth' | 'unsupported' | 'retry';
38
+ type CloseKind = 'normal' | 'auth' | 'retry';
42
39
  declare function classifyCloseCode(code: number): CloseKind;
43
40
  declare function nextReconnectDelayMs(attempt: number): number;
44
41
  interface ReconnectDecision {
45
- action: 'reconnect' | 'fallback' | 'fail-auth';
42
+ action: 'reconnect' | 'give-up' | 'fail-auth';
46
43
  delayMs?: number;
47
44
  }
48
45
  declare function decideReconnect(params: {
@@ -61,4 +58,4 @@ interface StreamSessionOptions {
61
58
  }
62
59
  declare function streamSession(opts: StreamSessionOptions): Promise<StreamOutcome>;
63
60
 
64
- export { type CloseKind, type OpenSocket, type ReconnectDecision, SESSION_STREAM_PROTOCOL_VERSION, StreamAuthError, StreamFrame, type StreamOutcome, type StreamSessionOptions, type StreamSocket, StreamUnavailableError, WS_CLOSE_AUTH_FAILED, WS_CLOSE_GOING_AWAY, WS_CLOSE_NORMAL, WS_CLOSE_NO_PROTOCOL, WS_CLOSE_OVER_CAPACITY, WS_CLOSE_SERVER_ERROR, WS_CLOSE_UNSUPPORTED_PROTOCOL_VERSION, classifyCloseCode, decideReconnect, nextReconnectDelayMs, sessionStatusWord, streamQuery, streamSession };
61
+ export { type CloseKind, type OpenSocket, type ReconnectDecision, StreamAuthError, StreamFrame, type StreamOutcome, type StreamSessionOptions, type StreamSocket, StreamUnavailableError, WS_CLOSE_AUTH_FAILED, WS_CLOSE_GOING_AWAY, WS_CLOSE_NORMAL, WS_CLOSE_OVER_CAPACITY, WS_CLOSE_SERVER_ERROR, classifyCloseCode, decideReconnect, nextReconnectDelayMs, sessionStatusWord, streamQuery, streamSession };
@@ -1,32 +1,198 @@
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-6V7QTLLF.js";
1
+ // src/stream/index.ts
2
+ var WS_CLOSE_NORMAL = 1e3;
3
+ var WS_CLOSE_GOING_AWAY = 1001;
4
+ var WS_CLOSE_AUTH_FAILED = 1008;
5
+ var WS_CLOSE_SERVER_ERROR = 1011;
6
+ var WS_CLOSE_OVER_CAPACITY = 1013;
7
+ function sessionStatusWord(session) {
8
+ return session.turn?.status ?? "none";
9
+ }
10
+ var StreamUnavailableError = class extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "StreamUnavailableError";
14
+ }
15
+ };
16
+ var StreamAuthError = class extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "StreamAuthError";
20
+ }
21
+ };
22
+ function streamQuery(afterSeq) {
23
+ return afterSeq > 0 ? `after_seq=${afterSeq}` : "";
24
+ }
25
+ var HEARTBEAT_TIMEOUT_MS = 45e3;
26
+ var DEFAULT_MAX_RECONNECTS = 5;
27
+ function classifyCloseCode(code) {
28
+ switch (code) {
29
+ case WS_CLOSE_NORMAL:
30
+ return "normal";
31
+ case WS_CLOSE_AUTH_FAILED:
32
+ case 4401:
33
+ // dashboard ticket door: bad/expired ticket
34
+ case 4403:
35
+ return "auth";
36
+ default:
37
+ return "retry";
38
+ }
39
+ }
40
+ function nextReconnectDelayMs(attempt) {
41
+ const base = 500;
42
+ const max = 8e3;
43
+ return Math.min(max, base * 2 ** Math.max(0, attempt - 1));
44
+ }
45
+ function decideReconnect(params) {
46
+ const { closeKind, everReceivedFrame, attempt, maxReconnects } = params;
47
+ if (closeKind === "auth") return { action: "fail-auth" };
48
+ const cap = everReceivedFrame ? maxReconnects : Math.min(2, maxReconnects);
49
+ if (attempt >= cap) return { action: "give-up" };
50
+ return { action: "reconnect", delayMs: nextReconnectDelayMs(attempt) };
51
+ }
52
+ function connectOnce(sock, emit, signal) {
53
+ return new Promise((resolve) => {
54
+ let settled = false;
55
+ let heartbeat;
56
+ const finish = (result) => {
57
+ if (settled) return;
58
+ settled = true;
59
+ if (heartbeat) clearTimeout(heartbeat);
60
+ if (signal) signal.removeEventListener("abort", onAbort);
61
+ sock.close();
62
+ resolve(result);
63
+ };
64
+ const onAbort = () => finish({ kind: "aborted" });
65
+ const bumpHeartbeat = () => {
66
+ if (heartbeat) clearTimeout(heartbeat);
67
+ heartbeat = setTimeout(
68
+ () => finish({ kind: "error", err: new Error("heartbeat timeout") }),
69
+ HEARTBEAT_TIMEOUT_MS
70
+ );
71
+ };
72
+ if (signal) {
73
+ if (signal.aborted) {
74
+ finish({ kind: "aborted" });
75
+ return;
76
+ }
77
+ signal.addEventListener("abort", onAbort);
78
+ }
79
+ sock.onOpen(() => bumpHeartbeat());
80
+ sock.onMessage((data) => {
81
+ bumpHeartbeat();
82
+ let frame;
83
+ try {
84
+ frame = JSON.parse(data);
85
+ } catch {
86
+ return;
87
+ }
88
+ if (!frame || ![
89
+ "snapshot",
90
+ "records_append",
91
+ "session",
92
+ "delta",
93
+ "heartbeat",
94
+ "done",
95
+ "error"
96
+ ].includes(frame.type))
97
+ return;
98
+ emit(frame);
99
+ if (frame.type === "done") {
100
+ finish({ kind: "done" });
101
+ } else if (frame.type === "error") {
102
+ finish({
103
+ kind: "frameError",
104
+ message: frame.message ?? "stream error"
105
+ });
106
+ }
107
+ });
108
+ sock.onClose((code) => finish({ kind: "closed", code }));
109
+ sock.onError((err) => finish({ kind: "error", err }));
110
+ });
111
+ }
112
+ function sleep(ms, signal) {
113
+ return new Promise((resolve) => {
114
+ const timer = setTimeout(resolve, ms);
115
+ signal?.addEventListener(
116
+ "abort",
117
+ () => {
118
+ clearTimeout(timer);
119
+ resolve();
120
+ },
121
+ { once: true }
122
+ );
123
+ });
124
+ }
125
+ async function streamSession(opts) {
126
+ const maxReconnects = opts.maxReconnects ?? DEFAULT_MAX_RECONNECTS;
127
+ let afterSeq = opts.afterSeq ?? 0;
128
+ let everReceivedFrame = false;
129
+ let attempt = 0;
130
+ let lastStatusWord = "";
131
+ let lastReason = null;
132
+ const emit = (frame) => {
133
+ everReceivedFrame = true;
134
+ attempt = 0;
135
+ if (frame.type === "records_append") {
136
+ const records = frame.records;
137
+ for (const record of records) {
138
+ if (typeof record.feed_seq === "number") {
139
+ afterSeq = Math.max(afterSeq, record.feed_seq);
140
+ }
141
+ }
142
+ } else if (frame.type === "snapshot" || frame.type === "session") {
143
+ const session = frame.session;
144
+ lastStatusWord = sessionStatusWord(session);
145
+ lastReason = session.turn?.reason ?? null;
146
+ }
147
+ opts.onFrame(frame);
148
+ };
149
+ for (; ; ) {
150
+ if (opts.signal?.aborted) return { type: "aborted" };
151
+ let res;
152
+ try {
153
+ const sock = await opts.openSocket({
154
+ sessionId: opts.sessionId,
155
+ afterSeq,
156
+ query: streamQuery(afterSeq)
157
+ });
158
+ res = await connectOnce(sock, emit, opts.signal);
159
+ } catch (err) {
160
+ res = {
161
+ kind: "error",
162
+ err: err instanceof Error ? err : new Error(String(err))
163
+ };
164
+ }
165
+ if (res.kind === "done") {
166
+ return { type: "done", status: lastStatusWord, reason: lastReason };
167
+ }
168
+ if (res.kind === "frameError")
169
+ return { type: "error", message: res.message };
170
+ if (res.kind === "aborted") return { type: "aborted" };
171
+ attempt++;
172
+ const decision = decideReconnect({
173
+ closeKind: res.kind === "closed" ? classifyCloseCode(res.code) : void 0,
174
+ everReceivedFrame,
175
+ attempt,
176
+ maxReconnects
177
+ });
178
+ if (decision.action === "fail-auth") {
179
+ throw new StreamAuthError("not authorized to stream this session");
180
+ }
181
+ if (decision.action === "give-up") {
182
+ const why = res.kind === "error" ? res.err.message : `stream closed (code ${res.code})`;
183
+ throw new StreamUnavailableError(why);
184
+ }
185
+ await sleep(decision.delayMs ?? 0, opts.signal);
186
+ }
187
+ }
19
188
  export {
20
- SESSION_STREAM_PROTOCOL_VERSION,
21
189
  StreamAuthError,
22
190
  StreamUnavailableError,
23
191
  WS_CLOSE_AUTH_FAILED,
24
192
  WS_CLOSE_GOING_AWAY,
25
193
  WS_CLOSE_NORMAL,
26
- WS_CLOSE_NO_PROTOCOL,
27
194
  WS_CLOSE_OVER_CAPACITY,
28
195
  WS_CLOSE_SERVER_ERROR,
29
- WS_CLOSE_UNSUPPORTED_PROTOCOL_VERSION,
30
196
  classifyCloseCode,
31
197
  decideReconnect,
32
198
  nextReconnectDelayMs,