@agentvault/agentvault 0.15.0 → 0.15.1

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.
Files changed (65) hide show
  1. package/dist/_cp.d.ts +12 -0
  2. package/dist/_cp.d.ts.map +1 -0
  3. package/dist/cli.js +72035 -253
  4. package/dist/cli.js.map +7 -1
  5. package/dist/index.js +76351 -24
  6. package/dist/index.js.map +7 -1
  7. package/dist/openclaw-entry.js +1443 -1201
  8. package/dist/openclaw-entry.js.map +7 -1
  9. package/package.json +1 -1
  10. package/dist/__tests__/crypto-helpers.test.d.ts +0 -2
  11. package/dist/__tests__/crypto-helpers.test.d.ts.map +0 -1
  12. package/dist/__tests__/functional.test.d.ts +0 -21
  13. package/dist/__tests__/functional.test.d.ts.map +0 -1
  14. package/dist/__tests__/install-plugin.test.d.ts +0 -2
  15. package/dist/__tests__/install-plugin.test.d.ts.map +0 -1
  16. package/dist/__tests__/multi-session.test.d.ts +0 -2
  17. package/dist/__tests__/multi-session.test.d.ts.map +0 -1
  18. package/dist/__tests__/state.test.d.ts +0 -2
  19. package/dist/__tests__/state.test.d.ts.map +0 -1
  20. package/dist/__tests__/transport.test.d.ts +0 -2
  21. package/dist/__tests__/transport.test.d.ts.map +0 -1
  22. package/dist/account-config.js +0 -60
  23. package/dist/account-config.js.map +0 -1
  24. package/dist/channel.js +0 -3411
  25. package/dist/channel.js.map +0 -1
  26. package/dist/create-agent.js +0 -314
  27. package/dist/create-agent.js.map +0 -1
  28. package/dist/crypto-helpers.js +0 -4
  29. package/dist/crypto-helpers.js.map +0 -1
  30. package/dist/doctor.js +0 -415
  31. package/dist/doctor.js.map +0 -1
  32. package/dist/fetch-interceptor.js +0 -213
  33. package/dist/fetch-interceptor.js.map +0 -1
  34. package/dist/gateway-send.js +0 -114
  35. package/dist/gateway-send.js.map +0 -1
  36. package/dist/http-handlers.js +0 -131
  37. package/dist/http-handlers.js.map +0 -1
  38. package/dist/mcp-handlers.js +0 -48
  39. package/dist/mcp-handlers.js.map +0 -1
  40. package/dist/mcp-server.js +0 -192
  41. package/dist/mcp-server.js.map +0 -1
  42. package/dist/openclaw-compat.js +0 -94
  43. package/dist/openclaw-compat.js.map +0 -1
  44. package/dist/openclaw-plugin.js +0 -297
  45. package/dist/openclaw-plugin.js.map +0 -1
  46. package/dist/openclaw-types.js +0 -13
  47. package/dist/openclaw-types.js.map +0 -1
  48. package/dist/setup.js +0 -460
  49. package/dist/setup.js.map +0 -1
  50. package/dist/skill-invoker.js +0 -100
  51. package/dist/skill-invoker.js.map +0 -1
  52. package/dist/skill-manifest.js +0 -249
  53. package/dist/skill-manifest.js.map +0 -1
  54. package/dist/skill-telemetry.js +0 -146
  55. package/dist/skill-telemetry.js.map +0 -1
  56. package/dist/skills-publish.js +0 -133
  57. package/dist/skills-publish.js.map +0 -1
  58. package/dist/state.js +0 -178
  59. package/dist/state.js.map +0 -1
  60. package/dist/transport.js +0 -43
  61. package/dist/transport.js.map +0 -1
  62. package/dist/types.js +0 -2
  63. package/dist/types.js.map +0 -1
  64. package/dist/workspace-handlers.js +0 -177
  65. package/dist/workspace-handlers.js.map +0 -1
@@ -1,1262 +1,1504 @@
1
- /**
2
- * OpenClaw channel plugin entry point.
3
- *
4
- * Intentionally thin no heavy imports (libsodium etc.) at module load time.
5
- * SecureChannel is dynamically imported inside gateway.startAccount (already async)
6
- * so libsodium's top-level await never runs during plugin registration.
7
- *
8
- * Loaded by OpenClaw via the `openclaw.extensions` field in package.json.
9
- */
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/openclaw-compat.ts
12
+ var openclaw_compat_exports = {};
13
+ __export(openclaw_compat_exports, {
14
+ onAgentEvent: () => onAgentEvent,
15
+ onSessionTranscriptUpdate: () => onSessionTranscriptUpdate,
16
+ requestHeartbeatNow: () => requestHeartbeatNow
17
+ });
18
+ async function requestHeartbeatNow(opts) {
19
+ if (_heartbeatFn === null) {
20
+ try {
21
+ const mod = await import("openclaw/dist/plugin-sdk/infra/heartbeat-wake.js");
22
+ _heartbeatFn = mod.requestHeartbeatNow ?? mod.default?.requestHeartbeatNow ?? false;
23
+ } catch {
24
+ _heartbeatFn = false;
25
+ }
26
+ }
27
+ if (typeof _heartbeatFn === "function") {
28
+ try {
29
+ await _heartbeatFn(opts);
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+ return false;
36
+ }
37
+ async function onAgentEvent(callback) {
38
+ if (_agentEventFn === null) {
39
+ try {
40
+ const mod = await import("openclaw/dist/plugin-sdk/infra/agent-events.js");
41
+ _agentEventFn = mod.onAgentEvent ?? mod.default?.onAgentEvent ?? false;
42
+ } catch {
43
+ _agentEventFn = false;
44
+ }
45
+ }
46
+ if (typeof _agentEventFn === "function") {
47
+ try {
48
+ return _agentEventFn(callback);
49
+ } catch {
50
+ return () => {
51
+ };
52
+ }
53
+ }
54
+ return () => {
55
+ };
56
+ }
57
+ async function onSessionTranscriptUpdate(callback) {
58
+ if (_transcriptFn === null) {
59
+ try {
60
+ const mod = await import("openclaw/dist/plugin-sdk/sessions/transcript-events.js");
61
+ _transcriptFn = mod.onSessionTranscriptUpdate ?? mod.default?.onSessionTranscriptUpdate ?? false;
62
+ } catch {
63
+ _transcriptFn = false;
64
+ }
65
+ }
66
+ if (typeof _transcriptFn === "function") {
67
+ try {
68
+ return _transcriptFn(callback);
69
+ } catch {
70
+ return () => {
71
+ };
72
+ }
73
+ }
74
+ return () => {
75
+ };
76
+ }
77
+ var _heartbeatFn, _agentEventFn, _transcriptFn;
78
+ var init_openclaw_compat = __esm({
79
+ "src/openclaw-compat.ts"() {
80
+ "use strict";
81
+ _heartbeatFn = null;
82
+ _agentEventFn = null;
83
+ _transcriptFn = null;
84
+ }
85
+ });
86
+
87
+ // src/openclaw-entry.ts
10
88
  import { resolve } from "node:path";
11
89
  import { randomBytes } from "node:crypto";
12
- import { listAccountIds, resolveAccount } from "./account-config.js";
13
- import { installFetchInterceptor, runWithTraceContext } from "./fetch-interceptor.js";
14
- import { handleSendRequest, handleActionRequest, handleDecisionRequest, handleStatusRequest } from "./http-handlers.js";
15
- import { requestHeartbeatNow } from "./openclaw-compat.js";
16
- // --- Runtime and active channels (set during register) ---
17
- let _ocRuntime = null;
18
- const _channels = new Map();
19
- const _messageQueue = [];
20
- // --- A2A conversation loop prevention ---
21
- // Tracks recent A2A reply timestamps per channel to prevent infinite loops.
22
- // Max replies per channel within the window, then cooldown.
23
- const A2A_MAX_REPLIES_PER_WINDOW = 4;
24
- const A2A_WINDOW_MS = 60_000; // 1-minute sliding window
25
- const A2A_COOLDOWN_MS = 120_000; // 2-minute cooldown after hitting limit
26
- const _a2aReplyTimestamps = new Map();
27
- const _a2aCooldownUntil = new Map();
28
- function _a2aCanReply(channelId) {
29
- const now = Date.now();
30
- // Check cooldown
31
- const cooldownEnd = _a2aCooldownUntil.get(channelId) ?? 0;
32
- if (now < cooldownEnd) {
33
- return false;
90
+
91
+ // src/account-config.ts
92
+ var DEFAULT_API_URL = "https://api.agentvault.chat";
93
+ var DEFAULT_HTTP_PORT = 18790;
94
+ function listAccountIds(cfg) {
95
+ const av = cfg?.channels?.agentvault;
96
+ if (!av) return [];
97
+ if (av.accounts && typeof av.accounts === "object") {
98
+ return Object.keys(av.accounts);
99
+ }
100
+ return av.dataDir ? ["default"] : [];
101
+ }
102
+ function resolveAccount(cfg, accountId) {
103
+ const av = cfg?.channels?.agentvault ?? {};
104
+ const id = accountId ?? "default";
105
+ if (av.accounts && typeof av.accounts === "object") {
106
+ const acct = av.accounts[id];
107
+ if (!acct) {
108
+ return {
109
+ accountId: id,
110
+ dataDir: "",
111
+ apiUrl: av.apiUrl ?? DEFAULT_API_URL,
112
+ agentName: id,
113
+ httpPort: DEFAULT_HTTP_PORT,
114
+ configured: false
115
+ };
116
+ }
117
+ let httpPort = acct.httpPort;
118
+ if (httpPort == null) {
119
+ const keys = Object.keys(av.accounts);
120
+ const index = keys.indexOf(id);
121
+ httpPort = DEFAULT_HTTP_PORT + (index >= 0 ? index : 0);
122
+ }
123
+ return {
124
+ accountId: id,
125
+ dataDir: acct.dataDir ?? "",
126
+ apiUrl: acct.apiUrl ?? av.apiUrl ?? DEFAULT_API_URL,
127
+ agentName: acct.agentName ?? id,
128
+ httpPort,
129
+ configured: Boolean(acct.dataDir)
130
+ };
131
+ }
132
+ return {
133
+ accountId: id,
134
+ dataDir: av.dataDir ?? "~/.openclaw/agentvault",
135
+ apiUrl: av.apiUrl ?? DEFAULT_API_URL,
136
+ agentName: av.agentName ?? "OpenClaw Agent",
137
+ httpPort: av.httpPort ?? DEFAULT_HTTP_PORT,
138
+ configured: Boolean(av.dataDir)
139
+ };
140
+ }
141
+
142
+ // src/fetch-interceptor.ts
143
+ import { AsyncLocalStorage } from "node:async_hooks";
144
+ import diagnosticsChannel from "node:diagnostics_channel";
145
+ var traceStore = new AsyncLocalStorage();
146
+ var DEFAULT_SKIP_PATTERNS = [
147
+ /^https?:\/\/localhost(:\d+)?/,
148
+ /^https?:\/\/127\.0\.0\.1(:\d+)?/,
149
+ /\.agentvault\.chat/,
150
+ /\.agentvault\.dev/,
151
+ /\.clerk\./
152
+ ];
153
+ var installed = false;
154
+ var originalFetch;
155
+ var inflightRequests = /* @__PURE__ */ new WeakMap();
156
+ var unsubCreate;
157
+ var unsubHeaders;
158
+ var unsubError;
159
+ function shouldSkip(url, extraPatterns) {
160
+ const allPatterns = [...DEFAULT_SKIP_PATTERNS, ...extraPatterns];
161
+ return allPatterns.some((p) => p.test(url));
162
+ }
163
+ function extractUrl(origin, path) {
164
+ return `${origin}${path}`;
165
+ }
166
+ function installFetchInterceptor(opts) {
167
+ if (installed) return;
168
+ installed = true;
169
+ const skipPatterns = [
170
+ ...DEFAULT_SKIP_PATTERNS,
171
+ ...opts.skipPatterns ?? []
172
+ ];
173
+ try {
174
+ const createChannel = diagnosticsChannel.channel("undici:request:create");
175
+ const headersChannel = diagnosticsChannel.channel("undici:request:headers");
176
+ const errorChannel = diagnosticsChannel.channel("undici:request:error");
177
+ const onRequestCreate = (message) => {
178
+ try {
179
+ const msg = message;
180
+ const req = msg.request;
181
+ if (!req) return;
182
+ const origin = String(req.origin ?? "");
183
+ const path = String(req.path ?? "/");
184
+ const url = extractUrl(origin, path);
185
+ if (shouldSkip(url, skipPatterns)) return;
186
+ inflightRequests.set(req, {
187
+ url,
188
+ method: String(req.method ?? "GET").toUpperCase(),
189
+ startTime: Date.now()
190
+ });
191
+ } catch {
192
+ }
193
+ };
194
+ const onRequestHeaders = (message) => {
195
+ try {
196
+ const msg = message;
197
+ const tracked = inflightRequests.get(msg.request);
198
+ if (!tracked) return;
199
+ inflightRequests.delete(msg.request);
200
+ const latencyMs = Date.now() - tracked.startTime;
201
+ const ctx = traceStore.getStore();
202
+ opts.onHttpCall({
203
+ method: tracked.method,
204
+ url: tracked.url,
205
+ statusCode: msg.response?.statusCode ?? 0,
206
+ latencyMs,
207
+ traceId: ctx?.traceId,
208
+ parentSpanId: ctx?.parentSpanId
209
+ });
210
+ } catch {
211
+ }
212
+ };
213
+ const onRequestError = (message) => {
214
+ try {
215
+ const msg = message;
216
+ const tracked = inflightRequests.get(msg.request);
217
+ if (!tracked) return;
218
+ inflightRequests.delete(msg.request);
219
+ const latencyMs = Date.now() - tracked.startTime;
220
+ const ctx = traceStore.getStore();
221
+ opts.onHttpCall({
222
+ method: tracked.method,
223
+ url: tracked.url,
224
+ statusCode: 0,
225
+ latencyMs,
226
+ traceId: ctx?.traceId,
227
+ parentSpanId: ctx?.parentSpanId
228
+ });
229
+ } catch {
230
+ }
231
+ };
232
+ createChannel.subscribe(onRequestCreate);
233
+ headersChannel.subscribe(onRequestHeaders);
234
+ errorChannel.subscribe(onRequestError);
235
+ unsubCreate = () => createChannel.unsubscribe(onRequestCreate);
236
+ unsubHeaders = () => headersChannel.unsubscribe(onRequestHeaders);
237
+ unsubError = () => errorChannel.unsubscribe(onRequestError);
238
+ } catch {
239
+ }
240
+ originalFetch = globalThis.fetch;
241
+ const savedOriginal = originalFetch;
242
+ globalThis.fetch = async (input, init) => {
243
+ const url = input instanceof Request ? input.url : input instanceof URL ? input.href : String(input);
244
+ if (shouldSkip(url, skipPatterns)) {
245
+ return savedOriginal(input, init);
34
246
  }
35
- // Get timestamps within window
36
- const timestamps = _a2aReplyTimestamps.get(channelId) ?? [];
37
- const recent = timestamps.filter((t) => now - t < A2A_WINDOW_MS);
38
- if (recent.length >= A2A_MAX_REPLIES_PER_WINDOW) {
39
- // Hit limit enter cooldown
40
- _a2aCooldownUntil.set(channelId, now + A2A_COOLDOWN_MS);
41
- _a2aReplyTimestamps.set(channelId, []);
42
- console.warn(`[AgentVault] A2A rate limit hit for channel ${channelId.slice(0, 8)}... — ` +
43
- `${A2A_MAX_REPLIES_PER_WINDOW} replies in ${A2A_WINDOW_MS / 1000}s, cooling down ${A2A_COOLDOWN_MS / 1000}s`);
44
- return false;
247
+ const method = input instanceof Request ? input.method : init?.method ?? "GET";
248
+ const ctx = traceStore.getStore();
249
+ const start = Date.now();
250
+ try {
251
+ const response = await savedOriginal(input, init);
252
+ const latencyMs = Date.now() - start;
253
+ try {
254
+ opts.onHttpCall({
255
+ method,
256
+ url,
257
+ statusCode: response.status,
258
+ latencyMs,
259
+ traceId: ctx?.traceId,
260
+ parentSpanId: ctx?.parentSpanId
261
+ });
262
+ } catch {
263
+ }
264
+ return response;
265
+ } catch (err) {
266
+ const latencyMs = Date.now() - start;
267
+ try {
268
+ opts.onHttpCall({
269
+ method,
270
+ url,
271
+ statusCode: 0,
272
+ latencyMs,
273
+ traceId: ctx?.traceId,
274
+ parentSpanId: ctx?.parentSpanId
275
+ });
276
+ } catch {
277
+ }
278
+ throw err;
45
279
  }
46
- return true;
280
+ };
281
+ }
282
+ function runWithTraceContext(ctx, fn) {
283
+ return traceStore.run(ctx, fn);
284
+ }
285
+
286
+ // src/http-handlers.ts
287
+ async function handleSendRequest(parsed, channel) {
288
+ const text = parsed.text;
289
+ if (!text || typeof text !== "string") {
290
+ return { status: 400, body: { ok: false, error: "Missing 'text' field" } };
291
+ }
292
+ try {
293
+ let a2aTarget = parsed.hub_address ?? parsed.a2a_address ?? parsed.a2aAddress;
294
+ if (!a2aTarget && parsed.channel_id && typeof parsed.channel_id === "string") {
295
+ const hubAddr = channel.resolveA2AChannelHub(parsed.channel_id);
296
+ if (hubAddr) a2aTarget = hubAddr;
297
+ }
298
+ const roomId = (typeof parsed.room_id === "string" ? parsed.room_id : void 0) ?? channel.lastInboundRoomId;
299
+ if (a2aTarget && typeof a2aTarget === "string") {
300
+ await channel.sendToAgent(a2aTarget, text);
301
+ } else if (roomId) {
302
+ await channel.sendToRoom(roomId, text, {
303
+ messageType: parsed.message_type,
304
+ priority: parsed.priority,
305
+ metadata: parsed.metadata
306
+ });
307
+ } else if (parsed.file_path && typeof parsed.file_path === "string") {
308
+ await channel.sendWithAttachment(text, parsed.file_path, {
309
+ topicId: parsed.topicId
310
+ });
311
+ } else {
312
+ await channel.send(text, {
313
+ topicId: parsed.topicId,
314
+ messageType: parsed.message_type,
315
+ metadata: parsed.metadata
316
+ });
317
+ }
318
+ return { status: 200, body: { ok: true } };
319
+ } catch (err) {
320
+ return { status: 500, body: { ok: false, error: String(err) } };
321
+ }
322
+ }
323
+ async function handleActionRequest(parsed, channel) {
324
+ if (!parsed.action || typeof parsed.action !== "string") {
325
+ return { status: 400, body: { ok: false, error: "Missing 'action' field" } };
326
+ }
327
+ try {
328
+ const confirmation = {
329
+ action: parsed.action,
330
+ status: parsed.status ?? "completed",
331
+ decisionId: parsed.decision_id,
332
+ detail: parsed.detail,
333
+ estimated_cost: parsed.estimated_cost
334
+ };
335
+ if (parsed.room_id && typeof parsed.room_id === "string") {
336
+ await channel.sendActionConfirmationToRoom(parsed.room_id, confirmation);
337
+ } else {
338
+ await channel.sendActionConfirmation(confirmation);
339
+ }
340
+ return { status: 200, body: { ok: true } };
341
+ } catch (err) {
342
+ return { status: 500, body: { ok: false, error: String(err) } };
343
+ }
344
+ }
345
+ async function handleDecisionRequest(parsed, channel) {
346
+ const title = parsed.title;
347
+ if (!title || typeof title !== "string") {
348
+ return { status: 400, body: { ok: false, error: "Missing 'title' field" } };
349
+ }
350
+ const options = parsed.options;
351
+ if (!Array.isArray(options) || options.length < 2) {
352
+ return { status: 400, body: { ok: false, error: "'options' must be an array with at least 2 items" } };
353
+ }
354
+ for (const opt of options) {
355
+ if (!opt || typeof opt !== "object" || !opt.option_id || !opt.label) {
356
+ return { status: 400, body: { ok: false, error: "Each option must have 'option_id' and 'label'" } };
357
+ }
358
+ }
359
+ try {
360
+ const decision_id = await channel.sendDecisionRequest({
361
+ title,
362
+ description: parsed.description,
363
+ options,
364
+ context_refs: parsed.context_refs,
365
+ deadline: parsed.deadline,
366
+ auto_action: parsed.auto_action
367
+ });
368
+ return { status: 200, body: { ok: true, decision_id } };
369
+ } catch (err) {
370
+ return { status: 500, body: { ok: false, error: String(err) } };
371
+ }
372
+ }
373
+ function handleStatusRequest(channel) {
374
+ return {
375
+ status: 200,
376
+ body: {
377
+ ok: true,
378
+ state: channel.state,
379
+ deviceId: channel.deviceId ?? void 0,
380
+ sessions: channel.sessionCount
381
+ }
382
+ };
383
+ }
384
+
385
+ // src/openclaw-entry.ts
386
+ init_openclaw_compat();
387
+ var _ocRuntime = null;
388
+ var _channels = /* @__PURE__ */ new Map();
389
+ var _messageQueue = [];
390
+ var A2A_MAX_REPLIES_PER_WINDOW = 4;
391
+ var A2A_WINDOW_MS = 6e4;
392
+ var A2A_COOLDOWN_MS = 12e4;
393
+ var _a2aReplyTimestamps = /* @__PURE__ */ new Map();
394
+ var _a2aCooldownUntil = /* @__PURE__ */ new Map();
395
+ function _a2aCanReply(channelId) {
396
+ const now = Date.now();
397
+ const cooldownEnd = _a2aCooldownUntil.get(channelId) ?? 0;
398
+ if (now < cooldownEnd) {
399
+ return false;
400
+ }
401
+ const timestamps = _a2aReplyTimestamps.get(channelId) ?? [];
402
+ const recent = timestamps.filter((t) => now - t < A2A_WINDOW_MS);
403
+ if (recent.length >= A2A_MAX_REPLIES_PER_WINDOW) {
404
+ _a2aCooldownUntil.set(channelId, now + A2A_COOLDOWN_MS);
405
+ _a2aReplyTimestamps.set(channelId, []);
406
+ console.warn(
407
+ `[AgentVault] A2A rate limit hit for channel ${channelId.slice(0, 8)}... \u2014 ${A2A_MAX_REPLIES_PER_WINDOW} replies in ${A2A_WINDOW_MS / 1e3}s, cooling down ${A2A_COOLDOWN_MS / 1e3}s`
408
+ );
409
+ return false;
410
+ }
411
+ return true;
47
412
  }
48
413
  function _a2aRecordReply(channelId) {
49
- const now = Date.now();
50
- const timestamps = _a2aReplyTimestamps.get(channelId) ?? [];
51
- timestamps.push(now);
52
- // Keep only timestamps within window
53
- _a2aReplyTimestamps.set(channelId, timestamps.filter((t) => now - t < A2A_WINDOW_MS));
414
+ const now = Date.now();
415
+ const timestamps = _a2aReplyTimestamps.get(channelId) ?? [];
416
+ timestamps.push(now);
417
+ _a2aReplyTimestamps.set(
418
+ channelId,
419
+ timestamps.filter((t) => now - t < A2A_WINDOW_MS)
420
+ );
54
421
  }
55
- /** Whether OpenClaw managed HTTP routes are active (vs self-managed server). */
56
- export let isUsingManagedRoutes = false;
57
- /**
58
- * Shared mutable targets array for OpenClaw outbound routing.
59
- * OpenClaw validates `to` against this list before calling sendText/sendPayload.
60
- * We mutate it at runtime to add A2A channel targets dynamically.
61
- */
62
- const _outboundTargets = [
63
- { id: "owner", label: "AgentVault Owner", accountId: "default" },
64
- { id: "default", label: "AgentVault Owner (default)", accountId: "default" },
422
+ var isUsingManagedRoutes = false;
423
+ var _outboundTargets = [
424
+ { id: "owner", label: "AgentVault Owner", accountId: "default" },
425
+ { id: "default", label: "AgentVault Owner (default)", accountId: "default" }
65
426
  ];
66
- /** Register a room as a valid outbound target (idempotent). */
67
427
  function _registerRoomTarget(roomId, roomName, accountId) {
68
- const targetId = `room:${roomId}`;
69
- if (_outboundTargets.some((t) => t.id === targetId))
70
- return;
71
- _outboundTargets.push({ id: targetId, label: `Room: ${roomName}`, accountId });
72
- console.log(`[AgentVault] Registered room target: ${roomName} (${roomId.slice(0, 8)}...)`);
428
+ const targetId = `room:${roomId}`;
429
+ if (_outboundTargets.some((t) => t.id === targetId)) return;
430
+ _outboundTargets.push({ id: targetId, label: `Room: ${roomName}`, accountId });
431
+ console.log(`[AgentVault] Registered room target: ${roomName} (${roomId.slice(0, 8)}...)`);
73
432
  }
74
- /** Register an A2A peer as a valid outbound target (idempotent). */
75
433
  function _registerA2ATarget(hubAddress, accountId) {
76
- const targetId = `a2a:${hubAddress}`;
77
- if (_outboundTargets.some((t) => t.id === targetId))
78
- return;
79
- _outboundTargets.push({
80
- id: targetId,
81
- label: `A2A: ${hubAddress}`,
82
- accountId,
83
- });
84
- console.log(`[AgentVault] Registered A2A target: ${targetId}`);
434
+ const targetId = `a2a:${hubAddress}`;
435
+ if (_outboundTargets.some((t) => t.id === targetId)) return;
436
+ _outboundTargets.push({
437
+ id: targetId,
438
+ label: `A2A: ${hubAddress}`,
439
+ accountId
440
+ });
441
+ console.log(`[AgentVault] Registered A2A target: ${targetId}`);
85
442
  }
86
443
  function _setRuntime(rt) {
87
- _ocRuntime = rt;
88
- // Flush any messages that arrived before runtime was ready
89
- if (_messageQueue.length > 0) {
90
- const pending = _messageQueue.splice(0);
91
- for (const fn of pending) {
92
- fn().catch(() => { });
93
- }
444
+ _ocRuntime = rt;
445
+ if (_messageQueue.length > 0) {
446
+ const pending = _messageQueue.splice(0);
447
+ for (const fn of pending) {
448
+ fn().catch(() => {
449
+ });
94
450
  }
451
+ }
95
452
  }
96
- // --- @mention filtering for multi-agent rooms ---
97
- /** Extract @mention names from plaintext. Returns lowercased names. */
98
453
  function _parseMentions(text) {
99
- const mentions = [];
100
- // Match @word at word boundary — supports multi-word via sequential matching
101
- const re = /@(\w[\w]*)/gi;
102
- let match;
103
- while ((match = re.exec(text)) !== null) {
104
- mentions.push(match[1].toLowerCase());
105
- }
106
- return mentions;
454
+ const mentions = [];
455
+ const re = /@(\w[\w]*)/gi;
456
+ let match;
457
+ while ((match = re.exec(text)) !== null) {
458
+ mentions.push(match[1].toLowerCase());
459
+ }
460
+ return mentions;
107
461
  }
108
- /** Determine whether this agent should process a room message based on @mentions. */
109
462
  function _shouldProcessRoomMessage(plaintext, agentName, accountId) {
110
- const mentions = _parseMentions(plaintext);
111
- // No mentions broadcast to all agents (current behavior)
112
- if (mentions.length === 0)
113
- return true;
114
- // @all / @everyone → all agents process
115
- if (mentions.includes("all") || mentions.includes("everyone"))
116
- return true;
117
- // Check if this agent is mentioned (by name or accountId)
118
- const nameLower = agentName.toLowerCase();
119
- const idLower = accountId.toLowerCase();
120
- // Match first word of agent name (e.g., "Cortina" from "Cortina (Coder)")
121
- const firstWord = nameLower.split(/[\s(]/)[0];
122
- return mentions.some((m) => m === nameLower || m === firstWord || m === idLower);
463
+ const mentions = _parseMentions(plaintext);
464
+ if (mentions.length === 0) return true;
465
+ if (mentions.includes("all") || mentions.includes("everyone")) return true;
466
+ const nameLower = agentName.toLowerCase();
467
+ const idLower = accountId.toLowerCase();
468
+ const firstWord = nameLower.split(/[\s(]/)[0];
469
+ return mentions.some(
470
+ (m) => m === nameLower || m === firstWord || m === idLower
471
+ );
123
472
  }
124
- /** Strip the matching @mention prefix from plaintext so the agent sees clean text. */
125
473
  function _stripMentions(text, agentName, accountId) {
126
- const nameLower = agentName.toLowerCase();
127
- const firstWord = nameLower.split(/[\s(]/)[0];
128
- const idLower = accountId.toLowerCase();
129
- // Remove all @mentions that match this agent (case-insensitive)
130
- return text
131
- .replace(/@(\w[\w]*)/gi, (full, name) => {
132
- const lower = name.toLowerCase();
133
- if (lower === nameLower || lower === firstWord || lower === idLower) {
134
- return "";
135
- }
136
- // Also strip @all/@everyone since they're routing directives
137
- if (lower === "all" || lower === "everyone")
138
- return "";
139
- return full;
140
- })
141
- .replace(/^\s+/, "") // trim leading whitespace left by stripped mention
142
- .trim();
474
+ const nameLower = agentName.toLowerCase();
475
+ const firstWord = nameLower.split(/[\s(]/)[0];
476
+ const idLower = accountId.toLowerCase();
477
+ return text.replace(/@(\w[\w]*)/gi, (full, name) => {
478
+ const lower = name.toLowerCase();
479
+ if (lower === nameLower || lower === firstWord || lower === idLower) {
480
+ return "";
481
+ }
482
+ if (lower === "all" || lower === "everyone") return "";
483
+ return full;
484
+ }).replace(/^\s+/, "").trim();
143
485
  }
144
- // --- Inbound message dispatch ---
145
486
  async function handleInbound(params) {
146
- const { plaintext: rawPlaintext, metadata, channel, account, cfg } = params;
147
- const isRoomMessage = Boolean(metadata.roomId);
148
- const isA2AMessage = Boolean(metadata.a2aChannelId);
149
- // @mention filtering: only for room messages
150
- if (isRoomMessage) {
151
- if (!_shouldProcessRoomMessage(rawPlaintext, account.agentName ?? "", account.accountId ?? "")) {
152
- return; // This agent is not mentioned — skip silently
153
- }
487
+ const { plaintext: rawPlaintext, metadata, channel, account, cfg } = params;
488
+ const isRoomMessage = Boolean(metadata.roomId);
489
+ const isA2AMessage = Boolean(metadata.a2aChannelId);
490
+ if (isRoomMessage) {
491
+ if (!_shouldProcessRoomMessage(rawPlaintext, account.agentName ?? "", account.accountId ?? "")) {
492
+ return;
154
493
  }
155
- // Strip @mentions from plaintext so the agent sees clean text
156
- const plaintext = isRoomMessage
157
- ? _stripMentions(rawPlaintext, account.agentName ?? "", account.accountId ?? "")
158
- : rawPlaintext;
159
- // Telemetry: hierarchical spans for the full message lifecycle
160
- const startTime = Date.now();
161
- const traceId = randomBytes(16).toString("hex");
162
- const rootSpanId = randomBytes(8).toString("hex");
163
- const inferenceSpanId = randomBytes(8).toString("hex");
164
- // Instrumentation context for agent code to report LLM/tool/error spans
165
- /** Helper: send an activity span over WS for real-time owner display. */
166
- const _sendActivity = (spanData) => {
167
- try {
168
- channel.sendActivitySpan({ ...spanData, trace_id: traceId, parent_span_id: inferenceSpanId });
169
- }
170
- catch { }
171
- };
172
- const _instrument = {
173
- reportLlm: (opts) => {
174
- const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
175
- if (opts.skillName || _instrument.skillName)
176
- enriched.skillName = opts.skillName ?? _instrument.skillName;
177
- try {
178
- channel.telemetry?.reportLlmCall(enriched);
179
- }
180
- catch { }
181
- const activityAttrs = { "ai.agent.llm.model": opts.model ?? "" };
182
- if (enriched.skillName)
183
- activityAttrs["ai.agent.skill.name"] = enriched.skillName;
184
- _sendActivity({
185
- span_id: randomBytes(8).toString("hex"), span_type: "llm", span_name: opts.model ?? "LLM",
186
- status: opts.status === "error" ? "error" : "ok",
187
- start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
188
- end_time: new Date().toISOString(), duration_ms: opts.latencyMs ?? 0,
189
- attributes: activityAttrs,
190
- tokens_input: opts.tokensInput, tokens_output: opts.tokensOutput,
191
- });
192
- },
193
- reportTool: (opts) => {
194
- const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
195
- if (opts.skillName || _instrument.skillName)
196
- enriched.skillName = opts.skillName ?? _instrument.skillName;
197
- try {
198
- channel.telemetry?.reportToolCall(enriched);
199
- }
200
- catch { }
201
- const activityAttrs = { "ai.agent.tool.name": opts.toolName ?? "" };
202
- if (enriched.skillName)
203
- activityAttrs["ai.agent.skill.name"] = enriched.skillName;
204
- _sendActivity({
205
- span_id: randomBytes(8).toString("hex"), span_type: "tool", span_name: opts.toolName ?? "tool",
206
- status: opts.success === false ? "error" : "ok",
207
- start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
208
- end_time: new Date().toISOString(), duration_ms: opts.latencyMs ?? 0,
209
- attributes: activityAttrs,
210
- tool_success: opts.success,
211
- });
212
- },
213
- reportError: (opts) => {
214
- const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
215
- if (opts.skillName || _instrument.skillName)
216
- enriched.skillName = opts.skillName ?? _instrument.skillName;
217
- try {
218
- channel.telemetry?.reportError(enriched);
219
- }
220
- catch { }
221
- const activityAttrs = { "ai.agent.error.type": opts.errorType ?? "" };
222
- if (enriched.skillName)
223
- activityAttrs["ai.agent.skill.name"] = enriched.skillName;
224
- _sendActivity({
225
- span_id: randomBytes(8).toString("hex"), span_type: "error", span_name: opts.errorType ?? "error",
226
- status: "error",
227
- start_time: new Date().toISOString(), end_time: new Date().toISOString(), duration_ms: 0,
228
- attributes: activityAttrs,
229
- error_type: opts.errorType, error_message: opts.errorMessage,
230
- });
231
- },
232
- reportHttp: (opts) => {
233
- const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
234
- if (opts.skillName || _instrument.skillName)
235
- enriched.skillName = opts.skillName ?? _instrument.skillName;
236
- try {
237
- channel.telemetry?.reportHttpCall(enriched);
238
- }
239
- catch { }
240
- const activityAttrs = { "ai.agent.http.method": opts.method ?? "", "ai.agent.http.url": opts.url ?? "" };
241
- if (enriched.skillName)
242
- activityAttrs["ai.agent.skill.name"] = enriched.skillName;
243
- _sendActivity({
244
- span_id: randomBytes(8).toString("hex"), span_type: "http",
245
- span_name: (() => { try {
246
- return new URL(opts.url).hostname;
247
- }
248
- catch {
249
- return opts.url?.slice(0, 40) ?? "http";
250
- } })(),
251
- status: (opts.statusCode ?? 200) >= 400 ? "error" : "ok",
252
- start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
253
- end_time: new Date().toISOString(), duration_ms: opts.latencyMs ?? 0,
254
- attributes: activityAttrs,
255
- http_method: opts.method, http_status_code: opts.statusCode, http_url: opts.url,
256
- });
257
- },
258
- reportAction: (opts) => {
259
- const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
260
- if (opts.skillName || _instrument.skillName)
261
- enriched.skillName = opts.skillName ?? _instrument.skillName;
262
- try {
263
- channel.telemetry?.reportActionCall(enriched);
264
- }
265
- catch { }
266
- const activityAttrs = { "ai.agent.action.type": opts.actionType ?? "", "ai.agent.action.target": opts.target ?? "" };
267
- if (enriched.skillName)
268
- activityAttrs["ai.agent.skill.name"] = enriched.skillName;
269
- _sendActivity({
270
- span_id: randomBytes(8).toString("hex"), span_type: "action",
271
- span_name: `${opts.actionType ?? "action"}: ${opts.target ?? ""}`,
272
- status: opts.success === false ? "error" : "ok",
273
- start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
274
- end_time: new Date().toISOString(), duration_ms: opts.latencyMs ?? 0,
275
- attributes: activityAttrs,
276
- action_type: opts.actionType, action_target: opts.target,
277
- });
278
- },
279
- reportNav: (opts) => {
280
- const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
281
- if (opts.skillName || _instrument.skillName)
282
- enriched.skillName = opts.skillName ?? _instrument.skillName;
283
- try {
284
- channel.telemetry?.reportNavCall(enriched);
285
- }
286
- catch { }
287
- const activityAttrs = { "ai.agent.nav.to_url": opts.toUrl ?? "" };
288
- if (enriched.skillName)
289
- activityAttrs["ai.agent.skill.name"] = enriched.skillName;
290
- _sendActivity({
291
- span_id: randomBytes(8).toString("hex"), span_type: "nav",
292
- span_name: opts.toUrl?.slice(0, 40) ?? "navigate",
293
- status: opts.status === "error" ? "error" : "ok",
294
- start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
295
- end_time: new Date().toISOString(), duration_ms: opts.latencyMs ?? 0,
296
- attributes: activityAttrs,
297
- });
298
- },
299
- skillName: undefined,
300
- traceId,
301
- parentSpanId: inferenceSpanId,
302
- };
303
- let replyCount = 0;
304
- let totalReplyChars = 0;
305
- let firstReplyTime = null;
306
- const replySpans = [];
307
- // Emit "receive" child span immediately — marks inbound arrival
308
- _emitChildSpan(channel, {
309
- traceId,
310
- parentSpanId: rootSpanId,
311
- name: "ai.agent.message.receive",
312
- startTime,
313
- endTime: startTime + 1, // instant
314
- attributes: {
315
- "ai.agent.message.input_chars": plaintext.length,
316
- "ai.agent.message.type": isA2AMessage ? "a2a" : metadata.roomId ? "room" : "direct",
317
- },
318
- status: "ok",
319
- });
320
- // Send typing indicator to owner (non-critical, best-effort)
494
+ }
495
+ const plaintext = isRoomMessage ? _stripMentions(rawPlaintext, account.agentName ?? "", account.accountId ?? "") : rawPlaintext;
496
+ const startTime = Date.now();
497
+ const traceId = randomBytes(16).toString("hex");
498
+ const rootSpanId = randomBytes(8).toString("hex");
499
+ const inferenceSpanId = randomBytes(8).toString("hex");
500
+ const _sendActivity = (spanData) => {
321
501
  try {
322
- channel.sendTyping();
502
+ channel.sendActivitySpan({ ...spanData, trace_id: traceId, parent_span_id: inferenceSpanId });
503
+ } catch {
323
504
  }
324
- catch { /* ignore */ }
325
- const core = _ocRuntime; // Non-null: caller guards with `if (!_ocRuntime)` check
326
- const route = core.channel.routing.resolveAgentRoute({
505
+ };
506
+ const _instrument = {
507
+ reportLlm: (opts) => {
508
+ const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
509
+ if (opts.skillName || _instrument.skillName) enriched.skillName = opts.skillName ?? _instrument.skillName;
510
+ try {
511
+ channel.telemetry?.reportLlmCall(enriched);
512
+ } catch {
513
+ }
514
+ const activityAttrs = { "ai.agent.llm.model": opts.model ?? "" };
515
+ if (enriched.skillName) activityAttrs["ai.agent.skill.name"] = enriched.skillName;
516
+ _sendActivity({
517
+ span_id: randomBytes(8).toString("hex"),
518
+ span_type: "llm",
519
+ span_name: opts.model ?? "LLM",
520
+ status: opts.status === "error" ? "error" : "ok",
521
+ start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
522
+ end_time: (/* @__PURE__ */ new Date()).toISOString(),
523
+ duration_ms: opts.latencyMs ?? 0,
524
+ attributes: activityAttrs,
525
+ tokens_input: opts.tokensInput,
526
+ tokens_output: opts.tokensOutput
527
+ });
528
+ },
529
+ reportTool: (opts) => {
530
+ const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
531
+ if (opts.skillName || _instrument.skillName) enriched.skillName = opts.skillName ?? _instrument.skillName;
532
+ try {
533
+ channel.telemetry?.reportToolCall(enriched);
534
+ } catch {
535
+ }
536
+ const activityAttrs = { "ai.agent.tool.name": opts.toolName ?? "" };
537
+ if (enriched.skillName) activityAttrs["ai.agent.skill.name"] = enriched.skillName;
538
+ _sendActivity({
539
+ span_id: randomBytes(8).toString("hex"),
540
+ span_type: "tool",
541
+ span_name: opts.toolName ?? "tool",
542
+ status: opts.success === false ? "error" : "ok",
543
+ start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
544
+ end_time: (/* @__PURE__ */ new Date()).toISOString(),
545
+ duration_ms: opts.latencyMs ?? 0,
546
+ attributes: activityAttrs,
547
+ tool_success: opts.success
548
+ });
549
+ },
550
+ reportError: (opts) => {
551
+ const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
552
+ if (opts.skillName || _instrument.skillName) enriched.skillName = opts.skillName ?? _instrument.skillName;
553
+ try {
554
+ channel.telemetry?.reportError(enriched);
555
+ } catch {
556
+ }
557
+ const activityAttrs = { "ai.agent.error.type": opts.errorType ?? "" };
558
+ if (enriched.skillName) activityAttrs["ai.agent.skill.name"] = enriched.skillName;
559
+ _sendActivity({
560
+ span_id: randomBytes(8).toString("hex"),
561
+ span_type: "error",
562
+ span_name: opts.errorType ?? "error",
563
+ status: "error",
564
+ start_time: (/* @__PURE__ */ new Date()).toISOString(),
565
+ end_time: (/* @__PURE__ */ new Date()).toISOString(),
566
+ duration_ms: 0,
567
+ attributes: activityAttrs,
568
+ error_type: opts.errorType,
569
+ error_message: opts.errorMessage
570
+ });
571
+ },
572
+ reportHttp: (opts) => {
573
+ const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
574
+ if (opts.skillName || _instrument.skillName) enriched.skillName = opts.skillName ?? _instrument.skillName;
575
+ try {
576
+ channel.telemetry?.reportHttpCall(enriched);
577
+ } catch {
578
+ }
579
+ const activityAttrs = { "ai.agent.http.method": opts.method ?? "", "ai.agent.http.url": opts.url ?? "" };
580
+ if (enriched.skillName) activityAttrs["ai.agent.skill.name"] = enriched.skillName;
581
+ _sendActivity({
582
+ span_id: randomBytes(8).toString("hex"),
583
+ span_type: "http",
584
+ span_name: (() => {
585
+ try {
586
+ return new URL(opts.url).hostname;
587
+ } catch {
588
+ return opts.url?.slice(0, 40) ?? "http";
589
+ }
590
+ })(),
591
+ status: (opts.statusCode ?? 200) >= 400 ? "error" : "ok",
592
+ start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
593
+ end_time: (/* @__PURE__ */ new Date()).toISOString(),
594
+ duration_ms: opts.latencyMs ?? 0,
595
+ attributes: activityAttrs,
596
+ http_method: opts.method,
597
+ http_status_code: opts.statusCode,
598
+ http_url: opts.url
599
+ });
600
+ },
601
+ reportAction: (opts) => {
602
+ const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
603
+ if (opts.skillName || _instrument.skillName) enriched.skillName = opts.skillName ?? _instrument.skillName;
604
+ try {
605
+ channel.telemetry?.reportActionCall(enriched);
606
+ } catch {
607
+ }
608
+ const activityAttrs = { "ai.agent.action.type": opts.actionType ?? "", "ai.agent.action.target": opts.target ?? "" };
609
+ if (enriched.skillName) activityAttrs["ai.agent.skill.name"] = enriched.skillName;
610
+ _sendActivity({
611
+ span_id: randomBytes(8).toString("hex"),
612
+ span_type: "action",
613
+ span_name: `${opts.actionType ?? "action"}: ${opts.target ?? ""}`,
614
+ status: opts.success === false ? "error" : "ok",
615
+ start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
616
+ end_time: (/* @__PURE__ */ new Date()).toISOString(),
617
+ duration_ms: opts.latencyMs ?? 0,
618
+ attributes: activityAttrs,
619
+ action_type: opts.actionType,
620
+ action_target: opts.target
621
+ });
622
+ },
623
+ reportNav: (opts) => {
624
+ const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
625
+ if (opts.skillName || _instrument.skillName) enriched.skillName = opts.skillName ?? _instrument.skillName;
626
+ try {
627
+ channel.telemetry?.reportNavCall(enriched);
628
+ } catch {
629
+ }
630
+ const activityAttrs = { "ai.agent.nav.to_url": opts.toUrl ?? "" };
631
+ if (enriched.skillName) activityAttrs["ai.agent.skill.name"] = enriched.skillName;
632
+ _sendActivity({
633
+ span_id: randomBytes(8).toString("hex"),
634
+ span_type: "nav",
635
+ span_name: opts.toUrl?.slice(0, 40) ?? "navigate",
636
+ status: opts.status === "error" ? "error" : "ok",
637
+ start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
638
+ end_time: (/* @__PURE__ */ new Date()).toISOString(),
639
+ duration_ms: opts.latencyMs ?? 0,
640
+ attributes: activityAttrs
641
+ });
642
+ },
643
+ skillName: void 0,
644
+ traceId,
645
+ parentSpanId: inferenceSpanId
646
+ };
647
+ let replyCount = 0;
648
+ let totalReplyChars = 0;
649
+ let firstReplyTime = null;
650
+ const replySpans = [];
651
+ _emitChildSpan(channel, {
652
+ traceId,
653
+ parentSpanId: rootSpanId,
654
+ name: "ai.agent.message.receive",
655
+ startTime,
656
+ endTime: startTime + 1,
657
+ // instant
658
+ attributes: {
659
+ "ai.agent.message.input_chars": plaintext.length,
660
+ "ai.agent.message.type": isA2AMessage ? "a2a" : metadata.roomId ? "room" : "direct"
661
+ },
662
+ status: "ok"
663
+ });
664
+ try {
665
+ channel.sendTyping();
666
+ } catch {
667
+ }
668
+ const core = _ocRuntime;
669
+ const route = core.channel.routing.resolveAgentRoute({
670
+ cfg,
671
+ channel: "agentvault",
672
+ accountId: account.accountId,
673
+ peer: {
674
+ kind: isA2AMessage ? "a2a" : "direct",
675
+ id: isA2AMessage ? `agentvault:a2a:${metadata.fromHubAddress}` : isRoomMessage ? `agentvault:room:${metadata.roomId}` : "agentvault:owner"
676
+ }
677
+ });
678
+ const storePath = core.channel.session.resolveStorePath(cfg?.session?.store, {
679
+ agentId: route.agentId
680
+ });
681
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
682
+ const previousTimestamp = core.channel.session.readSessionUpdatedAt({
683
+ storePath,
684
+ sessionKey: route.sessionKey
685
+ });
686
+ const body = core.channel.reply.formatAgentEnvelope({
687
+ channel: "AgentVault",
688
+ from: isA2AMessage ? `Agent (${metadata.fromHubAddress})` : isRoomMessage ? "Room" : "Owner",
689
+ timestamp: new Date(metadata.timestamp).getTime(),
690
+ previousTimestamp,
691
+ envelope: envelopeOptions,
692
+ body: plaintext
693
+ });
694
+ const attachmentFields = {};
695
+ if (metadata.attachment) {
696
+ attachmentFields.AttachmentPath = metadata.attachment.filePath;
697
+ attachmentFields.AttachmentFilename = metadata.attachment.filename;
698
+ attachmentFields.AttachmentMime = metadata.attachment.mime;
699
+ if (metadata.attachment.base64) {
700
+ attachmentFields.MediaUrl = metadata.attachment.base64;
701
+ attachmentFields.NumMedia = "1";
702
+ }
703
+ }
704
+ const ctxPayload = core.channel.reply.finalizeInboundContext({
705
+ Body: body,
706
+ RawBody: plaintext,
707
+ CommandBody: plaintext,
708
+ From: isA2AMessage ? `a2a:${metadata.fromHubAddress}` : isRoomMessage ? `agentvault:room:${metadata.roomId}` : "agentvault:owner",
709
+ To: `agentvault:agent:${account.accountId}`,
710
+ SessionKey: route.sessionKey,
711
+ AccountId: account.accountId,
712
+ ChatType: isA2AMessage ? "a2a" : isRoomMessage ? "room" : "direct",
713
+ ConversationLabel: isA2AMessage ? `A2A: ${metadata.fromHubAddress}` : isRoomMessage ? "AgentVault Room" : "AgentVault",
714
+ SenderName: isA2AMessage ? metadata.fromHubAddress : isRoomMessage ? "Room" : "Owner",
715
+ SenderId: isA2AMessage ? `a2a:${metadata.fromHubAddress}` : isRoomMessage ? `agentvault:room:${metadata.roomId}` : "agentvault:owner",
716
+ Provider: "agentvault",
717
+ Surface: "agentvault",
718
+ MessageSid: metadata.messageId,
719
+ Timestamp: new Date(metadata.timestamp).getTime(),
720
+ OriginatingChannel: "agentvault",
721
+ OriginatingTo: `agentvault:agent:${account.accountId}`,
722
+ CommandAuthorized: true,
723
+ Instrument: _instrument,
724
+ // OpenClaw ctx convention
725
+ AgentVaultTelemetry: _instrument,
726
+ // AgentVault-specific accessor
727
+ ...attachmentFields
728
+ });
729
+ await core.channel.session.recordInboundSession({
730
+ storePath,
731
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
732
+ ctx: ctxPayload,
733
+ onRecordError: (err) => {
734
+ core.error?.(`[AgentVault] session record failed: ${String(err)}`);
735
+ }
736
+ });
737
+ try {
738
+ await runWithTraceContext(
739
+ { traceId, parentSpanId: inferenceSpanId },
740
+ () => core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
741
+ ctx: ctxPayload,
327
742
  cfg,
328
- channel: "agentvault",
329
- accountId: account.accountId,
330
- peer: {
331
- kind: isA2AMessage ? "a2a" : "direct",
332
- id: isA2AMessage
333
- ? `agentvault:a2a:${metadata.fromHubAddress}`
334
- : isRoomMessage
335
- ? `agentvault:room:${metadata.roomId}`
336
- : "agentvault:owner",
743
+ dispatcherOptions: {
744
+ deliver: async (payload) => {
745
+ if (payload.kind === "thinking" || payload.kind === "reasoning") return;
746
+ const text = (payload.text ?? "").trim();
747
+ if (!text) return;
748
+ if (/^(Reasoning|Thinking|Let me think|I need to|Let me check):/i.test(text)) return;
749
+ const replyStart = Date.now();
750
+ replyCount++;
751
+ totalReplyChars += text.length;
752
+ if (!firstReplyTime) firstReplyTime = replyStart;
753
+ if (isA2AMessage) {
754
+ if (!_a2aCanReply(metadata.a2aChannelId)) {
755
+ console.warn(`[AgentVault] A2A reply suppressed (rate limit) for channel ${metadata.a2aChannelId?.slice(0, 8)}...`);
756
+ return;
757
+ }
758
+ await channel.sendToAgent(metadata.fromHubAddress, text, {
759
+ parentSpanId: inferenceSpanId
760
+ });
761
+ _a2aRecordReply(metadata.a2aChannelId);
762
+ } else if (isRoomMessage) {
763
+ await channel.sendToRoom(metadata.roomId, text, {
764
+ metadata: { content_length: text.length }
765
+ });
766
+ } else {
767
+ await channel.send(text, {
768
+ conversationId: metadata.conversationId,
769
+ topicId: metadata.topicId
770
+ });
771
+ }
772
+ const replyEnd = Date.now();
773
+ replySpans.push({ startTime: replyStart, endTime: replyEnd, chars: text.length, index: replyCount });
774
+ },
775
+ onError: (err, info) => {
776
+ core.error?.(`[AgentVault] ${info?.kind ?? "reply"} error: ${String(err)}`);
777
+ }
337
778
  },
779
+ replyOptions: {}
780
+ })
781
+ );
782
+ const endTime = Date.now();
783
+ _emitHierarchicalSpans(channel, {
784
+ traceId,
785
+ rootSpanId,
786
+ inferenceSpanId,
787
+ startTime,
788
+ endTime,
789
+ firstReplyTime,
790
+ replySpans,
791
+ inputChars: plaintext.length,
792
+ replyCount,
793
+ totalReplyChars,
794
+ isRoom: isRoomMessage,
795
+ status: "ok"
338
796
  });
339
- const storePath = core.channel.session.resolveStorePath(cfg?.session?.store, {
340
- agentId: route.agentId,
341
- });
342
- const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
343
- const previousTimestamp = core.channel.session.readSessionUpdatedAt({
344
- storePath,
345
- sessionKey: route.sessionKey,
346
- });
347
- const body = core.channel.reply.formatAgentEnvelope({
348
- channel: "AgentVault",
349
- from: isA2AMessage ? `Agent (${metadata.fromHubAddress})` : isRoomMessage ? "Room" : "Owner",
350
- timestamp: new Date(metadata.timestamp).getTime(),
351
- previousTimestamp,
352
- envelope: envelopeOptions,
353
- body: plaintext,
797
+ } catch (err) {
798
+ const endTime = Date.now();
799
+ _emitHierarchicalSpans(channel, {
800
+ traceId,
801
+ rootSpanId,
802
+ inferenceSpanId,
803
+ startTime,
804
+ endTime,
805
+ firstReplyTime,
806
+ replySpans,
807
+ inputChars: plaintext.length,
808
+ replyCount,
809
+ totalReplyChars,
810
+ isRoom: isRoomMessage,
811
+ status: "error",
812
+ statusMessage: String(err)
354
813
  });
355
- // Build attachment fields for the context payload
356
- const attachmentFields = {};
357
- if (metadata.attachment) {
358
- attachmentFields.AttachmentPath = metadata.attachment.filePath;
359
- attachmentFields.AttachmentFilename = metadata.attachment.filename;
360
- attachmentFields.AttachmentMime = metadata.attachment.mime;
361
- // For images: include as MediaUrl so the LLM can see the visual content
362
- if (metadata.attachment.base64) {
363
- attachmentFields.MediaUrl = metadata.attachment.base64;
364
- attachmentFields.NumMedia = "1";
365
- }
366
- // For text files: content is already inlined in plaintext body
367
- }
368
- const ctxPayload = core.channel.reply.finalizeInboundContext({
369
- Body: body,
370
- RawBody: plaintext,
371
- CommandBody: plaintext,
372
- From: isA2AMessage ? `a2a:${metadata.fromHubAddress}` : isRoomMessage ? `agentvault:room:${metadata.roomId}` : "agentvault:owner",
373
- To: `agentvault:agent:${account.accountId}`,
374
- SessionKey: route.sessionKey,
375
- AccountId: account.accountId,
376
- ChatType: isA2AMessage ? "a2a" : isRoomMessage ? "room" : "direct",
377
- ConversationLabel: isA2AMessage ? `A2A: ${metadata.fromHubAddress}` : isRoomMessage ? "AgentVault Room" : "AgentVault",
378
- SenderName: isA2AMessage ? metadata.fromHubAddress : isRoomMessage ? "Room" : "Owner",
379
- SenderId: isA2AMessage ? `a2a:${metadata.fromHubAddress}` : isRoomMessage ? `agentvault:room:${metadata.roomId}` : "agentvault:owner",
380
- Provider: "agentvault",
381
- Surface: "agentvault",
382
- MessageSid: metadata.messageId,
383
- Timestamp: new Date(metadata.timestamp).getTime(),
384
- OriginatingChannel: "agentvault",
385
- OriginatingTo: `agentvault:agent:${account.accountId}`,
386
- CommandAuthorized: true,
387
- Instrument: _instrument, // OpenClaw ctx convention
388
- AgentVaultTelemetry: _instrument, // AgentVault-specific accessor
389
- ...attachmentFields,
390
- });
391
- await core.channel.session.recordInboundSession({
392
- storePath,
393
- sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
394
- ctx: ctxPayload,
395
- onRecordError: (err) => {
396
- core.error?.(`[AgentVault] session record failed: ${String(err)}`);
397
- },
814
+ _emitChildSpan(channel, {
815
+ traceId,
816
+ parentSpanId: rootSpanId,
817
+ name: "error",
818
+ startTime: endTime,
819
+ endTime,
820
+ attributes: {
821
+ "ai.agent.error.type": err?.constructor?.name ?? "Error",
822
+ "ai.agent.error.message": String(err)
823
+ },
824
+ status: "error",
825
+ statusMessage: String(err)
398
826
  });
399
- try {
400
- await runWithTraceContext({ traceId, parentSpanId: inferenceSpanId }, () => core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
401
- ctx: ctxPayload,
402
- cfg,
403
- dispatcherOptions: {
404
- deliver: async (payload) => {
405
- // Filter out thinking/reasoning blocks — only deliver actual responses
406
- if (payload.kind === "thinking" || payload.kind === "reasoning")
407
- return;
408
- const text = (payload.text ?? "").trim();
409
- if (!text)
410
- return;
411
- // Heuristic: skip blocks that look like leaked chain-of-thought
412
- if (/^(Reasoning|Thinking|Let me think|I need to|Let me check):/i.test(text))
413
- return;
414
- const replyStart = Date.now();
415
- replyCount++;
416
- totalReplyChars += text.length;
417
- if (!firstReplyTime)
418
- firstReplyTime = replyStart;
419
- // Route reply: A2A → sendToAgent, room → sendToRoom, 1:1 → send
420
- if (isA2AMessage) {
421
- // Loop prevention: rate limit A2A replies per channel
422
- if (!_a2aCanReply(metadata.a2aChannelId)) {
423
- console.warn(`[AgentVault] A2A reply suppressed (rate limit) for channel ${metadata.a2aChannelId?.slice(0, 8)}...`);
424
- return;
425
- }
426
- await channel.sendToAgent(metadata.fromHubAddress, text, {
427
- parentSpanId: inferenceSpanId,
428
- });
429
- _a2aRecordReply(metadata.a2aChannelId);
430
- }
431
- else if (isRoomMessage) {
432
- await channel.sendToRoom(metadata.roomId, text, {
433
- metadata: { content_length: text.length },
434
- });
435
- }
436
- else {
437
- await channel.send(text, {
438
- conversationId: metadata.conversationId,
439
- topicId: metadata.topicId,
440
- });
441
- }
442
- const replyEnd = Date.now();
443
- replySpans.push({ startTime: replyStart, endTime: replyEnd, chars: text.length, index: replyCount });
444
- },
445
- onError: (err, info) => {
446
- core.error?.(`[AgentVault] ${info?.kind ?? "reply"} error: ${String(err)}`);
447
- },
448
- },
449
- replyOptions: {},
450
- }));
451
- const endTime = Date.now();
452
- // Emit child spans for the completed message lifecycle
453
- _emitHierarchicalSpans(channel, {
454
- traceId,
455
- rootSpanId,
456
- inferenceSpanId,
457
- startTime,
458
- endTime,
459
- firstReplyTime,
460
- replySpans,
461
- inputChars: plaintext.length,
462
- replyCount,
463
- totalReplyChars,
464
- isRoom: isRoomMessage,
465
- status: "ok",
466
- });
467
- }
468
- catch (err) {
469
- const endTime = Date.now();
470
- // Emit spans even on error — shows where failure occurred
471
- _emitHierarchicalSpans(channel, {
472
- traceId,
473
- rootSpanId,
474
- inferenceSpanId,
475
- startTime,
476
- endTime,
477
- firstReplyTime,
478
- replySpans,
479
- inputChars: plaintext.length,
480
- replyCount,
481
- totalReplyChars,
482
- isRoom: isRoomMessage,
483
- status: "error",
484
- statusMessage: String(err),
485
- });
486
- // Dedicated error span for the Errors tab
487
- _emitChildSpan(channel, {
488
- traceId,
489
- parentSpanId: rootSpanId,
490
- name: "error",
491
- startTime: endTime,
492
- endTime: endTime,
493
- attributes: {
494
- "ai.agent.error.type": err?.constructor?.name ?? "Error",
495
- "ai.agent.error.message": String(err),
496
- },
497
- status: "error",
498
- statusMessage: String(err),
499
- });
500
- throw err;
501
- }
827
+ throw err;
828
+ }
502
829
  }
503
- /** Infer a span type from the span name for activity stream display. */
504
830
  function _inferSpanType(name) {
505
- if (name === "error" || name.includes("error"))
506
- return "error";
507
- if (name.startsWith("llm.") || name.includes("inference"))
508
- return "llm";
509
- if (name.startsWith("tool.") || name.includes("tool"))
510
- return "tool";
511
- if (name.startsWith("http.") || name.includes("http"))
512
- return "http";
513
- if (name.startsWith("nav.") || name.includes("nav"))
514
- return "nav";
515
- if (name.startsWith("action.") || name.includes("action"))
516
- return "action";
517
- return "action"; // default fallback
831
+ if (name === "error" || name.includes("error")) return "error";
832
+ if (name.startsWith("llm.") || name.includes("inference")) return "llm";
833
+ if (name.startsWith("tool.") || name.includes("tool")) return "tool";
834
+ if (name.startsWith("http.") || name.includes("http")) return "http";
835
+ if (name.startsWith("nav.") || name.includes("nav")) return "nav";
836
+ if (name.startsWith("action.") || name.includes("action")) return "action";
837
+ return "action";
518
838
  }
519
- /** Extract a human-readable span name from span attributes. */
520
839
  function _extractSpanName(name, attrs) {
521
- if (attrs["ai.agent.llm.model"])
522
- return String(attrs["ai.agent.llm.model"]);
523
- if (attrs["ai.agent.tool.name"])
524
- return String(attrs["ai.agent.tool.name"]);
525
- if (attrs["ai.agent.http.url"]) {
526
- try {
527
- return new URL(String(attrs["ai.agent.http.url"])).hostname;
528
- }
529
- catch { /* use raw */ }
530
- return String(attrs["ai.agent.http.url"]).slice(0, 40);
840
+ if (attrs["ai.agent.llm.model"]) return String(attrs["ai.agent.llm.model"]);
841
+ if (attrs["ai.agent.tool.name"]) return String(attrs["ai.agent.tool.name"]);
842
+ if (attrs["ai.agent.http.url"]) {
843
+ try {
844
+ return new URL(String(attrs["ai.agent.http.url"])).hostname;
845
+ } catch {
531
846
  }
532
- if (attrs["ai.agent.action.type"])
533
- return `${attrs["ai.agent.action.type"]}: ${attrs["ai.agent.action.target"] ?? ""}`;
534
- if (attrs["ai.agent.nav.to_url"])
535
- return String(attrs["ai.agent.nav.to_url"]).slice(0, 40);
536
- if (attrs["ai.agent.error.type"])
537
- return String(attrs["ai.agent.error.type"]);
538
- return name;
847
+ return String(attrs["ai.agent.http.url"]).slice(0, 40);
848
+ }
849
+ if (attrs["ai.agent.action.type"]) return `${attrs["ai.agent.action.type"]}: ${attrs["ai.agent.action.target"] ?? ""}`;
850
+ if (attrs["ai.agent.nav.to_url"]) return String(attrs["ai.agent.nav.to_url"]).slice(0, 40);
851
+ if (attrs["ai.agent.error.type"]) return String(attrs["ai.agent.error.type"]);
852
+ return name;
539
853
  }
540
- /** Emit a single child span via the channel's telemetry reporter + WS activity stream. */
541
854
  function _emitChildSpan(channel, opts) {
855
+ try {
856
+ const reporter = channel.telemetry;
857
+ if (!reporter) return;
858
+ const spanId = opts.spanId ?? randomBytes(8).toString("hex");
859
+ reporter.reportCustomSpan({
860
+ traceId: opts.traceId,
861
+ spanId,
862
+ parentSpanId: opts.parentSpanId,
863
+ name: opts.name,
864
+ kind: "internal",
865
+ startTime: opts.startTime,
866
+ endTime: opts.endTime,
867
+ attributes: opts.attributes,
868
+ status: {
869
+ code: opts.status === "ok" ? 1 : 2,
870
+ message: opts.statusMessage
871
+ }
872
+ });
542
873
  try {
543
- const reporter = channel.telemetry;
544
- if (!reporter)
545
- return;
546
- const spanId = opts.spanId ?? randomBytes(8).toString("hex");
547
- reporter.reportCustomSpan({
548
- traceId: opts.traceId,
549
- spanId,
550
- parentSpanId: opts.parentSpanId,
551
- name: opts.name,
552
- kind: "internal",
553
- startTime: opts.startTime,
554
- endTime: opts.endTime,
555
- attributes: opts.attributes,
556
- status: {
557
- code: opts.status === "ok" ? 1 : 2,
558
- message: opts.statusMessage,
559
- },
560
- });
561
- // Dual-path: also send via WS for real-time activity streaming
562
- try {
563
- const spanType = _inferSpanType(opts.name);
564
- const spanName = _extractSpanName(opts.name, opts.attributes);
565
- const durationMs = opts.endTime - opts.startTime;
566
- channel.sendActivitySpan({
567
- trace_id: opts.traceId,
568
- span_id: spanId,
569
- parent_span_id: opts.parentSpanId,
570
- span_type: spanType,
571
- span_name: spanName,
572
- status: opts.status,
573
- start_time: new Date(opts.startTime).toISOString(),
574
- end_time: new Date(opts.endTime).toISOString(),
575
- duration_ms: durationMs,
576
- attributes: opts.attributes,
577
- ...(spanType === "llm" ? {
578
- tokens_input: opts.attributes["ai.agent.llm.tokens_input"],
579
- tokens_output: opts.attributes["ai.agent.llm.tokens_output"],
580
- } : {}),
581
- ...(spanType === "http" ? {
582
- http_method: opts.attributes["ai.agent.http.method"],
583
- http_status_code: opts.attributes["ai.agent.http.status_code"],
584
- http_url: opts.attributes["ai.agent.http.url"],
585
- } : {}),
586
- ...(spanType === "tool" ? {
587
- tool_success: opts.attributes["ai.agent.tool.success"],
588
- } : {}),
589
- ...(spanType === "error" ? {
590
- error_type: opts.attributes["ai.agent.error.type"],
591
- error_message: opts.attributes["ai.agent.error.message"],
592
- } : {}),
593
- });
594
- }
595
- catch { /* WS activity is best-effort */ }
874
+ const spanType = _inferSpanType(opts.name);
875
+ const spanName = _extractSpanName(opts.name, opts.attributes);
876
+ const durationMs = opts.endTime - opts.startTime;
877
+ channel.sendActivitySpan({
878
+ trace_id: opts.traceId,
879
+ span_id: spanId,
880
+ parent_span_id: opts.parentSpanId,
881
+ span_type: spanType,
882
+ span_name: spanName,
883
+ status: opts.status,
884
+ start_time: new Date(opts.startTime).toISOString(),
885
+ end_time: new Date(opts.endTime).toISOString(),
886
+ duration_ms: durationMs,
887
+ attributes: opts.attributes,
888
+ ...spanType === "llm" ? {
889
+ tokens_input: opts.attributes["ai.agent.llm.tokens_input"],
890
+ tokens_output: opts.attributes["ai.agent.llm.tokens_output"]
891
+ } : {},
892
+ ...spanType === "http" ? {
893
+ http_method: opts.attributes["ai.agent.http.method"],
894
+ http_status_code: opts.attributes["ai.agent.http.status_code"],
895
+ http_url: opts.attributes["ai.agent.http.url"]
896
+ } : {},
897
+ ...spanType === "tool" ? {
898
+ tool_success: opts.attributes["ai.agent.tool.success"]
899
+ } : {},
900
+ ...spanType === "error" ? {
901
+ error_type: opts.attributes["ai.agent.error.type"],
902
+ error_message: opts.attributes["ai.agent.error.message"]
903
+ } : {}
904
+ });
905
+ } catch {
596
906
  }
597
- catch { /* telemetry is best-effort */ }
907
+ } catch {
908
+ }
598
909
  }
599
- /** Emit the full hierarchical span tree for a message lifecycle. */
600
910
  function _emitHierarchicalSpans(channel, opts) {
601
- try {
602
- const reporter = channel.telemetry;
603
- if (!reporter)
604
- return;
605
- // 1. Inference span: from message arrival to first reply (or end if no replies)
606
- const inferenceEnd = opts.firstReplyTime ?? opts.endTime;
607
- _emitChildSpan(channel, {
608
- traceId: opts.traceId,
609
- parentSpanId: opts.rootSpanId,
610
- spanId: opts.inferenceSpanId,
611
- name: "ai.agent.inference",
612
- startTime: opts.startTime + 1, // just after receive
613
- endTime: inferenceEnd,
614
- attributes: {
615
- "ai.agent.inference.latency_ms": inferenceEnd - opts.startTime,
616
- "ai.agent.message.input_chars": opts.inputChars,
617
- },
618
- status: opts.status,
619
- statusMessage: opts.status === "error" ? opts.statusMessage : undefined,
620
- });
621
- // 2. Individual reply spans
622
- for (const reply of opts.replySpans) {
623
- _emitChildSpan(channel, {
624
- traceId: opts.traceId,
625
- parentSpanId: opts.rootSpanId,
626
- name: "ai.agent.reply",
627
- startTime: reply.startTime,
628
- endTime: reply.endTime,
629
- attributes: {
630
- "ai.agent.reply.index": reply.index,
631
- "ai.agent.reply.chars": reply.chars,
632
- },
633
- status: "ok",
634
- });
635
- }
636
- // 3. Root span (wraps everything) — emitted last
637
- reporter.reportCustomSpan({
638
- traceId: opts.traceId,
639
- spanId: opts.rootSpanId,
640
- name: "ai.agent.message",
641
- kind: "server",
642
- startTime: opts.startTime,
643
- endTime: opts.endTime,
644
- attributes: {
645
- "ai.agent.message.input_chars": opts.inputChars,
646
- "ai.agent.message.reply_count": opts.replyCount,
647
- "ai.agent.message.reply_chars": opts.totalReplyChars,
648
- "ai.agent.message.latency_ms": opts.endTime - opts.startTime,
649
- "ai.agent.message.type": opts.isRoom ? "room" : "direct",
650
- },
651
- status: {
652
- code: opts.status === "ok" ? 1 : 2,
653
- message: opts.statusMessage,
654
- },
655
- });
911
+ try {
912
+ const reporter = channel.telemetry;
913
+ if (!reporter) return;
914
+ const inferenceEnd = opts.firstReplyTime ?? opts.endTime;
915
+ _emitChildSpan(channel, {
916
+ traceId: opts.traceId,
917
+ parentSpanId: opts.rootSpanId,
918
+ spanId: opts.inferenceSpanId,
919
+ name: "ai.agent.inference",
920
+ startTime: opts.startTime + 1,
921
+ // just after receive
922
+ endTime: inferenceEnd,
923
+ attributes: {
924
+ "ai.agent.inference.latency_ms": inferenceEnd - opts.startTime,
925
+ "ai.agent.message.input_chars": opts.inputChars
926
+ },
927
+ status: opts.status,
928
+ statusMessage: opts.status === "error" ? opts.statusMessage : void 0
929
+ });
930
+ for (const reply of opts.replySpans) {
931
+ _emitChildSpan(channel, {
932
+ traceId: opts.traceId,
933
+ parentSpanId: opts.rootSpanId,
934
+ name: "ai.agent.reply",
935
+ startTime: reply.startTime,
936
+ endTime: reply.endTime,
937
+ attributes: {
938
+ "ai.agent.reply.index": reply.index,
939
+ "ai.agent.reply.chars": reply.chars
940
+ },
941
+ status: "ok"
942
+ });
656
943
  }
657
- catch { /* telemetry is best-effort */ }
944
+ reporter.reportCustomSpan({
945
+ traceId: opts.traceId,
946
+ spanId: opts.rootSpanId,
947
+ name: "ai.agent.message",
948
+ kind: "server",
949
+ startTime: opts.startTime,
950
+ endTime: opts.endTime,
951
+ attributes: {
952
+ "ai.agent.message.input_chars": opts.inputChars,
953
+ "ai.agent.message.reply_count": opts.replyCount,
954
+ "ai.agent.message.reply_chars": opts.totalReplyChars,
955
+ "ai.agent.message.latency_ms": opts.endTime - opts.startTime,
956
+ "ai.agent.message.type": opts.isRoom ? "room" : "direct"
957
+ },
958
+ status: {
959
+ code: opts.status === "ok" ? 1 : 2,
960
+ message: opts.statusMessage
961
+ }
962
+ });
963
+ } catch {
964
+ }
658
965
  }
659
- // --- Channel plugin definition ---
660
- const agentVaultPlugin = {
966
+ var agentVaultPlugin = {
967
+ id: "agentvault",
968
+ meta: {
661
969
  id: "agentvault",
662
- meta: {
663
- id: "agentvault",
664
- label: "AgentVault",
665
- selectionLabel: "AgentVault (E2E Encrypted)",
666
- docsPath: "https://agentvault.chat/docs",
667
- blurb: "Zero-knowledge, end-to-end encrypted messaging between owners and their AI agents.",
668
- aliases: ["av", "agent-vault"],
970
+ label: "AgentVault",
971
+ selectionLabel: "AgentVault (E2E Encrypted)",
972
+ docsPath: "https://agentvault.chat/docs",
973
+ blurb: "Zero-knowledge, end-to-end encrypted messaging between owners and their AI agents.",
974
+ aliases: ["av", "agent-vault"]
975
+ },
976
+ capabilities: { chatTypes: ["direct", "room"] },
977
+ config: { listAccountIds, resolveAccount },
978
+ gateway: {
979
+ /** Health probe for `openclaw channels status --probe` */
980
+ probe: async (ctx) => {
981
+ const accountId = ctx?.accountId ?? "default";
982
+ const ch = _channels.get(accountId);
983
+ if (!ch) return { ok: false, status: "disconnected", error: "Channel not started" };
984
+ const state = ch.state;
985
+ return {
986
+ ok: state === "ready",
987
+ status: state,
988
+ deviceId: ch.deviceId ?? void 0,
989
+ sessions: ch.sessionCount
990
+ };
669
991
  },
670
- capabilities: { chatTypes: ["direct", "room"] },
671
- config: { listAccountIds, resolveAccount },
672
- gateway: {
673
- /** Health probe for `openclaw channels status --probe` */
674
- probe: async (ctx) => {
675
- const accountId = ctx?.accountId ?? "default";
676
- const ch = _channels.get(accountId);
677
- if (!ch)
678
- return { ok: false, status: "disconnected", error: "Channel not started" };
679
- const state = ch.state;
680
- return {
681
- ok: state === "ready",
682
- status: state,
683
- deviceId: ch.deviceId ?? undefined,
684
- sessions: ch.sessionCount,
685
- };
686
- },
687
- /** Status for `openclaw health --json` per-channel summary */
688
- status: (ctx) => {
689
- const accountId = ctx?.accountId ?? "default";
690
- const ch = _channels.get(accountId);
691
- if (!ch)
692
- return { connected: false, status: "not_started" };
693
- return {
694
- connected: ch.state === "ready",
695
- status: ch.state,
696
- deviceId: ch.deviceId ?? undefined,
697
- sessions: ch.sessionCount,
698
- encrypted: true,
699
- };
700
- },
701
- startAccount: async (ctx) => {
702
- const { account, cfg, log, abortSignal } = ctx;
703
- const _log = typeof log === "function" ? log : log?.info?.bind(log);
704
- if (!account.configured) {
705
- throw new Error("AgentVault channel not configured. Run: npx @agentvault/agentvault setup --token=av_tok_...\nThen restart OpenClaw.");
706
- }
707
- const dataDir = resolve(account.dataDir.replace(/^~/, process.env.HOME ?? "~"));
708
- _log?.(`[AgentVault] starting (dataDir=${dataDir})`);
709
- // startAccount must STAY PENDING while the channel is running.
710
- // Resolving signals "channel stopped" to the gateway health monitor,
711
- // which triggers auto-restart. We block here until the abortSignal
712
- // fires (gateway shutdown / config reload), then clean up.
713
- await new Promise((resolve, reject) => {
714
- let channel;
715
- const onAbort = async () => {
716
- await channel?.stop();
717
- _channels.delete(account.accountId);
718
- resolve();
719
- };
720
- abortSignal?.addEventListener("abort", () => void onAbort());
721
- // Lazy import — defers libsodium initialization until channel starts
722
- import("./index.js").then(({ SecureChannel }) => {
723
- channel = new SecureChannel({
724
- inviteToken: "",
725
- dataDir,
726
- apiUrl: account.apiUrl,
727
- agentName: account.agentName,
728
- onMessage: async (plaintext, metadata) => {
729
- if (!_ocRuntime) {
730
- _log?.("[AgentVault] runtime not ready, queuing message");
731
- _messageQueue.push(async () => {
732
- await handleInbound({ plaintext, metadata, channel, account, cfg });
733
- });
734
- return;
735
- }
736
- try {
737
- await handleInbound({ plaintext, metadata, channel, account, cfg });
738
- }
739
- catch (err) {
740
- _log?.(`[AgentVault] inbound error: ${String(err)}`);
741
- }
742
- },
743
- onA2AMessage: async (msg) => {
744
- const a2aMetadata = {
745
- a2aChannelId: msg.channelId,
746
- fromHubAddress: msg.fromHubAddress,
747
- conversationId: msg.conversationId,
748
- timestamp: msg.timestamp,
749
- parentSpanId: msg.parentSpanId,
750
- messageId: `a2a:${msg.channelId}:${Date.now()}`,
751
- };
752
- if (!_ocRuntime) {
753
- _log?.("[AgentVault] runtime not ready, queuing A2A message");
754
- _messageQueue.push(async () => {
755
- await handleInbound({ plaintext: msg.text, metadata: a2aMetadata, channel, account, cfg });
756
- });
757
- return;
758
- }
759
- try {
760
- await handleInbound({ plaintext: msg.text, metadata: a2aMetadata, channel, account, cfg });
761
- }
762
- catch (err) {
763
- _log?.(`[AgentVault] A2A inbound error: ${String(err)}`);
764
- }
765
- },
766
- onA2AChannelReady: async (info) => {
767
- // Register peer as valid outbound target (both roles)
768
- _registerA2ATarget(info.peerHubAddress, account.accountId);
769
- // Only initiator sends the seed message
770
- if (info.role !== "initiator") {
771
- _log?.(`[AgentVault] A2A channel ready (responder) — waiting for initiator: ${info.peerHubAddress}`);
772
- return;
773
- }
774
- _log?.(`[AgentVault] A2A channel ready (initiator) — sending seed to ${info.peerHubAddress}${info.topic ? ` [topic: ${info.topic}]` : ""}`);
775
- const seedText = info.topic
776
- ? `[System] A new A2A channel has been established with agent ${info.peerHubAddress}. ` +
777
- `Topic: "${info.topic}". ` +
778
- `Introduce yourself briefly and begin discussing this topic.`
779
- : `[System] A new A2A channel has been established with agent ${info.peerHubAddress}. ` +
780
- `Introduce yourself briefly and state what you can help with.`;
781
- const seedMetadata = {
782
- a2aChannelId: info.channelId,
783
- fromHubAddress: info.peerHubAddress,
784
- conversationId: info.conversationId,
785
- timestamp: new Date().toISOString(),
786
- messageId: `a2a-seed:${info.channelId}:${Date.now()}`,
787
- };
788
- if (!_ocRuntime) {
789
- _log?.("[AgentVault] runtime not ready, queuing A2A seed");
790
- _messageQueue.push(async () => {
791
- await handleInbound({ plaintext: seedText, metadata: seedMetadata, channel, account, cfg });
792
- });
793
- return;
794
- }
795
- try {
796
- await handleInbound({ plaintext: seedText, metadata: seedMetadata, channel, account, cfg });
797
- }
798
- catch (err) {
799
- _log?.(`[AgentVault] A2A seed error: ${String(err)}`);
800
- }
801
- },
802
- onStateChange: (state) => {
803
- _log?.(`[AgentVault] → ${state}`);
804
- // "error" is a permanent failure — reject so gateway can restart
805
- if (state === "error")
806
- reject(new Error("AgentVault channel permanent error"));
807
- // All other states (connecting/ready/disconnected) are handled
808
- // internally by SecureChannel's reconnect logic — do NOT resolve.
809
- },
810
- });
811
- _channels.set(account.accountId, channel);
812
- // Prevent unhandled "error" events from crashing the process.
813
- // Without this handler, Node.js EventEmitter throws on emit("error").
814
- channel.on("error", (err) => {
815
- _log?.(`[AgentVault] channel error (non-fatal): ${String(err)}`);
816
- });
817
- // Always start local HTTP server — managed routes are an overlay, not a replacement
818
- const httpPort = account.httpPort ?? 18790;
819
- channel.on("ready", () => {
820
- channel.startHttpServer(httpPort);
821
- _log?.(`[AgentVault] HTTP send server listening on http://127.0.0.1:${httpPort}`);
822
- if (isUsingManagedRoutes) {
823
- _log?.(`[AgentVault] OpenClaw managed routes also registered`);
824
- }
825
- // Register persisted A2A peers as valid outbound targets
826
- for (const peerAddress of channel.a2aPeerAddresses) {
827
- _registerA2ATarget(peerAddress, account.accountId);
828
- }
829
- // Register persisted rooms as valid outbound targets
830
- for (const room of channel.roomIds) {
831
- _registerRoomTarget(room.roomId, room.name, account.accountId);
832
- }
833
- });
834
- // Register new rooms as outbound targets when agent joins
835
- channel.on("room_joined", (info) => {
836
- _registerRoomTarget(info.roomId, info.name, account.accountId);
837
- });
838
- channel.start().catch(reject);
839
- }).catch(reject);
840
- });
841
- return { stop: async () => { } }; // Channel already stopped via abortSignal by this point
842
- },
992
+ /** Status for `openclaw health --json` per-channel summary */
993
+ status: (ctx) => {
994
+ const accountId = ctx?.accountId ?? "default";
995
+ const ch = _channels.get(accountId);
996
+ if (!ch) return { connected: false, status: "not_started" };
997
+ return {
998
+ connected: ch.state === "ready",
999
+ status: ch.state,
1000
+ deviceId: ch.deviceId ?? void 0,
1001
+ sessions: ch.sessionCount,
1002
+ encrypted: true
1003
+ };
843
1004
  },
844
- outbound: {
845
- deliveryMode: "direct",
846
- // Register valid send targets so OpenClaw's `message` tool can route
847
- // proactive (agent-initiated) sends — not just replies to inbound messages.
848
- targets: _outboundTargets,
849
- sendText: async ({ to, text, accountId }) => {
850
- const resolvedId = accountId ?? "default";
851
- const ch = _channels.get(resolvedId);
852
- if (!ch)
853
- return { ok: false, error: "AgentVault channel not connected" };
854
- try {
855
- const wasReady = ch.state === "ready";
856
- if (to.startsWith("a2a:")) {
857
- await ch.sendToAgent(to.slice(4), text);
858
- }
859
- else if (to.startsWith("room:")) {
860
- await ch.sendToRoom(to.slice(5), text);
861
- }
862
- else {
863
- // "owner"/"default" auto-route to room if context exists
864
- const roomId = ch.lastInboundRoomId;
865
- if (roomId) {
866
- await ch.sendToRoom(roomId, text);
867
- }
868
- else {
869
- await ch.send(text);
870
- }
871
- }
872
- return { ok: true, queued: !wasReady };
873
- }
874
- catch (err) {
875
- return { ok: false, error: String(err) };
876
- }
877
- },
878
- sendMedia: async ({ to, text, mediaUrl, accountId }) => {
879
- const resolvedId = accountId ?? "default";
880
- const ch = _channels.get(resolvedId);
881
- if (!ch)
882
- return { ok: false, error: "AgentVault channel not connected" };
883
- try {
884
- // For now, send media URL as text — AgentVault handles attachments separately
885
- const message = text ? `${text}\n${mediaUrl}` : mediaUrl;
886
- const wasReady = ch.state === "ready";
887
- if (to.startsWith("a2a:")) {
888
- await ch.sendToAgent(to.slice(4), message);
889
- }
890
- else if (to.startsWith("room:")) {
891
- await ch.sendToRoom(to.slice(5), message);
892
- }
893
- else {
894
- // "owner"/"default" auto-route to room if context exists
895
- const roomId = ch.lastInboundRoomId;
896
- if (roomId) {
897
- await ch.sendToRoom(roomId, message);
898
- }
899
- else {
900
- await ch.send(message);
901
- }
902
- }
903
- return { ok: true, queued: !wasReady };
1005
+ startAccount: async (ctx) => {
1006
+ const { account, cfg, log, abortSignal } = ctx;
1007
+ const _log = typeof log === "function" ? log : log?.info?.bind(log);
1008
+ if (!account.configured) {
1009
+ throw new Error(
1010
+ "AgentVault channel not configured. Run: npx @agentvault/agentvault setup --token=av_tok_...\nThen restart OpenClaw."
1011
+ );
1012
+ }
1013
+ const dataDir = resolve(account.dataDir.replace(/^~/, process.env.HOME ?? "~"));
1014
+ _log?.(`[AgentVault] starting (dataDir=${dataDir})`);
1015
+ await new Promise((resolve2, reject) => {
1016
+ let channel;
1017
+ const onAbort = async () => {
1018
+ await channel?.stop();
1019
+ _channels.delete(account.accountId);
1020
+ resolve2();
1021
+ };
1022
+ abortSignal?.addEventListener("abort", () => void onAbort());
1023
+ import("./index.js").then(({ SecureChannel }) => {
1024
+ channel = new SecureChannel({
1025
+ inviteToken: "",
1026
+ dataDir,
1027
+ apiUrl: account.apiUrl,
1028
+ agentName: account.agentName,
1029
+ onMessage: async (plaintext, metadata) => {
1030
+ if (!_ocRuntime) {
1031
+ _log?.("[AgentVault] runtime not ready, queuing message");
1032
+ _messageQueue.push(async () => {
1033
+ await handleInbound({ plaintext, metadata, channel, account, cfg });
1034
+ });
1035
+ return;
1036
+ }
1037
+ try {
1038
+ await handleInbound({ plaintext, metadata, channel, account, cfg });
1039
+ } catch (err) {
1040
+ _log?.(`[AgentVault] inbound error: ${String(err)}`);
1041
+ }
1042
+ },
1043
+ onA2AMessage: async (msg) => {
1044
+ const a2aMetadata = {
1045
+ a2aChannelId: msg.channelId,
1046
+ fromHubAddress: msg.fromHubAddress,
1047
+ conversationId: msg.conversationId,
1048
+ timestamp: msg.timestamp,
1049
+ parentSpanId: msg.parentSpanId,
1050
+ messageId: `a2a:${msg.channelId}:${Date.now()}`
1051
+ };
1052
+ if (!_ocRuntime) {
1053
+ _log?.("[AgentVault] runtime not ready, queuing A2A message");
1054
+ _messageQueue.push(async () => {
1055
+ await handleInbound({ plaintext: msg.text, metadata: a2aMetadata, channel, account, cfg });
1056
+ });
1057
+ return;
1058
+ }
1059
+ try {
1060
+ await handleInbound({ plaintext: msg.text, metadata: a2aMetadata, channel, account, cfg });
1061
+ } catch (err) {
1062
+ _log?.(`[AgentVault] A2A inbound error: ${String(err)}`);
1063
+ }
1064
+ },
1065
+ onA2AChannelReady: async (info) => {
1066
+ _registerA2ATarget(info.peerHubAddress, account.accountId);
1067
+ if (info.role !== "initiator") {
1068
+ _log?.(`[AgentVault] A2A channel ready (responder) \u2014 waiting for initiator: ${info.peerHubAddress}`);
1069
+ return;
1070
+ }
1071
+ _log?.(`[AgentVault] A2A channel ready (initiator) \u2014 sending seed to ${info.peerHubAddress}${info.topic ? ` [topic: ${info.topic}]` : ""}`);
1072
+ const seedText = info.topic ? `[System] A new A2A channel has been established with agent ${info.peerHubAddress}. Topic: "${info.topic}". Introduce yourself briefly and begin discussing this topic.` : `[System] A new A2A channel has been established with agent ${info.peerHubAddress}. Introduce yourself briefly and state what you can help with.`;
1073
+ const seedMetadata = {
1074
+ a2aChannelId: info.channelId,
1075
+ fromHubAddress: info.peerHubAddress,
1076
+ conversationId: info.conversationId,
1077
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1078
+ messageId: `a2a-seed:${info.channelId}:${Date.now()}`
1079
+ };
1080
+ if (!_ocRuntime) {
1081
+ _log?.("[AgentVault] runtime not ready, queuing A2A seed");
1082
+ _messageQueue.push(async () => {
1083
+ await handleInbound({ plaintext: seedText, metadata: seedMetadata, channel, account, cfg });
1084
+ });
1085
+ return;
1086
+ }
1087
+ try {
1088
+ await handleInbound({ plaintext: seedText, metadata: seedMetadata, channel, account, cfg });
1089
+ } catch (err) {
1090
+ _log?.(`[AgentVault] A2A seed error: ${String(err)}`);
1091
+ }
1092
+ },
1093
+ onStateChange: (state) => {
1094
+ _log?.(`[AgentVault] \u2192 ${state}`);
1095
+ if (state === "error") reject(new Error("AgentVault channel permanent error"));
904
1096
  }
905
- catch (err) {
906
- return { ok: false, error: String(err) };
1097
+ });
1098
+ _channels.set(account.accountId, channel);
1099
+ channel.on("error", (err) => {
1100
+ _log?.(`[AgentVault] channel error (non-fatal): ${String(err)}`);
1101
+ });
1102
+ const httpPort = account.httpPort ?? 18790;
1103
+ channel.on("ready", () => {
1104
+ channel.startHttpServer(httpPort);
1105
+ _log?.(`[AgentVault] HTTP send server listening on http://127.0.0.1:${httpPort}`);
1106
+ if (isUsingManagedRoutes) {
1107
+ _log?.(`[AgentVault] OpenClaw managed routes also registered`);
907
1108
  }
908
- },
909
- /** Rich payload delivery — OpenClaw v2026.3.2+ calls this for structured messages. */
910
- sendPayload: async (ctx) => {
911
- const { payload, accountId } = ctx;
912
- const resolvedId = accountId ?? "default";
913
- const ch = _channels.get(resolvedId);
914
- if (!ch)
915
- return { ok: false, error: "AgentVault channel not connected" };
916
- try {
917
- // Suppress reasoning blocks — E2E channel should only deliver actual responses
918
- if (payload.isReasoning)
919
- return { ok: true };
920
- // Wake agent proactively before sending (fire-and-forget)
921
- requestHeartbeatNow({ reason: "outbound-payload" }).catch(() => { });
922
- const text = (payload.text ?? "").trim();
923
- if (!text && !payload.mediaUrls?.length)
924
- return { ok: true };
925
- // Helper: route through A2A, room, or direct depending on target
926
- const _send = async (msg) => {
927
- const target = ctx.to;
928
- if (target && typeof target === "string" && target.startsWith("a2a:")) {
929
- await ch.sendToAgent(target.slice(4), msg);
930
- }
931
- else if (target && typeof target === "string" && target.startsWith("room:")) {
932
- await ch.sendToRoom(target.slice(5), msg);
933
- }
934
- else {
935
- // "owner"/"default" — auto-route to room if context exists
936
- const roomId = ch.lastInboundRoomId;
937
- if (roomId) {
938
- await ch.sendToRoom(roomId, msg);
939
- }
940
- else {
941
- await ch.send(msg);
942
- }
943
- }
944
- };
945
- // Encrypt and deliver text content
946
- if (text) {
947
- await _send(text);
948
- }
949
- // Deliver media URLs as text messages (E2E encrypted)
950
- if (payload.mediaUrls?.length) {
951
- for (const url of payload.mediaUrls) {
952
- await _send(url);
953
- }
954
- }
955
- // Forward suggested actions as a structured message
956
- if (payload.suggestedActions?.length) {
957
- const actionsText = payload.suggestedActions
958
- .map((a) => `- ${a.label}: ${a.action}`)
959
- .join("\n");
960
- await _send(`Suggested actions:\n${actionsText}`);
961
- }
962
- return { ok: true };
1109
+ for (const peerAddress of channel.a2aPeerAddresses) {
1110
+ _registerA2ATarget(peerAddress, account.accountId);
963
1111
  }
964
- catch (err) {
965
- return { ok: false, error: String(err) };
1112
+ for (const room of channel.roomIds) {
1113
+ _registerRoomTarget(room.roomId, room.name, account.accountId);
966
1114
  }
967
- },
1115
+ });
1116
+ channel.on("room_joined", (info) => {
1117
+ _registerRoomTarget(info.roomId, info.name, account.accountId);
1118
+ });
1119
+ channel.start().catch(reject);
1120
+ }).catch(reject);
1121
+ });
1122
+ return { stop: async () => {
1123
+ } };
1124
+ }
1125
+ },
1126
+ outbound: {
1127
+ deliveryMode: "direct",
1128
+ // Register valid send targets so OpenClaw's `message` tool can route
1129
+ // proactive (agent-initiated) sends — not just replies to inbound messages.
1130
+ targets: _outboundTargets,
1131
+ sendText: async ({ to, text, accountId }) => {
1132
+ const resolvedId = accountId ?? "default";
1133
+ const ch = _channels.get(resolvedId);
1134
+ if (!ch) return { ok: false, error: "AgentVault channel not connected" };
1135
+ try {
1136
+ const wasReady = ch.state === "ready";
1137
+ if (to.startsWith("a2a:")) {
1138
+ await ch.sendToAgent(to.slice(4), text);
1139
+ } else if (to.startsWith("room:")) {
1140
+ await ch.sendToRoom(to.slice(5), text);
1141
+ } else {
1142
+ const roomId = ch.lastInboundRoomId;
1143
+ if (roomId) {
1144
+ await ch.sendToRoom(roomId, text);
1145
+ } else {
1146
+ await ch.send(text);
1147
+ }
1148
+ }
1149
+ return { ok: true, queued: !wasReady };
1150
+ } catch (err) {
1151
+ return { ok: false, error: String(err) };
1152
+ }
968
1153
  },
969
- };
970
- // --- Exported for testing ---
971
- export { _parseMentions, _shouldProcessRoomMessage, _stripMentions };
972
- // --- Plugin export ---
973
- export default {
974
- id: "agentvault",
975
- name: "AgentVault",
976
- description: "End-to-end encrypted, zero-knowledge messaging between AI agent owners and their agents.",
977
- register(api) {
978
- _setRuntime(api.runtime);
979
- // Install fetch interceptor to capture all outbound HTTP from the gateway process.
980
- installFetchInterceptor({
981
- onHttpCall: (report) => {
982
- // Find an active channel to emit through
983
- const ch = _channels.values().next().value;
984
- if (!ch)
985
- return;
986
- // Telemetry reporter is optional — may not be initialized
987
- if (ch.telemetry) {
988
- try {
989
- ch.telemetry.reportHttpCall({
990
- method: report.method,
991
- url: report.url,
992
- statusCode: report.statusCode,
993
- latencyMs: report.latencyMs,
994
- ...(report.traceId ? { traceId: report.traceId } : {}),
995
- ...(report.parentSpanId ? { parentSpanId: report.parentSpanId } : {}),
996
- });
997
- }
998
- catch { /* best-effort */ }
999
- }
1000
- // Activity span always fires (uses WS, not telemetry reporter)
1001
- try {
1002
- const hostname = (() => { try {
1003
- return new URL(report.url).hostname;
1004
- }
1005
- catch {
1006
- return report.url.slice(0, 40);
1007
- } })();
1008
- ch.sendActivitySpan({
1009
- trace_id: report.traceId ?? "",
1010
- parent_span_id: report.parentSpanId ?? "",
1011
- span_id: randomBytes(8).toString("hex"),
1012
- span_type: "http",
1013
- span_name: hostname,
1014
- status: report.statusCode >= 400 || report.statusCode === 0 ? "error" : "ok",
1015
- start_time: new Date(Date.now() - report.latencyMs).toISOString(),
1016
- end_time: new Date().toISOString(),
1017
- duration_ms: report.latencyMs,
1018
- attributes: {
1019
- "ai.agent.http.method": report.method,
1020
- "ai.agent.http.url": report.url,
1021
- },
1022
- http_method: report.method,
1023
- http_status_code: report.statusCode,
1024
- http_url: report.url,
1025
- });
1026
- }
1027
- catch { /* best-effort */ }
1028
- },
1154
+ sendMedia: async ({ to, text, mediaUrl, accountId }) => {
1155
+ const resolvedId = accountId ?? "default";
1156
+ const ch = _channels.get(resolvedId);
1157
+ if (!ch) return { ok: false, error: "AgentVault channel not connected" };
1158
+ try {
1159
+ const message = text ? `${text}
1160
+ ${mediaUrl}` : mediaUrl;
1161
+ const wasReady = ch.state === "ready";
1162
+ if (to.startsWith("a2a:")) {
1163
+ await ch.sendToAgent(to.slice(4), message);
1164
+ } else if (to.startsWith("room:")) {
1165
+ await ch.sendToRoom(to.slice(5), message);
1166
+ } else {
1167
+ const roomId = ch.lastInboundRoomId;
1168
+ if (roomId) {
1169
+ await ch.sendToRoom(roomId, message);
1170
+ } else {
1171
+ await ch.send(message);
1172
+ }
1173
+ }
1174
+ return { ok: true, queued: !wasReady };
1175
+ } catch (err) {
1176
+ return { ok: false, error: String(err) };
1177
+ }
1178
+ },
1179
+ /** Rich payload delivery — OpenClaw v2026.3.2+ calls this for structured messages. */
1180
+ sendPayload: async (ctx) => {
1181
+ const { payload, accountId } = ctx;
1182
+ const resolvedId = accountId ?? "default";
1183
+ const ch = _channels.get(resolvedId);
1184
+ if (!ch) return { ok: false, error: "AgentVault channel not connected" };
1185
+ try {
1186
+ if (payload.isReasoning) return { ok: true };
1187
+ requestHeartbeatNow({ reason: "outbound-payload" }).catch(() => {
1029
1188
  });
1030
- api.registerChannel({ plugin: agentVaultPlugin });
1031
- // --- Managed HTTP routes (OpenClaw v2026.3.2+) ---
1032
- if (typeof api.registerHttpRoute === "function") {
1033
- try {
1034
- api.registerHttpRoute({
1035
- path: "/agentvault/send",
1036
- method: "POST",
1037
- handler: async (req) => {
1038
- const ch = _channels.values().next().value;
1039
- if (!ch)
1040
- return { status: 503, body: { ok: false, error: "Channel not started" } };
1041
- const parsed = (typeof req.body === "string" ? JSON.parse(req.body) : req.body);
1042
- const result = await handleSendRequest(parsed, ch);
1043
- return { status: result.status, body: result.body };
1044
- },
1045
- });
1046
- api.registerHttpRoute({
1047
- path: "/agentvault/action",
1048
- method: "POST",
1049
- handler: async (req) => {
1050
- const ch = _channels.values().next().value;
1051
- if (!ch)
1052
- return { status: 503, body: { ok: false, error: "Channel not started" } };
1053
- const parsed = (typeof req.body === "string" ? JSON.parse(req.body) : req.body);
1054
- const result = await handleActionRequest(parsed, ch);
1055
- return { status: result.status, body: result.body };
1056
- },
1057
- });
1058
- api.registerHttpRoute({
1059
- path: "/agentvault/decision",
1060
- method: "POST",
1061
- handler: async (req) => {
1062
- const ch = _channels.values().next().value;
1063
- if (!ch)
1064
- return { status: 503, body: { ok: false, error: "Channel not started" } };
1065
- const parsed = (typeof req.body === "string" ? JSON.parse(req.body) : req.body);
1066
- const result = await handleDecisionRequest(parsed, ch);
1067
- return { status: result.status, body: result.body };
1068
- },
1069
- });
1070
- api.registerHttpRoute({
1071
- path: "/agentvault/status",
1072
- method: "GET",
1073
- handler: async () => {
1074
- const ch = _channels.values().next().value;
1075
- if (!ch)
1076
- return { status: 503, body: { ok: false, error: "Channel not started" } };
1077
- const result = handleStatusRequest(ch);
1078
- return { status: result.status, body: result.body };
1079
- },
1080
- });
1081
- isUsingManagedRoutes = true;
1189
+ const text = (payload.text ?? "").trim();
1190
+ if (!text && !payload.mediaUrls?.length) return { ok: true };
1191
+ const _send = async (msg) => {
1192
+ const target = ctx.to;
1193
+ if (target && typeof target === "string" && target.startsWith("a2a:")) {
1194
+ await ch.sendToAgent(target.slice(4), msg);
1195
+ } else if (target && typeof target === "string" && target.startsWith("room:")) {
1196
+ await ch.sendToRoom(target.slice(5), msg);
1197
+ } else {
1198
+ const roomId = ch.lastInboundRoomId;
1199
+ if (roomId) {
1200
+ await ch.sendToRoom(roomId, msg);
1201
+ } else {
1202
+ await ch.send(msg);
1082
1203
  }
1083
- catch { /* registerHttpRoute failed — fall back to self-managed server */ }
1204
+ }
1205
+ };
1206
+ if (text) {
1207
+ await _send(text);
1084
1208
  }
1085
- // --- Event hooks (OpenClaw v2026.3.2+) ---
1086
- if (typeof api.on === "function") {
1087
- // Phase 4: message_sent — delivery confirmation tracking
1088
- api.on("message_sent", async (event) => {
1089
- try {
1090
- const ch = _channels.values().next().value;
1091
- if (!ch?.telemetry)
1092
- return;
1093
- ch.telemetry.reportCustomSpan({
1094
- traceId: randomBytes(16).toString("hex"),
1095
- spanId: randomBytes(8).toString("hex"),
1096
- name: "agentvault.message.delivered",
1097
- kind: "internal",
1098
- startTime: event.timestamp,
1099
- endTime: event.timestamp + 1,
1100
- attributes: {
1101
- "agentvault.message.id": event.messageId,
1102
- "agentvault.delivery.status": event.deliveryStatus,
1103
- "agentvault.channel.id": event.channelId,
1104
- },
1105
- status: { code: event.deliveryStatus === "failed" ? 2 : 1 },
1106
- });
1107
- }
1108
- catch { /* best-effort */ }
1109
- });
1110
- // Phase 6: session lifecycle hooks
1111
- api.on("session_start", async (event) => {
1112
- try {
1113
- const ch = _channels.values().next().value;
1114
- if (!ch?.telemetry)
1115
- return;
1116
- ch.telemetry.reportCustomSpan({
1117
- traceId: randomBytes(16).toString("hex"),
1118
- spanId: randomBytes(8).toString("hex"),
1119
- name: "agentvault.session.start",
1120
- kind: "internal",
1121
- startTime: event.timestamp,
1122
- endTime: event.timestamp + 1,
1123
- attributes: {
1124
- "agentvault.session.key": event.sessionKey,
1125
- "agentvault.agent.id": event.agentId,
1126
- },
1127
- status: { code: 1 },
1128
- });
1129
- }
1130
- catch { /* best-effort */ }
1131
- });
1132
- api.on("session_end", async (event) => {
1133
- try {
1134
- const ch = _channels.values().next().value;
1135
- if (!ch?.telemetry)
1136
- return;
1137
- ch.telemetry.reportCustomSpan({
1138
- traceId: randomBytes(16).toString("hex"),
1139
- spanId: randomBytes(8).toString("hex"),
1140
- name: "agentvault.session.end",
1141
- kind: "internal",
1142
- startTime: event.timestamp,
1143
- endTime: event.timestamp + 1,
1144
- attributes: {
1145
- "agentvault.session.key": event.sessionKey,
1146
- "agentvault.agent.id": event.agentId,
1147
- ...(event.reason ? { "agentvault.session.end_reason": event.reason } : {}),
1148
- },
1149
- status: { code: 1 },
1150
- });
1151
- }
1152
- catch { /* best-effort */ }
1209
+ if (payload.mediaUrls?.length) {
1210
+ for (const url of payload.mediaUrls) {
1211
+ await _send(url);
1212
+ }
1213
+ }
1214
+ if (payload.suggestedActions?.length) {
1215
+ const actionsText = payload.suggestedActions.map((a) => `- ${a.label}: ${a.action}`).join("\n");
1216
+ await _send(`Suggested actions:
1217
+ ${actionsText}`);
1218
+ }
1219
+ return { ok: true };
1220
+ } catch (err) {
1221
+ return { ok: false, error: String(err) };
1222
+ }
1223
+ }
1224
+ }
1225
+ };
1226
+ var openclaw_entry_default = {
1227
+ id: "agentvault",
1228
+ name: "AgentVault",
1229
+ description: "End-to-end encrypted, zero-knowledge messaging between AI agent owners and their agents.",
1230
+ register(api) {
1231
+ _setRuntime(api.runtime);
1232
+ installFetchInterceptor({
1233
+ onHttpCall: (report) => {
1234
+ const ch = _channels.values().next().value;
1235
+ if (!ch) return;
1236
+ if (ch.telemetry) {
1237
+ try {
1238
+ ch.telemetry.reportHttpCall({
1239
+ method: report.method,
1240
+ url: report.url,
1241
+ statusCode: report.statusCode,
1242
+ latencyMs: report.latencyMs,
1243
+ ...report.traceId ? { traceId: report.traceId } : {},
1244
+ ...report.parentSpanId ? { parentSpanId: report.parentSpanId } : {}
1153
1245
  });
1246
+ } catch {
1247
+ }
1154
1248
  }
1155
- // --- Phase 7: Agent events for trust telemetry ---
1156
- import("./openclaw-compat.js").then(async ({ onAgentEvent, onSessionTranscriptUpdate }) => {
1157
- onAgentEvent((event) => {
1158
- try {
1159
- const ch = _channels.values().next().value;
1160
- if (!ch?.telemetry)
1161
- return;
1162
- ch.telemetry.reportCustomSpan({
1163
- traceId: randomBytes(16).toString("hex"),
1164
- spanId: randomBytes(8).toString("hex"),
1165
- name: `ai.agent.event.${event.type}`,
1166
- kind: "internal",
1167
- startTime: event.timestamp,
1168
- endTime: event.timestamp + 1,
1169
- attributes: {
1170
- "ai.agent.event.type": event.type,
1171
- "ai.agent.id": event.agentId,
1172
- ...(event.data ? Object.fromEntries(Object.entries(event.data).map(([k, v]) => [`ai.agent.event.${k}`, String(v)])) : {}),
1173
- },
1174
- status: { code: 1 },
1175
- });
1176
- }
1177
- catch { /* best-effort */ }
1178
- }).catch(() => { });
1179
- onSessionTranscriptUpdate((update) => {
1180
- try {
1181
- const ch = _channels.values().next().value;
1182
- if (!ch?.telemetry)
1183
- return;
1184
- ch.telemetry.reportCustomSpan({
1185
- traceId: randomBytes(16).toString("hex"),
1186
- spanId: randomBytes(8).toString("hex"),
1187
- name: "ai.agent.transcript.update",
1188
- kind: "internal",
1189
- startTime: update.timestamp,
1190
- endTime: update.timestamp + 1,
1191
- attributes: {
1192
- "agentvault.session.key": update.sessionKey,
1193
- "ai.agent.transcript.role": update.role ?? "unknown",
1194
- "ai.agent.transcript.delta_chars": update.delta.length,
1195
- },
1196
- status: { code: 1 },
1197
- });
1198
- }
1199
- catch { /* best-effort */ }
1200
- }).catch(() => { });
1201
- }).catch(() => { });
1202
- // --- Agent tool: agentvault_status ---
1203
- // Lets the agent check its own encrypted channel status
1204
1249
  try {
1205
- api.registerTool?.({
1206
- name: "agentvault_status",
1207
- description: "Check the AgentVault encrypted channel status, connection state, and session count.",
1208
- parameters: {
1209
- type: "object",
1210
- properties: {
1211
- accountId: {
1212
- type: "string",
1213
- description: "Account ID to check (default: 'default')",
1214
- },
1215
- },
1216
- },
1217
- execute: async (_id, params) => {
1218
- const id = params.accountId ?? "default";
1219
- const ch = _channels.get(id);
1220
- if (!ch) {
1221
- return { content: [{ type: "text", text: JSON.stringify({ connected: false, error: "Channel not started" }) }] };
1222
- }
1223
- return {
1224
- content: [{
1225
- type: "text",
1226
- text: JSON.stringify({
1227
- connected: ch.state === "ready",
1228
- state: ch.state,
1229
- deviceId: ch.deviceId,
1230
- sessions: ch.sessionCount,
1231
- encrypted: true,
1232
- }),
1233
- }],
1234
- };
1235
- },
1236
- }, { optional: true });
1250
+ const hostname = (() => {
1251
+ try {
1252
+ return new URL(report.url).hostname;
1253
+ } catch {
1254
+ return report.url.slice(0, 40);
1255
+ }
1256
+ })();
1257
+ ch.sendActivitySpan({
1258
+ trace_id: report.traceId ?? "",
1259
+ parent_span_id: report.parentSpanId ?? "",
1260
+ span_id: randomBytes(8).toString("hex"),
1261
+ span_type: "http",
1262
+ span_name: hostname,
1263
+ status: report.statusCode >= 400 || report.statusCode === 0 ? "error" : "ok",
1264
+ start_time: new Date(Date.now() - report.latencyMs).toISOString(),
1265
+ end_time: (/* @__PURE__ */ new Date()).toISOString(),
1266
+ duration_ms: report.latencyMs,
1267
+ attributes: {
1268
+ "ai.agent.http.method": report.method,
1269
+ "ai.agent.http.url": report.url
1270
+ },
1271
+ http_method: report.method,
1272
+ http_status_code: report.statusCode,
1273
+ http_url: report.url
1274
+ });
1275
+ } catch {
1237
1276
  }
1238
- catch { /* registerTool may not be available in older OpenClaw versions */ }
1239
- // --- Auto-reply command: /agentvault ---
1240
- // Quick status check without AI involvement
1277
+ }
1278
+ });
1279
+ api.registerChannel({ plugin: agentVaultPlugin });
1280
+ if (typeof api.registerHttpRoute === "function") {
1281
+ try {
1282
+ api.registerHttpRoute({
1283
+ path: "/agentvault/send",
1284
+ method: "POST",
1285
+ handler: async (req) => {
1286
+ const ch = _channels.values().next().value;
1287
+ if (!ch) return { status: 503, body: { ok: false, error: "Channel not started" } };
1288
+ const parsed = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
1289
+ const result = await handleSendRequest(parsed, ch);
1290
+ return { status: result.status, body: result.body };
1291
+ }
1292
+ });
1293
+ api.registerHttpRoute({
1294
+ path: "/agentvault/action",
1295
+ method: "POST",
1296
+ handler: async (req) => {
1297
+ const ch = _channels.values().next().value;
1298
+ if (!ch) return { status: 503, body: { ok: false, error: "Channel not started" } };
1299
+ const parsed = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
1300
+ const result = await handleActionRequest(parsed, ch);
1301
+ return { status: result.status, body: result.body };
1302
+ }
1303
+ });
1304
+ api.registerHttpRoute({
1305
+ path: "/agentvault/decision",
1306
+ method: "POST",
1307
+ handler: async (req) => {
1308
+ const ch = _channels.values().next().value;
1309
+ if (!ch) return { status: 503, body: { ok: false, error: "Channel not started" } };
1310
+ const parsed = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
1311
+ const result = await handleDecisionRequest(parsed, ch);
1312
+ return { status: result.status, body: result.body };
1313
+ }
1314
+ });
1315
+ api.registerHttpRoute({
1316
+ path: "/agentvault/status",
1317
+ method: "GET",
1318
+ handler: async () => {
1319
+ const ch = _channels.values().next().value;
1320
+ if (!ch) return { status: 503, body: { ok: false, error: "Channel not started" } };
1321
+ const result = handleStatusRequest(ch);
1322
+ return { status: result.status, body: result.body };
1323
+ }
1324
+ });
1325
+ isUsingManagedRoutes = true;
1326
+ } catch {
1327
+ }
1328
+ }
1329
+ if (typeof api.on === "function") {
1330
+ api.on("message_sent", async (event) => {
1241
1331
  try {
1242
- api.registerCommand?.({
1243
- name: "agentvault",
1244
- description: "Show AgentVault encrypted channel status",
1245
- handler: () => {
1246
- const statuses = [];
1247
- if (_channels.size === 0) {
1248
- statuses.push("AgentVault: no active channels");
1249
- }
1250
- else {
1251
- for (const [id, ch] of _channels) {
1252
- statuses.push(`AgentVault [${id}]: ${ch.state} | sessions: ${ch.sessionCount} | encrypted: yes`);
1253
- }
1254
- }
1255
- return { text: statuses.join("\n") };
1256
- },
1257
- });
1332
+ const ch = _channels.values().next().value;
1333
+ if (!ch?.telemetry) return;
1334
+ ch.telemetry.reportCustomSpan({
1335
+ traceId: randomBytes(16).toString("hex"),
1336
+ spanId: randomBytes(8).toString("hex"),
1337
+ name: "agentvault.message.delivered",
1338
+ kind: "internal",
1339
+ startTime: event.timestamp,
1340
+ endTime: event.timestamp + 1,
1341
+ attributes: {
1342
+ "agentvault.message.id": event.messageId,
1343
+ "agentvault.delivery.status": event.deliveryStatus,
1344
+ "agentvault.channel.id": event.channelId
1345
+ },
1346
+ status: { code: event.deliveryStatus === "failed" ? 2 : 1 }
1347
+ });
1348
+ } catch {
1258
1349
  }
1259
- catch { /* registerCommand may not be available in older OpenClaw versions */ }
1260
- },
1350
+ });
1351
+ api.on("session_start", async (event) => {
1352
+ try {
1353
+ const ch = _channels.values().next().value;
1354
+ if (!ch?.telemetry) return;
1355
+ ch.telemetry.reportCustomSpan({
1356
+ traceId: randomBytes(16).toString("hex"),
1357
+ spanId: randomBytes(8).toString("hex"),
1358
+ name: "agentvault.session.start",
1359
+ kind: "internal",
1360
+ startTime: event.timestamp,
1361
+ endTime: event.timestamp + 1,
1362
+ attributes: {
1363
+ "agentvault.session.key": event.sessionKey,
1364
+ "agentvault.agent.id": event.agentId
1365
+ },
1366
+ status: { code: 1 }
1367
+ });
1368
+ } catch {
1369
+ }
1370
+ });
1371
+ api.on("session_end", async (event) => {
1372
+ try {
1373
+ const ch = _channels.values().next().value;
1374
+ if (!ch?.telemetry) return;
1375
+ ch.telemetry.reportCustomSpan({
1376
+ traceId: randomBytes(16).toString("hex"),
1377
+ spanId: randomBytes(8).toString("hex"),
1378
+ name: "agentvault.session.end",
1379
+ kind: "internal",
1380
+ startTime: event.timestamp,
1381
+ endTime: event.timestamp + 1,
1382
+ attributes: {
1383
+ "agentvault.session.key": event.sessionKey,
1384
+ "agentvault.agent.id": event.agentId,
1385
+ ...event.reason ? { "agentvault.session.end_reason": event.reason } : {}
1386
+ },
1387
+ status: { code: 1 }
1388
+ });
1389
+ } catch {
1390
+ }
1391
+ });
1392
+ }
1393
+ Promise.resolve().then(() => (init_openclaw_compat(), openclaw_compat_exports)).then(async ({ onAgentEvent: onAgentEvent2, onSessionTranscriptUpdate: onSessionTranscriptUpdate2 }) => {
1394
+ onAgentEvent2((event) => {
1395
+ try {
1396
+ const ch = _channels.values().next().value;
1397
+ if (!ch?.telemetry) return;
1398
+ ch.telemetry.reportCustomSpan({
1399
+ traceId: randomBytes(16).toString("hex"),
1400
+ spanId: randomBytes(8).toString("hex"),
1401
+ name: `ai.agent.event.${event.type}`,
1402
+ kind: "internal",
1403
+ startTime: event.timestamp,
1404
+ endTime: event.timestamp + 1,
1405
+ attributes: {
1406
+ "ai.agent.event.type": event.type,
1407
+ "ai.agent.id": event.agentId,
1408
+ ...event.data ? Object.fromEntries(
1409
+ Object.entries(event.data).map(([k, v]) => [`ai.agent.event.${k}`, String(v)])
1410
+ ) : {}
1411
+ },
1412
+ status: { code: 1 }
1413
+ });
1414
+ } catch {
1415
+ }
1416
+ }).catch(() => {
1417
+ });
1418
+ onSessionTranscriptUpdate2((update) => {
1419
+ try {
1420
+ const ch = _channels.values().next().value;
1421
+ if (!ch?.telemetry) return;
1422
+ ch.telemetry.reportCustomSpan({
1423
+ traceId: randomBytes(16).toString("hex"),
1424
+ spanId: randomBytes(8).toString("hex"),
1425
+ name: "ai.agent.transcript.update",
1426
+ kind: "internal",
1427
+ startTime: update.timestamp,
1428
+ endTime: update.timestamp + 1,
1429
+ attributes: {
1430
+ "agentvault.session.key": update.sessionKey,
1431
+ "ai.agent.transcript.role": update.role ?? "unknown",
1432
+ "ai.agent.transcript.delta_chars": update.delta.length
1433
+ },
1434
+ status: { code: 1 }
1435
+ });
1436
+ } catch {
1437
+ }
1438
+ }).catch(() => {
1439
+ });
1440
+ }).catch(() => {
1441
+ });
1442
+ try {
1443
+ api.registerTool?.({
1444
+ name: "agentvault_status",
1445
+ description: "Check the AgentVault encrypted channel status, connection state, and session count.",
1446
+ parameters: {
1447
+ type: "object",
1448
+ properties: {
1449
+ accountId: {
1450
+ type: "string",
1451
+ description: "Account ID to check (default: 'default')"
1452
+ }
1453
+ }
1454
+ },
1455
+ execute: async (_id, params) => {
1456
+ const id = params.accountId ?? "default";
1457
+ const ch = _channels.get(id);
1458
+ if (!ch) {
1459
+ return { content: [{ type: "text", text: JSON.stringify({ connected: false, error: "Channel not started" }) }] };
1460
+ }
1461
+ return {
1462
+ content: [{
1463
+ type: "text",
1464
+ text: JSON.stringify({
1465
+ connected: ch.state === "ready",
1466
+ state: ch.state,
1467
+ deviceId: ch.deviceId,
1468
+ sessions: ch.sessionCount,
1469
+ encrypted: true
1470
+ })
1471
+ }]
1472
+ };
1473
+ }
1474
+ }, { optional: true });
1475
+ } catch {
1476
+ }
1477
+ try {
1478
+ api.registerCommand?.({
1479
+ name: "agentvault",
1480
+ description: "Show AgentVault encrypted channel status",
1481
+ handler: () => {
1482
+ const statuses = [];
1483
+ if (_channels.size === 0) {
1484
+ statuses.push("AgentVault: no active channels");
1485
+ } else {
1486
+ for (const [id, ch] of _channels) {
1487
+ statuses.push(`AgentVault [${id}]: ${ch.state} | sessions: ${ch.sessionCount} | encrypted: yes`);
1488
+ }
1489
+ }
1490
+ return { text: statuses.join("\n") };
1491
+ }
1492
+ });
1493
+ } catch {
1494
+ }
1495
+ }
1496
+ };
1497
+ export {
1498
+ _parseMentions,
1499
+ _shouldProcessRoomMessage,
1500
+ _stripMentions,
1501
+ openclaw_entry_default as default,
1502
+ isUsingManagedRoutes
1261
1503
  };
1262
- //# sourceMappingURL=openclaw-entry.js.map
1504
+ //# sourceMappingURL=openclaw-entry.js.map