@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.
@@ -86,6 +86,7 @@ class ApiClient {
86
86
  let fullText = '';
87
87
  let sources = [];
88
88
  let sessionId;
89
+ let messageId;
89
90
  try {
90
91
  while (true) {
91
92
  const { done, value } = await reader.read();
@@ -114,6 +115,7 @@ class ApiClient {
114
115
  else if (data.sessionId !== undefined) {
115
116
  // Done event
116
117
  sessionId = data.sessionId;
118
+ messageId = data.messageId;
117
119
  }
118
120
  }
119
121
  catch {
@@ -122,7 +124,7 @@ class ApiClient {
122
124
  }
123
125
  }
124
126
  }
125
- options.onComplete?.({ fullText, sessionId, sources });
127
+ options.onComplete?.({ fullText, sessionId, messageId, sources });
126
128
  }
127
129
  catch (error) {
128
130
  options.onError?.(error);
@@ -1375,6 +1377,230 @@ class WebSocketTransport {
1375
1377
  }
1376
1378
  }
1377
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
+
1378
1604
  // src/config.ts
1379
1605
  // Configuration loading and validation
1380
1606
  /** Default API URL when not specified */
@@ -2348,6 +2574,8 @@ class CyberneticClient {
2348
2574
  licenseKey: config.licenseKey,
2349
2575
  environment: config.environment,
2350
2576
  });
2577
+ // Initialize session storage (auto-loads existing session from browser sessionStorage)
2578
+ this.sessionStorage = new CyberneticSessionStorage(config.session);
2351
2579
  // Verify license asynchronously (non-blocking)
2352
2580
  this.licenseManager.verify().then(() => {
2353
2581
  // Check client feature after verification
@@ -2648,6 +2876,40 @@ class CyberneticClient {
2648
2876
  getOfflineStorage() {
2649
2877
  return this.offlineStorage;
2650
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
+ }
2651
2913
  // ==================== CORE METHODS ====================
2652
2914
  /**
2653
2915
  * Send a message to the chatbot
@@ -2661,6 +2923,11 @@ class CyberneticClient {
2661
2923
  if (!message || typeof message !== 'string') {
2662
2924
  return this.createErrorResponse('Message is required', 'none');
2663
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;
2664
2931
  // Check maintenance mode before API call (ADR-200)
2665
2932
  const settings = await this.checkSystemStatus();
2666
2933
  if (settings.maintenanceMode || settings.forceOfflineClients) {
@@ -2681,10 +2948,16 @@ class CyberneticClient {
2681
2948
  }
2682
2949
  // Try API first
2683
2950
  try {
2684
- const response = await this.apiWithRetry(message, options);
2951
+ const response = await this.apiWithRetry(message, resolvedOptions);
2685
2952
  this.setStatus('online');
2686
2953
  // Process response through license manager (may add warning in production)
2687
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' });
2688
2961
  return {
2689
2962
  reply: processedReply,
2690
2963
  confidence: 'high',
@@ -2730,6 +3003,13 @@ class CyberneticClient {
2730
3003
  });
2731
3004
  return;
2732
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 });
2733
3013
  // Check maintenance mode before API call (ADR-200)
2734
3014
  const settings = await this.checkSystemStatus();
2735
3015
  if (settings.maintenanceMode || settings.forceOfflineClients) {
@@ -2742,14 +3022,19 @@ class CyberneticClient {
2742
3022
  if (this.wsTransport && this.config.transport !== 'rest') {
2743
3023
  try {
2744
3024
  await this.wsTransport.chatStream(message, {
2745
- sessionId: options?.sessionId,
2746
- context: options?.context,
3025
+ sessionId: resolvedOptions?.sessionId,
3026
+ context: resolvedOptions?.context,
2747
3027
  onToken: callbacks.onToken,
2748
3028
  onSources: callbacks.onSources,
2749
3029
  onComplete: (response) => {
2750
3030
  this.setStatus('online');
2751
3031
  // Process through license manager
2752
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' });
2753
3038
  callbacks.onComplete?.({
2754
3039
  ...response,
2755
3040
  reply: processedReply
@@ -2759,7 +3044,7 @@ class CyberneticClient {
2759
3044
  // In 'auto' mode, fall back to SSE on WS error
2760
3045
  if (this.config.transport === 'auto') {
2761
3046
  console.warn('[Cybernetic] WebSocket error, falling back to SSE:', error.message);
2762
- this.streamViaSSE(message, callbacks, options);
3047
+ this.streamViaSSE(message, callbacks, resolvedOptions);
2763
3048
  }
2764
3049
  else {
2765
3050
  // 'websocket' mode — no fallback
@@ -2785,7 +3070,7 @@ class CyberneticClient {
2785
3070
  }
2786
3071
  }
2787
3072
  // REST+SSE path (on-prem, or fallback from WebSocket)
2788
- await this.streamViaSSE(message, callbacks, options);
3073
+ await this.streamViaSSE(message, callbacks, resolvedOptions);
2789
3074
  }
2790
3075
  /**
2791
3076
  * Stream chat via REST+SSE (original transport).
@@ -2802,12 +3087,18 @@ class CyberneticClient {
2802
3087
  this.setStatus('online');
2803
3088
  // Process response through license manager (may add warning in production)
2804
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' });
2805
3095
  callbacks.onComplete?.({
2806
3096
  reply: processedReply,
2807
3097
  confidence: 'high',
2808
3098
  sources: data.sources || [],
2809
3099
  offline: false,
2810
- sessionId: data.sessionId
3100
+ sessionId: data.sessionId,
3101
+ messageId: data.messageId
2811
3102
  });
2812
3103
  },
2813
3104
  onError: (error) => {
@@ -7179,5 +7470,5 @@ function createClient(config) {
7179
7470
  return new CyberneticClient(config);
7180
7471
  }
7181
7472
 
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 };
7473
+ 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
7474
  //# sourceMappingURL=cybernetic-chatbot-client.esm.js.map