@pingagent/sdk 0.1.14 → 0.1.16

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.
@@ -2,30 +2,42 @@ import {
2
2
  CollaborationEventManager,
3
3
  CollaborationProjectionOutboxManager,
4
4
  ContactManager,
5
+ HumanDeliveryBindingManager,
5
6
  LocalStore,
7
+ NotificationIntentManager,
8
+ OperatorSeenStateManager,
6
9
  PingAgentClient,
7
10
  TrustPolicyAuditManager,
8
11
  TrustRecommendationManager,
12
+ buildDeliveryTimeline,
13
+ buildProjectionPreview,
9
14
  decideContactPolicy,
10
15
  decideTaskPolicy,
11
16
  defaultTrustPolicyDoc,
17
+ deriveTransportHealth,
12
18
  ensureTokenValid,
13
19
  getActiveSessionFilePath,
14
20
  getSessionBindingAlertsFilePath,
15
21
  getSessionMapFilePath,
16
22
  getTrustRecommendationActionLabel,
23
+ listPendingDecisionViews,
24
+ listRecentBindingsForSession,
17
25
  loadIdentity,
18
26
  normalizeTrustPolicyDoc,
19
27
  readCurrentActiveSessionKey,
20
28
  readIngressRuntimeStatus,
21
29
  readSessionBindingAlerts,
22
30
  readSessionBindings,
31
+ readTransportPreference,
23
32
  removeSessionBinding,
24
33
  setSessionBinding,
34
+ summarizeHumanDelivery,
35
+ summarizeSinceLastSeen,
25
36
  summarizeTrustPolicyAudit,
37
+ switchTransportPreference,
26
38
  updateStoredToken,
27
39
  upsertTrustPolicyRecommendation
28
- } from "./chunk-MFKDD5X3.js";
40
+ } from "./chunk-GU5W4KRD.js";
29
41
 
30
42
  // src/web-server.ts
31
43
  import * as fs from "fs";
@@ -210,6 +222,10 @@ function getHostPanelHtml() {
210
222
 
211
223
  <section id="runtimePanel" class="panel active">
212
224
  <div class="card" id="activationCard" style="margin-bottom:16px"></div>
225
+ <div class="grid two-col" style="margin-bottom:16px">
226
+ <div class="card" id="transportHealthCard"></div>
227
+ <div class="card" id="sinceLastSeenCard"></div>
228
+ </div>
213
229
  <div class="grid stats" id="statsGrid"></div>
214
230
  <div class="grid runtime-layout" style="margin-top:16px">
215
231
  <div class="card">
@@ -269,6 +285,7 @@ function getHostPanelHtml() {
269
285
  <div class="row-actions">
270
286
  <button class="action-btn" id="saveProjectionPresetBtn">Save projection preset</button>
271
287
  </div>
288
+ <div id="projectionPolicyPreview" class="muted small" style="margin-top:12px"></div>
272
289
  </div>
273
290
  </div>
274
291
 
@@ -407,6 +424,10 @@ function getHostPanelHtml() {
407
424
  detailMode: sessionStorage.getItem('pingagent_host_panel_detail_mode') || 'basic',
408
425
  showUnreadOnly: false,
409
426
  };
427
+ const seenMarkers = {
428
+ globalTab: null,
429
+ sessionKey: null,
430
+ };
410
431
 
411
432
  function esc(value) {
412
433
  return String(value == null ? '' : value)
@@ -429,6 +450,220 @@ function getHostPanelHtml() {
429
450
  try { return new Date(value).toLocaleString(); } catch { return String(value); }
430
451
  }
431
452
 
453
+ function countSinceLastSeen(summary) {
454
+ if (!summary) return 0;
455
+ return Number(summary.new_external_messages || 0)
456
+ + Number(summary.new_conclusions || 0)
457
+ + Number(summary.new_handoffs || 0)
458
+ + Number(summary.new_decisions || 0)
459
+ + Number(summary.new_failures || 0)
460
+ + Number(summary.new_repairs || 0)
461
+ + Number(summary.new_projection_failures || 0);
462
+ }
463
+
464
+ function renderSinceLastSeenSummary(summary) {
465
+ if (!summary) return '<div class="empty">No seen-state yet.</div>';
466
+ const count = countSinceLastSeen(summary);
467
+ return '' +
468
+ '<div class="status-main">' +
469
+ '<h2>Since Last Seen</h2>' +
470
+ '<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>' +
471
+ '<div class="summary-pills">' +
472
+ '<span class="pill">new=' + esc(count) + '</span>' +
473
+ '<span class="pill">external=' + esc(summary.new_external_messages || 0) + '</span>' +
474
+ '<span class="pill">conclusions=' + esc(summary.new_conclusions || 0) + '</span>' +
475
+ '<span class="pill">handoffs=' + esc(summary.new_handoffs || 0) + '</span>' +
476
+ '<span class="pill">decisions=' + esc(summary.new_decisions || 0) + '</span>' +
477
+ '<span class="pill">failures=' + esc(summary.new_failures || 0) + '</span>' +
478
+ '<span class="pill">repairs=' + esc(summary.new_repairs || 0) + '</span>' +
479
+ '<span class="pill">projection=' + esc(summary.new_projection_failures || 0) + '</span>' +
480
+ '</div>' +
481
+ '<div class="muted small" style="margin-top:8px">latest=' + esc(fmtTs(summary.latest_ts)) + '</div>' +
482
+ '</div>';
483
+ }
484
+
485
+ function renderTransportHealthCard(transportHealth) {
486
+ const health = transportHealth || {};
487
+ const stateLabel = health.state || 'Ready';
488
+ const stateClass = stateLabel === 'Degraded' || stateLabel === 'Switching Recommended' ? 'degraded' : 'ready';
489
+ const current = health.transport_mode || 'bridge';
490
+ const preferred = health.preferred_transport_mode || current;
491
+ return '' +
492
+ '<div class="status-strip">' +
493
+ '<div class="status-main">' +
494
+ '<h2>Transport Health</h2>' +
495
+ '<div class="status-state ' + esc(stateClass) + '">' + esc(stateLabel) + '</div>' +
496
+ '<div class="summary-pills">' +
497
+ '<span class="pill">current=' + esc(current) + '</span>' +
498
+ '<span class="pill">preferred=' + esc(preferred) + '</span>' +
499
+ '<span class="pill">retry_queue=' + esc(health.retry_queue_length || 0) + '</span>' +
500
+ '<span class="pill">failures=' + esc(health.consecutive_failures || 0) + '</span>' +
501
+ '</div>' +
502
+ '<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>' +
503
+ '<div class="muted small">last degraded=' + esc(fmtTs(health.last_degraded_at)) + ' \xB7 last repaired=' + esc(fmtTs(health.last_repaired_at)) + '</div>' +
504
+ (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>') +
505
+ '</div>' +
506
+ '<div style="min-width:240px">' +
507
+ '<div class="label">Actions</div>' +
508
+ '<div class="row-actions" style="margin-top:8px">' +
509
+ '<button class="secondary-btn transport-switch-btn" data-mode="bridge" style="width:auto">Switch to bridge</button>' +
510
+ '<button class="secondary-btn transport-switch-btn" data-mode="channel" style="width:auto">Switch to channel</button>' +
511
+ '</div>' +
512
+ (health.state === 'Switching Recommended'
513
+ ? '<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>'
514
+ : '<div class="muted small" style="margin-top:10px">Managed restart keeps the transport layer swappable without changing the human-facing model.</div>') +
515
+ '</div>' +
516
+ '</div>';
517
+ }
518
+
519
+ function renderProjectionPreviewBlock(preview) {
520
+ if (!preview) return '<div class="empty">No projection preview available for the selected session yet.</div>';
521
+ const detailOnly = Array.isArray(preview.description && preview.description.detail_only) ? preview.description.detail_only : [];
522
+ const immediate = Array.isArray(preview.description && preview.description.immediate) ? preview.description.immediate : [];
523
+ return '' +
524
+ '<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>' +
525
+ '<div class="audit-list" style="margin-top:10px">' +
526
+ (Array.isArray(preview.recent) && preview.recent.length
527
+ ? preview.recent.map(function (entry) {
528
+ return '<div class="audit-row"><div class="top"><strong>' + esc(entry.event_type) + '</strong><span class="badge">' + esc(entry.disposition) + '</span></div>' +
529
+ '<div class="muted small">' + esc(entry.reason || '(no reason)') + '</div>' +
530
+ '</div>';
531
+ }).join('')
532
+ : '<div class="empty">No recent collaboration events to preview.</div>') +
533
+ '</div>';
534
+ }
535
+
536
+ function renderDeliveryTimeline(timeline) {
537
+ if (!Array.isArray(timeline) || !timeline.length) {
538
+ return '<div class="empty">No delivery timeline yet.</div>';
539
+ }
540
+ return timeline.map(function (entry) {
541
+ return '<div class="audit-row"><div class="top"><strong>' + esc(entry.kind || 'event') + '</strong><span class="badge">' + esc(fmtTs(entry.ts_ms)) + '</span></div>' +
542
+ '<div style="margin-top:8px">' + esc(entry.summary || '(no summary)') + '</div>' +
543
+ '</div>';
544
+ }).join('');
545
+ }
546
+
547
+ function renderHumanDeliveryIntents(intents, emptyLabel) {
548
+ if (!Array.isArray(intents) || !intents.length) {
549
+ return '<div class="empty">' + esc(emptyLabel || 'No notification intents yet.') + '</div>';
550
+ }
551
+ return intents.map(function (intent) {
552
+ const bindingLabel = intent.resolved_binding_id ? ('binding=' + intent.resolved_binding_id) : 'binding=(unresolved)';
553
+ const ackLabel = intent.acknowledged_at
554
+ ? (' \xB7 ack=' + fmtTs(intent.acknowledged_at))
555
+ : '';
556
+ const actions = [];
557
+ if (intent.status === 'failed' || intent.status === 'unresolved') {
558
+ actions.push('<button class="secondary-btn human-delivery-action-btn" data-action="retry_intent" data-intent-id="' + esc(intent.id) + '">Retry</button>');
559
+ actions.push('<button class="secondary-btn human-delivery-action-btn" data-action="re_resolve_intent_binding" data-intent-id="' + esc(intent.id) + '">Re-resolve</button>');
560
+ } else if (intent.status === 'pending' || intent.status === 'bound' || intent.status === 'sent') {
561
+ actions.push('<button class="secondary-btn human-delivery-action-btn" data-action="re_resolve_intent_binding" data-intent-id="' + esc(intent.id) + '">Re-resolve</button>');
562
+ }
563
+ if (intent.status !== 'canceled' && intent.status !== 'acknowledged') {
564
+ actions.push('<button class="danger-btn human-delivery-action-btn" data-action="cancel_intent" data-intent-id="' + esc(intent.id) + '">Cancel</button>');
565
+ }
566
+ return '<div class="audit-row"><div class="top"><strong>' + esc(intent.title || intent.summary || '(no title)') + '</strong>' +
567
+ '<span class="badge">' + esc(intent.status || 'pending') + '</span></div>' +
568
+ '<div class="muted small">' + esc(fmtTs(intent.created_at || intent.updated_at)) + ' \xB7 type=' + esc(intent.intent_type || 'collaboration_update') + ' \xB7 ' + esc(bindingLabel) + esc(ackLabel) + '</div>' +
569
+ '<div style="margin-top:8px">' + esc(intent.summary || '(no summary)') + '</div>' +
570
+ (intent.last_error ? '<div class="muted small" style="margin-top:8px;color:#fecaca">' + esc(intent.last_error) + '</div>' : '') +
571
+ (actions.length ? '<div class="row-actions" style="margin-top:10px">' + actions.join('') + '</div>' : '') +
572
+ '</div>';
573
+ }).join('');
574
+ }
575
+
576
+ function renderRecentBindings(bindings) {
577
+ if (!Array.isArray(bindings) || !bindings.length) {
578
+ return '<div class="empty">No human reply targets recorded yet. Once a person talks to OpenClaw from any channel, the explicit reply target will appear here.</div>';
579
+ }
580
+ return bindings.map(function (binding) {
581
+ const actions = binding.status === 'active'
582
+ ? '<div class="row-actions" style="margin-top:10px"><button class="secondary-btn human-delivery-action-btn" data-action="mark_binding_stale" data-binding-id="' + esc(binding.id) + '">Mark stale</button></div>'
583
+ : '';
584
+ return '<div class="audit-row"><div class="top"><strong>' + esc(binding.channel + ' -> ' + binding.to) + '</strong>' +
585
+ '<span class="badge">' + esc(binding.status || 'active') + '</span></div>' +
586
+ '<div class="muted small">' + esc(fmtTs(binding.last_inbound_at || binding.updated_at)) + ' \xB7 owner=' + esc(binding.owner_ref || '(none)') + '</div>' +
587
+ '<div class="muted small" style="margin-top:8px">session=' + esc(binding.source_session_key || '(none)') + ' \xB7 conversation=' + esc(binding.source_conversation_id || '(none)') + '</div>' +
588
+ actions +
589
+ '</div>';
590
+ }).join('');
591
+ }
592
+
593
+ function renderHumanDeliveryCapabilities(humanDelivery) {
594
+ const capabilities = Array.isArray(humanDelivery && humanDelivery.channel_capabilities) ? humanDelivery.channel_capabilities : [];
595
+ if (!capabilities.length) {
596
+ return '<div class="empty">No channel capability data yet.</div>';
597
+ }
598
+ return capabilities.map(function (cap) {
599
+ const supportLabel = cap.supports_explicit_send ? 'explicit-send' : 'unsupported';
600
+ const canary = cap.last_canary_at
601
+ ? (' \xB7 canary=' + (cap.last_canary_ok ? 'ok' : 'failed') + ' @ ' + fmtTs(cap.last_canary_at))
602
+ : ' \xB7 canary=pending';
603
+ return '<div class="audit-row"><div class="top"><strong>' + esc(cap.channel) + '</strong><span class="badge">' + esc(supportLabel) + '</span></div>' +
604
+ '<div class="muted small">configured=' + esc(cap.configured ? 'true' : 'false') + ' \xB7 thread=' + esc(cap.supports_thread_id ? 'yes' : 'no') + ' \xB7 account=' + esc(cap.supports_account_id ? 'yes' : 'no') + ' \xB7 dry_run=' + esc(cap.supports_dry_run ? 'yes' : 'no') + esc(canary) + '</div>' +
605
+ (cap.last_canary_error ? '<div class="muted small" style="margin-top:8px;color:#fecaca">' + esc(cap.last_canary_error) + '</div>' : '') +
606
+ '</div>';
607
+ }).join('');
608
+ }
609
+
610
+ async function runHumanDeliveryAction(action, payload) {
611
+ return api('/api/runtime/human-delivery/action', {
612
+ method: 'POST',
613
+ headers: { 'Content-Type': 'application/json' },
614
+ body: JSON.stringify(Object.assign({ action: action }, payload || {})),
615
+ });
616
+ }
617
+
618
+ function wireHumanDeliveryActionButtons(root) {
619
+ if (!root || !root.querySelectorAll) return;
620
+ root.querySelectorAll('.human-delivery-action-btn').forEach(function (btn) {
621
+ btn.addEventListener('click', async function () {
622
+ const action = btn.getAttribute('data-action');
623
+ if (!action) return;
624
+ const intentId = btn.getAttribute('data-intent-id');
625
+ const bindingId = btn.getAttribute('data-binding-id');
626
+ await runHumanDeliveryAction(action, {
627
+ intent_id: intentId ? Number(intentId) : undefined,
628
+ binding_id: bindingId ? Number(bindingId) : undefined,
629
+ });
630
+ await refreshAll();
631
+ if (state.selectedSessionKey) await loadSession(state.selectedSessionKey);
632
+ setTab(state.tab || 'runtime');
633
+ });
634
+ });
635
+ }
636
+
637
+ async function markSeen(scopeType, scopeKey) {
638
+ return api('/api/runtime/seen', {
639
+ method: 'POST',
640
+ headers: { 'Content-Type': 'application/json' },
641
+ body: JSON.stringify({
642
+ operator_id: 'host_panel',
643
+ scope_type: scopeType,
644
+ scope_key: scopeType === 'session' ? scopeKey : null,
645
+ }),
646
+ });
647
+ }
648
+
649
+ async function maybeMarkGlobalSeen(tab, force) {
650
+ if (tab !== 'runtime' && tab !== 'decisions') return;
651
+ if (!force && seenMarkers.globalTab === tab) return;
652
+ try {
653
+ await markSeen('global', null);
654
+ seenMarkers.globalTab = tab;
655
+ } catch {}
656
+ }
657
+
658
+ async function maybeMarkSessionSeen(sessionKey, force) {
659
+ if (!sessionKey) return;
660
+ if (!force && seenMarkers.sessionKey === sessionKey) return;
661
+ try {
662
+ await markSeen('session', sessionKey);
663
+ seenMarkers.sessionKey = sessionKey;
664
+ } catch {}
665
+ }
666
+
432
667
  function syncUrlState() {
433
668
  const url = new URL(window.location.href);
434
669
  if (state.selectedProfile) url.searchParams.set('profile', state.selectedProfile);
@@ -512,6 +747,7 @@ function getHostPanelHtml() {
512
747
  }
513
748
 
514
749
  function setTab(tab) {
750
+ const previous = state.currentTab;
515
751
  state.currentTab = tab;
516
752
  document.getElementById('navRuntime').classList.toggle('active', tab === 'runtime');
517
753
  document.getElementById('navDecisions').classList.toggle('active', tab === 'decisions');
@@ -519,7 +755,16 @@ function getHostPanelHtml() {
519
755
  document.getElementById('runtimePanel').classList.toggle('active', tab === 'runtime');
520
756
  document.getElementById('decisionsPanel').classList.toggle('active', tab === 'decisions');
521
757
  document.getElementById('policyPanel').classList.toggle('active', tab === 'policy');
758
+ if (previous !== tab && tab !== 'runtime') {
759
+ seenMarkers.sessionKey = null;
760
+ }
522
761
  syncUrlState();
762
+ if (previous !== tab) {
763
+ void maybeMarkGlobalSeen(tab, true);
764
+ if (tab === 'runtime' && state.selectedSessionKey) {
765
+ void maybeMarkSessionSeen(state.selectedSessionKey, true);
766
+ }
767
+ }
523
768
  }
524
769
 
525
770
  function renderHeader() {
@@ -650,15 +895,24 @@ function getHostPanelHtml() {
650
895
  function renderDecisionInbox() {
651
896
  const data = state.decisions;
652
897
  const pendingDecisions = data && Array.isArray(data.pendingDecisions) ? data.pendingDecisions : [];
898
+ const overdueDecisions = pendingDecisions.filter(function (event) { return !!event.overdue; });
653
899
  const pendingOutbox = data && Array.isArray(data.projectionOutboxPending) ? data.projectionOutboxPending : [];
654
900
  const failedOutbox = data && Array.isArray(data.projectionOutboxFailed) ? data.projectionOutboxFailed : [];
655
- document.getElementById('decisionInboxSummary').textContent =
656
- 'pending=' + pendingDecisions.length + ' \xB7 projection_pending=' + pendingOutbox.length + ' \xB7 projection_failed=' + failedOutbox.length;
901
+ const pendingIntents = data && Array.isArray(data.notificationIntentsPending) ? data.notificationIntentsPending : [];
902
+ const unresolvedIntents = data && Array.isArray(data.notificationIntentsUnresolved) ? data.notificationIntentsUnresolved : [];
903
+ const failedIntents = data && Array.isArray(data.notificationIntentsFailed) ? data.notificationIntentsFailed : [];
904
+ const humanDelivery = data && data.humanDelivery ? data.humanDelivery : null;
905
+ const boundChannelReply = !!(humanDelivery && humanDelivery.mode === 'bound_channel_reply');
906
+ document.getElementById('decisionInboxSummary').textContent = boundChannelReply
907
+ ? ('pending=' + pendingDecisions.length + ' \xB7 overdue=' + overdueDecisions.length + ' \xB7 notify_pending=' + pendingIntents.length + ' \xB7 unresolved=' + unresolvedIntents.length + ' \xB7 notify_failed=' + failedIntents.length)
908
+ : ('pending=' + pendingDecisions.length + ' \xB7 overdue=' + overdueDecisions.length + ' \xB7 projection_pending=' + pendingOutbox.length + ' \xB7 projection_failed=' + failedOutbox.length);
657
909
  document.getElementById('decisionInboxList').innerHTML = pendingDecisions.length
658
910
  ? pendingDecisions.map(function (event) {
659
911
  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>' +
912
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end"><span class="badge">' + esc(event.severity || 'warning') + '</span>' +
913
+ (event.overdue ? '<span class="badge alert">overdue</span>' : '') + '</div></div>' +
914
+ '<div class="muted small">' + esc(fmtTs(event.ts_ms)) + ' \xB7 session=' + esc(event.session_key || '(none)') + ' \xB7 target=' + esc(event.target_human_session || '(none)') +
915
+ (event.overdue ? ' \xB7 overdue_by=' + esc(Math.ceil((event.overdue_by_ms || 0) / 60000)) + 'm' : '') + '</div>' +
662
916
  '<div class="muted small" style="margin-top:8px">detail_ref=' + esc(event.conversation_id || '(none)') + '</div>' +
663
917
  '<div class="row-actions">' +
664
918
  '<button class="action-btn inbox-decision-btn" data-event-id="' + esc(event.id) + '" data-decision="approved">Approve</button>' +
@@ -670,20 +924,26 @@ function getHostPanelHtml() {
670
924
  : '<div class="empty">No pending collaboration decisions.</div>';
671
925
 
672
926
  const combinedOutbox = pendingOutbox.concat(failedOutbox);
673
- document.getElementById('projectionOutboxSummary').textContent =
674
- combinedOutbox.length
927
+ document.getElementById('projectionOutboxSummary').textContent = boundChannelReply
928
+ ? ('bound reply mode \xB7 pending=' + pendingIntents.length + ' \xB7 unresolved=' + unresolvedIntents.length + ' \xB7 failed=' + failedIntents.length)
929
+ : (combinedOutbox.length
675
930
  ? 'Human-thread projection uses a stable outbox. Failed rows stay visible until delivery recovers.'
676
- : 'No undelivered human-thread projections.';
677
- document.getElementById('projectionOutboxList').innerHTML = combinedOutbox.length
678
- ? combinedOutbox.map(function (entry) {
679
- return '<div class="audit-row"><div class="top"><strong>' + esc(entry.summary || '(no summary)') + '</strong>' +
680
- '<span class="badge">' + esc(entry.status || 'pending') + '</span></div>' +
681
- '<div class="muted small">' + esc(fmtTs(entry.created_at)) + ' \xB7 kind=' + esc(entry.projection_kind || 'collaboration_update') + ' \xB7 attempts=' + esc(entry.attempt_count || 0) + '</div>' +
682
- '<div class="muted small" style="margin-top:8px">target=' + esc(entry.target_human_session || '(missing)') + ' \xB7 session=' + esc(entry.session_key || '(none)') + '</div>' +
683
- (entry.last_error ? '<div style="margin-top:8px">' + esc(entry.last_error) + '</div>' : '') +
684
- '</div>';
685
- }).join('')
686
- : '<div class="empty">No pending or failed projection deliveries.</div>';
931
+ : 'No undelivered human-thread projections.');
932
+ document.getElementById('projectionOutboxList').innerHTML = boundChannelReply
933
+ ? renderHumanDeliveryIntents(
934
+ pendingIntents.concat(unresolvedIntents).concat(failedIntents),
935
+ 'No pending, unresolved, or failed human-delivery intents.',
936
+ )
937
+ : (combinedOutbox.length
938
+ ? combinedOutbox.map(function (entry) {
939
+ return '<div class="audit-row"><div class="top"><strong>' + esc(entry.summary || '(no summary)') + '</strong>' +
940
+ '<span class="badge">' + esc(entry.status || 'pending') + '</span></div>' +
941
+ '<div class="muted small">' + esc(fmtTs(entry.created_at)) + ' \xB7 kind=' + esc(entry.projection_kind || 'collaboration_update') + ' \xB7 attempts=' + esc(entry.attempt_count || 0) + '</div>' +
942
+ '<div class="muted small" style="margin-top:8px">target=' + esc(entry.target_human_session || '(missing)') + ' \xB7 session=' + esc(entry.session_key || '(none)') + '</div>' +
943
+ (entry.last_error ? '<div style="margin-top:8px">' + esc(entry.last_error) + '</div>' : '') +
944
+ '</div>';
945
+ }).join('')
946
+ : '<div class="empty">No pending or failed projection deliveries.</div>');
687
947
 
688
948
  document.querySelectorAll('.inbox-decision-btn').forEach(function (btn) {
689
949
  btn.addEventListener('click', async function () {
@@ -710,6 +970,7 @@ function getHostPanelHtml() {
710
970
  setTab('runtime');
711
971
  });
712
972
  });
973
+ wireHumanDeliveryActionButtons(document.getElementById('projectionOutboxList'));
713
974
  }
714
975
 
715
976
  function getOverviewSessions() {
@@ -747,6 +1008,12 @@ function getHostPanelHtml() {
747
1008
  if (!overview) return;
748
1009
  syncSelectedSessionFromOverview();
749
1010
  const ingressState = ingressStatusModel(overview);
1011
+ const transportHealth = overview.transportHealth || null;
1012
+ const sinceLastSeen = overview.sinceLastSeen || null;
1013
+ const humanDelivery = overview.humanDelivery || null;
1014
+ const overdueDecisions = Array.isArray(overview.pendingDecisions)
1015
+ ? overview.pendingDecisions.filter(function (event) { return !!event.overdue; })
1016
+ : [];
750
1017
  document.getElementById('activationCard').innerHTML =
751
1018
  '<div class="status-strip">' +
752
1019
  '<div class="status-main">' +
@@ -754,6 +1021,12 @@ function getHostPanelHtml() {
754
1021
  '<div class="status-state ' + ingressState.className + '">' + esc(ingressState.label) + '</div>' +
755
1022
  '<div class="muted small">' + esc(ingressState.detail) + '</div>' +
756
1023
  '<div class="muted small">Public link: ' + esc(overview.publicSelf && overview.publicSelf.public_url ? overview.publicSelf.public_url : '(not ready yet)') + '</div>' +
1024
+ '<div class="summary-pills">' +
1025
+ '<span class="pill">pending_decisions=' + esc((overview.pendingDecisions || []).length) + '</span>' +
1026
+ '<span class="pill">overdue=' + esc(overdueDecisions.length) + '</span>' +
1027
+ '<span class="pill">projection=' + esc(overview.collaborationProjection && overview.collaborationProjection.preset ? overview.collaborationProjection.preset : 'balanced') + '</span>' +
1028
+ (humanDelivery ? '<span class="pill">human_delivery=' + esc(humanDelivery.mode || 'projection_outbox') + '</span>' : '') +
1029
+ '</div>' +
757
1030
  '</div>' +
758
1031
  '<div style="min-width:320px">' +
759
1032
  '<div class="label">Quick Start</div>' +
@@ -768,6 +1041,8 @@ function getHostPanelHtml() {
768
1041
  '</div>' +
769
1042
  '</div>' +
770
1043
  '</div>';
1044
+ document.getElementById('transportHealthCard').innerHTML = renderTransportHealthCard(transportHealth);
1045
+ document.getElementById('sinceLastSeenCard').innerHTML = renderSinceLastSeenSummary(sinceLastSeen);
771
1046
  const subscription = overview.subscription || null;
772
1047
  const stats = [
773
1048
  { label: 'Plan', value: subscription ? subscription.tier : 'ghost', sub: subscription ? subscription.summary : 'subscription unavailable' },
@@ -779,12 +1054,16 @@ function getHostPanelHtml() {
779
1054
  { label: 'Tasks', value: overview.tasksTotal, sub: 'recent local task threads' },
780
1055
  { label: 'Audit', value: overview.auditSummary.total_events, sub: 'policy / runtime audit events' },
781
1056
  { label: 'Collaboration', value: overview.collaborationSummary ? overview.collaborationSummary.total_events : 0, sub: overview.collaborationSummary ? ('pending_review=' + overview.collaborationSummary.pending_approvals) : 'projected external collaboration events' },
1057
+ { label: 'Human Delivery', value: humanDelivery ? ((humanDelivery.pending_intents || 0) + '/' + (humanDelivery.active_bindings || 0)) : '-', sub: humanDelivery ? ('mode=' + (humanDelivery.mode || 'projection_outbox') + ' unresolved=' + (humanDelivery.unresolved_intents || 0) + ' failed=' + (humanDelivery.failed_intents || 0)) : 'binding-based human notification state' },
782
1058
  { label: 'Recommendations', value: overview.recommendationSummary ? overview.recommendationSummary.total : overview.recommendations.length, sub: overview.recommendationSummary ? JSON.stringify(overview.recommendationSummary.by_status || {}) : 'learned policy suggestions' },
783
1059
  { label: 'Public Link', value: overview.publicSelf && overview.publicSelf.public_url ? 'ready' : 'disabled', sub: overview.publicSelf && overview.publicSelf.public_url ? overview.publicSelf.public_url : 'create a hosted shareable profile link' },
784
1060
  ];
785
1061
  document.getElementById('statsGrid').innerHTML = stats.map(function (item) {
786
1062
  return '<div class="card"><div class="label">' + esc(item.label) + '</div><div class="value">' + esc(item.value) + '</div><div class="muted small">' + esc(item.sub) + '</div></div>';
787
1063
  }).join('');
1064
+ document.getElementById('taskList').innerHTML = '<div class="task-row"><div class="top"><strong>Human Delivery Channels</strong><span class="badge">' + esc((humanDelivery && humanDelivery.supported_channels ? humanDelivery.supported_channels.length : 0) + '/' + (humanDelivery && humanDelivery.channel_capabilities ? humanDelivery.channel_capabilities.length : 0)) + '</span></div>' +
1065
+ '<div class="muted small">Capabilities and canary state for explicit human-delivery replies.</div>' +
1066
+ '<div class="audit-list" style="margin-top:8px">' + renderHumanDeliveryCapabilities(humanDelivery) + '</div></div>';
788
1067
 
789
1068
  const toggleUnreadBtn = document.getElementById('toggleUnreadBtn');
790
1069
  if (toggleUnreadBtn) toggleUnreadBtn.textContent = 'Unread only: ' + (state.showUnreadOnly ? 'on' : 'off');
@@ -796,6 +1075,9 @@ function getHostPanelHtml() {
796
1075
  const active = session.session_key === state.selectedSessionKey ? ' active' : '';
797
1076
  const badges = [
798
1077
  '<span class="badge ' + esc(session.trust_state) + '">' + esc(session.trust_state) + '</span>',
1078
+ (countSinceLastSeen(session.since_last_seen) > 0
1079
+ ? '<span class="badge alert">new ' + esc(countSinceLastSeen(session.since_last_seen)) + '</span>'
1080
+ : ''),
799
1081
  session.binding_alert
800
1082
  ? '<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
1083
  : '',
@@ -833,8 +1115,27 @@ function getHostPanelHtml() {
833
1115
  });
834
1116
  }
835
1117
 
1118
+ document.querySelectorAll('.transport-switch-btn').forEach(function (btn) {
1119
+ btn.addEventListener('click', async function () {
1120
+ const mode = btn.getAttribute('data-mode');
1121
+ if (mode !== 'bridge' && mode !== 'channel') return;
1122
+ const confirmed = window.confirm('Switch preferred transport to ' + mode + ' and request a managed restart?');
1123
+ if (!confirmed) return;
1124
+ const result = await api('/api/runtime/transport-switch', {
1125
+ method: 'POST',
1126
+ headers: { 'Content-Type': 'application/json' },
1127
+ body: JSON.stringify({ mode: mode }),
1128
+ });
1129
+ window.alert(result && result.result && result.result.restarted
1130
+ ? ('Preferred transport switched to ' + mode + '. Managed restart was attempted.')
1131
+ : ('Preferred transport switched to ' + mode + '. Restart is still required.'));
1132
+ await refreshAll();
1133
+ setTab('runtime');
1134
+ });
1135
+ });
1136
+
836
1137
  const tasks = Array.isArray(overview.tasks) ? overview.tasks : [];
837
- document.getElementById('taskList').innerHTML = tasks.length
1138
+ document.getElementById('taskList').innerHTML += tasks.length
838
1139
  ? tasks.map(function (task) {
839
1140
  return '<div class="task-row"><div class="top"><strong>' + esc(task.title || task.task_id) + '</strong><span class="badge">' + esc(task.status) + '</span></div>' +
840
1141
  '<div class="muted small">task_id=' + esc(task.task_id) + ' \xB7 session=' + esc(task.session_key) + '</div>' +
@@ -843,7 +1144,7 @@ function getHostPanelHtml() {
843
1144
  (task.error_message ? '<div style="margin-top:8px;color:#fca5a5">' + esc(task.error_message) + '</div>' : '') +
844
1145
  renderHandoffBlock(task.handoff) +
845
1146
  '</div>';
846
- }).join('')
1147
+ }).join('')
847
1148
  : '<div class="empty">No recent task threads.</div>';
848
1149
 
849
1150
  if (subscription) {
@@ -937,6 +1238,13 @@ conversation=' + result.conversation_id));
937
1238
  ? detail.collaborationProjection.preset
938
1239
  : 'balanced';
939
1240
  const projectionOutbox = Array.isArray(detail.projectionOutbox) ? detail.projectionOutbox : [];
1241
+ const recentBindings = Array.isArray(detail.recentBindings) ? detail.recentBindings : [];
1242
+ const recentNotificationIntents = Array.isArray(detail.recentNotificationIntents) ? detail.recentNotificationIntents : [];
1243
+ const sinceLastSeen = detail.sinceLastSeen || null;
1244
+ const deliveryTimeline = Array.isArray(detail.deliveryTimeline) ? detail.deliveryTimeline : [];
1245
+ const projectionPreview = detail.projectionPreview || null;
1246
+ const transportHealth = detail.transportHealth || null;
1247
+ const humanDelivery = detail.humanDelivery || null;
940
1248
  const isAdvanced = state.detailMode === 'advanced';
941
1249
  const sessionLink = buildSessionLink(session.session_key);
942
1250
  const summaryPills = [];
@@ -972,7 +1280,8 @@ conversation=' + result.conversation_id));
972
1280
  : (reopenRecommendation ? '<div class="muted small" style="margin-top:8px">trust_action=' + esc(recommendationActionLabel(reopenRecommendation)) + '</div>' : '')) +
973
1281
  (summaryPills.length ? '<div class="summary-pills">' + summaryPills.join('') + '</div>' : '') +
974
1282
  (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>'
1283
+ ? '<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.' +
1284
+ (pendingCollaborationEvents[0].overdue ? ' This item is overdue.' : '') + '</div></div>'
976
1285
  : '') +
977
1286
  (bindingAlert
978
1287
  ? '<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 +1393,49 @@ conversation=' + result.conversation_id));
1084
1393
  '</div></div>' +
1085
1394
  '<div><div class="label">Human Thread Posture</div><div class="audit-list" style="margin-top:8px">' +
1086
1395
  '<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>' +
1396
+ '<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>' +
1397
+ '<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>' +
1398
+ (sinceLastSeen
1399
+ ? '<div class="summary-pills" style="margin-top:8px">' +
1400
+ '<span class="pill">new=' + esc(countSinceLastSeen(sinceLastSeen)) + '</span>' +
1401
+ '<span class="pill">decisions=' + esc(sinceLastSeen.new_decisions || 0) + '</span>' +
1402
+ '<span class="pill">failures=' + esc(sinceLastSeen.new_failures || 0) + '</span>' +
1403
+ '</div>'
1404
+ : '') +
1405
+ (transportHealth
1406
+ ? '<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>'
1407
+ : '') +
1408
+ (humanDelivery
1409
+ ? '<div class="muted small" style="margin-top:8px">human_delivery=' + esc(humanDelivery.mode || 'projection_outbox') + ' \xB7 unresolved=' + esc(humanDelivery.unresolved_intents || 0) + ' \xB7 failed=' + esc(humanDelivery.failed_intents || 0) + '</div>'
1410
+ : '') +
1089
1411
  (projectionOutbox.length
1090
1412
  ? '<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
1413
  : '<div class="muted small" style="margin-top:8px">outbox=clear</div>') +
1414
+ '<div style="margin-top:12px"><div class="label">Projection Preview</div><div style="margin-top:8px">' + renderProjectionPreviewBlock(projectionPreview) + '</div></div>' +
1092
1415
  '</div>' +
1093
1416
  '</div></div>' +
1094
1417
  '</div>';
1095
1418
 
1419
+ el.innerHTML +=
1420
+ '<div class="grid two-col" style="margin-top:16px">' +
1421
+ '<div><div class="label">Since Last Seen</div><div class="audit-list" style="margin-top:8px">' +
1422
+ '<div class="audit-row">' + renderSinceLastSeenSummary(sinceLastSeen) + '</div>' +
1423
+ '</div></div>' +
1424
+ '<div><div class="label">Delivery Timeline</div><div class="audit-list" style="margin-top:8px">' +
1425
+ renderDeliveryTimeline(deliveryTimeline) +
1426
+ '</div></div>' +
1427
+ '</div>';
1428
+
1429
+ el.innerHTML +=
1430
+ '<div class="grid two-col" style="margin-top:16px">' +
1431
+ '<div><div class="label">Human Reply Targets</div><div class="audit-list" style="margin-top:8px">' +
1432
+ renderRecentBindings(recentBindings) +
1433
+ '</div></div>' +
1434
+ '<div><div class="label">Notification Intents</div><div class="audit-list" style="margin-top:8px">' +
1435
+ renderHumanDeliveryIntents(recentNotificationIntents, 'No notification intents for this session yet.') +
1436
+ '</div></div>' +
1437
+ '</div>';
1438
+
1096
1439
  el.querySelectorAll('.approve-session-btn').forEach(function (btn) {
1097
1440
  btn.addEventListener('click', async function () {
1098
1441
  const sessionKey = btn.getAttribute('data-session');
@@ -1233,6 +1576,7 @@ conversation=' + result.conversation_id));
1233
1576
  setTab('runtime');
1234
1577
  });
1235
1578
  }
1579
+ wireHumanDeliveryActionButtons(el);
1236
1580
  }
1237
1581
 
1238
1582
  async function promptBindCurrentChat(conversationId, previousBinding, remoteDid) {
@@ -1272,6 +1616,7 @@ Previous chat link: ' + previous
1272
1616
  policy.doc && policy.doc.collaboration_projection && policy.doc.collaboration_projection.preset
1273
1617
  ? policy.doc.collaboration_projection.preset
1274
1618
  : 'balanced';
1619
+ document.getElementById('projectionPolicyPreview').innerHTML = renderProjectionPreviewBlock(state.session && state.session.projectionPreview ? state.session.projectionPreview : null);
1275
1620
  document.getElementById('contactDefault').value = policy.doc.contact_policy.default_action;
1276
1621
  document.getElementById('taskDefault').value = policy.doc.task_policy.default_action;
1277
1622
  document.getElementById('profileDisplayName').value = profile && profile.display_name ? profile.display_name : '';
@@ -1443,6 +1788,7 @@ Previous chat link: ' + previous
1443
1788
  syncSelectedSessionFromOverview();
1444
1789
  renderHeader();
1445
1790
  renderOverview();
1791
+ if (state.currentTab === 'runtime') await maybeMarkGlobalSeen('runtime');
1446
1792
  if (state.selectedSessionKey) {
1447
1793
  await loadSession(state.selectedSessionKey);
1448
1794
  } else {
@@ -1454,10 +1800,14 @@ Previous chat link: ' + previous
1454
1800
 
1455
1801
  async function loadSession(sessionKey) {
1456
1802
  if (!sessionKey) return;
1803
+ const previousSessionKey = state.selectedSessionKey;
1457
1804
  state.selectedSessionKey = sessionKey;
1458
1805
  state.session = await api('/api/runtime/session?session_key=' + encodeURIComponent(sessionKey));
1459
1806
  syncUrlState();
1460
1807
  renderSession();
1808
+ if (state.currentTab === 'runtime') {
1809
+ await maybeMarkSessionSeen(sessionKey, previousSessionKey !== sessionKey);
1810
+ }
1461
1811
  }
1462
1812
 
1463
1813
  async function loadPolicy() {
@@ -1468,6 +1818,7 @@ Previous chat link: ' + previous
1468
1818
  async function loadDecisions() {
1469
1819
  state.decisions = await api('/api/runtime/collaboration-decisions');
1470
1820
  renderDecisionInbox();
1821
+ if (state.currentTab === 'decisions') await maybeMarkGlobalSeen('decisions');
1471
1822
  }
1472
1823
 
1473
1824
  async function refreshAll() {
@@ -1897,6 +2248,50 @@ function readProjectionOutboxState(storePath, limit = 20) {
1897
2248
  store.close();
1898
2249
  }
1899
2250
  }
2251
+ function readTransportHealthState(storePath) {
2252
+ const store = new LocalStore(storePath);
2253
+ try {
2254
+ const eventManager = new CollaborationEventManager(store);
2255
+ const outboxManager = new CollaborationProjectionOutboxManager(store);
2256
+ const runtimeStatus = readIngressRuntimeStatus();
2257
+ const preference = readTransportPreference();
2258
+ const normalizedStatus = runtimeStatus ? {
2259
+ ...runtimeStatus,
2260
+ preferred_transport_mode: preference?.preferred_mode ?? runtimeStatus.preferred_transport_mode ?? "bridge"
2261
+ } : {
2262
+ receive_mode: "webhook",
2263
+ transport_mode: preference?.preferred_mode ?? "bridge",
2264
+ preferred_transport_mode: preference?.preferred_mode ?? "bridge"
2265
+ };
2266
+ return deriveTransportHealth({
2267
+ runtime_status: normalizedStatus,
2268
+ recent_events: eventManager.listRecent(30),
2269
+ projection_outbox_failed: outboxManager.listByStatus("failed", 20)
2270
+ });
2271
+ } finally {
2272
+ store.close();
2273
+ }
2274
+ }
2275
+ function readSinceLastSeenState(storePath, operatorId, scopeType, scopeKey) {
2276
+ const store = new LocalStore(storePath);
2277
+ try {
2278
+ return summarizeSinceLastSeen(store, {
2279
+ operator_id: operatorId,
2280
+ scope_type: scopeType,
2281
+ scope_key: scopeKey
2282
+ });
2283
+ } finally {
2284
+ store.close();
2285
+ }
2286
+ }
2287
+ function readHumanDeliveryState(storePath, limit = 12) {
2288
+ const store = new LocalStore(storePath);
2289
+ try {
2290
+ return summarizeHumanDelivery(store, limit);
2291
+ } finally {
2292
+ store.close();
2293
+ }
2294
+ }
1900
2295
  function buildPolicyDecisionShape(identityPath, remoteDid, opts) {
1901
2296
  const policy = readTrustPolicyDoc(identityPath);
1902
2297
  const runtimeMode = opts?.runtimeMode ?? getRuntimeMode();
@@ -2009,6 +2404,25 @@ async function buildRuntimeOverviewPayload(ctx) {
2009
2404
  }, {});
2010
2405
  const collaborationSummary = collaborationEventManager.summarize(200);
2011
2406
  const projectionOutbox = readProjectionOutboxState(ctx.storePath, 20);
2407
+ const transportHealth = readTransportHealthState(ctx.storePath);
2408
+ const sinceLastSeen = readSinceLastSeenState(ctx.storePath, "host_panel", "global");
2409
+ const humanDelivery = readHumanDeliveryState(ctx.storePath, 20);
2410
+ const decisionStore = new LocalStore(ctx.storePath);
2411
+ let decisionViews = [];
2412
+ let humanDeliveryDetails = null;
2413
+ try {
2414
+ decisionViews = listPendingDecisionViews(decisionStore, 100);
2415
+ const bindingManager = new HumanDeliveryBindingManager(decisionStore);
2416
+ const intentManager = new NotificationIntentManager(decisionStore);
2417
+ humanDeliveryDetails = {
2418
+ unresolved_intents: intentManager.listByStatus("unresolved", 20),
2419
+ failed_intents: intentManager.listByStatus("failed", 20),
2420
+ acknowledged_intents: intentManager.listByStatus("acknowledged", 20),
2421
+ recent_bindings: bindingManager.listRecent(20)
2422
+ };
2423
+ } finally {
2424
+ decisionStore.close();
2425
+ }
2012
2426
  return {
2013
2427
  did: ctx.myDid,
2014
2428
  serverUrl: ctx.serverUrl,
@@ -2046,6 +2460,11 @@ async function buildRuntimeOverviewPayload(ctx) {
2046
2460
  recommendationSummary: recommendationState.summary,
2047
2461
  collaborationSummary,
2048
2462
  projectionOutbox,
2463
+ transportHealth,
2464
+ sinceLastSeen,
2465
+ humanDelivery,
2466
+ humanDeliveryDetails,
2467
+ pendingDecisions: decisionViews,
2049
2468
  recentCollaborationEvents: collaborationEventManager.listRecent(20),
2050
2469
  sessions: sessions.map((session) => ({
2051
2470
  ...session,
@@ -2054,7 +2473,8 @@ async function buildRuntimeOverviewPayload(ctx) {
2054
2473
  binding_alert: session.conversation_id ? bindingAlertByConversation.get(session.conversation_id) ?? null : null,
2055
2474
  is_active_work_session: session.session_key === activeWorkSession,
2056
2475
  latest_messages: session.conversation_id ? historyManager.listRecent(session.conversation_id, 3) : [],
2057
- collaboration_events: collaborationEventManager.listBySession(session.session_key, 3)
2476
+ collaboration_events: collaborationEventManager.listBySession(session.session_key, 3),
2477
+ since_last_seen: readSinceLastSeenState(ctx.storePath, "host_panel", "session", session.session_key)
2058
2478
  })),
2059
2479
  tasks: refreshedTasks.map((task) => ({
2060
2480
  ...task,
@@ -2111,8 +2531,31 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
2111
2531
  });
2112
2532
  const outboxStore = new LocalStore(ctx.storePath);
2113
2533
  let projectionOutbox = [];
2534
+ let notificationIntents = [];
2535
+ let recentBindings = [];
2536
+ let sinceLastSeen = null;
2537
+ let deliveryTimeline = [];
2538
+ let projectionPreview = null;
2539
+ let pendingDecisionViews = [];
2540
+ let humanDelivery = null;
2114
2541
  try {
2115
2542
  projectionOutbox = new CollaborationProjectionOutboxManager(outboxStore).listBySession(session.session_key, 20);
2543
+ notificationIntents = new NotificationIntentManager(outboxStore).listBySession(session.session_key, 20);
2544
+ recentBindings = listRecentBindingsForSession(outboxStore, session.session_key, session.conversation_id, 20);
2545
+ sinceLastSeen = summarizeSinceLastSeen(outboxStore, {
2546
+ operator_id: "host_panel",
2547
+ scope_type: "session",
2548
+ scope_key: session.session_key
2549
+ });
2550
+ deliveryTimeline = buildDeliveryTimeline(outboxStore, session.session_key, 40);
2551
+ projectionPreview = buildProjectionPreview(
2552
+ outboxStore,
2553
+ session.session_key,
2554
+ policy.collaboration_projection.preset,
2555
+ 5
2556
+ );
2557
+ pendingDecisionViews = listPendingDecisionViews(outboxStore, 100).filter((event) => event.session_key === session.session_key).slice(0, 20);
2558
+ humanDelivery = summarizeHumanDelivery(outboxStore, 20);
2116
2559
  } finally {
2117
2560
  outboxStore.close();
2118
2561
  }
@@ -2120,9 +2563,10 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
2120
2563
  session,
2121
2564
  sessionSummary: sessionSummaryManager.get(session.session_key),
2122
2565
  collaborationEvents: collaborationEventManager.listBySession(session.session_key, 40),
2123
- pendingCollaborationEvents: collaborationEventManager.listPendingBySession(session.session_key, 20),
2566
+ pendingCollaborationEvents: pendingDecisionViews,
2124
2567
  collaborationSummary: collaborationEventManager.summarize(200),
2125
2568
  ingressRuntime: readIngressRuntimeStatus(),
2569
+ transportHealth: readTransportHealthState(ctx.storePath),
2126
2570
  binding,
2127
2571
  bindingAlert,
2128
2572
  activeWorkSession,
@@ -2131,6 +2575,12 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
2131
2575
  sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
2132
2576
  collaborationProjection: policy.collaboration_projection,
2133
2577
  projectionOutbox,
2578
+ recentBindings,
2579
+ recentNotificationIntents: notificationIntents,
2580
+ sinceLastSeen,
2581
+ deliveryTimeline,
2582
+ projectionPreview,
2583
+ humanDelivery,
2134
2584
  policyExplain: buildPolicyDecisionShape(ctx.identityPath, session.remote_did, { runtimeMode: getRuntimeMode() }),
2135
2585
  tasks: tasks.map((task) => ({
2136
2586
  ...task,
@@ -2207,6 +2657,121 @@ async function handleApi(pathname, req, ctx) {
2207
2657
  status: readIngressRuntimeStatus()
2208
2658
  };
2209
2659
  }
2660
+ if (parts[1] === "transport-health" && req.method === "GET") {
2661
+ return {
2662
+ transportHealth: readTransportHealthState(ctx.storePath),
2663
+ transportPreference: readTransportPreference()
2664
+ };
2665
+ }
2666
+ if (parts[1] === "human-delivery") {
2667
+ const store = new LocalStore(ctx.storePath);
2668
+ try {
2669
+ const bindingManager = new HumanDeliveryBindingManager(store);
2670
+ const intentManager = new NotificationIntentManager(store);
2671
+ if (req.method === "GET") {
2672
+ return {
2673
+ humanDelivery: summarizeHumanDelivery(store, 20),
2674
+ recentBindings: bindingManager.listRecent(50),
2675
+ unresolvedIntents: intentManager.listByStatus("unresolved", 50),
2676
+ failedIntents: intentManager.listByStatus("failed", 50),
2677
+ acknowledgedIntents: intentManager.listByStatus("acknowledged", 50)
2678
+ };
2679
+ }
2680
+ if (req.method === "POST" && parts[2] === "action") {
2681
+ const body = await readBody(req);
2682
+ const action = String(body?.action ?? "").trim();
2683
+ const intentId = Number(body?.intent_id);
2684
+ const bindingId = Number(body?.binding_id);
2685
+ let intent = null;
2686
+ let binding = null;
2687
+ if (action === "retry_intent") {
2688
+ if (!Number.isInteger(intentId) || intentId <= 0) throw new Error("intent_id is required");
2689
+ intent = intentManager.markPending(intentId, { clear_error: true, clear_ack: false, clear_binding: false });
2690
+ } else if (action === "re_resolve_intent_binding") {
2691
+ if (!Number.isInteger(intentId) || intentId <= 0) throw new Error("intent_id is required");
2692
+ intent = intentManager.markPending(intentId, { clear_error: true, clear_ack: false, clear_binding: true });
2693
+ } else if (action === "cancel_intent") {
2694
+ if (!Number.isInteger(intentId) || intentId <= 0) throw new Error("intent_id is required");
2695
+ intent = intentManager.markCanceled(intentId, "manual_cancel");
2696
+ } else if (action === "mark_binding_stale") {
2697
+ if (!Number.isInteger(bindingId) || bindingId <= 0) throw new Error("binding_id is required");
2698
+ binding = bindingManager.markStale(bindingId);
2699
+ } else {
2700
+ throw new Error("action must be retry_intent, re_resolve_intent_binding, cancel_intent, or mark_binding_stale");
2701
+ }
2702
+ return {
2703
+ ok: true,
2704
+ action,
2705
+ intent,
2706
+ binding,
2707
+ humanDelivery: summarizeHumanDelivery(store, 20),
2708
+ unresolvedIntents: intentManager.listByStatus("unresolved", 50),
2709
+ failedIntents: intentManager.listByStatus("failed", 50),
2710
+ acknowledgedIntents: intentManager.listByStatus("acknowledged", 50)
2711
+ };
2712
+ }
2713
+ } finally {
2714
+ store.close();
2715
+ }
2716
+ }
2717
+ if (parts[1] === "transport-switch" && req.method === "POST") {
2718
+ const body = await readBody(req);
2719
+ const mode = String(body?.mode ?? "").trim().toLowerCase();
2720
+ if (mode !== "bridge" && mode !== "channel") throw new Error("mode must be bridge or channel");
2721
+ const result = switchTransportPreference(mode, {
2722
+ updated_by: "host_panel"
2723
+ });
2724
+ const store = new LocalStore(ctx.storePath);
2725
+ try {
2726
+ new CollaborationEventManager(store).record({
2727
+ event_type: "transport_switched",
2728
+ severity: result.restarted ? "notice" : "warning",
2729
+ summary: result.restarted ? `Preferred transport switched to ${mode}. A managed restart was attempted.` : `Preferred transport switched to ${mode}. Restart is still required.`,
2730
+ detail: {
2731
+ preferred_mode: result.preferred_mode,
2732
+ restart_required: result.restart_required,
2733
+ restart_method: result.restart_method ?? null,
2734
+ restart_error: result.restart_error ?? null,
2735
+ preference_path: result.preference_path
2736
+ }
2737
+ });
2738
+ } finally {
2739
+ store.close();
2740
+ }
2741
+ return {
2742
+ ok: true,
2743
+ result,
2744
+ transportHealth: readTransportHealthState(ctx.storePath)
2745
+ };
2746
+ }
2747
+ if (parts[1] === "seen" && req.method === "POST") {
2748
+ const body = await readBody(req);
2749
+ const operatorId = String(body?.operator_id ?? "host_panel").trim();
2750
+ const scopeType = String(body?.scope_type ?? "global").trim();
2751
+ const scopeKey = typeof body?.scope_key === "string" ? body.scope_key.trim() : void 0;
2752
+ if (!["host_panel", "tui", "mcp"].includes(operatorId)) throw new Error("operator_id must be host_panel, tui, or mcp");
2753
+ if (scopeType !== "global" && scopeType !== "session") throw new Error("scope_type must be global or session");
2754
+ const store = new LocalStore(ctx.storePath);
2755
+ try {
2756
+ const seen = new OperatorSeenStateManager(store).markSeen({
2757
+ operator_id: operatorId,
2758
+ scope_type: scopeType,
2759
+ scope_key: scopeType === "session" ? scopeKey : null,
2760
+ last_seen_ts: Date.now()
2761
+ });
2762
+ return {
2763
+ ok: true,
2764
+ seen,
2765
+ summary: summarizeSinceLastSeen(store, {
2766
+ operator_id: operatorId,
2767
+ scope_type: scopeType,
2768
+ scope_key: scopeType === "session" ? scopeKey : null
2769
+ })
2770
+ };
2771
+ } finally {
2772
+ store.close();
2773
+ }
2774
+ }
2210
2775
  if (parts[1] === "demo" && req.method === "POST") {
2211
2776
  const body = await readBody(req);
2212
2777
  const preset = typeof body?.preset === "string" ? body.preset.trim().toLowerCase() : "";
@@ -2262,11 +2827,16 @@ async function handleApi(pathname, req, ctx) {
2262
2827
  const manager = new CollaborationEventManager(store);
2263
2828
  const outboxManager = new CollaborationProjectionOutboxManager(store);
2264
2829
  if (req.method === "GET") {
2265
- const pendingDecisions = manager.listPending(100);
2830
+ const pendingDecisions = listPendingDecisionViews(store, 100);
2831
+ const intentManager = new NotificationIntentManager(store);
2266
2832
  return {
2267
2833
  pendingDecisions,
2268
2834
  projectionOutboxPending: outboxManager.listByStatus("pending", 50),
2269
- projectionOutboxFailed: outboxManager.listByStatus("failed", 50)
2835
+ projectionOutboxFailed: outboxManager.listByStatus("failed", 50),
2836
+ notificationIntentsPending: intentManager.listByStatus("pending", 50).concat(intentManager.listByStatus("bound", 50)),
2837
+ notificationIntentsUnresolved: intentManager.listByStatus("unresolved", 50),
2838
+ notificationIntentsFailed: intentManager.listByStatus("failed", 50),
2839
+ humanDelivery: summarizeHumanDelivery(store, 20)
2270
2840
  };
2271
2841
  }
2272
2842
  if (parts[2] === "resolve" && req.method === "POST") {