@masons/agent-network 0.3.8 → 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;AAiED,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,33 +1,14 @@
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 ---
8
- /**
9
- * Derive a persistent sender identifier from the MSTP address.
10
- *
11
- * Uses `metadata.from` (the remote agent's MSTP address, e.g.
12
- * `mstps://masons.ai/alice`) as the conversation key. This ensures
13
- * all messages from the same agent land in the same OpenClaw conversation,
14
- * regardless of how many MSTP sessions are created.
15
- *
16
- * Falls back to `sessionId` for direct MSTP clients that connect without
17
- * a Connector (no `metadata.from`). These get ephemeral conversations.
18
- *
19
- * The `activeSession` reverse index in AccountState maps the MSTP address
20
- * back to the current sessionId for outbound routing (sendText, deliver).
21
- *
22
- * See: docs/openclaw/research/session-identity-mapping.md §5
23
- */
24
- function deriveSenderId(event) {
25
- const from = event.metadata?.from;
26
- if (typeof from === "string" && from.length > 0) {
27
- return from;
28
- }
29
- return event.sessionId;
30
- }
9
+ // Identity resolution uses session state (remoteAddress from SESSION_CREATED),
10
+ // not per-message metadata. See the message_received handler for details.
11
+ // See: docs/openclaw/research/session-identity-mapping.md §5
31
12
  /**
32
13
  * Extract a human-readable name from the remote Agent's MSTP address.
33
14
  *
@@ -45,6 +26,20 @@ function deriveSenderName(event) {
45
26
  const parts = trimmed.split("/");
46
27
  return parts[parts.length - 1] || from;
47
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
+ }
48
43
  const accounts = new Map();
49
44
  // --- Channel Plugin ---
50
45
  export const agentNetworkChannel = {
@@ -104,25 +99,14 @@ export const agentNetworkChannel = {
104
99
  outbound: {
105
100
  deliveryMode: "direct",
106
101
  async sendText(ctx) {
107
- // ctx.recipient is now an MSTP address (senderId = address from deriveSenderId).
108
- // 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.
109
104
  const state = accounts.get(ctx.accountId);
110
105
  if (!state)
111
106
  return { ok: false };
112
- let sessionId = state.activeSession.get(ctx.recipient);
113
- // Fallback: ctx.recipient might be a raw sessionId (direct MSTP client
114
- // without metadata.from deriveSenderId fell back to sessionId).
115
- if (!sessionId && state.sessions.has(ctx.recipient)) {
116
- sessionId = ctx.recipient;
117
- }
118
- if (!sessionId || !state.sessions.has(sessionId)) {
119
- return { ok: false };
120
- }
121
- const sent = state.client.sendMessage(sessionId, ctx.text, {
122
- contentType: "text",
123
- });
124
- dbg("sendText recipient=%s sessionId=%s contentLength=%d sent=%s", ctx.recipient, sessionId, ctx.text.length, sent);
125
- 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" };
126
110
  },
127
111
  },
128
112
  gateway: {
@@ -130,10 +114,11 @@ export const agentNetworkChannel = {
130
114
  dbg("starting account %s", ctx.accountId);
131
115
  const { connectorUrl, token } = ctx.account;
132
116
  const client = new ConnectorClient(connectorUrl, token);
117
+ const connectorHost = extractConnectorHost(connectorUrl);
118
+ const conversationManager = new ConversationManager(client, connectorHost);
133
119
  const state = {
134
120
  client,
135
- sessions: new Map(),
136
- activeSession: new Map(),
121
+ conversationManager,
137
122
  };
138
123
  accounts.set(ctx.accountId, state);
139
124
  // --- Inbound message routing (4-step channelRuntime dispatch) ---
@@ -163,19 +148,22 @@ export const agentNetworkChannel = {
163
148
  dbg("channelRuntime not available, dropping message sessionId=%s", event.sessionId);
164
149
  return;
165
150
  }
166
- // Resolve sender identity from session state, NOT from per-message metadata.
167
- // metadata.from is reliably present on SESSION_CREATED (Connector sets it
168
- // server-side) but NOT on MESSAGE_RECEIVED (per-message metadata is
169
- // pass-through from the remote plugin, which doesn't include `from`).
170
- // The session_created handler already stored the remoteAddress.
171
- const sessionMeta = state.sessions.get(event.sessionId);
172
- const senderId = sessionMeta?.remoteAddress ?? event.sessionId;
173
- // Derive sender name from the resolved address, not per-message metadata.
174
- // Build a synthetic event with the session's address for deriveSenderName.
175
- 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
176
164
  ? deriveSenderName({
177
165
  ...event,
178
- metadata: { ...event.metadata, from: sessionMeta.remoteAddress },
166
+ metadata: { ...event.metadata, from: senderAddress },
179
167
  })
180
168
  : deriveSenderName(event);
181
169
  try {
@@ -225,19 +213,11 @@ export const agentNetworkChannel = {
225
213
  const text = payload.text;
226
214
  if (!text)
227
215
  return;
228
- // senderId is now an MSTP address. Resolve to sessionId.
229
- let deliverSessionId = state.activeSession.get(senderId);
230
- // Fallback: senderId might be a raw sessionId (direct client).
231
- if (!deliverSessionId && state.sessions.has(senderId)) {
232
- deliverSessionId = senderId;
233
- }
234
- // Session may have ended while LLM was processing.
235
- if (!deliverSessionId || !state.sessions.has(deliverSessionId))
236
- return;
237
- const sent = state.client.sendMessage(deliverSessionId, text, {
238
- contentType: "text",
239
- });
240
- 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);
241
221
  },
242
222
  onError: (err) => {
243
223
  console.error(`[agent-network:${ctx.accountId}] dispatch error:`, err);
@@ -249,45 +229,37 @@ export const agentNetworkChannel = {
249
229
  console.error(`[agent-network:${ctx.accountId}] inbound dispatch failed sessionId=${event.sessionId}:`, err);
250
230
  }
251
231
  });
252
- // --- Session lifecycle ---
232
+ // --- Inbound session registration ---
233
+ // When a remote agent creates a session (direction=inbound),
234
+ // register it with the ConversationManager.
253
235
  client.on("session_created", (event) => {
254
236
  dbg("session created sessionId=%s direction=%s", event.sessionId, event.direction);
255
- const from = event.metadata?.from;
256
- const remoteAddress = typeof from === "string" && from.length > 0 ? from : null;
257
- state.sessions.set(event.sessionId, { remoteAddress });
258
- // Update reverse index: this address is now reachable via this session.
259
- // Last-write-wins: if a new session arrives from the same address,
260
- // replies go to the newest session (same as XMPP resource binding).
261
- if (remoteAddress) {
262
- state.activeSession.set(remoteAddress, event.sessionId);
263
- }
264
- });
265
- client.on("session_ended", (event) => {
266
- dbg("session ended sessionId=%s", event.sessionId);
267
- const sessionMeta = state.sessions.get(event.sessionId);
268
- state.sessions.delete(event.sessionId);
269
- // Only clear reverse index if this session was the active one.
270
- // If a newer session replaced it, the index already points elsewhere.
271
- if (sessionMeta?.remoteAddress) {
272
- const currentActive = state.activeSession.get(sessionMeta.remoteAddress);
273
- if (currentActive === event.sessionId) {
274
- 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);
275
247
  }
276
248
  }
249
+ // Outbound sessions are handled by SessionLifecycle internally
250
+ // (matched by requestId in the createSession flow).
277
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.
278
255
  // --- Error handling ---
279
256
  // Must register before connect() — unhandled "error" events crash the process.
280
257
  client.on("error", (err) => {
281
258
  console.error(`[agent-network:${ctx.accountId}]`, err.message);
282
259
  });
283
- // --- Disconnect cleanup ---
284
- client.on("disconnected", () => {
285
- dbg("disconnected, cleared %d sessions", state.sessions.size);
286
- state.sessions.clear();
287
- state.activeSession.clear();
288
- });
289
260
  // --- Abort signal ---
290
261
  ctx.abortSignal.addEventListener("abort", () => {
262
+ clearConversationManager();
291
263
  clearConnectorClient(); // idempotent — stopAccount() may also call
292
264
  client.disconnect();
293
265
  }, { once: true });
@@ -300,8 +272,9 @@ export const agentNetworkChannel = {
300
272
  // --- Connect ---
301
273
  await client.connect();
302
274
  dbg("connected account=%s", ctx.accountId);
303
- // --- Expose client to tools (only after WS is connected) ---
275
+ // --- Expose client and conversation manager to tools (only after WS is connected) ---
304
276
  initConnectorClient(client);
277
+ initConversationManager(conversationManager);
305
278
  // Park the Promise — DO NOT resolve/return.
306
279
  // Resolution signals "account stopped" to OpenClaw's orchestrator,
307
280
  // triggering auto-restart with exponential backoff (crash-loop).
@@ -319,11 +292,10 @@ export const agentNetworkChannel = {
319
292
  dbg("stopping account %s", ctx.accountId);
320
293
  const state = accounts.get(ctx.accountId);
321
294
  if (state) {
295
+ clearConversationManager();
322
296
  clearConnectorClient();
323
297
  state.client.removeAllListeners();
324
298
  state.client.disconnect();
325
- state.sessions.clear();
326
- state.activeSession.clear();
327
299
  accounts.delete(ctx.accountId);
328
300
  }
329
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
  }