libp2p-mesh 2026.6.22 → 2026.6.24

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.
@@ -121,6 +121,7 @@ export function createInstanceRouter(options) {
121
121
  const deliveryCache = new Map();
122
122
  const unsubs = [];
123
123
  const pendingAttributePeers = new Set();
124
+ let publicAttributeRefreshEnabled = options.publicAttributeRefreshInitiallyEnabled !== false;
124
125
  let attributeRefreshPromise;
125
126
  function localInstanceId() {
126
127
  const identity = mesh.getInstanceIdentity();
@@ -215,7 +216,9 @@ export function createInstanceRouter(options) {
215
216
  if (pendingAttributePeers.size === 0 && !attributeRefreshPromise) {
216
217
  return;
217
218
  }
218
- drainAttributeRefresh();
219
+ if (publicAttributeRefreshEnabled) {
220
+ drainAttributeRefresh();
221
+ }
219
222
  }
220
223
  function drainAttributeRefresh() {
221
224
  if (!attributeRefreshPromise) {
@@ -233,6 +236,12 @@ export function createInstanceRouter(options) {
233
236
  }
234
237
  return attributeRefreshPromise;
235
238
  }
239
+ function enablePublicAttributeRefresh() {
240
+ publicAttributeRefreshEnabled = true;
241
+ if (pendingAttributePeers.size > 0) {
242
+ drainAttributeRefresh();
243
+ }
244
+ }
236
245
  async function announceToPeer(peerId) {
237
246
  if (!isNonEmptyString(peerId) || peerId === mesh.getLocalPeerId()) {
238
247
  return;
@@ -667,6 +676,7 @@ export function createInstanceRouter(options) {
667
676
  stop,
668
677
  handleMessage,
669
678
  announceToPeer,
679
+ enablePublicAttributeRefresh,
670
680
  refreshPublicAttributes,
671
681
  listInstances,
672
682
  resolveInstance,
@@ -10,6 +10,7 @@ import { createOpenClawUserMdAttributeExtractor } from "./user-md-openclaw-extra
10
10
  import { createUserProfileStore } from "./user-profile-store.js";
11
11
  import { buildP2PTools } from "./agent-tools.js";
12
12
  import { registerLibp2pMeshCli } from "./profile-cli.js";
13
+ const DEFAULT_PUBLIC_ATTRIBUTE_REFRESH_STARTUP_DELAY_MS = 10000;
13
14
  export function registerLibp2pMesh(api) {
14
15
  registerLibp2pMeshWithDeps(api);
15
16
  }
@@ -18,6 +19,7 @@ export function registerLibp2pMeshWithDeps(api, deps = {}) {
18
19
  let unsubscribeInbound;
19
20
  let serviceStarted = false;
20
21
  let startPromise;
22
+ let publicAttributeRefreshTimer;
21
23
  const mesh = (deps.createMeshNetwork ?? createMeshNetwork)({
22
24
  config,
23
25
  logger: api.logger,
@@ -50,6 +52,7 @@ export function registerLibp2pMeshWithDeps(api, deps = {}) {
50
52
  userAttributeSource,
51
53
  userProfileStore,
52
54
  peerLabelStore,
55
+ publicAttributeRefreshInitiallyEnabled: false,
53
56
  });
54
57
  registerLibp2pMeshCli(api, {
55
58
  profile: {
@@ -91,7 +94,28 @@ export function registerLibp2pMeshWithDeps(api, deps = {}) {
91
94
  }
92
95
  });
93
96
  }
97
+ function publicAttributeRefreshStartupDelayMs() {
98
+ const configured = config?.publicAttributeRefreshStartupDelayMs;
99
+ if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
100
+ return configured;
101
+ }
102
+ return DEFAULT_PUBLIC_ATTRIBUTE_REFRESH_STARTUP_DELAY_MS;
103
+ }
104
+ function clearPublicAttributeRefreshTimer() {
105
+ if (publicAttributeRefreshTimer) {
106
+ clearTimeout(publicAttributeRefreshTimer);
107
+ publicAttributeRefreshTimer = undefined;
108
+ }
109
+ }
110
+ function schedulePublicAttributeRefreshEnable() {
111
+ clearPublicAttributeRefreshTimer();
112
+ publicAttributeRefreshTimer = setTimeout(() => {
113
+ publicAttributeRefreshTimer = undefined;
114
+ router.enablePublicAttributeRefresh();
115
+ }, publicAttributeRefreshStartupDelayMs());
116
+ }
94
117
  async function cleanupStartupFailure() {
118
+ clearPublicAttributeRefreshTimer();
95
119
  try {
96
120
  unsubscribeInbound?.();
97
121
  }
@@ -149,6 +173,7 @@ export function registerLibp2pMeshWithDeps(api, deps = {}) {
149
173
  api.logger.info?.(`[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`);
150
174
  }
151
175
  serviceStarted = true;
176
+ schedulePublicAttributeRefreshEnable();
152
177
  }
153
178
  catch (error) {
154
179
  await cleanupStartupFailure();
@@ -162,6 +187,7 @@ export function registerLibp2pMeshWithDeps(api, deps = {}) {
162
187
  },
163
188
  stop: async () => {
164
189
  await startPromise?.catch(() => undefined);
190
+ clearPublicAttributeRefreshTimer();
165
191
  unsubscribeInbound?.();
166
192
  unsubscribeInbound = undefined;
167
193
  await router.stop();
@@ -40,7 +40,7 @@ export const LIBP2P_MESH_AGENT_PROMPT = `
40
40
 
41
41
  - \`source="USER.md"\` 表示 gateway 使用 OpenClaw 已配置的 agent/API 模型,从 USER.md 异步提取并公开广播的 tag。
42
42
  - \`source="profile"\` 表示用户通过 \`openclaw libp2p-mesh profile\` 手动配置的公开结构化属性。
43
- - gateway 的基础 \`instance-announce\` 可能省略 \`userPublicAttributes\`;这表示本次 announce 没有携带属性,不一定表示该用户没有公开属性。按公开属性发送前先用 \`p2p_list_instances\` 查看当前实例记录。
43
+ - gateway 的基础 \`instance-announce\` 可能省略 \`userPublicAttributes\`;这表示本次 announce 没有携带属性,不一定表示该用户没有公开属性。启动后 USER.md 属性刷新可能会延迟数秒完成。按公开属性发送前先用 \`p2p_list_instances\` 查看当前实例记录。
44
44
  - 普通对话 agent 不应自己读取 USER.md 来决定公开属性。USER.md 属性提取是 gateway 后台职责。
45
45
 
46
46
  3. \`openclaw libp2p-mesh labels\` manages local labels for remote instances. These labels are stored in the local \`peer-labels.json\`, stay on this machine, and are used only when the send tool uses \`scope="local"\` or \`scope="all"\`.
@@ -216,6 +216,7 @@ export type InstanceRouterOptions = {
216
216
  peerLabelStore?: {
217
217
  listLabels(instanceId: string): Promise<LocalPeerLabelAttribute[]>;
218
218
  };
219
+ publicAttributeRefreshInitiallyEnabled?: boolean;
219
220
  };
220
221
  export interface InstanceRouter {
221
222
  attachHandlers(): void;
@@ -224,6 +225,7 @@ export interface InstanceRouter {
224
225
  stop(): Promise<void>;
225
226
  handleMessage(msg: P2PMessage): Promise<void>;
226
227
  announceToPeer(peerId: string): Promise<void>;
228
+ enablePublicAttributeRefresh(): void;
227
229
  refreshPublicAttributes(): Promise<void>;
228
230
  listInstances(): Promise<InstancePeerRecord[]>;
229
231
  resolveInstance(instanceId: string): Promise<InstancePeerRecord | undefined>;
@@ -246,6 +248,7 @@ export interface MeshConfig {
246
248
  bootstrapList?: string[];
247
249
  meshTopic?: string;
248
250
  announceLogDetail?: AnnounceLogDetail;
251
+ publicAttributeRefreshStartupDelayMs?: number;
249
252
  enableAgentSync?: boolean;
250
253
  enableWebSocket?: boolean;
251
254
  peerIdPath?: string;
@@ -6,6 +6,7 @@ type Logger = {
6
6
  warn?: (message: string) => void;
7
7
  };
8
8
  export declare function extractLatestAssistantText(messages: unknown[]): string | undefined;
9
+ export declare function extractCompletionText(result: unknown): string | undefined;
9
10
  export declare function parseUserMdAttributeResponse(text: string): UserPublicAttribute[];
10
11
  export declare function createOpenClawUserMdAttributeExtractor(api: OpenClawPluginApi, options?: {
11
12
  timeoutMs?: number;
@@ -1,7 +1,5 @@
1
- import { createHash } from "node:crypto";
2
1
  import { validateExtractedUserMdTags } from "./user-md-agent-attributes.js";
3
2
  const EXTRACTION_TIMEOUT_MS = 30000;
4
- const SESSION_HASH_LENGTH = 16;
5
3
  export const USER_MD_ATTRIBUTE_EXTRACTION_PROMPT = [
6
4
  "你是 libp2p-mesh 的 USER.md 公开属性提取器。",
7
5
  "",
@@ -16,9 +14,6 @@ export const USER_MD_ATTRIBUTE_EXTRACTION_PROMPT = [
16
14
  "- 不要提取“刚认识”“还在了解”“随时告诉我”这类无分类价值内容。",
17
15
  "- 优先提取稳定身份、技术方向、项目方向、长期偏好。",
18
16
  ].join("\n");
19
- function hashForSessionKey(value) {
20
- return createHash("sha256").update(value, "utf8").digest("hex").slice(0, SESSION_HASH_LENGTH);
21
- }
22
17
  function buildUserMdExtractionMessage(markdown, sourcePath) {
23
18
  return [
24
19
  `sourcePath: ${sourcePath}`,
@@ -75,6 +70,42 @@ export function extractLatestAssistantText(messages) {
75
70
  }
76
71
  return undefined;
77
72
  }
73
+ export function extractCompletionText(result) {
74
+ if (typeof result === "string") {
75
+ return result;
76
+ }
77
+ if (!isRecord(result)) {
78
+ return undefined;
79
+ }
80
+ const direct = extractTextFromContent(result.content) ??
81
+ (typeof result.text === "string" ? result.text : undefined) ??
82
+ (typeof result.outputText === "string" ? result.outputText : undefined) ??
83
+ (typeof result.message === "string" ? result.message : undefined);
84
+ if (direct?.trim()) {
85
+ return direct;
86
+ }
87
+ const choices = result.choices;
88
+ if (!Array.isArray(choices)) {
89
+ return undefined;
90
+ }
91
+ for (const choice of choices) {
92
+ if (!isRecord(choice)) {
93
+ continue;
94
+ }
95
+ const message = choice.message;
96
+ if (isRecord(message)) {
97
+ const text = extractTextFromContent(message.content);
98
+ if (text?.trim()) {
99
+ return text;
100
+ }
101
+ }
102
+ const text = extractTextFromContent(choice.content);
103
+ if (text?.trim()) {
104
+ return text;
105
+ }
106
+ }
107
+ return undefined;
108
+ }
78
109
  function stripJsonFence(text) {
79
110
  const trimmed = text.trim();
80
111
  const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
@@ -91,32 +122,38 @@ export function parseUserMdAttributeResponse(text) {
91
122
  function unavailable(reason) {
92
123
  return { unavailable: true, reason };
93
124
  }
125
+ function runtimeLlm(api) {
126
+ const runtime = api.runtime;
127
+ if (!isRecord(runtime)) {
128
+ return undefined;
129
+ }
130
+ const llm = runtime.llm;
131
+ if (!isRecord(llm) || typeof llm.complete !== "function") {
132
+ return undefined;
133
+ }
134
+ return llm;
135
+ }
94
136
  export function createOpenClawUserMdAttributeExtractor(api, options) {
95
137
  const timeoutMs = options?.timeoutMs ?? EXTRACTION_TIMEOUT_MS;
96
138
  const logger = options?.logger ?? api.logger;
97
139
  return {
98
140
  async extract({ markdown, sourcePath }) {
99
- const subagent = api.runtime?.subagent;
100
- if (!subagent?.run || !subagent.waitForRun || !subagent.getSessionMessages) {
101
- return unavailable("OpenClaw subagent runtime is unavailable");
141
+ const llm = runtimeLlm(api);
142
+ if (!llm) {
143
+ return unavailable("OpenClaw runtime llm.complete is unavailable");
102
144
  }
103
- const sessionKey = `libp2p-mesh:user-md-attributes:${hashForSessionKey(sourcePath)}`;
104
145
  try {
105
- await subagent.deleteSession?.({ sessionKey, deleteTranscript: true }).catch(() => undefined);
106
- const { runId } = await subagent.run({
107
- sessionKey,
108
- message: buildUserMdExtractionMessage(markdown, sourcePath),
109
- extraSystemPrompt: USER_MD_ATTRIBUTE_EXTRACTION_PROMPT,
110
- lane: "libp2p-mesh-user-md-attributes",
111
- lightContext: true,
112
- deliver: false,
146
+ const result = await llm.complete({
147
+ messages: [
148
+ { role: "system", content: USER_MD_ATTRIBUTE_EXTRACTION_PROMPT },
149
+ { role: "user", content: buildUserMdExtractionMessage(markdown, sourcePath) },
150
+ ],
151
+ purpose: "libp2p-mesh.user-md-attributes",
152
+ maxTokens: 512,
153
+ temperature: 0.1,
154
+ timeoutMs,
113
155
  });
114
- const waitResult = await subagent.waitForRun({ runId, timeoutMs });
115
- if (waitResult.status !== "ok") {
116
- return unavailable(waitResult.error ?? `OpenClaw subagent extraction ${waitResult.status}`);
117
- }
118
- const { messages } = await subagent.getSessionMessages({ sessionKey, limit: 10 });
119
- const text = extractLatestAssistantText(messages);
156
+ const text = extractCompletionText(result);
120
157
  if (!text) {
121
158
  return [];
122
159
  }
@@ -127,9 +164,6 @@ export function createOpenClawUserMdAttributeExtractor(api, options) {
127
164
  logger?.warn?.(`[libp2p-mesh] USER.md agent extraction failed: ${reason}`);
128
165
  return unavailable(reason);
129
166
  }
130
- finally {
131
- await subagent.deleteSession?.({ sessionKey, deleteTranscript: true }).catch(() => undefined);
132
- }
133
167
  },
134
168
  };
135
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2p-mesh",
3
- "version": "2026.6.22",
3
+ "version": "2026.6.24",
4
4
  "description": "OpenClaw libp2p P2P mesh network plugin for cross-instance agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -218,6 +218,7 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
218
218
  const deliveryCache = new Map<string, DeliveryCacheEntry>();
219
219
  const unsubs: Array<() => void> = [];
220
220
  const pendingAttributePeers = new Set<string>();
221
+ let publicAttributeRefreshEnabled = options.publicAttributeRefreshInitiallyEnabled !== false;
221
222
  let attributeRefreshPromise: Promise<void> | undefined;
222
223
 
223
224
  function localInstanceId(): string {
@@ -335,7 +336,9 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
335
336
  return;
336
337
  }
337
338
 
338
- drainAttributeRefresh();
339
+ if (publicAttributeRefreshEnabled) {
340
+ drainAttributeRefresh();
341
+ }
339
342
  }
340
343
 
341
344
  function drainAttributeRefresh(): Promise<void> {
@@ -358,6 +361,13 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
358
361
  return attributeRefreshPromise;
359
362
  }
360
363
 
364
+ function enablePublicAttributeRefresh(): void {
365
+ publicAttributeRefreshEnabled = true;
366
+ if (pendingAttributePeers.size > 0) {
367
+ drainAttributeRefresh();
368
+ }
369
+ }
370
+
361
371
  async function announceToPeer(peerId: string): Promise<void> {
362
372
  if (!isNonEmptyString(peerId) || peerId === mesh.getLocalPeerId()) {
363
373
  return;
@@ -896,6 +906,7 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
896
906
  stop,
897
907
  handleMessage,
898
908
  announceToPeer,
909
+ enablePublicAttributeRefresh,
899
910
  refreshPublicAttributes,
900
911
  listInstances,
901
912
  resolveInstance,
package/src/plugin.ts CHANGED
@@ -14,6 +14,8 @@ import { registerLibp2pMeshCli } from "./profile-cli.js";
14
14
  import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
15
15
  import type { MeshConfig } from "./types.js";
16
16
 
17
+ const DEFAULT_PUBLIC_ATTRIBUTE_REFRESH_STARTUP_DELAY_MS = 10000;
18
+
17
19
  export type Libp2pMeshPluginDeps = {
18
20
  createMeshNetwork?: typeof createMeshNetwork;
19
21
  createInstanceRouter?: typeof createInstanceRouter;
@@ -31,6 +33,7 @@ export function registerLibp2pMeshWithDeps(
31
33
  let unsubscribeInbound: (() => void) | undefined;
32
34
  let serviceStarted = false;
33
35
  let startPromise: Promise<void> | undefined;
36
+ let publicAttributeRefreshTimer: ReturnType<typeof setTimeout> | undefined;
34
37
  const mesh = (deps.createMeshNetwork ?? createMeshNetwork)({
35
38
  config,
36
39
  logger: api.logger,
@@ -65,6 +68,7 @@ export function registerLibp2pMeshWithDeps(
65
68
  userAttributeSource,
66
69
  userProfileStore,
67
70
  peerLabelStore,
71
+ publicAttributeRefreshInitiallyEnabled: false,
68
72
  });
69
73
 
70
74
  registerLibp2pMeshCli(api, {
@@ -114,7 +118,31 @@ export function registerLibp2pMeshWithDeps(
114
118
  });
115
119
  }
116
120
 
121
+ function publicAttributeRefreshStartupDelayMs(): number {
122
+ const configured = config?.publicAttributeRefreshStartupDelayMs;
123
+ if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
124
+ return configured;
125
+ }
126
+ return DEFAULT_PUBLIC_ATTRIBUTE_REFRESH_STARTUP_DELAY_MS;
127
+ }
128
+
129
+ function clearPublicAttributeRefreshTimer(): void {
130
+ if (publicAttributeRefreshTimer) {
131
+ clearTimeout(publicAttributeRefreshTimer);
132
+ publicAttributeRefreshTimer = undefined;
133
+ }
134
+ }
135
+
136
+ function schedulePublicAttributeRefreshEnable(): void {
137
+ clearPublicAttributeRefreshTimer();
138
+ publicAttributeRefreshTimer = setTimeout(() => {
139
+ publicAttributeRefreshTimer = undefined;
140
+ router.enablePublicAttributeRefresh();
141
+ }, publicAttributeRefreshStartupDelayMs());
142
+ }
143
+
117
144
  async function cleanupStartupFailure(): Promise<void> {
145
+ clearPublicAttributeRefreshTimer();
118
146
  try {
119
147
  unsubscribeInbound?.();
120
148
  } catch (error) {
@@ -186,6 +214,7 @@ export function registerLibp2pMeshWithDeps(
186
214
  );
187
215
  }
188
216
  serviceStarted = true;
217
+ schedulePublicAttributeRefreshEnable();
189
218
  } catch (error) {
190
219
  await cleanupStartupFailure();
191
220
  throw error;
@@ -198,6 +227,7 @@ export function registerLibp2pMeshWithDeps(
198
227
  },
199
228
  stop: async () => {
200
229
  await startPromise?.catch(() => undefined);
230
+ clearPublicAttributeRefreshTimer();
201
231
  unsubscribeInbound?.();
202
232
  unsubscribeInbound = undefined;
203
233
  await router.stop();
@@ -42,7 +42,7 @@ export const LIBP2P_MESH_AGENT_PROMPT = `
42
42
 
43
43
  - \`source="USER.md"\` 表示 gateway 使用 OpenClaw 已配置的 agent/API 模型,从 USER.md 异步提取并公开广播的 tag。
44
44
  - \`source="profile"\` 表示用户通过 \`openclaw libp2p-mesh profile\` 手动配置的公开结构化属性。
45
- - gateway 的基础 \`instance-announce\` 可能省略 \`userPublicAttributes\`;这表示本次 announce 没有携带属性,不一定表示该用户没有公开属性。按公开属性发送前先用 \`p2p_list_instances\` 查看当前实例记录。
45
+ - gateway 的基础 \`instance-announce\` 可能省略 \`userPublicAttributes\`;这表示本次 announce 没有携带属性,不一定表示该用户没有公开属性。启动后 USER.md 属性刷新可能会延迟数秒完成。按公开属性发送前先用 \`p2p_list_instances\` 查看当前实例记录。
46
46
  - 普通对话 agent 不应自己读取 USER.md 来决定公开属性。USER.md 属性提取是 gateway 后台职责。
47
47
 
48
48
  3. \`openclaw libp2p-mesh labels\` manages local labels for remote instances. These labels are stored in the local \`peer-labels.json\`, stay on this machine, and are used only when the send tool uses \`scope="local"\` or \`scope="all"\`.
package/src/types.ts CHANGED
@@ -244,6 +244,7 @@ export type InstanceRouterOptions = {
244
244
  peerLabelStore?: {
245
245
  listLabels(instanceId: string): Promise<LocalPeerLabelAttribute[]>;
246
246
  };
247
+ publicAttributeRefreshInitiallyEnabled?: boolean;
247
248
  };
248
249
 
249
250
  export interface InstanceRouter {
@@ -253,6 +254,7 @@ export interface InstanceRouter {
253
254
  stop(): Promise<void>;
254
255
  handleMessage(msg: P2PMessage): Promise<void>;
255
256
  announceToPeer(peerId: string): Promise<void>;
257
+ enablePublicAttributeRefresh(): void;
256
258
  refreshPublicAttributes(): Promise<void>;
257
259
  listInstances(): Promise<InstancePeerRecord[]>;
258
260
  resolveInstance(instanceId: string): Promise<InstancePeerRecord | undefined>;
@@ -280,6 +282,7 @@ export interface MeshConfig {
280
282
  bootstrapList?: string[];
281
283
  meshTopic?: string;
282
284
  announceLogDetail?: AnnounceLogDetail;
285
+ publicAttributeRefreshStartupDelayMs?: number;
283
286
  enableAgentSync?: boolean;
284
287
  enableWebSocket?: boolean;
285
288
  peerIdPath?: string;
@@ -1,4 +1,3 @@
1
- import { createHash } from "node:crypto";
2
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
3
2
  import type { UserPublicAttribute } from "./types.js";
4
3
  import type {
@@ -8,7 +7,6 @@ import type {
8
7
  import { validateExtractedUserMdTags } from "./user-md-agent-attributes.js";
9
8
 
10
9
  const EXTRACTION_TIMEOUT_MS = 30000;
11
- const SESSION_HASH_LENGTH = 16;
12
10
 
13
11
  export const USER_MD_ATTRIBUTE_EXTRACTION_PROMPT = [
14
12
  "你是 libp2p-mesh 的 USER.md 公开属性提取器。",
@@ -29,9 +27,15 @@ type Logger = {
29
27
  warn?: (message: string) => void;
30
28
  };
31
29
 
32
- function hashForSessionKey(value: string): string {
33
- return createHash("sha256").update(value, "utf8").digest("hex").slice(0, SESSION_HASH_LENGTH);
34
- }
30
+ type RuntimeLlmComplete = {
31
+ complete(request: {
32
+ messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
33
+ purpose: string;
34
+ maxTokens?: number;
35
+ temperature?: number;
36
+ timeoutMs?: number;
37
+ }): Promise<unknown>;
38
+ };
35
39
 
36
40
  function buildUserMdExtractionMessage(markdown: string, sourcePath: string): string {
37
41
  return [
@@ -99,6 +103,48 @@ export function extractLatestAssistantText(messages: unknown[]): string | undefi
99
103
  return undefined;
100
104
  }
101
105
 
106
+ export function extractCompletionText(result: unknown): string | undefined {
107
+ if (typeof result === "string") {
108
+ return result;
109
+ }
110
+ if (!isRecord(result)) {
111
+ return undefined;
112
+ }
113
+
114
+ const direct =
115
+ extractTextFromContent(result.content) ??
116
+ (typeof result.text === "string" ? result.text : undefined) ??
117
+ (typeof result.outputText === "string" ? result.outputText : undefined) ??
118
+ (typeof result.message === "string" ? result.message : undefined);
119
+ if (direct?.trim()) {
120
+ return direct;
121
+ }
122
+
123
+ const choices = result.choices;
124
+ if (!Array.isArray(choices)) {
125
+ return undefined;
126
+ }
127
+
128
+ for (const choice of choices) {
129
+ if (!isRecord(choice)) {
130
+ continue;
131
+ }
132
+ const message = choice.message;
133
+ if (isRecord(message)) {
134
+ const text = extractTextFromContent(message.content);
135
+ if (text?.trim()) {
136
+ return text;
137
+ }
138
+ }
139
+ const text = extractTextFromContent(choice.content);
140
+ if (text?.trim()) {
141
+ return text;
142
+ }
143
+ }
144
+
145
+ return undefined;
146
+ }
147
+
102
148
  function stripJsonFence(text: string): string {
103
149
  const trimmed = text.trim();
104
150
  const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
@@ -117,6 +163,18 @@ function unavailable(reason: string): UserMdAttributeExtractionUnavailable {
117
163
  return { unavailable: true, reason };
118
164
  }
119
165
 
166
+ function runtimeLlm(api: OpenClawPluginApi): RuntimeLlmComplete | undefined {
167
+ const runtime = api.runtime as unknown;
168
+ if (!isRecord(runtime)) {
169
+ return undefined;
170
+ }
171
+ const llm = runtime.llm;
172
+ if (!isRecord(llm) || typeof llm.complete !== "function") {
173
+ return undefined;
174
+ }
175
+ return llm as RuntimeLlmComplete;
176
+ }
177
+
120
178
  export function createOpenClawUserMdAttributeExtractor(
121
179
  api: OpenClawPluginApi,
122
180
  options?: {
@@ -129,31 +187,23 @@ export function createOpenClawUserMdAttributeExtractor(
129
187
 
130
188
  return {
131
189
  async extract({ markdown, sourcePath }) {
132
- const subagent = api.runtime?.subagent;
133
- if (!subagent?.run || !subagent.waitForRun || !subagent.getSessionMessages) {
134
- return unavailable("OpenClaw subagent runtime is unavailable");
190
+ const llm = runtimeLlm(api);
191
+ if (!llm) {
192
+ return unavailable("OpenClaw runtime llm.complete is unavailable");
135
193
  }
136
194
 
137
- const sessionKey = `libp2p-mesh:user-md-attributes:${hashForSessionKey(sourcePath)}`;
138
-
139
195
  try {
140
- await subagent.deleteSession?.({ sessionKey, deleteTranscript: true }).catch(() => undefined);
141
- const { runId } = await subagent.run({
142
- sessionKey,
143
- message: buildUserMdExtractionMessage(markdown, sourcePath),
144
- extraSystemPrompt: USER_MD_ATTRIBUTE_EXTRACTION_PROMPT,
145
- lane: "libp2p-mesh-user-md-attributes",
146
- lightContext: true,
147
- deliver: false,
196
+ const result = await llm.complete({
197
+ messages: [
198
+ { role: "system", content: USER_MD_ATTRIBUTE_EXTRACTION_PROMPT },
199
+ { role: "user", content: buildUserMdExtractionMessage(markdown, sourcePath) },
200
+ ],
201
+ purpose: "libp2p-mesh.user-md-attributes",
202
+ maxTokens: 512,
203
+ temperature: 0.1,
204
+ timeoutMs,
148
205
  });
149
-
150
- const waitResult = await subagent.waitForRun({ runId, timeoutMs });
151
- if (waitResult.status !== "ok") {
152
- return unavailable(waitResult.error ?? `OpenClaw subagent extraction ${waitResult.status}`);
153
- }
154
-
155
- const { messages } = await subagent.getSessionMessages({ sessionKey, limit: 10 });
156
- const text = extractLatestAssistantText(messages);
206
+ const text = extractCompletionText(result);
157
207
  if (!text) {
158
208
  return [];
159
209
  }
@@ -163,8 +213,6 @@ export function createOpenClawUserMdAttributeExtractor(
163
213
  const reason = error instanceof Error ? error.message : String(error);
164
214
  logger?.warn?.(`[libp2p-mesh] USER.md agent extraction failed: ${reason}`);
165
215
  return unavailable(reason);
166
- } finally {
167
- await subagent.deleteSession?.({ sessionKey, deleteTranscript: true }).catch(() => undefined);
168
216
  }
169
217
  },
170
218
  };