@ellipsis-dev/sdk 0.15.1 → 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.
@@ -0,0 +1,208 @@
1
+ // src/stream/index.ts
2
+ var SESSION_STREAM_PROTOCOL_VERSION = 3;
3
+ var WS_CLOSE_NORMAL = 1e3;
4
+ var WS_CLOSE_GOING_AWAY = 1001;
5
+ var WS_CLOSE_UNSUPPORTED_PROTOCOL_VERSION = 1002;
6
+ var WS_CLOSE_NO_PROTOCOL = 1003;
7
+ var WS_CLOSE_AUTH_FAILED = 1008;
8
+ var WS_CLOSE_SERVER_ERROR = 1011;
9
+ var WS_CLOSE_OVER_CAPACITY = 1013;
10
+ function sessionStatusWord(session) {
11
+ return session.surface?.status ?? session.status;
12
+ }
13
+ var StreamUnavailableError = class extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = "StreamUnavailableError";
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
+ }
190
+
191
+ export {
192
+ SESSION_STREAM_PROTOCOL_VERSION,
193
+ WS_CLOSE_NORMAL,
194
+ WS_CLOSE_GOING_AWAY,
195
+ WS_CLOSE_UNSUPPORTED_PROTOCOL_VERSION,
196
+ WS_CLOSE_NO_PROTOCOL,
197
+ WS_CLOSE_AUTH_FAILED,
198
+ WS_CLOSE_SERVER_ERROR,
199
+ WS_CLOSE_OVER_CAPACITY,
200
+ sessionStatusWord,
201
+ StreamUnavailableError,
202
+ StreamAuthError,
203
+ streamQuery,
204
+ classifyCloseCode,
205
+ nextReconnectDelayMs,
206
+ decideReconnect,
207
+ streamSession
208
+ };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { c as components } from './types-D9g-eZui.js';
2
- export { A as AgentConfigSource, a as AttributionType, B as BudgetSource, C as ClaudeSessionRecord, b as CodexEvent, d as CodexItem, e as CodexSessionRecord, f as CreateReviewRequest, D as DeltaFrame, g as DoneFrame, E as ErrorFrame, F as Finding, G as GithubAccountSnippet, h as GithubAccountType, H as Harness, i as HeartbeatFrame, L as LifecycleSessionRecord, P as ParentKind, j as PromptBlockedReason, R as RecordsAppendFrame, k as ResolvedReviewScope, l as Review, m as ReviewConfiguration, n as ReviewCounters, o as ReviewFinding, p as ReviewRequester, q as ReviewScope, r as ReviewScopeKind, s as ReviewStage, t as ReviewedCommit, u as ReviewsListResponse, S as SdkAssistantRecord, v as SdkContentBlock, w as SdkRateLimitRecord, x as SdkRecord, y as SdkResultRecord, z as SdkSystemRecord, I as SdkUserRecord, J as SendSessionMessageRequest, K as Session, M as SessionExecution, N as SessionExecutionsListResponse, O as SessionExitStatus, Q as SessionFrame, T as SessionLiveness, U as SessionMessage, V as SessionMessageResponse, W as SessionMessageStatus, X as SessionPr, Y as SessionPrompting, Z as SessionRecord, _ as SessionRecordsListResponse, $ as SessionResponse, a0 as SessionSource, a1 as SessionState, a2 as SessionStatus, a3 as SessionStreamFrame, a4 as SessionSurface, a5 as SessionsListResponse, a6 as SnapshotFrame, a7 as StreamFrame, a8 as TokensInfo, a9 as paths } from './types-D9g-eZui.js';
1
+ import { c as components } from './types-DB1hOyQE.js';
2
+ export { A as AgentConfigSource, a as AttributionType, B as BudgetSource, C as ClaudeSessionRecord, b as CodexEvent, d as CodexItem, e as CodexSessionRecord, f as CreateReviewRequest, D as DeltaFrame, g as DoneFrame, E as ErrorFrame, F as Finding, G as GithubAccountSnippet, h as GithubAccountType, H as Harness, i as HeartbeatFrame, L as LifecycleSessionRecord, P as ParentKind, j as PromptBlockedReason, R as RecordsAppendFrame, k as ResolvedReviewScope, l as Review, m as ReviewConfiguration, n as ReviewCounters, o as ReviewFinding, p as ReviewRequester, q as ReviewScope, r as ReviewScopeKind, s as ReviewStage, t as ReviewedCommit, u as ReviewsListResponse, S as SdkAssistantRecord, v as SdkContentBlock, w as SdkRateLimitRecord, x as SdkRecord, y as SdkResultRecord, z as SdkSystemRecord, I as SdkUserRecord, J as SendSessionMessageRequest, K as Session, M as SessionExecution, N as SessionExecutionsListResponse, O as SessionExitStatus, Q as SessionFrame, T as SessionGit, U as SessionLiveness, V as SessionMessage, W as SessionMessageResponse, X as SessionMessageStatus, Y as SessionPrompting, Z as SessionRecord, _ as SessionRecordsListResponse, $ as SessionResponse, a0 as SessionSource, a1 as SessionState, a2 as SessionStatus, a3 as SessionStreamFrame, a4 as SessionSurface, a5 as SessionsListResponse, a6 as SnapshotFrame, a7 as StreamFrame, a8 as TokensInfo, a9 as paths } from './types-DB1hOyQE.js';
3
3
 
4
4
  interface CursorResponse {
5
5
  has_more: boolean;
@@ -1,4 +1,4 @@
1
- import { Z as SessionRecord, aa as CodexFileChangeItem, b as CodexEvent, ab as CodexMcpToolCallItem, x as SdkRecord, K as Session, U as SessionMessage, a7 as StreamFrame } from '../types-D9g-eZui.js';
1
+ import { Z as SessionRecord, aa as CodexFileChangeItem, b as CodexEvent, ab as CodexMcpToolCallItem, x as SdkRecord, K as Session, V as SessionMessage, a7 as StreamFrame } from '../types-DB1hOyQE.js';
2
2
 
3
3
  interface ChatToolNode {
4
4
  key: string;
@@ -42,9 +42,46 @@ interface ChatTurn {
42
42
  costUsd: number | null;
43
43
  tokens: number | null;
44
44
  resumed: boolean;
45
+ isError: boolean;
45
46
  }
46
47
  declare function groupRecordsToChatTurns(records: readonly SessionRecord[]): ChatTurn[];
47
48
 
49
+ type RecordSlice = {
50
+ feed_seq: number;
51
+ source: string;
52
+ record_type: string;
53
+ payload: Record<string, unknown>;
54
+ session_message_id?: string | null;
55
+ };
56
+ declare function recordSlice(records: readonly SessionRecord[]): readonly RecordSlice[];
57
+ declare function humanDuration(seconds: number): string;
58
+ declare function sessionLogText(recordType: string, payload: Record<string, unknown>): string | null;
59
+ declare function awaitingAgentPhase(records: readonly RecordSlice[]): 'boot' | 'turn' | null;
60
+ declare function deliveredUnechoedSends(records: readonly RecordSlice[]): {
61
+ id: string;
62
+ body: string;
63
+ cancelled: boolean;
64
+ }[];
65
+ type SandboxLogKind = 'step' | 'output' | 'done' | 'failed';
66
+ type SandboxLogLine = {
67
+ key: string;
68
+ kind: SandboxLogKind;
69
+ text: string;
70
+ };
71
+ type SandboxState = {
72
+ headline: string;
73
+ done: boolean;
74
+ readySeconds: number | null;
75
+ sandboxDone: boolean;
76
+ configName: string | null;
77
+ configCommitSha: string | null;
78
+ log: SandboxLogLine[];
79
+ };
80
+ declare function hookPhrase(step: string): string;
81
+ declare function deriveSandboxState(records: readonly RecordSlice[], minFeedSeq: number): SandboxState | null;
82
+ declare function sandboxSummary(sandbox: SandboxState | null): string;
83
+ declare function lastLines(log: readonly SandboxLogLine[], max: number): SandboxLogLine[];
84
+
48
85
  declare function sandboxOutputStep(payload: Record<string, unknown>): string;
49
86
  declare function sandboxOutputLines(payload: Record<string, unknown>): string[];
50
87
  declare function sandboxOutputLine(payload: Record<string, unknown>): string | null;
@@ -104,6 +141,12 @@ interface SessionTranscriptSnapshot {
104
141
  lastEventAt: number | null;
105
142
  conversationOver: boolean;
106
143
  }
144
+ declare function seedTranscriptStore(store: SessionTranscriptStore, seed: {
145
+ session: Session;
146
+ records: readonly SessionRecord[];
147
+ messages?: readonly SessionMessage[] | null;
148
+ earliestFeedSeq?: number | null;
149
+ }): void;
107
150
  declare function emptySessionTranscriptSnapshot(): SessionTranscriptSnapshot;
108
151
  declare class SessionTranscriptStore {
109
152
  private snapshot;
@@ -122,4 +165,4 @@ declare class SessionTranscriptStore {
122
165
  ingest: (rawFrame: StreamFrame) => void;
123
166
  }
124
167
 
125
- export { type ChatNode, type ChatToolNode, type ChatTurn, type EventToItemsOptions, type ItemKind, type SessionTranscriptSnapshot, SessionTranscriptStore, StreamFrame, type TranscriptItem, cacheTierLabel, clampLines, codexChangedPaths, codexEventToItems, codexMcpToolName, collapseToolRuns, emptySessionTranscriptSnapshot, eventToItems, foldCosts, formatDuration, groupRecordsToChatTurns, isConnectVisibleRecord, isConversationOver, lifecycleText, oneLine, pendingToolCalls, recordToItems, resultCostUsd, sandboxOutputLine, sandboxOutputLines, sandboxOutputStep, sandboxPhaseLabel, statusActivityText, summarizeToolInput, toolResultText };
168
+ export { type ChatNode, type ChatToolNode, type ChatTurn, type EventToItemsOptions, type ItemKind, type RecordSlice, type SandboxLogKind, type SandboxLogLine, type SandboxState, type SessionTranscriptSnapshot, SessionTranscriptStore, StreamFrame, type TranscriptItem, awaitingAgentPhase, cacheTierLabel, clampLines, codexChangedPaths, codexEventToItems, codexMcpToolName, collapseToolRuns, deliveredUnechoedSends, deriveSandboxState, emptySessionTranscriptSnapshot, eventToItems, foldCosts, formatDuration, groupRecordsToChatTurns, hookPhrase, humanDuration, isConnectVisibleRecord, isConversationOver, lastLines, lifecycleText, oneLine, pendingToolCalls, recordSlice, recordToItems, resultCostUsd, sandboxOutputLine, sandboxOutputLines, sandboxOutputStep, sandboxPhaseLabel, sandboxSummary, seedTranscriptStore, sessionLogText, statusActivityText, summarizeToolInput, toolResultText };
@@ -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;
@@ -483,7 +487,8 @@ function groupRecordsToChatTurns(records) {
483
487
  durationMs: null,
484
488
  costUsd: null,
485
489
  tokens: null,
486
- resumed: pendingResume
490
+ resumed: pendingResume,
491
+ isError: false
487
492
  });
488
493
  pendingResume = false;
489
494
  openIdx = turns.length - 1;
@@ -549,16 +554,18 @@ function groupRecordsToChatTurns(records) {
549
554
  durationMs: null,
550
555
  costUsd: null,
551
556
  tokens: null,
552
- resumed: false
557
+ resumed: false,
558
+ isError: false
553
559
  });
554
560
  continue;
555
561
  }
556
562
  if (record.record_format === "codex_jsonl@1") {
557
563
  codexEventIntoTurns(record, record.payload, {
558
564
  openAgentTurn,
559
- closeTurn: (completedAt) => {
565
+ closeTurn: (completedAt, isError) => {
560
566
  if (openIdx >= 0) {
561
567
  turns[openIdx].completedAt = completedAt;
568
+ if (isError) turns[openIdx].isError = true;
562
569
  openIdx = -1;
563
570
  }
564
571
  },
@@ -581,6 +588,7 @@ function groupRecordsToChatTurns(records) {
581
588
  if (typeof data.cost_usd === "number") {
582
589
  turns[openIdx].costUsd = data.cost_usd;
583
590
  }
591
+ if (data.is_error) turns[openIdx].isError = true;
584
592
  openIdx = -1;
585
593
  }
586
594
  continue;
@@ -592,6 +600,7 @@ function groupRecordsToChatTurns(records) {
592
600
  openIdx = -1;
593
601
  turns.push({
594
602
  resumed: false,
603
+ isError: false,
595
604
  key: nextKey(record.id),
596
605
  role: "user",
597
606
  nodes: [
@@ -672,7 +681,7 @@ function groupRecordsToChatTurns(records) {
672
681
  }
673
682
  function codexEventIntoTurns(record, event, ops) {
674
683
  if (event.type === "turn.completed" || event.type === "turn.failed") {
675
- ops.closeTurn(record.created_at);
684
+ ops.closeTurn(record.created_at, event.type === "turn.failed");
676
685
  return;
677
686
  }
678
687
  if (event.type === "error") {
@@ -770,6 +779,289 @@ function codexEventIntoTurns(record, event, ops) {
770
779
  }
771
780
  }
772
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
+
773
1065
  // src/store/index.ts
774
1066
  var TERMINAL_SESSION_STATUSES = /* @__PURE__ */ new Set([
775
1067
  "completed",
@@ -783,6 +1075,18 @@ function isConversationOver(session) {
783
1075
  }
784
1076
  return TERMINAL_SESSION_STATUSES.has(session.status);
785
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
+ }
786
1090
  var EMPTY_SNAPSHOT = {
787
1091
  session: null,
788
1092
  records: [],
@@ -963,28 +1267,38 @@ var SessionTranscriptStore = class {
963
1267
  };
964
1268
  export {
965
1269
  SessionTranscriptStore,
1270
+ awaitingAgentPhase,
966
1271
  cacheTierLabel,
967
1272
  clampLines,
968
1273
  codexChangedPaths,
969
1274
  codexEventToItems,
970
1275
  codexMcpToolName,
971
1276
  collapseToolRuns,
1277
+ deliveredUnechoedSends,
1278
+ deriveSandboxState,
972
1279
  emptySessionTranscriptSnapshot,
973
1280
  eventToItems,
974
1281
  foldCosts,
975
1282
  formatDuration,
976
1283
  groupRecordsToChatTurns,
1284
+ hookPhrase,
1285
+ humanDuration,
977
1286
  isConnectVisibleRecord,
978
1287
  isConversationOver,
1288
+ lastLines,
979
1289
  lifecycleText,
980
1290
  oneLine,
981
1291
  pendingToolCalls,
1292
+ recordSlice,
982
1293
  recordToItems,
983
1294
  resultCostUsd,
984
1295
  sandboxOutputLine,
985
1296
  sandboxOutputLines,
986
1297
  sandboxOutputStep,
987
1298
  sandboxPhaseLabel,
1299
+ sandboxSummary,
1300
+ seedTranscriptStore,
1301
+ sessionLogText,
988
1302
  statusActivityText,
989
1303
  summarizeToolInput,
990
1304
  toolResultText