@pingagent/sdk 0.1.15 → 0.1.17

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,7 +2,9 @@ import {
2
2
  CollaborationEventManager,
3
3
  CollaborationProjectionOutboxManager,
4
4
  ContactManager,
5
+ HumanDeliveryBindingManager,
5
6
  LocalStore,
7
+ NotificationIntentManager,
6
8
  OperatorSeenStateManager,
7
9
  PingAgentClient,
8
10
  TrustPolicyAuditManager,
@@ -12,6 +14,7 @@ import {
12
14
  decideContactPolicy,
13
15
  decideTaskPolicy,
14
16
  defaultTrustPolicyDoc,
17
+ deriveOpenClawAgentState,
15
18
  deriveTransportHealth,
16
19
  ensureTokenValid,
17
20
  getActiveSessionFilePath,
@@ -19,6 +22,7 @@ import {
19
22
  getSessionMapFilePath,
20
23
  getTrustRecommendationActionLabel,
21
24
  listPendingDecisionViews,
25
+ listRecentBindingsForSession,
22
26
  loadIdentity,
23
27
  normalizeTrustPolicyDoc,
24
28
  readCurrentActiveSessionKey,
@@ -28,12 +32,13 @@ import {
28
32
  readTransportPreference,
29
33
  removeSessionBinding,
30
34
  setSessionBinding,
35
+ summarizeHumanDelivery,
31
36
  summarizeSinceLastSeen,
32
37
  summarizeTrustPolicyAudit,
33
38
  switchTransportPreference,
34
39
  updateStoredToken,
35
40
  upsertTrustPolicyRecommendation
36
- } from "./chunk-BCYHGKQE.js";
41
+ } from "./chunk-IB7OSFZS.js";
37
42
 
38
43
  // src/web-server.ts
39
44
  import * as fs from "fs";
@@ -259,8 +264,8 @@ function getHostPanelHtml() {
259
264
  <div class="audit-list" id="decisionInboxList"></div>
260
265
  </div>
261
266
  <div class="card">
262
- <h2>Projection Delivery</h2>
263
- <div id="projectionOutboxSummary" class="muted small" style="margin-bottom:12px">Loading projection delivery state\u2026</div>
267
+ <h2>Human Delivery</h2>
268
+ <div id="projectionOutboxSummary" class="muted small" style="margin-bottom:12px">Loading human-delivery state\u2026</div>
264
269
  <div class="audit-list" id="projectionOutboxList"></div>
265
270
  </div>
266
271
  </div>
@@ -277,7 +282,7 @@ function getHostPanelHtml() {
277
282
  <option value="balanced">balanced</option>
278
283
  <option value="strict">strict</option>
279
284
  </select>
280
- <div class="muted small">Balanced is the default: key conclusions, handoffs, repairs, and required decisions are pushed to the human thread; ordinary progress stays summary-first.</div>
285
+ <div class="muted small">Balanced is the default: key conclusions, handoffs, repairs, and required decisions become notification intents and are sent through bound-channel reply; ordinary progress stays summary-first.</div>
281
286
  <div class="row-actions">
282
287
  <button class="action-btn" id="saveProjectionPresetBtn">Save projection preset</button>
283
288
  </div>
@@ -540,6 +545,96 @@ function getHostPanelHtml() {
540
545
  }).join('');
541
546
  }
542
547
 
548
+ function renderHumanDeliveryIntents(intents, emptyLabel) {
549
+ if (!Array.isArray(intents) || !intents.length) {
550
+ return '<div class="empty">' + esc(emptyLabel || 'No notification intents yet.') + '</div>';
551
+ }
552
+ return intents.map(function (intent) {
553
+ const bindingLabel = intent.resolved_binding_id ? ('binding=' + intent.resolved_binding_id) : 'binding=(unresolved)';
554
+ const ackLabel = intent.acknowledged_at
555
+ ? (' \xB7 ack=' + fmtTs(intent.acknowledged_at))
556
+ : '';
557
+ const actions = [];
558
+ if (intent.status === 'failed' || intent.status === 'unresolved') {
559
+ actions.push('<button class="secondary-btn human-delivery-action-btn" data-action="retry_intent" data-intent-id="' + esc(intent.id) + '">Retry</button>');
560
+ 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>');
561
+ } else if (intent.status === 'pending' || intent.status === 'bound' || intent.status === 'sent') {
562
+ 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>');
563
+ }
564
+ if (intent.status !== 'canceled' && intent.status !== 'acknowledged') {
565
+ actions.push('<button class="danger-btn human-delivery-action-btn" data-action="cancel_intent" data-intent-id="' + esc(intent.id) + '">Cancel</button>');
566
+ }
567
+ return '<div class="audit-row"><div class="top"><strong>' + esc(intent.title || intent.summary || '(no title)') + '</strong>' +
568
+ '<span class="badge">' + esc(intent.status || 'pending') + '</span></div>' +
569
+ '<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>' +
570
+ '<div style="margin-top:8px">' + esc(intent.summary || '(no summary)') + '</div>' +
571
+ (intent.last_error ? '<div class="muted small" style="margin-top:8px;color:#fecaca">' + esc(intent.last_error) + '</div>' : '') +
572
+ (actions.length ? '<div class="row-actions" style="margin-top:10px">' + actions.join('') + '</div>' : '') +
573
+ '</div>';
574
+ }).join('');
575
+ }
576
+
577
+ function renderRecentBindings(bindings) {
578
+ if (!Array.isArray(bindings) || !bindings.length) {
579
+ 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>';
580
+ }
581
+ return bindings.map(function (binding) {
582
+ const actions = binding.status === 'active'
583
+ ? '<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>'
584
+ : '';
585
+ return '<div class="audit-row"><div class="top"><strong>' + esc(binding.channel + ' -> ' + binding.to) + '</strong>' +
586
+ '<span class="badge">' + esc(binding.status || 'active') + '</span></div>' +
587
+ '<div class="muted small">' + esc(fmtTs(binding.last_inbound_at || binding.updated_at)) + ' \xB7 owner=' + esc(binding.owner_ref || '(none)') + '</div>' +
588
+ '<div class="muted small" style="margin-top:8px">session=' + esc(binding.source_session_key || '(none)') + ' \xB7 conversation=' + esc(binding.source_conversation_id || '(none)') + '</div>' +
589
+ actions +
590
+ '</div>';
591
+ }).join('');
592
+ }
593
+
594
+ function renderHumanDeliveryCapabilities(humanDelivery) {
595
+ const capabilities = Array.isArray(humanDelivery && humanDelivery.channel_capabilities) ? humanDelivery.channel_capabilities : [];
596
+ if (!capabilities.length) {
597
+ return '<div class="empty">No channel capability data yet.</div>';
598
+ }
599
+ return capabilities.map(function (cap) {
600
+ const supportLabel = cap.supports_explicit_send ? 'explicit-send' : 'unsupported';
601
+ const canary = cap.last_canary_at
602
+ ? (' \xB7 canary=' + (cap.last_canary_ok ? 'ok' : 'failed') + ' @ ' + fmtTs(cap.last_canary_at))
603
+ : ' \xB7 canary=pending';
604
+ return '<div class="audit-row"><div class="top"><strong>' + esc(cap.channel) + '</strong><span class="badge">' + esc(supportLabel) + '</span></div>' +
605
+ '<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>' +
606
+ (cap.last_canary_error ? '<div class="muted small" style="margin-top:8px;color:#fecaca">' + esc(cap.last_canary_error) + '</div>' : '') +
607
+ '</div>';
608
+ }).join('');
609
+ }
610
+
611
+ async function runHumanDeliveryAction(action, payload) {
612
+ return api('/api/runtime/human-delivery/action', {
613
+ method: 'POST',
614
+ headers: { 'Content-Type': 'application/json' },
615
+ body: JSON.stringify(Object.assign({ action: action }, payload || {})),
616
+ });
617
+ }
618
+
619
+ function wireHumanDeliveryActionButtons(root) {
620
+ if (!root || !root.querySelectorAll) return;
621
+ root.querySelectorAll('.human-delivery-action-btn').forEach(function (btn) {
622
+ btn.addEventListener('click', async function () {
623
+ const action = btn.getAttribute('data-action');
624
+ if (!action) return;
625
+ const intentId = btn.getAttribute('data-intent-id');
626
+ const bindingId = btn.getAttribute('data-binding-id');
627
+ await runHumanDeliveryAction(action, {
628
+ intent_id: intentId ? Number(intentId) : undefined,
629
+ binding_id: bindingId ? Number(bindingId) : undefined,
630
+ });
631
+ await refreshAll();
632
+ if (state.selectedSessionKey) await loadSession(state.selectedSessionKey);
633
+ setTab(state.tab || 'runtime');
634
+ });
635
+ });
636
+ }
637
+
543
638
  async function markSeen(scopeType, scopeKey) {
544
639
  return api('/api/runtime/seen', {
545
640
  method: 'POST',
@@ -772,7 +867,7 @@ function getHostPanelHtml() {
772
867
  event.approval_required
773
868
  ? '<span class="badge">' + esc(event.approval_status === 'pending' ? 'review' : event.approval_status) + '</span>'
774
869
  : '',
775
- event.target_human_session ? '<span class="badge">human thread</span>' : '',
870
+ event.target_human_session ? '<span class="badge">human delivery</span>' : '',
776
871
  ].filter(Boolean).join('');
777
872
  const actions = event.approval_required && event.approval_status === 'pending'
778
873
  ? '<div class="row-actions" style="margin-top:10px">' +
@@ -804,8 +899,14 @@ function getHostPanelHtml() {
804
899
  const overdueDecisions = pendingDecisions.filter(function (event) { return !!event.overdue; });
805
900
  const pendingOutbox = data && Array.isArray(data.projectionOutboxPending) ? data.projectionOutboxPending : [];
806
901
  const failedOutbox = data && Array.isArray(data.projectionOutboxFailed) ? data.projectionOutboxFailed : [];
807
- document.getElementById('decisionInboxSummary').textContent =
808
- 'pending=' + pendingDecisions.length + ' \xB7 overdue=' + overdueDecisions.length + ' \xB7 projection_pending=' + pendingOutbox.length + ' \xB7 projection_failed=' + failedOutbox.length;
902
+ const pendingIntents = data && Array.isArray(data.notificationIntentsPending) ? data.notificationIntentsPending : [];
903
+ const unresolvedIntents = data && Array.isArray(data.notificationIntentsUnresolved) ? data.notificationIntentsUnresolved : [];
904
+ const failedIntents = data && Array.isArray(data.notificationIntentsFailed) ? data.notificationIntentsFailed : [];
905
+ const humanDelivery = data && data.humanDelivery ? data.humanDelivery : null;
906
+ const boundChannelReply = !!(humanDelivery && humanDelivery.mode === 'bound_channel_reply');
907
+ document.getElementById('decisionInboxSummary').textContent = boundChannelReply
908
+ ? ('pending=' + pendingDecisions.length + ' \xB7 overdue=' + overdueDecisions.length + ' \xB7 notify_pending=' + pendingIntents.length + ' \xB7 unresolved=' + unresolvedIntents.length + ' \xB7 notify_failed=' + failedIntents.length)
909
+ : ('pending=' + pendingDecisions.length + ' \xB7 overdue=' + overdueDecisions.length + ' \xB7 projection_pending=' + pendingOutbox.length + ' \xB7 projection_failed=' + failedOutbox.length);
809
910
  document.getElementById('decisionInboxList').innerHTML = pendingDecisions.length
810
911
  ? pendingDecisions.map(function (event) {
811
912
  return '<div class="audit-row"><div class="top"><strong>' + esc(event.summary || '(no summary)') + '</strong>' +
@@ -824,20 +925,26 @@ function getHostPanelHtml() {
824
925
  : '<div class="empty">No pending collaboration decisions.</div>';
825
926
 
826
927
  const combinedOutbox = pendingOutbox.concat(failedOutbox);
827
- document.getElementById('projectionOutboxSummary').textContent =
828
- combinedOutbox.length
829
- ? 'Human-thread projection uses a stable outbox. Failed rows stay visible until delivery recovers.'
830
- : 'No undelivered human-thread projections.';
831
- document.getElementById('projectionOutboxList').innerHTML = combinedOutbox.length
832
- ? combinedOutbox.map(function (entry) {
833
- return '<div class="audit-row"><div class="top"><strong>' + esc(entry.summary || '(no summary)') + '</strong>' +
834
- '<span class="badge">' + esc(entry.status || 'pending') + '</span></div>' +
835
- '<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>' +
836
- '<div class="muted small" style="margin-top:8px">target=' + esc(entry.target_human_session || '(missing)') + ' \xB7 session=' + esc(entry.session_key || '(none)') + '</div>' +
837
- (entry.last_error ? '<div style="margin-top:8px">' + esc(entry.last_error) + '</div>' : '') +
838
- '</div>';
839
- }).join('')
840
- : '<div class="empty">No pending or failed projection deliveries.</div>';
928
+ document.getElementById('projectionOutboxSummary').textContent = boundChannelReply
929
+ ? ('bound reply mode \xB7 pending=' + pendingIntents.length + ' \xB7 unresolved=' + unresolvedIntents.length + ' \xB7 failed=' + failedIntents.length)
930
+ : (combinedOutbox.length
931
+ ? 'Legacy projection outbox remains visible for audit and compatibility. Failed rows stay visible until delivery recovers.'
932
+ : 'No legacy projection-outbox rows need attention.');
933
+ document.getElementById('projectionOutboxList').innerHTML = boundChannelReply
934
+ ? renderHumanDeliveryIntents(
935
+ pendingIntents.concat(unresolvedIntents).concat(failedIntents),
936
+ 'No pending, unresolved, or failed human-delivery intents.',
937
+ )
938
+ : (combinedOutbox.length
939
+ ? combinedOutbox.map(function (entry) {
940
+ return '<div class="audit-row"><div class="top"><strong>' + esc(entry.summary || '(no summary)') + '</strong>' +
941
+ '<span class="badge">' + esc(entry.status || 'pending') + '</span></div>' +
942
+ '<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>' +
943
+ '<div class="muted small" style="margin-top:8px">target=' + esc(entry.target_human_session || '(missing)') + ' \xB7 session=' + esc(entry.session_key || '(none)') + '</div>' +
944
+ (entry.last_error ? '<div style="margin-top:8px">' + esc(entry.last_error) + '</div>' : '') +
945
+ '</div>';
946
+ }).join('')
947
+ : '<div class="empty">No pending or failed legacy projection deliveries.</div>');
841
948
 
842
949
  document.querySelectorAll('.inbox-decision-btn').forEach(function (btn) {
843
950
  btn.addEventListener('click', async function () {
@@ -864,6 +971,7 @@ function getHostPanelHtml() {
864
971
  setTab('runtime');
865
972
  });
866
973
  });
974
+ wireHumanDeliveryActionButtons(document.getElementById('projectionOutboxList'));
867
975
  }
868
976
 
869
977
  function getOverviewSessions() {
@@ -901,22 +1009,41 @@ function getHostPanelHtml() {
901
1009
  if (!overview) return;
902
1010
  syncSelectedSessionFromOverview();
903
1011
  const ingressState = ingressStatusModel(overview);
1012
+ const agentState = overview.agentState || null;
904
1013
  const transportHealth = overview.transportHealth || null;
905
1014
  const sinceLastSeen = overview.sinceLastSeen || null;
1015
+ const humanDelivery = overview.humanDelivery || null;
906
1016
  const overdueDecisions = Array.isArray(overview.pendingDecisions)
907
1017
  ? overview.pendingDecisions.filter(function (event) { return !!event.overdue; })
908
1018
  : [];
1019
+ const statusClass = agentState && (agentState.blocked || agentState.degraded)
1020
+ ? 'degraded'
1021
+ : ingressState.className;
1022
+ const statusLabel = agentState && agentState.state
1023
+ ? agentState.state
1024
+ : ingressState.label;
1025
+ const statusDetail = agentState && agentState.summary
1026
+ ? agentState.summary
1027
+ : ingressState.detail;
909
1028
  document.getElementById('activationCard').innerHTML =
910
1029
  '<div class="status-strip">' +
911
1030
  '<div class="status-main">' +
912
- '<h2>Activation</h2>' +
913
- '<div class="status-state ' + ingressState.className + '">' + esc(ingressState.label) + '</div>' +
914
- '<div class="muted small">' + esc(ingressState.detail) + '</div>' +
1031
+ '<h2>Agent Runtime</h2>' +
1032
+ '<div class="status-state ' + statusClass + '">' + esc(statusLabel) + '</div>' +
1033
+ '<div class="muted small">' + esc(statusDetail) + '</div>' +
1034
+ (agentState && agentState.next_action
1035
+ ? '<div class="muted small">next_action=' + esc(agentState.next_action) + '</div>'
1036
+ : '') +
1037
+ (agentState && agentState.reason
1038
+ ? '<div class="muted small">reason=' + esc(agentState.reason) + '</div>'
1039
+ : '') +
915
1040
  '<div class="muted small">Public link: ' + esc(overview.publicSelf && overview.publicSelf.public_url ? overview.publicSelf.public_url : '(not ready yet)') + '</div>' +
916
1041
  '<div class="summary-pills">' +
917
1042
  '<span class="pill">pending_decisions=' + esc((overview.pendingDecisions || []).length) + '</span>' +
918
1043
  '<span class="pill">overdue=' + esc(overdueDecisions.length) + '</span>' +
919
- '<span class="pill">projection=' + esc(overview.collaborationProjection && overview.collaborationProjection.preset ? overview.collaborationProjection.preset : 'balanced') + '</span>' +
1044
+ '<span class="pill">projection_policy=' + esc(overview.collaborationProjection && overview.collaborationProjection.preset ? overview.collaborationProjection.preset : 'balanced') + '</span>' +
1045
+ (humanDelivery ? '<span class="pill">human_delivery=' + esc(humanDelivery.mode || 'projection_outbox') + '</span>' : '') +
1046
+ (agentState && agentState.active_session_key ? '<span class="pill">active_session=ready</span>' : '') +
920
1047
  '</div>' +
921
1048
  '</div>' +
922
1049
  '<div style="min-width:320px">' +
@@ -945,18 +1072,28 @@ function getHostPanelHtml() {
945
1072
  { label: 'Tasks', value: overview.tasksTotal, sub: 'recent local task threads' },
946
1073
  { label: 'Audit', value: overview.auditSummary.total_events, sub: 'policy / runtime audit events' },
947
1074
  { label: 'Collaboration', value: overview.collaborationSummary ? overview.collaborationSummary.total_events : 0, sub: overview.collaborationSummary ? ('pending_review=' + overview.collaborationSummary.pending_approvals) : 'projected external collaboration events' },
1075
+ { 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' },
948
1076
  { label: 'Recommendations', value: overview.recommendationSummary ? overview.recommendationSummary.total : overview.recommendations.length, sub: overview.recommendationSummary ? JSON.stringify(overview.recommendationSummary.by_status || {}) : 'learned policy suggestions' },
949
1077
  { 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' },
950
1078
  ];
951
1079
  document.getElementById('statsGrid').innerHTML = stats.map(function (item) {
952
1080
  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>';
953
1081
  }).join('');
1082
+ 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>' +
1083
+ '<div class="muted small">Capabilities and canary state for explicit human-delivery replies.</div>' +
1084
+ '<div class="audit-list" style="margin-top:8px">' + renderHumanDeliveryCapabilities(humanDelivery) + '</div></div>';
954
1085
 
955
1086
  const toggleUnreadBtn = document.getElementById('toggleUnreadBtn');
956
1087
  if (toggleUnreadBtn) toggleUnreadBtn.textContent = 'Unread only: ' + (state.showUnreadOnly ? 'on' : 'off');
957
1088
  const sessions = getVisibleSessions();
958
1089
  if (!sessions.length) {
959
- document.getElementById('sessionList').innerHTML = '<div class="empty">' + (state.showUnreadOnly ? 'No unread sessions.' : 'No sessions yet.') + '</div>';
1090
+ document.getElementById('sessionList').innerHTML = '<div class="empty">' + (
1091
+ state.showUnreadOnly
1092
+ ? 'No unread sessions.'
1093
+ : (agentState && agentState.summary
1094
+ ? agentState.summary + ' Wait for inbound work or review pending decisions.'
1095
+ : 'No sessions yet.')
1096
+ ) + '</div>';
960
1097
  } else {
961
1098
  document.getElementById('sessionList').innerHTML = sessions.map(function (session) {
962
1099
  const active = session.session_key === state.selectedSessionKey ? ' active' : '';
@@ -966,7 +1103,7 @@ function getHostPanelHtml() {
966
1103
  ? '<span class="badge alert">new ' + esc(countSinceLastSeen(session.since_last_seen)) + '</span>'
967
1104
  : ''),
968
1105
  session.binding_alert
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>'
1106
+ ? '<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="Legacy compatibility route needs attention">Compatibility repair</button>'
970
1107
  : '',
971
1108
  ].filter(Boolean).join('');
972
1109
  return '<div class="session-row' + active + '" data-session="' + esc(session.session_key) + '">' +
@@ -974,7 +1111,7 @@ function getHostPanelHtml() {
974
1111
  '<div class="muted small">unread=' + esc(session.unread_count) + ' \xB7 last=' + esc(fmtTs(session.last_remote_activity_at || session.updated_at)) + '</div>' +
975
1112
  (state.detailMode === 'advanced'
976
1113
  ? '<div class="muted small" style="margin-top:6px">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
977
- '<div class="muted small">work_session=' + esc(session.mapped_work_session || '(unbound)') + (session.is_active_work_session ? ' \xB7 active_chat=true' : '') + '</div>'
1114
+ '<div class="muted small">compatibility_route=' + esc(session.mapped_work_session || '(unbound)') + (session.is_active_work_session ? ' \xB7 active_route=true' : '') + '</div>'
978
1115
  : '') +
979
1116
  '<div class="muted small" style="margin-top:6px">' + esc(session.last_message_preview || '(no preview)') + '</div>' +
980
1117
  '</div>';
@@ -1022,7 +1159,7 @@ function getHostPanelHtml() {
1022
1159
  });
1023
1160
 
1024
1161
  const tasks = Array.isArray(overview.tasks) ? overview.tasks : [];
1025
- document.getElementById('taskList').innerHTML = tasks.length
1162
+ document.getElementById('taskList').innerHTML += tasks.length
1026
1163
  ? tasks.map(function (task) {
1027
1164
  return '<div class="task-row"><div class="top"><strong>' + esc(task.title || task.task_id) + '</strong><span class="badge">' + esc(task.status) + '</span></div>' +
1028
1165
  '<div class="muted small">task_id=' + esc(task.task_id) + ' \xB7 session=' + esc(task.session_key) + '</div>' +
@@ -1031,7 +1168,7 @@ function getHostPanelHtml() {
1031
1168
  (task.error_message ? '<div style="margin-top:8px;color:#fca5a5">' + esc(task.error_message) + '</div>' : '') +
1032
1169
  renderHandoffBlock(task.handoff) +
1033
1170
  '</div>';
1034
- }).join('')
1171
+ }).join('')
1035
1172
  : '<div class="empty">No recent task threads.</div>';
1036
1173
 
1037
1174
  if (subscription) {
@@ -1125,10 +1262,13 @@ conversation=' + result.conversation_id));
1125
1262
  ? detail.collaborationProjection.preset
1126
1263
  : 'balanced';
1127
1264
  const projectionOutbox = Array.isArray(detail.projectionOutbox) ? detail.projectionOutbox : [];
1265
+ const recentBindings = Array.isArray(detail.recentBindings) ? detail.recentBindings : [];
1266
+ const recentNotificationIntents = Array.isArray(detail.recentNotificationIntents) ? detail.recentNotificationIntents : [];
1128
1267
  const sinceLastSeen = detail.sinceLastSeen || null;
1129
1268
  const deliveryTimeline = Array.isArray(detail.deliveryTimeline) ? detail.deliveryTimeline : [];
1130
1269
  const projectionPreview = detail.projectionPreview || null;
1131
1270
  const transportHealth = detail.transportHealth || null;
1271
+ const humanDelivery = detail.humanDelivery || null;
1132
1272
  const isAdvanced = state.detailMode === 'advanced';
1133
1273
  const sessionLink = buildSessionLink(session.session_key);
1134
1274
  const summaryPills = [];
@@ -1136,8 +1276,8 @@ conversation=' + result.conversation_id));
1136
1276
  if (task && task.action) summaryPills.push('<span class="pill">task=' + esc(task.action) + '</span>');
1137
1277
  if (session.trust_state) summaryPills.push('<span class="pill">trust=' + esc(session.trust_state) + '</span>');
1138
1278
  if (Number(session.unread_count || 0) > 0) summaryPills.push('<span class="pill">unread=' + esc(session.unread_count) + '</span>');
1139
- if (binding && binding.session_key) summaryPills.push('<span class="pill">chat link attached</span>');
1140
- else if (activeWorkSession) summaryPills.push('<span class="pill">current OpenClaw chat available</span>');
1279
+ if (binding && binding.target_key) summaryPills.push('<span class="pill">human reply target ready</span>');
1280
+ if (isAdvanced && activeWorkSession) summaryPills.push('<span class="pill">compatibility route available</span>');
1141
1281
  const policyBlock = isAdvanced
1142
1282
  ? '<pre style="margin-top:8px">[Contact]\\naction=' + esc(contact.action) + '\\nsource=' + esc(contact.source) + (contact.matched_rule ? '\\nmatched_rule=' + esc(contact.matched_rule) : '') + '\\n' + esc(contact.explanation) + '\\n\\n[Task]\\naction=' + esc(task.action) + '\\nsource=' + esc(task.source) + (task.matched_rule ? '\\nmatched_rule=' + esc(task.matched_rule) : '') + '\\n' + esc(task.explanation) + '</pre>'
1143
1283
  : '<div class="summary-pills" style="margin-top:8px">' + summaryPills.join('') + '</div>' +
@@ -1155,8 +1295,8 @@ conversation=' + result.conversation_id));
1155
1295
  ? '<div style="margin-top:8px">' +
1156
1296
  '<div class="muted small">session=' + esc(session.session_key) + '</div>' +
1157
1297
  '<div class="muted small">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
1158
- '<div class="muted small">active_chat_session=' + esc(activeWorkSession || '(none)') + '</div>' +
1159
- '<div class="muted small">binding=' + esc(binding ? binding.session_key : '(unbound)') + '</div>' +
1298
+ '<div class="muted small">compatibility_active_session=' + esc(activeWorkSession || '(none)') + '</div>' +
1299
+ '<div class="muted small">reply_binding=' + esc(binding ? (binding.target_key || binding.session_key || '(bound)') : '(unbound)') + '</div>' +
1160
1300
  '</div>'
1161
1301
  : '') +
1162
1302
  (openRecommendation
@@ -1164,11 +1304,11 @@ conversation=' + result.conversation_id));
1164
1304
  : (reopenRecommendation ? '<div class="muted small" style="margin-top:8px">trust_action=' + esc(recommendationActionLabel(reopenRecommendation)) + '</div>' : '')) +
1165
1305
  (summaryPills.length ? '<div class="summary-pills">' + summaryPills.join('') + '</div>' : '') +
1166
1306
  (pendingCollaborationEvents.length
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.' +
1307
+ ? '<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 this session is treated as settled. Resolve it in the Collaboration Feed below.' +
1168
1308
  (pendingCollaborationEvents[0].overdue ? ' This item is overdue.' : '') + '</div></div>'
1169
1309
  : '') +
1170
1310
  (bindingAlert
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>'
1311
+ ? '<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>Compatibility route stale</strong><div class="small" style="margin-top:6px">' + esc(isAdvanced ? (bindingAlert.message || 'Legacy current-thread compatibility routing is missing. Repair it only if you intentionally depend on the compatibility path.') : 'Legacy current-thread compatibility routing is stale. Default human delivery continues to use bindings + notification intents.') + '</div></div>'
1172
1312
  : '') +
1173
1313
  '<div class="row-actions">' +
1174
1314
  (session.trust_state === 'pending'
@@ -1183,11 +1323,16 @@ conversation=' + result.conversation_id));
1183
1323
  (!openRecommendation && reopenRecommendation
1184
1324
  ? '<button class="secondary-btn reopen-session-recommendation-btn" data-session="' + esc(session.session_key) + '">Reopen</button>'
1185
1325
  : '') +
1186
- '<button class="action-btn bind-current-btn" data-conversation="' + esc(session.conversation_id || '') + '">Attach to Current Chat</button>' +
1187
1326
  '<button class="secondary-btn mark-read-btn" data-session="' + esc(session.session_key) + '">Mark read</button>' +
1188
1327
  '<button class="secondary-btn copy-session-link-btn" data-session="' + esc(session.session_key) + '">Copy Session Link</button>' +
1189
- '<button class="danger-btn clear-binding-btn" data-conversation="' + esc(session.conversation_id || '') + '">Detach Chat Link</button>' +
1328
+ (isAdvanced
1329
+ ? '<button class="action-btn bind-current-btn" data-conversation="' + esc(session.conversation_id || '') + '">Repair Compatibility Route</button>' +
1330
+ '<button class="danger-btn clear-binding-btn" data-conversation="' + esc(session.conversation_id || '') + '">Clear Compatibility Route</button>'
1331
+ : '') +
1190
1332
  '</div>' +
1333
+ (isAdvanced
1334
+ ? '<div class="muted small" style="margin-top:8px">Compatibility routing is repair-only. The default human-delivery path uses Human Delivery Binding plus Notification Intent.</div>'
1335
+ : '') +
1191
1336
  '<div class="form-grid" style="margin-top:16px">' +
1192
1337
  '<label class="label">Reply in this session</label>' +
1193
1338
  '<textarea id="sessionReplyInput" placeholder="Send a text reply in this session"></textarea>' +
@@ -1275,9 +1420,9 @@ conversation=' + result.conversation_id));
1275
1420
  '</div>';
1276
1421
  }).join('') : '<div class="empty">No audit events for this session.</div>') +
1277
1422
  '</div></div>' +
1278
- '<div><div class="label">Human Thread Posture</div><div class="audit-list" style="margin-top:8px">' +
1423
+ '<div><div class="label">Human Delivery Posture</div><div class="audit-list" style="margin-top:8px">' +
1279
1424
  '<div class="audit-row"><div class="top"><strong>Projection policy</strong><span class="badge">' + esc(projectionPreset) + '</span></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>' +
1425
+ '<div class="muted small">The collaboration session keeps the raw transcript. Human-visible updates now flow through Human Delivery Binding plus Notification Intent. Projection outbox remains visible for audit and compatibility.</div>' +
1281
1426
  '<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
1427
  (sinceLastSeen
1283
1428
  ? '<div class="summary-pills" style="margin-top:8px">' +
@@ -1289,6 +1434,9 @@ conversation=' + result.conversation_id));
1289
1434
  (transportHealth
1290
1435
  ? '<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
1436
  : '') +
1437
+ (humanDelivery
1438
+ ? '<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>'
1439
+ : '') +
1292
1440
  (projectionOutbox.length
1293
1441
  ? '<div class="muted small" style="margin-top:8px">outbox=' + esc(projectionOutbox[0].status || 'pending') + ' \xB7 target=' + esc(projectionOutbox[0].target_human_session || '(missing)') + '</div>'
1294
1442
  : '<div class="muted small" style="margin-top:8px">outbox=clear</div>') +
@@ -1307,6 +1455,16 @@ conversation=' + result.conversation_id));
1307
1455
  '</div></div>' +
1308
1456
  '</div>';
1309
1457
 
1458
+ el.innerHTML +=
1459
+ '<div class="grid two-col" style="margin-top:16px">' +
1460
+ '<div><div class="label">Human Reply Targets</div><div class="audit-list" style="margin-top:8px">' +
1461
+ renderRecentBindings(recentBindings) +
1462
+ '</div></div>' +
1463
+ '<div><div class="label">Notification Intents</div><div class="audit-list" style="margin-top:8px">' +
1464
+ renderHumanDeliveryIntents(recentNotificationIntents, 'No notification intents for this session yet.') +
1465
+ '</div></div>' +
1466
+ '</div>';
1467
+
1310
1468
  el.querySelectorAll('.approve-session-btn').forEach(function (btn) {
1311
1469
  btn.addEventListener('click', async function () {
1312
1470
  const sessionKey = btn.getAttribute('data-session');
@@ -1447,6 +1605,7 @@ conversation=' + result.conversation_id));
1447
1605
  setTab('runtime');
1448
1606
  });
1449
1607
  }
1608
+ wireHumanDeliveryActionButtons(el);
1450
1609
  }
1451
1610
 
1452
1611
  async function promptBindCurrentChat(conversationId, previousBinding, remoteDid) {
@@ -1456,7 +1615,7 @@ conversation=' + result.conversation_id));
1456
1615
  const previous = previousBinding || (state.session && state.session.binding ? state.session.binding.session_key : null) || '(unbound)';
1457
1616
  const targetRemoteDid = remoteDid || (state.session && state.session.session ? state.session.session.remote_did : null) || '(unknown)';
1458
1617
  const confirmed = window.confirm(
1459
- 'Attach this PingAgent session to the current OpenClaw chat?' +
1618
+ 'Repair the legacy current-thread compatibility route for this PingAgent session?' +
1460
1619
  '
1461
1620
 
1462
1621
  Conversation: ' + conversationId +
@@ -1464,9 +1623,9 @@ Conversation: ' + conversationId +
1464
1623
  Remote DID: ' + targetRemoteDid +
1465
1624
  '
1466
1625
 
1467
- Current OpenClaw chat: ' + (current || '(none)') +
1626
+ Current OpenClaw compatibility target: ' + (current || '(none)') +
1468
1627
  '
1469
- Previous chat link: ' + previous
1628
+ Previous compatibility route: ' + previous
1470
1629
  );
1471
1630
  if (!confirmed) return;
1472
1631
  await api('/api/runtime/session-bindings/bind-current', {
@@ -2154,6 +2313,24 @@ function readSinceLastSeenState(storePath, operatorId, scopeType, scopeKey) {
2154
2313
  store.close();
2155
2314
  }
2156
2315
  }
2316
+ function readHumanDeliveryState(storePath, limit = 12) {
2317
+ const store = new LocalStore(storePath);
2318
+ try {
2319
+ return summarizeHumanDelivery(store, limit);
2320
+ } finally {
2321
+ store.close();
2322
+ }
2323
+ }
2324
+ function buildOpenClawAgentStatePayload(input) {
2325
+ return deriveOpenClawAgentState({
2326
+ runtime_status: input.runtimeStatus ?? null,
2327
+ sessions: input.sessions ?? [],
2328
+ pending_decisions: Array.isArray(input.pendingDecisions) ? input.pendingDecisions.length : 0,
2329
+ human_delivery: input.humanDelivery ?? null,
2330
+ transport_health: input.transportHealth ?? null,
2331
+ blocked_reason: input.blockedReason ?? null
2332
+ });
2333
+ }
2157
2334
  function buildPolicyDecisionShape(identityPath, remoteDid, opts) {
2158
2335
  const policy = readTrustPolicyDoc(identityPath);
2159
2336
  const runtimeMode = opts?.runtimeMode ?? getRuntimeMode();
@@ -2268,13 +2445,30 @@ async function buildRuntimeOverviewPayload(ctx) {
2268
2445
  const projectionOutbox = readProjectionOutboxState(ctx.storePath, 20);
2269
2446
  const transportHealth = readTransportHealthState(ctx.storePath);
2270
2447
  const sinceLastSeen = readSinceLastSeenState(ctx.storePath, "host_panel", "global");
2448
+ const humanDelivery = readHumanDeliveryState(ctx.storePath, 20);
2271
2449
  const decisionStore = new LocalStore(ctx.storePath);
2272
2450
  let decisionViews = [];
2451
+ let humanDeliveryDetails = null;
2273
2452
  try {
2274
2453
  decisionViews = listPendingDecisionViews(decisionStore, 100);
2454
+ const bindingManager = new HumanDeliveryBindingManager(decisionStore);
2455
+ const intentManager = new NotificationIntentManager(decisionStore);
2456
+ humanDeliveryDetails = {
2457
+ unresolved_intents: intentManager.listByStatus("unresolved", 20),
2458
+ failed_intents: intentManager.listByStatus("failed", 20),
2459
+ acknowledged_intents: intentManager.listByStatus("acknowledged", 20),
2460
+ recent_bindings: bindingManager.listRecent(20)
2461
+ };
2275
2462
  } finally {
2276
2463
  decisionStore.close();
2277
2464
  }
2465
+ const agentState = buildOpenClawAgentStatePayload({
2466
+ runtimeStatus: ingressRuntime,
2467
+ sessions,
2468
+ pendingDecisions: decisionViews,
2469
+ humanDelivery,
2470
+ transportHealth
2471
+ });
2278
2472
  return {
2279
2473
  did: ctx.myDid,
2280
2474
  serverUrl: ctx.serverUrl,
@@ -2313,7 +2507,10 @@ async function buildRuntimeOverviewPayload(ctx) {
2313
2507
  collaborationSummary,
2314
2508
  projectionOutbox,
2315
2509
  transportHealth,
2510
+ agentState,
2316
2511
  sinceLastSeen,
2512
+ humanDelivery,
2513
+ humanDeliveryDetails,
2317
2514
  pendingDecisions: decisionViews,
2318
2515
  recentCollaborationEvents: collaborationEventManager.listRecent(20),
2319
2516
  sessions: sessions.map((session) => ({
@@ -2381,12 +2578,17 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
2381
2578
  });
2382
2579
  const outboxStore = new LocalStore(ctx.storePath);
2383
2580
  let projectionOutbox = [];
2581
+ let notificationIntents = [];
2582
+ let recentBindings = [];
2384
2583
  let sinceLastSeen = null;
2385
2584
  let deliveryTimeline = [];
2386
2585
  let projectionPreview = null;
2387
2586
  let pendingDecisionViews = [];
2587
+ let humanDelivery = null;
2388
2588
  try {
2389
2589
  projectionOutbox = new CollaborationProjectionOutboxManager(outboxStore).listBySession(session.session_key, 20);
2590
+ notificationIntents = new NotificationIntentManager(outboxStore).listBySession(session.session_key, 20);
2591
+ recentBindings = listRecentBindingsForSession(outboxStore, session.session_key, session.conversation_id, 20);
2390
2592
  sinceLastSeen = summarizeSinceLastSeen(outboxStore, {
2391
2593
  operator_id: "host_panel",
2392
2594
  scope_type: "session",
@@ -2400,9 +2602,17 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
2400
2602
  5
2401
2603
  );
2402
2604
  pendingDecisionViews = listPendingDecisionViews(outboxStore, 100).filter((event) => event.session_key === session.session_key).slice(0, 20);
2605
+ humanDelivery = summarizeHumanDelivery(outboxStore, 20);
2403
2606
  } finally {
2404
2607
  outboxStore.close();
2405
2608
  }
2609
+ const agentState = buildOpenClawAgentStatePayload({
2610
+ runtimeStatus: readIngressRuntimeStatus(),
2611
+ sessions: sessionManager.listRecentSessions(50),
2612
+ pendingDecisions: pendingDecisionViews,
2613
+ humanDelivery,
2614
+ transportHealth: readTransportHealthState(ctx.storePath)
2615
+ });
2406
2616
  return {
2407
2617
  session,
2408
2618
  sessionSummary: sessionSummaryManager.get(session.session_key),
@@ -2411,6 +2621,7 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
2411
2621
  collaborationSummary: collaborationEventManager.summarize(200),
2412
2622
  ingressRuntime: readIngressRuntimeStatus(),
2413
2623
  transportHealth: readTransportHealthState(ctx.storePath),
2624
+ agentState,
2414
2625
  binding,
2415
2626
  bindingAlert,
2416
2627
  activeWorkSession,
@@ -2419,9 +2630,12 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
2419
2630
  sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
2420
2631
  collaborationProjection: policy.collaboration_projection,
2421
2632
  projectionOutbox,
2633
+ recentBindings,
2634
+ recentNotificationIntents: notificationIntents,
2422
2635
  sinceLastSeen,
2423
2636
  deliveryTimeline,
2424
2637
  projectionPreview,
2638
+ humanDelivery,
2425
2639
  policyExplain: buildPolicyDecisionShape(ctx.identityPath, session.remote_did, { runtimeMode: getRuntimeMode() }),
2426
2640
  tasks: tasks.map((task) => ({
2427
2641
  ...task,
@@ -2504,6 +2718,57 @@ async function handleApi(pathname, req, ctx) {
2504
2718
  transportPreference: readTransportPreference()
2505
2719
  };
2506
2720
  }
2721
+ if (parts[1] === "human-delivery") {
2722
+ const store = new LocalStore(ctx.storePath);
2723
+ try {
2724
+ const bindingManager = new HumanDeliveryBindingManager(store);
2725
+ const intentManager = new NotificationIntentManager(store);
2726
+ if (req.method === "GET") {
2727
+ return {
2728
+ humanDelivery: summarizeHumanDelivery(store, 20),
2729
+ recentBindings: bindingManager.listRecent(50),
2730
+ unresolvedIntents: intentManager.listByStatus("unresolved", 50),
2731
+ failedIntents: intentManager.listByStatus("failed", 50),
2732
+ acknowledgedIntents: intentManager.listByStatus("acknowledged", 50)
2733
+ };
2734
+ }
2735
+ if (req.method === "POST" && parts[2] === "action") {
2736
+ const body = await readBody(req);
2737
+ const action = String(body?.action ?? "").trim();
2738
+ const intentId = Number(body?.intent_id);
2739
+ const bindingId = Number(body?.binding_id);
2740
+ let intent = null;
2741
+ let binding = null;
2742
+ if (action === "retry_intent") {
2743
+ if (!Number.isInteger(intentId) || intentId <= 0) throw new Error("intent_id is required");
2744
+ intent = intentManager.markPending(intentId, { clear_error: true, clear_ack: false, clear_binding: false });
2745
+ } else if (action === "re_resolve_intent_binding") {
2746
+ if (!Number.isInteger(intentId) || intentId <= 0) throw new Error("intent_id is required");
2747
+ intent = intentManager.markPending(intentId, { clear_error: true, clear_ack: false, clear_binding: true });
2748
+ } else if (action === "cancel_intent") {
2749
+ if (!Number.isInteger(intentId) || intentId <= 0) throw new Error("intent_id is required");
2750
+ intent = intentManager.markCanceled(intentId, "manual_cancel");
2751
+ } else if (action === "mark_binding_stale") {
2752
+ if (!Number.isInteger(bindingId) || bindingId <= 0) throw new Error("binding_id is required");
2753
+ binding = bindingManager.markStale(bindingId);
2754
+ } else {
2755
+ throw new Error("action must be retry_intent, re_resolve_intent_binding, cancel_intent, or mark_binding_stale");
2756
+ }
2757
+ return {
2758
+ ok: true,
2759
+ action,
2760
+ intent,
2761
+ binding,
2762
+ humanDelivery: summarizeHumanDelivery(store, 20),
2763
+ unresolvedIntents: intentManager.listByStatus("unresolved", 50),
2764
+ failedIntents: intentManager.listByStatus("failed", 50),
2765
+ acknowledgedIntents: intentManager.listByStatus("acknowledged", 50)
2766
+ };
2767
+ }
2768
+ } finally {
2769
+ store.close();
2770
+ }
2771
+ }
2507
2772
  if (parts[1] === "transport-switch" && req.method === "POST") {
2508
2773
  const body = await readBody(req);
2509
2774
  const mode = String(body?.mode ?? "").trim().toLowerCase();
@@ -2618,10 +2883,15 @@ async function handleApi(pathname, req, ctx) {
2618
2883
  const outboxManager = new CollaborationProjectionOutboxManager(store);
2619
2884
  if (req.method === "GET") {
2620
2885
  const pendingDecisions = listPendingDecisionViews(store, 100);
2886
+ const intentManager = new NotificationIntentManager(store);
2621
2887
  return {
2622
2888
  pendingDecisions,
2623
2889
  projectionOutboxPending: outboxManager.listByStatus("pending", 50),
2624
- projectionOutboxFailed: outboxManager.listByStatus("failed", 50)
2890
+ projectionOutboxFailed: outboxManager.listByStatus("failed", 50),
2891
+ notificationIntentsPending: intentManager.listByStatus("pending", 50).concat(intentManager.listByStatus("bound", 50)),
2892
+ notificationIntentsUnresolved: intentManager.listByStatus("unresolved", 50),
2893
+ notificationIntentsFailed: intentManager.listByStatus("failed", 50),
2894
+ humanDelivery: summarizeHumanDelivery(store, 20)
2625
2895
  };
2626
2896
  }
2627
2897
  if (parts[2] === "resolve" && req.method === "POST") {