@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.
@@ -3,29 +3,37 @@ import {
3
3
  CollaborationProjectionOutboxManager,
4
4
  ContactManager,
5
5
  LocalStore,
6
+ OperatorSeenStateManager,
6
7
  PingAgentClient,
7
8
  TrustPolicyAuditManager,
8
9
  TrustRecommendationManager,
10
+ buildDeliveryTimeline,
11
+ buildProjectionPreview,
9
12
  decideContactPolicy,
10
13
  decideTaskPolicy,
11
14
  defaultTrustPolicyDoc,
15
+ deriveTransportHealth,
12
16
  ensureTokenValid,
13
17
  getActiveSessionFilePath,
14
18
  getSessionBindingAlertsFilePath,
15
19
  getSessionMapFilePath,
16
20
  getTrustRecommendationActionLabel,
21
+ listPendingDecisionViews,
17
22
  loadIdentity,
18
23
  normalizeTrustPolicyDoc,
19
24
  readCurrentActiveSessionKey,
20
25
  readIngressRuntimeStatus,
21
26
  readSessionBindingAlerts,
22
27
  readSessionBindings,
28
+ readTransportPreference,
23
29
  removeSessionBinding,
24
30
  setSessionBinding,
31
+ summarizeSinceLastSeen,
25
32
  summarizeTrustPolicyAudit,
33
+ switchTransportPreference,
26
34
  updateStoredToken,
27
35
  upsertTrustPolicyRecommendation
28
- } from "./chunk-MFKDD5X3.js";
36
+ } from "./chunk-BCYHGKQE.js";
29
37
 
30
38
  // src/web-server.ts
31
39
  import * as fs from "fs";
@@ -210,6 +218,10 @@ function getHostPanelHtml() {
210
218
 
211
219
  <section id="runtimePanel" class="panel active">
212
220
  <div class="card" id="activationCard" style="margin-bottom:16px"></div>
221
+ <div class="grid two-col" style="margin-bottom:16px">
222
+ <div class="card" id="transportHealthCard"></div>
223
+ <div class="card" id="sinceLastSeenCard"></div>
224
+ </div>
213
225
  <div class="grid stats" id="statsGrid"></div>
214
226
  <div class="grid runtime-layout" style="margin-top:16px">
215
227
  <div class="card">
@@ -269,6 +281,7 @@ function getHostPanelHtml() {
269
281
  <div class="row-actions">
270
282
  <button class="action-btn" id="saveProjectionPresetBtn">Save projection preset</button>
271
283
  </div>
284
+ <div id="projectionPolicyPreview" class="muted small" style="margin-top:12px"></div>
272
285
  </div>
273
286
  </div>
274
287
 
@@ -407,6 +420,10 @@ function getHostPanelHtml() {
407
420
  detailMode: sessionStorage.getItem('pingagent_host_panel_detail_mode') || 'basic',
408
421
  showUnreadOnly: false,
409
422
  };
423
+ const seenMarkers = {
424
+ globalTab: null,
425
+ sessionKey: null,
426
+ };
410
427
 
411
428
  function esc(value) {
412
429
  return String(value == null ? '' : value)
@@ -429,6 +446,130 @@ function getHostPanelHtml() {
429
446
  try { return new Date(value).toLocaleString(); } catch { return String(value); }
430
447
  }
431
448
 
449
+ function countSinceLastSeen(summary) {
450
+ if (!summary) return 0;
451
+ return Number(summary.new_external_messages || 0)
452
+ + Number(summary.new_conclusions || 0)
453
+ + Number(summary.new_handoffs || 0)
454
+ + Number(summary.new_decisions || 0)
455
+ + Number(summary.new_failures || 0)
456
+ + Number(summary.new_repairs || 0)
457
+ + Number(summary.new_projection_failures || 0);
458
+ }
459
+
460
+ function renderSinceLastSeenSummary(summary) {
461
+ if (!summary) return '<div class="empty">No seen-state yet.</div>';
462
+ const count = countSinceLastSeen(summary);
463
+ return '' +
464
+ '<div class="status-main">' +
465
+ '<h2>Since Last Seen</h2>' +
466
+ '<div class="muted small">Per operator view. Opening runtime, detail, or decisions resets the matching anchor instead of forcing every refresh to zero out the feed.</div>' +
467
+ '<div class="summary-pills">' +
468
+ '<span class="pill">new=' + esc(count) + '</span>' +
469
+ '<span class="pill">external=' + esc(summary.new_external_messages || 0) + '</span>' +
470
+ '<span class="pill">conclusions=' + esc(summary.new_conclusions || 0) + '</span>' +
471
+ '<span class="pill">handoffs=' + esc(summary.new_handoffs || 0) + '</span>' +
472
+ '<span class="pill">decisions=' + esc(summary.new_decisions || 0) + '</span>' +
473
+ '<span class="pill">failures=' + esc(summary.new_failures || 0) + '</span>' +
474
+ '<span class="pill">repairs=' + esc(summary.new_repairs || 0) + '</span>' +
475
+ '<span class="pill">projection=' + esc(summary.new_projection_failures || 0) + '</span>' +
476
+ '</div>' +
477
+ '<div class="muted small" style="margin-top:8px">latest=' + esc(fmtTs(summary.latest_ts)) + '</div>' +
478
+ '</div>';
479
+ }
480
+
481
+ function renderTransportHealthCard(transportHealth) {
482
+ const health = transportHealth || {};
483
+ const stateLabel = health.state || 'Ready';
484
+ const stateClass = stateLabel === 'Degraded' || stateLabel === 'Switching Recommended' ? 'degraded' : 'ready';
485
+ const current = health.transport_mode || 'bridge';
486
+ const preferred = health.preferred_transport_mode || current;
487
+ return '' +
488
+ '<div class="status-strip">' +
489
+ '<div class="status-main">' +
490
+ '<h2>Transport Health</h2>' +
491
+ '<div class="status-state ' + esc(stateClass) + '">' + esc(stateLabel) + '</div>' +
492
+ '<div class="summary-pills">' +
493
+ '<span class="pill">current=' + esc(current) + '</span>' +
494
+ '<span class="pill">preferred=' + esc(preferred) + '</span>' +
495
+ '<span class="pill">retry_queue=' + esc(health.retry_queue_length || 0) + '</span>' +
496
+ '<span class="pill">failures=' + esc(health.consecutive_failures || 0) + '</span>' +
497
+ '</div>' +
498
+ '<div class="muted small" style="margin-top:8px">last inbound=' + esc(fmtTs(health.last_inbound_at)) + ' \xB7 last outbound=' + esc(fmtTs(health.last_outbound_at)) + '</div>' +
499
+ '<div class="muted small">last degraded=' + esc(fmtTs(health.last_degraded_at)) + ' \xB7 last repaired=' + esc(fmtTs(health.last_repaired_at)) + '</div>' +
500
+ (health.last_error ? '<div style="margin-top:8px;color:#fecaca">' + esc(health.last_error) + '</div>' : '<div class="muted small" style="margin-top:8px">No recent transport errors.</div>') +
501
+ '</div>' +
502
+ '<div style="min-width:240px">' +
503
+ '<div class="label">Actions</div>' +
504
+ '<div class="row-actions" style="margin-top:8px">' +
505
+ '<button class="secondary-btn transport-switch-btn" data-mode="bridge" style="width:auto">Switch to bridge</button>' +
506
+ '<button class="secondary-btn transport-switch-btn" data-mode="channel" style="width:auto">Switch to channel</button>' +
507
+ '</div>' +
508
+ (health.state === 'Switching Recommended'
509
+ ? '<div class="muted small" style="margin-top:10px">Assist Then Switch is recommending a move back to bridge. The preference file already captures the desired fallback path.</div>'
510
+ : '<div class="muted small" style="margin-top:10px">Managed restart keeps the transport layer swappable without changing the human-facing model.</div>') +
511
+ '</div>' +
512
+ '</div>';
513
+ }
514
+
515
+ function renderProjectionPreviewBlock(preview) {
516
+ if (!preview) return '<div class="empty">No projection preview available for the selected session yet.</div>';
517
+ const detailOnly = Array.isArray(preview.description && preview.description.detail_only) ? preview.description.detail_only : [];
518
+ const immediate = Array.isArray(preview.description && preview.description.immediate) ? preview.description.immediate : [];
519
+ return '' +
520
+ '<div class="muted small">Preset <strong>' + esc(preview.preset) + '</strong> projects <code>' + esc(immediate.join(', ') || 'none') + '</code> immediately and keeps <code>' + esc(detailOnly.join(', ') || 'none') + '</code> in detail-only mode.</div>' +
521
+ '<div class="audit-list" style="margin-top:10px">' +
522
+ (Array.isArray(preview.recent) && preview.recent.length
523
+ ? preview.recent.map(function (entry) {
524
+ return '<div class="audit-row"><div class="top"><strong>' + esc(entry.event_type) + '</strong><span class="badge">' + esc(entry.disposition) + '</span></div>' +
525
+ '<div class="muted small">' + esc(entry.reason || '(no reason)') + '</div>' +
526
+ '</div>';
527
+ }).join('')
528
+ : '<div class="empty">No recent collaboration events to preview.</div>') +
529
+ '</div>';
530
+ }
531
+
532
+ function renderDeliveryTimeline(timeline) {
533
+ if (!Array.isArray(timeline) || !timeline.length) {
534
+ return '<div class="empty">No delivery timeline yet.</div>';
535
+ }
536
+ return timeline.map(function (entry) {
537
+ return '<div class="audit-row"><div class="top"><strong>' + esc(entry.kind || 'event') + '</strong><span class="badge">' + esc(fmtTs(entry.ts_ms)) + '</span></div>' +
538
+ '<div style="margin-top:8px">' + esc(entry.summary || '(no summary)') + '</div>' +
539
+ '</div>';
540
+ }).join('');
541
+ }
542
+
543
+ async function markSeen(scopeType, scopeKey) {
544
+ return api('/api/runtime/seen', {
545
+ method: 'POST',
546
+ headers: { 'Content-Type': 'application/json' },
547
+ body: JSON.stringify({
548
+ operator_id: 'host_panel',
549
+ scope_type: scopeType,
550
+ scope_key: scopeType === 'session' ? scopeKey : null,
551
+ }),
552
+ });
553
+ }
554
+
555
+ async function maybeMarkGlobalSeen(tab, force) {
556
+ if (tab !== 'runtime' && tab !== 'decisions') return;
557
+ if (!force && seenMarkers.globalTab === tab) return;
558
+ try {
559
+ await markSeen('global', null);
560
+ seenMarkers.globalTab = tab;
561
+ } catch {}
562
+ }
563
+
564
+ async function maybeMarkSessionSeen(sessionKey, force) {
565
+ if (!sessionKey) return;
566
+ if (!force && seenMarkers.sessionKey === sessionKey) return;
567
+ try {
568
+ await markSeen('session', sessionKey);
569
+ seenMarkers.sessionKey = sessionKey;
570
+ } catch {}
571
+ }
572
+
432
573
  function syncUrlState() {
433
574
  const url = new URL(window.location.href);
434
575
  if (state.selectedProfile) url.searchParams.set('profile', state.selectedProfile);
@@ -512,6 +653,7 @@ function getHostPanelHtml() {
512
653
  }
513
654
 
514
655
  function setTab(tab) {
656
+ const previous = state.currentTab;
515
657
  state.currentTab = tab;
516
658
  document.getElementById('navRuntime').classList.toggle('active', tab === 'runtime');
517
659
  document.getElementById('navDecisions').classList.toggle('active', tab === 'decisions');
@@ -519,7 +661,16 @@ function getHostPanelHtml() {
519
661
  document.getElementById('runtimePanel').classList.toggle('active', tab === 'runtime');
520
662
  document.getElementById('decisionsPanel').classList.toggle('active', tab === 'decisions');
521
663
  document.getElementById('policyPanel').classList.toggle('active', tab === 'policy');
664
+ if (previous !== tab && tab !== 'runtime') {
665
+ seenMarkers.sessionKey = null;
666
+ }
522
667
  syncUrlState();
668
+ if (previous !== tab) {
669
+ void maybeMarkGlobalSeen(tab, true);
670
+ if (tab === 'runtime' && state.selectedSessionKey) {
671
+ void maybeMarkSessionSeen(state.selectedSessionKey, true);
672
+ }
673
+ }
523
674
  }
524
675
 
525
676
  function renderHeader() {
@@ -650,15 +801,18 @@ function getHostPanelHtml() {
650
801
  function renderDecisionInbox() {
651
802
  const data = state.decisions;
652
803
  const pendingDecisions = data && Array.isArray(data.pendingDecisions) ? data.pendingDecisions : [];
804
+ const overdueDecisions = pendingDecisions.filter(function (event) { return !!event.overdue; });
653
805
  const pendingOutbox = data && Array.isArray(data.projectionOutboxPending) ? data.projectionOutboxPending : [];
654
806
  const failedOutbox = data && Array.isArray(data.projectionOutboxFailed) ? data.projectionOutboxFailed : [];
655
807
  document.getElementById('decisionInboxSummary').textContent =
656
- 'pending=' + pendingDecisions.length + ' \xB7 projection_pending=' + pendingOutbox.length + ' \xB7 projection_failed=' + failedOutbox.length;
808
+ 'pending=' + pendingDecisions.length + ' \xB7 overdue=' + overdueDecisions.length + ' \xB7 projection_pending=' + pendingOutbox.length + ' \xB7 projection_failed=' + failedOutbox.length;
657
809
  document.getElementById('decisionInboxList').innerHTML = pendingDecisions.length
658
810
  ? pendingDecisions.map(function (event) {
659
811
  return '<div class="audit-row"><div class="top"><strong>' + esc(event.summary || '(no summary)') + '</strong>' +
660
- '<span class="badge">' + esc(event.severity || 'warning') + '</span></div>' +
661
- '<div class="muted small">' + esc(fmtTs(event.ts_ms)) + ' \xB7 session=' + esc(event.session_key || '(none)') + ' \xB7 target=' + esc(event.target_human_session || '(none)') + '</div>' +
812
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end"><span class="badge">' + esc(event.severity || 'warning') + '</span>' +
813
+ (event.overdue ? '<span class="badge alert">overdue</span>' : '') + '</div></div>' +
814
+ '<div class="muted small">' + esc(fmtTs(event.ts_ms)) + ' \xB7 session=' + esc(event.session_key || '(none)') + ' \xB7 target=' + esc(event.target_human_session || '(none)') +
815
+ (event.overdue ? ' \xB7 overdue_by=' + esc(Math.ceil((event.overdue_by_ms || 0) / 60000)) + 'm' : '') + '</div>' +
662
816
  '<div class="muted small" style="margin-top:8px">detail_ref=' + esc(event.conversation_id || '(none)') + '</div>' +
663
817
  '<div class="row-actions">' +
664
818
  '<button class="action-btn inbox-decision-btn" data-event-id="' + esc(event.id) + '" data-decision="approved">Approve</button>' +
@@ -747,6 +901,11 @@ function getHostPanelHtml() {
747
901
  if (!overview) return;
748
902
  syncSelectedSessionFromOverview();
749
903
  const ingressState = ingressStatusModel(overview);
904
+ const transportHealth = overview.transportHealth || null;
905
+ const sinceLastSeen = overview.sinceLastSeen || null;
906
+ const overdueDecisions = Array.isArray(overview.pendingDecisions)
907
+ ? overview.pendingDecisions.filter(function (event) { return !!event.overdue; })
908
+ : [];
750
909
  document.getElementById('activationCard').innerHTML =
751
910
  '<div class="status-strip">' +
752
911
  '<div class="status-main">' +
@@ -754,6 +913,11 @@ function getHostPanelHtml() {
754
913
  '<div class="status-state ' + ingressState.className + '">' + esc(ingressState.label) + '</div>' +
755
914
  '<div class="muted small">' + esc(ingressState.detail) + '</div>' +
756
915
  '<div class="muted small">Public link: ' + esc(overview.publicSelf && overview.publicSelf.public_url ? overview.publicSelf.public_url : '(not ready yet)') + '</div>' +
916
+ '<div class="summary-pills">' +
917
+ '<span class="pill">pending_decisions=' + esc((overview.pendingDecisions || []).length) + '</span>' +
918
+ '<span class="pill">overdue=' + esc(overdueDecisions.length) + '</span>' +
919
+ '<span class="pill">projection=' + esc(overview.collaborationProjection && overview.collaborationProjection.preset ? overview.collaborationProjection.preset : 'balanced') + '</span>' +
920
+ '</div>' +
757
921
  '</div>' +
758
922
  '<div style="min-width:320px">' +
759
923
  '<div class="label">Quick Start</div>' +
@@ -768,6 +932,8 @@ function getHostPanelHtml() {
768
932
  '</div>' +
769
933
  '</div>' +
770
934
  '</div>';
935
+ document.getElementById('transportHealthCard').innerHTML = renderTransportHealthCard(transportHealth);
936
+ document.getElementById('sinceLastSeenCard').innerHTML = renderSinceLastSeenSummary(sinceLastSeen);
771
937
  const subscription = overview.subscription || null;
772
938
  const stats = [
773
939
  { label: 'Plan', value: subscription ? subscription.tier : 'ghost', sub: subscription ? subscription.summary : 'subscription unavailable' },
@@ -796,6 +962,9 @@ function getHostPanelHtml() {
796
962
  const active = session.session_key === state.selectedSessionKey ? ' active' : '';
797
963
  const badges = [
798
964
  '<span class="badge ' + esc(session.trust_state) + '">' + esc(session.trust_state) + '</span>',
965
+ (countSinceLastSeen(session.since_last_seen) > 0
966
+ ? '<span class="badge alert">new ' + esc(countSinceLastSeen(session.since_last_seen)) + '</span>'
967
+ : ''),
799
968
  session.binding_alert
800
969
  ? '<button type="button" class="badge alert rebind-badge-btn" data-session="' + esc(session.session_key) + '" data-conversation="' + esc(session.conversation_id || '') + '" data-bound-session="' + esc(session.mapped_work_session || '') + '" data-remote-did="' + esc(session.remote_did || '') + '" title="OpenClaw chat link needs attention">Needs reconnect</button>'
801
970
  : '',
@@ -833,6 +1002,25 @@ function getHostPanelHtml() {
833
1002
  });
834
1003
  }
835
1004
 
1005
+ document.querySelectorAll('.transport-switch-btn').forEach(function (btn) {
1006
+ btn.addEventListener('click', async function () {
1007
+ const mode = btn.getAttribute('data-mode');
1008
+ if (mode !== 'bridge' && mode !== 'channel') return;
1009
+ const confirmed = window.confirm('Switch preferred transport to ' + mode + ' and request a managed restart?');
1010
+ if (!confirmed) return;
1011
+ const result = await api('/api/runtime/transport-switch', {
1012
+ method: 'POST',
1013
+ headers: { 'Content-Type': 'application/json' },
1014
+ body: JSON.stringify({ mode: mode }),
1015
+ });
1016
+ window.alert(result && result.result && result.result.restarted
1017
+ ? ('Preferred transport switched to ' + mode + '. Managed restart was attempted.')
1018
+ : ('Preferred transport switched to ' + mode + '. Restart is still required.'));
1019
+ await refreshAll();
1020
+ setTab('runtime');
1021
+ });
1022
+ });
1023
+
836
1024
  const tasks = Array.isArray(overview.tasks) ? overview.tasks : [];
837
1025
  document.getElementById('taskList').innerHTML = tasks.length
838
1026
  ? tasks.map(function (task) {
@@ -937,6 +1125,10 @@ conversation=' + result.conversation_id));
937
1125
  ? detail.collaborationProjection.preset
938
1126
  : 'balanced';
939
1127
  const projectionOutbox = Array.isArray(detail.projectionOutbox) ? detail.projectionOutbox : [];
1128
+ const sinceLastSeen = detail.sinceLastSeen || null;
1129
+ const deliveryTimeline = Array.isArray(detail.deliveryTimeline) ? detail.deliveryTimeline : [];
1130
+ const projectionPreview = detail.projectionPreview || null;
1131
+ const transportHealth = detail.transportHealth || null;
940
1132
  const isAdvanced = state.detailMode === 'advanced';
941
1133
  const sessionLink = buildSessionLink(session.session_key);
942
1134
  const summaryPills = [];
@@ -972,7 +1164,8 @@ conversation=' + result.conversation_id));
972
1164
  : (reopenRecommendation ? '<div class="muted small" style="margin-top:8px">trust_action=' + esc(recommendationActionLabel(reopenRecommendation)) + '</div>' : '')) +
973
1165
  (summaryPills.length ? '<div class="summary-pills">' + summaryPills.join('') + '</div>' : '') +
974
1166
  (pendingCollaborationEvents.length
975
- ? '<div style="margin-top:10px;padding:10px 12px;border:1px solid #f59e0b;border-radius:10px;background:rgba(120,53,15,0.25);color:#fde68a"><strong>Decision pending</strong><div class="small" style="margin-top:6px">A collaboration update needs review before the human thread can be treated as fully current. Resolve it in the Collaboration Feed below.</div></div>'
1167
+ ? '<div style="margin-top:10px;padding:10px 12px;border:1px solid #f59e0b;border-radius:10px;background:rgba(120,53,15,0.25);color:#fde68a"><strong>Decision pending</strong><div class="small" style="margin-top:6px">A collaboration update needs review before the human thread can be treated as fully current. Resolve it in the Collaboration Feed below.' +
1168
+ (pendingCollaborationEvents[0].overdue ? ' This item is overdue.' : '') + '</div></div>'
976
1169
  : '') +
977
1170
  (bindingAlert
978
1171
  ? '<div style="margin-top:10px;padding:10px 12px;border:1px solid #ef4444;border-radius:10px;background:rgba(127,29,29,0.25);color:#fecaca"><strong>Needs reconnect</strong><div class="small" style="margin-top:6px">' + esc(isAdvanced ? (bindingAlert.message || 'Bound work session is missing. Rebind this PingAgent conversation to the current chat session.') : 'OpenClaw chat link is stale. Attach this PingAgent session to the current OpenClaw chat.') + '</div></div>'
@@ -1084,15 +1277,36 @@ conversation=' + result.conversation_id));
1084
1277
  '</div></div>' +
1085
1278
  '<div><div class="label">Human Thread Posture</div><div class="audit-list" style="margin-top:8px">' +
1086
1279
  '<div class="audit-row"><div class="top"><strong>Projection policy</strong><span class="badge">' + esc(projectionPreset) + '</span></div>' +
1087
- '<div class="muted small">The collaboration session keeps the raw transcript. The human thread receives concise updates, risk signals, decisions, and approval results through the projection outbox.</div>' +
1088
- '<div style="margin-top:8px">Use this detail view to inspect every raw message, task, handoff, audit event, and runtime change before taking action.</div>' +
1280
+ '<div class="muted small">The collaboration session keeps the raw transcript. The human thread receives concise updates, risk signals, decisions, and approval results through the projection outbox.</div>' +
1281
+ '<div style="margin-top:8px">Use this detail view to inspect every raw message, task, handoff, audit event, and runtime change before taking action.</div>' +
1282
+ (sinceLastSeen
1283
+ ? '<div class="summary-pills" style="margin-top:8px">' +
1284
+ '<span class="pill">new=' + esc(countSinceLastSeen(sinceLastSeen)) + '</span>' +
1285
+ '<span class="pill">decisions=' + esc(sinceLastSeen.new_decisions || 0) + '</span>' +
1286
+ '<span class="pill">failures=' + esc(sinceLastSeen.new_failures || 0) + '</span>' +
1287
+ '</div>'
1288
+ : '') +
1289
+ (transportHealth
1290
+ ? '<div class="muted small" style="margin-top:8px">transport=' + esc(transportHealth.transport_mode || 'bridge') + ' \xB7 state=' + esc(transportHealth.state || 'Ready') + ' \xB7 retry_queue=' + esc(transportHealth.retry_queue_length || 0) + '</div>'
1291
+ : '') +
1089
1292
  (projectionOutbox.length
1090
1293
  ? '<div class="muted small" style="margin-top:8px">outbox=' + esc(projectionOutbox[0].status || 'pending') + ' \xB7 target=' + esc(projectionOutbox[0].target_human_session || '(missing)') + '</div>'
1091
1294
  : '<div class="muted small" style="margin-top:8px">outbox=clear</div>') +
1295
+ '<div style="margin-top:12px"><div class="label">Projection Preview</div><div style="margin-top:8px">' + renderProjectionPreviewBlock(projectionPreview) + '</div></div>' +
1092
1296
  '</div>' +
1093
1297
  '</div></div>' +
1094
1298
  '</div>';
1095
1299
 
1300
+ el.innerHTML +=
1301
+ '<div class="grid two-col" style="margin-top:16px">' +
1302
+ '<div><div class="label">Since Last Seen</div><div class="audit-list" style="margin-top:8px">' +
1303
+ '<div class="audit-row">' + renderSinceLastSeenSummary(sinceLastSeen) + '</div>' +
1304
+ '</div></div>' +
1305
+ '<div><div class="label">Delivery Timeline</div><div class="audit-list" style="margin-top:8px">' +
1306
+ renderDeliveryTimeline(deliveryTimeline) +
1307
+ '</div></div>' +
1308
+ '</div>';
1309
+
1096
1310
  el.querySelectorAll('.approve-session-btn').forEach(function (btn) {
1097
1311
  btn.addEventListener('click', async function () {
1098
1312
  const sessionKey = btn.getAttribute('data-session');
@@ -1272,6 +1486,7 @@ Previous chat link: ' + previous
1272
1486
  policy.doc && policy.doc.collaboration_projection && policy.doc.collaboration_projection.preset
1273
1487
  ? policy.doc.collaboration_projection.preset
1274
1488
  : 'balanced';
1489
+ document.getElementById('projectionPolicyPreview').innerHTML = renderProjectionPreviewBlock(state.session && state.session.projectionPreview ? state.session.projectionPreview : null);
1275
1490
  document.getElementById('contactDefault').value = policy.doc.contact_policy.default_action;
1276
1491
  document.getElementById('taskDefault').value = policy.doc.task_policy.default_action;
1277
1492
  document.getElementById('profileDisplayName').value = profile && profile.display_name ? profile.display_name : '';
@@ -1443,6 +1658,7 @@ Previous chat link: ' + previous
1443
1658
  syncSelectedSessionFromOverview();
1444
1659
  renderHeader();
1445
1660
  renderOverview();
1661
+ if (state.currentTab === 'runtime') await maybeMarkGlobalSeen('runtime');
1446
1662
  if (state.selectedSessionKey) {
1447
1663
  await loadSession(state.selectedSessionKey);
1448
1664
  } else {
@@ -1454,10 +1670,14 @@ Previous chat link: ' + previous
1454
1670
 
1455
1671
  async function loadSession(sessionKey) {
1456
1672
  if (!sessionKey) return;
1673
+ const previousSessionKey = state.selectedSessionKey;
1457
1674
  state.selectedSessionKey = sessionKey;
1458
1675
  state.session = await api('/api/runtime/session?session_key=' + encodeURIComponent(sessionKey));
1459
1676
  syncUrlState();
1460
1677
  renderSession();
1678
+ if (state.currentTab === 'runtime') {
1679
+ await maybeMarkSessionSeen(sessionKey, previousSessionKey !== sessionKey);
1680
+ }
1461
1681
  }
1462
1682
 
1463
1683
  async function loadPolicy() {
@@ -1468,6 +1688,7 @@ Previous chat link: ' + previous
1468
1688
  async function loadDecisions() {
1469
1689
  state.decisions = await api('/api/runtime/collaboration-decisions');
1470
1690
  renderDecisionInbox();
1691
+ if (state.currentTab === 'decisions') await maybeMarkGlobalSeen('decisions');
1471
1692
  }
1472
1693
 
1473
1694
  async function refreshAll() {
@@ -1897,6 +2118,42 @@ function readProjectionOutboxState(storePath, limit = 20) {
1897
2118
  store.close();
1898
2119
  }
1899
2120
  }
2121
+ function readTransportHealthState(storePath) {
2122
+ const store = new LocalStore(storePath);
2123
+ try {
2124
+ const eventManager = new CollaborationEventManager(store);
2125
+ const outboxManager = new CollaborationProjectionOutboxManager(store);
2126
+ const runtimeStatus = readIngressRuntimeStatus();
2127
+ const preference = readTransportPreference();
2128
+ const normalizedStatus = runtimeStatus ? {
2129
+ ...runtimeStatus,
2130
+ preferred_transport_mode: preference?.preferred_mode ?? runtimeStatus.preferred_transport_mode ?? "bridge"
2131
+ } : {
2132
+ receive_mode: "webhook",
2133
+ transport_mode: preference?.preferred_mode ?? "bridge",
2134
+ preferred_transport_mode: preference?.preferred_mode ?? "bridge"
2135
+ };
2136
+ return deriveTransportHealth({
2137
+ runtime_status: normalizedStatus,
2138
+ recent_events: eventManager.listRecent(30),
2139
+ projection_outbox_failed: outboxManager.listByStatus("failed", 20)
2140
+ });
2141
+ } finally {
2142
+ store.close();
2143
+ }
2144
+ }
2145
+ function readSinceLastSeenState(storePath, operatorId, scopeType, scopeKey) {
2146
+ const store = new LocalStore(storePath);
2147
+ try {
2148
+ return summarizeSinceLastSeen(store, {
2149
+ operator_id: operatorId,
2150
+ scope_type: scopeType,
2151
+ scope_key: scopeKey
2152
+ });
2153
+ } finally {
2154
+ store.close();
2155
+ }
2156
+ }
1900
2157
  function buildPolicyDecisionShape(identityPath, remoteDid, opts) {
1901
2158
  const policy = readTrustPolicyDoc(identityPath);
1902
2159
  const runtimeMode = opts?.runtimeMode ?? getRuntimeMode();
@@ -2009,6 +2266,15 @@ async function buildRuntimeOverviewPayload(ctx) {
2009
2266
  }, {});
2010
2267
  const collaborationSummary = collaborationEventManager.summarize(200);
2011
2268
  const projectionOutbox = readProjectionOutboxState(ctx.storePath, 20);
2269
+ const transportHealth = readTransportHealthState(ctx.storePath);
2270
+ const sinceLastSeen = readSinceLastSeenState(ctx.storePath, "host_panel", "global");
2271
+ const decisionStore = new LocalStore(ctx.storePath);
2272
+ let decisionViews = [];
2273
+ try {
2274
+ decisionViews = listPendingDecisionViews(decisionStore, 100);
2275
+ } finally {
2276
+ decisionStore.close();
2277
+ }
2012
2278
  return {
2013
2279
  did: ctx.myDid,
2014
2280
  serverUrl: ctx.serverUrl,
@@ -2046,6 +2312,9 @@ async function buildRuntimeOverviewPayload(ctx) {
2046
2312
  recommendationSummary: recommendationState.summary,
2047
2313
  collaborationSummary,
2048
2314
  projectionOutbox,
2315
+ transportHealth,
2316
+ sinceLastSeen,
2317
+ pendingDecisions: decisionViews,
2049
2318
  recentCollaborationEvents: collaborationEventManager.listRecent(20),
2050
2319
  sessions: sessions.map((session) => ({
2051
2320
  ...session,
@@ -2054,7 +2323,8 @@ async function buildRuntimeOverviewPayload(ctx) {
2054
2323
  binding_alert: session.conversation_id ? bindingAlertByConversation.get(session.conversation_id) ?? null : null,
2055
2324
  is_active_work_session: session.session_key === activeWorkSession,
2056
2325
  latest_messages: session.conversation_id ? historyManager.listRecent(session.conversation_id, 3) : [],
2057
- collaboration_events: collaborationEventManager.listBySession(session.session_key, 3)
2326
+ collaboration_events: collaborationEventManager.listBySession(session.session_key, 3),
2327
+ since_last_seen: readSinceLastSeenState(ctx.storePath, "host_panel", "session", session.session_key)
2058
2328
  })),
2059
2329
  tasks: refreshedTasks.map((task) => ({
2060
2330
  ...task,
@@ -2111,8 +2381,25 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
2111
2381
  });
2112
2382
  const outboxStore = new LocalStore(ctx.storePath);
2113
2383
  let projectionOutbox = [];
2384
+ let sinceLastSeen = null;
2385
+ let deliveryTimeline = [];
2386
+ let projectionPreview = null;
2387
+ let pendingDecisionViews = [];
2114
2388
  try {
2115
2389
  projectionOutbox = new CollaborationProjectionOutboxManager(outboxStore).listBySession(session.session_key, 20);
2390
+ sinceLastSeen = summarizeSinceLastSeen(outboxStore, {
2391
+ operator_id: "host_panel",
2392
+ scope_type: "session",
2393
+ scope_key: session.session_key
2394
+ });
2395
+ deliveryTimeline = buildDeliveryTimeline(outboxStore, session.session_key, 40);
2396
+ projectionPreview = buildProjectionPreview(
2397
+ outboxStore,
2398
+ session.session_key,
2399
+ policy.collaboration_projection.preset,
2400
+ 5
2401
+ );
2402
+ pendingDecisionViews = listPendingDecisionViews(outboxStore, 100).filter((event) => event.session_key === session.session_key).slice(0, 20);
2116
2403
  } finally {
2117
2404
  outboxStore.close();
2118
2405
  }
@@ -2120,9 +2407,10 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
2120
2407
  session,
2121
2408
  sessionSummary: sessionSummaryManager.get(session.session_key),
2122
2409
  collaborationEvents: collaborationEventManager.listBySession(session.session_key, 40),
2123
- pendingCollaborationEvents: collaborationEventManager.listPendingBySession(session.session_key, 20),
2410
+ pendingCollaborationEvents: pendingDecisionViews,
2124
2411
  collaborationSummary: collaborationEventManager.summarize(200),
2125
2412
  ingressRuntime: readIngressRuntimeStatus(),
2413
+ transportHealth: readTransportHealthState(ctx.storePath),
2126
2414
  binding,
2127
2415
  bindingAlert,
2128
2416
  activeWorkSession,
@@ -2131,6 +2419,9 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
2131
2419
  sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
2132
2420
  collaborationProjection: policy.collaboration_projection,
2133
2421
  projectionOutbox,
2422
+ sinceLastSeen,
2423
+ deliveryTimeline,
2424
+ projectionPreview,
2134
2425
  policyExplain: buildPolicyDecisionShape(ctx.identityPath, session.remote_did, { runtimeMode: getRuntimeMode() }),
2135
2426
  tasks: tasks.map((task) => ({
2136
2427
  ...task,
@@ -2207,6 +2498,70 @@ async function handleApi(pathname, req, ctx) {
2207
2498
  status: readIngressRuntimeStatus()
2208
2499
  };
2209
2500
  }
2501
+ if (parts[1] === "transport-health" && req.method === "GET") {
2502
+ return {
2503
+ transportHealth: readTransportHealthState(ctx.storePath),
2504
+ transportPreference: readTransportPreference()
2505
+ };
2506
+ }
2507
+ if (parts[1] === "transport-switch" && req.method === "POST") {
2508
+ const body = await readBody(req);
2509
+ const mode = String(body?.mode ?? "").trim().toLowerCase();
2510
+ if (mode !== "bridge" && mode !== "channel") throw new Error("mode must be bridge or channel");
2511
+ const result = switchTransportPreference(mode, {
2512
+ updated_by: "host_panel"
2513
+ });
2514
+ const store = new LocalStore(ctx.storePath);
2515
+ try {
2516
+ new CollaborationEventManager(store).record({
2517
+ event_type: "transport_switched",
2518
+ severity: result.restarted ? "notice" : "warning",
2519
+ summary: result.restarted ? `Preferred transport switched to ${mode}. A managed restart was attempted.` : `Preferred transport switched to ${mode}. Restart is still required.`,
2520
+ detail: {
2521
+ preferred_mode: result.preferred_mode,
2522
+ restart_required: result.restart_required,
2523
+ restart_method: result.restart_method ?? null,
2524
+ restart_error: result.restart_error ?? null,
2525
+ preference_path: result.preference_path
2526
+ }
2527
+ });
2528
+ } finally {
2529
+ store.close();
2530
+ }
2531
+ return {
2532
+ ok: true,
2533
+ result,
2534
+ transportHealth: readTransportHealthState(ctx.storePath)
2535
+ };
2536
+ }
2537
+ if (parts[1] === "seen" && req.method === "POST") {
2538
+ const body = await readBody(req);
2539
+ const operatorId = String(body?.operator_id ?? "host_panel").trim();
2540
+ const scopeType = String(body?.scope_type ?? "global").trim();
2541
+ const scopeKey = typeof body?.scope_key === "string" ? body.scope_key.trim() : void 0;
2542
+ if (!["host_panel", "tui", "mcp"].includes(operatorId)) throw new Error("operator_id must be host_panel, tui, or mcp");
2543
+ if (scopeType !== "global" && scopeType !== "session") throw new Error("scope_type must be global or session");
2544
+ const store = new LocalStore(ctx.storePath);
2545
+ try {
2546
+ const seen = new OperatorSeenStateManager(store).markSeen({
2547
+ operator_id: operatorId,
2548
+ scope_type: scopeType,
2549
+ scope_key: scopeType === "session" ? scopeKey : null,
2550
+ last_seen_ts: Date.now()
2551
+ });
2552
+ return {
2553
+ ok: true,
2554
+ seen,
2555
+ summary: summarizeSinceLastSeen(store, {
2556
+ operator_id: operatorId,
2557
+ scope_type: scopeType,
2558
+ scope_key: scopeType === "session" ? scopeKey : null
2559
+ })
2560
+ };
2561
+ } finally {
2562
+ store.close();
2563
+ }
2564
+ }
2210
2565
  if (parts[1] === "demo" && req.method === "POST") {
2211
2566
  const body = await readBody(req);
2212
2567
  const preset = typeof body?.preset === "string" ? body.preset.trim().toLowerCase() : "";
@@ -2262,7 +2617,7 @@ async function handleApi(pathname, req, ctx) {
2262
2617
  const manager = new CollaborationEventManager(store);
2263
2618
  const outboxManager = new CollaborationProjectionOutboxManager(store);
2264
2619
  if (req.method === "GET") {
2265
- const pendingDecisions = manager.listPending(100);
2620
+ const pendingDecisions = listPendingDecisionViews(store, 100);
2266
2621
  return {
2267
2622
  pendingDecisions,
2268
2623
  projectionOutboxPending: outboxManager.listByStatus("pending", 50),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pingagent/sdk",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -35,8 +35,8 @@
35
35
  "uuid": "^11.0.0",
36
36
  "ws": "^8.0.0",
37
37
  "@pingagent/protocol": "0.1.1",
38
- "@pingagent/a2a": "0.1.1",
39
- "@pingagent/schemas": "0.1.4"
38
+ "@pingagent/schemas": "0.1.4",
39
+ "@pingagent/a2a": "0.1.1"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/better-sqlite3": "^7.6.0",