@masons/agent-network 0.6.19 → 0.6.21

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.
Files changed (54) hide show
  1. package/dist/channel-setup.d.ts.map +1 -1
  2. package/dist/channel-setup.js +2 -1
  3. package/dist/channel.d.ts.map +1 -1
  4. package/dist/channel.js +54 -44
  5. package/dist/cli-setup.d.ts.map +1 -1
  6. package/dist/cli-setup.js +18 -4
  7. package/dist/config-fs.d.ts +1 -1
  8. package/dist/config-fs.d.ts.map +1 -1
  9. package/dist/config-fs.js +2 -1
  10. package/dist/config.d.ts +3 -1
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +8 -2
  13. package/dist/connector-client.d.ts +35 -4
  14. package/dist/connector-client.d.ts.map +1 -1
  15. package/dist/connector-client.js +126 -19
  16. package/dist/conversation-manager-context.d.ts +4 -0
  17. package/dist/conversation-manager-context.d.ts.map +1 -0
  18. package/dist/conversation-manager-context.js +8 -0
  19. package/dist/conversation-manager.d.ts +29 -4
  20. package/dist/conversation-manager.d.ts.map +1 -1
  21. package/dist/conversation-manager.js +163 -14
  22. package/dist/handoff-deadline.d.ts +4 -0
  23. package/dist/handoff-deadline.d.ts.map +1 -0
  24. package/dist/handoff-deadline.js +7 -0
  25. package/dist/handoff.d.ts +1 -2
  26. package/dist/handoff.d.ts.map +1 -1
  27. package/dist/handoff.js +20 -10
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/platform-client.d.ts +1 -0
  31. package/dist/platform-client.d.ts.map +1 -1
  32. package/dist/platform-client.js +12 -0
  33. package/dist/plugin.d.ts +2 -1
  34. package/dist/plugin.d.ts.map +1 -1
  35. package/dist/plugin.js +49 -30
  36. package/dist/sent-message-buffer.d.ts +2 -3
  37. package/dist/sent-message-buffer.d.ts.map +1 -1
  38. package/dist/sent-message-buffer.js +8 -11
  39. package/dist/tools.d.ts.map +1 -1
  40. package/dist/tools.js +101 -7
  41. package/dist/turn-context.d.ts +0 -4
  42. package/dist/turn-context.d.ts.map +1 -1
  43. package/dist/turn-context.js +0 -18
  44. package/dist/types.d.ts +10 -1
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/types.js +3 -1
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/dist/ws-heartbeat.d.ts +23 -0
  50. package/dist/ws-heartbeat.d.ts.map +1 -0
  51. package/dist/ws-heartbeat.js +63 -0
  52. package/openclaw.plugin.json +2 -1
  53. package/package.json +1 -1
  54. package/skills/agent-network/SKILL.md +9 -0
@@ -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 Error("SEND_ACK timeout"));
124
+ reject(new SendOutcomeUnknownError(messageId, "ack_timeout"));
82
125
  }, SEND_ACK_TIMEOUT_MS);
83
126
  this.pendingSends.set(messageId, { resolve, reject, timer });
84
- const sent = this.sendEvent(event);
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", this.handleMessage);
133
- ws.on("close", this.handleClose);
134
- ws.on("error", this.handleError);
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 = (code, reason) => {
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 Error("Connection closed"));
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(JSON.stringify(event));
562
+ this.ws.send(serializedEvent);
456
563
  return true;
457
564
  }
458
- dbg("send failed: ws not open event=%s", event.event);
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
- export interface SendResult {
3
- status: "sent" | "failed";
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,EAAE,KAAK,eAAe,EAAkB,MAAM,uBAAuB,CAAC;AAa7E,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;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;IAchC,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;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;AAuBD,MAAM,WAAW,eAAe;IAE9B,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AASD,eAAO,MAAM,4BAA4B,QAAc,CAAC;AA0CxD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IAGzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAwC;IAiBtE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;gBAEzC,MAAM,EAAE,eAAe;IAsB7B,IAAI,CACR,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,OAAO,CAAC,UAAU,CAAC;IA0HtB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAYvD,qBAAqB,CACnB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,YAAY,CAAC,GAC5C,IAAI;IAmBP,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAkBxD,iBAAiB,IAAI,mBAAmB,EAAE;IAwB1C,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAiBzC,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAoCzC,wBAAwB,IAAI,IAAI;IAuChC,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAwBvC,OAAO,CAAC,qBAAqB;IAuC7B,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAQtC,OAAO,CAAC,uBAAuB;IAiC/B,gBAAgB,IAAI,IAAI;CAGzB"}
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("mstps://") ||
23
- value.startsWith("mstp://") ||
24
- value.startsWith("passport:"));
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
- if (isRoutableAddress(address)) {
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 { status: "sent" };
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 { address: contact, ambiguous: false };
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
- if (!this.conversations.has(address)) {
199
- const outreachKey = extractHandleFromAddress(address).toLowerCase();
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(address, {
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(address);
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,4 @@
1
+ export declare class SetupFlowError extends Error {
2
+ }
3
+ export declare function assertHandoffDeadlineActive(expiresAt: number): void;
4
+ //# sourceMappingURL=handoff-deadline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff-deadline.d.ts","sourceRoot":"","sources":["../src/handoff-deadline.ts"],"names":[],"mappings":"AAAA,qBAAa,cAAe,SAAQ,KAAK;CAAG;AAG5C,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAMnE"}
@@ -0,0 +1,7 @@
1
+ export class SetupFlowError extends Error {
2
+ }
3
+ export function assertHandoffDeadlineActive(expiresAt) {
4
+ if (Date.now() >= expiresAt) {
5
+ throw new SetupFlowError("Handoff session expired before the runtime key was delivered. Re-run setup.");
6
+ }
7
+ }