@masons/agent-network 0.4.16 → 0.4.17

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":"AAWA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAsB9D,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;AAkED,eAAO,MAAM,mBAAmB,EAAE,yBA6ajC,CAAC"}
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAsB9D,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;AA+ND,eAAO,MAAM,mBAAmB,EAAE,yBAoNjC,CAAC"}
package/dist/channel.js CHANGED
@@ -3,26 +3,18 @@ import createDebug from "debug";
3
3
  import { clearConnectorClient, clearConversationManager, extractNetworkConfig, initConnectorClient, initConversationManager, initToolConfig, requirePluginRuntime, } from "./config.js";
4
4
  import { ConnectorClient } from "./connector-client.js";
5
5
  import { ConversationManager } from "./conversation-manager.js";
6
- import { clearOwnerSessions, isOwnerSession, markOwnerSession, removeOwnerSession, setCurrentTurnIsOwner, } from "./owner-session-state.js";
6
+ import { extractHandleFromAddress } from "./handle-utils.js";
7
+ import { clearOwnerState, isOwnerAddress, markOwnerAddress, setCurrentTurnIsOwner, } from "./owner-session-state.js";
7
8
  import { setCurrentTurnSender } from "./turn-context.js";
8
9
  import { checkForUpdate } from "./update-check.js";
9
10
  const dbg = createDebug("agent-network:channel");
10
11
  // --- Sender identity ---
11
- // Identity resolution uses session state (remoteAddress from SESSION_CREATED),
12
- // not per-message metadata. See the message_received handler for details.
13
- // See: docs/openclaw/research/session-identity-mapping.md §5
14
12
  /**
15
13
  * Extract a human-readable name from the sender's metadata.
16
14
  *
17
- * Resolution order (account-level identity first, then address):
15
+ * Resolution order:
18
16
  * 1. `metadata.visitor_name` — display name for Passport browser visitors
19
- * (resolved by Connector from user_info.displayName)
20
- * 2. `metadata.from` — MSTP address for agent-to-agent connections
21
- * (e.g. `mstps://preview.masons.ai/alice` → "alice")
22
- * 3. Fallback: "Unknown" for direct MSTP clients without metadata
23
- *
24
- * Design: Channel determines routing, Account determines identity.
25
- * See: interop-routing-system-design.md §3 "Channel vs Account".
17
+ * 2. `event.from` — extract handle from routable address
26
18
  */
27
19
  function deriveSenderName(event) {
28
20
  // Account-level identity: Passport visitor with resolved display name
@@ -30,14 +22,8 @@ function deriveSenderName(event) {
30
22
  if (visitorName && typeof visitorName === "string") {
31
23
  return visitorName;
32
24
  }
33
- // MSTP address: extract handle from agent address
34
- const from = event.metadata?.from;
35
- if (from && typeof from === "string") {
36
- const trimmed = from.replace(/\/+$/, "");
37
- const parts = trimmed.split("/");
38
- return parts[parts.length - 1] || from;
39
- }
40
- return "Unknown";
25
+ // Extract handle from `from` address
26
+ return extractHandleFromAddress(event.from);
41
27
  }
42
28
  /**
43
29
  * Extract the Connector host from a connectorUrl.
@@ -53,6 +39,140 @@ function extractConnectorHost(connectorUrl) {
53
39
  return connectorUrl.replace(/^wss?:\/\//, "").replace(/\/.*$/, "");
54
40
  }
55
41
  }
42
+ // --- Duplicate message protection for at-least-once delivery ---
43
+ // Short-window dedup: stored messages re-delivered on reconnect are
44
+ // suppressed if the same (from, content, sentAt) was seen within 60 seconds.
45
+ const DEDUP_WINDOW_MS = 60_000;
46
+ const recentMessages = new Map(); // key → timestamp
47
+ function isDuplicate(from, content, sentAt) {
48
+ if (!sentAt)
49
+ return false; // only dedup stored messages (have sentAt)
50
+ const key = `${from}\0${content}\0${sentAt}`;
51
+ const now = Date.now();
52
+ const seen = recentMessages.get(key);
53
+ if (seen && now - seen < DEDUP_WINDOW_MS) {
54
+ return true;
55
+ }
56
+ recentMessages.set(key, now);
57
+ // Lazy cleanup: remove expired entries when map grows large
58
+ if (recentMessages.size > 200) {
59
+ for (const [k, ts] of recentMessages) {
60
+ if (now - ts > DEDUP_WINDOW_MS)
61
+ recentMessages.delete(k);
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+ // --- Message dispatch helpers ---
67
+ /**
68
+ * Handle an address-based MESSAGE_RECEIVED { from, content, metadata }.
69
+ * This is the primary path for the address-based protocol.
70
+ */
71
+ async function handleAddressedMessage(event, conversationManager, client, ctx) {
72
+ const fromAddress = event.from;
73
+ dbg("routing inbound message from=%s contentLength=%d", fromAddress, event.content.length);
74
+ // Duplicate protection for at-least-once re-delivery (stored messages).
75
+ if (isDuplicate(fromAddress, event.content, event.sentAt)) {
76
+ dbg("duplicate message suppressed from=%s sentAt=%s", fromAddress, event.sentAt);
77
+ return;
78
+ }
79
+ // Owner detection: first-message-sets, subsequent-inherits.
80
+ // Connector includes is_owner in metadata for Passport visitors.
81
+ if (event.metadata?.is_owner === true) {
82
+ markOwnerAddress(fromAddress);
83
+ }
84
+ setCurrentTurnIsOwner(isOwnerAddress(fromAddress));
85
+ // Resolve contact from address. Register inbound conversation if new.
86
+ const contact = deriveSenderName(event);
87
+ if (!conversationManager.getContactByAddress(fromAddress)) {
88
+ conversationManager.registerInbound(contact, fromAddress);
89
+ }
90
+ setCurrentTurnSender(conversationManager.getContactByAddress(fromAddress) ?? null);
91
+ // Runtime guard
92
+ let runtime;
93
+ try {
94
+ runtime = requirePluginRuntime();
95
+ }
96
+ catch {
97
+ dbg("plugin runtime not available, dropping message from=%s", fromAddress);
98
+ return;
99
+ }
100
+ if (!runtime.channel) {
101
+ dbg("channelRuntime not available, dropping message from=%s", fromAddress);
102
+ return;
103
+ }
104
+ // senderId for OpenClaw routing: use the routable address
105
+ const senderId = fromAddress;
106
+ const senderName = contact;
107
+ try {
108
+ const route = await runtime.channel.routing.resolveAgentRoute({
109
+ cfg: ctx.cfg,
110
+ channel: "agent-network",
111
+ accountId: ctx.accountId,
112
+ peer: { kind: "direct", id: senderId },
113
+ });
114
+ const msgCtx = runtime.channel.reply.finalizeInboundContext({
115
+ Body: event.content,
116
+ RawBody: event.content,
117
+ CommandBody: event.content,
118
+ From: senderId,
119
+ To: senderId,
120
+ SessionKey: route.sessionKey,
121
+ AccountId: ctx.accountId,
122
+ ChatType: "direct",
123
+ SenderName: senderName,
124
+ SenderId: senderId,
125
+ Timestamp: event.sentAt ? new Date(event.sentAt).getTime() : Date.now(),
126
+ Provider: "agent-network",
127
+ Surface: "agent-network",
128
+ MessageSid: `an:${extractHandleFromAddress(fromAddress)}:${randomUUID()}`,
129
+ CommandAuthorized: true,
130
+ OriginatingChannel: "agent-network",
131
+ OriginatingTo: senderId,
132
+ });
133
+ const storePath = runtime.channel.session.resolveStorePath(undefined, {
134
+ agentId: route.agentId,
135
+ });
136
+ const sessionKey = typeof msgCtx.SessionKey === "string"
137
+ ? msgCtx.SessionKey
138
+ : route.sessionKey;
139
+ await runtime.channel.session.recordInboundSession({
140
+ storePath,
141
+ sessionKey,
142
+ ctx: msgCtx,
143
+ });
144
+ // Typing: address-based
145
+ client.sendTyping(fromAddress, true);
146
+ try {
147
+ await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
148
+ ctx: msgCtx,
149
+ cfg: ctx.cfg,
150
+ dispatcherOptions: {
151
+ deliver: async (payload) => {
152
+ const text = payload.text;
153
+ if (!text)
154
+ return;
155
+ if (!conversationManager.hasConversation(senderId)) {
156
+ dbg("deliver suppressed: conversation ended senderId=%s len=%d", senderId, text.length);
157
+ return;
158
+ }
159
+ const deliverResult = await conversationManager.send(senderId, text);
160
+ dbg("deliver reply senderId=%s status=%s contentLength=%d", senderId, deliverResult.status, text.length);
161
+ },
162
+ onError: (err) => {
163
+ console.error(`[agent-network:${ctx.accountId}] dispatch error:`, err);
164
+ },
165
+ },
166
+ });
167
+ }
168
+ finally {
169
+ client.sendTyping(fromAddress, false);
170
+ }
171
+ }
172
+ catch (err) {
173
+ console.error(`[agent-network:${ctx.accountId}] inbound dispatch failed from=${fromAddress}:`, err);
174
+ }
175
+ }
56
176
  const accounts = new Map();
57
177
  // --- Channel Plugin ---
58
178
  export const agentNetworkChannel = {
@@ -112,8 +232,8 @@ export const agentNetworkChannel = {
112
232
  outbound: {
113
233
  deliveryMode: "direct",
114
234
  async sendText(ctx) {
115
- // ctx.recipient is the MSTP address or sessionId fallback (senderId from inbound dispatch).
116
- // ConversationManager.send() handles session management transparently.
235
+ // ctx.recipient is the routable address (senderId from inbound dispatch).
236
+ // ConversationManager.send() handles address resolution transparently.
117
237
  const state = accounts.get(ctx.accountId);
118
238
  if (!state)
119
239
  return { ok: false };
@@ -151,182 +271,21 @@ export const agentNetworkChannel = {
151
271
  // Acceptable for unstructured text conversation (Phase 1). If MSTP adds
152
272
  // structured multi-step interactions, add a per-session dispatch queue.
153
273
  client.on("message_received", async (event) => {
154
- dbg("routing inbound message sessionId=%s contentLength=%d", event.sessionId, event.content.length);
155
- // --- Per-turn context (#836, #873) ---
156
- // Set BEFORE dispatch so before_prompt_build can read these synchronously.
157
- setCurrentTurnIsOwner(isOwnerSession(event.sessionId));
158
- // Sender identity: resolve sessionId → contact handle using
159
- // the closure-scoped conversationManager (NOT imported from config.ts).
160
- setCurrentTurnSender(conversationManager.getContactBySessionId(event.sessionId) ?? null);
161
- // --- Runtime guard ---
162
- let runtime;
163
- try {
164
- runtime = requirePluginRuntime();
165
- }
166
- catch {
167
- dbg("plugin runtime not available, dropping message sessionId=%s", event.sessionId);
168
- return;
169
- }
170
- // channel is required in our PluginRuntime type but may be absent
171
- // at runtime on older OpenClaw versions (pre-2026.2.19).
172
- if (!runtime.channel) {
173
- dbg("channelRuntime not available, dropping message sessionId=%s", event.sessionId);
174
- return;
175
- }
176
- // Resolve sender identity via ConversationManager.
177
- // The session_created handler registered the inbound session with
178
- // the ConversationManager, so we can look up the contact handle.
179
- const senderAddress = conversationManager.getAddressBySessionId(event.sessionId);
180
- // Determine if the address is a real MSTP address or a sessionId fallback.
181
- const isRealAddress = typeof senderAddress === "string" &&
182
- (senderAddress.startsWith("mstps://") ||
183
- senderAddress.startsWith("mstp://"));
184
- // senderId for OpenClaw routing: use MSTP address if available,
185
- // fall back to sessionId for unknown sessions / direct MSTP clients.
186
- const senderId = isRealAddress ? senderAddress : event.sessionId;
187
- // Derive sender name.
188
- // - MSTP agents: extract handle from address.
189
- // - Passport visitors / direct clients: use the contact name stored
190
- // in ConversationManager at session_created time (which read
191
- // visitor_name from session metadata). Per-message metadata does
192
- // NOT carry session-level fields like visitor_name, so we must
193
- // use the stored value. Falls back to deriveSenderName → "Unknown".
194
- const senderName = isRealAddress
195
- ? deriveSenderName({
196
- ...event,
197
- metadata: { ...event.metadata, from: senderAddress },
198
- })
199
- : (conversationManager.getContactBySessionId(event.sessionId) ??
200
- deriveSenderName(event));
201
- try {
202
- // Step 1 — Resolve agent route (await defensively — may be sync or async)
203
- const route = await runtime.channel.routing.resolveAgentRoute({
204
- cfg: ctx.cfg,
205
- channel: "agent-network",
206
- accountId: ctx.accountId,
207
- peer: { kind: "direct", id: senderId },
208
- });
209
- // Step 2 — Finalize inbound context (builds MsgContext for LLM pipeline)
210
- const msgCtx = runtime.channel.reply.finalizeInboundContext({
211
- Body: event.content,
212
- RawBody: event.content,
213
- CommandBody: event.content,
214
- From: senderId,
215
- To: senderId,
216
- SessionKey: route.sessionKey,
217
- AccountId: ctx.accountId,
218
- ChatType: "direct",
219
- SenderName: senderName,
220
- SenderId: senderId,
221
- Timestamp: Date.now(),
222
- Provider: "agent-network",
223
- Surface: "agent-network",
224
- MessageSid: `mstp:${event.sessionId}:${randomUUID()}`,
225
- CommandAuthorized: true,
226
- OriginatingChannel: "agent-network",
227
- OriginatingTo: senderId,
228
- });
229
- // Step 3 — Record inbound session
230
- const storePath = runtime.channel.session.resolveStorePath(undefined, { agentId: route.agentId });
231
- const sessionKey = typeof msgCtx.SessionKey === "string"
232
- ? msgCtx.SessionKey
233
- : route.sessionKey;
234
- await runtime.channel.session.recordInboundSession({
235
- storePath,
236
- sessionKey,
237
- ctx: msgCtx,
238
- });
239
- // Step 4 — Dispatch to LLM pipeline + deliver reply via callback
240
- // Signal typing start — agent is composing a reply.
241
- // Wrapped in try/finally so typing stop fires even on dispatch error.
242
- // Without this, a failed dispatch leaves a stale "typing..." for 30s.
243
- client.sendTypingStart(event.sessionId);
244
- try {
245
- await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
246
- ctx: msgCtx,
247
- cfg: ctx.cfg,
248
- dispatcherOptions: {
249
- deliver: async (payload) => {
250
- const text = payload.text;
251
- if (!text)
252
- return;
253
- // Guard: if conversation was ended during this dispatch
254
- // (LLM called masons_end_conversation mid-turn), suppress
255
- // straggler text to prevent auto-creating a new session.
256
- // endConversation() deletes the conversation entry — its
257
- // absence is the signal. Normal inbound replies always
258
- // have an entry (registerInbound creates it before dispatch).
259
- if (!conversationManager.hasConversation(senderId)) {
260
- dbg("deliver suppressed: conversation ended senderId=%s len=%d", senderId, text.length);
261
- return;
262
- }
263
- const deliverResult = await conversationManager.send(senderId, text);
264
- dbg("deliver reply senderId=%s status=%s contentLength=%d", senderId, deliverResult.status, text.length);
265
- },
266
- onError: (err) => {
267
- console.error(`[agent-network:${ctx.accountId}] dispatch error:`, err);
268
- },
269
- },
270
- });
271
- }
272
- finally {
273
- // Signal typing stop — dispatch complete (explicit stop, layer 1).
274
- // Also implicitly cleared by Connector on SEND_MESSAGE (layer 2).
275
- client.sendTypingStop(event.sessionId);
276
- }
277
- }
278
- catch (err) {
279
- console.error(`[agent-network:${ctx.accountId}] inbound dispatch failed sessionId=${event.sessionId}:`, err);
280
- }
281
- });
282
- // --- Inbound session registration ---
283
- // When a remote agent creates a session (direction=inbound),
284
- // register it with the ConversationManager.
285
- client.on("session_created", (event) => {
286
- dbg("session created sessionId=%s direction=%s", event.sessionId, event.direction);
287
- if (event.direction === "inbound") {
288
- // --- Owner identity binding (#836) ---
289
- // Connector sets `is_owner: true` when visitor_user_id === agent.userId.
290
- // Track this session so message_received can set the per-turn flag.
291
- if (event.metadata?.is_owner === true) {
292
- markOwnerSession(event.sessionId);
293
- dbg("owner session detected sessionId=%s", event.sessionId);
294
- }
295
- const from = event.metadata?.from;
296
- const remoteAddress = typeof from === "string" && from.length > 0 ? from : null;
297
- if (remoteAddress) {
298
- const contact = deriveSenderName(event);
299
- conversationManager.registerInbound(event.sessionId, contact, remoteAddress);
300
- }
301
- else {
302
- // No MSTP address — Passport visitor or direct MSTP client.
303
- // Use visitor_name from session metadata (account-level identity
304
- // resolved by Connector) if available, else sessionId fallback.
305
- const contact = deriveSenderName(event);
306
- conversationManager.registerInbound(event.sessionId, contact, event.sessionId);
307
- }
308
- }
309
- // Outbound sessions are handled by SessionLifecycle internally
310
- // (matched by requestId in the createSession flow).
311
- });
312
- // --- Mailbox delivery ACK ---
313
- // "received" semantics — ACK immediately on FLUSH_COMPLETE,
314
- // don't wait for LLM processing. This tells Connector it's safe
315
- // to delete the flushed entries from MongoDB.
316
- client.on("flush_complete", (event) => {
317
- client.sendDeliveryAck(event.mailboxSessionId);
318
- dbg("flush_complete → ACK sent mailboxSessionId=%s", event.mailboxSessionId);
274
+ if (!("from" in event))
275
+ return; // address-based only
276
+ await handleAddressedMessage(event, conversationManager, client, ctx);
319
277
  });
320
- // --- Session ended + disconnect ---
321
- // SessionLifecycle handles session state transitions (session_ended, disconnected)
322
- // via its own event subscriptions. No CM calls needed here.
323
- // Owner session cleanup (#836): remove from ownerSessionIds on session_ended.
324
- client.on("session_ended", (event) => {
325
- removeOwnerSession(event.sessionId);
278
+ // --- Delivery ACK ---
279
+ // ACK immediately on DELIVERY_PENDING tells Connector it's safe
280
+ // to delete delivered stored messages.
281
+ client.on("delivery_pending", (event) => {
282
+ client.ackDelivery(event.upTo);
283
+ dbg("delivery_pending → ACK sent count=%d upTo=%s", event.count, event.upTo);
326
284
  });
327
- // On disconnect, all sessions are invalid — clear owner state.
285
+ // --- Disconnect ---
286
+ // On disconnect, all owner addresses are invalid — clear owner state.
328
287
  client.on("disconnected", () => {
329
- clearOwnerSessions();
288
+ clearOwnerState();
330
289
  });
331
290
  // --- Error handling ---
332
291
  // Must register before connect() — unhandled "error" events crash the process.
@@ -1,11 +1,19 @@
1
1
  import { EventEmitter } from "node:events";
2
- import { type FlushCompleteEvent, type MessageReceivedEvent, type SessionCreatedEvent, type SessionEndedEvent } from "./types.js";
2
+ import { type AddressedMessageEvent, type DeliveryPendingEvent, type SendAckEvent, type StructuredErrorEvent } from "./types.js";
3
+ /**
4
+ * Error with machine-readable `code` from Connector structured errors.
5
+ * Thrown by `send()` when the Connector responds with ERROR { code }.
6
+ */
7
+ export declare class ConnectorError extends Error {
8
+ readonly code: string;
9
+ readonly to?: string;
10
+ constructor(message: string, code: string, to?: string);
11
+ }
3
12
  type ConnectorClientEvents = {
4
- session_created: (event: SessionCreatedEvent) => void;
5
- message_received: (event: MessageReceivedEvent) => void;
6
- session_ended: (event: SessionEndedEvent) => void;
7
- flush_complete: (event: FlushCompleteEvent) => void;
8
- session_error: (sessionId: string, message: string) => void;
13
+ message_received: (event: AddressedMessageEvent) => void;
14
+ send_ack: (event: SendAckEvent) => void;
15
+ delivery_pending: (event: DeliveryPendingEvent) => void;
16
+ structured_error: (event: StructuredErrorEvent) => void;
9
17
  error: (error: Error) => void;
10
18
  connected: () => void;
11
19
  disconnected: () => void;
@@ -21,24 +29,27 @@ export declare class ConnectorClient extends EventEmitter {
21
29
  private registerResolve;
22
30
  private registerReject;
23
31
  private registerTimer;
32
+ /** Pending SEND_ACK correlation map for address-based sends. */
33
+ private pendingSends;
24
34
  constructor(url: string, token: string);
25
35
  connect(): Promise<void>;
26
36
  disconnect(): void;
27
- createSession(target: string, opts?: {
28
- secret?: string;
29
- metadata?: Record<string, unknown>;
30
- }): {
31
- requestId: string;
32
- sent: boolean;
33
- };
34
- sendMessage(sessionId: string, content: string, opts?: {
35
- contentType?: string;
36
- metadata?: Record<string, unknown>;
37
- }): boolean;
38
- endSession(sessionId: string, reason?: string): boolean;
39
- sendDeliveryAck(mailboxSessionId: string): boolean;
40
- sendTypingStart(sessionId: string): boolean;
41
- sendTypingStop(sessionId: string): boolean;
37
+ /**
38
+ * Send a message to a routable address. Returns a Promise that resolves
39
+ * with the SEND_ACK from the Connector, or rejects with a ConnectorError
40
+ * (structured error code) or a plain Error (timeout / disconnect).
41
+ *
42
+ * The messageId is generated internally and used for ACK correlation.
43
+ */
44
+ send(to: string, content: string, contentType?: string, metadata?: Record<string, unknown>): Promise<SendAckEvent>;
45
+ /**
46
+ * Send a typing indicator to a routable address.
47
+ */
48
+ sendTyping(to: string, isTyping: boolean): boolean;
49
+ /**
50
+ * Acknowledge delivery of stored messages up to a timestamp.
51
+ */
52
+ ackDelivery(upTo: string): boolean;
42
53
  on<K extends keyof ConnectorClientEvents>(event: K, listener: ConnectorClientEvents[K]): this;
43
54
  on(event: string | symbol, listener: (...args: unknown[]) => void): this;
44
55
  once<K extends keyof ConnectorClientEvents>(event: K, listener: ConnectorClientEvents[K]): this;
@@ -53,9 +64,19 @@ export declare class ConnectorClient extends EventEmitter {
53
64
  private resolveRegister;
54
65
  private rejectRegister;
55
66
  private handleMessage;
67
+ /** Dispatch inbound address-based events. */
68
+ private dispatchAddressed;
56
69
  private handleRegisterAck;
57
- private handleSessionCreated;
58
- private handleErrorEvent;
70
+ /**
71
+ * Resolve the pending send promise matching this SEND_ACK.
72
+ * Also emits `send_ack` for listeners that want to observe all ACKs.
73
+ */
74
+ private handleSendAck;
75
+ /**
76
+ * Structured ERROR: message-scoped errors reject pending sends,
77
+ * connection-scoped errors emit generic event.
78
+ */
79
+ private handleStructuredError;
59
80
  private handleClose;
60
81
  private handleError;
61
82
  private scheduleReconnect;
@@ -63,7 +84,8 @@ export declare class ConnectorClient extends EventEmitter {
63
84
  private nextExponentialBackoff;
64
85
  private clearReconnectTimer;
65
86
  private cleanupConnection;
66
- private send;
87
+ /** Send an event over the WebSocket. */
88
+ private sendEvent;
67
89
  }
68
90
  export {};
69
91
  //# sourceMappingURL=connector-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"connector-client.d.ts","sourceRoot":"","sources":["../src/connector-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"connector-client.d.ts","sourceRoot":"","sources":["../src/connector-client.ts"],"names":[],"mappings":"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"}