libp2p-mesh 2026.6.13 → 2026.6.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/README.md CHANGED
@@ -225,11 +225,34 @@ If `inboundTargets` is an empty array, inbound delivery is disabled. If `inbound
225
225
  | `relayList` | `string[]` | `[]` | Multiaddrs of relays to reserve a slot on |
226
226
  | `discoverRelays` | `number` | `0` | Auto-discover this many relays via content routing |
227
227
  | `announceAddrs` | `string[]` | `[]` | Extra multiaddrs to announce on top of auto-detected ones |
228
+ | `announceLogDetail` | `"off" \| "summary" \| "payload"` | `"summary"` | Controls instance announce logging. `summary` logs peer, instance, address count, and attribute count; `payload` also logs the full announce JSON; `off` disables only the new announce summary/payload logs and keeps legacy/basic info logs. |
228
229
  | `inboundChannel` | `string` | `undefined` | OpenClaw channel used to display inbound P2P user messages, for example `"feishu"` |
229
230
  | `inboundTarget` | `string` | `undefined` | OpenClaw channel target for inbound P2P messages, for example `user:ou_xxx` or `chat:oc_xxx` |
230
231
  | `inboundTargets` | `array` | `undefined` | Optional list of receiver-owned channel targets for inbound P2P user messages. When present, it overrides `inboundChannel`/`inboundTarget`; an empty array disables inbound delivery. |
231
232
  | `deliveryAckTimeoutMs` | `number` | `15000` | Timeout for waiting on remote channel delivery ACKs |
232
233
 
234
+ ### Announce Startup and Logging
235
+
236
+ During gateway startup, `libp2p-mesh` registers the instance router handlers and direct/broadcast inbound handlers before starting the mesh node. This makes early `instance-announce` messages observable as soon as peers connect, instead of waiting until after mesh startup has already completed.
237
+
238
+ Instance announce logs are controlled by `plugins.entries["libp2p-mesh"].config.announceLogDetail`:
239
+
240
+ - `summary` is the default. It logs send/receive direction, peer ID, instance ID, multiaddr count, and public attribute count. It does not print the full announce JSON.
241
+ - `off` disables the new announce summary and payload logs. It still keeps legacy/basic info logs such as sent announce lines and instance mapping updates, along with warnings and errors.
242
+ - `payload` logs the same summary plus the full announce JSON at debug level.
243
+
244
+ Use the debug command to inspect or change this value:
245
+
246
+ ```bash
247
+ openclaw libp2p-mesh debug
248
+ ```
249
+
250
+ Full payload logging is intended for short-lived troubleshooting only. Announce payloads can include `userPublicAttributes`, peer multiaddrs, the instance pubkey, and instance identity fields. After changing the setting, restart the gateway for the new logging level to take effect:
251
+
252
+ ```bash
253
+ openclaw gateway restart
254
+ ```
255
+
233
256
  ## NAT Traversal
234
257
 
235
258
  When both peers have a routable address (same LAN, public IPs, or working port-forwarding) no extra setup is needed. The defaults above kick in automatically:
@@ -616,6 +639,20 @@ Peer connection and disconnection are logged at `info` level:
616
639
 
617
640
  If these lines are missing, confirm the gateway is running with normal info logs enabled and that both instances are on the same mDNS, bootstrap, or relay network.
618
641
 
642
+ ### Instance announce routes are missing between two machines
643
+
644
+ If peers connect but sending by OpenClaw instance ID fails or `instance-peer.json` is not updated, first confirm both gateways were restarted after the latest config change. On startup, the gateway now attaches the instance router plus inbound message handlers before starting the mesh, so early announces should be handled once the peer connection appears.
645
+
646
+ For a short debug session on both computers:
647
+
648
+ 1. Run `openclaw libp2p-mesh debug`.
649
+ 2. Set `announceLogDetail` to `payload` and confirm the privacy warning.
650
+ 3. Restart both gateways with `openclaw gateway restart`.
651
+ 4. Watch for summary lines and debug lines containing full announce payload JSON.
652
+ 5. Return to `summary` or `off` with `openclaw libp2p-mesh debug`, then restart again.
653
+
654
+ Full payload logs may expose `userPublicAttributes`, multiaddrs, pubkey, and instance identity, so avoid sharing these logs outside the debugging context.
655
+
619
656
  ## Architecture
620
657
 
621
658
  ```
@@ -0,0 +1,21 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
+ import type { OpenClawPluginCliContext } from "openclaw/plugin-sdk/plugin-runtime";
3
+ import type { SetupPrompter } from "./setup-wizard.js";
4
+ import type { AnnounceLogDetail } from "./types.js";
5
+ type CliRootCommand = {
6
+ command(name: string): {
7
+ description(text: string): {
8
+ action(handler: () => Promise<void>): void;
9
+ };
10
+ };
11
+ };
12
+ export type DebugConfigWriter = {
13
+ saveAnnounceLogDetail(detail: AnnounceLogDetail): Promise<void>;
14
+ };
15
+ export type DebugCliDeps = {
16
+ createPrompter?: (ctx: OpenClawPluginCliContext) => SetupPrompter;
17
+ createWriter?: (api: OpenClawPluginApi) => DebugConfigWriter;
18
+ };
19
+ export declare function registerLibp2pMeshDebugCli(api: OpenClawPluginApi, deps?: DebugCliDeps): void;
20
+ export declare function registerLibp2pMeshDebugCommand(root: CliRootCommand, api: OpenClawPluginApi, ctx: OpenClawPluginCliContext, deps?: DebugCliDeps): void;
21
+ export {};
@@ -0,0 +1,54 @@
1
+ import { createReadlinePrompter, LIBP2P_MESH_CLI_REGISTRATION, } from "./setup-cli.js";
2
+ import { applyAnnounceLogDetail, getAnnounceLogDetail, } from "./setup-config.js";
3
+ import { runDebugWizard } from "./debug-wizard.js";
4
+ const DEBUG_CLI_AFTER_WRITE = {
5
+ mode: "none",
6
+ reason: "libp2p-mesh debug config updated; restart manually to apply gateway changes.",
7
+ };
8
+ export function registerLibp2pMeshDebugCli(api, deps = {}) {
9
+ api.registerCli((ctx) => {
10
+ const root = ctx.program
11
+ .command("libp2p-mesh")
12
+ .description("Configure libp2p-mesh plugin.");
13
+ registerLibp2pMeshDebugCommand(root, api, ctx, deps);
14
+ }, LIBP2P_MESH_CLI_REGISTRATION);
15
+ }
16
+ export function registerLibp2pMeshDebugCommand(root, api, ctx, deps = {}) {
17
+ root
18
+ .command("debug")
19
+ .description("Manage libp2p-mesh debug logging config.")
20
+ .action(async () => {
21
+ const prompter = (deps.createPrompter?.(ctx) ?? createReadlinePrompter());
22
+ const writer = deps.createWriter?.(api) ?? createOpenClawDebugConfigWriter(api);
23
+ try {
24
+ const result = await runDebugWizard({
25
+ prompter,
26
+ current: getAnnounceLogDetail(ctx.config),
27
+ writer,
28
+ });
29
+ prompter.print(result.message);
30
+ }
31
+ finally {
32
+ prompter.close?.();
33
+ }
34
+ });
35
+ }
36
+ function createOpenClawDebugConfigWriter(api) {
37
+ return {
38
+ async saveAnnounceLogDetail(detail) {
39
+ await api.runtime.config.mutateConfigFile({
40
+ afterWrite: DEBUG_CLI_AFTER_WRITE,
41
+ mutate(draft) {
42
+ const nextConfig = applyAnnounceLogDetail(draft, detail);
43
+ replaceConfig(draft, nextConfig);
44
+ },
45
+ });
46
+ },
47
+ };
48
+ }
49
+ function replaceConfig(draft, nextConfig) {
50
+ for (const key of Object.keys(draft)) {
51
+ delete draft[key];
52
+ }
53
+ Object.assign(draft, structuredClone(nextConfig));
54
+ }
@@ -0,0 +1,19 @@
1
+ import { type SetupPrompter } from "./setup-wizard.js";
2
+ import type { AnnounceLogDetail } from "./types.js";
3
+ export type DebugPromptChoice = AnnounceLogDetail | "cancel";
4
+ export type RunDebugWizardOptions = {
5
+ prompter: SetupPrompter;
6
+ current: AnnounceLogDetail;
7
+ writer: {
8
+ saveAnnounceLogDetail(detail: AnnounceLogDetail): Promise<void>;
9
+ };
10
+ };
11
+ export type DebugWizardResult = {
12
+ status: "saved";
13
+ announceLogDetail: AnnounceLogDetail;
14
+ message: string;
15
+ } | {
16
+ status: "cancelled";
17
+ message: string;
18
+ };
19
+ export declare function runDebugWizard(options: RunDebugWizardOptions): Promise<DebugWizardResult>;
@@ -0,0 +1,41 @@
1
+ import { SetupCancelledError } from "./setup-wizard.js";
2
+ const CANCELLED_MESSAGE = "Debug configuration cancelled. No changes were written.";
3
+ const SAVED_MESSAGE = "Debug config updated.\n\nRestart the gateway to apply changes:\nopenclaw gateway restart";
4
+ export async function runDebugWizard(options) {
5
+ try {
6
+ options.prompter.print(`Current announceLogDetail: ${options.current}`);
7
+ const selected = await options.prompter.select("Set announceLogDetail:", [
8
+ { label: "summary: log peer, instance, address and attribute counts", value: "summary" },
9
+ { label: "off: disable announce summary/payload logs", value: "off" },
10
+ { label: "payload: log full announce JSON", value: "payload" },
11
+ { label: "Cancel", value: "cancel" },
12
+ ]);
13
+ if (selected === "cancel") {
14
+ return cancelledResult();
15
+ }
16
+ if (selected === "payload") {
17
+ const confirmed = await options.prompter.confirm("Full announce payload logs may include userPublicAttributes, multiaddrs, pubkey and instance identity. Enable payload logs?", false);
18
+ if (!confirmed) {
19
+ return cancelledResult();
20
+ }
21
+ }
22
+ await options.writer.saveAnnounceLogDetail(selected);
23
+ return {
24
+ status: "saved",
25
+ announceLogDetail: selected,
26
+ message: SAVED_MESSAGE,
27
+ };
28
+ }
29
+ catch (error) {
30
+ if (error instanceof SetupCancelledError) {
31
+ return cancelledResult();
32
+ }
33
+ throw error;
34
+ }
35
+ }
36
+ function cancelledResult() {
37
+ return {
38
+ status: "cancelled",
39
+ message: CANCELLED_MESSAGE,
40
+ };
41
+ }
@@ -14,6 +14,17 @@ function isNonEmptyString(value) {
14
14
  function summarizeError(error) {
15
15
  return error instanceof Error ? error.message : String(error);
16
16
  }
17
+ function effectiveAnnounceLogDetail(value) {
18
+ if (value === "off" || value === "payload") {
19
+ return value;
20
+ }
21
+ return "summary";
22
+ }
23
+ function countAnnounceAttributes(payload) {
24
+ return Array.isArray(payload.userPublicAttributes)
25
+ ? payload.userPublicAttributes.length
26
+ : 0;
27
+ }
17
28
  function describeAttributeMatch(match) {
18
29
  if (match.kind === "tag") {
19
30
  return `tag ${match.value}`;
@@ -81,6 +92,7 @@ export function createInstanceRouter(options) {
81
92
  const { mesh, store, delivery } = options;
82
93
  const config = options.config ?? {};
83
94
  const logger = options.logger;
95
+ const announceLogDetail = effectiveAnnounceLogDetail(config.announceLogDetail);
84
96
  const ackTimeoutMs = config.deliveryAckTimeoutMs ?? 15000;
85
97
  const announcedPeers = new Set();
86
98
  const pendingAcks = new Map();
@@ -133,7 +145,29 @@ export function createInstanceRouter(options) {
133
145
  payload: JSON.stringify(payload),
134
146
  });
135
147
  announcedPeers.add(peerId);
136
- logger?.info?.(`[libp2p-mesh] Sent instance announce to ${peerId} (${payload.instanceId})`);
148
+ logAnnounce("Sent", peerId, payload);
149
+ }
150
+ function announceSummary(direction, peerId, payload, changed) {
151
+ const changedDetail = changed === undefined ? "" : ` changed=${changed}`;
152
+ return `[libp2p-mesh] ${direction} instance announce peer=${peerId} instance=${payload.instanceId} addrs=${payload.multiaddrs.length} attrs=${countAnnounceAttributes(payload)}${changedDetail}`;
153
+ }
154
+ function logAnnounce(direction, peerId, payload, changed) {
155
+ if (announceLogDetail === "off") {
156
+ if (direction === "Sent") {
157
+ logger?.info?.(`[libp2p-mesh] Sent instance announce to ${peerId} (${payload.instanceId})`);
158
+ }
159
+ return;
160
+ }
161
+ logger?.info?.(announceSummary(direction, peerId, payload, changed));
162
+ if (announceLogDetail !== "payload") {
163
+ return;
164
+ }
165
+ try {
166
+ logger?.debug?.(`[libp2p-mesh] ${direction} instance announce payload=${JSON.stringify(payload)}`);
167
+ }
168
+ catch {
169
+ return;
170
+ }
137
171
  }
138
172
  async function announceToConnectedPeers() {
139
173
  for (const peerId of mesh.getConnectedPeers()) {
@@ -163,9 +197,7 @@ export function createInstanceRouter(options) {
163
197
  if (result.changed) {
164
198
  logger?.info?.(`[libp2p-mesh] Instance mapping updated: ${payload.instanceId} -> ${payload.peerId}`);
165
199
  }
166
- else {
167
- logger?.debug?.(`[libp2p-mesh] Instance mapping unchanged: ${payload.instanceId}`);
168
- }
200
+ logAnnounce("Received", msg.from, payload, result.changed);
169
201
  if (!announcedPeers.has(msg.from)) {
170
202
  await announceToPeer(msg.from).catch((error) => {
171
203
  logger?.warn?.(`[libp2p-mesh] Failed to respond to announce from ${msg.from}: ${summarizeError(error)}`);
@@ -328,7 +360,10 @@ export function createInstanceRouter(options) {
328
360
  handleAck(msg);
329
361
  }
330
362
  }
331
- async function start() {
363
+ function attachHandlers() {
364
+ if (unsubs.length > 0) {
365
+ return;
366
+ }
332
367
  unsubs.push(mesh.onMessage((msg) => {
333
368
  handleMessage(msg).catch((error) => {
334
369
  logger?.error?.(`[libp2p-mesh] Instance router message error: ${summarizeError(error)}`);
@@ -339,6 +374,9 @@ export function createInstanceRouter(options) {
339
374
  logger?.warn?.(`[libp2p-mesh] Failed to announce to connected peer ${peerId}: ${summarizeError(error)}`);
340
375
  });
341
376
  }));
377
+ }
378
+ async function start() {
379
+ attachHandlers();
342
380
  await announceToConnectedPeers();
343
381
  }
344
382
  async function stop() {
@@ -505,6 +543,8 @@ export function createInstanceRouter(options) {
505
543
  };
506
544
  }
507
545
  return {
546
+ attachHandlers,
547
+ announceToConnectedPeers,
508
548
  start,
509
549
  stop,
510
550
  handleMessage,
@@ -1,2 +1,9 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
+ import { createInstanceRouter } from "./instance-router.js";
3
+ import { createMeshNetwork } from "./mesh.js";
4
+ export type Libp2pMeshPluginDeps = {
5
+ createMeshNetwork?: typeof createMeshNetwork;
6
+ createInstanceRouter?: typeof createInstanceRouter;
7
+ };
2
8
  export declare function registerLibp2pMesh(api: OpenClawPluginApi): void;
9
+ export declare function registerLibp2pMeshWithDeps(api: OpenClawPluginApi, deps?: Libp2pMeshPluginDeps): void;
@@ -9,11 +9,15 @@ import { createUserProfileStore } from "./user-profile-store.js";
9
9
  import { buildP2PTools } from "./agent-tools.js";
10
10
  import { registerLibp2pMeshCli } from "./profile-cli.js";
11
11
  export function registerLibp2pMesh(api) {
12
+ registerLibp2pMeshWithDeps(api);
13
+ }
14
+ export function registerLibp2pMeshWithDeps(api, deps = {}) {
12
15
  registerLibp2pMeshCli(api);
13
16
  const config = api.pluginConfig;
14
17
  let unsubscribeInbound;
15
18
  let serviceStarted = false;
16
- const mesh = createMeshNetwork({
19
+ let startPromise;
20
+ const mesh = (deps.createMeshNetwork ?? createMeshNetwork)({
17
21
  config,
18
22
  logger: api.logger,
19
23
  });
@@ -32,7 +36,7 @@ export function registerLibp2pMesh(api) {
32
36
  },
33
37
  logger: api.logger,
34
38
  });
35
- const router = createInstanceRouter({
39
+ const router = (deps.createInstanceRouter ?? createInstanceRouter)({
36
40
  mesh,
37
41
  store,
38
42
  delivery,
@@ -42,6 +46,62 @@ export function registerLibp2pMesh(api) {
42
46
  userProfileStore,
43
47
  });
44
48
  const channel = createLibp2pMeshChannel(mesh);
49
+ function attachInboundHandlers() {
50
+ return mesh.onMessage((msg) => {
51
+ if (msg.type === "direct" || msg.type === "broadcast") {
52
+ const sendToChannel = async (_channelId, _target, text) => {
53
+ if (!config?.inboundChannel || !config?.inboundTarget) {
54
+ api.logger.warn?.("[libp2p-mesh] inboundChannel/inboundTarget not configured; direct message logged only.");
55
+ return;
56
+ }
57
+ const result = await delivery.deliver({
58
+ channel: config.inboundChannel,
59
+ target: config.inboundTarget,
60
+ text,
61
+ metadata: {
62
+ fromInstanceId: msg.instanceId ?? msg.from,
63
+ fromPeerId: msg.from,
64
+ p2pMessageId: msg.id,
65
+ allowAgentAutoReply: false,
66
+ replyToInstanceId: msg.instanceId ?? msg.from,
67
+ replyTool: "p2p_send_instance_message",
68
+ },
69
+ });
70
+ if (!result.ok) {
71
+ api.logger.error?.(`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${result.error}`);
72
+ }
73
+ };
74
+ handleP2PInbound(msg, { logger: api.logger, sendToChannel });
75
+ }
76
+ else if (msg.type === "agent-sync") {
77
+ handleP2PInbound(msg, { logger: api.logger });
78
+ }
79
+ });
80
+ }
81
+ async function cleanupStartupFailure() {
82
+ try {
83
+ unsubscribeInbound?.();
84
+ }
85
+ catch (error) {
86
+ api.logger.warn?.(`[libp2p-mesh] Failed to clean inbound handler after startup failure: ${String(error)}`);
87
+ }
88
+ finally {
89
+ unsubscribeInbound = undefined;
90
+ }
91
+ try {
92
+ await router.stop();
93
+ }
94
+ catch (error) {
95
+ api.logger.warn?.(`[libp2p-mesh] Failed to stop router after startup failure: ${String(error)}`);
96
+ }
97
+ try {
98
+ await mesh.stop();
99
+ }
100
+ catch (error) {
101
+ api.logger.warn?.(`[libp2p-mesh] Failed to stop mesh after startup failure: ${String(error)}`);
102
+ }
103
+ serviceStarted = false;
104
+ }
45
105
  // 1. Register Service (manages libp2p node lifecycle)
46
106
  api.registerService({
47
107
  id: "libp2p-mesh",
@@ -50,56 +110,45 @@ export function registerLibp2pMesh(api) {
50
110
  api.logger.debug?.("[libp2p-mesh] Service already started; ignoring duplicate start.");
51
111
  return;
52
112
  }
53
- await mesh.start();
54
- await router.start();
55
- unsubscribeInbound = mesh.onMessage((msg) => {
56
- if (msg.type === "direct" || msg.type === "broadcast") {
57
- const sendToChannel = async (_channelId, _target, text) => {
58
- if (!config?.inboundChannel || !config?.inboundTarget) {
59
- api.logger.warn?.("[libp2p-mesh] inboundChannel/inboundTarget not configured; direct message logged only.");
60
- return;
61
- }
62
- const result = await delivery.deliver({
63
- channel: config.inboundChannel,
64
- target: config.inboundTarget,
65
- text,
66
- metadata: {
67
- fromInstanceId: msg.instanceId ?? msg.from,
68
- fromPeerId: msg.from,
69
- p2pMessageId: msg.id,
70
- allowAgentAutoReply: false,
71
- replyToInstanceId: msg.instanceId ?? msg.from,
72
- replyTool: "p2p_send_instance_message",
73
- },
74
- });
75
- if (!result.ok) {
76
- api.logger.error?.(`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${result.error}`);
77
- }
78
- };
79
- handleP2PInbound(msg, { logger: api.logger, sendToChannel });
113
+ if (startPromise) {
114
+ api.logger.debug?.("[libp2p-mesh] Service start already in progress; waiting for it.");
115
+ return startPromise;
116
+ }
117
+ startPromise = (async () => {
118
+ try {
119
+ router.attachHandlers();
120
+ unsubscribeInbound = attachInboundHandlers();
121
+ await mesh.start();
122
+ await router.announceToConnectedPeers();
123
+ const identity = mesh.getInstanceIdentity();
124
+ api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
125
+ if (identity) {
126
+ api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
127
+ }
128
+ const nat = mesh.getNATStatus();
129
+ const enabledNames = Object.entries(nat.enabled)
130
+ .filter(([, on]) => on)
131
+ .map(([k]) => k);
132
+ if (enabledNames.length > 0) {
133
+ api.logger.info?.(`[libp2p-mesh] NAT traversal services: ${enabledNames.join(", ")}`);
134
+ }
135
+ if (nat.reservedRelays.length > 0) {
136
+ api.logger.info?.(`[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`);
137
+ }
138
+ serviceStarted = true;
80
139
  }
81
- else if (msg.type === "agent-sync") {
82
- handleP2PInbound(msg, { logger: api.logger });
140
+ catch (error) {
141
+ await cleanupStartupFailure();
142
+ throw error;
83
143
  }
84
- });
85
- const identity = mesh.getInstanceIdentity();
86
- api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
87
- if (identity) {
88
- api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
89
- }
90
- const nat = mesh.getNATStatus();
91
- const enabledNames = Object.entries(nat.enabled)
92
- .filter(([, on]) => on)
93
- .map(([k]) => k);
94
- if (enabledNames.length > 0) {
95
- api.logger.info?.(`[libp2p-mesh] NAT traversal services: ${enabledNames.join(", ")}`);
96
- }
97
- if (nat.reservedRelays.length > 0) {
98
- api.logger.info?.(`[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`);
99
- }
100
- serviceStarted = true;
144
+ finally {
145
+ startPromise = undefined;
146
+ }
147
+ })();
148
+ return startPromise;
101
149
  },
102
150
  stop: async () => {
151
+ await startPromise?.catch(() => undefined);
103
152
  unsubscribeInbound?.();
104
153
  unsubscribeInbound = undefined;
105
154
  await router.stop();
@@ -1,6 +1,7 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
2
  import type { OpenClawPluginCliContext } from "openclaw/plugin-sdk/plugin-runtime";
3
3
  import { type SetupCliDeps } from "./setup-cli.js";
4
+ import { type DebugCliDeps } from "./debug-cli.js";
4
5
  import { type UserProfileStore } from "./user-profile-store.js";
5
6
  import type { SetupPrompter } from "./setup-wizard.js";
6
7
  type CliRootCommand = {
@@ -20,6 +21,7 @@ export type ProfileCliDeps = {
20
21
  export type Libp2pMeshCliDeps = {
21
22
  setup?: SetupCliDeps;
22
23
  profile?: ProfileCliDeps;
24
+ debug?: DebugCliDeps;
23
25
  };
24
26
  export declare function registerLibp2pMeshCli(api: OpenClawPluginApi, deps?: Libp2pMeshCliDeps): void;
25
27
  export declare function registerLibp2pMeshProfileCli(api: OpenClawPluginApi, deps?: ProfileCliDeps): void;
@@ -1,4 +1,5 @@
1
1
  import { createReadlinePrompter, LIBP2P_MESH_CLI_REGISTRATION, registerLibp2pMeshSetupCommand, } from "./setup-cli.js";
2
+ import { registerLibp2pMeshDebugCommand } from "./debug-cli.js";
2
3
  import { runProfileWizard } from "./profile-wizard.js";
3
4
  import { createUserMdAttributeSource } from "./user-md-attributes.js";
4
5
  import { createUserProfileStore } from "./user-profile-store.js";
@@ -9,6 +10,7 @@ export function registerLibp2pMeshCli(api, deps = {}) {
9
10
  .description("Configure libp2p-mesh plugin.");
10
11
  registerLibp2pMeshSetupCommand(root, api, ctx, deps.setup);
11
12
  registerLibp2pMeshProfileCommand(root, api, ctx, deps.profile);
13
+ registerLibp2pMeshDebugCommand(root, api, ctx, deps.debug);
12
14
  }, LIBP2P_MESH_CLI_REGISTRATION);
13
15
  }
14
16
  export function registerLibp2pMeshProfileCli(api, deps = {}) {
@@ -1,4 +1,4 @@
1
- import type { InboundTargetConfig, MeshConfig } from "./types.js";
1
+ import type { AnnounceLogDetail, InboundTargetConfig, MeshConfig } from "./types.js";
2
2
  export declare const LIBP2P_MESH_PLUGIN_ID = "libp2p-mesh";
3
3
  export declare const DEFAULT_DELIVERY_ACK_TIMEOUT_MS = 15000;
4
4
  export type SetupMode = "lan" | "cross-network" | "relay-node" | "tools-only";
@@ -32,11 +32,14 @@ export type AddInboundTargetResult = {
32
32
  };
33
33
  export type LegacyInboundMigrationMode = "convert" | "keep" | "replace";
34
34
  export declare function getLibp2pMeshConfig(config: OpenClawConfigLike): MeshConfig | undefined;
35
+ export declare function getAnnounceLogDetail(config: OpenClawConfigLike): AnnounceLogDetail;
36
+ export declare function normalizeAnnounceLogDetail(value: unknown): AnnounceLogDetail;
35
37
  export declare function buildNetworkConfig(mode: SetupMode, options?: {
36
38
  crossNetwork?: CrossNetworkOptions;
37
39
  relayNode?: RelayNodeOptions;
38
40
  }): MeshConfig;
39
41
  export declare function applyPluginConfig(config: OpenClawConfigLike, pluginConfig: MeshConfig): OpenClawConfigLike;
42
+ export declare function applyAnnounceLogDetail(config: OpenClawConfigLike, announceLogDetail: AnnounceLogDetail): OpenClawConfigLike;
40
43
  export declare function mergeNetworkConfig(existing: MeshConfig | undefined, networkConfig: MeshConfig): MeshConfig;
41
44
  export declare function listConfiguredChannels(config: OpenClawConfigLike): string[];
42
45
  export declare function generateInboundTargetId(channel: string, existingTargets: InboundTargetConfig[]): string;
@@ -3,6 +3,12 @@ export const DEFAULT_DELIVERY_ACK_TIMEOUT_MS = 15000;
3
3
  export function getLibp2pMeshConfig(config) {
4
4
  return config.plugins?.entries?.[LIBP2P_MESH_PLUGIN_ID]?.config;
5
5
  }
6
+ export function getAnnounceLogDetail(config) {
7
+ return normalizeAnnounceLogDetail(getLibp2pMeshConfig(config)?.announceLogDetail);
8
+ }
9
+ export function normalizeAnnounceLogDetail(value) {
10
+ return value === "off" || value === "payload" || value === "summary" ? value : "summary";
11
+ }
6
12
  export function buildNetworkConfig(mode, options) {
7
13
  switch (mode) {
8
14
  case "lan":
@@ -53,6 +59,27 @@ export function applyPluginConfig(config, pluginConfig) {
53
59
  },
54
60
  };
55
61
  }
62
+ export function applyAnnounceLogDetail(config, announceLogDetail) {
63
+ const existingEntry = config.plugins?.entries?.[LIBP2P_MESH_PLUGIN_ID];
64
+ const existingPluginConfig = existingEntry?.config ?? {};
65
+ return {
66
+ ...config,
67
+ plugins: {
68
+ ...config.plugins,
69
+ entries: {
70
+ ...config.plugins?.entries,
71
+ [LIBP2P_MESH_PLUGIN_ID]: {
72
+ ...existingEntry,
73
+ enabled: existingEntry?.enabled ?? true,
74
+ config: {
75
+ ...existingPluginConfig,
76
+ announceLogDetail,
77
+ },
78
+ },
79
+ },
80
+ },
81
+ };
82
+ }
56
83
  export function mergeNetworkConfig(existing, networkConfig) {
57
84
  if (!existing) {
58
85
  return { ...networkConfig };
@@ -180,6 +180,8 @@ export type InstanceRouterOptions = {
180
180
  };
181
181
  };
182
182
  export interface InstanceRouter {
183
+ attachHandlers(): void;
184
+ announceToConnectedPeers(): Promise<void>;
183
185
  start(): Promise<void>;
184
186
  stop(): Promise<void>;
185
187
  handleMessage(msg: P2PMessage): Promise<void>;
@@ -206,6 +208,7 @@ export interface MeshConfig {
206
208
  discovery?: "mdns" | "bootstrap" | "dht";
207
209
  bootstrapList?: string[];
208
210
  meshTopic?: string;
211
+ announceLogDetail?: AnnounceLogDetail;
209
212
  enableAgentSync?: boolean;
210
213
  enableWebSocket?: boolean;
211
214
  peerIdPath?: string;
@@ -299,6 +302,7 @@ export interface MeshNetwork {
299
302
  /** Inspect which NAT-traversal services are running and whether any relay reservations are active */
300
303
  getNATStatus(): NATTraversalStatus;
301
304
  }
305
+ export type AnnounceLogDetail = "off" | "summary" | "payload";
302
306
  export type MeshAccount = {
303
307
  accountId: string;
304
308
  configured: boolean;
@@ -35,6 +35,12 @@
35
35
  "type": "string",
36
36
  "default": "openclaw-mesh"
37
37
  },
38
+ "announceLogDetail": {
39
+ "type": "string",
40
+ "enum": ["off", "summary", "payload"],
41
+ "default": "summary",
42
+ "description": "Controls instance announce logging detail."
43
+ },
38
44
  "inboundTargets": {
39
45
  "type": "array",
40
46
  "items": {
@@ -82,6 +88,12 @@
82
88
  "type": "string",
83
89
  "default": "openclaw-mesh"
84
90
  },
91
+ "announceLogDetail": {
92
+ "type": "string",
93
+ "enum": ["off", "summary", "payload"],
94
+ "default": "summary",
95
+ "description": "Controls instance announce logging detail."
96
+ },
85
97
  "enablePubsub": {
86
98
  "type": "boolean",
87
99
  "default": true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2p-mesh",
3
- "version": "2026.6.13",
3
+ "version": "2026.6.14",
4
4
  "description": "OpenClaw libp2p P2P mesh network plugin for cross-instance agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,93 @@
1
+ import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
+ import type { OpenClawPluginCliContext } from "openclaw/plugin-sdk/plugin-runtime";
3
+ import {
4
+ createReadlinePrompter,
5
+ LIBP2P_MESH_CLI_REGISTRATION,
6
+ type ClosableSetupPrompter,
7
+ } from "./setup-cli.js";
8
+ import {
9
+ applyAnnounceLogDetail,
10
+ getAnnounceLogDetail,
11
+ type OpenClawConfigLike,
12
+ } from "./setup-config.js";
13
+ import type { SetupPrompter } from "./setup-wizard.js";
14
+ import { runDebugWizard } from "./debug-wizard.js";
15
+ import type { AnnounceLogDetail } from "./types.js";
16
+
17
+ const DEBUG_CLI_AFTER_WRITE = {
18
+ mode: "none",
19
+ reason: "libp2p-mesh debug config updated; restart manually to apply gateway changes.",
20
+ } as const;
21
+
22
+ type CliRootCommand = {
23
+ command(name: string): {
24
+ description(text: string): {
25
+ action(handler: () => Promise<void>): void;
26
+ };
27
+ };
28
+ };
29
+
30
+ export type DebugConfigWriter = {
31
+ saveAnnounceLogDetail(detail: AnnounceLogDetail): Promise<void>;
32
+ };
33
+
34
+ export type DebugCliDeps = {
35
+ createPrompter?: (ctx: OpenClawPluginCliContext) => SetupPrompter;
36
+ createWriter?: (api: OpenClawPluginApi) => DebugConfigWriter;
37
+ };
38
+
39
+ export function registerLibp2pMeshDebugCli(api: OpenClawPluginApi, deps: DebugCliDeps = {}): void {
40
+ api.registerCli((ctx) => {
41
+ const root = ctx.program
42
+ .command("libp2p-mesh")
43
+ .description("Configure libp2p-mesh plugin.");
44
+
45
+ registerLibp2pMeshDebugCommand(root, api, ctx, deps);
46
+ }, LIBP2P_MESH_CLI_REGISTRATION);
47
+ }
48
+
49
+ export function registerLibp2pMeshDebugCommand(
50
+ root: CliRootCommand,
51
+ api: OpenClawPluginApi,
52
+ ctx: OpenClawPluginCliContext,
53
+ deps: DebugCliDeps = {},
54
+ ): void {
55
+ root
56
+ .command("debug")
57
+ .description("Manage libp2p-mesh debug logging config.")
58
+ .action(async () => {
59
+ const prompter = (deps.createPrompter?.(ctx) ?? createReadlinePrompter()) as ClosableSetupPrompter;
60
+ const writer = deps.createWriter?.(api) ?? createOpenClawDebugConfigWriter(api);
61
+ try {
62
+ const result = await runDebugWizard({
63
+ prompter,
64
+ current: getAnnounceLogDetail(ctx.config as OpenClawConfigLike),
65
+ writer,
66
+ });
67
+ prompter.print(result.message);
68
+ } finally {
69
+ prompter.close?.();
70
+ }
71
+ });
72
+ }
73
+
74
+ function createOpenClawDebugConfigWriter(api: OpenClawPluginApi): DebugConfigWriter {
75
+ return {
76
+ async saveAnnounceLogDetail(detail) {
77
+ await api.runtime.config.mutateConfigFile({
78
+ afterWrite: DEBUG_CLI_AFTER_WRITE,
79
+ mutate(draft) {
80
+ const nextConfig = applyAnnounceLogDetail(draft as OpenClawConfigLike, detail);
81
+ replaceConfig(draft as OpenClawConfig, nextConfig as OpenClawConfig);
82
+ },
83
+ });
84
+ },
85
+ };
86
+ }
87
+
88
+ function replaceConfig(draft: OpenClawConfig, nextConfig: OpenClawConfig): void {
89
+ for (const key of Object.keys(draft) as Array<keyof OpenClawConfig>) {
90
+ delete draft[key];
91
+ }
92
+ Object.assign(draft, structuredClone(nextConfig));
93
+ }
@@ -0,0 +1,64 @@
1
+ import { SetupCancelledError, type SetupPrompter } from "./setup-wizard.js";
2
+ import type { AnnounceLogDetail } from "./types.js";
3
+
4
+ const CANCELLED_MESSAGE = "Debug configuration cancelled. No changes were written.";
5
+ const SAVED_MESSAGE = "Debug config updated.\n\nRestart the gateway to apply changes:\nopenclaw gateway restart";
6
+
7
+ export type DebugPromptChoice = AnnounceLogDetail | "cancel";
8
+
9
+ export type RunDebugWizardOptions = {
10
+ prompter: SetupPrompter;
11
+ current: AnnounceLogDetail;
12
+ writer: {
13
+ saveAnnounceLogDetail(detail: AnnounceLogDetail): Promise<void>;
14
+ };
15
+ };
16
+
17
+ export type DebugWizardResult =
18
+ | { status: "saved"; announceLogDetail: AnnounceLogDetail; message: string }
19
+ | { status: "cancelled"; message: string };
20
+
21
+ export async function runDebugWizard(options: RunDebugWizardOptions): Promise<DebugWizardResult> {
22
+ try {
23
+ options.prompter.print(`Current announceLogDetail: ${options.current}`);
24
+ const selected = await options.prompter.select<DebugPromptChoice>("Set announceLogDetail:", [
25
+ { label: "summary: log peer, instance, address and attribute counts", value: "summary" },
26
+ { label: "off: disable announce summary/payload logs", value: "off" },
27
+ { label: "payload: log full announce JSON", value: "payload" },
28
+ { label: "Cancel", value: "cancel" },
29
+ ]);
30
+
31
+ if (selected === "cancel") {
32
+ return cancelledResult();
33
+ }
34
+
35
+ if (selected === "payload") {
36
+ const confirmed = await options.prompter.confirm(
37
+ "Full announce payload logs may include userPublicAttributes, multiaddrs, pubkey and instance identity. Enable payload logs?",
38
+ false,
39
+ );
40
+ if (!confirmed) {
41
+ return cancelledResult();
42
+ }
43
+ }
44
+
45
+ await options.writer.saveAnnounceLogDetail(selected);
46
+ return {
47
+ status: "saved",
48
+ announceLogDetail: selected,
49
+ message: SAVED_MESSAGE,
50
+ };
51
+ } catch (error) {
52
+ if (error instanceof SetupCancelledError) {
53
+ return cancelledResult();
54
+ }
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ function cancelledResult(): DebugWizardResult {
60
+ return {
61
+ status: "cancelled",
62
+ message: CANCELLED_MESSAGE,
63
+ };
64
+ }
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ AnnounceLogDetail,
2
3
  DeliveryAckPayload,
3
4
  DeliveryTargetResult,
4
5
  InboundTargetConfig,
@@ -50,6 +51,20 @@ function summarizeError(error: unknown): string {
50
51
  return error instanceof Error ? error.message : String(error);
51
52
  }
52
53
 
54
+ function effectiveAnnounceLogDetail(value: unknown): AnnounceLogDetail {
55
+ if (value === "off" || value === "payload") {
56
+ return value;
57
+ }
58
+
59
+ return "summary";
60
+ }
61
+
62
+ function countAnnounceAttributes(payload: InstanceAnnouncePayload): number {
63
+ return Array.isArray(payload.userPublicAttributes)
64
+ ? payload.userPublicAttributes.length
65
+ : 0;
66
+ }
67
+
53
68
  function describeAttributeMatch(match: UserAttributeMatch): string {
54
69
  if (match.kind === "tag") {
55
70
  return `tag ${match.value}`;
@@ -149,6 +164,7 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
149
164
  const { mesh, store, delivery } = options;
150
165
  const config = options.config ?? {};
151
166
  const logger = options.logger;
167
+ const announceLogDetail = effectiveAnnounceLogDetail(config.announceLogDetail);
152
168
  const ackTimeoutMs = config.deliveryAckTimeoutMs ?? 15000;
153
169
  const announcedPeers = new Set<string>();
154
170
  const pendingAcks = new Map<string, PendingAck>();
@@ -212,9 +228,46 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
212
228
  payload: JSON.stringify(payload),
213
229
  });
214
230
  announcedPeers.add(peerId);
215
- logger?.info?.(
216
- `[libp2p-mesh] Sent instance announce to ${peerId} (${payload.instanceId})`,
217
- );
231
+ logAnnounce("Sent", peerId, payload);
232
+ }
233
+
234
+ function announceSummary(
235
+ direction: "Sent" | "Received",
236
+ peerId: string,
237
+ payload: InstanceAnnouncePayload,
238
+ changed?: boolean,
239
+ ): string {
240
+ const changedDetail = changed === undefined ? "" : ` changed=${changed}`;
241
+ return `[libp2p-mesh] ${direction} instance announce peer=${peerId} instance=${payload.instanceId} addrs=${payload.multiaddrs.length} attrs=${countAnnounceAttributes(payload)}${changedDetail}`;
242
+ }
243
+
244
+ function logAnnounce(
245
+ direction: "Sent" | "Received",
246
+ peerId: string,
247
+ payload: InstanceAnnouncePayload,
248
+ changed?: boolean,
249
+ ): void {
250
+ if (announceLogDetail === "off") {
251
+ if (direction === "Sent") {
252
+ logger?.info?.(
253
+ `[libp2p-mesh] Sent instance announce to ${peerId} (${payload.instanceId})`,
254
+ );
255
+ }
256
+ return;
257
+ }
258
+
259
+ logger?.info?.(announceSummary(direction, peerId, payload, changed));
260
+ if (announceLogDetail !== "payload") {
261
+ return;
262
+ }
263
+
264
+ try {
265
+ logger?.debug?.(
266
+ `[libp2p-mesh] ${direction} instance announce payload=${JSON.stringify(payload)}`,
267
+ );
268
+ } catch {
269
+ return;
270
+ }
218
271
  }
219
272
 
220
273
  async function announceToConnectedPeers(): Promise<void> {
@@ -255,9 +308,8 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
255
308
  logger?.info?.(
256
309
  `[libp2p-mesh] Instance mapping updated: ${payload.instanceId} -> ${payload.peerId}`,
257
310
  );
258
- } else {
259
- logger?.debug?.(`[libp2p-mesh] Instance mapping unchanged: ${payload.instanceId}`);
260
311
  }
312
+ logAnnounce("Received", msg.from, payload, result.changed);
261
313
 
262
314
  if (!announcedPeers.has(msg.from)) {
263
315
  await announceToPeer(msg.from).catch((error) => {
@@ -449,7 +501,11 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
449
501
  }
450
502
  }
451
503
 
452
- async function start(): Promise<void> {
504
+ function attachHandlers(): void {
505
+ if (unsubs.length > 0) {
506
+ return;
507
+ }
508
+
453
509
  unsubs.push(
454
510
  mesh.onMessage((msg) => {
455
511
  handleMessage(msg).catch((error) => {
@@ -468,7 +524,10 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
468
524
  });
469
525
  }),
470
526
  );
527
+ }
471
528
 
529
+ async function start(): Promise<void> {
530
+ attachHandlers();
472
531
  await announceToConnectedPeers();
473
532
  }
474
533
 
@@ -658,6 +717,8 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
658
717
  }
659
718
 
660
719
  return {
720
+ attachHandlers,
721
+ announceToConnectedPeers,
661
722
  start,
662
723
  stop,
663
724
  handleMessage,
package/src/plugin.ts CHANGED
@@ -12,13 +12,26 @@ import { registerLibp2pMeshCli } from "./profile-cli.js";
12
12
  import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
13
13
  import type { MeshConfig } from "./types.js";
14
14
 
15
+ export type Libp2pMeshPluginDeps = {
16
+ createMeshNetwork?: typeof createMeshNetwork;
17
+ createInstanceRouter?: typeof createInstanceRouter;
18
+ };
19
+
15
20
  export function registerLibp2pMesh(api: OpenClawPluginApi) {
21
+ registerLibp2pMeshWithDeps(api);
22
+ }
23
+
24
+ export function registerLibp2pMeshWithDeps(
25
+ api: OpenClawPluginApi,
26
+ deps: Libp2pMeshPluginDeps = {},
27
+ ) {
16
28
  registerLibp2pMeshCli(api);
17
29
 
18
30
  const config = api.pluginConfig as MeshConfig | undefined;
19
31
  let unsubscribeInbound: (() => void) | undefined;
20
32
  let serviceStarted = false;
21
- const mesh = createMeshNetwork({
33
+ let startPromise: Promise<void> | undefined;
34
+ const mesh = (deps.createMeshNetwork ?? createMeshNetwork)({
22
35
  config,
23
36
  logger: api.logger,
24
37
  });
@@ -39,7 +52,7 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
39
52
  },
40
53
  logger: api.logger,
41
54
  });
42
- const router = createInstanceRouter({
55
+ const router = (deps.createInstanceRouter ?? createInstanceRouter)({
43
56
  mesh,
44
57
  store,
45
58
  delivery,
@@ -51,6 +64,73 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
51
64
 
52
65
  const channel = createLibp2pMeshChannel(mesh);
53
66
 
67
+ function attachInboundHandlers(): () => void {
68
+ return mesh.onMessage((msg) => {
69
+ if (msg.type === "direct" || msg.type === "broadcast") {
70
+ const sendToChannel: InboundHandlerDeps["sendToChannel"] = async (_channelId, _target, text) => {
71
+ if (!config?.inboundChannel || !config?.inboundTarget) {
72
+ api.logger.warn?.(
73
+ "[libp2p-mesh] inboundChannel/inboundTarget not configured; direct message logged only.",
74
+ );
75
+ return;
76
+ }
77
+
78
+ const result = await delivery.deliver({
79
+ channel: config.inboundChannel,
80
+ target: config.inboundTarget,
81
+ text,
82
+ metadata: {
83
+ fromInstanceId: msg.instanceId ?? msg.from,
84
+ fromPeerId: msg.from,
85
+ p2pMessageId: msg.id,
86
+ allowAgentAutoReply: false,
87
+ replyToInstanceId: msg.instanceId ?? msg.from,
88
+ replyTool: "p2p_send_instance_message",
89
+ },
90
+ });
91
+ if (!result.ok) {
92
+ api.logger.error?.(
93
+ `[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${result.error}`,
94
+ );
95
+ }
96
+ };
97
+ handleP2PInbound(msg, { logger: api.logger, sendToChannel });
98
+ } else if (msg.type === "agent-sync") {
99
+ handleP2PInbound(msg, { logger: api.logger });
100
+ }
101
+ });
102
+ }
103
+
104
+ async function cleanupStartupFailure(): Promise<void> {
105
+ try {
106
+ unsubscribeInbound?.();
107
+ } catch (error) {
108
+ api.logger.warn?.(
109
+ `[libp2p-mesh] Failed to clean inbound handler after startup failure: ${String(error)}`,
110
+ );
111
+ } finally {
112
+ unsubscribeInbound = undefined;
113
+ }
114
+
115
+ try {
116
+ await router.stop();
117
+ } catch (error) {
118
+ api.logger.warn?.(
119
+ `[libp2p-mesh] Failed to stop router after startup failure: ${String(error)}`,
120
+ );
121
+ }
122
+
123
+ try {
124
+ await mesh.stop();
125
+ } catch (error) {
126
+ api.logger.warn?.(
127
+ `[libp2p-mesh] Failed to stop mesh after startup failure: ${String(error)}`,
128
+ );
129
+ }
130
+
131
+ serviceStarted = false;
132
+ }
133
+
54
134
  // 1. Register Service (manages libp2p node lifecycle)
55
135
  api.registerService({
56
136
  id: "libp2p-mesh",
@@ -59,64 +139,52 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
59
139
  api.logger.debug?.("[libp2p-mesh] Service already started; ignoring duplicate start.");
60
140
  return;
61
141
  }
62
- await mesh.start();
63
- await router.start();
64
- unsubscribeInbound = mesh.onMessage((msg) => {
65
- if (msg.type === "direct" || msg.type === "broadcast") {
66
- const sendToChannel: InboundHandlerDeps["sendToChannel"] = async (_channelId, _target, text) => {
67
- if (!config?.inboundChannel || !config?.inboundTarget) {
68
- api.logger.warn?.(
69
- "[libp2p-mesh] inboundChannel/inboundTarget not configured; direct message logged only.",
70
- );
71
- return;
72
- }
73
-
74
- const result = await delivery.deliver({
75
- channel: config.inboundChannel,
76
- target: config.inboundTarget,
77
- text,
78
- metadata: {
79
- fromInstanceId: msg.instanceId ?? msg.from,
80
- fromPeerId: msg.from,
81
- p2pMessageId: msg.id,
82
- allowAgentAutoReply: false,
83
- replyToInstanceId: msg.instanceId ?? msg.from,
84
- replyTool: "p2p_send_instance_message",
85
- },
86
- });
87
- if (!result.ok) {
88
- api.logger.error?.(
89
- `[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${result.error}`,
90
- );
91
- }
92
- };
93
- handleP2PInbound(msg, { logger: api.logger, sendToChannel });
94
- } else if (msg.type === "agent-sync") {
95
- handleP2PInbound(msg, { logger: api.logger });
96
- }
97
- });
98
- const identity = mesh.getInstanceIdentity();
99
- api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
100
- if (identity) {
101
- api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
102
- }
103
- const nat = mesh.getNATStatus();
104
- const enabledNames = Object.entries(nat.enabled)
105
- .filter(([, on]) => on)
106
- .map(([k]) => k);
107
- if (enabledNames.length > 0) {
108
- api.logger.info?.(
109
- `[libp2p-mesh] NAT traversal services: ${enabledNames.join(", ")}`,
110
- );
111
- }
112
- if (nat.reservedRelays.length > 0) {
113
- api.logger.info?.(
114
- `[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`,
115
- );
142
+
143
+ if (startPromise) {
144
+ api.logger.debug?.("[libp2p-mesh] Service start already in progress; waiting for it.");
145
+ return startPromise;
116
146
  }
117
- serviceStarted = true;
147
+
148
+ startPromise = (async () => {
149
+ try {
150
+ router.attachHandlers();
151
+ unsubscribeInbound = attachInboundHandlers();
152
+ await mesh.start();
153
+ await router.announceToConnectedPeers();
154
+ const identity = mesh.getInstanceIdentity();
155
+ api.logger.info?.(
156
+ `[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`,
157
+ );
158
+ if (identity) {
159
+ api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
160
+ }
161
+ const nat = mesh.getNATStatus();
162
+ const enabledNames = Object.entries(nat.enabled)
163
+ .filter(([, on]) => on)
164
+ .map(([k]) => k);
165
+ if (enabledNames.length > 0) {
166
+ api.logger.info?.(
167
+ `[libp2p-mesh] NAT traversal services: ${enabledNames.join(", ")}`,
168
+ );
169
+ }
170
+ if (nat.reservedRelays.length > 0) {
171
+ api.logger.info?.(
172
+ `[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`,
173
+ );
174
+ }
175
+ serviceStarted = true;
176
+ } catch (error) {
177
+ await cleanupStartupFailure();
178
+ throw error;
179
+ } finally {
180
+ startPromise = undefined;
181
+ }
182
+ })();
183
+
184
+ return startPromise;
118
185
  },
119
186
  stop: async () => {
187
+ await startPromise?.catch(() => undefined);
120
188
  unsubscribeInbound?.();
121
189
  unsubscribeInbound = undefined;
122
190
  await router.stop();
@@ -7,6 +7,7 @@ import {
7
7
  type ClosableSetupPrompter,
8
8
  type SetupCliDeps,
9
9
  } from "./setup-cli.js";
10
+ import { registerLibp2pMeshDebugCommand, type DebugCliDeps } from "./debug-cli.js";
10
11
  import { runProfileWizard } from "./profile-wizard.js";
11
12
  import { createUserMdAttributeSource } from "./user-md-attributes.js";
12
13
  import { createUserProfileStore, type UserProfileStore } from "./user-profile-store.js";
@@ -29,6 +30,7 @@ export type ProfileCliDeps = {
29
30
  export type Libp2pMeshCliDeps = {
30
31
  setup?: SetupCliDeps;
31
32
  profile?: ProfileCliDeps;
33
+ debug?: DebugCliDeps;
32
34
  };
33
35
 
34
36
  export function registerLibp2pMeshCli(api: OpenClawPluginApi, deps: Libp2pMeshCliDeps = {}): void {
@@ -39,6 +41,7 @@ export function registerLibp2pMeshCli(api: OpenClawPluginApi, deps: Libp2pMeshCl
39
41
 
40
42
  registerLibp2pMeshSetupCommand(root, api, ctx, deps.setup);
41
43
  registerLibp2pMeshProfileCommand(root, api, ctx, deps.profile);
44
+ registerLibp2pMeshDebugCommand(root, api, ctx, deps.debug);
42
45
  }, LIBP2P_MESH_CLI_REGISTRATION);
43
46
  }
44
47
 
@@ -1,4 +1,4 @@
1
- import type { InboundTargetConfig, MeshConfig } from "./types.js";
1
+ import type { AnnounceLogDetail, InboundTargetConfig, MeshConfig } from "./types.js";
2
2
 
3
3
  export const LIBP2P_MESH_PLUGIN_ID = "libp2p-mesh";
4
4
  export const DEFAULT_DELIVERY_ACK_TIMEOUT_MS = 15000;
@@ -38,6 +38,14 @@ export function getLibp2pMeshConfig(config: OpenClawConfigLike): MeshConfig | un
38
38
  return config.plugins?.entries?.[LIBP2P_MESH_PLUGIN_ID]?.config as MeshConfig | undefined;
39
39
  }
40
40
 
41
+ export function getAnnounceLogDetail(config: OpenClawConfigLike): AnnounceLogDetail {
42
+ return normalizeAnnounceLogDetail(getLibp2pMeshConfig(config)?.announceLogDetail);
43
+ }
44
+
45
+ export function normalizeAnnounceLogDetail(value: unknown): AnnounceLogDetail {
46
+ return value === "off" || value === "payload" || value === "summary" ? value : "summary";
47
+ }
48
+
41
49
  export function buildNetworkConfig(
42
50
  mode: SetupMode,
43
51
  options?: {
@@ -99,6 +107,32 @@ export function applyPluginConfig(config: OpenClawConfigLike, pluginConfig: Mesh
99
107
  };
100
108
  }
101
109
 
110
+ export function applyAnnounceLogDetail(
111
+ config: OpenClawConfigLike,
112
+ announceLogDetail: AnnounceLogDetail,
113
+ ): OpenClawConfigLike {
114
+ const existingEntry = config.plugins?.entries?.[LIBP2P_MESH_PLUGIN_ID];
115
+ const existingPluginConfig = existingEntry?.config ?? {};
116
+
117
+ return {
118
+ ...config,
119
+ plugins: {
120
+ ...config.plugins,
121
+ entries: {
122
+ ...config.plugins?.entries,
123
+ [LIBP2P_MESH_PLUGIN_ID]: {
124
+ ...existingEntry,
125
+ enabled: existingEntry?.enabled ?? true,
126
+ config: {
127
+ ...existingPluginConfig,
128
+ announceLogDetail,
129
+ },
130
+ },
131
+ },
132
+ },
133
+ };
134
+ }
135
+
102
136
  export function mergeNetworkConfig(existing: MeshConfig | undefined, networkConfig: MeshConfig): MeshConfig {
103
137
  if (!existing) {
104
138
  return { ...networkConfig };
package/src/types.ts CHANGED
@@ -203,6 +203,8 @@ export type InstanceRouterOptions = {
203
203
  };
204
204
 
205
205
  export interface InstanceRouter {
206
+ attachHandlers(): void;
207
+ announceToConnectedPeers(): Promise<void>;
206
208
  start(): Promise<void>;
207
209
  stop(): Promise<void>;
208
210
  handleMessage(msg: P2PMessage): Promise<void>;
@@ -232,6 +234,7 @@ export interface MeshConfig {
232
234
  discovery?: "mdns" | "bootstrap" | "dht";
233
235
  bootstrapList?: string[];
234
236
  meshTopic?: string;
237
+ announceLogDetail?: AnnounceLogDetail;
235
238
  enableAgentSync?: boolean;
236
239
  enableWebSocket?: boolean;
237
240
  peerIdPath?: string;
@@ -335,6 +338,8 @@ export interface MeshNetwork {
335
338
  getNATStatus(): NATTraversalStatus;
336
339
  }
337
340
 
341
+ export type AnnounceLogDetail = "off" | "summary" | "payload";
342
+
338
343
  export type MeshAccount = {
339
344
  accountId: string;
340
345
  configured: boolean;