@silicaclaw/cli 2026.3.20-12 → 2026.3.20-14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## v1.0 beta - 2026-03-20
4
4
 
5
+ ### 2026.3.20-14
6
+
7
+ - release build:
8
+ - prepared another fresh latest-channel package build without publishing
9
+ - regenerated the npm tarball through the verified release packing workflow
10
+
11
+ ### 2026.3.20-13
12
+
13
+ - release build:
14
+ - prepared another fresh latest-channel package build without publishing
15
+ - regenerated the npm tarball through the verified release packing workflow
16
+
5
17
  ### 2026.3.20-12
6
18
 
7
19
  - release build:
package/VERSION CHANGED
@@ -1 +1 @@
1
- v2026.3.20-12
1
+ v2026.3.20-14
@@ -38,7 +38,7 @@ type PrivateMessageView = {
38
38
  body: string;
39
39
  created_at: number;
40
40
  is_self: boolean;
41
- delivery_status: "sent" | "received" | "read";
41
+ delivery_status: "sent" | "direct-sent" | "fallback-sent" | "received" | "read";
42
42
  };
43
43
  type RuntimeMessageGovernance = SocialMessageGovernanceConfig;
44
44
  type OpenClawBridgeStatus = {
@@ -181,6 +181,7 @@ export declare class LocalNodeService {
181
181
  private privateEncryptionKeyPair;
182
182
  private privatePeerRoutes;
183
183
  private privateMessageBodyCache;
184
+ private privateMessageDeliveryStatusCache;
184
185
  private messageGovernance;
185
186
  private privateMessagesPersistDirty;
186
187
  private privateMessageReceiptsPersistDirty;
@@ -622,6 +623,7 @@ export declare class LocalNodeService {
622
623
  getIdentity(): AgentIdentity | null;
623
624
  getSocialMessages(limit?: number, options?: {
624
625
  agent_id?: string | null;
626
+ offset?: number | null;
625
627
  }): {
626
628
  total: number;
627
629
  items: SocialMessageView[];
@@ -655,7 +657,10 @@ export declare class LocalNodeService {
655
657
  last_message_at: number | null;
656
658
  unread_count: number;
657
659
  }>;
658
- getPrivateMessages(conversationId: string, limit?: number): PrivateMessageView[];
660
+ getPrivateMessages(conversationId: string, limit?: number, offset?: number): {
661
+ total: number;
662
+ items: PrivateMessageView[];
663
+ };
659
664
  sendPrivateMessage(input: {
660
665
  to_agent_id: string;
661
666
  recipient_encryption_public_key: string;
@@ -787,6 +787,7 @@ class LocalNodeService {
787
787
  privateEncryptionKeyPair = null;
788
788
  privatePeerRoutes = {};
789
789
  privateMessageBodyCache = new Map();
790
+ privateMessageDeliveryStatusCache = new Map();
790
791
  messageGovernance;
791
792
  privateMessagesPersistDirty = false;
792
793
  privateMessageReceiptsPersistDirty = false;
@@ -1579,6 +1580,7 @@ class LocalNodeService {
1579
1580
  }
1580
1581
  getSocialMessages(limit = 50, options) {
1581
1582
  const resolvedLimit = Math.max(1, Math.min(200, Number(limit) || 50));
1583
+ const resolvedOffset = Math.max(0, Number(options?.offset) || 0);
1582
1584
  this.ensureLocalDirectoryBaseline();
1583
1585
  this.compactCacheInMemory();
1584
1586
  const agentId = String(options?.agent_id || "").trim();
@@ -1587,7 +1589,7 @@ class LocalNodeService {
1587
1589
  : this.socialMessages;
1588
1590
  return {
1589
1591
  total: filtered.length,
1590
- items: filtered.slice(0, resolvedLimit).map((message) => {
1592
+ items: filtered.slice(resolvedOffset, resolvedOffset + resolvedLimit).map((message) => {
1591
1593
  const profile = this.directory.profiles[message.agent_id];
1592
1594
  const lastSeenAt = this.directory.presence[message.agent_id] ?? 0;
1593
1595
  const observations = this.socialMessageObservations.filter((item) => item.message_id === message.message_id);
@@ -1649,11 +1651,12 @@ class LocalNodeService {
1649
1651
  }
1650
1652
  return Array.from(conversations.values()).sort((a, b) => (b.last_message_at || 0) - (a.last_message_at || 0));
1651
1653
  }
1652
- getPrivateMessages(conversationId, limit = PRIVATE_MESSAGE_QUERY_LIMIT) {
1654
+ getPrivateMessages(conversationId, limit = PRIVATE_MESSAGE_QUERY_LIMIT, offset = 0) {
1653
1655
  const normalizedConversationId = String(conversationId || "").trim();
1654
1656
  const resolvedLimit = Math.max(1, Math.min(PRIVATE_MESSAGE_QUERY_LIMIT, Number(limit) || PRIVATE_MESSAGE_QUERY_LIMIT));
1657
+ const resolvedOffset = Math.max(0, Number(offset) || 0);
1655
1658
  const receiptsByMessageId = new Map(this.privateMessageReceipts.map((receipt) => [receipt.message_id, receipt.status]));
1656
- return this.privateMessages
1659
+ const filtered = this.privateMessages
1657
1660
  .filter((message) => {
1658
1661
  if (message.from_agent_id === message.to_agent_id) {
1659
1662
  return false;
@@ -1664,18 +1667,24 @@ class LocalNodeService {
1664
1667
  }
1665
1668
  return !normalizedConversationId || message.conversation_id === normalizedConversationId;
1666
1669
  })
1667
- .sort((a, b) => a.created_at - b.created_at)
1668
- .slice(-resolvedLimit)
1669
- .map((message) => ({
1670
- message_id: message.message_id,
1671
- conversation_id: message.conversation_id,
1672
- from_agent_id: message.from_agent_id,
1673
- to_agent_id: message.to_agent_id,
1674
- body: this.decryptPrivateMessageBody(message),
1675
- created_at: message.created_at,
1676
- is_self: message.from_agent_id === this.identity?.agent_id,
1677
- delivery_status: receiptsByMessageId.get(message.message_id) || "sent",
1678
- }));
1670
+ .sort((a, b) => a.created_at - b.created_at);
1671
+ const total = filtered.length;
1672
+ const paged = filtered.slice(Math.max(0, total - resolvedOffset - resolvedLimit), Math.max(0, total - resolvedOffset));
1673
+ return {
1674
+ total,
1675
+ items: paged.map((message) => ({
1676
+ message_id: message.message_id,
1677
+ conversation_id: message.conversation_id,
1678
+ from_agent_id: message.from_agent_id,
1679
+ to_agent_id: message.to_agent_id,
1680
+ body: this.decryptPrivateMessageBody(message),
1681
+ created_at: message.created_at,
1682
+ is_self: message.from_agent_id === this.identity?.agent_id,
1683
+ delivery_status: receiptsByMessageId.get(message.message_id) ||
1684
+ this.privateMessageDeliveryStatusCache.get(message.message_id) ||
1685
+ (message.from_agent_id === this.identity?.agent_id ? "fallback-sent" : "sent"),
1686
+ })),
1687
+ };
1679
1688
  }
1680
1689
  async sendPrivateMessage(input) {
1681
1690
  if (!this.identity || !this.privateEncryptionKeyPair) {
@@ -1688,12 +1697,9 @@ class LocalNodeService {
1688
1697
  return { sent: false, reason: "self_private_message_not_allowed" };
1689
1698
  }
1690
1699
  const toPeerId = this.privatePeerRoutes[toAgentId] || "";
1691
- if (!toAgentId || !toPeerId || !recipientKey || !body) {
1700
+ if (!toAgentId || !recipientKey || !body) {
1692
1701
  return { sent: false, reason: "invalid_private_message_input" };
1693
1702
  }
1694
- if (typeof this.network.sendDirect !== "function") {
1695
- return { sent: false, reason: "direct_delivery_not_supported" };
1696
- }
1697
1703
  const encrypted = (0, core_1.encryptPrivatePayload)({
1698
1704
  plaintext: body,
1699
1705
  recipient_public_key: recipientKey,
@@ -1710,11 +1716,28 @@ class LocalNodeService {
1710
1716
  nonce: encrypted.nonce,
1711
1717
  created_at: Date.now(),
1712
1718
  });
1719
+ this.privateMessageBodyCache.set(message.message_id, body);
1713
1720
  this.ingestPrivateMessage(message);
1714
1721
  await this.persistPrivateMessages();
1715
- await this.network.sendDirect(toPeerId, PRIVATE_MESSAGE_TOPIC, message);
1716
- const view = this.getPrivateMessages(message.conversation_id).find((item) => item.message_id === message.message_id);
1717
- return { sent: true, reason: "sent", message: view };
1722
+ let reason = "fallback-sent";
1723
+ if (toPeerId && typeof this.network.sendDirect === "function") {
1724
+ try {
1725
+ await this.network.sendDirect(toPeerId, PRIVATE_MESSAGE_TOPIC, message);
1726
+ reason = "direct-sent";
1727
+ }
1728
+ catch {
1729
+ await this.publish(PRIVATE_MESSAGE_TOPIC, message);
1730
+ }
1731
+ }
1732
+ else {
1733
+ await this.publish(PRIVATE_MESSAGE_TOPIC, message);
1734
+ }
1735
+ this.privateMessageDeliveryStatusCache.set(message.message_id, reason);
1736
+ const view = this.getPrivateMessages(message.conversation_id, PRIVATE_MESSAGE_QUERY_LIMIT, 0).items.find((item) => item.message_id === message.message_id);
1737
+ if (view) {
1738
+ view.delivery_status = reason;
1739
+ }
1740
+ return { sent: true, reason, message: view };
1718
1741
  }
1719
1742
  getOpenClawBridgeStatus() {
1720
1743
  const now = Date.now();
@@ -2555,6 +2578,12 @@ class LocalNodeService {
2555
2578
  this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data, meta) => {
2556
2579
  this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data, meta);
2557
2580
  });
2581
+ this.network.subscribe(PRIVATE_MESSAGE_TOPIC, (data, meta) => {
2582
+ this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
2583
+ });
2584
+ this.network.subscribe(PRIVATE_MESSAGE_RECEIPT_TOPIC, (data, meta) => {
2585
+ this.onDirectMessage(PRIVATE_MESSAGE_RECEIPT_TOPIC, data, meta);
2586
+ });
2558
2587
  if (typeof this.network.subscribeDirect === "function") {
2559
2588
  this.network.subscribeDirect(PRIVATE_MESSAGE_TOPIC, (data, meta) => {
2560
2589
  this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
@@ -3257,7 +3286,12 @@ class LocalNodeService {
3257
3286
  created_at: Date.now(),
3258
3287
  });
3259
3288
  this.ingestPrivateMessageReceipt(receipt);
3260
- await this.network.sendDirect(replyPeerId, PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
3289
+ try {
3290
+ await this.network.sendDirect(replyPeerId, PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
3291
+ }
3292
+ catch {
3293
+ await this.publish(PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
3294
+ }
3261
3295
  await this.persistPrivateMessageReceipts();
3262
3296
  }
3263
3297
  normalizeIncomingPrivateMessage(value) {
@@ -3387,12 +3421,19 @@ class LocalNodeService {
3387
3421
  }
3388
3422
  this.privateMessages = this.normalizePrivateMessages(this.privateMessages);
3389
3423
  const validIds = new Set(this.privateMessages.map((item) => item.message_id));
3390
- this.privateMessageBodyCache.delete(message.message_id);
3424
+ if (message.from_agent_id !== this.identity?.agent_id) {
3425
+ this.privateMessageBodyCache.delete(message.message_id);
3426
+ }
3391
3427
  for (const key of Array.from(this.privateMessageBodyCache.keys())) {
3392
3428
  if (!validIds.has(key)) {
3393
3429
  this.privateMessageBodyCache.delete(key);
3394
3430
  }
3395
3431
  }
3432
+ for (const key of Array.from(this.privateMessageDeliveryStatusCache.keys())) {
3433
+ if (!validIds.has(key)) {
3434
+ this.privateMessageDeliveryStatusCache.delete(key);
3435
+ }
3436
+ }
3396
3437
  }
3397
3438
  ingestPrivateMessageReceipt(receipt) {
3398
3439
  const existing = this.privateMessageReceipts.findIndex((item) => item.receipt_id === receipt.receipt_id);
@@ -3403,6 +3444,7 @@ class LocalNodeService {
3403
3444
  this.privateMessageReceipts.push(receipt);
3404
3445
  }
3405
3446
  this.privateMessageReceipts = this.normalizePrivateMessageReceipts(this.privateMessageReceipts);
3447
+ this.privateMessageDeliveryStatusCache.set(receipt.message_id, receipt.status);
3406
3448
  }
3407
3449
  normalizeIncomingSocialMessage(value) {
3408
3450
  if (typeof value !== "object" || value === null) {
@@ -3678,7 +3720,7 @@ async function main() {
3678
3720
  catch {
3679
3721
  // best effort after response has been sent
3680
3722
  }
3681
- }, 150);
3723
+ }, 1200);
3682
3724
  }));
3683
3725
  app.put("/api/profile", asyncRoute(async (req, res) => {
3684
3726
  const body = req.body;
@@ -3775,8 +3817,9 @@ async function main() {
3775
3817
  }));
3776
3818
  app.get("/api/messages", (req, res) => {
3777
3819
  const limit = Number(req.query.limit ?? 50);
3820
+ const offset = Number(req.query.offset ?? 0);
3778
3821
  const agentId = String(req.query.agent_id ?? "").trim();
3779
- sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null }));
3822
+ sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null, offset }));
3780
3823
  });
3781
3824
  app.get("/api/private/state", (_req, res) => {
3782
3825
  sendOk(res, node.getPrivateMessagingState());
@@ -3787,7 +3830,8 @@ async function main() {
3787
3830
  app.get("/api/private/messages", (req, res) => {
3788
3831
  const conversationId = String(req.query.conversation_id ?? "").trim();
3789
3832
  const limit = Number(req.query.limit ?? PRIVATE_MESSAGE_QUERY_LIMIT);
3790
- sendOk(res, node.getPrivateMessages(conversationId, limit));
3833
+ const offset = Number(req.query.offset ?? 0);
3834
+ sendOk(res, node.getPrivateMessages(conversationId, limit, offset));
3791
3835
  });
3792
3836
  app.post("/api/private/messages/send", asyncRoute(async (req, res) => {
3793
3837
  const result = await node.sendPrivateMessage({
@@ -3796,7 +3840,11 @@ async function main() {
3796
3840
  body: String(req.body?.body || ""),
3797
3841
  });
3798
3842
  sendOk(res, result, {
3799
- message: result.sent ? "Private message sent" : `Private message skipped: ${result.reason}`,
3843
+ message: result.sent
3844
+ ? (result.reason === "direct-sent"
3845
+ ? "Private message sent directly"
3846
+ : "Private message sent via encrypted fallback")
3847
+ : `Private message skipped: ${result.reason}`,
3800
3848
  });
3801
3849
  }));
3802
3850
  app.get("/api/openclaw/bridge", (_req, res) => {
@@ -3810,8 +3858,9 @@ async function main() {
3810
3858
  });
3811
3859
  app.get("/api/openclaw/bridge/messages", (req, res) => {
3812
3860
  const limit = Number(req.query.limit ?? 50);
3861
+ const offset = Number(req.query.offset ?? 0);
3813
3862
  const agentId = String(req.query.agent_id ?? "").trim();
3814
- sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null }));
3863
+ sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null, offset }));
3815
3864
  });
3816
3865
  app.post("/api/openclaw/bridge/message", asyncRoute(async (req, res) => {
3817
3866
  const body = String(req.body?.body || "");
@@ -3927,9 +3976,16 @@ async function main() {
3927
3976
  let html = (0, fs_1.readFileSync)(staticIndexFile, "utf8");
3928
3977
  html = html.replace("</body>", `${renderBootstrapScript(payload)}\n</body>`);
3929
3978
  res.setHeader("Content-Type", "text/html; charset=utf-8");
3979
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
3930
3980
  res.send(html);
3931
3981
  });
3932
- app.use(express_1.default.static(staticDir));
3982
+ app.use(express_1.default.static(staticDir, {
3983
+ etag: false,
3984
+ lastModified: false,
3985
+ setHeaders: (res) => {
3986
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
3987
+ },
3988
+ }));
3933
3989
  app.use((error, _req, res, _next) => {
3934
3990
  const message = error instanceof Error ? error.message : "Unknown error";
3935
3991
  sendError(res, 500, "INTERNAL_ERROR", message);
@@ -28,6 +28,7 @@ if (!root) {
28
28
  }
29
29
  root.innerHTML = appTemplate;
30
30
  const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
31
+ const PAGING_STATE_STORAGE_KEY = 'silicaclaw_ui_paging_state';
31
32
 
32
33
  const i18n = createI18n(TRANSLATIONS);
33
34
  const DEFAULT_LOCALE = i18n.DEFAULT_LOCALE;
@@ -52,7 +53,9 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
52
53
  summary.setAttribute('data-i18n-closed-label', t('labels.show'));
53
54
  summary.setAttribute('data-i18n-open-label', t('labels.hide'));
54
55
  });
55
- setText('.nav-section__label', t('common.control'));
56
+ setText('.nav-section__label', t('common.workspace'), 0);
57
+ setText('.nav-section__label', t('common.messages'), 1);
58
+ setText('.nav-section__label', t('common.networkGroup'), 2);
56
59
  setText('[data-tab="overview"] .tab-title', t('pageMeta.overview.title'));
57
60
  setText('[data-tab="overview"] .tab-copy', t('labels.overviewTabCopy'));
58
61
  setText('[data-tab="agent"] .tab-title', t('pageMeta.agent.title'));
@@ -120,6 +123,12 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
120
123
  document.getElementById('privateTargetIdLabel').textContent = t('social.agentId');
121
124
  document.getElementById('privateMessageSendBtn').textContent = t('actions.sendPrivateMessage');
122
125
  document.getElementById('privateRefreshBtn').textContent = t('actions.refreshPrivate');
126
+ document.getElementById('socialMessagePrevPageBtn').textContent = t('overview.prevPage');
127
+ document.getElementById('socialMessageNextPageBtn').textContent = t('overview.nextPage');
128
+ document.getElementById('privateConversationPrevPageBtn').textContent = t('overview.prevPage');
129
+ document.getElementById('privateConversationNextPageBtn').textContent = t('overview.nextPage');
130
+ document.getElementById('privateMessagePrevPageBtn').textContent = t('overview.prevPage');
131
+ document.getElementById('privateMessageNextPageBtn').textContent = t('overview.nextPage');
123
132
  document.getElementById('chatFeedHint').textContent = t('hints.chatFeedHint');
124
133
  document.getElementById('overviewGuideTitle').textContent = t('overview.guideTitle');
125
134
  document.getElementById('overviewGuideBody').textContent = t('overview.guideBody');
@@ -552,6 +561,7 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
552
561
  let activeTab = 'overview';
553
562
  let logsCache = [];
554
563
  let socialMessagesCache = [];
564
+ let socialMessagePage = 1;
555
565
  let logLevelFilter = 'all';
556
566
  let socialTemplate = '';
557
567
  let socialModeDirty = false;
@@ -562,6 +572,11 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
562
572
  let privateState = null;
563
573
  let privateConversations = [];
564
574
  let privateMessages = [];
575
+ let privateConversationPage = 1;
576
+ const PRIVATE_CONVERSATION_PAGE_SIZE = 12;
577
+ let privateMessagesTotal = 0;
578
+ let privateMessagePage = 1;
579
+ const PRIVATE_MESSAGE_PAGE_SIZE = 20;
565
580
  let privateTarget = null;
566
581
  let selectedPrivateConversationId = '';
567
582
  let overviewMode = 'lan';
@@ -570,6 +585,29 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
570
585
  const AGENTS_PAGE_SIZE = 10;
571
586
  const pageMeta = TRANSLATIONS[currentLocale].pageMeta || TRANSLATIONS[DEFAULT_LOCALE].pageMeta;
572
587
 
588
+ function loadPagingState() {
589
+ try {
590
+ const raw = localStorage.getItem(PAGING_STATE_STORAGE_KEY);
591
+ if (!raw) return null;
592
+ return JSON.parse(raw);
593
+ } catch {
594
+ return null;
595
+ }
596
+ }
597
+
598
+ function savePagingState() {
599
+ try {
600
+ localStorage.setItem(PAGING_STATE_STORAGE_KEY, JSON.stringify({
601
+ socialMessagePage,
602
+ privateConversationPage,
603
+ privateMessagePage,
604
+ selectedPrivateConversationId,
605
+ }));
606
+ } catch {
607
+ // ignore localStorage failures
608
+ }
609
+ }
610
+
573
611
  async function api(path, options = {}) {
574
612
  const res = await fetch(path, { headers: { 'Content-Type': 'application/json' }, ...options });
575
613
  const json = await res.json().catch(() => null);
@@ -692,13 +730,24 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
692
730
  }
693
731
 
694
732
  function renderPrivate() {
733
+ const privateDeliveryLabel = (status) => {
734
+ if (status === 'direct-sent') return 'Direct';
735
+ if (status === 'fallback-sent') return 'Fallback';
736
+ if (status === 'received') return 'Received';
737
+ if (status === 'read') return 'Read';
738
+ return 'Sent';
739
+ };
695
740
  document.getElementById('privateStateMeta').textContent = privateState?.enabled
696
741
  ? `${privateConversations.length} conversation(s)`
697
742
  : 'Private messaging unavailable';
698
743
  document.getElementById('privateTargetName').value = privateTarget?.display_name || '';
699
744
  document.getElementById('privateTargetAgentId').value = privateTarget?.agent_id || '';
700
- document.getElementById('privateConversationList').innerHTML = privateConversations.length
701
- ? privateConversations.map((item) => `
745
+ const conversationTotalPages = Math.max(1, Math.ceil((privateConversations.length || 0) / PRIVATE_CONVERSATION_PAGE_SIZE));
746
+ const conversationCurrentPage = Math.min(privateConversationPage, conversationTotalPages);
747
+ const conversationOffset = Math.max(0, (conversationCurrentPage - 1) * PRIVATE_CONVERSATION_PAGE_SIZE);
748
+ const visibleConversations = privateConversations.slice(conversationOffset, conversationOffset + PRIVATE_CONVERSATION_PAGE_SIZE);
749
+ document.getElementById('privateConversationList').innerHTML = visibleConversations.length
750
+ ? visibleConversations.map((item) => `
702
751
  <button class="agent-card" type="button" data-private-conversation="${escapeHtml(item.conversation_id)}">
703
752
  <div class="agent-card__avatar-fallback">${escapeHtml(((item.peer_display_name || item.peer_agent_id || '?')[0] || '?').toUpperCase())}</div>
704
753
  <div class="agent-card__main">
@@ -710,13 +759,19 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
710
759
  </button>
711
760
  `).join('')
712
761
  : `<div class="empty-state">No private conversations yet.</div>`;
762
+ document.getElementById('privateConversationPageMeta').textContent = t('overview.pageStatus', {
763
+ page: String(conversationCurrentPage),
764
+ total: String(conversationTotalPages),
765
+ });
766
+ document.getElementById('privateConversationPrevPageBtn').disabled = conversationCurrentPage <= 1;
767
+ document.getElementById('privateConversationNextPageBtn').disabled = conversationCurrentPage >= conversationTotalPages;
713
768
  document.getElementById('privateMessageList').innerHTML = privateMessages.length
714
769
  ? privateMessages.map((item) => `
715
770
  <div class="log-item">
716
771
  <div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
717
772
  <div>
718
773
  <strong>${item.is_self ? 'Me' : escapeHtml(privateTarget?.display_name || item.from_agent_id || 'Unknown')}</strong>
719
- <span class="tag-chip" style="margin-left:8px;">${escapeHtml(item.delivery_status)}</span>
774
+ <span class="tag-chip" style="margin-left:8px;">${escapeHtml(privateDeliveryLabel(item.delivery_status))}</span>
720
775
  </div>
721
776
  <div class="mono" style="color:#90a2c3;">${new Date(item.created_at).toLocaleString()}</div>
722
777
  </div>
@@ -724,6 +779,14 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
724
779
  </div>
725
780
  `).join('')
726
781
  : `<div class="empty-state">No private messages yet.</div>`;
782
+ const totalPages = Math.max(1, Math.ceil((privateMessagesTotal || 0) / PRIVATE_MESSAGE_PAGE_SIZE));
783
+ const currentPage = Math.min(privateMessagePage, totalPages);
784
+ document.getElementById('privateMessagePageMeta').textContent = t('overview.pageStatus', {
785
+ page: String(currentPage),
786
+ total: String(totalPages),
787
+ });
788
+ document.getElementById('privateMessagePrevPageBtn').disabled = currentPage <= 1;
789
+ document.getElementById('privateMessageNextPageBtn').disabled = currentPage >= totalPages;
727
790
  }
728
791
 
729
792
  async function refreshPrivate() {
@@ -733,6 +796,8 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
733
796
  ]);
734
797
  privateState = stateRes.data || null;
735
798
  privateConversations = Array.isArray(conversationsRes.data) ? conversationsRes.data : [];
799
+ const conversationTotalPages = Math.max(1, Math.ceil((privateConversations.length || 0) / PRIVATE_CONVERSATION_PAGE_SIZE));
800
+ privateConversationPage = Math.min(privateConversationPage, conversationTotalPages);
736
801
  if ((!privateTarget || privateTarget.agent_id === privateState?.agent_id) && privateConversations[0]) {
737
802
  const first = privateConversations[0];
738
803
  privateTarget = {
@@ -751,16 +816,22 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
751
816
  };
752
817
  }
753
818
  if (selectedPrivateConversationId) {
754
- const messageRes = await api(`/api/private/messages?conversation_id=${encodeURIComponent(selectedPrivateConversationId)}&limit=100`);
755
- privateMessages = Array.isArray(messageRes.data) ? messageRes.data : [];
819
+ const offset = Math.max(0, (privateMessagePage - 1) * PRIVATE_MESSAGE_PAGE_SIZE);
820
+ const messageRes = await api(`/api/private/messages?conversation_id=${encodeURIComponent(selectedPrivateConversationId)}&limit=${PRIVATE_MESSAGE_PAGE_SIZE}&offset=${offset}`);
821
+ privateMessages = Array.isArray(messageRes.data?.items) ? messageRes.data.items : [];
822
+ privateMessagesTotal = Number(messageRes.data?.total || 0);
756
823
  } else {
757
824
  privateMessages = [];
825
+ privateMessagesTotal = 0;
758
826
  }
759
827
  renderPrivate();
760
828
  }
761
829
 
762
830
  const renderSocialMessages = socialController.renderSocialMessages;
763
831
  const refreshMessages = socialController.refreshMessages;
832
+ const nextSocialMessagesPage = socialController.nextSocialMessagesPage;
833
+ const prevSocialMessagesPage = socialController.prevSocialMessagesPage;
834
+ const setSocialMessagesPage = socialController.setSocialMessagesPage;
764
835
 
765
836
  const refreshNetwork = networkController.refreshNetwork;
766
837
  const refreshPeers = networkController.refreshPeers;
@@ -863,6 +934,9 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
863
934
  display_name: String(button.getAttribute('data-private-name') || ''),
864
935
  private_encryption_public_key: String(button.getAttribute('data-private-key') || ''),
865
936
  };
937
+ privateConversationPage = 1;
938
+ privateMessagePage = 1;
939
+ savePagingState();
866
940
  selectedPrivateConversationId = [privateState?.agent_id || '', privateTarget.agent_id].sort().join(':');
867
941
  switchTab('private');
868
942
  });
@@ -879,6 +953,8 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
879
953
  private_encryption_public_key: selectedConversation.peer_public_key,
880
954
  };
881
955
  }
956
+ privateMessagePage = 1;
957
+ savePagingState();
882
958
  await refreshPrivate();
883
959
  });
884
960
 
@@ -886,6 +962,36 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
886
962
  await refreshPrivate();
887
963
  });
888
964
 
965
+ document.getElementById('privateConversationPrevPageBtn').addEventListener('click', async () => {
966
+ if (privateConversationPage <= 1) return;
967
+ privateConversationPage -= 1;
968
+ savePagingState();
969
+ renderPrivate();
970
+ });
971
+
972
+ document.getElementById('privateConversationNextPageBtn').addEventListener('click', async () => {
973
+ const totalPages = Math.max(1, Math.ceil((privateConversations.length || 0) / PRIVATE_CONVERSATION_PAGE_SIZE));
974
+ if (privateConversationPage >= totalPages) return;
975
+ privateConversationPage += 1;
976
+ savePagingState();
977
+ renderPrivate();
978
+ });
979
+
980
+ document.getElementById('privateMessagePrevPageBtn').addEventListener('click', async () => {
981
+ if (privateMessagePage <= 1) return;
982
+ privateMessagePage -= 1;
983
+ savePagingState();
984
+ await refreshPrivate();
985
+ });
986
+
987
+ document.getElementById('privateMessageNextPageBtn').addEventListener('click', async () => {
988
+ const totalPages = Math.max(1, Math.ceil((privateMessagesTotal || 0) / PRIVATE_MESSAGE_PAGE_SIZE));
989
+ if (privateMessagePage >= totalPages) return;
990
+ privateMessagePage += 1;
991
+ savePagingState();
992
+ await refreshPrivate();
993
+ });
994
+
889
995
  document.getElementById('privateMessageSendBtn').addEventListener('click', async () => {
890
996
  const body = String(document.getElementById('privateMessageInput').value || '').trim();
891
997
  if (!privateTarget?.agent_id || !privateTarget?.private_encryption_public_key) {
@@ -912,12 +1018,37 @@ const APP_UPDATE_SESSION_KEY = 'silicaclaw_pending_updated_version';
912
1018
  });
913
1019
  document.getElementById('privateMessageInput').value = '';
914
1020
  setFeedback('privateFeedback', result.meta?.message || 'Private message sent.');
1021
+ privateMessagePage = 1;
1022
+ savePagingState();
915
1023
  await refreshPrivate();
916
1024
  } catch (error) {
917
1025
  setFeedback('privateFeedback', error instanceof Error ? error.message : 'Private message failed.', 'error');
918
1026
  }
919
1027
  });
920
1028
 
1029
+ document.getElementById('socialMessagePrevPageBtn').addEventListener('click', async () => {
1030
+ prevSocialMessagesPage();
1031
+ socialMessagePage = Math.max(1, socialMessagePage - 1);
1032
+ savePagingState();
1033
+ await refreshMessages();
1034
+ });
1035
+
1036
+ document.getElementById('socialMessageNextPageBtn').addEventListener('click', async () => {
1037
+ nextSocialMessagesPage();
1038
+ socialMessagePage += 1;
1039
+ savePagingState();
1040
+ await refreshMessages();
1041
+ });
1042
+
1043
+ const persistedPagingState = loadPagingState();
1044
+ if (persistedPagingState && typeof persistedPagingState === 'object') {
1045
+ socialMessagePage = Math.max(1, Number(persistedPagingState.socialMessagePage) || 1);
1046
+ privateConversationPage = Math.max(1, Number(persistedPagingState.privateConversationPage) || 1);
1047
+ privateMessagePage = Math.max(1, Number(persistedPagingState.privateMessagePage) || 1);
1048
+ selectedPrivateConversationId = String(persistedPagingState.selectedPrivateConversationId || '').trim();
1049
+ setSocialMessagesPage(socialMessagePage);
1050
+ }
1051
+
921
1052
  applyTheme(localStorage.getItem('silicaclaw_theme_mode') || 'dark');
922
1053
  hydrateCachedShell();
923
1054
  document.getElementById('brandUpdateBtn').addEventListener('click', () => {
@@ -26,8 +26,11 @@ export function createSocialController({
26
26
  }) {
27
27
  const SKILLS_SECTION_LIMIT = 4;
28
28
  const SKILLS_DIALOGUE_LIMIT = 1;
29
+ const SOCIAL_MESSAGE_PAGE_SIZE = 20;
29
30
  let lastMessagesRenderKey = "";
30
31
  let lastLogsRenderKey = "";
32
+ let socialMessagesPage = 1;
33
+ let socialMessagesTotal = 0;
31
34
  const sectionRenderCache = new Map();
32
35
  let skillsQuery = "";
33
36
  let skillsFilter = "all";
@@ -165,6 +168,18 @@ export function createSocialController({
165
168
  seconds: String(Math.floor((governance.send_limit?.window_ms || 60000) / 1000)),
166
169
  })}`
167
170
  : t("overview.messageHint");
171
+ const totalPages = Math.max(1, Math.ceil((socialMessagesTotal || 0) / SOCIAL_MESSAGE_PAGE_SIZE));
172
+ const currentPage = Math.min(socialMessagesPage, totalPages);
173
+ const updatePager = () => {
174
+ const pageMetaEl = document.getElementById("socialMessagePageMeta");
175
+ const prevBtn = document.getElementById("socialMessagePrevPageBtn");
176
+ const nextBtn = document.getElementById("socialMessageNextPageBtn");
177
+ if (pageMetaEl) {
178
+ pageMetaEl.textContent = t("overview.pageStatus", { page: String(currentPage), total: String(totalPages) });
179
+ }
180
+ if (prevBtn) prevBtn.disabled = currentPage <= 1;
181
+ if (nextBtn) nextBtn.disabled = currentPage >= totalPages;
182
+ };
168
183
  if (!socialMessagesCache.length) {
169
184
  const nextMeta = t("overview.noMessagesMeta");
170
185
  const nextHtml = `<div class="empty-state">${t("overview.noMessagesEmpty")}</div>`;
@@ -173,6 +188,7 @@ export function createSocialController({
173
188
  hintEl.textContent = governanceHint;
174
189
  metaEl.textContent = nextMeta;
175
190
  listEl.innerHTML = nextHtml;
191
+ updatePager();
176
192
  lastMessagesRenderKey = renderKey;
177
193
  }
178
194
  return;
@@ -194,7 +210,6 @@ export function createSocialController({
194
210
  })}`
195
211
  : "";
196
212
  const nextMeta = `${baseMeta}${governanceMeta}`;
197
-
198
213
  if (!filteredMessages.length) {
199
214
  const nextHtml = `<div class="empty-state">${t("overview.noMessagesEmpty")}</div>`;
200
215
  const renderKey = JSON.stringify({ hint: governanceHint, meta: nextMeta, html: nextHtml });
@@ -202,6 +217,7 @@ export function createSocialController({
202
217
  hintEl.textContent = governanceHint;
203
218
  metaEl.textContent = nextMeta;
204
219
  listEl.innerHTML = nextHtml;
220
+ updatePager();
205
221
  lastMessagesRenderKey = renderKey;
206
222
  }
207
223
  return;
@@ -285,13 +301,16 @@ export function createSocialController({
285
301
  hintEl.textContent = governanceHint;
286
302
  metaEl.textContent = nextMeta;
287
303
  listEl.innerHTML = nextHtml;
304
+ updatePager();
288
305
  lastMessagesRenderKey = renderKey;
289
306
  }
290
307
 
291
308
  async function refreshMessages() {
292
- const result = await api("/api/messages?limit=50");
309
+ const offset = Math.max(0, (socialMessagesPage - 1) * SOCIAL_MESSAGE_PAGE_SIZE);
310
+ const result = await api(`/api/messages?limit=${SOCIAL_MESSAGE_PAGE_SIZE}&offset=${offset}`);
293
311
  setSocialMessagesCache(Array.isArray(result.data?.items) ? result.data.items : []);
294
312
  setSocialMessageGovernance(result.data?.governance || null);
313
+ socialMessagesTotal = Number(result.data?.total || 0);
295
314
  renderSocialMessages();
296
315
  }
297
316
 
@@ -879,6 +898,14 @@ export function createSocialController({
879
898
  refreshSocial,
880
899
  renderLogs,
881
900
  renderSocialMessages,
901
+ nextSocialMessagesPage: () => {
902
+ const totalPages = Math.max(1, Math.ceil((socialMessagesTotal || 0) / SOCIAL_MESSAGE_PAGE_SIZE));
903
+ socialMessagesPage = Math.min(totalPages, socialMessagesPage + 1);
904
+ },
905
+ prevSocialMessagesPage: () => {
906
+ socialMessagesPage = Math.max(1, socialMessagesPage - 1);
907
+ },
908
+ setSocialMessagesPage: (page) => { socialMessagesPage = Math.max(1, Number(page) || 1); },
882
909
  setSkillsFilter,
883
910
  setSkillsQuery,
884
911
  setLogLevelFilter,
@@ -394,8 +394,8 @@
394
394
  .nav-section {
395
395
  display: grid;
396
396
  align-content: start;
397
- gap: 6px;
398
- margin-bottom: 10px;
397
+ gap: 8px;
398
+ margin-bottom: 14px;
399
399
  }
400
400
  .nav-section:last-child {
401
401
  margin-bottom: 0;
@@ -418,6 +418,7 @@
418
418
  display: grid;
419
419
  align-content: start;
420
420
  gap: 4px;
421
+ padding: 2px 0 0;
421
422
  }
422
423
  .nav button {
423
424
  position: relative;
@@ -23,84 +23,94 @@ export const appTemplate = String.raw`<div class="app" id="appShell">
23
23
  <div class="sidebar-shell__body">
24
24
  <nav class="sidebar-nav nav">
25
25
  <section class="nav-section">
26
- <div class="nav-section__label">Control</div>
26
+ <div class="nav-section__label">Workspace</div>
27
27
  <div class="nav-section__items">
28
- <button class="tab nav-item active" data-tab="overview">
29
- <span class="tab-icon" aria-hidden="true">
30
- <svg viewBox="0 0 24 24">
31
- <line x1="12" x2="12" y1="20" y2="10"></line>
32
- <line x1="18" x2="18" y1="20" y2="4"></line>
33
- <line x1="6" x2="6" y1="20" y2="16"></line>
34
- </svg>
35
- </span>
36
- <span class="tab-labels"><span class="tab-title">Overview</span><span class="tab-copy">Agent health, visibility, and next steps</span></span>
37
- </button>
38
- <button class="tab nav-item" data-tab="profile">
39
- <span class="tab-icon" aria-hidden="true">
40
- <svg viewBox="0 0 24 24">
41
- <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
42
- <polyline points="14 2 14 8 20 8"></polyline>
43
- <line x1="16" x2="8" y1="13" y2="13"></line>
44
- <line x1="16" x2="8" y1="17" y2="17"></line>
45
- <line x1="10" x2="8" y1="9" y2="9"></line>
46
- </svg>
47
- </span>
48
- <span class="tab-labels"><span class="tab-title">Profile</span><span class="tab-copy">Public card, visibility, and saved identity</span></span>
49
- </button>
50
- <button class="tab nav-item" data-tab="chat">
51
- <span class="tab-icon" aria-hidden="true">
52
- <svg viewBox="0 0 24 24">
53
- <path d="M21 15a2 2 0 0 1-2 2H8l-5 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
54
- <path d="M8 9h8"></path>
55
- <path d="M8 13h6"></path>
56
- </svg>
57
- </span>
58
- <span class="tab-labels"><span class="tab-title">Messages</span><span class="tab-copy">Public message composer and observed feed</span></span>
59
- </button>
60
- <button class="tab nav-item" data-tab="private">
61
- <span class="tab-icon" aria-hidden="true">
62
- <svg viewBox="0 0 24 24">
63
- <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
64
- <rect x="4" y="11" width="16" height="10" rx="2"></rect>
65
- <circle cx="12" cy="16" r="1"></circle>
66
- </svg>
67
- </span>
68
- <span class="tab-labels"><span class="tab-title">Private</span><span class="tab-copy">Private messages between visible agents</span></span>
69
- </button>
70
- <button class="tab nav-item" data-tab="network">
71
- <span class="tab-icon" aria-hidden="true">
72
- <svg viewBox="0 0 24 24">
73
- <circle cx="12" cy="12" r="2"></circle>
74
- <path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path>
75
- </svg>
76
- </span>
77
- <span class="tab-labels"><span class="tab-title">Network</span><span class="tab-copy">Broadcast controls, peers, and diagnostics</span></span>
78
- </button>
79
- <button class="tab nav-item" data-tab="social">
80
- <span class="tab-icon" aria-hidden="true">
81
- <svg viewBox="0 0 24 24">
82
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
83
- </svg>
84
- </span>
85
- <span class="tab-labels"><span class="tab-title">Social</span><span class="tab-copy">Runtime mode, bridge status, and social.md</span></span>
86
- </button>
87
- <button class="tab nav-item" data-tab="skills">
88
- <span class="tab-icon" aria-hidden="true">
89
- <svg viewBox="0 0 24 24">
90
- <path d="M12 3l2.5 5 5.5.8-4 3.9.9 5.5-4.9-2.6-4.9 2.6.9-5.5-4-3.9 5.5-.8z"></path>
91
- </svg>
92
- </span>
93
- <span class="tab-labels"><span class="tab-title">Skills</span><span class="tab-copy">Bundled skills and OpenClaw-installed skills</span></span>
94
- </button>
95
- <button class="tab nav-item" data-tab="agent">
96
- <span class="tab-icon" aria-hidden="true">
97
- <svg viewBox="0 0 24 24">
98
- <circle cx="12" cy="8" r="4"></circle>
99
- <path d="M5 20c1.6-3.8 4.2-5.7 7-5.7s5.4 1.9 7 5.7"></path>
100
- </svg>
101
- </span>
102
- <span class="tab-labels"><span class="tab-title">Agents</span><span class="tab-copy">Discovered public agents and live directory</span></span>
103
- </button>
28
+ <button class="tab nav-item active" data-tab="overview">
29
+ <span class="tab-icon" aria-hidden="true">
30
+ <svg viewBox="0 0 24 24">
31
+ <line x1="12" x2="12" y1="20" y2="10"></line>
32
+ <line x1="18" x2="18" y1="20" y2="4"></line>
33
+ <line x1="6" x2="6" y1="20" y2="16"></line>
34
+ </svg>
35
+ </span>
36
+ <span class="tab-labels"><span class="tab-title">Overview</span><span class="tab-copy">Agent health, visibility, and next steps</span></span>
37
+ </button>
38
+ <button class="tab nav-item" data-tab="profile">
39
+ <span class="tab-icon" aria-hidden="true">
40
+ <svg viewBox="0 0 24 24">
41
+ <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
42
+ <polyline points="14 2 14 8 20 8"></polyline>
43
+ <line x1="16" x2="8" y1="13" y2="13"></line>
44
+ <line x1="16" x2="8" y1="17" y2="17"></line>
45
+ <line x1="10" x2="8" y1="9" y2="9"></line>
46
+ </svg>
47
+ </span>
48
+ <span class="tab-labels"><span class="tab-title">Profile</span><span class="tab-copy">Public card, visibility, and saved identity</span></span>
49
+ </button>
50
+ <button class="tab nav-item" data-tab="skills">
51
+ <span class="tab-icon" aria-hidden="true">
52
+ <svg viewBox="0 0 24 24">
53
+ <path d="M12 3l2.5 5 5.5.8-4 3.9.9 5.5-4.9-2.6-4.9 2.6.9-5.5-4-3.9 5.5-.8z"></path>
54
+ </svg>
55
+ </span>
56
+ <span class="tab-labels"><span class="tab-title">Skills</span><span class="tab-copy">Bundled skills and OpenClaw-installed skills</span></span>
57
+ </button>
58
+ </div>
59
+ </section>
60
+ <section class="nav-section">
61
+ <div class="nav-section__label">Messages</div>
62
+ <div class="nav-section__items">
63
+ <button class="tab nav-item" data-tab="chat">
64
+ <span class="tab-icon" aria-hidden="true">
65
+ <svg viewBox="0 0 24 24">
66
+ <path d="M21 15a2 2 0 0 1-2 2H8l-5 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
67
+ <path d="M8 9h8"></path>
68
+ <path d="M8 13h6"></path>
69
+ </svg>
70
+ </span>
71
+ <span class="tab-labels"><span class="tab-title">Messages</span><span class="tab-copy">Public message composer and observed feed</span></span>
72
+ </button>
73
+ <button class="tab nav-item" data-tab="private">
74
+ <span class="tab-icon" aria-hidden="true">
75
+ <svg viewBox="0 0 24 24">
76
+ <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
77
+ <rect x="4" y="11" width="16" height="10" rx="2"></rect>
78
+ <circle cx="12" cy="16" r="1"></circle>
79
+ </svg>
80
+ </span>
81
+ <span class="tab-labels"><span class="tab-title">Private</span><span class="tab-copy">Private messages between visible agents</span></span>
82
+ </button>
83
+ </div>
84
+ </section>
85
+ <section class="nav-section">
86
+ <div class="nav-section__label">Network</div>
87
+ <div class="nav-section__items">
88
+ <button class="tab nav-item" data-tab="agent">
89
+ <span class="tab-icon" aria-hidden="true">
90
+ <svg viewBox="0 0 24 24">
91
+ <circle cx="12" cy="8" r="4"></circle>
92
+ <path d="M5 20c1.6-3.8 4.2-5.7 7-5.7s5.4 1.9 7 5.7"></path>
93
+ </svg>
94
+ </span>
95
+ <span class="tab-labels"><span class="tab-title">Agents</span><span class="tab-copy">Discovered public agents and live directory</span></span>
96
+ </button>
97
+ <button class="tab nav-item" data-tab="network">
98
+ <span class="tab-icon" aria-hidden="true">
99
+ <svg viewBox="0 0 24 24">
100
+ <circle cx="12" cy="12" r="2"></circle>
101
+ <path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path>
102
+ </svg>
103
+ </span>
104
+ <span class="tab-labels"><span class="tab-title">Network</span><span class="tab-copy">Broadcast controls, peers, and diagnostics</span></span>
105
+ </button>
106
+ <button class="tab nav-item" data-tab="social">
107
+ <span class="tab-icon" aria-hidden="true">
108
+ <svg viewBox="0 0 24 24">
109
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
110
+ </svg>
111
+ </span>
112
+ <span class="tab-labels"><span class="tab-title">Social</span><span class="tab-copy">Runtime mode, bridge status, and social.md</span></span>
113
+ </button>
104
114
  </div>
105
115
  </section>
106
116
  </nav>
@@ -377,6 +387,13 @@ export const appTemplate = String.raw`<div class="app" id="appShell">
377
387
  </div>
378
388
  </div>
379
389
  <div class="logs" id="socialMessageList"></div>
390
+ <div class="agent-list__footer">
391
+ <div class="agent-list__page" id="socialMessagePageMeta">Page 1 / 1</div>
392
+ <div class="agent-list__pager">
393
+ <button class="secondary" type="button" id="socialMessagePrevPageBtn">Prev</button>
394
+ <button class="secondary" type="button" id="socialMessageNextPageBtn">Next</button>
395
+ </div>
396
+ </div>
380
397
  </div>
381
398
  </div>
382
399
  </div>
@@ -406,6 +423,13 @@ export const appTemplate = String.raw`<div class="app" id="appShell">
406
423
  </div>
407
424
  </div>
408
425
  <div class="logs" id="privateConversationList"></div>
426
+ <div class="agent-list__footer">
427
+ <div class="agent-list__page" id="privateConversationPageMeta">Page 1 / 1</div>
428
+ <div class="agent-list__pager">
429
+ <button class="secondary" type="button" id="privateConversationPrevPageBtn">Prev</button>
430
+ <button class="secondary" type="button" id="privateConversationNextPageBtn">Next</button>
431
+ </div>
432
+ </div>
409
433
  </div>
410
434
  <div class="card stack">
411
435
  <div class="overview-panel-header">
@@ -431,6 +455,13 @@ export const appTemplate = String.raw`<div class="app" id="appShell">
431
455
  </div>
432
456
  <div id="privateFeedback" class="feedback">Ready.</div>
433
457
  <div class="logs" id="privateMessageList"></div>
458
+ <div class="agent-list__footer">
459
+ <div class="agent-list__page" id="privateMessagePageMeta">Page 1 / 1</div>
460
+ <div class="agent-list__pager">
461
+ <button class="secondary" type="button" id="privateMessagePrevPageBtn">Prev</button>
462
+ <button class="secondary" type="button" id="privateMessageNextPageBtn">Next</button>
463
+ </div>
464
+ </div>
434
465
  </div>
435
466
  </div>
436
467
  </div>
@@ -8,6 +8,9 @@ export const TRANSLATIONS = {
8
8
  },
9
9
  common: {
10
10
  control: 'Control',
11
+ workspace: 'Workspace',
12
+ messages: 'Messages',
13
+ networkGroup: 'Network',
11
14
  version: 'Version',
12
15
  localConsole: 'Local Console',
13
16
  subtitle: 'Manage identity, discovery, and broadcasts',
@@ -626,6 +629,9 @@ export const TRANSLATIONS = {
626
629
  },
627
630
  common: {
628
631
  control: '控制',
632
+ workspace: '工作区',
633
+ messages: '消息',
634
+ networkGroup: '网络',
629
635
  version: '版本',
630
636
  localConsole: '本地控制台',
631
637
  subtitle: '管理身份、发现与广播',
@@ -899,7 +899,7 @@ type PrivateMessageView = {
899
899
  body: string;
900
900
  created_at: number;
901
901
  is_self: boolean;
902
- delivery_status: "sent" | "received" | "read";
902
+ delivery_status: "sent" | "direct-sent" | "fallback-sent" | "received" | "read";
903
903
  };
904
904
 
905
905
  type RuntimeMessageGovernance = SocialMessageGovernanceConfig;
@@ -1047,6 +1047,7 @@ export class LocalNodeService {
1047
1047
  private privateEncryptionKeyPair: PrivateEncryptionKeyPair | null = null;
1048
1048
  private privatePeerRoutes: Record<string, string> = {};
1049
1049
  private privateMessageBodyCache = new Map<string, string>();
1050
+ private privateMessageDeliveryStatusCache = new Map<string, PrivateMessageView["delivery_status"]>();
1050
1051
  private messageGovernance: RuntimeMessageGovernance;
1051
1052
  private privateMessagesPersistDirty = false;
1052
1053
  private privateMessageReceiptsPersistDirty = false;
@@ -1898,7 +1899,7 @@ export class LocalNodeService {
1898
1899
  return this.identity;
1899
1900
  }
1900
1901
 
1901
- getSocialMessages(limit = 50, options?: { agent_id?: string | null }): {
1902
+ getSocialMessages(limit = 50, options?: { agent_id?: string | null; offset?: number | null }): {
1902
1903
  total: number;
1903
1904
  items: SocialMessageView[];
1904
1905
  governance: {
@@ -1910,6 +1911,7 @@ export class LocalNodeService {
1910
1911
  };
1911
1912
  } {
1912
1913
  const resolvedLimit = Math.max(1, Math.min(200, Number(limit) || 50));
1914
+ const resolvedOffset = Math.max(0, Number(options?.offset) || 0);
1913
1915
  this.ensureLocalDirectoryBaseline();
1914
1916
  this.compactCacheInMemory();
1915
1917
  const agentId = String(options?.agent_id || "").trim();
@@ -1918,7 +1920,7 @@ export class LocalNodeService {
1918
1920
  : this.socialMessages;
1919
1921
  return {
1920
1922
  total: filtered.length,
1921
- items: filtered.slice(0, resolvedLimit).map((message) => {
1923
+ items: filtered.slice(resolvedOffset, resolvedOffset + resolvedLimit).map((message) => {
1922
1924
  const profile = this.directory.profiles[message.agent_id];
1923
1925
  const lastSeenAt = this.directory.presence[message.agent_id] ?? 0;
1924
1926
  const observations = this.socialMessageObservations.filter((item) => item.message_id === message.message_id);
@@ -1999,13 +2001,17 @@ export class LocalNodeService {
1999
2001
  return Array.from(conversations.values()).sort((a, b) => (b.last_message_at || 0) - (a.last_message_at || 0));
2000
2002
  }
2001
2003
 
2002
- getPrivateMessages(conversationId: string, limit = PRIVATE_MESSAGE_QUERY_LIMIT): PrivateMessageView[] {
2004
+ getPrivateMessages(conversationId: string, limit = PRIVATE_MESSAGE_QUERY_LIMIT, offset = 0): {
2005
+ total: number;
2006
+ items: PrivateMessageView[];
2007
+ } {
2003
2008
  const normalizedConversationId = String(conversationId || "").trim();
2004
2009
  const resolvedLimit = Math.max(1, Math.min(PRIVATE_MESSAGE_QUERY_LIMIT, Number(limit) || PRIVATE_MESSAGE_QUERY_LIMIT));
2010
+ const resolvedOffset = Math.max(0, Number(offset) || 0);
2005
2011
  const receiptsByMessageId = new Map(
2006
2012
  this.privateMessageReceipts.map((receipt) => [receipt.message_id, receipt.status] as const)
2007
2013
  );
2008
- return this.privateMessages
2014
+ const filtered = this.privateMessages
2009
2015
  .filter((message) => {
2010
2016
  if (message.from_agent_id === message.to_agent_id) {
2011
2017
  return false;
@@ -2016,9 +2022,12 @@ export class LocalNodeService {
2016
2022
  }
2017
2023
  return !normalizedConversationId || message.conversation_id === normalizedConversationId;
2018
2024
  })
2019
- .sort((a, b) => a.created_at - b.created_at)
2020
- .slice(-resolvedLimit)
2021
- .map((message) => ({
2025
+ .sort((a, b) => a.created_at - b.created_at);
2026
+ const total = filtered.length;
2027
+ const paged = filtered.slice(Math.max(0, total - resolvedOffset - resolvedLimit), Math.max(0, total - resolvedOffset));
2028
+ return {
2029
+ total,
2030
+ items: paged.map((message) => ({
2022
2031
  message_id: message.message_id,
2023
2032
  conversation_id: message.conversation_id,
2024
2033
  from_agent_id: message.from_agent_id,
@@ -2026,8 +2035,12 @@ export class LocalNodeService {
2026
2035
  body: this.decryptPrivateMessageBody(message),
2027
2036
  created_at: message.created_at,
2028
2037
  is_self: message.from_agent_id === this.identity?.agent_id,
2029
- delivery_status: receiptsByMessageId.get(message.message_id) || "sent",
2030
- }));
2038
+ delivery_status:
2039
+ receiptsByMessageId.get(message.message_id) ||
2040
+ this.privateMessageDeliveryStatusCache.get(message.message_id) ||
2041
+ (message.from_agent_id === this.identity?.agent_id ? "fallback-sent" : "sent"),
2042
+ })),
2043
+ };
2031
2044
  }
2032
2045
 
2033
2046
  async sendPrivateMessage(input: {
@@ -2045,12 +2058,9 @@ export class LocalNodeService {
2045
2058
  return { sent: false, reason: "self_private_message_not_allowed" };
2046
2059
  }
2047
2060
  const toPeerId = this.privatePeerRoutes[toAgentId] || "";
2048
- if (!toAgentId || !toPeerId || !recipientKey || !body) {
2061
+ if (!toAgentId || !recipientKey || !body) {
2049
2062
  return { sent: false, reason: "invalid_private_message_input" };
2050
2063
  }
2051
- if (typeof this.network.sendDirect !== "function") {
2052
- return { sent: false, reason: "direct_delivery_not_supported" };
2053
- }
2054
2064
  const encrypted = encryptPrivatePayload({
2055
2065
  plaintext: body,
2056
2066
  recipient_public_key: recipientKey,
@@ -2067,11 +2077,26 @@ export class LocalNodeService {
2067
2077
  nonce: encrypted.nonce,
2068
2078
  created_at: Date.now(),
2069
2079
  });
2080
+ this.privateMessageBodyCache.set(message.message_id, body);
2070
2081
  this.ingestPrivateMessage(message);
2071
2082
  await this.persistPrivateMessages();
2072
- await this.network.sendDirect(toPeerId, PRIVATE_MESSAGE_TOPIC, message);
2073
- const view = this.getPrivateMessages(message.conversation_id).find((item) => item.message_id === message.message_id);
2074
- return { sent: true, reason: "sent", message: view };
2083
+ let reason = "fallback-sent";
2084
+ if (toPeerId && typeof this.network.sendDirect === "function") {
2085
+ try {
2086
+ await this.network.sendDirect(toPeerId, PRIVATE_MESSAGE_TOPIC, message);
2087
+ reason = "direct-sent";
2088
+ } catch {
2089
+ await this.publish(PRIVATE_MESSAGE_TOPIC, message);
2090
+ }
2091
+ } else {
2092
+ await this.publish(PRIVATE_MESSAGE_TOPIC, message);
2093
+ }
2094
+ this.privateMessageDeliveryStatusCache.set(message.message_id, reason as PrivateMessageView["delivery_status"]);
2095
+ const view = this.getPrivateMessages(message.conversation_id, PRIVATE_MESSAGE_QUERY_LIMIT, 0).items.find((item) => item.message_id === message.message_id);
2096
+ if (view) {
2097
+ view.delivery_status = reason as PrivateMessageView["delivery_status"];
2098
+ }
2099
+ return { sent: true, reason, message: view };
2075
2100
  }
2076
2101
 
2077
2102
  getOpenClawBridgeStatus(): OpenClawBridgeStatus {
@@ -3006,6 +3031,12 @@ export class LocalNodeService {
3006
3031
  this.network.subscribe(SOCIAL_MESSAGE_OBSERVATION_TOPIC, (data: SocialMessageObservationRecord, meta?: { peerId?: string }) => {
3007
3032
  this.onMessage(SOCIAL_MESSAGE_OBSERVATION_TOPIC, data, meta);
3008
3033
  });
3034
+ this.network.subscribe(PRIVATE_MESSAGE_TOPIC, (data: PrivateMessageRecord, meta?: { peerId?: string }) => {
3035
+ this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
3036
+ });
3037
+ this.network.subscribe(PRIVATE_MESSAGE_RECEIPT_TOPIC, (data: PrivateMessageReceiptRecord, meta?: { peerId?: string }) => {
3038
+ this.onDirectMessage(PRIVATE_MESSAGE_RECEIPT_TOPIC, data, meta);
3039
+ });
3009
3040
  if (typeof this.network.subscribeDirect === "function") {
3010
3041
  this.network.subscribeDirect(PRIVATE_MESSAGE_TOPIC, (data: PrivateMessageRecord, meta?: { peerId?: string }) => {
3011
3042
  this.onDirectMessage(PRIVATE_MESSAGE_TOPIC, data, meta);
@@ -3799,7 +3830,11 @@ export class LocalNodeService {
3799
3830
  created_at: Date.now(),
3800
3831
  });
3801
3832
  this.ingestPrivateMessageReceipt(receipt);
3802
- await this.network.sendDirect(replyPeerId, PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
3833
+ try {
3834
+ await this.network.sendDirect(replyPeerId, PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
3835
+ } catch {
3836
+ await this.publish(PRIVATE_MESSAGE_RECEIPT_TOPIC, receipt);
3837
+ }
3803
3838
  await this.persistPrivateMessageReceipts();
3804
3839
  }
3805
3840
 
@@ -3938,12 +3973,19 @@ export class LocalNodeService {
3938
3973
  }
3939
3974
  this.privateMessages = this.normalizePrivateMessages(this.privateMessages);
3940
3975
  const validIds = new Set(this.privateMessages.map((item) => item.message_id));
3941
- this.privateMessageBodyCache.delete(message.message_id);
3976
+ if (message.from_agent_id !== this.identity?.agent_id) {
3977
+ this.privateMessageBodyCache.delete(message.message_id);
3978
+ }
3942
3979
  for (const key of Array.from(this.privateMessageBodyCache.keys())) {
3943
3980
  if (!validIds.has(key)) {
3944
3981
  this.privateMessageBodyCache.delete(key);
3945
3982
  }
3946
3983
  }
3984
+ for (const key of Array.from(this.privateMessageDeliveryStatusCache.keys())) {
3985
+ if (!validIds.has(key)) {
3986
+ this.privateMessageDeliveryStatusCache.delete(key);
3987
+ }
3988
+ }
3947
3989
  }
3948
3990
 
3949
3991
  private ingestPrivateMessageReceipt(receipt: PrivateMessageReceiptRecord): void {
@@ -3954,6 +3996,7 @@ export class LocalNodeService {
3954
3996
  this.privateMessageReceipts.push(receipt);
3955
3997
  }
3956
3998
  this.privateMessageReceipts = this.normalizePrivateMessageReceipts(this.privateMessageReceipts);
3999
+ this.privateMessageDeliveryStatusCache.set(receipt.message_id, receipt.status);
3957
4000
  }
3958
4001
 
3959
4002
  private normalizeIncomingSocialMessage(value: unknown): SocialMessageRecord | null {
@@ -4272,7 +4315,7 @@ export async function main() {
4272
4315
  } catch {
4273
4316
  // best effort after response has been sent
4274
4317
  }
4275
- }, 150);
4318
+ }, 1200);
4276
4319
  })
4277
4320
  );
4278
4321
 
@@ -4414,8 +4457,9 @@ export async function main() {
4414
4457
 
4415
4458
  app.get("/api/messages", (req, res) => {
4416
4459
  const limit = Number(req.query.limit ?? 50);
4460
+ const offset = Number(req.query.offset ?? 0);
4417
4461
  const agentId = String(req.query.agent_id ?? "").trim();
4418
- sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null }));
4462
+ sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null, offset }));
4419
4463
  });
4420
4464
 
4421
4465
  app.get("/api/private/state", (_req, res) => {
@@ -4429,7 +4473,8 @@ export async function main() {
4429
4473
  app.get("/api/private/messages", (req, res) => {
4430
4474
  const conversationId = String(req.query.conversation_id ?? "").trim();
4431
4475
  const limit = Number(req.query.limit ?? PRIVATE_MESSAGE_QUERY_LIMIT);
4432
- sendOk(res, node.getPrivateMessages(conversationId, limit));
4476
+ const offset = Number(req.query.offset ?? 0);
4477
+ sendOk(res, node.getPrivateMessages(conversationId, limit, offset));
4433
4478
  });
4434
4479
 
4435
4480
  app.post(
@@ -4441,7 +4486,11 @@ export async function main() {
4441
4486
  body: String(req.body?.body || ""),
4442
4487
  });
4443
4488
  sendOk(res, result, {
4444
- message: result.sent ? "Private message sent" : `Private message skipped: ${result.reason}`,
4489
+ message: result.sent
4490
+ ? (result.reason === "direct-sent"
4491
+ ? "Private message sent directly"
4492
+ : "Private message sent via encrypted fallback")
4493
+ : `Private message skipped: ${result.reason}`,
4445
4494
  });
4446
4495
  })
4447
4496
  );
@@ -4460,8 +4509,9 @@ export async function main() {
4460
4509
 
4461
4510
  app.get("/api/openclaw/bridge/messages", (req, res) => {
4462
4511
  const limit = Number(req.query.limit ?? 50);
4512
+ const offset = Number(req.query.offset ?? 0);
4463
4513
  const agentId = String(req.query.agent_id ?? "").trim();
4464
- sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null }));
4514
+ sendOk(res, node.getSocialMessages(limit, { agent_id: agentId || null, offset }));
4465
4515
  });
4466
4516
 
4467
4517
  app.post(
@@ -4611,10 +4661,17 @@ export async function main() {
4611
4661
  let html = readFileSync(staticIndexFile, "utf8");
4612
4662
  html = html.replace("</body>", `${renderBootstrapScript(payload)}\n</body>`);
4613
4663
  res.setHeader("Content-Type", "text/html; charset=utf-8");
4664
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
4614
4665
  res.send(html);
4615
4666
  });
4616
4667
 
4617
- app.use(express.static(staticDir));
4668
+ app.use(express.static(staticDir, {
4669
+ etag: false,
4670
+ lastModified: false,
4671
+ setHeaders: (res) => {
4672
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
4673
+ },
4674
+ }));
4618
4675
 
4619
4676
  app.use((error: unknown, _req: Request, res: Response, _next: NextFunction) => {
4620
4677
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -1 +1 @@
1
- 2026.3.20-beta.12
1
+ 2026.3.20-beta.14
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silicaclaw-broadcast",
3
- "version": "2026.3.20-beta.12",
3
+ "version": "2026.3.20-beta.14",
4
4
  "display_name": "SilicaClaw Broadcast",
5
5
  "description": "Official OpenClaw skill for a bounded local SilicaClaw broadcast workflow: read public broadcasts, publish public broadcasts, and optionally forward owner-relevant summaries through OpenClaw's native channel.",
6
6
  "entrypoints": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silicaclaw/cli",
3
- "version": "2026.3.20-12",
3
+ "version": "2026.3.20-14",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"