@astermind/cybernetic-chatbot-client 2.2.59 → 2.2.62

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