@astermind/cybernetic-chatbot-client 2.2.59 → 2.2.67

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.
@@ -88,6 +88,7 @@
88
88
  let fullText = '';
89
89
  let sources = [];
90
90
  let sessionId;
91
+ let messageId;
91
92
  try {
92
93
  while (true) {
93
94
  const { done, value } = await reader.read();
@@ -116,6 +117,7 @@
116
117
  else if (data.sessionId !== undefined) {
117
118
  // Done event
118
119
  sessionId = data.sessionId;
120
+ messageId = data.messageId;
119
121
  }
120
122
  }
121
123
  catch {
@@ -124,7 +126,7 @@
124
126
  }
125
127
  }
126
128
  }
127
- options.onComplete?.({ fullText, sessionId, sources });
129
+ options.onComplete?.({ fullText, sessionId, messageId, sources });
128
130
  }
129
131
  catch (error) {
130
132
  options.onError?.(error);
@@ -1377,6 +1379,230 @@
1377
1379
  }
1378
1380
  }
1379
1381
 
1382
+ // src/CyberneticSessionStorage.ts
1383
+ // Session storage manager for persisting chatbot session across page navigations
1384
+ const DEFAULT_CONFIG = {
1385
+ enabled: true,
1386
+ storageKey: 'astermind_session',
1387
+ sessionTtl: 30 * 60 * 1000, // 30 minutes
1388
+ persistMessages: true,
1389
+ maxMessages: 50
1390
+ };
1391
+ /**
1392
+ * Session storage manager for persisting chatbot sessions
1393
+ *
1394
+ * Uses sessionStorage to maintain conversation continuity across
1395
+ * page navigations within the same browser session.
1396
+ */
1397
+ class CyberneticSessionStorage {
1398
+ constructor(config) {
1399
+ this.cachedSession = null;
1400
+ this.config = { ...DEFAULT_CONFIG, ...config };
1401
+ // Load cached session on initialization
1402
+ if (this.config.enabled && this.isAvailable()) {
1403
+ this.cachedSession = this.loadFromStorage();
1404
+ }
1405
+ }
1406
+ /**
1407
+ * Check if sessionStorage is available
1408
+ */
1409
+ isAvailable() {
1410
+ if (typeof window === 'undefined')
1411
+ return false;
1412
+ try {
1413
+ const testKey = '__astermind_test__';
1414
+ window.sessionStorage.setItem(testKey, 'test');
1415
+ window.sessionStorage.removeItem(testKey);
1416
+ return true;
1417
+ }
1418
+ catch {
1419
+ return false;
1420
+ }
1421
+ }
1422
+ /**
1423
+ * Get the current session ID (if valid)
1424
+ */
1425
+ getSessionId() {
1426
+ if (!this.config.enabled)
1427
+ return null;
1428
+ const session = this.getSession();
1429
+ return session?.sessionId ?? null;
1430
+ }
1431
+ /**
1432
+ * Get the full stored session (if valid)
1433
+ */
1434
+ getSession() {
1435
+ if (!this.config.enabled)
1436
+ return null;
1437
+ // Use cached session if available
1438
+ if (this.cachedSession) {
1439
+ if (this.isSessionValid(this.cachedSession)) {
1440
+ return this.cachedSession;
1441
+ }
1442
+ // Session expired, clear it
1443
+ this.clear();
1444
+ return null;
1445
+ }
1446
+ // Try loading from storage
1447
+ const session = this.loadFromStorage();
1448
+ if (session && this.isSessionValid(session)) {
1449
+ this.cachedSession = session;
1450
+ return session;
1451
+ }
1452
+ return null;
1453
+ }
1454
+ /**
1455
+ * Save/update session ID
1456
+ */
1457
+ saveSessionId(sessionId) {
1458
+ if (!this.config.enabled || !sessionId)
1459
+ return;
1460
+ const existingSession = this.getSession();
1461
+ const session = {
1462
+ sessionId,
1463
+ updatedAt: Date.now(),
1464
+ messages: existingSession?.messages ?? []
1465
+ };
1466
+ this.saveToStorage(session);
1467
+ this.cachedSession = session;
1468
+ }
1469
+ /**
1470
+ * Add a message to the conversation history
1471
+ */
1472
+ addMessage(message) {
1473
+ if (!this.config.enabled || !this.config.persistMessages)
1474
+ return;
1475
+ const session = this.getSession();
1476
+ if (!session)
1477
+ return;
1478
+ const newMessage = {
1479
+ ...message,
1480
+ id: this.generateMessageId(),
1481
+ timestamp: Date.now()
1482
+ };
1483
+ // Add new message and trim if necessary
1484
+ session.messages = session.messages ?? [];
1485
+ session.messages.push(newMessage);
1486
+ // Keep only the most recent messages
1487
+ if (session.messages.length > this.config.maxMessages) {
1488
+ session.messages = session.messages.slice(-this.config.maxMessages);
1489
+ }
1490
+ session.updatedAt = Date.now();
1491
+ this.saveToStorage(session);
1492
+ this.cachedSession = session;
1493
+ }
1494
+ /**
1495
+ * Get conversation messages
1496
+ */
1497
+ getMessages() {
1498
+ if (!this.config.enabled || !this.config.persistMessages)
1499
+ return [];
1500
+ const session = this.getSession();
1501
+ return session?.messages ?? [];
1502
+ }
1503
+ /**
1504
+ * Clear the stored session
1505
+ */
1506
+ clear() {
1507
+ this.cachedSession = null;
1508
+ if (!this.isAvailable())
1509
+ return;
1510
+ try {
1511
+ window.sessionStorage.removeItem(this.config.storageKey);
1512
+ }
1513
+ catch (error) {
1514
+ console.warn('[CyberneticSessionStorage] Failed to clear session:', error);
1515
+ }
1516
+ }
1517
+ /**
1518
+ * Start a new session (clears existing and optionally sets new ID)
1519
+ */
1520
+ startNewSession(sessionId) {
1521
+ this.clear();
1522
+ if (sessionId) {
1523
+ this.saveSessionId(sessionId);
1524
+ }
1525
+ }
1526
+ /**
1527
+ * Check if we have a valid stored session
1528
+ */
1529
+ hasValidSession() {
1530
+ return this.getSession() !== null;
1531
+ }
1532
+ /**
1533
+ * Get session info for debugging/status
1534
+ */
1535
+ getSessionInfo() {
1536
+ const session = this.getSession();
1537
+ if (!session) {
1538
+ return {
1539
+ hasSession: false,
1540
+ sessionId: null,
1541
+ messageCount: 0,
1542
+ lastUpdated: null,
1543
+ ttlRemaining: null
1544
+ };
1545
+ }
1546
+ const ttlRemaining = Math.max(0, this.config.sessionTtl - (Date.now() - session.updatedAt));
1547
+ return {
1548
+ hasSession: true,
1549
+ sessionId: session.sessionId,
1550
+ messageCount: session.messages?.length ?? 0,
1551
+ lastUpdated: new Date(session.updatedAt),
1552
+ ttlRemaining
1553
+ };
1554
+ }
1555
+ // ==================== PRIVATE METHODS ====================
1556
+ /**
1557
+ * Check if a session is still valid (not expired)
1558
+ */
1559
+ isSessionValid(session) {
1560
+ const age = Date.now() - session.updatedAt;
1561
+ return age < this.config.sessionTtl;
1562
+ }
1563
+ /**
1564
+ * Load session from sessionStorage
1565
+ */
1566
+ loadFromStorage() {
1567
+ if (!this.isAvailable())
1568
+ return null;
1569
+ try {
1570
+ const stored = window.sessionStorage.getItem(this.config.storageKey);
1571
+ if (!stored)
1572
+ return null;
1573
+ const session = JSON.parse(stored);
1574
+ // Validate structure
1575
+ if (!session.sessionId || typeof session.updatedAt !== 'number') {
1576
+ return null;
1577
+ }
1578
+ return session;
1579
+ }
1580
+ catch (error) {
1581
+ console.warn('[CyberneticSessionStorage] Failed to load session:', error);
1582
+ return null;
1583
+ }
1584
+ }
1585
+ /**
1586
+ * Save session to sessionStorage
1587
+ */
1588
+ saveToStorage(session) {
1589
+ if (!this.isAvailable())
1590
+ return;
1591
+ try {
1592
+ window.sessionStorage.setItem(this.config.storageKey, JSON.stringify(session));
1593
+ }
1594
+ catch (error) {
1595
+ console.warn('[CyberneticSessionStorage] Failed to save session:', error);
1596
+ }
1597
+ }
1598
+ /**
1599
+ * Generate a unique message ID
1600
+ */
1601
+ generateMessageId() {
1602
+ return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1603
+ }
1604
+ }
1605
+
1380
1606
  // src/config.ts
1381
1607
  // Configuration loading and validation
1382
1608
  /** Default API URL when not specified */
@@ -2350,6 +2576,8 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2350
2576
  licenseKey: config.licenseKey,
2351
2577
  environment: config.environment,
2352
2578
  });
2579
+ // Initialize session storage (auto-loads existing session from browser sessionStorage)
2580
+ this.sessionStorage = new CyberneticSessionStorage(config.session);
2353
2581
  // Verify license asynchronously (non-blocking)
2354
2582
  this.licenseManager.verify().then(() => {
2355
2583
  // Check client feature after verification
@@ -2650,6 +2878,40 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2650
2878
  getOfflineStorage() {
2651
2879
  return this.offlineStorage;
2652
2880
  }
2881
+ // ==================== SESSION PERSISTENCE METHODS (ADR-028) ====================
2882
+ /**
2883
+ * Get stored messages from a previous session for UI restoration.
2884
+ * Call after construction to check for resumable conversations.
2885
+ */
2886
+ getStoredMessages() {
2887
+ return this.sessionStorage.getMessages();
2888
+ }
2889
+ /**
2890
+ * Check if a valid stored session exists that can be resumed.
2891
+ */
2892
+ hasStoredSession() {
2893
+ return this.sessionStorage.hasValidSession();
2894
+ }
2895
+ /**
2896
+ * Get session info for debugging/status display.
2897
+ */
2898
+ getSessionInfo() {
2899
+ return this.sessionStorage.getSessionInfo();
2900
+ }
2901
+ /**
2902
+ * Clear the stored session and start fresh.
2903
+ * Use for "New Conversation" actions.
2904
+ */
2905
+ clearSession() {
2906
+ this.sessionStorage.clear();
2907
+ }
2908
+ /**
2909
+ * Start a new session, clearing any stored data.
2910
+ * Optionally sets a new sessionId immediately.
2911
+ */
2912
+ startNewSession(sessionId) {
2913
+ this.sessionStorage.startNewSession(sessionId);
2914
+ }
2653
2915
  // ==================== CORE METHODS ====================
2654
2916
  /**
2655
2917
  * Send a message to the chatbot
@@ -2663,6 +2925,11 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2663
2925
  if (!message || typeof message !== 'string') {
2664
2926
  return this.createErrorResponse('Message is required', 'none');
2665
2927
  }
2928
+ // Auto-resolve sessionId: explicit option > stored session > none (ADR-028)
2929
+ const resolvedSessionId = options?.sessionId ?? this.sessionStorage.getSessionId() ?? undefined;
2930
+ const resolvedOptions = resolvedSessionId
2931
+ ? { ...options, sessionId: resolvedSessionId }
2932
+ : options;
2666
2933
  // Check maintenance mode before API call (ADR-200)
2667
2934
  const settings = await this.checkSystemStatus();
2668
2935
  if (settings.maintenanceMode || settings.forceOfflineClients) {
@@ -2683,10 +2950,16 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2683
2950
  }
2684
2951
  // Try API first
2685
2952
  try {
2686
- const response = await this.apiWithRetry(message, options);
2953
+ const response = await this.apiWithRetry(message, resolvedOptions);
2687
2954
  this.setStatus('online');
2688
2955
  // Process response through license manager (may add warning in production)
2689
2956
  const processedReply = this.licenseManager.processResponse(response.reply);
2957
+ // Persist session state (ADR-028)
2958
+ if (response.sessionId) {
2959
+ this.sessionStorage.saveSessionId(response.sessionId);
2960
+ }
2961
+ this.sessionStorage.addMessage({ role: 'user', content: message });
2962
+ this.sessionStorage.addMessage({ role: 'assistant', content: processedReply, confidence: 'high' });
2690
2963
  return {
2691
2964
  reply: processedReply,
2692
2965
  confidence: 'high',
@@ -2732,6 +3005,13 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2732
3005
  });
2733
3006
  return;
2734
3007
  }
3008
+ // Auto-resolve sessionId: explicit option > stored session > none (ADR-028)
3009
+ const resolvedSessionId = options?.sessionId ?? this.sessionStorage.getSessionId() ?? undefined;
3010
+ const resolvedOptions = resolvedSessionId
3011
+ ? { ...options, sessionId: resolvedSessionId }
3012
+ : options;
3013
+ // Save user message immediately (ADR-028)
3014
+ this.sessionStorage.addMessage({ role: 'user', content: message });
2735
3015
  // Check maintenance mode before API call (ADR-200)
2736
3016
  const settings = await this.checkSystemStatus();
2737
3017
  if (settings.maintenanceMode || settings.forceOfflineClients) {
@@ -2744,14 +3024,19 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2744
3024
  if (this.wsTransport && this.config.transport !== 'rest') {
2745
3025
  try {
2746
3026
  await this.wsTransport.chatStream(message, {
2747
- sessionId: options?.sessionId,
2748
- context: options?.context,
3027
+ sessionId: resolvedOptions?.sessionId,
3028
+ context: resolvedOptions?.context,
2749
3029
  onToken: callbacks.onToken,
2750
3030
  onSources: callbacks.onSources,
2751
3031
  onComplete: (response) => {
2752
3032
  this.setStatus('online');
2753
3033
  // Process through license manager
2754
3034
  const processedReply = this.licenseManager.processResponse(response.reply);
3035
+ // Persist session state (ADR-028)
3036
+ if (response.sessionId) {
3037
+ this.sessionStorage.saveSessionId(response.sessionId);
3038
+ }
3039
+ this.sessionStorage.addMessage({ role: 'assistant', content: processedReply, confidence: 'high' });
2755
3040
  callbacks.onComplete?.({
2756
3041
  ...response,
2757
3042
  reply: processedReply
@@ -2761,7 +3046,7 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2761
3046
  // In 'auto' mode, fall back to SSE on WS error
2762
3047
  if (this.config.transport === 'auto') {
2763
3048
  console.warn('[Cybernetic] WebSocket error, falling back to SSE:', error.message);
2764
- this.streamViaSSE(message, callbacks, options);
3049
+ this.streamViaSSE(message, callbacks, resolvedOptions);
2765
3050
  }
2766
3051
  else {
2767
3052
  // 'websocket' mode — no fallback
@@ -2787,7 +3072,7 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2787
3072
  }
2788
3073
  }
2789
3074
  // REST+SSE path (on-prem, or fallback from WebSocket)
2790
- await this.streamViaSSE(message, callbacks, options);
3075
+ await this.streamViaSSE(message, callbacks, resolvedOptions);
2791
3076
  }
2792
3077
  /**
2793
3078
  * Stream chat via REST+SSE (original transport).
@@ -2804,12 +3089,18 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2804
3089
  this.setStatus('online');
2805
3090
  // Process response through license manager (may add warning in production)
2806
3091
  const processedReply = this.licenseManager.processResponse(data.fullText);
3092
+ // Persist session state (ADR-028)
3093
+ if (data.sessionId) {
3094
+ this.sessionStorage.saveSessionId(data.sessionId);
3095
+ }
3096
+ this.sessionStorage.addMessage({ role: 'assistant', content: processedReply, confidence: 'high' });
2807
3097
  callbacks.onComplete?.({
2808
3098
  reply: processedReply,
2809
3099
  confidence: 'high',
2810
3100
  sources: data.sources || [],
2811
3101
  offline: false,
2812
- sessionId: data.sessionId
3102
+ sessionId: data.sessionId,
3103
+ messageId: data.messageId
2813
3104
  });
2814
3105
  },
2815
3106
  onError: (error) => {
@@ -7188,6 +7479,7 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
7188
7479
  exports.CyberneticIntentClassifier = CyberneticIntentClassifier;
7189
7480
  exports.CyberneticLocalRAG = CyberneticLocalRAG;
7190
7481
  exports.CyberneticOfflineStorage = CyberneticOfflineStorage;
7482
+ exports.CyberneticSessionStorage = CyberneticSessionStorage;
7191
7483
  exports.LicenseManager = LicenseManager;
7192
7484
  exports.OmegaOfflineRAG = OmegaOfflineRAG;
7193
7485
  exports.REQUIRED_FEATURES = REQUIRED_FEATURES;