@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 */
@@ -2341,6 +2565,8 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2341
2565
  licenseKey: config.licenseKey,
2342
2566
  environment: config.environment,
2343
2567
  });
2568
+ // Initialize session storage (auto-loads existing session from browser sessionStorage)
2569
+ this.sessionStorage = new CyberneticSessionStorage(config.session);
2344
2570
  // Verify license asynchronously (non-blocking)
2345
2571
  this.licenseManager.verify().then(() => {
2346
2572
  // Check client feature after verification
@@ -2641,6 +2867,40 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2641
2867
  getOfflineStorage() {
2642
2868
  return this.offlineStorage;
2643
2869
  }
2870
+ // ==================== SESSION PERSISTENCE METHODS (ADR-028) ====================
2871
+ /**
2872
+ * Get stored messages from a previous session for UI restoration.
2873
+ * Call after construction to check for resumable conversations.
2874
+ */
2875
+ getStoredMessages() {
2876
+ return this.sessionStorage.getMessages();
2877
+ }
2878
+ /**
2879
+ * Check if a valid stored session exists that can be resumed.
2880
+ */
2881
+ hasStoredSession() {
2882
+ return this.sessionStorage.hasValidSession();
2883
+ }
2884
+ /**
2885
+ * Get session info for debugging/status display.
2886
+ */
2887
+ getSessionInfo() {
2888
+ return this.sessionStorage.getSessionInfo();
2889
+ }
2890
+ /**
2891
+ * Clear the stored session and start fresh.
2892
+ * Use for "New Conversation" actions.
2893
+ */
2894
+ clearSession() {
2895
+ this.sessionStorage.clear();
2896
+ }
2897
+ /**
2898
+ * Start a new session, clearing any stored data.
2899
+ * Optionally sets a new sessionId immediately.
2900
+ */
2901
+ startNewSession(sessionId) {
2902
+ this.sessionStorage.startNewSession(sessionId);
2903
+ }
2644
2904
  // ==================== CORE METHODS ====================
2645
2905
  /**
2646
2906
  * Send a message to the chatbot
@@ -2654,6 +2914,11 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2654
2914
  if (!message || typeof message !== 'string') {
2655
2915
  return this.createErrorResponse('Message is required', 'none');
2656
2916
  }
2917
+ // Auto-resolve sessionId: explicit option > stored session > none (ADR-028)
2918
+ const resolvedSessionId = options?.sessionId ?? this.sessionStorage.getSessionId() ?? undefined;
2919
+ const resolvedOptions = resolvedSessionId
2920
+ ? { ...options, sessionId: resolvedSessionId }
2921
+ : options;
2657
2922
  // Check maintenance mode before API call (ADR-200)
2658
2923
  const settings = await this.checkSystemStatus();
2659
2924
  if (settings.maintenanceMode || settings.forceOfflineClients) {
@@ -2674,10 +2939,16 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2674
2939
  }
2675
2940
  // Try API first
2676
2941
  try {
2677
- const response = await this.apiWithRetry(message, options);
2942
+ const response = await this.apiWithRetry(message, resolvedOptions);
2678
2943
  this.setStatus('online');
2679
2944
  // Process response through license manager (may add warning in production)
2680
2945
  const processedReply = this.licenseManager.processResponse(response.reply);
2946
+ // Persist session state (ADR-028)
2947
+ if (response.sessionId) {
2948
+ this.sessionStorage.saveSessionId(response.sessionId);
2949
+ }
2950
+ this.sessionStorage.addMessage({ role: 'user', content: message });
2951
+ this.sessionStorage.addMessage({ role: 'assistant', content: processedReply, confidence: 'high' });
2681
2952
  return {
2682
2953
  reply: processedReply,
2683
2954
  confidence: 'high',
@@ -2723,6 +2994,13 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2723
2994
  });
2724
2995
  return;
2725
2996
  }
2997
+ // Auto-resolve sessionId: explicit option > stored session > none (ADR-028)
2998
+ const resolvedSessionId = options?.sessionId ?? this.sessionStorage.getSessionId() ?? undefined;
2999
+ const resolvedOptions = resolvedSessionId
3000
+ ? { ...options, sessionId: resolvedSessionId }
3001
+ : options;
3002
+ // Save user message immediately (ADR-028)
3003
+ this.sessionStorage.addMessage({ role: 'user', content: message });
2726
3004
  // Check maintenance mode before API call (ADR-200)
2727
3005
  const settings = await this.checkSystemStatus();
2728
3006
  if (settings.maintenanceMode || settings.forceOfflineClients) {
@@ -2735,14 +3013,19 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2735
3013
  if (this.wsTransport && this.config.transport !== 'rest') {
2736
3014
  try {
2737
3015
  await this.wsTransport.chatStream(message, {
2738
- sessionId: options?.sessionId,
2739
- context: options?.context,
3016
+ sessionId: resolvedOptions?.sessionId,
3017
+ context: resolvedOptions?.context,
2740
3018
  onToken: callbacks.onToken,
2741
3019
  onSources: callbacks.onSources,
2742
3020
  onComplete: (response) => {
2743
3021
  this.setStatus('online');
2744
3022
  // Process through license manager
2745
3023
  const processedReply = this.licenseManager.processResponse(response.reply);
3024
+ // Persist session state (ADR-028)
3025
+ if (response.sessionId) {
3026
+ this.sessionStorage.saveSessionId(response.sessionId);
3027
+ }
3028
+ this.sessionStorage.addMessage({ role: 'assistant', content: processedReply, confidence: 'high' });
2746
3029
  callbacks.onComplete?.({
2747
3030
  ...response,
2748
3031
  reply: processedReply
@@ -2752,7 +3035,7 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2752
3035
  // In 'auto' mode, fall back to SSE on WS error
2753
3036
  if (this.config.transport === 'auto') {
2754
3037
  console.warn('[Cybernetic] WebSocket error, falling back to SSE:', error.message);
2755
- this.streamViaSSE(message, callbacks, options);
3038
+ this.streamViaSSE(message, callbacks, resolvedOptions);
2756
3039
  }
2757
3040
  else {
2758
3041
  // 'websocket' mode — no fallback
@@ -2778,7 +3061,7 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2778
3061
  }
2779
3062
  }
2780
3063
  // REST+SSE path (on-prem, or fallback from WebSocket)
2781
- await this.streamViaSSE(message, callbacks, options);
3064
+ await this.streamViaSSE(message, callbacks, resolvedOptions);
2782
3065
  }
2783
3066
  /**
2784
3067
  * Stream chat via REST+SSE (original transport).
@@ -2795,6 +3078,11 @@ LJ5AZXvOhHaXdHzMuYKX5BpK4w7TqbPvJ6QPvKmLKvHh1VKcUJ6mJQgJJw==
2795
3078
  this.setStatus('online');
2796
3079
  // Process response through license manager (may add warning in production)
2797
3080
  const processedReply = this.licenseManager.processResponse(data.fullText);
3081
+ // Persist session state (ADR-028)
3082
+ if (data.sessionId) {
3083
+ this.sessionStorage.saveSessionId(data.sessionId);
3084
+ }
3085
+ this.sessionStorage.addMessage({ role: 'assistant', content: processedReply, confidence: 'high' });
2798
3086
  callbacks.onComplete?.({
2799
3087
  reply: processedReply,
2800
3088
  confidence: 'high',