@raisindb/client 0.1.0 → 0.1.1

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.
Files changed (44) hide show
  1. package/README.md +313 -70
  2. package/dist/chunk-ASOWGNQZ.mjs +532 -0
  3. package/dist/chunk-ASOWGNQZ.mjs.map +1 -0
  4. package/dist/chunk-FYWYSNQG.mjs +190 -0
  5. package/dist/chunk-FYWYSNQG.mjs.map +1 -0
  6. package/dist/chunk-NOPZAMIV.mjs +1427 -0
  7. package/dist/chunk-NOPZAMIV.mjs.map +1 -0
  8. package/dist/chunk-T2ZSNYFG.mjs +248 -0
  9. package/dist/chunk-T2ZSNYFG.mjs.map +1 -0
  10. package/dist/chunk-TPZNJNZR.mjs +866 -0
  11. package/dist/chunk-TPZNJNZR.mjs.map +1 -0
  12. package/dist/chunk-ZCRYQCXS.mjs +170 -0
  13. package/dist/chunk-ZCRYQCXS.mjs.map +1 -0
  14. package/dist/conversation-list-store-D-W6_nsv.d.mts +8447 -0
  15. package/dist/conversation-list-store-D-W6_nsv.d.ts +8447 -0
  16. package/dist/index-B4CqE3Cb.d.ts +301 -0
  17. package/dist/index-CeBRaSop.d.mts +301 -0
  18. package/dist/index.d.mts +72 -6109
  19. package/dist/index.d.ts +72 -6109
  20. package/dist/index.js +3123 -257
  21. package/dist/index.js.map +1 -0
  22. package/dist/index.mjs +2745 -3185
  23. package/dist/index.mjs.map +1 -0
  24. package/dist/react.d.mts +4 -0
  25. package/dist/react.d.ts +4 -0
  26. package/dist/react.js +2569 -0
  27. package/dist/react.js.map +1 -0
  28. package/dist/react.mjs +9 -0
  29. package/dist/react.mjs.map +1 -0
  30. package/dist/svelte-sql-CQoB4FEm.d.mts +52 -0
  31. package/dist/svelte-sql-CTXjkz0R.d.ts +52 -0
  32. package/dist/svelte.d.mts +288 -0
  33. package/dist/svelte.d.ts +288 -0
  34. package/dist/svelte.js +2482 -0
  35. package/dist/svelte.js.map +1 -0
  36. package/dist/svelte.mjs +25 -0
  37. package/dist/svelte.mjs.map +1 -0
  38. package/dist/vue.d.mts +197 -0
  39. package/dist/vue.d.ts +197 -0
  40. package/dist/vue.js +1510 -0
  41. package/dist/vue.js.map +1 -0
  42. package/dist/vue.mjs +9 -0
  43. package/dist/vue.mjs.map +1 -0
  44. package/package.json +18 -3
@@ -0,0 +1,1427 @@
1
+ var __typeError = (msg) => {
2
+ throw TypeError(msg);
3
+ };
4
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
+ }) : x)(function(x) {
7
+ if (typeof require !== "undefined") return require.apply(this, arguments);
8
+ throw Error('Dynamic require of "' + x + '" is not supported');
9
+ });
10
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
11
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
12
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
13
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
14
+
15
+ // src/utils/reconnect.ts
16
+ var ReconnectManager = class {
17
+ constructor(options = {}) {
18
+ this.attempts = 0;
19
+ this.isReconnecting = false;
20
+ this.initialDelay = options.initialDelay ?? 1e3;
21
+ this.maxDelay = options.maxDelay ?? 3e4;
22
+ this.backoffMultiplier = options.backoffMultiplier ?? 2;
23
+ this.maxAttempts = options.maxAttempts ?? Infinity;
24
+ this.jitter = options.jitter ?? true;
25
+ this.currentDelay = this.initialDelay;
26
+ }
27
+ /**
28
+ * Schedule a reconnection attempt
29
+ *
30
+ * @param callback - Function to call when it's time to reconnect
31
+ * @returns true if reconnection was scheduled, false if max attempts reached
32
+ */
33
+ scheduleReconnect(callback) {
34
+ if (this.attempts >= this.maxAttempts) {
35
+ return false;
36
+ }
37
+ this.cancelReconnect();
38
+ this.isReconnecting = true;
39
+ this.attempts++;
40
+ let delay = this.currentDelay;
41
+ if (this.jitter) {
42
+ const jitterRange = delay * 0.25;
43
+ delay += (Math.random() * 2 - 1) * jitterRange;
44
+ delay = Math.max(0, Math.round(delay));
45
+ }
46
+ this.reconnectTimeout = setTimeout(async () => {
47
+ try {
48
+ await callback();
49
+ this.reset();
50
+ } catch (error) {
51
+ this.currentDelay = Math.min(
52
+ this.currentDelay * this.backoffMultiplier,
53
+ this.maxDelay
54
+ );
55
+ }
56
+ }, delay);
57
+ return true;
58
+ }
59
+ /**
60
+ * Cancel any pending reconnection attempt
61
+ */
62
+ cancelReconnect() {
63
+ if (this.reconnectTimeout) {
64
+ clearTimeout(this.reconnectTimeout);
65
+ this.reconnectTimeout = void 0;
66
+ }
67
+ this.isReconnecting = false;
68
+ }
69
+ /**
70
+ * Reset the reconnection state (call after successful connection)
71
+ */
72
+ reset() {
73
+ this.cancelReconnect();
74
+ this.currentDelay = this.initialDelay;
75
+ this.attempts = 0;
76
+ }
77
+ /**
78
+ * Check if currently in reconnection mode
79
+ */
80
+ isActive() {
81
+ return this.isReconnecting;
82
+ }
83
+ /**
84
+ * Get the current number of reconnection attempts
85
+ */
86
+ getAttempts() {
87
+ return this.attempts;
88
+ }
89
+ /**
90
+ * Get the current delay in milliseconds
91
+ */
92
+ getCurrentDelay() {
93
+ return this.currentDelay;
94
+ }
95
+ /**
96
+ * Check if max attempts have been reached
97
+ */
98
+ hasReachedMaxAttempts() {
99
+ return this.attempts >= this.maxAttempts;
100
+ }
101
+ };
102
+
103
+ // src/logger.ts
104
+ var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
105
+ LogLevel2[LogLevel2["Silent"] = 0] = "Silent";
106
+ LogLevel2[LogLevel2["Error"] = 1] = "Error";
107
+ LogLevel2[LogLevel2["Warn"] = 2] = "Warn";
108
+ LogLevel2[LogLevel2["Info"] = 3] = "Info";
109
+ LogLevel2[LogLevel2["Debug"] = 4] = "Debug";
110
+ return LogLevel2;
111
+ })(LogLevel || {});
112
+ var config = {
113
+ level: 1 /* Error */,
114
+ prefix: "[RaisinDB]"
115
+ };
116
+ function configureLogger(options) {
117
+ config = { ...config, ...options };
118
+ }
119
+ function getLoggerConfig() {
120
+ return { ...config };
121
+ }
122
+ function setLogLevel(level) {
123
+ config.level = level;
124
+ }
125
+ function getLogLevel() {
126
+ return config.level;
127
+ }
128
+ var logger = {
129
+ /**
130
+ * Log debug message (only shown when level >= Debug)
131
+ */
132
+ debug(...args) {
133
+ if (config.level >= 4 /* Debug */) {
134
+ console.log(config.prefix, ...args);
135
+ }
136
+ },
137
+ /**
138
+ * Log info message (only shown when level >= Info)
139
+ */
140
+ info(...args) {
141
+ if (config.level >= 3 /* Info */) {
142
+ console.log(config.prefix, ...args);
143
+ }
144
+ },
145
+ /**
146
+ * Log warning message (only shown when level >= Warn)
147
+ */
148
+ warn(...args) {
149
+ if (config.level >= 2 /* Warn */) {
150
+ console.warn(config.prefix, ...args);
151
+ }
152
+ },
153
+ /**
154
+ * Log error message (only shown when level >= Error)
155
+ */
156
+ error(...args) {
157
+ if (config.level >= 1 /* Error */) {
158
+ console.error(config.prefix, ...args);
159
+ }
160
+ }
161
+ };
162
+ if (typeof globalThis !== "undefined") {
163
+ globalThis.__raisinDebug = (enable = true) => {
164
+ setLogLevel(enable ? 4 /* Debug */ : 1 /* Error */);
165
+ console.log(`[RaisinDB] Debug logging ${enable ? "ENABLED" : "DISABLED"} (includes server handler logs)`);
166
+ };
167
+ }
168
+
169
+ // src/connection.ts
170
+ import { EventEmitter } from "events";
171
+ var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
172
+ ConnectionState2["Disconnected"] = "disconnected";
173
+ ConnectionState2["Connecting"] = "connecting";
174
+ ConnectionState2["Connected"] = "connected";
175
+ ConnectionState2["Reconnecting"] = "reconnecting";
176
+ ConnectionState2["Closed"] = "closed";
177
+ return ConnectionState2;
178
+ })(ConnectionState || {});
179
+ function getWebSocketImpl() {
180
+ if (typeof WebSocket !== "undefined") {
181
+ return WebSocket;
182
+ }
183
+ try {
184
+ const ws = __require("ws");
185
+ return ws.WebSocket || ws.default || ws;
186
+ } catch (error) {
187
+ throw new Error(
188
+ 'WebSocket implementation not found. In Node.js, please install the "ws" package: npm install ws'
189
+ );
190
+ }
191
+ }
192
+ var Connection = class extends EventEmitter {
193
+ constructor(url, options = {}) {
194
+ super();
195
+ this.state = "disconnected" /* Disconnected */;
196
+ this.manualClose = false;
197
+ this.url = url.replace(/^raisin:\/\//, "ws://").replace(/^raisins:\/\//, "wss://");
198
+ this.options = {
199
+ autoReconnect: options.autoReconnect ?? true,
200
+ heartbeatInterval: options.heartbeatInterval ?? 3e4,
201
+ heartbeatTimeout: options.heartbeatTimeout ?? 5e3,
202
+ protocols: options.protocols,
203
+ headers: options.headers
204
+ };
205
+ this.reconnectManager = new ReconnectManager(options.reconnectOptions);
206
+ this.WebSocketImpl = getWebSocketImpl();
207
+ }
208
+ /**
209
+ * Connect to the WebSocket server
210
+ */
211
+ async connect() {
212
+ if (this.state === "connected" /* Connected */ || this.state === "connecting" /* Connecting */) {
213
+ return;
214
+ }
215
+ this.manualClose = false;
216
+ this.setState("connecting" /* Connecting */);
217
+ return new Promise((resolve, reject) => {
218
+ try {
219
+ const isBrowserWebSocket = typeof WebSocket !== "undefined" && this.WebSocketImpl === WebSocket;
220
+ this.ws = isBrowserWebSocket || !this.options.headers ? new this.WebSocketImpl(this.url, this.options.protocols) : new this.WebSocketImpl(this.url, this.options.protocols, {
221
+ headers: this.options.headers
222
+ });
223
+ this.ws.binaryType = "arraybuffer";
224
+ this.ws.onopen = () => {
225
+ this.setState("connected" /* Connected */);
226
+ this.reconnectManager.reset();
227
+ this.startHeartbeat();
228
+ resolve();
229
+ };
230
+ this.ws.onmessage = (event) => {
231
+ logger.debug(`WebSocket onmessage fired - data type: ${typeof event.data}, is ArrayBuffer: ${event.data instanceof ArrayBuffer}`);
232
+ this.resetHeartbeat();
233
+ const data = event.data;
234
+ logger.debug(`Received binary message - size: ${data.byteLength} bytes`);
235
+ logger.debug(`Emitting 'message' event to client handlers`);
236
+ this.emit("message", data);
237
+ };
238
+ this.ws.onerror = () => {
239
+ const error = new Error("WebSocket error occurred");
240
+ this.emit("error", error);
241
+ if (this.state === "connecting" /* Connecting */) {
242
+ reject(error);
243
+ }
244
+ };
245
+ this.ws.onclose = (event) => {
246
+ this.stopHeartbeat();
247
+ this.emit("close", event);
248
+ if (!this.manualClose && this.options.autoReconnect) {
249
+ this.setState("reconnecting" /* Reconnecting */);
250
+ this.reconnect();
251
+ } else {
252
+ this.setState("disconnected" /* Disconnected */);
253
+ }
254
+ if (this.state === "connecting" /* Connecting */) {
255
+ reject(new Error(`Connection closed: ${event.reason || "Unknown reason"}`));
256
+ }
257
+ };
258
+ } catch (error) {
259
+ this.setState("disconnected" /* Disconnected */);
260
+ reject(error);
261
+ }
262
+ });
263
+ }
264
+ /**
265
+ * Disconnect from the WebSocket server
266
+ */
267
+ disconnect() {
268
+ this.manualClose = true;
269
+ this.reconnectManager.cancelReconnect();
270
+ this.stopHeartbeat();
271
+ if (this.ws) {
272
+ this.ws.close(1e3, "Client disconnect");
273
+ this.ws = void 0;
274
+ }
275
+ this.setState("closed" /* Closed */);
276
+ }
277
+ /**
278
+ * Send data through the WebSocket
279
+ *
280
+ * @param data - Data to send (Uint8Array for MessagePack)
281
+ */
282
+ send(data) {
283
+ if (this.state !== "connected" /* Connected */ || !this.ws) {
284
+ throw new Error("Cannot send data: not connected");
285
+ }
286
+ if (this.ws.readyState !== 1) {
287
+ logger.error(`WebSocket not open - readyState: ${this.ws.readyState} (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)`);
288
+ throw new Error(`Cannot send data: WebSocket not open (readyState: ${this.ws.readyState})`);
289
+ }
290
+ logger.debug(`Sending message - size: ${data.length} bytes, readyState: ${this.ws.readyState}`);
291
+ try {
292
+ this.ws.send(data);
293
+ logger.debug(`Message sent successfully`);
294
+ } catch (error) {
295
+ logger.error(`Failed to send message:`, error);
296
+ throw new Error(`Failed to send data: ${error}`);
297
+ }
298
+ }
299
+ /**
300
+ * Get current connection state
301
+ */
302
+ getState() {
303
+ return this.state;
304
+ }
305
+ /**
306
+ * Check if connected
307
+ */
308
+ isConnected() {
309
+ return this.state === "connected" /* Connected */;
310
+ }
311
+ /**
312
+ * Whether auto-reconnect is enabled for this connection.
313
+ *
314
+ * Used by the client to decide whether requests issued while the
315
+ * connection is down can be queued (the connection will come back) or
316
+ * should fail immediately.
317
+ */
318
+ isAutoReconnectEnabled() {
319
+ return this.options.autoReconnect;
320
+ }
321
+ /**
322
+ * Reconnect to the server
323
+ */
324
+ reconnect() {
325
+ const success = this.reconnectManager.scheduleReconnect(async () => {
326
+ try {
327
+ await this.connect();
328
+ } catch (error) {
329
+ }
330
+ });
331
+ if (!success) {
332
+ this.setState("disconnected" /* Disconnected */);
333
+ this.emit("error", new Error("Max reconnection attempts reached"));
334
+ }
335
+ }
336
+ /**
337
+ * Set connection state and emit event
338
+ */
339
+ setState(state) {
340
+ if (this.state !== state) {
341
+ this.state = state;
342
+ this.emit("stateChange", state);
343
+ }
344
+ }
345
+ /**
346
+ * Start heartbeat mechanism
347
+ */
348
+ startHeartbeat() {
349
+ if (this.options.heartbeatInterval <= 0) {
350
+ return;
351
+ }
352
+ this.stopHeartbeat();
353
+ }
354
+ /**
355
+ * Stop heartbeat mechanism
356
+ */
357
+ stopHeartbeat() {
358
+ if (this.heartbeatTimer) {
359
+ clearInterval(this.heartbeatTimer);
360
+ this.heartbeatTimer = void 0;
361
+ }
362
+ if (this.heartbeatTimeoutTimer) {
363
+ clearTimeout(this.heartbeatTimeoutTimer);
364
+ this.heartbeatTimeoutTimer = void 0;
365
+ }
366
+ }
367
+ /**
368
+ * Reset heartbeat timeout (called when message received)
369
+ */
370
+ resetHeartbeat() {
371
+ if (this.heartbeatTimeoutTimer) {
372
+ clearTimeout(this.heartbeatTimeoutTimer);
373
+ this.heartbeatTimeoutTimer = void 0;
374
+ }
375
+ }
376
+ };
377
+
378
+ // src/utils/home-path.ts
379
+ var ACCESS_CONTROL_PREFIX = "/raisin:access_control";
380
+ function normalizeHomePath(home) {
381
+ if (home === null || home === void 0) return null;
382
+ let path = home.trim();
383
+ if (path.length === 0) return null;
384
+ if (path === ACCESS_CONTROL_PREFIX) return "/";
385
+ if (path.startsWith(`${ACCESS_CONTROL_PREFIX}/`)) {
386
+ path = path.slice(ACCESS_CONTROL_PREFIX.length);
387
+ }
388
+ while (path.length > 1 && path.endsWith("/")) {
389
+ path = path.slice(0, -1);
390
+ }
391
+ return path;
392
+ }
393
+
394
+ // src/utils/plan-projection.ts
395
+ function asRecord(value) {
396
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
397
+ return value;
398
+ }
399
+ function asString(value) {
400
+ return typeof value === "string" && value.length > 0 ? value : void 0;
401
+ }
402
+ function parsePlanTasks(rawTasks) {
403
+ if (!Array.isArray(rawTasks)) return [];
404
+ return rawTasks.map((rawTask, index) => {
405
+ if (typeof rawTask === "string") {
406
+ return {
407
+ title: rawTask,
408
+ status: "pending"
409
+ };
410
+ }
411
+ const task = asRecord(rawTask);
412
+ if (!task) return null;
413
+ const delegation = asRecord(task.delegation);
414
+ return {
415
+ taskId: asString(task.task_id) ?? asString(task.id),
416
+ title: asString(task.title) ?? `Task ${index + 1}`,
417
+ status: asString(task.status) ?? "pending",
418
+ description: asString(task.description),
419
+ priority: asString(task.priority),
420
+ delegation: delegation ? {
421
+ id: asString(delegation.id),
422
+ status: asString(delegation.status) ?? "unknown",
423
+ agentRef: asString(delegation.agent_ref) ?? asString(delegation.agentRef),
424
+ flowInstanceId: asString(delegation.flow_instance_id) ?? asString(delegation.flowInstanceId),
425
+ branch: asString(delegation.branch),
426
+ baseBranch: asString(delegation.base_branch) ?? asString(delegation.baseBranch),
427
+ result: delegation.result,
428
+ error: asString(delegation.error)
429
+ } : void 0
430
+ };
431
+ }).filter((task) => task !== null);
432
+ }
433
+ var TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set(["completed", "cancelled"]);
434
+ var TERMINAL_PLAN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
435
+ function setPlanStatus(plan, nextStatus) {
436
+ if (!nextStatus) return;
437
+ if (TERMINAL_PLAN_STATUSES.has(plan.status) && !TERMINAL_PLAN_STATUSES.has(nextStatus)) {
438
+ return;
439
+ }
440
+ plan.status = nextStatus;
441
+ }
442
+ function upsertTask(plan, nextTask) {
443
+ const existingIndex = plan.tasks.findIndex(
444
+ (task) => nextTask.taskId && task.taskId === nextTask.taskId || !nextTask.taskId && task.title === nextTask.title
445
+ );
446
+ if (existingIndex === -1) {
447
+ plan.tasks.push(nextTask);
448
+ return;
449
+ }
450
+ const existing = plan.tasks[existingIndex];
451
+ if (TERMINAL_TASK_STATUSES.has(existing.status) && !TERMINAL_TASK_STATUSES.has(nextTask.status)) {
452
+ return;
453
+ }
454
+ plan.tasks[existingIndex] = {
455
+ ...existing,
456
+ ...nextTask
457
+ };
458
+ }
459
+ function ensurePlan(plans, key, fallbackTitle, timestamp) {
460
+ const existing = plans.get(key);
461
+ if (existing) {
462
+ if (timestamp) existing.updatedAt = timestamp;
463
+ return existing;
464
+ }
465
+ const created = {
466
+ key,
467
+ title: fallbackTitle,
468
+ status: "pending_approval",
469
+ requiresApproval: true,
470
+ tasks: [],
471
+ updatedAt: timestamp
472
+ };
473
+ plans.set(key, created);
474
+ return created;
475
+ }
476
+ function projectPlansFromMessages(messages) {
477
+ const ordered = [...messages].sort((a, b) => {
478
+ const aTime = Date.parse(a.timestamp || "");
479
+ const bTime = Date.parse(b.timestamp || "");
480
+ return (Number.isNaN(aTime) ? 0 : aTime) - (Number.isNaN(bTime) ? 0 : bTime);
481
+ });
482
+ const plans = /* @__PURE__ */ new Map();
483
+ for (const message of ordered) {
484
+ const timestamp = message.timestamp;
485
+ const data = asRecord(message.data);
486
+ if (message.messageType === "ai_plan" && data) {
487
+ const planPath = asString(data.plan_path);
488
+ const planId = asString(data.plan_id);
489
+ const key = planPath || planId || message.path || message.id || `plan:${timestamp}`;
490
+ const plan = ensurePlan(
491
+ plans,
492
+ key,
493
+ asString(data.title) || message.content || "Plan",
494
+ timestamp
495
+ );
496
+ plan.planPath = planPath ?? plan.planPath;
497
+ plan.planId = planId ?? plan.planId;
498
+ plan.title = asString(data.title) || plan.title;
499
+ setPlanStatus(plan, asString(data.status));
500
+ plan.requiresApproval = Boolean(data.requires_approval) || plan.status === "pending_approval";
501
+ plan.sourceMessagePath = message.path ?? plan.sourceMessagePath;
502
+ const parsedTasks = parsePlanTasks(data.tasks);
503
+ if (parsedTasks.length > 0) {
504
+ plan.tasks = parsedTasks;
505
+ }
506
+ }
507
+ if (message.messageType === "ai_task_update" && data) {
508
+ const planPath = asString(data.plan_path);
509
+ const planId = asString(data.plan_id);
510
+ const key = planPath || planId;
511
+ if (!key) continue;
512
+ const plan = ensurePlan(plans, key, "Plan", timestamp);
513
+ plan.planPath = planPath ?? plan.planPath;
514
+ plan.planId = planId ?? plan.planId;
515
+ setPlanStatus(plan, asString(data.plan_status));
516
+ plan.requiresApproval = plan.status === "pending_approval";
517
+ const taskUpdate = {
518
+ taskId: asString(data.task_id),
519
+ title: asString(data.task_title) || asString(data.title) || "Task",
520
+ status: asString(data.status) || "pending"
521
+ };
522
+ upsertTask(plan, taskUpdate);
523
+ }
524
+ if (Array.isArray(message.children)) {
525
+ for (const child of message.children) {
526
+ if (child.type !== "plan") continue;
527
+ const key = child.path || message.path || message.id || `plan-child:${child.id}:${timestamp}`;
528
+ const plan = ensurePlan(plans, key, child.planTitle || message.content || "Plan", timestamp);
529
+ plan.title = child.planTitle || plan.title;
530
+ plan.status = child.status || plan.status;
531
+ plan.requiresApproval = plan.status === "pending_approval";
532
+ if (Array.isArray(child.tasks) && child.tasks.length > 0) {
533
+ plan.tasks = child.tasks.map((task) => ({
534
+ taskId: task.id,
535
+ title: task.title,
536
+ status: task.status,
537
+ delegation: task.delegation ? {
538
+ id: task.delegation.id,
539
+ status: task.delegation.status,
540
+ agentRef: task.delegation.agentRef,
541
+ flowInstanceId: task.delegation.flowInstanceId,
542
+ branch: task.delegation.branch,
543
+ baseBranch: task.delegation.baseBranch,
544
+ result: task.delegation.result,
545
+ error: task.delegation.error
546
+ } : void 0
547
+ }));
548
+ }
549
+ }
550
+ }
551
+ }
552
+ for (const plan of plans.values()) {
553
+ if (!TERMINAL_PLAN_STATUSES.has(plan.status) && plan.tasks.length > 0 && plan.tasks.every((task) => TERMINAL_TASK_STATUSES.has(task.status))) {
554
+ plan.status = "completed";
555
+ plan.requiresApproval = false;
556
+ }
557
+ }
558
+ return [...plans.values()].sort((a, b) => {
559
+ const aTime = Date.parse(a.updatedAt || "");
560
+ const bTime = Date.parse(b.updatedAt || "");
561
+ return (Number.isNaN(bTime) ? 0 : bTime) - (Number.isNaN(aTime) ? 0 : aTime);
562
+ });
563
+ }
564
+
565
+ // src/utils/chat-events.ts
566
+ function isRecoveredDoneEvent(event) {
567
+ if (event.type !== "done") return false;
568
+ const done = event;
569
+ if (done.recovered !== true) return false;
570
+ return typeof done.content !== "string" || done.content.trim().length === 0;
571
+ }
572
+
573
+ // src/stores/conversation-store.ts
574
+ var ConversationStore = class {
575
+ constructor(options) {
576
+ this.subscribers = /* @__PURE__ */ new Set();
577
+ // Persistent SSE subscription
578
+ this.subscription = null;
579
+ this._messages = [];
580
+ this._isStreaming = false;
581
+ this._isWaiting = false;
582
+ this._streamingText = "";
583
+ this._error = null;
584
+ this._activeToolCalls = [];
585
+ this._isLoading = false;
586
+ this._conversationType = null;
587
+ this._streamingTimer = null;
588
+ this._watchdogTimer = null;
589
+ this.manager = options.database.conversations;
590
+ this._conversationPath = options.conversationPath ?? null;
591
+ this.createOptions = options.createOptions;
592
+ this.onEvent = options.onEvent;
593
+ this._streamingTimeoutMs = options.streamingTimeoutMs ?? 12e4;
594
+ this._watchdogIntervalMs = options.watchdogIntervalMs ?? 3e4;
595
+ if (this._conversationPath) {
596
+ this.ensureSubscription();
597
+ } else if (this.createOptions?.anonymous) {
598
+ void this.resumeAnonymous();
599
+ }
600
+ }
601
+ anonymousOptions() {
602
+ const a = this.createOptions?.anonymous;
603
+ if (!a) return null;
604
+ return a === true ? {} : a;
605
+ }
606
+ /**
607
+ * Resume the visitor conversation this tab already has (the session secret
608
+ * and conversation id live in sessionStorage), and load its history. A tab
609
+ * without one starts nothing until the first message.
610
+ */
611
+ async resumeAnonymous() {
612
+ const opts = this.anonymousOptions();
613
+ if (!opts || !this.createOptions || this._conversationPath) return;
614
+ const agent = this.createOptions.participant;
615
+ try {
616
+ if (!this.manager.hasStoredAnonymous(agent, opts)) return;
617
+ const convo = await this.manager.startAnonymous(agent, opts);
618
+ if (this._conversationPath) return;
619
+ this._conversationPath = convo.conversationPath;
620
+ this._conversationType = convo.type;
621
+ this.ensureSubscription();
622
+ await this.loadMessages();
623
+ } catch (err) {
624
+ logger.debug("[ConversationStore] No visitor conversation to resume", err);
625
+ }
626
+ }
627
+ // ==========================================================================
628
+ // Subscription
629
+ // ==========================================================================
630
+ /**
631
+ * Subscribe to state changes.
632
+ * Returns an unsubscribe function.
633
+ */
634
+ subscribe(callback) {
635
+ this.subscribers.add(callback);
636
+ callback(this.getSnapshot());
637
+ return () => {
638
+ this.subscribers.delete(callback);
639
+ };
640
+ }
641
+ /**
642
+ * Get the current state snapshot.
643
+ */
644
+ getSnapshot() {
645
+ return {
646
+ conversation: this._conversationPath ? { conversationPath: this._conversationPath, type: this._conversationType || "ai_chat" } : null,
647
+ messages: [...this._messages],
648
+ isStreaming: this._isStreaming,
649
+ isWaiting: this._isWaiting,
650
+ streamingText: this._streamingText,
651
+ error: this._error,
652
+ activeToolCalls: [...this._activeToolCalls],
653
+ plans: projectPlansFromMessages(this._messages),
654
+ isLoading: this._isLoading,
655
+ conversationPath: this._conversationPath
656
+ };
657
+ }
658
+ // ==========================================================================
659
+ // Actions
660
+ // ==========================================================================
661
+ /**
662
+ * Send a user message and stream the AI response.
663
+ *
664
+ * If no conversation exists yet and `createOptions` was provided,
665
+ * the conversation is created automatically on the first call.
666
+ */
667
+ async sendMessage(content) {
668
+ if (this._isStreaming) {
669
+ logger.warn("[ConversationStore] Cannot send message while streaming");
670
+ return;
671
+ }
672
+ logger.info("[ConversationStore] Sending message", { path: this._conversationPath });
673
+ this._error = null;
674
+ if (!this._conversationPath) {
675
+ if (!this.createOptions) {
676
+ throw new Error("No conversationPath and no createOptions provided");
677
+ }
678
+ try {
679
+ const anonymous = this.anonymousOptions();
680
+ const convo = anonymous ? await this.manager.startAnonymous(this.createOptions.participant, anonymous) : await this.manager.create({
681
+ participant: this.createOptions.participant,
682
+ input: this.createOptions.input
683
+ });
684
+ this._conversationPath = convo.conversationPath;
685
+ this._conversationType = convo.type;
686
+ for (const event of convo.initialEvents ?? []) {
687
+ this.onEvent?.(event);
688
+ this.handleEvent(event);
689
+ }
690
+ this.notify();
691
+ } catch (err) {
692
+ this._error = err instanceof Error ? err.message : String(err);
693
+ this.notify();
694
+ return;
695
+ }
696
+ }
697
+ const userMessage = {
698
+ role: "user",
699
+ content,
700
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
701
+ };
702
+ this._messages.push(userMessage);
703
+ this._isStreaming = true;
704
+ this._isWaiting = false;
705
+ this._streamingText = "";
706
+ this._activeToolCalls = [];
707
+ this.resetStreamingTimer();
708
+ this.startWatchdog();
709
+ this.notify();
710
+ try {
711
+ this.ensureSubscription();
712
+ await this.subscription.waitUntilConnected();
713
+ await this.manager.createUserMessage(this._conversationPath, content);
714
+ } catch (err) {
715
+ logger.error("[ConversationStore] Send failed", err);
716
+ this._error = err instanceof Error ? err.message : String(err);
717
+ this._isStreaming = false;
718
+ this.clearStreamingTimer();
719
+ this.stopWatchdog();
720
+ this.notify();
721
+ }
722
+ }
723
+ /**
724
+ * Load messages from the persisted node tree.
725
+ * Useful for loading history on page reload.
726
+ */
727
+ async loadMessages() {
728
+ if (!this._conversationPath) return [];
729
+ this._isLoading = true;
730
+ this.notify();
731
+ try {
732
+ const messages = await this.manager.getMessages(this._conversationPath);
733
+ logger.debug(`[ConversationStore] Loaded ${messages.length} messages`);
734
+ if (messages.length > 0) {
735
+ this._messages = messages;
736
+ this._activeToolCalls = [];
737
+ }
738
+ return messages;
739
+ } finally {
740
+ this._isLoading = false;
741
+ this.notify();
742
+ }
743
+ }
744
+ /**
745
+ * Approve a pending plan.
746
+ */
747
+ async approvePlan(planPath) {
748
+ await this.manager.approvePlan(planPath);
749
+ }
750
+ /**
751
+ * Reject a pending plan.
752
+ */
753
+ async rejectPlan(planPath, feedback) {
754
+ await this.manager.rejectPlan(planPath, feedback);
755
+ }
756
+ /**
757
+ * Mark a single message as read.
758
+ */
759
+ async markMessageAsRead(messagePath) {
760
+ await this.manager.markMessageAsRead(messagePath);
761
+ }
762
+ /**
763
+ * Stop the current streaming response.
764
+ */
765
+ stop() {
766
+ this._isStreaming = false;
767
+ this.clearStreamingTimer();
768
+ this.stopWatchdog();
769
+ this.notify();
770
+ }
771
+ /**
772
+ * Clean up the store and release resources.
773
+ */
774
+ destroy() {
775
+ this.stop();
776
+ this.clearStreamingTimer();
777
+ this.stopWatchdog();
778
+ this.subscription?.unsubscribe();
779
+ this.subscription = null;
780
+ this.subscribers.clear();
781
+ }
782
+ /**
783
+ * Get the conversation path.
784
+ */
785
+ getConversationPath() {
786
+ return this._conversationPath;
787
+ }
788
+ // ==========================================================================
789
+ // Internal
790
+ // ==========================================================================
791
+ ensureSubscription() {
792
+ if (this.subscription) return;
793
+ if (!this._conversationPath) return;
794
+ this.subscription = this.manager.subscribe(
795
+ this._conversationPath,
796
+ (event) => {
797
+ this.onEvent?.(event);
798
+ this.handleEvent(event);
799
+ }
800
+ );
801
+ }
802
+ handleEvent(event) {
803
+ logger.debug("[ConversationStore] SSE:", event.type);
804
+ if (this._isStreaming) {
805
+ this.resetStreamingTimer();
806
+ }
807
+ switch (event.type) {
808
+ case "text_chunk":
809
+ this.markStreamingActivity();
810
+ this._streamingText += event.text;
811
+ this.notify();
812
+ break;
813
+ case "thought_chunk":
814
+ this.markStreamingActivity();
815
+ this.notify();
816
+ break;
817
+ case "assistant_message":
818
+ this._messages.push(event.message);
819
+ this._streamingText = "";
820
+ this.notify();
821
+ break;
822
+ case "tool_call_started": {
823
+ this.markStreamingActivity();
824
+ logger.debug("[ConversationStore] Tool started:", event.functionName);
825
+ const info = {
826
+ id: event.toolCallId,
827
+ functionName: event.functionName,
828
+ arguments: event.arguments,
829
+ status: "running"
830
+ };
831
+ this._activeToolCalls.push(info);
832
+ this.notify();
833
+ break;
834
+ }
835
+ case "tool_call_completed": {
836
+ this.markStreamingActivity();
837
+ logger.debug("[ConversationStore] Tool completed:", event.toolCallId);
838
+ const idx = this._activeToolCalls.findIndex((tc) => tc.id === event.toolCallId);
839
+ if (idx >= 0) {
840
+ this._activeToolCalls[idx] = {
841
+ ...this._activeToolCalls[idx],
842
+ status: event.error ? "failed" : "completed",
843
+ result: event.result,
844
+ durationMs: event.durationMs
845
+ };
846
+ }
847
+ this.notify();
848
+ break;
849
+ }
850
+ case "waiting": {
851
+ logger.info("[ConversationStore] Waiting for input");
852
+ this.flushStreamingText(event.timestamp);
853
+ this._isStreaming = false;
854
+ this._isWaiting = true;
855
+ this._activeToolCalls = [];
856
+ this.clearStreamingTimer();
857
+ this.stopWatchdog();
858
+ this.notify();
859
+ this.deferredReload();
860
+ break;
861
+ }
862
+ case "done": {
863
+ if (isRecoveredDoneEvent(event)) {
864
+ logger.debug("[ConversationStore] Ignoring recovered done event");
865
+ this.reconcileRecoveredDone();
866
+ break;
867
+ }
868
+ const doneEvent = event;
869
+ if (doneEvent.dispatchPhase && doneEvent.dispatchPhase !== "terminal") {
870
+ logger.debug("[ConversationStore] Ignoring non-terminal done event");
871
+ break;
872
+ }
873
+ logger.info("[ConversationStore] Turn done");
874
+ this.flushStreamingText(event.timestamp);
875
+ if (doneEvent.content && !this.lastMessageIsAssistant()) {
876
+ this._messages.push({
877
+ role: doneEvent.role || "assistant",
878
+ content: doneEvent.content,
879
+ timestamp: event.timestamp,
880
+ senderDisplayName: doneEvent.senderDisplayName,
881
+ finishReason: doneEvent.finishReason
882
+ });
883
+ }
884
+ this._isStreaming = false;
885
+ this._isWaiting = true;
886
+ this._activeToolCalls = [];
887
+ this.clearStreamingTimer();
888
+ this.stopWatchdog();
889
+ this.notify();
890
+ this.deferredReload();
891
+ break;
892
+ }
893
+ case "completed": {
894
+ this.flushStreamingText(event.timestamp);
895
+ if (event.messages && event.messages.length > 0) {
896
+ this._messages = event.messages;
897
+ }
898
+ this._isStreaming = false;
899
+ this._isWaiting = false;
900
+ this._activeToolCalls = [];
901
+ this.clearStreamingTimer();
902
+ this.stopWatchdog();
903
+ this.notify();
904
+ this.deferredReload();
905
+ break;
906
+ }
907
+ case "failed":
908
+ logger.error("[ConversationStore] Turn failed", event.error);
909
+ this._error = event.error;
910
+ this._isStreaming = false;
911
+ this._isWaiting = false;
912
+ this._streamingText = "";
913
+ this._activeToolCalls = [];
914
+ this.clearStreamingTimer();
915
+ this.stopWatchdog();
916
+ this.notify();
917
+ break;
918
+ case "conversation_created":
919
+ this._conversationPath = event.conversationPath;
920
+ this.notify();
921
+ break;
922
+ case "message_saved":
923
+ this.silentReload().catch(() => {
924
+ });
925
+ this.notify();
926
+ break;
927
+ case "message_delivered":
928
+ this._messages.push(event.message);
929
+ this.notify();
930
+ break;
931
+ case "log": {
932
+ const logEvent = event;
933
+ const currentLevel = getLogLevel();
934
+ const levelMap = {
935
+ debug: 4 /* Debug */,
936
+ info: 3 /* Info */,
937
+ warn: 2 /* Warn */,
938
+ error: 1 /* Error */
939
+ };
940
+ const eventLevel = levelMap[logEvent.level] ?? 4 /* Debug */;
941
+ if (currentLevel >= eventLevel) {
942
+ const fn = logEvent.level === "error" ? console.error : logEvent.level === "warn" ? console.warn : logEvent.level === "debug" ? console.debug : console.info;
943
+ fn("[server]", logEvent.message);
944
+ }
945
+ break;
946
+ }
947
+ }
948
+ }
949
+ /**
950
+ * Agent activity observed while the store thought the turn was over.
951
+ *
952
+ * This happens when execution continues WITHOUT a user message — plan
953
+ * approval in approve_then_auto / step_by_step (approvePlan() returns an
954
+ * enqueue receipt and the agent starts executing), or agent-initiated
955
+ * follow-ups. The events arrive over the persistent live subscription
956
+ * (no history replay), so activity is always "now": re-enter streaming
957
+ * state so UIs show progress and the end-of-turn handling (done/waiting
958
+ * → deferred reload) runs again for this continuation.
959
+ */
960
+ markStreamingActivity() {
961
+ if (this._isStreaming) return;
962
+ logger.debug("[ConversationStore] Agent activity while idle \u2014 re-entering streaming state");
963
+ this._isStreaming = true;
964
+ this._isWaiting = false;
965
+ this._streamingText = "";
966
+ this.resetStreamingTimer();
967
+ this.startWatchdog();
968
+ }
969
+ /**
970
+ * If accumulated streaming text hasn't been captured in an assistant_message,
971
+ * synthesize one.
972
+ */
973
+ flushStreamingText(timestamp) {
974
+ if (this._streamingText && !this.lastMessageIsAssistant()) {
975
+ this._messages.push({
976
+ role: "assistant",
977
+ content: this._streamingText,
978
+ timestamp
979
+ });
980
+ }
981
+ this._streamingText = "";
982
+ }
983
+ lastMessageIsAssistant() {
984
+ const last = this._messages[this._messages.length - 1];
985
+ return last?.role === "assistant";
986
+ }
987
+ /**
988
+ * After a turn completes, reload canonical messages from the node tree
989
+ * to catch any events missed during SSE gaps.
990
+ */
991
+ deferredReload() {
992
+ if (!this._conversationPath) return;
993
+ logger.debug("[ConversationStore] Scheduling deferred reload");
994
+ const attemptReload = (delay, retriesLeft) => {
995
+ setTimeout(async () => {
996
+ try {
997
+ const prevCount = this._messages.length;
998
+ await this.silentReload();
999
+ if (this._messages.length === prevCount && retriesLeft > 0) {
1000
+ attemptReload(delay * 2, retriesLeft - 1);
1001
+ }
1002
+ } catch {
1003
+ }
1004
+ }, delay);
1005
+ };
1006
+ attemptReload(500, 2);
1007
+ }
1008
+ async silentReload() {
1009
+ if (!this._conversationPath) return;
1010
+ try {
1011
+ const fresh = await this.manager.getMessages(this._conversationPath);
1012
+ if (fresh.length === 0) return;
1013
+ let matchCount = 0;
1014
+ const limit = Math.min(this._messages.length, fresh.length);
1015
+ for (let i = 0; i < limit; i++) {
1016
+ if (this._messages[i].role !== fresh[i].role || this._messages[i].content !== fresh[i].content) break;
1017
+ matchCount++;
1018
+ }
1019
+ if (matchCount === this._messages.length && fresh.length === matchCount) {
1020
+ for (let i = 0; i < matchCount; i++) {
1021
+ const f = fresh[i];
1022
+ const e = this._messages[i];
1023
+ if (f.id) e.id = f.id;
1024
+ if (f.path) e.path = f.path;
1025
+ if (f.messageType) e.messageType = f.messageType;
1026
+ if (f.data) e.data = f.data;
1027
+ if (f.children) e.children = f.children;
1028
+ if (f.finishReason) e.finishReason = f.finishReason;
1029
+ }
1030
+ await this.rebuildToolCallState();
1031
+ return;
1032
+ }
1033
+ if (matchCount === this._messages.length && fresh.length > matchCount) {
1034
+ for (let i = matchCount; i < fresh.length; i++) {
1035
+ this._messages.push(fresh[i]);
1036
+ }
1037
+ for (let i = 0; i < matchCount; i++) {
1038
+ const f = fresh[i];
1039
+ const e = this._messages[i];
1040
+ if (f.id) e.id = f.id;
1041
+ if (f.path) e.path = f.path;
1042
+ if (f.messageType) e.messageType = f.messageType;
1043
+ if (f.data) e.data = f.data;
1044
+ if (f.children) e.children = f.children;
1045
+ }
1046
+ await this.rebuildToolCallState();
1047
+ this.notify();
1048
+ return;
1049
+ }
1050
+ if (matchCount === fresh.length && fresh.length < this._messages.length) {
1051
+ for (let i = 0; i < matchCount; i++) {
1052
+ const f = fresh[i];
1053
+ const e = this._messages[i];
1054
+ if (f.id) e.id = f.id;
1055
+ if (f.path) e.path = f.path;
1056
+ if (f.messageType) e.messageType = f.messageType;
1057
+ if (f.data) e.data = f.data;
1058
+ if (f.children) e.children = f.children;
1059
+ }
1060
+ await this.rebuildToolCallState();
1061
+ return;
1062
+ }
1063
+ this._messages = fresh;
1064
+ await this.rebuildToolCallState();
1065
+ this.notify();
1066
+ } catch (err) {
1067
+ logger.debug("[ConversationStore] Silent reload failed:", err);
1068
+ }
1069
+ }
1070
+ // ==========================================================================
1071
+ // Streaming timeout (Fix A) — auto-recover on SSE inactivity
1072
+ // ==========================================================================
1073
+ resetStreamingTimer() {
1074
+ this.clearStreamingTimer();
1075
+ if (!this._isStreaming) return;
1076
+ this._streamingTimer = setTimeout(() => {
1077
+ if (this._isStreaming) {
1078
+ logger.warn("[ConversationStore] Streaming timeout \u2014 no SSE activity for", this._streamingTimeoutMs, "ms");
1079
+ this.recoverFromHang();
1080
+ }
1081
+ }, this._streamingTimeoutMs);
1082
+ }
1083
+ clearStreamingTimer() {
1084
+ if (this._streamingTimer) {
1085
+ clearTimeout(this._streamingTimer);
1086
+ this._streamingTimer = null;
1087
+ }
1088
+ }
1089
+ // ==========================================================================
1090
+ // Activity watchdog (Fix B) — periodic backend health check
1091
+ // ==========================================================================
1092
+ startWatchdog() {
1093
+ this.stopWatchdog();
1094
+ if (!this._conversationPath) return;
1095
+ this._watchdogTimer = setInterval(async () => {
1096
+ if (!this._isStreaming || !this._conversationPath) {
1097
+ this.stopWatchdog();
1098
+ return;
1099
+ }
1100
+ try {
1101
+ const health = await this.manager.checkTurnHealth(this._conversationPath);
1102
+ if (health === "done" && this._isStreaming) {
1103
+ logger.warn("[ConversationStore] Watchdog detected completed turn while still streaming \u2014 recovering");
1104
+ this.recoverFromHang();
1105
+ }
1106
+ } catch {
1107
+ }
1108
+ }, this._watchdogIntervalMs);
1109
+ }
1110
+ stopWatchdog() {
1111
+ if (this._watchdogTimer) {
1112
+ clearInterval(this._watchdogTimer);
1113
+ this._watchdogTimer = null;
1114
+ }
1115
+ }
1116
+ // ==========================================================================
1117
+ // Hang recovery (shared by timeout + watchdog)
1118
+ // ==========================================================================
1119
+ recoverFromHang() {
1120
+ logger.info("[ConversationStore] Recovering from hang");
1121
+ this.clearStreamingTimer();
1122
+ this.stopWatchdog();
1123
+ this._isStreaming = false;
1124
+ this._isWaiting = true;
1125
+ this._streamingText = "";
1126
+ this.notify();
1127
+ this.silentReload().catch(() => {
1128
+ });
1129
+ }
1130
+ /**
1131
+ * Reconcile a recovered done event against persisted state.
1132
+ * If the backend turn is actually complete, finalize the local state.
1133
+ * Otherwise keep streaming active and let normal events continue.
1134
+ */
1135
+ reconcileRecoveredDone() {
1136
+ if (!this._conversationPath) return;
1137
+ void this.manager.checkTurnHealth(this._conversationPath).then((health) => {
1138
+ if (health !== "done") return;
1139
+ if (!this._isStreaming) return;
1140
+ this._isStreaming = false;
1141
+ this._isWaiting = true;
1142
+ this._activeToolCalls = [];
1143
+ this.clearStreamingTimer();
1144
+ this.stopWatchdog();
1145
+ this.notify();
1146
+ this.deferredReload();
1147
+ }).catch(() => {
1148
+ });
1149
+ }
1150
+ // ==========================================================================
1151
+ // Tool call state rebuild (Fix C) — reconstruct from node tree on reload
1152
+ // ==========================================================================
1153
+ async rebuildToolCallState() {
1154
+ if (!this._conversationPath) {
1155
+ this._activeToolCalls = [];
1156
+ return;
1157
+ }
1158
+ try {
1159
+ const pendingCalls = await this.manager.getActiveToolCalls(this._conversationPath);
1160
+ if (pendingCalls.length > 0) {
1161
+ this._activeToolCalls = pendingCalls.map((tc) => ({
1162
+ id: tc.id,
1163
+ functionName: tc.name,
1164
+ arguments: void 0,
1165
+ status: "running"
1166
+ }));
1167
+ if (!this._isStreaming) {
1168
+ this._isStreaming = true;
1169
+ this.resetStreamingTimer();
1170
+ this.startWatchdog();
1171
+ }
1172
+ } else {
1173
+ this._activeToolCalls = [];
1174
+ if (this._isStreaming) {
1175
+ const lastMsg = this._messages[this._messages.length - 1];
1176
+ const isLastAssistantDone = lastMsg?.role === "assistant" && (lastMsg.dispatchPhase ? lastMsg.dispatchPhase === "terminal" : !!lastMsg.finishReason && lastMsg.finishReason !== "tool_calls");
1177
+ if (isLastAssistantDone) {
1178
+ this._isStreaming = false;
1179
+ this._isWaiting = true;
1180
+ this.clearStreamingTimer();
1181
+ this.stopWatchdog();
1182
+ }
1183
+ }
1184
+ }
1185
+ } catch {
1186
+ this._activeToolCalls = [];
1187
+ }
1188
+ }
1189
+ notify() {
1190
+ const snapshot = this.getSnapshot();
1191
+ for (const sub of this.subscribers) {
1192
+ try {
1193
+ sub(snapshot);
1194
+ } catch (err) {
1195
+ logger.error("Error in conversation store subscriber:", err);
1196
+ }
1197
+ }
1198
+ }
1199
+ };
1200
+
1201
+ // src/stores/conversation-list-store.ts
1202
+ function buildChatsSubscriptionFilters(userHome) {
1203
+ const home = normalizeHomePath(userHome) ?? userHome;
1204
+ return {
1205
+ workspace: "raisin:access_control",
1206
+ path: `${home}/inbox/chats/**`,
1207
+ event_types: ["node:created", "node:updated"]
1208
+ };
1209
+ }
1210
+ var ConversationListStore = class {
1211
+ constructor(options) {
1212
+ this.subscribers = /* @__PURE__ */ new Set();
1213
+ // Internal state
1214
+ this._conversations = [];
1215
+ this._isLoading = false;
1216
+ this._error = null;
1217
+ // Cached ConversationStore instances
1218
+ this._stores = /* @__PURE__ */ new Map();
1219
+ // Real-time subscription
1220
+ this._realtimeSubscription = null;
1221
+ this.database = options.database;
1222
+ this.type = options.type;
1223
+ this.realtime = options.realtime ?? false;
1224
+ }
1225
+ // ==========================================================================
1226
+ // Subscription
1227
+ // ==========================================================================
1228
+ subscribe(callback) {
1229
+ this.subscribers.add(callback);
1230
+ callback(this.getSnapshot());
1231
+ return () => {
1232
+ this.subscribers.delete(callback);
1233
+ };
1234
+ }
1235
+ getSnapshot() {
1236
+ return {
1237
+ conversations: [...this._conversations],
1238
+ totalUnreadCount: this._conversations.reduce((sum, c) => sum + (c.unreadCount ?? 0), 0),
1239
+ isLoading: this._isLoading,
1240
+ error: this._error
1241
+ };
1242
+ }
1243
+ // ==========================================================================
1244
+ // Actions
1245
+ // ==========================================================================
1246
+ /**
1247
+ * Load conversations from the server.
1248
+ */
1249
+ async load() {
1250
+ this._isLoading = true;
1251
+ this._error = null;
1252
+ this.notify();
1253
+ try {
1254
+ const conversations = await this.database.conversations.list({
1255
+ type: this.type
1256
+ });
1257
+ logger.debug(`[ConversationListStore] Loaded ${conversations.length} conversations`);
1258
+ this._conversations = conversations;
1259
+ if (this.realtime) {
1260
+ this.startRealtimeSubscription();
1261
+ }
1262
+ } catch (err) {
1263
+ logger.error("[ConversationListStore] Failed to load conversations", err);
1264
+ this._error = err instanceof Error ? err.message : String(err);
1265
+ } finally {
1266
+ this._isLoading = false;
1267
+ this.notify();
1268
+ }
1269
+ }
1270
+ /**
1271
+ * Create a new conversation.
1272
+ */
1273
+ async createConversation(options) {
1274
+ const convo = await this.database.conversations.create(options);
1275
+ const listItem = {
1276
+ id: convo.id,
1277
+ type: convo.type,
1278
+ conversationPath: convo.conversationPath,
1279
+ conversationWorkspace: convo.conversationWorkspace,
1280
+ agentRef: convo.agentRef,
1281
+ participants: convo.participants,
1282
+ unreadCount: 0,
1283
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1284
+ };
1285
+ this._conversations = [listItem, ...this._conversations];
1286
+ this.notify();
1287
+ return listItem;
1288
+ }
1289
+ /**
1290
+ * Delete a conversation.
1291
+ */
1292
+ async deleteConversation(conversationPath) {
1293
+ const store = this._stores.get(conversationPath);
1294
+ if (store) {
1295
+ store.destroy();
1296
+ this._stores.delete(conversationPath);
1297
+ }
1298
+ await this.database.conversations.delete(conversationPath);
1299
+ this._conversations = this._conversations.filter((c) => c.conversationPath !== conversationPath);
1300
+ this.notify();
1301
+ }
1302
+ /**
1303
+ * Mark a conversation as read.
1304
+ */
1305
+ async markAsRead(conversationPath) {
1306
+ await this.database.conversations.markAsRead(conversationPath);
1307
+ this._conversations = this._conversations.map(
1308
+ (c) => c.conversationPath === conversationPath ? { ...c, unreadCount: 0 } : c
1309
+ );
1310
+ this.notify();
1311
+ }
1312
+ /**
1313
+ * Get or create a ConversationStore for a specific conversation.
1314
+ * Caches instances so repeated calls return the same store.
1315
+ */
1316
+ getConversationStore(conversationPath, overrides) {
1317
+ let store = this._stores.get(conversationPath);
1318
+ if (!store) {
1319
+ store = new ConversationStore({
1320
+ database: this.database,
1321
+ conversationPath,
1322
+ ...overrides
1323
+ });
1324
+ this._stores.set(conversationPath, store);
1325
+ }
1326
+ return store;
1327
+ }
1328
+ /**
1329
+ * Clean up all resources.
1330
+ */
1331
+ destroy() {
1332
+ this._realtimeSubscription?.unsubscribe();
1333
+ this._realtimeSubscription = null;
1334
+ for (const store of this._stores.values()) {
1335
+ store.destroy();
1336
+ }
1337
+ this._stores.clear();
1338
+ this.subscribers.clear();
1339
+ }
1340
+ // ==========================================================================
1341
+ // Internal
1342
+ // ==========================================================================
1343
+ async startRealtimeSubscription() {
1344
+ if (this._realtimeSubscription) return;
1345
+ logger.debug("[ConversationListStore] Starting realtime subscription");
1346
+ try {
1347
+ const workspace = this.database.workspace("raisin:access_control");
1348
+ const events = workspace.events();
1349
+ const result = await this.database.executeSql(
1350
+ `SELECT RAISIN_CURRENT_USER()->>'path'::String as home`
1351
+ );
1352
+ const rows = result.rows;
1353
+ const userHome = rows?.[0]?.home;
1354
+ if (!userHome) return;
1355
+ this._realtimeSubscription = await events.subscribe(
1356
+ buildChatsSubscriptionFilters(userHome),
1357
+ async (event) => {
1358
+ const eventData = event;
1359
+ const nodeId = eventData.payload?.node_id;
1360
+ if (!nodeId) return;
1361
+ try {
1362
+ const nodeResult = await this.database.executeSql(
1363
+ `SELECT id, path, node_type, properties, created_at, updated_at FROM 'raisin:access_control' WHERE id = $1 LIMIT 1`,
1364
+ [nodeId]
1365
+ );
1366
+ const row = nodeResult.rows?.[0];
1367
+ if (!row || row.node_type !== "raisin:Conversation") return;
1368
+ const convPath = row.path;
1369
+ const props = row.properties ?? {};
1370
+ const item = {
1371
+ id: row.id,
1372
+ type: props.conversation_type ?? "ai_chat",
1373
+ conversationPath: convPath,
1374
+ conversationWorkspace: "raisin:access_control",
1375
+ agentRef: props.agent_ref,
1376
+ participants: props.participants,
1377
+ unreadCount: props.unread_count ?? 0,
1378
+ lastMessage: props.last_message,
1379
+ updatedAt: row.updated_at ?? row.created_at
1380
+ };
1381
+ const existingIdx = this._conversations.findIndex((c) => c.conversationPath === convPath);
1382
+ if (existingIdx >= 0) {
1383
+ this._conversations.splice(existingIdx, 1);
1384
+ }
1385
+ this._conversations = [item, ...this._conversations];
1386
+ this.notify();
1387
+ } catch (err) {
1388
+ logger.debug("Failed to process conversation event:", err);
1389
+ }
1390
+ }
1391
+ );
1392
+ } catch (err) {
1393
+ logger.debug("Failed to start realtime subscription:", err);
1394
+ }
1395
+ }
1396
+ notify() {
1397
+ const snapshot = this.getSnapshot();
1398
+ for (const sub of this.subscribers) {
1399
+ try {
1400
+ sub(snapshot);
1401
+ } catch (err) {
1402
+ logger.error("Error in conversation list store subscriber:", err);
1403
+ }
1404
+ }
1405
+ }
1406
+ };
1407
+
1408
+ export {
1409
+ __privateGet,
1410
+ __privateAdd,
1411
+ __privateSet,
1412
+ ReconnectManager,
1413
+ LogLevel,
1414
+ configureLogger,
1415
+ getLoggerConfig,
1416
+ setLogLevel,
1417
+ getLogLevel,
1418
+ logger,
1419
+ ConnectionState,
1420
+ Connection,
1421
+ isRecoveredDoneEvent,
1422
+ normalizeHomePath,
1423
+ projectPlansFromMessages,
1424
+ ConversationStore,
1425
+ ConversationListStore
1426
+ };
1427
+ //# sourceMappingURL=chunk-NOPZAMIV.mjs.map