libp2p-mesh 2026.6.13 → 2026.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.15",
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;
@@ -1,4 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
2
3
  import path from "node:path";
3
4
 
4
5
  import type { UserPublicAttribute } from "./types.js";
@@ -59,6 +60,19 @@ export type UserMdAttributeSource = {
59
60
  };
60
61
  };
61
62
 
63
+ export function resolveUserMdPath(customPath?: string): string {
64
+ if (customPath) {
65
+ return customPath;
66
+ }
67
+
68
+ const stateDir = process.env.OPENCLAW_STATE_DIR;
69
+ if (stateDir) {
70
+ return path.join(stateDir, "workspace", "USER.md");
71
+ }
72
+
73
+ return path.join(homedir(), ".openclaw", "workspace", "USER.md");
74
+ }
75
+
62
76
  function isTemplateText(value: string): boolean {
63
77
  const trimmed = value.trim();
64
78
  return trimmed.length === 0 || TEMPLATE_PATTERN.test(trimmed) || /^\[[^\]]+\]$/.test(trimmed);
@@ -236,7 +250,7 @@ export function extractUserMdTags(markdown: string): UserPublicAttribute[] {
236
250
  export function createUserMdAttributeSource(options?: UserMdAttributeSource): {
237
251
  loadTags(): Promise<UserPublicAttribute[]>;
238
252
  } {
239
- const filePath = options?.path ?? path.join(process.cwd(), "USER.md");
253
+ const filePath = resolveUserMdPath(options?.path);
240
254
  const logger = options?.logger;
241
255
 
242
256
  return {