@masons/agent-network 0.4.16 → 0.4.18
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.
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +156 -195
- package/dist/connector-client.d.ts +46 -24
- package/dist/connector-client.d.ts.map +1 -1
- package/dist/connector-client.js +127 -76
- package/dist/conversation-manager.d.ts +40 -33
- package/dist/conversation-manager.d.ts.map +1 -1
- package/dist/conversation-manager.js +103 -80
- package/dist/environment-context.d.ts +45 -0
- package/dist/environment-context.d.ts.map +1 -0
- package/dist/environment-context.js +81 -0
- package/dist/handle-utils.d.ts +16 -0
- package/dist/handle-utils.d.ts.map +1 -0
- package/dist/handle-utils.js +25 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/owner-session-state.d.ts +11 -18
- package/dist/owner-session-state.d.ts.map +1 -1
- package/dist/owner-session-state.js +17 -26
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +32 -4
- package/dist/tools.d.ts +1 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +2 -33
- package/dist/types.d.ts +57 -58
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +18 -33
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -11
package/dist/channel.d.ts.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAuB9D,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,yBAqNjC,CAAC"}
|
package/dist/channel.js
CHANGED
|
@@ -3,26 +3,19 @@ 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 {
|
|
6
|
+
import { clearEnvironmentContext } from "./environment-context.js";
|
|
7
|
+
import { extractHandleFromAddress } from "./handle-utils.js";
|
|
8
|
+
import { clearOwnerState, isOwnerAddress, markOwnerAddress, setCurrentTurnIsOwner, } from "./owner-session-state.js";
|
|
7
9
|
import { setCurrentTurnSender } from "./turn-context.js";
|
|
8
10
|
import { checkForUpdate } from "./update-check.js";
|
|
9
11
|
const dbg = createDebug("agent-network:channel");
|
|
10
12
|
// --- 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
13
|
/**
|
|
15
14
|
* Extract a human-readable name from the sender's metadata.
|
|
16
15
|
*
|
|
17
|
-
* Resolution order
|
|
16
|
+
* Resolution order:
|
|
18
17
|
* 1. `metadata.visitor_name` — display name for Passport browser visitors
|
|
19
|
-
*
|
|
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".
|
|
18
|
+
* 2. `event.from` — extract handle from routable address
|
|
26
19
|
*/
|
|
27
20
|
function deriveSenderName(event) {
|
|
28
21
|
// Account-level identity: Passport visitor with resolved display name
|
|
@@ -30,14 +23,8 @@ function deriveSenderName(event) {
|
|
|
30
23
|
if (visitorName && typeof visitorName === "string") {
|
|
31
24
|
return visitorName;
|
|
32
25
|
}
|
|
33
|
-
//
|
|
34
|
-
|
|
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";
|
|
26
|
+
// Extract handle from `from` address
|
|
27
|
+
return extractHandleFromAddress(event.from);
|
|
41
28
|
}
|
|
42
29
|
/**
|
|
43
30
|
* Extract the Connector host from a connectorUrl.
|
|
@@ -53,6 +40,140 @@ function extractConnectorHost(connectorUrl) {
|
|
|
53
40
|
return connectorUrl.replace(/^wss?:\/\//, "").replace(/\/.*$/, "");
|
|
54
41
|
}
|
|
55
42
|
}
|
|
43
|
+
// --- Duplicate message protection for at-least-once delivery ---
|
|
44
|
+
// Short-window dedup: stored messages re-delivered on reconnect are
|
|
45
|
+
// suppressed if the same (from, content, sentAt) was seen within 60 seconds.
|
|
46
|
+
const DEDUP_WINDOW_MS = 60_000;
|
|
47
|
+
const recentMessages = new Map(); // key → timestamp
|
|
48
|
+
function isDuplicate(from, content, sentAt) {
|
|
49
|
+
if (!sentAt)
|
|
50
|
+
return false; // only dedup stored messages (have sentAt)
|
|
51
|
+
const key = `${from}\0${content}\0${sentAt}`;
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const seen = recentMessages.get(key);
|
|
54
|
+
if (seen && now - seen < DEDUP_WINDOW_MS) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
recentMessages.set(key, now);
|
|
58
|
+
// Lazy cleanup: remove expired entries when map grows large
|
|
59
|
+
if (recentMessages.size > 200) {
|
|
60
|
+
for (const [k, ts] of recentMessages) {
|
|
61
|
+
if (now - ts > DEDUP_WINDOW_MS)
|
|
62
|
+
recentMessages.delete(k);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
// --- Message dispatch helpers ---
|
|
68
|
+
/**
|
|
69
|
+
* Handle an address-based MESSAGE_RECEIVED { from, content, metadata }.
|
|
70
|
+
* This is the primary path for the address-based protocol.
|
|
71
|
+
*/
|
|
72
|
+
async function handleAddressedMessage(event, conversationManager, client, ctx) {
|
|
73
|
+
const fromAddress = event.from;
|
|
74
|
+
dbg("routing inbound message from=%s contentLength=%d", fromAddress, event.content.length);
|
|
75
|
+
// Duplicate protection for at-least-once re-delivery (stored messages).
|
|
76
|
+
if (isDuplicate(fromAddress, event.content, event.sentAt)) {
|
|
77
|
+
dbg("duplicate message suppressed from=%s sentAt=%s", fromAddress, event.sentAt);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Owner detection: first-message-sets, subsequent-inherits.
|
|
81
|
+
// Connector includes is_owner in metadata for Passport visitors.
|
|
82
|
+
if (event.metadata?.is_owner === true) {
|
|
83
|
+
markOwnerAddress(fromAddress);
|
|
84
|
+
}
|
|
85
|
+
setCurrentTurnIsOwner(isOwnerAddress(fromAddress));
|
|
86
|
+
// Resolve contact from address. Register inbound conversation if new.
|
|
87
|
+
const contact = deriveSenderName(event);
|
|
88
|
+
if (!conversationManager.getContactByAddress(fromAddress)) {
|
|
89
|
+
conversationManager.registerInbound(contact, fromAddress);
|
|
90
|
+
}
|
|
91
|
+
setCurrentTurnSender(conversationManager.getContactByAddress(fromAddress) ?? null);
|
|
92
|
+
// Runtime guard
|
|
93
|
+
let runtime;
|
|
94
|
+
try {
|
|
95
|
+
runtime = requirePluginRuntime();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
dbg("plugin runtime not available, dropping message from=%s", fromAddress);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!runtime.channel) {
|
|
102
|
+
dbg("channelRuntime not available, dropping message from=%s", fromAddress);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// senderId for OpenClaw routing: use the routable address
|
|
106
|
+
const senderId = fromAddress;
|
|
107
|
+
const senderName = contact;
|
|
108
|
+
try {
|
|
109
|
+
const route = await runtime.channel.routing.resolveAgentRoute({
|
|
110
|
+
cfg: ctx.cfg,
|
|
111
|
+
channel: "agent-network",
|
|
112
|
+
accountId: ctx.accountId,
|
|
113
|
+
peer: { kind: "direct", id: senderId },
|
|
114
|
+
});
|
|
115
|
+
const msgCtx = runtime.channel.reply.finalizeInboundContext({
|
|
116
|
+
Body: event.content,
|
|
117
|
+
RawBody: event.content,
|
|
118
|
+
CommandBody: event.content,
|
|
119
|
+
From: senderId,
|
|
120
|
+
To: senderId,
|
|
121
|
+
SessionKey: route.sessionKey,
|
|
122
|
+
AccountId: ctx.accountId,
|
|
123
|
+
ChatType: "direct",
|
|
124
|
+
SenderName: senderName,
|
|
125
|
+
SenderId: senderId,
|
|
126
|
+
Timestamp: event.sentAt ? new Date(event.sentAt).getTime() : Date.now(),
|
|
127
|
+
Provider: "agent-network",
|
|
128
|
+
Surface: "agent-network",
|
|
129
|
+
MessageSid: `an:${extractHandleFromAddress(fromAddress)}:${randomUUID()}`,
|
|
130
|
+
CommandAuthorized: true,
|
|
131
|
+
OriginatingChannel: "agent-network",
|
|
132
|
+
OriginatingTo: senderId,
|
|
133
|
+
});
|
|
134
|
+
const storePath = runtime.channel.session.resolveStorePath(undefined, {
|
|
135
|
+
agentId: route.agentId,
|
|
136
|
+
});
|
|
137
|
+
const sessionKey = typeof msgCtx.SessionKey === "string"
|
|
138
|
+
? msgCtx.SessionKey
|
|
139
|
+
: route.sessionKey;
|
|
140
|
+
await runtime.channel.session.recordInboundSession({
|
|
141
|
+
storePath,
|
|
142
|
+
sessionKey,
|
|
143
|
+
ctx: msgCtx,
|
|
144
|
+
});
|
|
145
|
+
// Typing: address-based
|
|
146
|
+
client.sendTyping(fromAddress, true);
|
|
147
|
+
try {
|
|
148
|
+
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
149
|
+
ctx: msgCtx,
|
|
150
|
+
cfg: ctx.cfg,
|
|
151
|
+
dispatcherOptions: {
|
|
152
|
+
deliver: async (payload) => {
|
|
153
|
+
const text = payload.text;
|
|
154
|
+
if (!text)
|
|
155
|
+
return;
|
|
156
|
+
if (!conversationManager.hasConversation(senderId)) {
|
|
157
|
+
dbg("deliver suppressed: conversation ended senderId=%s len=%d", senderId, text.length);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const deliverResult = await conversationManager.send(senderId, text);
|
|
161
|
+
dbg("deliver reply senderId=%s status=%s contentLength=%d", senderId, deliverResult.status, text.length);
|
|
162
|
+
},
|
|
163
|
+
onError: (err) => {
|
|
164
|
+
console.error(`[agent-network:${ctx.accountId}] dispatch error:`, err);
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
client.sendTyping(fromAddress, false);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
console.error(`[agent-network:${ctx.accountId}] inbound dispatch failed from=${fromAddress}:`, err);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
56
177
|
const accounts = new Map();
|
|
57
178
|
// --- Channel Plugin ---
|
|
58
179
|
export const agentNetworkChannel = {
|
|
@@ -112,8 +233,8 @@ export const agentNetworkChannel = {
|
|
|
112
233
|
outbound: {
|
|
113
234
|
deliveryMode: "direct",
|
|
114
235
|
async sendText(ctx) {
|
|
115
|
-
// ctx.recipient is the
|
|
116
|
-
// ConversationManager.send() handles
|
|
236
|
+
// ctx.recipient is the routable address (senderId from inbound dispatch).
|
|
237
|
+
// ConversationManager.send() handles address resolution transparently.
|
|
117
238
|
const state = accounts.get(ctx.accountId);
|
|
118
239
|
if (!state)
|
|
119
240
|
return { ok: false };
|
|
@@ -151,182 +272,22 @@ export const agentNetworkChannel = {
|
|
|
151
272
|
// Acceptable for unstructured text conversation (Phase 1). If MSTP adds
|
|
152
273
|
// structured multi-step interactions, add a per-session dispatch queue.
|
|
153
274
|
client.on("message_received", async (event) => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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);
|
|
275
|
+
if (!("from" in event))
|
|
276
|
+
return; // address-based only
|
|
277
|
+
await handleAddressedMessage(event, conversationManager, client, ctx);
|
|
319
278
|
});
|
|
320
|
-
// ---
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
279
|
+
// --- Delivery ACK ---
|
|
280
|
+
// ACK immediately on DELIVERY_PENDING — tells Connector it's safe
|
|
281
|
+
// to delete delivered stored messages.
|
|
282
|
+
client.on("delivery_pending", (event) => {
|
|
283
|
+
client.ackDelivery(event.upTo);
|
|
284
|
+
dbg("delivery_pending → ACK sent count=%d upTo=%s", event.count, event.upTo);
|
|
326
285
|
});
|
|
327
|
-
//
|
|
286
|
+
// --- Disconnect ---
|
|
287
|
+
// On disconnect, all session state is invalid — clear owner + environment.
|
|
328
288
|
client.on("disconnected", () => {
|
|
329
|
-
|
|
289
|
+
clearOwnerState();
|
|
290
|
+
clearEnvironmentContext();
|
|
330
291
|
});
|
|
331
292
|
// --- Error handling ---
|
|
332
293
|
// Must register before connect() — unhandled "error" events crash the process.
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import { type
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"connector-client.d.ts","sourceRoot":"","sources":["../src/connector-client.ts"],"names":[],"mappings":"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"}
|