@masons/agent-network 0.3.9 → 0.4.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAa9D,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,UAAU,oBAAoB,CAAC,CAAC;IAC9B,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAAC;IACvD,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;IACpE,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;IACjE,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;CAC/D;AAED,UAAU,sBAAsB;IAC9B,EAAE,EAAE,OAAO,CAAC;CACb;AAED,UAAU,sBAAsB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,sBAAsB;IAC9B,YAAY,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC9C,QAAQ,CAAC,CAAC,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACzE;AAED,UAAU,qBAAqB,CAAC,CAAC,GAAG,OAAO;IACzC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAsDD,UAAU,qBAAqB,CAAC,CAAC,GAAG,OAAO;IACzC,YAAY,CAAC,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/D,WAAW,CAAC,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,UAAU,yBAAyB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,YAAY,EAAE,mBAAmB,CAAC;IAClC,MAAM,EAAE,oBAAoB,CAAC,mBAAmB,CAAC,CAAC;IAClD,QAAQ,EAAE,sBAAsB,CAAC;IACjC,OAAO,EAAE,qBAAqB,CAAC,mBAAmB,CAAC,CAAC;CACrD;AA0CD,eAAO,MAAM,mBAAmB,EAAE,yBAqWjC,CAAC"}
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAc9D,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,UAAU,oBAAoB,CAAC,CAAC;IAC9B,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAAC;IACvD,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;IACpE,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;IACjE,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;CAC/D;AAED,UAAU,sBAAsB;IAC9B,EAAE,EAAE,OAAO,CAAC;CACb;AAED,UAAU,sBAAsB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,sBAAsB;IAC9B,YAAY,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC9C,QAAQ,CAAC,CAAC,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACzE;AAED,UAAU,qBAAqB,CAAC,CAAC,GAAG,OAAO;IACzC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAsDD,UAAU,qBAAqB,CAAC,CAAC,GAAG,OAAO;IACzC,YAAY,CAAC,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/D,WAAW,CAAC,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,UAAU,yBAAyB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,YAAY,EAAE,mBAAmB,CAAC;IAClC,MAAM,EAAE,oBAAoB,CAAC,mBAAmB,CAAC,CAAC;IAClD,QAAQ,EAAE,sBAAsB,CAAC;IACjC,OAAO,EAAE,qBAAqB,CAAC,mBAAmB,CAAC,CAAC;CACrD;AAqDD,eAAO,MAAM,mBAAmB,EAAE,yBA4VjC,CAAC"}
package/dist/channel.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import createDebug from "debug";
3
- import { clearConnectorClient, extractNetworkConfig, initConnectorClient, initToolConfig, requirePluginRuntime, } from "./config.js";
3
+ import { clearConnectorClient, clearConversationManager, extractNetworkConfig, initConnectorClient, initConversationManager, initToolConfig, requirePluginRuntime, } from "./config.js";
4
4
  import { ConnectorClient } from "./connector-client.js";
5
+ import { ConversationManager } from "./conversation-manager.js";
5
6
  import { checkForUpdate } from "./update-check.js";
6
7
  const dbg = createDebug("agent-network:channel");
7
8
  // --- Sender identity ---
@@ -25,6 +26,20 @@ function deriveSenderName(event) {
25
26
  const parts = trimmed.split("/");
26
27
  return parts[parts.length - 1] || from;
27
28
  }
29
+ /**
30
+ * Extract the Connector host from a connectorUrl.
31
+ * `wss://preview.masons.ai/gw` -> `preview.masons.ai`
32
+ */
33
+ function extractConnectorHost(connectorUrl) {
34
+ try {
35
+ const url = new URL(connectorUrl);
36
+ return url.hostname;
37
+ }
38
+ catch {
39
+ // Fallback: strip protocol and path
40
+ return connectorUrl.replace(/^wss?:\/\//, "").replace(/\/.*$/, "");
41
+ }
42
+ }
28
43
  const accounts = new Map();
29
44
  // --- Channel Plugin ---
30
45
  export const agentNetworkChannel = {
@@ -84,25 +99,14 @@ export const agentNetworkChannel = {
84
99
  outbound: {
85
100
  deliveryMode: "direct",
86
101
  async sendText(ctx) {
87
- // ctx.recipient is now an MSTP address (senderId = address from deriveSenderId).
88
- // Resolve to the active sessionId via the reverse index.
102
+ // ctx.recipient is the MSTP address or sessionId fallback (senderId from inbound dispatch).
103
+ // ConversationManager.send() handles session management transparently.
89
104
  const state = accounts.get(ctx.accountId);
90
105
  if (!state)
91
106
  return { ok: false };
92
- let sessionId = state.activeSession.get(ctx.recipient);
93
- // Fallback: ctx.recipient might be a raw sessionId (direct MSTP client
94
- // without metadata.from deriveSenderId fell back to sessionId).
95
- if (!sessionId && state.sessions.has(ctx.recipient)) {
96
- sessionId = ctx.recipient;
97
- }
98
- if (!sessionId || !state.sessions.has(sessionId)) {
99
- return { ok: false };
100
- }
101
- const sent = state.client.sendMessage(sessionId, ctx.text, {
102
- contentType: "text",
103
- });
104
- dbg("sendText recipient=%s sessionId=%s contentLength=%d sent=%s", ctx.recipient, sessionId, ctx.text.length, sent);
105
- return { ok: sent };
107
+ const result = await state.conversationManager.send(ctx.recipient, ctx.text);
108
+ dbg("sendText recipient=%s status=%s contentLength=%d", ctx.recipient, result.status, ctx.text.length);
109
+ return { ok: result.status === "sent" };
106
110
  },
107
111
  },
108
112
  gateway: {
@@ -110,10 +114,11 @@ export const agentNetworkChannel = {
110
114
  dbg("starting account %s", ctx.accountId);
111
115
  const { connectorUrl, token } = ctx.account;
112
116
  const client = new ConnectorClient(connectorUrl, token);
117
+ const connectorHost = extractConnectorHost(connectorUrl);
118
+ const conversationManager = new ConversationManager(client, connectorHost);
113
119
  const state = {
114
120
  client,
115
- sessions: new Map(),
116
- activeSession: new Map(),
121
+ conversationManager,
117
122
  };
118
123
  accounts.set(ctx.accountId, state);
119
124
  // --- Inbound message routing (4-step channelRuntime dispatch) ---
@@ -143,19 +148,22 @@ export const agentNetworkChannel = {
143
148
  dbg("channelRuntime not available, dropping message sessionId=%s", event.sessionId);
144
149
  return;
145
150
  }
146
- // Resolve sender identity from session state, NOT from per-message metadata.
147
- // metadata.from is reliably present on SESSION_CREATED (Connector sets it
148
- // server-side) but NOT on MESSAGE_RECEIVED (per-message metadata is
149
- // pass-through from the remote plugin, which doesn't include `from`).
150
- // The session_created handler already stored the remoteAddress.
151
- const sessionMeta = state.sessions.get(event.sessionId);
152
- const senderId = sessionMeta?.remoteAddress ?? event.sessionId;
153
- // Derive sender name from the resolved address, not per-message metadata.
154
- // Build a synthetic event with the session's address for deriveSenderName.
155
- const senderName = sessionMeta?.remoteAddress
151
+ // Resolve sender identity via ConversationManager.
152
+ // The session_created handler registered the inbound session with
153
+ // the ConversationManager, so we can look up the contact handle.
154
+ const senderAddress = conversationManager.getAddressBySessionId(event.sessionId);
155
+ // Determine if the address is a real MSTP address or a sessionId fallback.
156
+ const isRealAddress = typeof senderAddress === "string" &&
157
+ (senderAddress.startsWith("mstps://") ||
158
+ senderAddress.startsWith("mstp://"));
159
+ // senderId for OpenClaw routing: use MSTP address if available,
160
+ // fall back to sessionId for unknown sessions / direct MSTP clients.
161
+ const senderId = isRealAddress ? senderAddress : event.sessionId;
162
+ // Derive sender name from the MSTP address, or "Unknown Agent" for fallback.
163
+ const senderName = isRealAddress
156
164
  ? deriveSenderName({
157
165
  ...event,
158
- metadata: { ...event.metadata, from: sessionMeta.remoteAddress },
166
+ metadata: { ...event.metadata, from: senderAddress },
159
167
  })
160
168
  : deriveSenderName(event);
161
169
  try {
@@ -205,19 +213,11 @@ export const agentNetworkChannel = {
205
213
  const text = payload.text;
206
214
  if (!text)
207
215
  return;
208
- // senderId is now an MSTP address. Resolve to sessionId.
209
- let deliverSessionId = state.activeSession.get(senderId);
210
- // Fallback: senderId might be a raw sessionId (direct client).
211
- if (!deliverSessionId && state.sessions.has(senderId)) {
212
- deliverSessionId = senderId;
213
- }
214
- // Session may have ended while LLM was processing.
215
- if (!deliverSessionId || !state.sessions.has(deliverSessionId))
216
- return;
217
- const sent = state.client.sendMessage(deliverSessionId, text, {
218
- contentType: "text",
219
- });
220
- dbg("deliver reply address=%s sessionId=%s contentLength=%d sent=%s", senderId, deliverSessionId, text.length, sent);
216
+ // Use ConversationManager to send the reply.
217
+ // Pass the MSTP address (senderId) directly so the CM
218
+ // can match the registered conversation entry.
219
+ const deliverResult = await conversationManager.send(senderId, text);
220
+ dbg("deliver reply senderId=%s status=%s contentLength=%d", senderId, deliverResult.status, text.length);
221
221
  },
222
222
  onError: (err) => {
223
223
  console.error(`[agent-network:${ctx.accountId}] dispatch error:`, err);
@@ -229,45 +229,37 @@ export const agentNetworkChannel = {
229
229
  console.error(`[agent-network:${ctx.accountId}] inbound dispatch failed sessionId=${event.sessionId}:`, err);
230
230
  }
231
231
  });
232
- // --- Session lifecycle ---
232
+ // --- Inbound session registration ---
233
+ // When a remote agent creates a session (direction=inbound),
234
+ // register it with the ConversationManager.
233
235
  client.on("session_created", (event) => {
234
236
  dbg("session created sessionId=%s direction=%s", event.sessionId, event.direction);
235
- const from = event.metadata?.from;
236
- const remoteAddress = typeof from === "string" && from.length > 0 ? from : null;
237
- state.sessions.set(event.sessionId, { remoteAddress });
238
- // Update reverse index: this address is now reachable via this session.
239
- // Last-write-wins: if a new session arrives from the same address,
240
- // replies go to the newest session (same as XMPP resource binding).
241
- if (remoteAddress) {
242
- state.activeSession.set(remoteAddress, event.sessionId);
243
- }
244
- });
245
- client.on("session_ended", (event) => {
246
- dbg("session ended sessionId=%s", event.sessionId);
247
- const sessionMeta = state.sessions.get(event.sessionId);
248
- state.sessions.delete(event.sessionId);
249
- // Only clear reverse index if this session was the active one.
250
- // If a newer session replaced it, the index already points elsewhere.
251
- if (sessionMeta?.remoteAddress) {
252
- const currentActive = state.activeSession.get(sessionMeta.remoteAddress);
253
- if (currentActive === event.sessionId) {
254
- state.activeSession.delete(sessionMeta.remoteAddress);
237
+ if (event.direction === "inbound") {
238
+ const from = event.metadata?.from;
239
+ const remoteAddress = typeof from === "string" && from.length > 0 ? from : null;
240
+ if (remoteAddress) {
241
+ const contact = deriveSenderName(event);
242
+ conversationManager.registerInbound(event.sessionId, contact, remoteAddress);
243
+ }
244
+ else {
245
+ // No address metadata — register with sessionId as fallback
246
+ conversationManager.registerInbound(event.sessionId, event.sessionId, event.sessionId);
255
247
  }
256
248
  }
249
+ // Outbound sessions are handled by SessionLifecycle internally
250
+ // (matched by requestId in the createSession flow).
257
251
  });
252
+ // --- Session ended + disconnect ---
253
+ // SessionLifecycle handles session state transitions (session_ended, disconnected)
254
+ // via its own event subscriptions. No CM calls needed here.
258
255
  // --- Error handling ---
259
256
  // Must register before connect() — unhandled "error" events crash the process.
260
257
  client.on("error", (err) => {
261
258
  console.error(`[agent-network:${ctx.accountId}]`, err.message);
262
259
  });
263
- // --- Disconnect cleanup ---
264
- client.on("disconnected", () => {
265
- dbg("disconnected, cleared %d sessions", state.sessions.size);
266
- state.sessions.clear();
267
- state.activeSession.clear();
268
- });
269
260
  // --- Abort signal ---
270
261
  ctx.abortSignal.addEventListener("abort", () => {
262
+ clearConversationManager();
271
263
  clearConnectorClient(); // idempotent — stopAccount() may also call
272
264
  client.disconnect();
273
265
  }, { once: true });
@@ -280,8 +272,9 @@ export const agentNetworkChannel = {
280
272
  // --- Connect ---
281
273
  await client.connect();
282
274
  dbg("connected account=%s", ctx.accountId);
283
- // --- Expose client to tools (only after WS is connected) ---
275
+ // --- Expose client and conversation manager to tools (only after WS is connected) ---
284
276
  initConnectorClient(client);
277
+ initConversationManager(conversationManager);
285
278
  // Park the Promise — DO NOT resolve/return.
286
279
  // Resolution signals "account stopped" to OpenClaw's orchestrator,
287
280
  // triggering auto-restart with exponential backoff (crash-loop).
@@ -299,11 +292,10 @@ export const agentNetworkChannel = {
299
292
  dbg("stopping account %s", ctx.accountId);
300
293
  const state = accounts.get(ctx.accountId);
301
294
  if (state) {
295
+ clearConversationManager();
302
296
  clearConnectorClient();
303
297
  state.client.removeAllListeners();
304
298
  state.client.disconnect();
305
- state.sessions.clear();
306
- state.activeSession.clear();
307
299
  accounts.delete(ctx.accountId);
308
300
  }
309
301
  },
package/dist/config.d.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  *
16
16
  */
17
17
  import type { ConnectorClient } from "./connector-client.js";
18
+ import type { ConversationManager } from "./conversation-manager.js";
18
19
  import { type PlatformClientConfig } from "./platform-client.js";
19
20
  /**
20
21
  * Extract the `channels.agent-network` section from the full OpenClaw config.
@@ -49,6 +50,24 @@ export declare function initConnectorClient(client: ConnectorClient): void;
49
50
  * operating on a disconnected client.
50
51
  */
51
52
  export declare function clearConnectorClient(): void;
53
+ /**
54
+ * Inject the live ConversationManager after WebSocket connection is established.
55
+ *
56
+ * Called by `startAccount()` AFTER `client.connect()` resolves — ensures
57
+ * tools always get a manager backed by a connected client.
58
+ */
59
+ export declare function initConversationManager(manager: ConversationManager): void;
60
+ /**
61
+ * Clear the stored ConversationManager reference.
62
+ *
63
+ * Called by `stopAccount()` before disconnecting — prevents tools from
64
+ * operating on a stale manager.
65
+ */
66
+ export declare function clearConversationManager(): void;
67
+ /**
68
+ * Get the live ConversationManager. Throws if not initialized.
69
+ */
70
+ export declare function requireConversationManager(): ConversationManager;
52
71
  /**
53
72
  * Store the OpenClaw PluginRuntime for inbound message dispatch.
54
73
  *
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,sBAAsB,CAAC;AA2B9B;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAMhC;AAMD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAsBjE;AAMD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAOjE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAMD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAExD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAO9C;AAMD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,oBAAoB,CAE5D;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAKtC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI,eAAe,CAOxD;AAMD,+EAA+E;AAC/E,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAgDD,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,kBAAkB,EACzB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CA2Bf;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKrE;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAQvD;AAMD;;;;;GAKG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAKvD;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAKzD;AAMD,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,cAAc,EAAE,OAAO,CAAC;IACxB,wDAAwD;IACxD,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,4EAA4E;IAC5E,YAAY,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,YAAY,CAAC,CA2BhE;AAMD,uDAAuD;AACvD,wBAAgB,gBAAgB,IAAI,IAAI,CAMvC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,sBAAsB,CAAC;AA4B9B;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAMhC;AAMD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAsBjE;AAMD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAOjE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAMD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAE1E;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAED;;GAEG;AACH,wBAAgB,0BAA0B,IAAI,mBAAmB,CAOhE;AAMD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAExD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAO9C;AAMD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,oBAAoB,CAE5D;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAKtC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI,eAAe,CAOxD;AAMD,+EAA+E;AAC/E,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAgDD,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,kBAAkB,EACzB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CA2Bf;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKrE;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAQvD;AAMD;;;;;GAKG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAKvD;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAKzD;AAMD,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,cAAc,EAAE,OAAO,CAAC;IACxB,wDAAwD;IACxD,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,4EAA4E;IAC5E,YAAY,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,YAAY,CAAC,CA2BhE;AAMD,uDAAuD;AACvD,wBAAgB,gBAAgB,IAAI,IAAI,CAOvC"}
package/dist/config.js CHANGED
@@ -32,6 +32,7 @@ let storedApiKey = null;
32
32
  let storedPendingTarget = null;
33
33
  // Runtime state (live connection — set by startAccount, cleared by stopAccount)
34
34
  let storedConnectorClient = null;
35
+ let storedConversationManager = null;
35
36
  // PluginRuntime state (set by register() in plugin.ts, used by channel.ts)
36
37
  // Typed as unknown to avoid circular imports — channel.ts casts to PluginRuntime.
37
38
  let storedPluginRuntime = null;
@@ -109,6 +110,36 @@ export function clearConnectorClient() {
109
110
  storedConnectorClient = null;
110
111
  }
111
112
  // ---------------------------------------------------------------------------
113
+ // ConversationManager injection (called by startAccount() after connect)
114
+ // ---------------------------------------------------------------------------
115
+ /**
116
+ * Inject the live ConversationManager after WebSocket connection is established.
117
+ *
118
+ * Called by `startAccount()` AFTER `client.connect()` resolves — ensures
119
+ * tools always get a manager backed by a connected client.
120
+ */
121
+ export function initConversationManager(manager) {
122
+ storedConversationManager = manager;
123
+ }
124
+ /**
125
+ * Clear the stored ConversationManager reference.
126
+ *
127
+ * Called by `stopAccount()` before disconnecting — prevents tools from
128
+ * operating on a stale manager.
129
+ */
130
+ export function clearConversationManager() {
131
+ storedConversationManager = null;
132
+ }
133
+ /**
134
+ * Get the live ConversationManager. Throws if not initialized.
135
+ */
136
+ export function requireConversationManager() {
137
+ if (!storedConversationManager) {
138
+ throw new Error("Not connected to the agent network. Wait for the connection to be established.");
139
+ }
140
+ return storedConversationManager;
141
+ }
142
+ // ---------------------------------------------------------------------------
112
143
  // PluginRuntime injection (called by register() in plugin.ts)
113
144
  // ---------------------------------------------------------------------------
114
145
  /**
@@ -349,5 +380,6 @@ export function _resetForTesting() {
349
380
  storedApiKey = null;
350
381
  storedPendingTarget = null;
351
382
  storedConnectorClient = null;
383
+ storedConversationManager = null;
352
384
  storedPluginRuntime = null;
353
385
  }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Conversation Manager — maps contacts to conversations.
3
+ *
4
+ * Owns the identity-based API that the Tool Interface and Inbound Dispatcher
5
+ * call. Internally delegates session management to SessionLifecycle.
6
+ *
7
+ * Contact resolution: handle -> `mstps://${connectorHost}/${handle}`
8
+ * Internal key: MSTP address (same as SessionLifecycle).
9
+ * One session per contact, last-write-wins (D2).
10
+ * In-memory only, no persistence (D3).
11
+ *
12
+ * @see docs/openclaw/session-abstraction-system-design.md §4.1
13
+ */
14
+ import type { ConnectorClient } from "./connector-client.js";
15
+ import { SessionLifecycle } from "./session-lifecycle.js";
16
+ export interface SendResult {
17
+ status: "sent" | "failed";
18
+ error?: string;
19
+ }
20
+ export interface ConversationEntry {
21
+ contact: string;
22
+ address: string;
23
+ lastMessageAt: number;
24
+ initiatedBy: "local" | "remote";
25
+ }
26
+ export interface ConversationSummary {
27
+ contact: string;
28
+ address: string;
29
+ active: boolean;
30
+ lastMessageAt: number;
31
+ initiatedBy: "local" | "remote";
32
+ }
33
+ export declare class ConversationManager {
34
+ readonly sessionLifecycle: SessionLifecycle;
35
+ private readonly client;
36
+ private readonly connectorHost;
37
+ /** Conversations keyed by MSTP address */
38
+ private readonly conversations;
39
+ constructor(client: ConnectorClient, connectorHost: string);
40
+ /**
41
+ * Send a message to a contact. Session management is transparent.
42
+ * Creates a session automatically if none exists (D1 — sync-but-transparent).
43
+ *
44
+ * @param contact - Handle or MSTP address of the target agent.
45
+ * @param content - Message content to send.
46
+ */
47
+ send(contact: string, content: string): Promise<SendResult>;
48
+ /**
49
+ * Register an inbound session (remote agent initiated).
50
+ * Called by the channel adapter on SESSION_CREATED with direction=inbound.
51
+ */
52
+ registerInbound(sessionId: string, contact: string, address: string): void;
53
+ /**
54
+ * Resolve a sessionId to a contact handle.
55
+ * Used by the inbound dispatcher to derive sender identity from a message event.
56
+ */
57
+ getContactBySessionId(sessionId: string): string | undefined;
58
+ /**
59
+ * Resolve a sessionId to an MSTP address.
60
+ * Used by the channel adapter for reverse lookup.
61
+ */
62
+ getAddressBySessionId(sessionId: string): string | undefined;
63
+ /**
64
+ * List all conversations with metadata.
65
+ */
66
+ listConversations(): ConversationSummary[];
67
+ /**
68
+ * End the conversation with a contact.
69
+ * Closes the active session if one exists.
70
+ */
71
+ endConversation(contact: string): void;
72
+ /**
73
+ * Resolve a contact (handle or MSTP address) to an MSTP address.
74
+ *
75
+ * Resolution order:
76
+ * 1. If `contact` is already an MSTP address (starts with mstps:// or mstp://),
77
+ * return it as-is.
78
+ * 2. If `contact` is already a key in the conversations map (e.g., a sessionId
79
+ * used as fallback address for direct MSTP clients), return it as-is.
80
+ * 3. Otherwise construct: `mstps://${connectorHost}/${handle}`.
81
+ */
82
+ resolveAddress(contact: string): string;
83
+ /**
84
+ * Extract handle from an MSTP address.
85
+ * `mstps://preview.masons.ai/alice` -> `alice`
86
+ */
87
+ extractHandle(address: string): string;
88
+ /** @internal Reset for test isolation. */
89
+ _resetForTesting(): void;
90
+ }
91
+ //# sourceMappingURL=conversation-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-manager.d.ts","sourceRoot":"","sources":["../src/conversation-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAQ1D,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,GAAG,QAAQ,CAAC;CACjC;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,GAAG,QAAQ,CAAC;CACjC;AAMD,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IAEvC,0CAA0C;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAwC;gBAE1D,MAAM,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM;IAU1D;;;;;;OAMG;IACG,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAgDjE;;;OAGG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAyB1E;;;OAGG;IACH,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAQ5D;;;OAGG;IACH,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI5D;;OAEG;IACH,iBAAiB,IAAI,mBAAmB,EAAE;IAe1C;;;OAGG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAkBtC;;;;;;;;;OASG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAYvC;;;OAGG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAUtC,0CAA0C;IAC1C,gBAAgB,IAAI,IAAI;CAIzB"}
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Conversation Manager — maps contacts to conversations.
3
+ *
4
+ * Owns the identity-based API that the Tool Interface and Inbound Dispatcher
5
+ * call. Internally delegates session management to SessionLifecycle.
6
+ *
7
+ * Contact resolution: handle -> `mstps://${connectorHost}/${handle}`
8
+ * Internal key: MSTP address (same as SessionLifecycle).
9
+ * One session per contact, last-write-wins (D2).
10
+ * In-memory only, no persistence (D3).
11
+ *
12
+ * @see docs/openclaw/session-abstraction-system-design.md §4.1
13
+ */
14
+ import createDebug from "debug";
15
+ import { SessionLifecycle } from "./session-lifecycle.js";
16
+ const dbg = createDebug("agent-network:conversation-manager");
17
+ // ---------------------------------------------------------------------------
18
+ // ConversationManager
19
+ // ---------------------------------------------------------------------------
20
+ export class ConversationManager {
21
+ sessionLifecycle;
22
+ client;
23
+ connectorHost;
24
+ /** Conversations keyed by MSTP address */
25
+ conversations = new Map();
26
+ constructor(client, connectorHost) {
27
+ this.client = client;
28
+ this.connectorHost = connectorHost;
29
+ this.sessionLifecycle = new SessionLifecycle(client);
30
+ }
31
+ // -------------------------------------------------------------------------
32
+ // Public API — identity-based
33
+ // -------------------------------------------------------------------------
34
+ /**
35
+ * Send a message to a contact. Session management is transparent.
36
+ * Creates a session automatically if none exists (D1 — sync-but-transparent).
37
+ *
38
+ * @param contact - Handle or MSTP address of the target agent.
39
+ * @param content - Message content to send.
40
+ */
41
+ async send(contact, content) {
42
+ const address = this.resolveAddress(contact);
43
+ // Ensure conversation entry exists
44
+ if (!this.conversations.has(address)) {
45
+ this.conversations.set(address, {
46
+ contact: this.extractHandle(address),
47
+ address,
48
+ lastMessageAt: Date.now(),
49
+ initiatedBy: "local",
50
+ });
51
+ }
52
+ try {
53
+ const sessionId = await this.sessionLifecycle.ensureSession(address);
54
+ const sent = this.client.sendMessage(sessionId, content, {
55
+ contentType: "text",
56
+ });
57
+ if (!sent) {
58
+ return {
59
+ status: "failed",
60
+ error: "Failed to send message. The network connection may be temporarily unavailable.",
61
+ };
62
+ }
63
+ // Update lastMessageAt
64
+ const entry = this.conversations.get(address);
65
+ if (entry) {
66
+ entry.lastMessageAt = Date.now();
67
+ }
68
+ dbg("send contact=%s address=%s sessionId=%s", contact, address, sessionId);
69
+ return { status: "sent" };
70
+ }
71
+ catch (err) {
72
+ const message = err instanceof Error ? err.message : "Unknown error";
73
+ dbg("send failed contact=%s error=%s", contact, message);
74
+ return { status: "failed", error: message };
75
+ }
76
+ }
77
+ /**
78
+ * Register an inbound session (remote agent initiated).
79
+ * Called by the channel adapter on SESSION_CREATED with direction=inbound.
80
+ */
81
+ registerInbound(sessionId, contact, address) {
82
+ this.sessionLifecycle.registerInbound(sessionId, address);
83
+ if (!this.conversations.has(address)) {
84
+ this.conversations.set(address, {
85
+ contact,
86
+ address,
87
+ lastMessageAt: Date.now(),
88
+ initiatedBy: "remote",
89
+ });
90
+ }
91
+ else {
92
+ const entry = this.conversations.get(address);
93
+ if (entry) {
94
+ entry.lastMessageAt = Date.now();
95
+ }
96
+ }
97
+ dbg("registerInbound sessionId=%s contact=%s address=%s", sessionId, contact, address);
98
+ }
99
+ /**
100
+ * Resolve a sessionId to a contact handle.
101
+ * Used by the inbound dispatcher to derive sender identity from a message event.
102
+ */
103
+ getContactBySessionId(sessionId) {
104
+ const address = this.sessionLifecycle.getAddressBySessionId(sessionId);
105
+ if (!address)
106
+ return undefined;
107
+ const entry = this.conversations.get(address);
108
+ return entry?.contact;
109
+ }
110
+ /**
111
+ * Resolve a sessionId to an MSTP address.
112
+ * Used by the channel adapter for reverse lookup.
113
+ */
114
+ getAddressBySessionId(sessionId) {
115
+ return this.sessionLifecycle.getAddressBySessionId(sessionId);
116
+ }
117
+ /**
118
+ * List all conversations with metadata.
119
+ */
120
+ listConversations() {
121
+ const result = [];
122
+ for (const [address, entry] of this.conversations) {
123
+ const sessionId = this.sessionLifecycle.getSession(address);
124
+ result.push({
125
+ contact: entry.contact,
126
+ address: entry.address,
127
+ active: sessionId !== null,
128
+ lastMessageAt: entry.lastMessageAt,
129
+ initiatedBy: entry.initiatedBy,
130
+ });
131
+ }
132
+ return result;
133
+ }
134
+ /**
135
+ * End the conversation with a contact.
136
+ * Closes the active session if one exists.
137
+ */
138
+ endConversation(contact) {
139
+ const address = this.resolveAddress(contact);
140
+ this.sessionLifecycle.closeSession(address);
141
+ this.conversations.delete(address);
142
+ dbg("endConversation contact=%s address=%s", contact, address);
143
+ }
144
+ // NOTE: handleDisconnected() and handleSessionEnded() are NOT needed here.
145
+ // SessionLifecycle owns session state transitions via its own event subscriptions
146
+ // (session_ended, disconnected). Conversations persist across both events —
147
+ // sessions are recreated transparently on the next send().
148
+ // -------------------------------------------------------------------------
149
+ // Contact resolution
150
+ // -------------------------------------------------------------------------
151
+ /**
152
+ * Resolve a contact (handle or MSTP address) to an MSTP address.
153
+ *
154
+ * Resolution order:
155
+ * 1. If `contact` is already an MSTP address (starts with mstps:// or mstp://),
156
+ * return it as-is.
157
+ * 2. If `contact` is already a key in the conversations map (e.g., a sessionId
158
+ * used as fallback address for direct MSTP clients), return it as-is.
159
+ * 3. Otherwise construct: `mstps://${connectorHost}/${handle}`.
160
+ */
161
+ resolveAddress(contact) {
162
+ if (contact.startsWith("mstps://") || contact.startsWith("mstp://")) {
163
+ return contact;
164
+ }
165
+ // Check if the contact string is already a registered conversation key
166
+ // (handles the fallback case where sessionId is used as address).
167
+ if (this.conversations.has(contact)) {
168
+ return contact;
169
+ }
170
+ return `mstps://${this.connectorHost}/${contact}`;
171
+ }
172
+ /**
173
+ * Extract handle from an MSTP address.
174
+ * `mstps://preview.masons.ai/alice` -> `alice`
175
+ */
176
+ extractHandle(address) {
177
+ const trimmed = address.replace(/\/+$/, "");
178
+ const parts = trimmed.split("/");
179
+ return parts[parts.length - 1] || address;
180
+ }
181
+ // -------------------------------------------------------------------------
182
+ // Test helpers
183
+ // -------------------------------------------------------------------------
184
+ /** @internal Reset for test isolation. */
185
+ _resetForTesting() {
186
+ this.conversations.clear();
187
+ this.sessionLifecycle._resetForTesting();
188
+ }
189
+ }
package/dist/plugin.js CHANGED
@@ -115,8 +115,8 @@ const plugin = {
115
115
  "masons_send_connection_request (connect to another agent), " +
116
116
  "masons_accept_request (accept an incoming connection request), " +
117
117
  "masons_decline_request (decline an incoming connection request), " +
118
- "masons_create_session (start a conversation), " +
119
- "masons_send_message (send a message in a session), masons_end_session (end a conversation). " +
118
+ "masons_send_message (send a message — sessions are automatic), " +
119
+ "masons_end_conversation (end a conversation). " +
120
120
  "Always try these tools first for network operations. " +
121
121
  "If a tool call fails or a tool is not available, report the error to the user — do not silently work around it.";
122
122
  }