@steventsao/agent-session 0.1.23 → 0.1.25

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/slice.js CHANGED
@@ -1,816 +1,852 @@
1
- /**
2
- * AgentSession Redux Slice
3
- *
4
- * RTK-based state management for agent-session WebSocket.
5
- * Uses singleton pattern - socket lives outside React (no reconnection loops).
6
- *
7
- * Singleton socket pattern to avoid React reconnection loops
8
- */
9
- import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
10
- const initialState = {
11
- isConnected: false,
12
- clientId: null,
13
- sessionId: null,
14
- lastEventId: null,
15
- lastServerInteraction: 0,
16
- connectionHealth: 'healthy',
17
- reconnectAttempts: 0,
18
- sandboxStatus: 'idle',
19
- sandboxId: null,
20
- sandboxUrl: null,
21
- lifecycle: 'idle',
22
- lifecycleMessage: null,
23
- agentStatus: 'idle',
24
- agentSessionId: null,
25
- messages: [],
26
- uploadedFiles: [],
27
- sandboxFiles: [],
28
- error: null,
1
+ // src/slice.ts
2
+ import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
3
+ var initialState = {
4
+ isConnected: false,
5
+ clientId: null,
6
+ sessionId: null,
7
+ lastEventId: null,
8
+ lastServerInteraction: 0,
9
+ connectionHealth: "healthy",
10
+ reconnectAttempts: 0,
11
+ sandboxStatus: "idle",
12
+ sandboxId: null,
13
+ sandboxUrl: null,
14
+ lifecycle: "idle",
15
+ lifecycleMessage: null,
16
+ agentStatus: "idle",
17
+ agentSessionId: null,
18
+ messages: [],
19
+ uploadedFiles: [],
20
+ sandboxFiles: [],
21
+ error: null
29
22
  };
30
- // Connection health constants (inspired by tldraw)
31
- const STALE_THRESHOLD_MS = 45_000; // 45s without server interaction = stale
32
- const DEAD_THRESHOLD_MS = 90_000; // 90s = dead, force reconnect
33
- const HEALTH_CHECK_INTERVAL_MS = 10_000; // Check every 10s
23
+ var STALE_THRESHOLD_MS = 45e3;
24
+ var DEAD_THRESHOLD_MS = 9e4;
25
+ var HEALTH_CHECK_INTERVAL_MS = 1e4;
34
26
  function createClientMessageId() {
35
- if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
36
- return crypto.randomUUID();
37
- }
38
- return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
27
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
28
+ return crypto.randomUUID();
29
+ }
30
+ return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
39
31
  }
40
- class SessionSocketManager {
41
- ws = null;
42
- dispatch = null;
43
- baseUrl = null;
44
- currentSessionId = null;
45
- currentUserId = null;
46
- pingInterval = null;
47
- healthCheckInterval = null;
48
- reconnectTimeout = null;
49
- reconnectAttempts = 0;
50
- maxReconnectAttempts = Infinity; // Unlimited retries (PartyKit pattern)
51
- socketId = 0; // Prevents stale handlers
52
- lastServerInteraction = 0; // Track last meaningful server response
53
- lastEventId = null; // Track last event ID for session resumption
54
- isReplaying = false; // True when replaying EVENTS_BATCH
55
- pendingClientMessageIds = new Set();
56
- pendingClientMessageTimers = new Map();
57
- pendingClientMessages = new Map();
58
- seenEventIds = new Set();
59
- setDispatch(dispatch) {
60
- this.dispatch = dispatch;
61
- }
62
- setBaseUrl(url) {
63
- this.baseUrl = url.replace(/\/$/, '');
64
- }
65
- connect(sessionId, userId) {
66
- if (!this.baseUrl) {
67
- console.error('[SessionSocket] Base URL not set');
68
- return;
69
- }
70
- // Already connected to same session
71
- if (this.ws?.readyState === WebSocket.OPEN && this.currentSessionId === sessionId) {
72
- console.log('[SessionSocket] Already connected to', sessionId);
73
- return;
74
- }
75
- // Disconnect from previous if different session and reset state
76
- if (this.currentSessionId && this.currentSessionId !== sessionId) {
77
- console.log(`[SessionSocket] Switching from ${this.currentSessionId} to ${sessionId}, resetting state`);
78
- this.disconnect();
79
- // Reset all session-specific state when switching documents
80
- // (OpenHands pattern: comprehensive state reset on conversation switch)
81
- this.dispatch?.(resetSessionState());
82
- }
83
- this.currentSessionId = sessionId;
84
- this.currentUserId = userId || null;
85
- this.reconnectAttempts = 0;
86
- // Load cursor from localStorage for THIS session's resumption
87
- // Clear first to avoid leaking previous session's cursor
88
- // (PartyKit pattern: clean slate before new room connection)
89
- this.lastEventId = null;
90
- if (typeof window !== 'undefined') {
91
- const storedCursor = localStorage.getItem(`agent-session-cursor:${sessionId}`);
92
- if (storedCursor) {
93
- this.lastEventId = storedCursor;
94
- console.log('[SessionSocket] Loaded cursor from localStorage:', storedCursor);
95
- }
96
- }
97
- const thisSocketId = ++this.socketId;
98
- const params = new URLSearchParams();
99
- if (userId)
100
- params.set('userId', userId);
101
- const url = `${this.baseUrl}/session/${sessionId}/ws?${params}`;
102
- console.log('[SessionSocket] Connecting to:', url);
103
- this.ws = new WebSocket(url);
104
- this.ws.onopen = () => {
105
- if (thisSocketId !== this.socketId)
106
- return;
107
- console.log('[SessionSocket] Connected');
108
- this.reconnectAttempts = 0;
109
- this.dispatch?.(setReconnectAttempts(0));
110
- this.lastServerInteraction = Date.now(); // Initialize on connect
111
- this.startPing();
112
- this.startHealthCheck();
113
- // Send JOIN_SESSION for event resumption
114
- this.send({
115
- type: 'JOIN_SESSION',
116
- lastEventId: this.lastEventId,
117
- });
118
- this.flushPendingMessages();
119
- };
120
- this.ws.onmessage = (event) => {
121
- if (thisSocketId !== this.socketId)
122
- return;
123
- try {
124
- const data = JSON.parse(event.data);
125
- this.handleMessage(data);
126
- }
127
- catch (err) {
128
- console.error('[SessionSocket] Parse error:', err);
129
- }
130
- };
131
- this.ws.onclose = (event) => {
132
- if (thisSocketId !== this.socketId) {
133
- console.log(`[SessionSocket] Ignoring stale onclose`);
134
- return;
135
- }
136
- console.log('[SessionSocket] Disconnected:', event.code);
137
- this.stopPing();
138
- this.stopHealthCheck();
139
- this.dispatch?.(setDisconnected());
140
- this.maybeReconnect();
141
- };
142
- this.ws.onerror = (event) => {
143
- if (thisSocketId !== this.socketId)
144
- return;
145
- console.error('[SessionSocket] Error:', event);
146
- };
147
- }
148
- disconnect() {
149
- this.stopPing();
150
- this.stopHealthCheck();
151
- this.clearReconnect();
152
- this.socketId++;
153
- this.ws?.close();
154
- this.ws = null;
155
- this.currentSessionId = null;
156
- this.currentUserId = null;
157
- this.lastEventId = null; // Clear cursor to prevent leaking to next session
158
- for (const timer of this.pendingClientMessageTimers.values()) {
159
- clearTimeout(timer);
160
- }
161
- this.pendingClientMessageTimers.clear();
162
- this.pendingClientMessageIds.clear();
163
- this.pendingClientMessages.clear();
164
- this.seenEventIds.clear();
165
- this.dispatch?.(setDisconnected());
166
- }
167
- get isConnected() {
168
- return this.ws?.readyState === WebSocket.OPEN;
169
- }
170
- handleMessage(event) {
171
- if (!this.dispatch)
172
- return;
173
- const eventType = event.type;
174
- if (eventType === 'MESSAGE_ACK') {
175
- const ackEvent = event;
176
- this.confirmClientMessage(ackEvent.clientMessageId);
177
- return;
178
- }
179
- if (eventType === 'MESSAGE_ERROR') {
180
- const errorEvent = event;
181
- this.pendingClientMessageIds.delete(errorEvent.clientMessageId);
182
- this.clearAckTimer(errorEvent.clientMessageId);
183
- this.pendingClientMessages.delete(errorEvent.clientMessageId);
184
- this.dispatch(markMessageFailed({ clientMessageId: errorEvent.clientMessageId, error: errorEvent.error }));
185
- return;
32
+ var SessionSocketManager = class {
33
+ ws = null;
34
+ dispatch = null;
35
+ baseUrl = null;
36
+ currentSessionId = null;
37
+ currentUserId = null;
38
+ pingInterval = null;
39
+ healthCheckInterval = null;
40
+ reconnectTimeout = null;
41
+ reconnectAttempts = 0;
42
+ maxReconnectAttempts = Infinity;
43
+ // Unlimited retries (PartyKit pattern)
44
+ socketId = 0;
45
+ // Prevents stale handlers
46
+ lastServerInteraction = 0;
47
+ // Track last meaningful server response
48
+ lastEventId = null;
49
+ // Track last event ID for session resumption
50
+ isReplaying = false;
51
+ // True when replaying EVENTS_BATCH
52
+ pendingClientMessageIds = /* @__PURE__ */ new Set();
53
+ pendingClientMessageTimers = /* @__PURE__ */ new Map();
54
+ pendingClientMessages = /* @__PURE__ */ new Map();
55
+ seenEventIds = /* @__PURE__ */ new Set();
56
+ setDispatch(dispatch) {
57
+ this.dispatch = dispatch;
58
+ }
59
+ setBaseUrl(url) {
60
+ this.baseUrl = url.replace(/\/$/, "");
61
+ }
62
+ connect(sessionId, userId) {
63
+ if (!this.baseUrl) {
64
+ console.error("[SessionSocket] Base URL not set");
65
+ return;
66
+ }
67
+ if (this.ws?.readyState === WebSocket.OPEN && this.currentSessionId === sessionId) {
68
+ console.log("[SessionSocket] Already connected to", sessionId);
69
+ return;
70
+ }
71
+ if (this.currentSessionId && this.currentSessionId !== sessionId) {
72
+ console.log(`[SessionSocket] Switching from ${this.currentSessionId} to ${sessionId}, resetting state`);
73
+ this.disconnect();
74
+ this.dispatch?.(resetSessionState());
75
+ }
76
+ this.currentSessionId = sessionId;
77
+ this.currentUserId = userId || null;
78
+ this.reconnectAttempts = 0;
79
+ this.lastEventId = null;
80
+ if (typeof window !== "undefined") {
81
+ const storedCursor = localStorage.getItem(`agent-session-cursor:${sessionId}`);
82
+ if (storedCursor) {
83
+ this.lastEventId = storedCursor;
84
+ console.log("[SessionSocket] Loaded cursor from localStorage:", storedCursor);
85
+ }
86
+ }
87
+ const thisSocketId = ++this.socketId;
88
+ const params = new URLSearchParams();
89
+ if (userId) params.set("userId", userId);
90
+ const url = `${this.baseUrl}/session/${sessionId}/ws?${params}`;
91
+ console.log("[SessionSocket] Connecting to:", url);
92
+ this.ws = new WebSocket(url);
93
+ this.ws.onopen = () => {
94
+ if (thisSocketId !== this.socketId) return;
95
+ console.log("[SessionSocket] Connected");
96
+ this.reconnectAttempts = 0;
97
+ this.dispatch?.(setReconnectAttempts(0));
98
+ this.lastServerInteraction = Date.now();
99
+ this.startPing();
100
+ this.startHealthCheck();
101
+ this.send({
102
+ type: "JOIN_SESSION",
103
+ lastEventId: this.lastEventId
104
+ });
105
+ this.flushPendingMessages();
106
+ };
107
+ this.ws.onmessage = (event) => {
108
+ if (thisSocketId !== this.socketId) return;
109
+ try {
110
+ const data = JSON.parse(event.data);
111
+ this.handleMessage(data);
112
+ } catch (err) {
113
+ console.error("[SessionSocket] Parse error:", err);
114
+ }
115
+ };
116
+ this.ws.onclose = (event) => {
117
+ if (thisSocketId !== this.socketId) {
118
+ console.log(`[SessionSocket] Ignoring stale onclose`);
119
+ return;
120
+ }
121
+ console.log("[SessionSocket] Disconnected:", event.code);
122
+ this.stopPing();
123
+ this.stopHealthCheck();
124
+ this.dispatch?.(setDisconnected());
125
+ this.maybeReconnect();
126
+ };
127
+ this.ws.onerror = (event) => {
128
+ if (thisSocketId !== this.socketId) return;
129
+ console.error("[SessionSocket] Error:", event);
130
+ };
131
+ }
132
+ disconnect() {
133
+ this.stopPing();
134
+ this.stopHealthCheck();
135
+ this.clearReconnect();
136
+ this.socketId++;
137
+ this.ws?.close();
138
+ this.ws = null;
139
+ this.currentSessionId = null;
140
+ this.currentUserId = null;
141
+ this.lastEventId = null;
142
+ for (const timer of this.pendingClientMessageTimers.values()) {
143
+ clearTimeout(timer);
144
+ }
145
+ this.pendingClientMessageTimers.clear();
146
+ this.pendingClientMessageIds.clear();
147
+ this.pendingClientMessages.clear();
148
+ this.seenEventIds.clear();
149
+ this.dispatch?.(setDisconnected());
150
+ }
151
+ get isConnected() {
152
+ return this.ws?.readyState === WebSocket.OPEN;
153
+ }
154
+ handleMessage(event) {
155
+ if (!this.dispatch) return;
156
+ const eventType = event.type;
157
+ if (eventType === "MESSAGE_ACK") {
158
+ const ackEvent = event;
159
+ this.confirmClientMessage(ackEvent.clientMessageId);
160
+ return;
161
+ }
162
+ if (eventType === "MESSAGE_ERROR") {
163
+ const errorEvent = event;
164
+ this.pendingClientMessageIds.delete(errorEvent.clientMessageId);
165
+ this.clearAckTimer(errorEvent.clientMessageId);
166
+ this.pendingClientMessages.delete(errorEvent.clientMessageId);
167
+ this.dispatch(markMessageFailed({ clientMessageId: errorEvent.clientMessageId, error: errorEvent.error }));
168
+ return;
169
+ }
170
+ this.recordServerInteraction();
171
+ const eventWithId = event;
172
+ if (eventWithId.clientMessageId) {
173
+ this.confirmClientMessage(eventWithId.clientMessageId);
174
+ }
175
+ if (eventWithId.eventId) {
176
+ const eventId = eventWithId.eventId;
177
+ if (this.seenEventIds.has(eventId)) {
178
+ return;
179
+ }
180
+ this.seenEventIds.add(eventId);
181
+ this.lastEventId = eventId;
182
+ this.dispatch(setLastEventId(eventId));
183
+ if (typeof window !== "undefined" && this.currentSessionId) {
184
+ localStorage.setItem(`agent-session-cursor:${this.currentSessionId}`, eventId);
185
+ }
186
+ }
187
+ switch (event.type) {
188
+ case "CONNECTED":
189
+ this.dispatch(setConnected({
190
+ clientId: event.clientId,
191
+ sessionId: event.sessionId
192
+ }));
193
+ break;
194
+ case "READY":
195
+ this.dispatch(setReady({
196
+ sandboxId: event.sandboxId ?? void 0,
197
+ sandboxUrl: event.sandboxUrl ?? void 0
198
+ }));
199
+ break;
200
+ case "SANDBOX_STATUS":
201
+ this.dispatch(setSandboxStatus({
202
+ status: event.status,
203
+ sandboxId: event.sandboxId,
204
+ sandboxUrl: event.sandboxUrl,
205
+ error: event.error
206
+ }));
207
+ break;
208
+ case "LIFECYCLE":
209
+ this.dispatch(setLifecycle({
210
+ phase: event.phase,
211
+ message: event.message
212
+ }));
213
+ break;
214
+ case "AGENT_STARTED": {
215
+ const agentStartedEvent = event;
216
+ const includePrompt = this.isReplaying || !!agentStartedEvent.author;
217
+ this.dispatch(agentStarted({
218
+ prompt: includePrompt ? agentStartedEvent.prompt : void 0,
219
+ clientMessageId: agentStartedEvent.clientMessageId,
220
+ author: agentStartedEvent.author
221
+ }));
222
+ break;
223
+ }
224
+ case "AGENT_MESSAGE":
225
+ if (event.message?.content) {
226
+ this.dispatch(updateAgentMessage({ content: event.message.content }));
227
+ break;
186
228
  }
187
- // Record server interaction for ALL messages (health check)
188
- this.recordServerInteraction();
189
- // Extract and store eventId from any event that has it
190
- const eventWithId = event;
191
- if (eventWithId.clientMessageId) {
192
- this.confirmClientMessage(eventWithId.clientMessageId);
229
+ if (typeof event.result === "string") {
230
+ this.dispatch(updateAgentMessage({
231
+ content: [{ type: "text", text: event.result }]
232
+ }));
233
+ break;
193
234
  }
194
- if (eventWithId.eventId) {
195
- const eventId = eventWithId.eventId;
196
- if (this.seenEventIds.has(eventId)) {
197
- return;
198
- }
199
- this.seenEventIds.add(eventId);
200
- this.lastEventId = eventId;
201
- this.dispatch(setLastEventId(eventId));
202
- // Persist to localStorage
203
- if (typeof window !== 'undefined' && this.currentSessionId) {
204
- localStorage.setItem(`agent-session-cursor:${this.currentSessionId}`, eventId);
205
- }
235
+ if (typeof event.content === "string") {
236
+ this.dispatch(updateAgentMessage({
237
+ content: [{ type: "text", text: event.content }]
238
+ }));
206
239
  }
207
- switch (event.type) {
208
- case 'CONNECTED':
209
- this.dispatch(setConnected({
210
- clientId: event.clientId,
211
- sessionId: event.sessionId,
212
- }));
213
- break;
214
- case 'READY':
215
- this.dispatch(setReady({
216
- sandboxId: event.sandboxId ?? undefined,
217
- sandboxUrl: event.sandboxUrl ?? undefined,
218
- }));
219
- break;
220
- case 'SANDBOX_STATUS':
221
- this.dispatch(setSandboxStatus({
222
- status: event.status,
223
- sandboxId: event.sandboxId,
224
- sandboxUrl: event.sandboxUrl,
225
- error: event.error,
226
- }));
227
- break;
228
- case 'LIFECYCLE':
229
- this.dispatch(setLifecycle({
230
- phase: event.phase,
231
- message: event.message,
232
- }));
233
- break;
234
- case 'AGENT_STARTED': {
235
- // Include prompt during replay (chat history restoration) OR when
236
- // another client sent the message (author present = multi-client).
237
- // For our own live messages, sendAgentMessage thunk already called addUserMessage.
238
- const agentStartedEvent = event;
239
- const includePrompt = this.isReplaying || !!agentStartedEvent.author;
240
- this.dispatch(agentStarted({
241
- prompt: includePrompt ? agentStartedEvent.prompt : undefined,
242
- clientMessageId: agentStartedEvent.clientMessageId,
243
- author: agentStartedEvent.author,
244
- }));
245
- break;
246
- }
247
- case 'AGENT_MESSAGE':
248
- if (event.message?.content) {
249
- this.dispatch(updateAgentMessage({ content: event.message.content }));
250
- break;
251
- }
252
- if (typeof event.result === 'string') {
253
- this.dispatch(updateAgentMessage({
254
- content: [{ type: 'text', text: event.result }],
255
- }));
256
- break;
257
- }
258
- if (typeof event.content === 'string') {
259
- this.dispatch(updateAgentMessage({
260
- content: [{ type: 'text', text: event.content }],
261
- }));
262
- }
263
- break;
264
- case 'AGENT_DONE':
265
- this.dispatch(agentDone({ sessionId: event.sessionId }));
266
- break;
267
- case 'AGENT_ERROR':
268
- this.dispatch(agentError({ error: event.error }));
269
- break;
270
- case 'ERROR':
271
- this.dispatch(setError(event.msg));
272
- break;
273
- case 'PONG':
274
- // PONG already recorded above, nothing else to do
275
- break;
276
- case 'EVENTS_BATCH':
277
- // Replay batched events to rebuild state
278
- const batchEvent = event;
279
- console.log('[SessionSocket] Replaying', batchEvent.events.length, 'batched events');
280
- this.isReplaying = true;
281
- for (const batchedEvent of batchEvent.events) {
282
- this.handleMessage(batchedEvent);
283
- }
284
- this.isReplaying = false;
285
- if (batchEvent.lastEventId) {
286
- const lastEventId = batchEvent.lastEventId;
287
- this.lastEventId = lastEventId;
288
- this.dispatch(setLastEventId(lastEventId));
289
- // Persist to localStorage
290
- if (typeof window !== 'undefined' && this.currentSessionId) {
291
- localStorage.setItem(`agent-session-cursor:${this.currentSessionId}`, lastEventId);
292
- }
293
- }
294
- break;
295
- case 'FILE_UPLOADED':
296
- const fileEvent = event;
297
- this.dispatch(addUploadedFile({
298
- path: fileEvent.path,
299
- filename: fileEvent.filename,
300
- url: fileEvent.url,
301
- mimeType: fileEvent.mimeType,
302
- description: fileEvent.description,
303
- sizeBytes: fileEvent.sizeBytes,
304
- timestamp: Date.now(),
305
- }));
306
- break;
307
- case 'FILES_SYNC':
308
- const syncEvent = event;
309
- this.dispatch(setSandboxFiles(syncEvent.files));
310
- break;
311
- default:
312
- console.log('[SessionSocket] Event:', event.type);
240
+ break;
241
+ case "AGENT_DONE":
242
+ this.dispatch(agentDone({ sessionId: event.sessionId }));
243
+ break;
244
+ case "AGENT_ERROR":
245
+ this.dispatch(agentError({ error: event.error }));
246
+ break;
247
+ case "ERROR":
248
+ this.dispatch(setError(event.msg));
249
+ break;
250
+ case "PONG":
251
+ break;
252
+ case "EVENTS_BATCH":
253
+ const batchEvent = event;
254
+ console.log("[SessionSocket] Replaying", batchEvent.events.length, "batched events");
255
+ this.isReplaying = true;
256
+ for (const batchedEvent of batchEvent.events) {
257
+ this.handleMessage(batchedEvent);
313
258
  }
314
- }
315
- send(message) {
316
- if (this.ws?.readyState !== WebSocket.OPEN) {
317
- console.warn('[SessionSocket] Not connected');
318
- return false;
319
- }
320
- this.ws.send(JSON.stringify(message));
321
- return true;
322
- }
323
- // Commands
324
- init(metadata) {
325
- this.send({ type: 'INIT', metadata });
326
- }
327
- startSandbox(template, bootstrap) {
328
- this.send({ type: 'START_SANDBOX', template, bootstrap });
329
- }
330
- stopSandbox() {
331
- this.send({ type: 'STOP_SANDBOX' });
332
- }
333
- exec(cmd, cwd) {
334
- this.send({ type: 'EXEC', cmd, cwd });
335
- }
336
- sendAgentMessage(content, model, config, systemPrompt, agentType, clientMessageId, author) {
337
- const effectiveClientMessageId = clientMessageId || createClientMessageId();
338
- const message = {
339
- type: 'AGENT_MESSAGE',
340
- clientMessageId: effectiveClientMessageId,
341
- content,
342
- model,
343
- config,
344
- systemPrompt,
345
- agentType,
346
- author,
347
- };
348
- if (!this.pendingClientMessages.has(effectiveClientMessageId)) {
349
- this.pendingClientMessages.set(effectiveClientMessageId, { message, attempts: 0 });
350
- }
351
- this.trySendPending(effectiveClientMessageId);
352
- return effectiveClientMessageId;
353
- }
354
- trySendPending(clientMessageId) {
355
- const pending = this.pendingClientMessages.get(clientMessageId);
356
- if (!pending)
357
- return;
358
- if (this.pendingClientMessageIds.has(clientMessageId))
359
- return;
360
- const sent = this.send(pending.message);
361
- if (!sent)
362
- return;
363
- pending.attempts += 1;
364
- this.pendingClientMessageIds.add(clientMessageId);
365
- this.startAckTimer(clientMessageId);
366
- }
367
- flushPendingMessages() {
368
- for (const clientMessageId of this.pendingClientMessages.keys()) {
369
- this.trySendPending(clientMessageId);
370
- }
371
- }
372
- startAckTimer(clientMessageId) {
373
- this.clearAckTimer(clientMessageId);
374
- const dispatch = this.dispatch;
375
- const timer = setTimeout(() => {
376
- if (!this.pendingClientMessageIds.has(clientMessageId))
377
- return;
378
- const pending = this.pendingClientMessages.get(clientMessageId);
379
- if (pending && pending.attempts < 2) {
380
- this.pendingClientMessageIds.delete(clientMessageId);
381
- this.trySendPending(clientMessageId);
382
- return;
383
- }
384
- this.pendingClientMessageIds.delete(clientMessageId);
385
- this.pendingClientMessages.delete(clientMessageId);
386
- if (dispatch) {
387
- dispatch(markMessageFailed({
388
- clientMessageId,
389
- error: 'Server did not acknowledge message',
390
- }));
391
- }
392
- }, 2000);
393
- this.pendingClientMessageTimers.set(clientMessageId, timer);
394
- }
395
- confirmClientMessage(clientMessageId) {
396
- if (!this.pendingClientMessageIds.has(clientMessageId) && !this.pendingClientMessages.has(clientMessageId)) {
397
- return;
259
+ this.isReplaying = false;
260
+ if (batchEvent.lastEventId) {
261
+ const lastEventId = batchEvent.lastEventId;
262
+ this.lastEventId = lastEventId;
263
+ this.dispatch(setLastEventId(lastEventId));
264
+ if (typeof window !== "undefined" && this.currentSessionId) {
265
+ localStorage.setItem(`agent-session-cursor:${this.currentSessionId}`, lastEventId);
266
+ }
398
267
  }
268
+ break;
269
+ case "FILE_UPLOADED":
270
+ const fileEvent = event;
271
+ this.dispatch(addUploadedFile({
272
+ path: fileEvent.path,
273
+ filename: fileEvent.filename,
274
+ url: fileEvent.url,
275
+ mimeType: fileEvent.mimeType,
276
+ description: fileEvent.description,
277
+ sizeBytes: fileEvent.sizeBytes,
278
+ timestamp: Date.now()
279
+ }));
280
+ break;
281
+ case "FILES_SYNC":
282
+ const syncEvent = event;
283
+ this.dispatch(setSandboxFiles(syncEvent.files));
284
+ break;
285
+ default:
286
+ console.log("[SessionSocket] Event:", event.type);
287
+ }
288
+ }
289
+ send(message) {
290
+ if (this.ws?.readyState !== WebSocket.OPEN) {
291
+ console.warn("[SessionSocket] Not connected");
292
+ return false;
293
+ }
294
+ this.ws.send(JSON.stringify(message));
295
+ return true;
296
+ }
297
+ // Commands
298
+ init(metadata) {
299
+ this.send({ type: "INIT", metadata });
300
+ }
301
+ startSandbox(template, bootstrap) {
302
+ this.send({ type: "START_SANDBOX", template, bootstrap });
303
+ }
304
+ stopSandbox() {
305
+ this.send({ type: "STOP_SANDBOX" });
306
+ }
307
+ exec(cmd, cwd) {
308
+ this.send({ type: "EXEC", cmd, cwd });
309
+ }
310
+ sendAgentMessage(content, model, config, systemPrompt, agentType, clientMessageId, author) {
311
+ const effectiveClientMessageId = clientMessageId || createClientMessageId();
312
+ const message = {
313
+ type: "AGENT_MESSAGE",
314
+ clientMessageId: effectiveClientMessageId,
315
+ content,
316
+ model,
317
+ config,
318
+ systemPrompt,
319
+ agentType,
320
+ author
321
+ };
322
+ if (!this.pendingClientMessages.has(effectiveClientMessageId)) {
323
+ this.pendingClientMessages.set(effectiveClientMessageId, { message, attempts: 0 });
324
+ }
325
+ this.trySendPending(effectiveClientMessageId);
326
+ return effectiveClientMessageId;
327
+ }
328
+ trySendPending(clientMessageId) {
329
+ const pending = this.pendingClientMessages.get(clientMessageId);
330
+ if (!pending) return;
331
+ if (this.pendingClientMessageIds.has(clientMessageId)) return;
332
+ const sent = this.send(pending.message);
333
+ if (!sent) return;
334
+ pending.attempts += 1;
335
+ this.pendingClientMessageIds.add(clientMessageId);
336
+ this.startAckTimer(clientMessageId);
337
+ }
338
+ flushPendingMessages() {
339
+ for (const clientMessageId of this.pendingClientMessages.keys()) {
340
+ this.trySendPending(clientMessageId);
341
+ }
342
+ }
343
+ startAckTimer(clientMessageId) {
344
+ this.clearAckTimer(clientMessageId);
345
+ const dispatch = this.dispatch;
346
+ const timer = setTimeout(() => {
347
+ if (!this.pendingClientMessageIds.has(clientMessageId)) return;
348
+ const pending = this.pendingClientMessages.get(clientMessageId);
349
+ if (pending && pending.attempts < 2) {
399
350
  this.pendingClientMessageIds.delete(clientMessageId);
400
- this.pendingClientMessages.delete(clientMessageId);
401
- this.clearAckTimer(clientMessageId);
402
- this.dispatch?.(markMessageSent({ clientMessageId }));
403
- }
404
- clearAckTimer(clientMessageId) {
405
- const timer = this.pendingClientMessageTimers.get(clientMessageId);
406
- if (timer) {
407
- clearTimeout(timer);
408
- this.pendingClientMessageTimers.delete(clientMessageId);
409
- }
410
- }
411
- stopAgent() {
412
- this.send({ type: 'AGENT_STOP' });
413
- }
414
- resetAgent() {
415
- this.send({ type: 'AGENT_RESET' });
416
- }
417
- startPing() {
418
- this.stopPing();
419
- this.pingInterval = setInterval(() => {
420
- this.send({ type: 'PING' });
421
- }, 30000);
422
- }
423
- stopPing() {
424
- if (this.pingInterval) {
425
- clearInterval(this.pingInterval);
426
- this.pingInterval = null;
427
- }
428
- }
429
- /**
430
- * Start health check interval (tldraw pattern)
431
- * Monitors time since last server interaction and resets connection if stale
432
- */
433
- startHealthCheck() {
434
- this.stopHealthCheck();
435
- this.healthCheckInterval = setInterval(() => {
436
- this.checkConnectionHealth();
437
- }, HEALTH_CHECK_INTERVAL_MS);
438
- }
439
- stopHealthCheck() {
440
- if (this.healthCheckInterval) {
441
- clearInterval(this.healthCheckInterval);
442
- this.healthCheckInterval = null;
443
- }
444
- }
445
- /**
446
- * Check connection health based on time since last server interaction
447
- * Inspired by tldraw's MAX_TIME_TO_WAIT_FOR_SERVER_INTERACTION_BEFORE_RESETTING_CONNECTION
448
- */
449
- checkConnectionHealth() {
450
- if (!this.isConnected)
451
- return;
452
- const now = Date.now();
453
- const timeSinceLastInteraction = now - this.lastServerInteraction;
454
- if (timeSinceLastInteraction > DEAD_THRESHOLD_MS) {
455
- console.warn(`[SessionSocket] Connection dead (${timeSinceLastInteraction}ms since last interaction), forcing reconnect`);
456
- this.dispatch?.(setConnectionHealth('dead'));
457
- this.forceReconnect();
458
- }
459
- else if (timeSinceLastInteraction > STALE_THRESHOLD_MS) {
460
- console.warn(`[SessionSocket] Connection stale (${timeSinceLastInteraction}ms since last interaction)`);
461
- this.dispatch?.(setConnectionHealth('stale'));
462
- // Send a ping to try to revive it
463
- this.send({ type: 'PING' });
464
- }
465
- else {
466
- this.dispatch?.(setConnectionHealth('healthy'));
467
- }
468
- }
469
- /**
470
- * Record server interaction timestamp
471
- * Called on any meaningful server message (not just PONG)
472
- */
473
- recordServerInteraction() {
474
- this.lastServerInteraction = Date.now();
475
- this.dispatch?.(updateLastServerInteraction(this.lastServerInteraction));
476
- }
477
- /**
478
- * Force reconnect - closes current socket and reconnects immediately
479
- * Used when connection is detected as dead
480
- */
481
- forceReconnect() {
482
- if (!this.currentSessionId)
483
- return;
484
- console.log('[SessionSocket] Force reconnecting...');
485
- // Close current socket without triggering normal reconnect logic
486
- this.stopPing();
487
- this.stopHealthCheck();
488
- this.socketId++;
489
- this.ws?.close();
490
- this.ws = null;
491
- // Reset attempts and reconnect immediately
492
- this.reconnectAttempts = 0;
493
- this.dispatch?.(setReconnectAttempts(0));
494
- this.connect(this.currentSessionId, this.currentUserId || undefined);
495
- }
496
- clearReconnect() {
497
- if (this.reconnectTimeout) {
498
- clearTimeout(this.reconnectTimeout);
499
- this.reconnectTimeout = null;
500
- }
501
- }
502
- maybeReconnect() {
503
- if (!this.currentSessionId)
504
- return;
505
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
506
- console.error('[SessionSocket] Max reconnect attempts');
507
- this.dispatch?.(setError('Max reconnect attempts reached'));
508
- return;
509
- }
510
- this.reconnectAttempts++;
511
- this.dispatch?.(setReconnectAttempts(this.reconnectAttempts));
512
- // Exponential backoff with jitter (PartyKit pattern)
513
- // Base delay: 1-5s, growth factor: 1.3, max: 10s
514
- const baseDelay = 1000 + Math.random() * 4000; // 1-5s jitter
515
- const delay = Math.min(baseDelay * Math.pow(1.3, this.reconnectAttempts - 1), 10000);
516
- console.log(`[SessionSocket] Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts})`);
517
- this.dispatch?.(setError(`Reconnecting... (attempt ${this.reconnectAttempts})`));
518
- this.reconnectTimeout = setTimeout(() => {
519
- if (this.currentSessionId) {
520
- this.connect(this.currentSessionId, this.currentUserId || undefined);
521
- }
522
- }, delay);
523
- }
524
- }
525
- // Singleton instance
526
- export const sessionSocketManager = new SessionSocketManager();
527
- // =============================================================================
528
- // Async Thunks
529
- // =============================================================================
530
- export const connectSession = createAsyncThunk('agentSession/connect', async ({ url, sessionId, userId }, { dispatch }) => {
531
- sessionSocketManager.setDispatch(dispatch);
532
- sessionSocketManager.setBaseUrl(url);
533
- sessionSocketManager.connect(sessionId, userId);
351
+ this.trySendPending(clientMessageId);
352
+ return;
353
+ }
354
+ this.pendingClientMessageIds.delete(clientMessageId);
355
+ this.pendingClientMessages.delete(clientMessageId);
356
+ if (dispatch) {
357
+ dispatch(markMessageFailed({
358
+ clientMessageId,
359
+ error: "Server did not acknowledge message"
360
+ }));
361
+ }
362
+ }, 2e3);
363
+ this.pendingClientMessageTimers.set(clientMessageId, timer);
364
+ }
365
+ confirmClientMessage(clientMessageId) {
366
+ if (!this.pendingClientMessageIds.has(clientMessageId) && !this.pendingClientMessages.has(clientMessageId)) {
367
+ return;
368
+ }
369
+ this.pendingClientMessageIds.delete(clientMessageId);
370
+ this.pendingClientMessages.delete(clientMessageId);
371
+ this.clearAckTimer(clientMessageId);
372
+ this.dispatch?.(markMessageSent({ clientMessageId }));
373
+ }
374
+ clearAckTimer(clientMessageId) {
375
+ const timer = this.pendingClientMessageTimers.get(clientMessageId);
376
+ if (timer) {
377
+ clearTimeout(timer);
378
+ this.pendingClientMessageTimers.delete(clientMessageId);
379
+ }
380
+ }
381
+ stopAgent() {
382
+ this.send({ type: "AGENT_STOP" });
383
+ }
384
+ resetAgent() {
385
+ this.send({ type: "AGENT_RESET" });
386
+ }
387
+ startPing() {
388
+ this.stopPing();
389
+ this.pingInterval = setInterval(() => {
390
+ this.send({ type: "PING" });
391
+ }, 3e4);
392
+ }
393
+ stopPing() {
394
+ if (this.pingInterval) {
395
+ clearInterval(this.pingInterval);
396
+ this.pingInterval = null;
397
+ }
398
+ }
399
+ /**
400
+ * Start health check interval (tldraw pattern)
401
+ * Monitors time since last server interaction and resets connection if stale
402
+ */
403
+ startHealthCheck() {
404
+ this.stopHealthCheck();
405
+ this.healthCheckInterval = setInterval(() => {
406
+ this.checkConnectionHealth();
407
+ }, HEALTH_CHECK_INTERVAL_MS);
408
+ }
409
+ stopHealthCheck() {
410
+ if (this.healthCheckInterval) {
411
+ clearInterval(this.healthCheckInterval);
412
+ this.healthCheckInterval = null;
413
+ }
414
+ }
415
+ /**
416
+ * Check connection health based on time since last server interaction
417
+ * Inspired by tldraw's MAX_TIME_TO_WAIT_FOR_SERVER_INTERACTION_BEFORE_RESETTING_CONNECTION
418
+ */
419
+ checkConnectionHealth() {
420
+ if (!this.isConnected) return;
421
+ const now = Date.now();
422
+ const timeSinceLastInteraction = now - this.lastServerInteraction;
423
+ if (timeSinceLastInteraction > DEAD_THRESHOLD_MS) {
424
+ console.warn(`[SessionSocket] Connection dead (${timeSinceLastInteraction}ms since last interaction), forcing reconnect`);
425
+ this.dispatch?.(setConnectionHealth("dead"));
426
+ this.forceReconnect();
427
+ } else if (timeSinceLastInteraction > STALE_THRESHOLD_MS) {
428
+ console.warn(`[SessionSocket] Connection stale (${timeSinceLastInteraction}ms since last interaction)`);
429
+ this.dispatch?.(setConnectionHealth("stale"));
430
+ this.send({ type: "PING" });
431
+ } else {
432
+ this.dispatch?.(setConnectionHealth("healthy"));
433
+ }
434
+ }
435
+ /**
436
+ * Record server interaction timestamp
437
+ * Called on any meaningful server message (not just PONG)
438
+ */
439
+ recordServerInteraction() {
440
+ this.lastServerInteraction = Date.now();
441
+ this.dispatch?.(updateLastServerInteraction(this.lastServerInteraction));
442
+ }
443
+ /**
444
+ * Force reconnect - closes current socket and reconnects immediately
445
+ * Used when connection is detected as dead
446
+ */
447
+ forceReconnect() {
448
+ if (!this.currentSessionId) return;
449
+ console.log("[SessionSocket] Force reconnecting...");
450
+ this.stopPing();
451
+ this.stopHealthCheck();
452
+ this.socketId++;
453
+ this.ws?.close();
454
+ this.ws = null;
455
+ this.reconnectAttempts = 0;
456
+ this.dispatch?.(setReconnectAttempts(0));
457
+ this.connect(this.currentSessionId, this.currentUserId || void 0);
458
+ }
459
+ clearReconnect() {
460
+ if (this.reconnectTimeout) {
461
+ clearTimeout(this.reconnectTimeout);
462
+ this.reconnectTimeout = null;
463
+ }
464
+ }
465
+ maybeReconnect() {
466
+ if (!this.currentSessionId) return;
467
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
468
+ console.error("[SessionSocket] Max reconnect attempts");
469
+ this.dispatch?.(setError("Max reconnect attempts reached"));
470
+ return;
471
+ }
472
+ this.reconnectAttempts++;
473
+ this.dispatch?.(setReconnectAttempts(this.reconnectAttempts));
474
+ const baseDelay = 1e3 + Math.random() * 4e3;
475
+ const delay = Math.min(baseDelay * Math.pow(1.3, this.reconnectAttempts - 1), 1e4);
476
+ console.log(`[SessionSocket] Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts})`);
477
+ this.dispatch?.(setError(`Reconnecting... (attempt ${this.reconnectAttempts})`));
478
+ this.reconnectTimeout = setTimeout(() => {
479
+ if (this.currentSessionId) {
480
+ this.connect(this.currentSessionId, this.currentUserId || void 0);
481
+ }
482
+ }, delay);
483
+ }
484
+ };
485
+ var sessionSocketManager = new SessionSocketManager();
486
+ var connectSession = createAsyncThunk("agentSession/connect", async ({ url, sessionId, userId }, { dispatch }) => {
487
+ sessionSocketManager.setDispatch(dispatch);
488
+ sessionSocketManager.setBaseUrl(url);
489
+ sessionSocketManager.connect(sessionId, userId);
534
490
  });
535
- export const disconnectSession = createAsyncThunk('agentSession/disconnect', async () => {
491
+ var disconnectSession = createAsyncThunk(
492
+ "agentSession/disconnect",
493
+ async () => {
536
494
  sessionSocketManager.disconnect();
537
- });
538
- export const startSandbox = createAsyncThunk('agentSession/startSandbox', async (payload) => {
539
- if (typeof payload === 'string') {
540
- // Legacy: just template string
541
- sessionSocketManager.startSandbox(payload);
542
- }
543
- else if (payload) {
544
- // New: full payload with bootstrap options
545
- sessionSocketManager.startSandbox(payload.template, payload.bootstrap);
546
- }
547
- else {
548
- sessionSocketManager.startSandbox();
549
- }
550
- });
551
- export const stopSandbox = createAsyncThunk('agentSession/stopSandbox', async () => {
495
+ }
496
+ );
497
+ var startSandbox = createAsyncThunk(
498
+ "agentSession/startSandbox",
499
+ async (payload) => {
500
+ if (typeof payload === "string") {
501
+ sessionSocketManager.startSandbox(payload);
502
+ } else if (payload) {
503
+ sessionSocketManager.startSandbox(payload.template, payload.bootstrap);
504
+ } else {
505
+ sessionSocketManager.startSandbox();
506
+ }
507
+ }
508
+ );
509
+ var stopSandbox = createAsyncThunk(
510
+ "agentSession/stopSandbox",
511
+ async () => {
552
512
  sessionSocketManager.stopSandbox();
553
- });
554
- export const execCommand = createAsyncThunk('agentSession/exec', async ({ cmd, cwd }) => {
513
+ }
514
+ );
515
+ var execCommand = createAsyncThunk(
516
+ "agentSession/exec",
517
+ async ({ cmd, cwd }) => {
555
518
  sessionSocketManager.exec(cmd, cwd);
519
+ }
520
+ );
521
+ var sendAgentMessage = createAsyncThunk("agentSession/sendMessage", async ({ content, model, config, systemPrompt, agentType, author }, { dispatch }) => {
522
+ const clientMessageId = createClientMessageId();
523
+ dispatch(addUserMessage({ content, clientMessageId, author }));
524
+ sessionSocketManager.sendAgentMessage(
525
+ content,
526
+ model,
527
+ config,
528
+ systemPrompt,
529
+ agentType,
530
+ clientMessageId,
531
+ author
532
+ );
556
533
  });
557
- export const sendAgentMessage = createAsyncThunk('agentSession/sendMessage', async ({ content, model, config, systemPrompt, agentType, author }, { dispatch }) => {
558
- const clientMessageId = createClientMessageId();
559
- dispatch(addUserMessage({ content, clientMessageId, author }));
560
- sessionSocketManager.sendAgentMessage(content, model, config, systemPrompt, agentType, clientMessageId, author);
561
- });
562
- export const stopAgent = createAsyncThunk('agentSession/stopAgent', async () => {
534
+ var stopAgent = createAsyncThunk(
535
+ "agentSession/stopAgent",
536
+ async () => {
563
537
  sessionSocketManager.stopAgent();
564
- });
565
- export const resetAgent = createAsyncThunk('agentSession/resetAgent', async (_, { dispatch }) => {
538
+ }
539
+ );
540
+ var resetAgent = createAsyncThunk(
541
+ "agentSession/resetAgent",
542
+ async (_, { dispatch }) => {
566
543
  sessionSocketManager.resetAgent();
567
544
  dispatch(clearAgentSession());
568
- });
569
- /**
570
- * Force reconnect - useful when connection is stale/dead
571
- * Inspired by tldraw's restart() method
572
- */
573
- export const forceReconnect = createAsyncThunk('agentSession/forceReconnect', async () => {
545
+ }
546
+ );
547
+ var forceReconnect = createAsyncThunk(
548
+ "agentSession/forceReconnect",
549
+ async () => {
574
550
  sessionSocketManager.forceReconnect();
575
- });
576
- // =============================================================================
577
- // Slice
578
- // =============================================================================
579
- const agentSessionSlice = createSlice({
580
- name: 'agentSession',
581
- initialState,
582
- reducers: {
583
- // Connection
584
- setConnected: (state, action) => {
585
- state.isConnected = true;
586
- state.clientId = action.payload.clientId;
587
- state.sessionId = action.payload.sessionId;
588
- state.error = null;
589
- },
590
- setDisconnected: (state) => {
591
- state.isConnected = false;
592
- state.clientId = null;
593
- state.connectionHealth = 'healthy'; // Reset health on disconnect
594
- },
595
- setLastEventId: (state, action) => {
596
- state.lastEventId = action.payload;
597
- },
598
- // Connection health (tldraw pattern)
599
- updateLastServerInteraction: (state, action) => {
600
- state.lastServerInteraction = action.payload;
601
- },
602
- setConnectionHealth: (state, action) => {
603
- state.connectionHealth = action.payload;
604
- },
605
- setReconnectAttempts: (state, action) => {
606
- state.reconnectAttempts = action.payload;
607
- },
608
- // Sandbox
609
- setReady: (state, action) => {
610
- if (action.payload.sandboxId) {
611
- state.sandboxId = action.payload.sandboxId;
612
- // If sandbox already exists on reconnect, set status to ready immediately.
613
- // This prevents the auto-start effect from firing unnecessarily.
614
- //
615
- // Trust relationship: Server validates sandbox health via checkSandboxHealth()
616
- // before including sandboxId in READY message. If sandboxId is present, server
617
- // already confirmed the sandbox is alive. See session-socket.ts constructor.
618
- state.sandboxStatus = 'ready';
619
- }
620
- if (action.payload.sandboxUrl)
621
- state.sandboxUrl = action.payload.sandboxUrl;
622
- },
623
- setSandboxStatus: (state, action) => {
624
- state.sandboxStatus = action.payload.status;
625
- if (action.payload.sandboxId)
626
- state.sandboxId = action.payload.sandboxId;
627
- if (action.payload.sandboxUrl)
628
- state.sandboxUrl = action.payload.sandboxUrl;
629
- if (action.payload.error)
630
- state.error = action.payload.error;
631
- },
632
- setLifecycle: (state, action) => {
633
- state.lifecycle = action.payload.phase;
634
- state.lifecycleMessage = action.payload.message || null;
635
- },
636
- // Agent
637
- agentStarted: (state, action) => {
638
- state.agentStatus = 'streaming';
639
- // Add user message if prompt provided (replay or another client's message).
640
- // Skip if we already added it locally (deduplicate by clientMessageId).
641
- if (action.payload?.prompt) {
642
- const isDuplicate = action.payload.clientMessageId &&
643
- state.messages.some(m => m.clientMessageId === action.payload.clientMessageId);
644
- if (!isDuplicate) {
645
- state.messages.push({
646
- id: `msg-user-${Date.now()}`,
647
- role: 'user',
648
- content: action.payload.prompt,
649
- timestamp: Date.now(),
650
- author: action.payload.author,
651
- });
652
- }
653
- }
654
- const lastIdx = state.messages.length - 1;
655
- if (lastIdx >= 0) {
656
- const last = state.messages[lastIdx];
657
- const lastContent = last.content;
658
- const isEmptyAssistant = last.role === 'assistant' &&
659
- (lastContent === '' || (Array.isArray(lastContent) && lastContent.length === 0));
660
- if (isEmptyAssistant)
661
- return;
662
- }
663
- state.messages.push({
664
- id: `msg-${Date.now()}`,
665
- role: 'assistant',
666
- content: '',
667
- timestamp: Date.now(),
668
- });
669
- },
670
- updateAgentMessage: (state, action) => {
671
- const lastIdx = state.messages.length - 1;
672
- if (lastIdx >= 0 && state.messages[lastIdx].role === 'assistant') {
673
- const existing = state.messages[lastIdx].content;
674
- if (Array.isArray(existing) && existing.length > 0) {
675
- // Append new turn's content blocks to existing
676
- state.messages[lastIdx].content = [...existing, ...action.payload.content];
677
- }
678
- else {
679
- // First update (content was '' from agentStarted)
680
- state.messages[lastIdx].content = action.payload.content;
681
- }
682
- return;
683
- }
684
- state.messages.push({
685
- id: `msg-${Date.now()}`,
686
- role: 'assistant',
687
- content: action.payload.content,
688
- timestamp: Date.now(),
689
- });
690
- },
691
- agentDone: (state, action) => {
692
- state.agentStatus = 'idle';
693
- if (action.payload.sessionId)
694
- state.agentSessionId = action.payload.sessionId;
695
- },
696
- agentError: (state, action) => {
697
- state.agentStatus = 'error';
698
- state.error = action.payload.error;
699
- },
700
- addUserMessage: (state, action) => {
701
- state.messages.push({
702
- id: `msg-${Date.now()}`,
703
- role: 'user',
704
- content: action.payload.content,
705
- timestamp: Date.now(),
706
- clientMessageId: action.payload.clientMessageId,
707
- status: action.payload.clientMessageId ? 'pending' : undefined,
708
- author: action.payload.author,
709
- });
710
- state.error = null;
711
- },
712
- addAssistantPlaceholder: (state, action) => {
713
- state.messages.push({
714
- id: `msg-${Date.now()}`,
715
- role: 'assistant',
716
- content: '',
717
- timestamp: Date.now(),
718
- clientMessageId: action.payload.clientMessageId,
719
- status: 'pending',
720
- });
721
- },
722
- markMessageSent: (state, action) => {
723
- const msg = state.messages.find((m) => m.clientMessageId === action.payload.clientMessageId);
724
- if (msg) {
725
- msg.status = 'sent';
726
- }
727
- const hasAssistant = state.messages.some((m) => m.role === 'assistant' && m.clientMessageId === action.payload.clientMessageId);
728
- if (!hasAssistant) {
729
- state.messages.push({
730
- id: `msg-${Date.now()}`,
731
- role: 'assistant',
732
- content: '',
733
- timestamp: Date.now(),
734
- clientMessageId: action.payload.clientMessageId,
735
- status: 'pending',
736
- });
737
- }
738
- },
739
- markMessageFailed: (state, action) => {
740
- const msg = state.messages.find((m) => m.clientMessageId === action.payload.clientMessageId);
741
- if (msg) {
742
- msg.status = 'failed';
743
- }
744
- state.error = action.payload.error;
745
- const lastIdx = state.messages.length - 1;
746
- if (lastIdx >= 0 && state.messages[lastIdx].role === 'assistant') {
747
- const lastContent = state.messages[lastIdx].content;
748
- if (lastContent === '' || (Array.isArray(lastContent) && lastContent.length === 0)) {
749
- state.messages.pop();
750
- }
751
- }
752
- },
753
- // Files
754
- addUploadedFile: (state, action) => {
755
- state.uploadedFiles.push(action.payload);
756
- },
757
- clearUploadedFiles: (state) => {
758
- state.uploadedFiles = [];
759
- },
760
- setSandboxFiles: (state, action) => {
761
- state.sandboxFiles = action.payload;
762
- },
763
- // Error
764
- setError: (state, action) => {
765
- state.error = action.payload;
766
- },
767
- // Reset
768
- clearMessages: (state) => {
769
- state.messages = [];
770
- },
771
- clearAgentSession: (state) => {
772
- state.agentSessionId = null;
773
- },
774
- resetState: () => initialState,
775
- /**
776
- * Reset session-specific state when switching documents.
777
- * Clears sandbox, messages, agent state but preserves connection state.
778
- * Called automatically when sessionId changes in connect().
779
- */
780
- resetSessionState: (state) => {
781
- state.lastEventId = null; // Clear cursor to prevent cross-session contamination
782
- state.sandboxStatus = 'idle';
783
- state.sandboxId = null;
784
- state.sandboxUrl = null;
785
- state.lifecycle = 'idle';
786
- state.lifecycleMessage = null;
787
- state.agentStatus = 'idle';
788
- state.agentSessionId = null;
789
- state.messages = [];
790
- state.uploadedFiles = [];
791
- state.sandboxFiles = [];
792
- state.error = null;
793
- },
551
+ }
552
+ );
553
+ var agentSessionSlice = createSlice({
554
+ name: "agentSession",
555
+ initialState,
556
+ reducers: {
557
+ // Connection
558
+ setConnected: (state, action) => {
559
+ state.isConnected = true;
560
+ state.clientId = action.payload.clientId;
561
+ state.sessionId = action.payload.sessionId;
562
+ state.error = null;
563
+ },
564
+ setDisconnected: (state) => {
565
+ state.isConnected = false;
566
+ state.clientId = null;
567
+ state.connectionHealth = "healthy";
568
+ },
569
+ setLastEventId: (state, action) => {
570
+ state.lastEventId = action.payload;
571
+ },
572
+ // Connection health (tldraw pattern)
573
+ updateLastServerInteraction: (state, action) => {
574
+ state.lastServerInteraction = action.payload;
575
+ },
576
+ setConnectionHealth: (state, action) => {
577
+ state.connectionHealth = action.payload;
578
+ },
579
+ setReconnectAttempts: (state, action) => {
580
+ state.reconnectAttempts = action.payload;
794
581
  },
582
+ // Sandbox
583
+ setReady: (state, action) => {
584
+ if (action.payload.sandboxId) {
585
+ state.sandboxId = action.payload.sandboxId;
586
+ state.sandboxStatus = "ready";
587
+ }
588
+ if (action.payload.sandboxUrl) state.sandboxUrl = action.payload.sandboxUrl;
589
+ },
590
+ setSandboxStatus: (state, action) => {
591
+ state.sandboxStatus = action.payload.status;
592
+ if (action.payload.sandboxId) state.sandboxId = action.payload.sandboxId;
593
+ if (action.payload.sandboxUrl) state.sandboxUrl = action.payload.sandboxUrl;
594
+ if (action.payload.error) state.error = action.payload.error;
595
+ },
596
+ setLifecycle: (state, action) => {
597
+ state.lifecycle = action.payload.phase;
598
+ state.lifecycleMessage = action.payload.message || null;
599
+ },
600
+ // Agent
601
+ agentStarted: (state, action) => {
602
+ state.agentStatus = "streaming";
603
+ if (action.payload?.prompt) {
604
+ const isDuplicate = action.payload.clientMessageId && state.messages.some((m) => m.clientMessageId === action.payload.clientMessageId);
605
+ if (!isDuplicate) {
606
+ state.messages.push({
607
+ id: `msg-user-${Date.now()}`,
608
+ role: "user",
609
+ content: action.payload.prompt,
610
+ timestamp: Date.now(),
611
+ author: action.payload.author
612
+ });
613
+ }
614
+ }
615
+ const lastIdx = state.messages.length - 1;
616
+ if (lastIdx >= 0) {
617
+ const last = state.messages[lastIdx];
618
+ const lastContent = last.content;
619
+ const isEmptyAssistant = last.role === "assistant" && (lastContent === "" || Array.isArray(lastContent) && lastContent.length === 0);
620
+ if (isEmptyAssistant) return;
621
+ }
622
+ state.messages.push({
623
+ id: `msg-${Date.now()}`,
624
+ role: "assistant",
625
+ content: "",
626
+ timestamp: Date.now()
627
+ });
628
+ },
629
+ updateAgentMessage: (state, action) => {
630
+ const lastIdx = state.messages.length - 1;
631
+ if (lastIdx >= 0 && state.messages[lastIdx].role === "assistant") {
632
+ const existing = state.messages[lastIdx].content;
633
+ if (Array.isArray(existing) && existing.length > 0) {
634
+ state.messages[lastIdx].content = [...existing, ...action.payload.content];
635
+ } else {
636
+ state.messages[lastIdx].content = action.payload.content;
637
+ }
638
+ return;
639
+ }
640
+ state.messages.push({
641
+ id: `msg-${Date.now()}`,
642
+ role: "assistant",
643
+ content: action.payload.content,
644
+ timestamp: Date.now()
645
+ });
646
+ },
647
+ agentDone: (state, action) => {
648
+ state.agentStatus = "idle";
649
+ if (action.payload.sessionId) state.agentSessionId = action.payload.sessionId;
650
+ },
651
+ agentError: (state, action) => {
652
+ state.agentStatus = "error";
653
+ state.error = action.payload.error;
654
+ },
655
+ addUserMessage: (state, action) => {
656
+ state.messages.push({
657
+ id: `msg-${Date.now()}`,
658
+ role: "user",
659
+ content: action.payload.content,
660
+ timestamp: Date.now(),
661
+ clientMessageId: action.payload.clientMessageId,
662
+ status: action.payload.clientMessageId ? "pending" : void 0,
663
+ author: action.payload.author
664
+ });
665
+ state.error = null;
666
+ },
667
+ addAssistantPlaceholder: (state, action) => {
668
+ state.messages.push({
669
+ id: `msg-${Date.now()}`,
670
+ role: "assistant",
671
+ content: "",
672
+ timestamp: Date.now(),
673
+ clientMessageId: action.payload.clientMessageId,
674
+ status: "pending"
675
+ });
676
+ },
677
+ markMessageSent: (state, action) => {
678
+ const msg = state.messages.find((m) => m.clientMessageId === action.payload.clientMessageId);
679
+ if (msg) {
680
+ msg.status = "sent";
681
+ }
682
+ const hasAssistant = state.messages.some(
683
+ (m) => m.role === "assistant" && m.clientMessageId === action.payload.clientMessageId
684
+ );
685
+ if (!hasAssistant) {
686
+ state.messages.push({
687
+ id: `msg-${Date.now()}`,
688
+ role: "assistant",
689
+ content: "",
690
+ timestamp: Date.now(),
691
+ clientMessageId: action.payload.clientMessageId,
692
+ status: "pending"
693
+ });
694
+ }
695
+ },
696
+ markMessageFailed: (state, action) => {
697
+ const msg = state.messages.find((m) => m.clientMessageId === action.payload.clientMessageId);
698
+ if (msg) {
699
+ msg.status = "failed";
700
+ }
701
+ state.error = action.payload.error;
702
+ const lastIdx = state.messages.length - 1;
703
+ if (lastIdx >= 0 && state.messages[lastIdx].role === "assistant") {
704
+ const lastContent = state.messages[lastIdx].content;
705
+ if (lastContent === "" || Array.isArray(lastContent) && lastContent.length === 0) {
706
+ state.messages.pop();
707
+ }
708
+ }
709
+ },
710
+ // Files
711
+ addUploadedFile: (state, action) => {
712
+ state.uploadedFiles.push(action.payload);
713
+ },
714
+ clearUploadedFiles: (state) => {
715
+ state.uploadedFiles = [];
716
+ },
717
+ setSandboxFiles: (state, action) => {
718
+ state.sandboxFiles = action.payload;
719
+ },
720
+ // Error
721
+ setError: (state, action) => {
722
+ state.error = action.payload;
723
+ },
724
+ // Reset
725
+ clearMessages: (state) => {
726
+ state.messages = [];
727
+ },
728
+ clearAgentSession: (state) => {
729
+ state.agentSessionId = null;
730
+ },
731
+ resetState: () => initialState,
732
+ /**
733
+ * Reset session-specific state when switching documents.
734
+ * Clears sandbox, messages, agent state but preserves connection state.
735
+ * Called automatically when sessionId changes in connect().
736
+ */
737
+ resetSessionState: (state) => {
738
+ state.lastEventId = null;
739
+ state.sandboxStatus = "idle";
740
+ state.sandboxId = null;
741
+ state.sandboxUrl = null;
742
+ state.lifecycle = "idle";
743
+ state.lifecycleMessage = null;
744
+ state.agentStatus = "idle";
745
+ state.agentSessionId = null;
746
+ state.messages = [];
747
+ state.uploadedFiles = [];
748
+ state.sandboxFiles = [];
749
+ state.error = null;
750
+ }
751
+ }
795
752
  });
796
- export const { setConnected, setDisconnected, setLastEventId, updateLastServerInteraction, setConnectionHealth, setReconnectAttempts, setReady, setSandboxStatus, setLifecycle, agentStarted, updateAgentMessage, agentDone, agentError, addUserMessage, markMessageSent, markMessageFailed, addUploadedFile, clearUploadedFiles, setSandboxFiles, setError, clearMessages, clearAgentSession, resetState, resetSessionState, } = agentSessionSlice.actions;
797
- export const selectIsConnected = (state) => state.agentSession.isConnected;
798
- export const selectClientId = (state) => state.agentSession.clientId;
799
- export const selectSessionId = (state) => state.agentSession.sessionId;
800
- export const selectLastEventId = (state) => state.agentSession.lastEventId;
801
- export const selectLastServerInteraction = (state) => state.agentSession.lastServerInteraction;
802
- export const selectConnectionHealth = (state) => state.agentSession.connectionHealth;
803
- export const selectReconnectAttempts = (state) => state.agentSession.reconnectAttempts;
804
- export const selectSandboxStatus = (state) => state.agentSession.sandboxStatus;
805
- export const selectSandboxId = (state) => state.agentSession.sandboxId;
806
- export const selectSandboxUrl = (state) => state.agentSession.sandboxUrl;
807
- export const selectLifecycle = (state) => state.agentSession.lifecycle;
808
- export const selectLifecycleMessage = (state) => state.agentSession.lifecycleMessage;
809
- export const selectAgentStatus = (state) => state.agentSession.agentStatus;
810
- export const selectAgentSessionId = (state) => state.agentSession.agentSessionId;
811
- export const selectMessages = (state) => state.agentSession.messages;
812
- export const selectUploadedFiles = (state) => state.agentSession.uploadedFiles;
813
- export const selectSandboxFiles = (state) => state.agentSession.sandboxFiles;
814
- export const selectError = (state) => state.agentSession.error;
815
- export default agentSessionSlice.reducer;
816
- //# sourceMappingURL=slice.js.map
753
+ var {
754
+ setConnected,
755
+ setDisconnected,
756
+ setLastEventId,
757
+ updateLastServerInteraction,
758
+ setConnectionHealth,
759
+ setReconnectAttempts,
760
+ setReady,
761
+ setSandboxStatus,
762
+ setLifecycle,
763
+ agentStarted,
764
+ updateAgentMessage,
765
+ agentDone,
766
+ agentError,
767
+ addUserMessage,
768
+ markMessageSent,
769
+ markMessageFailed,
770
+ addUploadedFile,
771
+ clearUploadedFiles,
772
+ setSandboxFiles,
773
+ setError,
774
+ clearMessages,
775
+ clearAgentSession,
776
+ resetState,
777
+ resetSessionState
778
+ } = agentSessionSlice.actions;
779
+ var selectIsConnected = (state) => state.agentSession.isConnected;
780
+ var selectClientId = (state) => state.agentSession.clientId;
781
+ var selectSessionId = (state) => state.agentSession.sessionId;
782
+ var selectLastEventId = (state) => state.agentSession.lastEventId;
783
+ var selectLastServerInteraction = (state) => state.agentSession.lastServerInteraction;
784
+ var selectConnectionHealth = (state) => state.agentSession.connectionHealth;
785
+ var selectReconnectAttempts = (state) => state.agentSession.reconnectAttempts;
786
+ var selectSandboxStatus = (state) => state.agentSession.sandboxStatus;
787
+ var selectSandboxId = (state) => state.agentSession.sandboxId;
788
+ var selectSandboxUrl = (state) => state.agentSession.sandboxUrl;
789
+ var selectLifecycle = (state) => state.agentSession.lifecycle;
790
+ var selectLifecycleMessage = (state) => state.agentSession.lifecycleMessage;
791
+ var selectAgentStatus = (state) => state.agentSession.agentStatus;
792
+ var selectAgentSessionId = (state) => state.agentSession.agentSessionId;
793
+ var selectMessages = (state) => state.agentSession.messages;
794
+ var selectUploadedFiles = (state) => state.agentSession.uploadedFiles;
795
+ var selectSandboxFiles = (state) => state.agentSession.sandboxFiles;
796
+ var selectError = (state) => state.agentSession.error;
797
+ var slice_default = agentSessionSlice.reducer;
798
+ export {
799
+ addUploadedFile,
800
+ addUserMessage,
801
+ agentDone,
802
+ agentError,
803
+ agentStarted,
804
+ clearAgentSession,
805
+ clearMessages,
806
+ clearUploadedFiles,
807
+ connectSession,
808
+ slice_default as default,
809
+ disconnectSession,
810
+ execCommand,
811
+ forceReconnect,
812
+ markMessageFailed,
813
+ markMessageSent,
814
+ resetAgent,
815
+ resetSessionState,
816
+ resetState,
817
+ selectAgentSessionId,
818
+ selectAgentStatus,
819
+ selectClientId,
820
+ selectConnectionHealth,
821
+ selectError,
822
+ selectIsConnected,
823
+ selectLastEventId,
824
+ selectLastServerInteraction,
825
+ selectLifecycle,
826
+ selectLifecycleMessage,
827
+ selectMessages,
828
+ selectReconnectAttempts,
829
+ selectSandboxFiles,
830
+ selectSandboxId,
831
+ selectSandboxStatus,
832
+ selectSandboxUrl,
833
+ selectSessionId,
834
+ selectUploadedFiles,
835
+ sendAgentMessage,
836
+ sessionSocketManager,
837
+ setConnected,
838
+ setConnectionHealth,
839
+ setDisconnected,
840
+ setError,
841
+ setLastEventId,
842
+ setLifecycle,
843
+ setReady,
844
+ setReconnectAttempts,
845
+ setSandboxFiles,
846
+ setSandboxStatus,
847
+ startSandbox,
848
+ stopAgent,
849
+ stopSandbox,
850
+ updateAgentMessage,
851
+ updateLastServerInteraction
852
+ };