@pingagent/sdk 0.1.14 → 0.1.15

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.
package/bin/pingagent.js CHANGED
@@ -25,12 +25,21 @@ import {
25
25
  TaskHandoffManager,
26
26
  TrustPolicyAuditManager,
27
27
  CollaborationEventManager,
28
+ CollaborationProjectionOutboxManager,
29
+ OperatorSeenStateManager,
28
30
  TrustRecommendationManager,
29
31
  getTrustRecommendationActionLabel,
30
32
  formatCapabilityCardSummary,
31
33
  defaultTrustPolicyDoc,
32
34
  normalizeTrustPolicyDoc,
33
35
  upsertTrustPolicyRecommendation,
36
+ summarizeSinceLastSeen,
37
+ buildDeliveryTimeline,
38
+ buildProjectionPreview,
39
+ listPendingDecisionViews,
40
+ deriveTransportHealth,
41
+ readTransportPreference,
42
+ switchTransportPreference,
34
43
  getActiveSessionFilePath,
35
44
  getSessionMapFilePath,
36
45
  getSessionBindingAlertsFilePath,
@@ -243,7 +252,16 @@ function formatSessionRow(session, selected) {
243
252
  const trust = session.trust_state || 'unknown';
244
253
  const unread = session.unread_count ?? 0;
245
254
  const who = truncateLine(session.remote_did || session.conversation_id || 'unknown', 40);
246
- return `${marker} ${who} [${trust}] unread=${unread}${reconnect}`;
255
+ const seen = session.since_last_seen || {};
256
+ const sinceCount = (seen.new_external_messages ?? 0)
257
+ + (seen.new_conclusions ?? 0)
258
+ + (seen.new_handoffs ?? 0)
259
+ + (seen.new_decisions ?? 0)
260
+ + (seen.new_failures ?? 0)
261
+ + (seen.new_repairs ?? 0)
262
+ + (seen.new_projection_failures ?? 0);
263
+ const sinceBadge = sinceCount > 0 ? ` new=${sinceCount}` : '';
264
+ return `${marker} ${who} [${trust}] unread=${unread}${sinceBadge}${reconnect}`;
247
265
  }
248
266
 
249
267
  function formatMessageRow(message) {
@@ -346,6 +364,21 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
346
364
  const alertByConversation = new Map(alerts.map((row) => [row.conversation_id, row]));
347
365
  const activeChatSession = readCurrentActiveSessionKey();
348
366
  const ingressRuntime = readIngressRuntimeStatus();
367
+ const transportPreference = readTransportPreference();
368
+ const transportHealth = deriveTransportHealth({
369
+ runtime_status: ingressRuntime
370
+ ? {
371
+ ...ingressRuntime,
372
+ preferred_transport_mode: transportPreference?.preferred_mode ?? ingressRuntime.preferred_transport_mode ?? 'bridge',
373
+ }
374
+ : {
375
+ receive_mode: 'webhook',
376
+ transport_mode: transportPreference?.preferred_mode ?? 'bridge',
377
+ preferred_transport_mode: transportPreference?.preferred_mode ?? 'bridge',
378
+ },
379
+ recent_events: collaborationEventManager.listRecent(30),
380
+ projection_outbox_failed: new CollaborationProjectionOutboxManager(store).listByStatus('failed', 20),
381
+ });
349
382
  const desiredSelectedSessionKey =
350
383
  (selectedSessionKey && sessions.some((session) => session.session_key === selectedSessionKey) ? selectedSessionKey : null) ??
351
384
  sessionManager.getActiveSession()?.session_key ??
@@ -376,9 +409,9 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
376
409
  ? collaborationEventManager.listBySession(selectedSession.session_key, 12)
377
410
  : [];
378
411
  const selectedPendingCollaborationEvents = selectedSession
379
- ? collaborationEventManager.listPendingBySession(selectedSession.session_key, 12)
412
+ ? listPendingDecisionViews(store, 100).filter((event) => event.session_key === selectedSession.session_key).slice(0, 12)
380
413
  : [];
381
- const pendingCollaborationEventsGlobal = collaborationEventManager.listPending(50);
414
+ const pendingCollaborationEventsGlobal = listPendingDecisionViews(store, 50);
382
415
  const selectedMessages = selectedSession?.conversation_id
383
416
  ? historyManager.listRecent(selectedSession.conversation_id, 12)
384
417
  : [];
@@ -402,18 +435,39 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
402
435
  : [];
403
436
  const unreadTotal = sessionsWithMeta.reduce((sum, session) => sum + (session.unread_count ?? 0), 0);
404
437
  const alertSessions = sessionsWithMeta.filter((session) => !!session.binding_alert).length;
438
+ const sinceLastSeenGlobal = summarizeSinceLastSeen(store, {
439
+ operator_id: 'tui',
440
+ scope_type: 'global',
441
+ });
442
+ const sessionsWithSeen = sessionsWithMeta.map((session) => ({
443
+ ...session,
444
+ since_last_seen: summarizeSinceLastSeen(store, {
445
+ operator_id: 'tui',
446
+ scope_type: 'session',
447
+ scope_key: session.session_key,
448
+ }),
449
+ }));
450
+ const selectedDeliveryTimeline = selectedSession
451
+ ? buildDeliveryTimeline(store, selectedSession.session_key, 30)
452
+ : [];
453
+ const selectedProjectionPreview = selectedSession
454
+ ? buildProjectionPreview(store, selectedSession.session_key, policy.doc.collaboration_projection?.preset || 'balanced', 5)
455
+ : null;
405
456
  return {
406
457
  identity,
407
458
  runtimeMode,
408
459
  activeChatSession,
409
460
  ingressRuntime,
461
+ transportPreference,
462
+ transportHealth,
410
463
  activeChatSessionFile: getActiveSessionFilePath(),
411
464
  sessionMapPath: getSessionMapFilePath(),
412
465
  sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
413
466
  policyPath: policy.path,
414
467
  policyDoc: policy.doc,
415
468
  projectionPreset: policy.doc.collaboration_projection?.preset || 'balanced',
416
- sessions: sessionsWithMeta,
469
+ sinceLastSeenGlobal,
470
+ sessions: sessionsWithSeen,
417
471
  tasks: taskManager.listRecent(30).map((task) => ({
418
472
  ...task,
419
473
  handoff: taskHandoffManager.get(task.task_id),
@@ -426,6 +480,8 @@ function buildHostState(identityPath, selectedSessionKey = null, historyPageInde
426
480
  selectedAuditEvents,
427
481
  selectedCollaborationEvents,
428
482
  selectedPendingCollaborationEvents,
483
+ selectedDeliveryTimeline,
484
+ selectedProjectionPreview,
429
485
  pendingCollaborationEventsGlobal,
430
486
  selectedMessages,
431
487
  selectedHistoryPage,
@@ -450,6 +506,8 @@ function renderHostTuiScreen(hostState, uiState) {
450
506
  const pendingCollaborationEvents = hostState.selectedPendingCollaborationEvents || [];
451
507
  const pendingCollaborationEventsGlobal = hostState.pendingCollaborationEventsGlobal || [];
452
508
  const messages = hostState.selectedMessages || [];
509
+ const deliveryTimeline = hostState.selectedDeliveryTimeline || [];
510
+ const projectionPreview = hostState.selectedProjectionPreview || null;
453
511
  const recommendations = hostState.selectedRecommendations || [];
454
512
  const openRecommendation = recommendations.find((item) => item.status === 'open') || null;
455
513
  const reopenRecommendation = recommendations.find((item) => item.status === 'dismissed' || item.status === 'superseded') || null;
@@ -469,14 +527,18 @@ function renderHostTuiScreen(hostState, uiState) {
469
527
  || hostState.ingressRuntime.receive_mode === 'polling_degraded'
470
528
  || !!hostState.ingressRuntime.hooks_last_error;
471
529
  const ingressLabel = degraded ? 'Degraded' : 'Ready';
530
+ const transportHealth = hostState.transportHealth || { state: 'Ready', transport_mode: 'bridge', preferred_transport_mode: 'bridge', retry_queue_length: 0, consecutive_failures: 0 };
531
+ const sinceLastSeenGlobal = hostState.sinceLastSeenGlobal || {};
472
532
  const lines = [
473
533
  'PingAgent Host TUI',
474
534
  `DID: ${hostState.identity.did}`,
475
535
  `status=${formatStatusLine(uiState?.statusLevel || 'info', uiState?.statusMessage || '(ready)')}${statusTs}${statusCountdown}`,
476
536
  `runtime_mode=${hostState.runtimeMode} receive_mode=${hostState.ingressRuntime?.receive_mode || 'webhook'} current_openclaw_chat=${hostState.activeChatSession || '(none)'}`,
477
537
  `ingress=${ingressLabel}${degraded ? ' action=[f] fix-now' : ''}`,
538
+ `transport=${transportHealth.transport_mode} preferred=${transportHealth.preferred_transport_mode} state=${transportHealth.state} retry_queue=${transportHealth.retry_queue_length} failures=${transportHealth.consecutive_failures}`,
478
539
  uiState?.publicLinkUrl ? `public_link=${uiState.publicLinkUrl}` : null,
479
540
  `sessions=${sessions.length} unread_total=${hostState.unreadTotal ?? 0} alert_sessions=${hostState.alertSessions ?? 0} view=${view} projection=${hostState.projectionPreset || 'balanced'}`,
541
+ `since_last_seen external=${sinceLastSeenGlobal.new_external_messages ?? 0} conclusions=${sinceLastSeenGlobal.new_conclusions ?? 0} decisions=${sinceLastSeenGlobal.new_decisions ?? 0} failures=${sinceLastSeenGlobal.new_failures ?? 0}`,
480
542
  `policy=${hostState.policyPath}`,
481
543
  `chat_link_map=${hostState.sessionMapPath}`,
482
544
  `chat_link_alerts=${hostState.sessionBindingAlertsPath}`,
@@ -505,12 +567,14 @@ function renderHostTuiScreen(hostState, uiState) {
505
567
  lines.push('- y: dump task detail to stdout (task-detail view, choose json/plain)');
506
568
  lines.push('- u: approve the selected collaboration decision');
507
569
  lines.push('- U: reject the selected collaboration decision');
570
+ lines.push('- b: switch preferred transport to bridge');
571
+ lines.push('- c: switch preferred transport to channel');
508
572
  lines.push('- f: repair OpenClaw hooks config');
509
573
  lines.push('- A: apply first open trust recommendation for selected session');
510
574
  lines.push('- D: dismiss current open recommendation');
511
575
  lines.push('- R: reopen dismissed/superseded recommendation');
512
- lines.push('- b: attach selected session to the current OpenClaw chat');
513
- lines.push('- c: detach the selected chat link');
576
+ lines.push('- B: attach selected session to the current OpenClaw chat');
577
+ lines.push('- C: detach the selected chat link');
514
578
  lines.push('- q: quit');
515
579
  } else if (view === 'history') {
516
580
  lines.push('Conversation History');
@@ -576,7 +640,7 @@ function renderHostTuiScreen(hostState, uiState) {
576
640
  lines.push(...(pendingCollaborationEventsGlobal.length
577
641
  ? pendingCollaborationEventsGlobal.map((event, idx) => {
578
642
  const marker = idx === selectedDecisionIndex ? '>' : ' ';
579
- return `${marker} ${event.id} [${event.severity}] ${truncateLine(event.summary || '(none)', 88)} session=${event.session_key || '(none)'}`;
643
+ return `${marker} ${event.id} [${event.severity}${event.overdue ? ':overdue' : ''}] ${truncateLine(event.summary || '(none)', 88)} session=${event.session_key || '(none)'}`;
580
644
  })
581
645
  : ['- none']));
582
646
  } else if (view === 'tasks') {
@@ -642,7 +706,12 @@ function renderHostTuiScreen(hostState, uiState) {
642
706
  if (pendingCollaborationEvents.length > 0) {
643
707
  lines.push(`pending_collaboration_decisions=${pendingCollaborationEvents.length}`);
644
708
  lines.push(`next_decision=${truncateLine(pendingCollaborationEvents[0].summary || '(none)', 100)}`);
709
+ if (pendingCollaborationEvents[0].overdue) {
710
+ lines.push(`next_decision_overdue=${Math.ceil((pendingCollaborationEvents[0].overdue_by_ms || 0) / 60000)}m`);
711
+ }
645
712
  }
713
+ const sessionSeen = selected.since_last_seen || {};
714
+ lines.push(`since_last_seen external=${sessionSeen.new_external_messages ?? 0} conclusions=${sessionSeen.new_conclusions ?? 0} decisions=${sessionSeen.new_decisions ?? 0} failures=${sessionSeen.new_failures ?? 0}`);
646
715
  const actionBar = [
647
716
  selected.trust_state === 'pending' ? '[a] approve' : null,
648
717
  '[A] apply-rec',
@@ -656,8 +725,8 @@ function renderHostTuiScreen(hostState, uiState) {
656
725
  pendingCollaborationEvents.length ? '[U] reject-decision' : null,
657
726
  '[o] history',
658
727
  '[t] tasks',
659
- '[b] attach-chat',
660
- '[c] detach-chat',
728
+ '[B] attach-chat',
729
+ '[C] detach-chat',
661
730
  ].filter(Boolean).join(' ');
662
731
  lines.push(`actions=${actionBar}`);
663
732
  lines.push('');
@@ -688,6 +757,20 @@ function renderHostTuiScreen(hostState, uiState) {
688
757
  lines.push(...(collaborationEvents.length
689
758
  ? collaborationEvents.map((event) => `- ${event.event_type} [${event.severity}${event.approval_required ? `:${event.approval_status}` : ''}] ${truncateLine(event.summary || '', 90)}`)
690
759
  : ['- none']));
760
+ if (projectionPreview) {
761
+ lines.push('');
762
+ lines.push(`Projection Preview [preset=${projectionPreview.preset}]`);
763
+ lines.push(...(projectionPreview.recent.length
764
+ ? projectionPreview.recent.map((entry) => `- ${entry.event_type} => ${entry.disposition} (${truncateLine(entry.reason, 70)})`)
765
+ : ['- none']));
766
+ }
767
+ if (view === 'detail') {
768
+ lines.push('');
769
+ lines.push('Delivery Timeline');
770
+ lines.push(...(deliveryTimeline.length
771
+ ? deliveryTimeline.slice(0, 10).map((entry) => `- ${formatTs(entry.ts_ms, false)} ${entry.kind} ${truncateLine(entry.summary || '', 72)}`)
772
+ : ['- none']));
773
+ }
691
774
  lines.push('');
692
775
  lines.push('Audit');
693
776
  lines.push(...(auditEvents.length
@@ -699,7 +782,7 @@ function renderHostTuiScreen(hostState, uiState) {
699
782
  }
700
783
 
701
784
  lines.push('');
702
- lines.push('Keys: ↑/↓ or j/k select Enter/l open Esc/h back g/G jump r refresh a approve u/U decide A apply-rec D dismiss-rec R reopen-rec d demo m read p reply o history s search t tasks x cancel-task y dump f fix-hooks b attach-chat c detach-chat ? help q quit');
785
+ lines.push('Keys: ↑/↓ or j/k select Enter/l open Esc/h back g/G jump r refresh a approve u/U decide A apply-rec D dismiss-rec R reopen-rec d demo m read p reply o history s search t tasks x cancel-task y dump f fix-hooks b transport->bridge c transport->channel B attach-chat C detach-chat ? help q quit');
703
786
  return lines.join('\n');
704
787
  }
705
788
 
@@ -720,6 +803,10 @@ async function runHostTui(identityPath, opts) {
720
803
  historySearchQuery: '',
721
804
  publicLinkUrl: '',
722
805
  };
806
+ const seenState = {
807
+ globalView: null,
808
+ sessionKey: null,
809
+ };
723
810
 
724
811
  const render = () => {
725
812
  if (uiState.statusExpiresAt && Date.now() >= uiState.statusExpiresAt) {
@@ -762,7 +849,49 @@ async function runHostTui(identityPath, opts) {
762
849
  return rendered.hostState;
763
850
  };
764
851
 
852
+ const markSeen = (scopeType, scopeKey = null) => {
853
+ const store = openStore(identityPath);
854
+ try {
855
+ return new OperatorSeenStateManager(store).markSeen({
856
+ operator_id: 'tui',
857
+ scope_type: scopeType,
858
+ scope_key: scopeType === 'session' ? (scopeKey || null) : null,
859
+ last_seen_ts: Date.now(),
860
+ });
861
+ } finally {
862
+ store.close();
863
+ }
864
+ };
865
+
866
+ const refreshSeenMarkers = (hostState, options = {}) => {
867
+ let changed = false;
868
+ const forceGlobal = !!options.forceGlobal;
869
+ const forceSession = !!options.forceSession;
870
+ if (uiState.view === 'overview' || uiState.view === 'decisions') {
871
+ if (forceGlobal || seenState.globalView !== uiState.view) {
872
+ markSeen('global');
873
+ seenState.globalView = uiState.view;
874
+ changed = true;
875
+ }
876
+ if (uiState.view === 'overview') seenState.sessionKey = null;
877
+ } else if (['detail', 'history', 'tasks', 'task-detail'].includes(uiState.view)) {
878
+ const sessionKey = hostState.selectedSession?.session_key || null;
879
+ if (sessionKey && (forceSession || seenState.sessionKey !== sessionKey)) {
880
+ markSeen('session', sessionKey);
881
+ seenState.sessionKey = sessionKey;
882
+ changed = true;
883
+ }
884
+ }
885
+ return changed;
886
+ };
887
+
888
+ const rerenderAfterSeenUpdate = (hostState, options = {}) => {
889
+ if (!refreshSeenMarkers(hostState, options)) return hostState;
890
+ return redraw();
891
+ };
892
+
765
893
  let latestState = redraw();
894
+ latestState = rerenderAfterSeenUpdate(latestState, { forceGlobal: true });
766
895
  readline.emitKeypressEvents(process.stdin);
767
896
  if (process.stdin.setRawMode) process.stdin.setRawMode(true);
768
897
 
@@ -779,6 +908,50 @@ async function runHostTui(identityPath, opts) {
779
908
  uiState.statusAt = Date.now();
780
909
  };
781
910
 
911
+ const switchTransport = async (mode) => {
912
+ const label = mode === 'channel' ? 'channel' : 'bridge';
913
+ const confirmed = await confirmAction(`Switch preferred transport to ${label} and request a managed restart?`);
914
+ if (!confirmed) {
915
+ setStatus(`Transport switch to ${label} cancelled.`, 'warn');
916
+ latestState = redraw();
917
+ return;
918
+ }
919
+ try {
920
+ const result = switchTransportPreference(mode, {
921
+ updated_by: 'tui',
922
+ });
923
+ const store = openStore(identityPath);
924
+ try {
925
+ new CollaborationEventManager(store).record({
926
+ event_type: 'transport_switched',
927
+ severity: result.restarted ? 'notice' : 'warning',
928
+ summary: result.restarted
929
+ ? `Preferred transport switched to ${label}. A managed restart was attempted.`
930
+ : `Preferred transport switched to ${label}. Restart is still required.`,
931
+ detail: {
932
+ preferred_mode: result.preferred_mode,
933
+ restart_required: result.restart_required,
934
+ restart_method: result.restart_method ?? null,
935
+ restart_error: result.restart_error ?? null,
936
+ preference_path: result.preference_path,
937
+ },
938
+ });
939
+ } finally {
940
+ store.close();
941
+ }
942
+ setStatus(
943
+ result.restarted
944
+ ? `Preferred transport is now ${label}; managed restart attempted via ${result.restart_method || 'unknown method'}.`
945
+ : `Preferred transport saved as ${label}; restart still required.`,
946
+ result.restarted ? 'ok' : 'warn',
947
+ result.restarted ? 7000 : 9000,
948
+ );
949
+ } catch (error) {
950
+ setStatus(`Transport switch failed: ${error?.message || 'unknown error'}`, 'err', 9000);
951
+ }
952
+ latestState = redraw();
953
+ };
954
+
782
955
  const applySessionRecommendation = (selected) => {
783
956
  if (!selected?.remote_did) return { ok: false, message: 'No remote DID for selected session.' };
784
957
  const store = openStore(identityPath);
@@ -985,6 +1158,10 @@ async function runHostTui(identityPath, opts) {
985
1158
  uiState.historySearchQuery = '';
986
1159
  }
987
1160
  latestState = redraw();
1161
+ latestState = rerenderAfterSeenUpdate(latestState, {
1162
+ forceGlobal: uiState.view === 'overview' || uiState.view === 'decisions',
1163
+ forceSession: ['detail', 'history', 'tasks', 'task-detail'].includes(uiState.view),
1164
+ });
988
1165
  };
989
1166
 
990
1167
  const jumpSelection = (target) => {
@@ -1007,6 +1184,10 @@ async function runHostTui(identityPath, opts) {
1007
1184
  uiState.historySearchQuery = '';
1008
1185
  }
1009
1186
  latestState = redraw();
1187
+ latestState = rerenderAfterSeenUpdate(latestState, {
1188
+ forceGlobal: uiState.view === 'overview' || uiState.view === 'decisions',
1189
+ forceSession: ['detail', 'history', 'tasks', 'task-detail'].includes(uiState.view),
1190
+ });
1010
1191
  };
1011
1192
 
1012
1193
  const stopInterval = () => {
@@ -1060,6 +1241,7 @@ async function runHostTui(identityPath, opts) {
1060
1241
  uiState.view = 'detail';
1061
1242
  }
1062
1243
  latestState = redraw();
1244
+ latestState = rerenderAfterSeenUpdate(latestState, { forceSession: uiState.view !== 'decisions' && uiState.view !== 'overview' });
1063
1245
  return;
1064
1246
  }
1065
1247
  if (key?.name === 'escape' || key?.name === 'h') {
@@ -1068,6 +1250,7 @@ async function runHostTui(identityPath, opts) {
1068
1250
  else if (uiState.view === 'decisions') uiState.view = 'overview';
1069
1251
  else uiState.view = 'overview';
1070
1252
  latestState = redraw();
1253
+ latestState = rerenderAfterSeenUpdate(latestState, { forceGlobal: uiState.view === 'overview' || uiState.view === 'decisions' });
1071
1254
  return;
1072
1255
  }
1073
1256
  if (_str === '?') {
@@ -1104,6 +1287,7 @@ async function runHostTui(identityPath, opts) {
1104
1287
  uiState.selectedHistoryPageIndex = 0;
1105
1288
  setStatus('Opened task list view.', 'info');
1106
1289
  latestState = redraw();
1290
+ latestState = rerenderAfterSeenUpdate(latestState, { forceSession: true });
1107
1291
  return;
1108
1292
  }
1109
1293
  if (key?.name === 'i') {
@@ -1111,6 +1295,7 @@ async function runHostTui(identityPath, opts) {
1111
1295
  uiState.selectedDecisionIndex = 0;
1112
1296
  setStatus('Opened decision inbox.', 'info');
1113
1297
  latestState = redraw();
1298
+ latestState = rerenderAfterSeenUpdate(latestState, { forceGlobal: true });
1114
1299
  return;
1115
1300
  }
1116
1301
  if (key?.name === 'r') {
@@ -1293,6 +1478,7 @@ async function runHostTui(identityPath, opts) {
1293
1478
  uiState.historySearchQuery = '';
1294
1479
  setStatus('Opened local history view.', 'info');
1295
1480
  latestState = redraw();
1481
+ latestState = rerenderAfterSeenUpdate(latestState, { forceSession: true });
1296
1482
  return;
1297
1483
  }
1298
1484
  if ((_str === '/' || key?.name === 's') && uiState.view === 'history') {
@@ -1305,6 +1491,14 @@ async function runHostTui(identityPath, opts) {
1305
1491
  return;
1306
1492
  }
1307
1493
  if (key?.name === 'b') {
1494
+ await switchTransport('bridge');
1495
+ return;
1496
+ }
1497
+ if (key?.name === 'c') {
1498
+ await switchTransport('channel');
1499
+ return;
1500
+ }
1501
+ if (_str === 'B') {
1308
1502
  const selected = (latestState.sessions || []).find((session) => session.session_key === uiState.selectedSessionKey);
1309
1503
  if (!selected?.conversation_id) return;
1310
1504
  const current = latestState.activeChatSession || '(none)';
@@ -1324,7 +1518,7 @@ async function runHostTui(identityPath, opts) {
1324
1518
  latestState = redraw();
1325
1519
  return;
1326
1520
  }
1327
- if (key?.name === 'c') {
1521
+ if (_str === 'C') {
1328
1522
  const selected = (latestState.sessions || []).find((session) => session.session_key === uiState.selectedSessionKey);
1329
1523
  if (!selected?.conversation_id) return;
1330
1524
  removeSessionBinding(selected.conversation_id);