@masons/agent-network 0.6.20 → 0.6.22
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-setup.d.ts +0 -1
- package/dist/channel-setup.d.ts.map +1 -1
- package/dist/channel-setup.js +20 -13
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +54 -44
- package/dist/cli-setup.d.ts.map +1 -1
- package/dist/cli-setup.js +13 -7
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -1
- package/dist/connector-client.d.ts +35 -4
- package/dist/connector-client.d.ts.map +1 -1
- package/dist/connector-client.js +126 -19
- package/dist/conversation-manager-context.d.ts +4 -0
- package/dist/conversation-manager-context.d.ts.map +1 -0
- package/dist/conversation-manager-context.js +8 -0
- package/dist/conversation-manager.d.ts +29 -4
- package/dist/conversation-manager.d.ts.map +1 -1
- package/dist/conversation-manager.js +163 -14
- package/dist/handoff-acceptance.d.ts +11 -0
- package/dist/handoff-acceptance.d.ts.map +1 -0
- package/dist/handoff-acceptance.js +62 -0
- package/dist/handoff.d.ts +0 -1
- package/dist/handoff.d.ts.map +1 -1
- package/dist/handoff.js +4 -19
- package/dist/identity-format.d.ts +6 -0
- package/dist/identity-format.d.ts.map +1 -0
- package/dist/identity-format.js +30 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/platform-client.d.ts +1 -0
- package/dist/platform-client.d.ts.map +1 -1
- package/dist/platform-client.js +24 -0
- package/dist/plugin.d.ts +2 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +49 -30
- package/dist/sent-message-buffer.d.ts +2 -3
- package/dist/sent-message-buffer.d.ts.map +1 -1
- package/dist/sent-message-buffer.js +8 -11
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +97 -20
- package/dist/turn-context.d.ts +0 -4
- package/dist/turn-context.d.ts.map +1 -1
- package/dist/turn-context.js +0 -18
- package/dist/types.d.ts +10 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/ws-heartbeat.d.ts +23 -0
- package/dist/ws-heartbeat.d.ts.map +1 -0
- package/dist/ws-heartbeat.js +63 -0
- package/openclaw.plugin.json +2 -1
- package/package.json +1 -1
- package/skills/agent-network/SKILL.md +9 -0
package/dist/connector-client.js
CHANGED
|
@@ -5,6 +5,7 @@ import WebSocket from "ws";
|
|
|
5
5
|
import { setEnvironmentContext } from "./environment-context.js";
|
|
6
6
|
import { CURRENT_PROTOCOL_VERSION, isAddressedMessage, isCustodyPending, isDeliveryCursor, isDeliveryPending, isDeliverySkip, isDeliveryStatus, isRegisterAck, isSendAck, isStructuredError, REGISTER_ACK_TIMEOUT_MS, SEND_ACK_TIMEOUT_MS, } from "./types.js";
|
|
7
7
|
import { PLUGIN_VERSION } from "./version.js";
|
|
8
|
+
import { startWsHeartbeat, } from "./ws-heartbeat.js";
|
|
8
9
|
const dbg = createDebug("agent-network:connector");
|
|
9
10
|
const dbgMsg = createDebug("agent-network:connector:msg");
|
|
10
11
|
const BACKOFF_INITIAL_MS = 1_000;
|
|
@@ -24,6 +25,23 @@ export class ConnectorError extends Error {
|
|
|
24
25
|
this.to = to;
|
|
25
26
|
}
|
|
26
27
|
}
|
|
28
|
+
export class SendOutcomeUnknownError extends Error {
|
|
29
|
+
code = "SEND_OUTCOME_UNKNOWN";
|
|
30
|
+
retryable = false;
|
|
31
|
+
attempts = 1;
|
|
32
|
+
messageId;
|
|
33
|
+
reason;
|
|
34
|
+
constructor(messageId, reason) {
|
|
35
|
+
const detail = reason === "ack_timeout"
|
|
36
|
+
? "no SEND_ACK was observed before the acknowledgement deadline"
|
|
37
|
+
: "the connection closed before a SEND_ACK was observed";
|
|
38
|
+
super(`Send outcome unknown for message ${messageId}: ${detail}. ` +
|
|
39
|
+
"It may have been sent. Do not send it again as a new message.");
|
|
40
|
+
this.name = "SendOutcomeUnknownError";
|
|
41
|
+
this.messageId = messageId;
|
|
42
|
+
this.reason = reason;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
27
45
|
export class ConnectorClient extends EventEmitter {
|
|
28
46
|
url;
|
|
29
47
|
token;
|
|
@@ -32,25 +50,32 @@ export class ConnectorClient extends EventEmitter {
|
|
|
32
50
|
backoffMs = BACKOFF_INITIAL_MS;
|
|
33
51
|
alreadyConnectedRetries = 0;
|
|
34
52
|
reconnectTimer = null;
|
|
53
|
+
keepalive = null;
|
|
54
|
+
keepaliveOptions;
|
|
35
55
|
registerResolve = null;
|
|
36
56
|
registerReject = null;
|
|
37
57
|
registerTimer = null;
|
|
58
|
+
registration = { state: "none" };
|
|
59
|
+
everDropped = false;
|
|
38
60
|
lastKnownSeq;
|
|
39
61
|
pendingSeqMessages = new Map();
|
|
40
62
|
seenPayloadIds = new Set();
|
|
41
63
|
seenPayloadIdOrder = [];
|
|
42
64
|
pendingSends = new Map();
|
|
43
|
-
constructor(url, token) {
|
|
65
|
+
constructor(url, token, options = {}) {
|
|
44
66
|
super();
|
|
45
67
|
this.url = url;
|
|
46
68
|
this.token = token;
|
|
69
|
+
this.keepaliveOptions = options.keepalive;
|
|
47
70
|
}
|
|
48
71
|
connect() {
|
|
49
72
|
if (this.ws) {
|
|
50
73
|
return Promise.reject(new Error("Already connecting or connected"));
|
|
51
74
|
}
|
|
75
|
+
this.clearReconnectTimer();
|
|
52
76
|
dbg("connecting to %s", this.url);
|
|
53
77
|
this.intentionalClose = false;
|
|
78
|
+
this.everDropped = false;
|
|
54
79
|
return this.doConnect();
|
|
55
80
|
}
|
|
56
81
|
disconnect() {
|
|
@@ -65,6 +90,17 @@ export class ConnectorClient extends EventEmitter {
|
|
|
65
90
|
wsToClose.close(1000, "Client disconnect");
|
|
66
91
|
}
|
|
67
92
|
}
|
|
93
|
+
getChannelState() {
|
|
94
|
+
if (this.ws?.readyState === WebSocket.OPEN)
|
|
95
|
+
return "connected";
|
|
96
|
+
if (this.ws !== null || this.reconnectTimer !== null) {
|
|
97
|
+
return this.everDropped ? "reconnecting" : "connecting";
|
|
98
|
+
}
|
|
99
|
+
return "disconnected";
|
|
100
|
+
}
|
|
101
|
+
getRegistration() {
|
|
102
|
+
return this.registration;
|
|
103
|
+
}
|
|
68
104
|
send(to, content, contentType = "text", metadata) {
|
|
69
105
|
const messageId = randomUUID();
|
|
70
106
|
const event = {
|
|
@@ -75,13 +111,29 @@ export class ConnectorClient extends EventEmitter {
|
|
|
75
111
|
contentType,
|
|
76
112
|
metadata,
|
|
77
113
|
};
|
|
114
|
+
let serializedEvent;
|
|
115
|
+
try {
|
|
116
|
+
serializedEvent = JSON.stringify(event);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
120
|
+
}
|
|
78
121
|
return new Promise((resolve, reject) => {
|
|
79
122
|
const timer = setTimeout(() => {
|
|
80
123
|
this.pendingSends.delete(messageId);
|
|
81
|
-
reject(new
|
|
124
|
+
reject(new SendOutcomeUnknownError(messageId, "ack_timeout"));
|
|
82
125
|
}, SEND_ACK_TIMEOUT_MS);
|
|
83
126
|
this.pendingSends.set(messageId, { resolve, reject, timer });
|
|
84
|
-
|
|
127
|
+
let sent = false;
|
|
128
|
+
try {
|
|
129
|
+
sent = this.sendSerializedEvent(serializedEvent, event.event);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
this.pendingSends.delete(messageId);
|
|
134
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
85
137
|
if (!sent) {
|
|
86
138
|
clearTimeout(timer);
|
|
87
139
|
this.pendingSends.delete(messageId);
|
|
@@ -120,18 +172,42 @@ export class ConnectorClient extends EventEmitter {
|
|
|
120
172
|
return super.emit(event, ...args);
|
|
121
173
|
}
|
|
122
174
|
doConnect() {
|
|
175
|
+
if (this.ws) {
|
|
176
|
+
return Promise.reject(new Error("Already connecting or connected"));
|
|
177
|
+
}
|
|
123
178
|
return new Promise((resolve, reject) => {
|
|
124
179
|
this.registerResolve = resolve;
|
|
125
180
|
this.registerReject = reject;
|
|
126
181
|
const ws = new WebSocket(this.url, { handshakeTimeout: 10_000 });
|
|
127
182
|
this.ws = ws;
|
|
128
183
|
ws.on("open", () => {
|
|
184
|
+
if (this.ws !== ws) {
|
|
185
|
+
ws.terminate();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.keepalive = startWsHeartbeat(ws, {
|
|
189
|
+
...this.keepaliveOptions,
|
|
190
|
+
onDead: () => {
|
|
191
|
+
dbg("keepalive: silent Gateway socket terminated");
|
|
192
|
+
},
|
|
193
|
+
onPingError: (error) => {
|
|
194
|
+
dbg("keepalive: ping failed: %s", error.message);
|
|
195
|
+
},
|
|
196
|
+
});
|
|
129
197
|
this.sendRegister();
|
|
130
198
|
this.startRegisterTimeout();
|
|
131
199
|
});
|
|
132
|
-
ws.on("message",
|
|
133
|
-
|
|
134
|
-
|
|
200
|
+
ws.on("message", (data) => {
|
|
201
|
+
if (this.ws === ws)
|
|
202
|
+
this.handleMessage(data);
|
|
203
|
+
});
|
|
204
|
+
ws.on("close", (code, reason) => {
|
|
205
|
+
this.handleClose(ws, code, reason);
|
|
206
|
+
});
|
|
207
|
+
ws.on("error", (error) => {
|
|
208
|
+
if (this.ws === ws)
|
|
209
|
+
this.handleError(error);
|
|
210
|
+
});
|
|
135
211
|
});
|
|
136
212
|
}
|
|
137
213
|
sendRegister() {
|
|
@@ -322,11 +398,13 @@ export class ConnectorClient extends EventEmitter {
|
|
|
322
398
|
}
|
|
323
399
|
this.backoffMs = BACKOFF_INITIAL_MS;
|
|
324
400
|
this.alreadyConnectedRetries = 0;
|
|
401
|
+
this.registration = { state: "accepted" };
|
|
325
402
|
this.resolveRegister();
|
|
326
403
|
this.emit("connected");
|
|
327
404
|
}
|
|
328
405
|
else {
|
|
329
406
|
const reason = ack.reason ?? "Unknown registration error";
|
|
407
|
+
this.registration = { state: "refused", reason };
|
|
330
408
|
if (reason === "Unsupported protocol version") {
|
|
331
409
|
this.intentionalClose = true;
|
|
332
410
|
this.rejectRegister(new Error(reason));
|
|
@@ -342,28 +420,46 @@ export class ConnectorClient extends EventEmitter {
|
|
|
342
420
|
}
|
|
343
421
|
}
|
|
344
422
|
handleSendAck(ack) {
|
|
345
|
-
this.emit("send_ack", ack);
|
|
346
423
|
const pending = this.pendingSends.get(ack.messageId);
|
|
347
424
|
if (pending) {
|
|
348
425
|
clearTimeout(pending.timer);
|
|
349
426
|
this.pendingSends.delete(ack.messageId);
|
|
350
|
-
pending.resolve(ack);
|
|
427
|
+
pending.resolve(structuredClone(ack));
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
this.emit("send_ack", ack);
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
console.error("[agent-network:connector-client] send_ack listener failed:", err);
|
|
351
434
|
}
|
|
352
435
|
}
|
|
353
436
|
handleStructuredError(event) {
|
|
354
|
-
this.emit("structured_error", event);
|
|
355
437
|
if (event.messageId) {
|
|
356
438
|
const pending = this.pendingSends.get(event.messageId);
|
|
357
439
|
if (pending) {
|
|
358
440
|
clearTimeout(pending.timer);
|
|
359
441
|
this.pendingSends.delete(event.messageId);
|
|
360
442
|
pending.reject(new ConnectorError(event.message, event.code, event.to));
|
|
361
|
-
return;
|
|
362
443
|
}
|
|
444
|
+
try {
|
|
445
|
+
this.emit("structured_error", event);
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
console.error("[agent-network:connector-client] structured_error listener failed:", err);
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
this.emit("structured_error", event);
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
console.error("[agent-network:connector-client] structured_error listener failed:", err);
|
|
363
457
|
}
|
|
364
458
|
this.emit("error", new Error(`[${event.code}] ${event.message}`));
|
|
365
459
|
}
|
|
366
|
-
handleClose
|
|
460
|
+
handleClose(ws, code, reason) {
|
|
461
|
+
if (this.ws !== ws)
|
|
462
|
+
return;
|
|
367
463
|
const reasonStr = reason
|
|
368
464
|
? typeof reason === "string"
|
|
369
465
|
? reason
|
|
@@ -371,11 +467,13 @@ export class ConnectorClient extends EventEmitter {
|
|
|
371
467
|
: "";
|
|
372
468
|
dbg("connection closed code=%s reason=%s intentional=%s", code ?? "?", reasonStr, this.intentionalClose);
|
|
373
469
|
const wasRegistering = this.registerReject !== null;
|
|
470
|
+
this.everDropped = true;
|
|
374
471
|
this.cleanupConnection();
|
|
375
472
|
const replacedByNewRuntime = code === REPLACED_BY_NEW_RUNTIME_CODE &&
|
|
376
473
|
reasonStr === REPLACED_BY_NEW_RUNTIME_REASON;
|
|
377
474
|
if (replacedByNewRuntime) {
|
|
378
475
|
this.intentionalClose = true;
|
|
476
|
+
this.registration = { state: "replaced" };
|
|
379
477
|
const msg = "This agent was claimed by another Runtime " +
|
|
380
478
|
"(this Runtime disconnected). " +
|
|
381
479
|
"Re-run `openclaw channels login --channel agent-network` " +
|
|
@@ -396,17 +494,21 @@ export class ConnectorClient extends EventEmitter {
|
|
|
396
494
|
if (!this.intentionalClose) {
|
|
397
495
|
this.scheduleReconnect();
|
|
398
496
|
}
|
|
399
|
-
}
|
|
497
|
+
}
|
|
400
498
|
handleError = (err) => {
|
|
401
499
|
dbg("websocket error: %s", err.message);
|
|
402
500
|
this.emit("error", err);
|
|
403
501
|
};
|
|
404
502
|
scheduleReconnect() {
|
|
503
|
+
if (this.intentionalClose || this.ws)
|
|
504
|
+
return;
|
|
405
505
|
this.clearReconnectTimer();
|
|
406
506
|
const delay = this.calculateBackoff();
|
|
407
507
|
dbg("reconnecting in %dms", Math.round(delay));
|
|
408
508
|
this.reconnectTimer = setTimeout(() => {
|
|
409
509
|
this.reconnectTimer = null;
|
|
510
|
+
if (this.intentionalClose || this.ws)
|
|
511
|
+
return;
|
|
410
512
|
this.doConnect().catch(() => {
|
|
411
513
|
});
|
|
412
514
|
}, delay);
|
|
@@ -434,28 +536,33 @@ export class ConnectorClient extends EventEmitter {
|
|
|
434
536
|
}
|
|
435
537
|
}
|
|
436
538
|
cleanupConnection() {
|
|
539
|
+
this.registration = { state: "none" };
|
|
540
|
+
if (this.keepalive) {
|
|
541
|
+
this.keepalive.stop();
|
|
542
|
+
this.keepalive = null;
|
|
543
|
+
}
|
|
437
544
|
if (this.ws) {
|
|
438
|
-
this.ws.off("message", this.handleMessage);
|
|
439
|
-
this.ws.off("close", this.handleClose);
|
|
440
|
-
this.ws.off("error", this.handleError);
|
|
441
545
|
this.ws = null;
|
|
442
546
|
}
|
|
443
547
|
if (this.registerTimer) {
|
|
444
548
|
clearTimeout(this.registerTimer);
|
|
445
549
|
this.registerTimer = null;
|
|
446
550
|
}
|
|
447
|
-
for (const [, pending] of this.pendingSends) {
|
|
551
|
+
for (const [messageId, pending] of this.pendingSends) {
|
|
448
552
|
clearTimeout(pending.timer);
|
|
449
|
-
pending.reject(new
|
|
553
|
+
pending.reject(new SendOutcomeUnknownError(messageId, "connection_closed"));
|
|
450
554
|
}
|
|
451
555
|
this.pendingSends.clear();
|
|
452
556
|
}
|
|
453
557
|
sendEvent(event) {
|
|
558
|
+
return this.sendSerializedEvent(JSON.stringify(event), event.event);
|
|
559
|
+
}
|
|
560
|
+
sendSerializedEvent(serializedEvent, eventName) {
|
|
454
561
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
455
|
-
this.ws.send(
|
|
562
|
+
this.ws.send(serializedEvent);
|
|
456
563
|
return true;
|
|
457
564
|
}
|
|
458
|
-
dbg("send failed: ws not open event=%s",
|
|
565
|
+
dbg("send failed: ws not open event=%s", eventName);
|
|
459
566
|
return false;
|
|
460
567
|
}
|
|
461
568
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ConversationManager } from "./conversation-manager.js";
|
|
2
|
+
export declare function runWithConversationManagerContext<T>(manager: ConversationManager, callback: () => T): T;
|
|
3
|
+
export declare function getInvocationConversationManager(): ConversationManager | null;
|
|
4
|
+
//# sourceMappingURL=conversation-manager-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-manager-context.d.ts","sourceRoot":"","sources":["../src/conversation-manager-context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAIrE,wBAAgB,iCAAiC,CAAC,CAAC,EACjD,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,MAAM,CAAC,GAChB,CAAC,CAEH;AAED,wBAAgB,gCAAgC,IAAI,mBAAmB,GAAG,IAAI,CAE7E"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const conversationManagerContext = new AsyncLocalStorage();
|
|
3
|
+
export function runWithConversationManagerContext(manager, callback) {
|
|
4
|
+
return conversationManagerContext.run(manager, callback);
|
|
5
|
+
}
|
|
6
|
+
export function getInvocationConversationManager() {
|
|
7
|
+
return conversationManagerContext.getStore() ?? null;
|
|
8
|
+
}
|
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
import { type ConnectorClient } from "./connector-client.js";
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { type ConnectorClient, type SendOutcomeUnknownReason } from "./connector-client.js";
|
|
2
|
+
import { type SentMessage } from "./sent-message-buffer.js";
|
|
3
|
+
import type { SendAckEvent } from "./types.js";
|
|
4
|
+
export type SendResult = {
|
|
5
|
+
status: "sent";
|
|
6
|
+
messageId: string;
|
|
7
|
+
acknowledgementStatus: string;
|
|
8
|
+
messageRef?: string;
|
|
9
|
+
addressedTarget?: NonNullable<SendAckEvent["addressedTarget"]>;
|
|
10
|
+
} | {
|
|
11
|
+
status: "unknown";
|
|
12
|
+
messageId: string;
|
|
13
|
+
attempts: 1;
|
|
14
|
+
retryable: false;
|
|
15
|
+
reason: SendOutcomeUnknownReason;
|
|
16
|
+
error: string;
|
|
17
|
+
errorCode: "SEND_OUTCOME_UNKNOWN";
|
|
18
|
+
} | {
|
|
19
|
+
status: "failed";
|
|
4
20
|
error?: string;
|
|
5
21
|
errorCode?: string;
|
|
6
|
-
}
|
|
22
|
+
};
|
|
7
23
|
export interface ConversationEntry {
|
|
8
24
|
contact: string;
|
|
9
25
|
address: string;
|
|
@@ -28,11 +44,16 @@ export interface InboundEnvelope {
|
|
|
28
44
|
export declare const REPLY_CORRELATION_MAX_AGE_MS: number;
|
|
29
45
|
export declare class ConversationManager {
|
|
30
46
|
private readonly client;
|
|
47
|
+
private readonly sentMessageBuffer;
|
|
31
48
|
private readonly conversations;
|
|
49
|
+
private readonly conversationAliases;
|
|
32
50
|
private readonly pendingOutreach;
|
|
33
51
|
constructor(client: ConnectorClient);
|
|
52
|
+
recordOutboundContext(peerAddress: string, content: string): void;
|
|
53
|
+
getRecentOutboundContext(peerAddress: string): SentMessage[];
|
|
34
54
|
send(contact: string, content: string, metadata?: Record<string, unknown>): Promise<SendResult>;
|
|
35
55
|
registerInbound(contact: string, address: string): void;
|
|
56
|
+
refreshInboundPortrayal(contact: string, address: string): boolean;
|
|
36
57
|
recordInboundEnvelope(address: string, envelope: Omit<InboundEnvelope, "receivedAt">): void;
|
|
37
58
|
getContactByAddress(address: string): string | undefined;
|
|
38
59
|
listConversations(): ConversationSummary[];
|
|
@@ -42,6 +63,10 @@ export declare class ConversationManager {
|
|
|
42
63
|
resolveAddress(contact: string): string;
|
|
43
64
|
private resolveContactForSend;
|
|
44
65
|
extractHandle(address: string): string;
|
|
66
|
+
private resolveConversationAlias;
|
|
67
|
+
private recordConversationAlias;
|
|
68
|
+
private deleteConversationAliases;
|
|
69
|
+
private fileResolvedConversation;
|
|
45
70
|
private ensureConversationEntry;
|
|
46
71
|
_resetForTesting(): void;
|
|
47
72
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation-manager.d.ts","sourceRoot":"","sources":["../src/conversation-manager.ts"],"names":[],"mappings":"AAiBA,OAAO,
|
|
1
|
+
{"version":3,"file":"conversation-manager.d.ts","sourceRoot":"","sources":["../src/conversation-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import createDebug from "debug";
|
|
2
|
-
import { ConnectorError } from "./connector-client.js";
|
|
2
|
+
import { ConnectorError, SendOutcomeUnknownError, } from "./connector-client.js";
|
|
3
3
|
import { extractHandleFromAddress } from "./handle-utils.js";
|
|
4
|
+
import { SentMessageBuffer } from "./sent-message-buffer.js";
|
|
4
5
|
import { createReplyTraceMetadata, ensureTraceMetadata, } from "./trace-metadata.js";
|
|
5
6
|
const dbg = createDebug("agent-network:conversation-manager");
|
|
6
7
|
export const REPLY_CORRELATION_MAX_AGE_MS = 10 * 60_000;
|
|
@@ -19,17 +20,68 @@ function normalizeHandleContact(contact) {
|
|
|
19
20
|
return contact.startsWith("@") ? contact.slice(1) : contact;
|
|
20
21
|
}
|
|
21
22
|
function isRoutableAddress(value) {
|
|
22
|
-
return (value.startsWith("
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
return /^mstps?:\/\//i.test(value) || value.startsWith("passport:");
|
|
24
|
+
}
|
|
25
|
+
function isAddressedTargetAddress(value) {
|
|
26
|
+
for (let index = 0; index < value.length; index++) {
|
|
27
|
+
const codeUnit = value.charCodeAt(index);
|
|
28
|
+
if (codeUnit <= 0x1f || codeUnit === 0x7f)
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (!/^mstps?:\/\//i.test(value)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(value);
|
|
36
|
+
return ((parsed.protocol === "mstp:" || parsed.protocol === "mstps:") &&
|
|
37
|
+
parsed.hostname.length > 0);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function isCanonicalResolvedNodeAddress(value) {
|
|
44
|
+
return /^mstps?:\/\/[a-z0-9.-]+(?::[0-9]+)?\/[a-z][a-z0-9_-]{2,14}$/.test(value);
|
|
45
|
+
}
|
|
46
|
+
function normalizedAddressedTarget(value) {
|
|
47
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const candidate = value;
|
|
51
|
+
if (typeof candidate.address !== "string" ||
|
|
52
|
+
candidate.address.length === 0 ||
|
|
53
|
+
(candidate.basis !== "resolved" && candidate.basis !== "forwarded")) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const validAddress = candidate.basis === "resolved"
|
|
57
|
+
? isCanonicalResolvedNodeAddress(candidate.address)
|
|
58
|
+
: isAddressedTargetAddress(candidate.address);
|
|
59
|
+
if (!validAddress) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
address: candidate.address,
|
|
64
|
+
basis: candidate.basis,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function normalizedMessageRef(value) {
|
|
68
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
25
69
|
}
|
|
26
70
|
export class ConversationManager {
|
|
27
71
|
client;
|
|
72
|
+
sentMessageBuffer = new SentMessageBuffer();
|
|
28
73
|
conversations = new Map();
|
|
74
|
+
conversationAliases = new Map();
|
|
29
75
|
pendingOutreach = new Set();
|
|
30
76
|
constructor(client) {
|
|
31
77
|
this.client = client;
|
|
32
78
|
}
|
|
79
|
+
recordOutboundContext(peerAddress, content) {
|
|
80
|
+
this.sentMessageBuffer.record(peerAddress, content);
|
|
81
|
+
}
|
|
82
|
+
getRecentOutboundContext(peerAddress) {
|
|
83
|
+
return this.sentMessageBuffer.getRecent(peerAddress);
|
|
84
|
+
}
|
|
33
85
|
async send(contact, content, metadata) {
|
|
34
86
|
const resolution = this.resolveContactForSend(contact);
|
|
35
87
|
if (resolution.ambiguous) {
|
|
@@ -74,16 +126,45 @@ export class ConversationManager {
|
|
|
74
126
|
}
|
|
75
127
|
try {
|
|
76
128
|
const ack = await this.client.send(address, content, "text", wireMetadata);
|
|
77
|
-
|
|
129
|
+
const addressedTarget = normalizedAddressedTarget(ack.addressedTarget);
|
|
130
|
+
const messageRef = normalizedMessageRef(ack.messageRef);
|
|
131
|
+
const resolvedAddress = addressedTarget?.basis === "resolved"
|
|
132
|
+
? addressedTarget.address
|
|
133
|
+
: undefined;
|
|
134
|
+
if (resolvedAddress !== undefined) {
|
|
135
|
+
if (!isRoutableAddress(address)) {
|
|
136
|
+
this.pendingOutreach.delete(normalizeHandleContact(address).toLowerCase());
|
|
137
|
+
}
|
|
138
|
+
this.fileResolvedConversation(address, resolvedAddress);
|
|
139
|
+
}
|
|
140
|
+
else if (isRoutableAddress(address)) {
|
|
78
141
|
this.ensureConversationEntry(this.extractHandle(address), address, "local");
|
|
79
142
|
}
|
|
80
143
|
else {
|
|
81
144
|
this.pendingOutreach.add(address.toLowerCase());
|
|
82
145
|
}
|
|
83
146
|
dbg("send contact=%s address=%s status=%s traceId=%s spanName=send", contact, address, ack.status, sendTraceId);
|
|
84
|
-
return {
|
|
147
|
+
return {
|
|
148
|
+
status: "sent",
|
|
149
|
+
messageId: ack.messageId,
|
|
150
|
+
acknowledgementStatus: ack.status,
|
|
151
|
+
...(messageRef !== undefined && { messageRef }),
|
|
152
|
+
...(addressedTarget !== undefined && { addressedTarget }),
|
|
153
|
+
};
|
|
85
154
|
}
|
|
86
155
|
catch (err) {
|
|
156
|
+
if (err instanceof SendOutcomeUnknownError) {
|
|
157
|
+
dbg("send outcome unknown contact=%s messageId=%s reason=%s", contact, err.messageId, err.reason);
|
|
158
|
+
return {
|
|
159
|
+
status: "unknown",
|
|
160
|
+
messageId: err.messageId,
|
|
161
|
+
attempts: err.attempts,
|
|
162
|
+
retryable: err.retryable,
|
|
163
|
+
reason: err.reason,
|
|
164
|
+
error: err.message,
|
|
165
|
+
errorCode: err.code,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
87
168
|
if (err instanceof ConnectorError) {
|
|
88
169
|
const message = formatErrorMessage(err.code, err.to ?? contact);
|
|
89
170
|
dbg("send failed contact=%s code=%s message=%s", contact, err.code, message);
|
|
@@ -98,8 +179,16 @@ export class ConversationManager {
|
|
|
98
179
|
this.ensureConversationEntry(contact, address, "remote");
|
|
99
180
|
dbg("registerInbound contact=%s address=%s", contact, address);
|
|
100
181
|
}
|
|
182
|
+
refreshInboundPortrayal(contact, address) {
|
|
183
|
+
const entry = this.conversations.get(this.resolveConversationAlias(address));
|
|
184
|
+
if (!entry)
|
|
185
|
+
return false;
|
|
186
|
+
entry.contact = contact;
|
|
187
|
+
dbg("refreshInboundPortrayal contact=%s address=%s", contact, address);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
101
190
|
recordInboundEnvelope(address, envelope) {
|
|
102
|
-
const entry = this.conversations.get(address);
|
|
191
|
+
const entry = this.conversations.get(this.resolveConversationAlias(address));
|
|
103
192
|
if (!entry) {
|
|
104
193
|
dbg("recordInboundEnvelope no entry, skipped address=%s", address);
|
|
105
194
|
return;
|
|
@@ -108,7 +197,7 @@ export class ConversationManager {
|
|
|
108
197
|
dbg("recordInboundEnvelope address=%s messageId=%s correlationId=%s", address, envelope.messageId ?? "(absent)", envelope.correlationId ?? "(absent)");
|
|
109
198
|
}
|
|
110
199
|
getContactByAddress(address) {
|
|
111
|
-
const entry = this.conversations.get(address);
|
|
200
|
+
const entry = this.conversations.get(this.resolveConversationAlias(address));
|
|
112
201
|
if (entry)
|
|
113
202
|
return entry.contact;
|
|
114
203
|
for (const [, e] of this.conversations) {
|
|
@@ -155,6 +244,7 @@ export class ConversationManager {
|
|
|
155
244
|
for (const [address, entry] of this.conversations) {
|
|
156
245
|
if (entry.ended) {
|
|
157
246
|
this.conversations.delete(address);
|
|
247
|
+
this.deleteConversationAliases(address);
|
|
158
248
|
count++;
|
|
159
249
|
}
|
|
160
250
|
}
|
|
@@ -167,7 +257,10 @@ export class ConversationManager {
|
|
|
167
257
|
}
|
|
168
258
|
resolveContactForSend(contact) {
|
|
169
259
|
if (isRoutableAddress(contact)) {
|
|
170
|
-
return {
|
|
260
|
+
return {
|
|
261
|
+
address: this.resolveConversationAlias(contact),
|
|
262
|
+
ambiguous: false,
|
|
263
|
+
};
|
|
171
264
|
}
|
|
172
265
|
const normalizedContact = normalizeHandleContact(contact);
|
|
173
266
|
if (this.conversations.has(normalizedContact)) {
|
|
@@ -194,22 +287,77 @@ export class ConversationManager {
|
|
|
194
287
|
extractHandle(address) {
|
|
195
288
|
return extractHandleFromAddress(address);
|
|
196
289
|
}
|
|
290
|
+
resolveConversationAlias(address) {
|
|
291
|
+
return this.conversationAliases.get(address) ?? address;
|
|
292
|
+
}
|
|
293
|
+
recordConversationAlias(aliasAddress, canonicalAddress) {
|
|
294
|
+
if (aliasAddress === canonicalAddress)
|
|
295
|
+
return;
|
|
296
|
+
this.conversationAliases.delete(canonicalAddress);
|
|
297
|
+
for (const [alias, target] of this.conversationAliases) {
|
|
298
|
+
if (target === aliasAddress) {
|
|
299
|
+
this.conversationAliases.set(alias, canonicalAddress);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
this.conversationAliases.set(aliasAddress, canonicalAddress);
|
|
303
|
+
}
|
|
304
|
+
deleteConversationAliases(canonicalAddress) {
|
|
305
|
+
for (const [alias, target] of this.conversationAliases) {
|
|
306
|
+
if (target === canonicalAddress)
|
|
307
|
+
this.conversationAliases.delete(alias);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
fileResolvedConversation(sentAddress, resolvedAddress) {
|
|
311
|
+
const prior = sentAddress !== resolvedAddress && isRoutableAddress(sentAddress)
|
|
312
|
+
? this.conversations.get(sentAddress)
|
|
313
|
+
: undefined;
|
|
314
|
+
const canonical = this.conversations.get(resolvedAddress);
|
|
315
|
+
if (prior)
|
|
316
|
+
this.conversations.delete(sentAddress);
|
|
317
|
+
if (sentAddress !== resolvedAddress && isRoutableAddress(sentAddress)) {
|
|
318
|
+
this.recordConversationAlias(sentAddress, resolvedAddress);
|
|
319
|
+
}
|
|
320
|
+
if (!canonical && prior) {
|
|
321
|
+
this.conversations.set(resolvedAddress, {
|
|
322
|
+
...prior,
|
|
323
|
+
address: resolvedAddress,
|
|
324
|
+
lastMessageAt: Date.now(),
|
|
325
|
+
ended: false,
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (canonical) {
|
|
330
|
+
canonical.lastMessageAt = Date.now();
|
|
331
|
+
canonical.ended = false;
|
|
332
|
+
if (prior?.initiatedBy === "local")
|
|
333
|
+
canonical.initiatedBy = "local";
|
|
334
|
+
if (prior?.inboundEnvelope &&
|
|
335
|
+
(!canonical.inboundEnvelope ||
|
|
336
|
+
prior.inboundEnvelope.receivedAt >
|
|
337
|
+
canonical.inboundEnvelope.receivedAt)) {
|
|
338
|
+
canonical.inboundEnvelope = prior.inboundEnvelope;
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
this.ensureConversationEntry(this.extractHandle(resolvedAddress), resolvedAddress, "local");
|
|
343
|
+
}
|
|
197
344
|
ensureConversationEntry(contact, address, initiatedBy) {
|
|
198
|
-
|
|
199
|
-
|
|
345
|
+
const canonicalAddress = this.resolveConversationAlias(address);
|
|
346
|
+
if (!this.conversations.has(canonicalAddress)) {
|
|
347
|
+
const outreachKey = extractHandleFromAddress(canonicalAddress).toLowerCase();
|
|
200
348
|
let origin = initiatedBy;
|
|
201
349
|
if (origin === "remote" && this.pendingOutreach.delete(outreachKey)) {
|
|
202
350
|
origin = "local";
|
|
203
351
|
}
|
|
204
|
-
this.conversations.set(
|
|
352
|
+
this.conversations.set(canonicalAddress, {
|
|
205
353
|
contact,
|
|
206
|
-
address,
|
|
354
|
+
address: canonicalAddress,
|
|
207
355
|
lastMessageAt: Date.now(),
|
|
208
356
|
initiatedBy: origin,
|
|
209
357
|
});
|
|
210
358
|
}
|
|
211
359
|
else {
|
|
212
|
-
const entry = this.conversations.get(
|
|
360
|
+
const entry = this.conversations.get(canonicalAddress);
|
|
213
361
|
if (entry) {
|
|
214
362
|
entry.lastMessageAt = Date.now();
|
|
215
363
|
entry.ended = false;
|
|
@@ -218,5 +366,6 @@ export class ConversationManager {
|
|
|
218
366
|
}
|
|
219
367
|
_resetForTesting() {
|
|
220
368
|
this.conversations.clear();
|
|
369
|
+
this.conversationAliases.clear();
|
|
221
370
|
}
|
|
222
371
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BridgeHandoffResult } from "./handoff.js";
|
|
2
|
+
export interface AcceptBridgeHandoffOptions {
|
|
3
|
+
handoff: BridgeHandoffResult;
|
|
4
|
+
apiHost: string;
|
|
5
|
+
connectorUrl: string;
|
|
6
|
+
idpBaseUrl: string;
|
|
7
|
+
pendingTarget?: string;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function acceptBridgeHandoff(options: AcceptBridgeHandoffOptions): Promise<string>;
|
|
11
|
+
//# sourceMappingURL=handoff-acceptance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff-acceptance.d.ts","sourceRoot":"","sources":["../src/handoff-acceptance.ts"],"names":[],"mappings":"AAgDA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAiCxD,MAAM,WAAW,0BAA0B;IAEzC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IAEnB,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAOD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CAmBjB"}
|