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