@masons/agent-network 0.6.14 → 0.6.16

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 (47) hide show
  1. package/dist/_vendor/runtime-adapter-client/conformance.d.ts +8 -0
  2. package/dist/_vendor/runtime-adapter-client/conformance.d.ts.map +1 -0
  3. package/dist/_vendor/runtime-adapter-client/conformance.js +75 -0
  4. package/dist/_vendor/runtime-adapter-client/icm.d.ts +6 -0
  5. package/dist/_vendor/runtime-adapter-client/icm.d.ts.map +1 -0
  6. package/dist/_vendor/runtime-adapter-client/icm.js +86 -0
  7. package/dist/_vendor/runtime-adapter-client/index.d.ts +7 -0
  8. package/dist/_vendor/runtime-adapter-client/index.d.ts.map +1 -0
  9. package/dist/_vendor/runtime-adapter-client/index.js +6 -0
  10. package/dist/_vendor/runtime-adapter-client/runtime-adapter-api.d.ts +300 -0
  11. package/dist/_vendor/runtime-adapter-client/runtime-adapter-api.d.ts.map +1 -0
  12. package/dist/_vendor/runtime-adapter-client/runtime-adapter-api.js +3112 -0
  13. package/dist/_vendor/runtime-adapter-client/send-admission.d.ts +89 -0
  14. package/dist/_vendor/runtime-adapter-client/send-admission.d.ts.map +1 -0
  15. package/dist/_vendor/runtime-adapter-client/send-admission.js +169 -0
  16. package/dist/_vendor/runtime-adapter-client/types.d.ts +632 -0
  17. package/dist/_vendor/runtime-adapter-client/types.d.ts.map +1 -0
  18. package/dist/_vendor/runtime-adapter-client/types.js +33 -0
  19. package/dist/_vendor/runtime-adapter-client/work-target.d.ts +5 -0
  20. package/dist/_vendor/runtime-adapter-client/work-target.d.ts.map +1 -0
  21. package/dist/_vendor/runtime-adapter-client/work-target.js +47 -0
  22. package/dist/channel.d.ts.map +1 -1
  23. package/dist/channel.js +103 -31
  24. package/dist/connector-client.d.ts +2 -4
  25. package/dist/connector-client.d.ts.map +1 -1
  26. package/dist/connector-client.js +9 -62
  27. package/dist/connector-url-boundary.d.ts +0 -5
  28. package/dist/connector-url-boundary.d.ts.map +1 -1
  29. package/dist/connector-url-boundary.js +0 -13
  30. package/dist/conversation-manager.d.ts +2 -2
  31. package/dist/conversation-manager.d.ts.map +1 -1
  32. package/dist/conversation-manager.js +24 -16
  33. package/dist/plugin.d.ts +1 -0
  34. package/dist/plugin.d.ts.map +1 -1
  35. package/dist/plugin.js +26 -13
  36. package/dist/services-retained-recipient.d.ts +58 -0
  37. package/dist/services-retained-recipient.d.ts.map +1 -0
  38. package/dist/services-retained-recipient.js +324 -0
  39. package/dist/turn-context.d.ts +4 -0
  40. package/dist/turn-context.d.ts.map +1 -1
  41. package/dist/turn-context.js +18 -0
  42. package/dist/types.d.ts +6 -1
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js +3 -0
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/package.json +3 -3
@@ -18,13 +18,17 @@ function formatErrorMessage(code, to) {
18
18
  function normalizeHandleContact(contact) {
19
19
  return contact.startsWith("@") ? contact.slice(1) : contact;
20
20
  }
21
+ function isRoutableAddress(value) {
22
+ return (value.startsWith("mstps://") ||
23
+ value.startsWith("mstp://") ||
24
+ value.startsWith("passport:"));
25
+ }
21
26
  export class ConversationManager {
22
27
  client;
23
- mstpAuthority;
24
28
  conversations = new Map();
25
- constructor(client, mstpAuthority) {
29
+ pendingOutreach = new Set();
30
+ constructor(client) {
26
31
  this.client = client;
27
- this.mstpAuthority = mstpAuthority;
28
32
  }
29
33
  async send(contact, content, metadata) {
30
34
  const resolution = this.resolveContactForSend(contact);
@@ -70,7 +74,12 @@ export class ConversationManager {
70
74
  }
71
75
  try {
72
76
  const ack = await this.client.send(address, content, "text", wireMetadata);
73
- this.ensureConversationEntry(this.extractHandle(address), address, "local");
77
+ if (isRoutableAddress(address)) {
78
+ this.ensureConversationEntry(this.extractHandle(address), address, "local");
79
+ }
80
+ else {
81
+ this.pendingOutreach.add(address.toLowerCase());
82
+ }
74
83
  dbg("send contact=%s address=%s status=%s traceId=%s spanName=send", contact, address, ack.status, sendTraceId);
75
84
  return { status: "sent" };
76
85
  }
@@ -157,9 +166,7 @@ export class ConversationManager {
157
166
  return this.resolveContactForSend(contact).address;
158
167
  }
159
168
  resolveContactForSend(contact) {
160
- if (contact.startsWith("mstps://") ||
161
- contact.startsWith("mstp://") ||
162
- contact.startsWith("passport:")) {
169
+ if (isRoutableAddress(contact)) {
163
170
  return { address: contact, ambiguous: false };
164
171
  }
165
172
  const normalizedContact = normalizeHandleContact(contact);
@@ -168,10 +175,9 @@ export class ConversationManager {
168
175
  }
169
176
  let first;
170
177
  let ambiguous = false;
171
- for (const [addr, entry] of this.conversations) {
172
- if (entry.contact !== normalizedContact &&
173
- extractHandleFromAddress(addr).toLowerCase() !==
174
- normalizedContact.toLowerCase()) {
178
+ for (const addr of this.conversations.keys()) {
179
+ if (extractHandleFromAddress(addr).toLowerCase() !==
180
+ normalizedContact.toLowerCase()) {
175
181
  continue;
176
182
  }
177
183
  if (first === undefined) {
@@ -183,21 +189,23 @@ export class ConversationManager {
183
189
  }
184
190
  if (first !== undefined)
185
191
  return { address: first, ambiguous };
186
- return {
187
- address: `mstps://${this.mstpAuthority}/${normalizedContact}`,
188
- ambiguous: false,
189
- };
192
+ return { address: normalizedContact, ambiguous: false };
190
193
  }
191
194
  extractHandle(address) {
192
195
  return extractHandleFromAddress(address);
193
196
  }
194
197
  ensureConversationEntry(contact, address, initiatedBy) {
195
198
  if (!this.conversations.has(address)) {
199
+ const outreachKey = extractHandleFromAddress(address).toLowerCase();
200
+ let origin = initiatedBy;
201
+ if (origin === "remote" && this.pendingOutreach.delete(outreachKey)) {
202
+ origin = "local";
203
+ }
196
204
  this.conversations.set(address, {
197
205
  contact,
198
206
  address,
199
207
  lastMessageAt: Date.now(),
200
- initiatedBy,
208
+ initiatedBy: origin,
201
209
  });
202
210
  }
203
211
  else {
package/dist/plugin.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export declare function buildInteractionContext(channelId: string | undefined, turnSender: string | null, turnSenderAddress: string | null, turnReplyRoute?: "routable" | "unavailable" | null): string | undefined;
1
2
  interface OpenClawPluginApi {
2
3
  runtime: unknown;
3
4
  registerChannel(opts: {
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAqJA,UAAU,iBAAiB;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACjD,YAAY,CACV,IAAI,EAAE,OAAO,EACb,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAC3C,IAAI,CAAC;IACR,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,GAAG,IAAI,CAAC;CACnE;AAED,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAaI,iBAAiB;CAmYhC,CAAC;AAEF,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAsFA,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,iBAAiB,EAAE,MAAM,GAAG,IAAI,EAChC,cAAc,GAAE,UAAU,GAAG,aAAa,GAAG,IAAW,GACvD,MAAM,GAAG,SAAS,CA+EpB;AAKD,UAAU,iBAAiB;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACjD,YAAY,CACV,IAAI,EAAE,OAAO,EACb,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAC3C,IAAI,CAAC;IACR,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,GAAG,IAAI,CAAC;CACnE;AAED,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAaI,iBAAiB;CA2YhC,CAAC;AAEF,eAAe,MAAM,CAAC"}
package/dist/plugin.js CHANGED
@@ -7,7 +7,7 @@ import { ownerNotesQueue } from "./owner-notes.js";
7
7
  import { consumeCurrentTurnIsOwner, consumeCurrentTurnOwnerSignal, } from "./owner-session-state.js";
8
8
  import { sentMessageBuffer } from "./sent-message-buffer.js";
9
9
  import { registerTools } from "./tools.js";
10
- import { consumeCurrentTurnSender, setCurrentTurnChannelId, setCurrentTurnIsOwnerForTools, setCurrentTurnOwnerSignalForTools, } from "./turn-context.js";
10
+ import { consumeCurrentTurnReplyRoute, consumeCurrentTurnSender, consumeCurrentTurnSenderAddress, setCurrentTurnChannelId, setCurrentTurnIsOwnerForTools, setCurrentTurnOwnerSignalForTools, } from "./turn-context.js";
11
11
  import { getUpdateInfo } from "./update-check.js";
12
12
  import { PLUGIN_VERSION } from "./version.js";
13
13
  function formatTimeAgo(timestamp) {
@@ -38,7 +38,7 @@ function drainAndFormatNotes(reportInstruction) {
38
38
  reportInstruction);
39
39
  }
40
40
  const MAX_INTERACTIONS = 5;
41
- function buildInteractionContext(channelId, turnSender) {
41
+ export function buildInteractionContext(channelId, turnSender, turnSenderAddress, turnReplyRoute = null) {
42
42
  const cm = getConversationManager();
43
43
  if (!cm)
44
44
  return undefined;
@@ -53,20 +53,27 @@ function buildInteractionContext(channelId, turnSender) {
53
53
  ? `[Active interactions (showing ${sorted.length} of ${conversations.length})]`
54
54
  : "[Active interactions]";
55
55
  const lines = sorted.map((c) => {
56
- const who = `@${c.contact}`;
56
+ const who = c.contact === c.address ? c.address : `${c.contact} (${c.address})`;
57
57
  const initiated = c.initiatedBy === "local" ? "you initiated" : "they initiated";
58
58
  const when = formatTimeAgo(c.lastMessageAt);
59
59
  return `• ${who} — ${initiated} — last activity ${when}`;
60
60
  });
61
61
  const isNetworkTurn = channelId === "agent-network";
62
62
  if (isNetworkTurn && turnSender) {
63
- const senderConvo = conversations.find((c) => c.contact === turnSender);
63
+ const senderConvo = turnSenderAddress
64
+ ? conversations.find((c) => c.address === turnSenderAddress)
65
+ : undefined;
64
66
  const initiated = senderConvo?.initiatedBy === "local"
65
67
  ? "You initiated this interaction."
66
68
  : "They initiated this interaction.";
69
+ const from = turnSenderAddress && turnSenderAddress !== turnSender
70
+ ? `${turnSender} (${turnSenderAddress})`
71
+ : turnSender;
67
72
  return (`${header}\n${lines.join("\n")}\n` +
68
- `[This turn] From @${turnSender}. ${initiated}\n` +
69
- "Your text reply goes to this sender. " +
73
+ `[This turn] From ${from}. ${initiated}\n` +
74
+ (turnReplyRoute === "unavailable"
75
+ ? "No reply route is available for this message. "
76
+ : "Your text reply goes to this sender. ") +
70
77
  "To report to your owner: masons_note_for_owner.");
71
78
  }
72
79
  if (isNetworkTurn) {
@@ -137,6 +144,8 @@ const plugin = {
137
144
  const turnIsOwner = consumeCurrentTurnIsOwner();
138
145
  const turnOwnerSignal = consumeCurrentTurnOwnerSignal();
139
146
  const turnSender = consumeCurrentTurnSender();
147
+ const turnSenderAddress = consumeCurrentTurnSenderAddress();
148
+ const turnReplyRoute = consumeCurrentTurnReplyRoute();
140
149
  setCurrentTurnChannelId(channelId ?? null);
141
150
  setCurrentTurnIsOwnerForTools(turnIsOwner);
142
151
  setCurrentTurnOwnerSignalForTools(turnOwnerSignal);
@@ -203,11 +212,15 @@ const plugin = {
203
212
  }
204
213
  else {
205
214
  dynamicContext =
206
- "[Agent Network — Routing] This turn was triggered by a message " +
207
- "on the agent network. Your text reply will be sent to that sender — " +
208
- "your owner (Principal) will NOT see it. To report something to your owner, " +
209
- "call masons_note_for_owner.";
210
- const interactionCtx = buildInteractionContext(channelId, turnSender);
215
+ turnReplyRoute === "unavailable"
216
+ ? "[Agent Network Services-retained message] This canonical message carries no reply address. " +
217
+ "A plain text reply will NOT be routed to its sender. Do not infer or fabricate an address or Session. " +
218
+ "Treat the message as inbound information; to report something to your owner, call masons_note_for_owner."
219
+ : "[Agent Network Routing] This turn was triggered by a message " +
220
+ "on the agent network. Your text reply will be sent to that sender — " +
221
+ "your owner (Principal) will NOT see it. To report something to your owner, " +
222
+ "call masons_note_for_owner.";
223
+ const interactionCtx = buildInteractionContext(channelId, turnSender, turnSenderAddress, turnReplyRoute);
211
224
  if (interactionCtx) {
212
225
  dynamicContext += `\n\n${interactionCtx}`;
213
226
  }
@@ -221,7 +234,7 @@ const plugin = {
221
234
  return `• (${formatTimeAgo(m.timestamp)}): "${preview}"`;
222
235
  });
223
236
  dynamicContext +=
224
- `\n\n[Your recent messages to @${turnSender}]\n` +
237
+ `\n\n[Your recent messages to ${turnSender}]\n` +
225
238
  lines.join("\n");
226
239
  }
227
240
  }
@@ -242,7 +255,7 @@ const plugin = {
242
255
  ? `${dynamicContext}\n\n${nudge}`
243
256
  : nudge;
244
257
  }
245
- const interactionCtx = buildInteractionContext(channelId, null);
258
+ const interactionCtx = buildInteractionContext(channelId, null, null);
246
259
  if (interactionCtx) {
247
260
  dynamicContext = dynamicContext
248
261
  ? `${dynamicContext}\n\n${interactionCtx}`
@@ -0,0 +1,58 @@
1
+ import { type RuntimeExactTargetPresentationEnvelopeV1 } from "./_vendor/runtime-adapter-client/index.js";
2
+ export interface ServicesRetainedRecipientPresentation {
3
+ contentType: string;
4
+ envelope: RuntimeExactTargetPresentationEnvelopeV1;
5
+ }
6
+ export interface ServicesRetainedRecipientLogger {
7
+ info(message: string): void;
8
+ warn(message: string): void;
9
+ error(message: string, error?: unknown): void;
10
+ }
11
+ export interface ServicesRetainedRecipientOptions {
12
+ apiHost: string;
13
+ runtimeKey: string;
14
+ present: (message: ServicesRetainedRecipientPresentation) => Promise<boolean>;
15
+ logger: ServicesRetainedRecipientLogger;
16
+ endpointNonce?: string;
17
+ retryDelayMs?: number;
18
+ minimumHeartbeatDelayMs?: number;
19
+ fetch?: typeof fetch;
20
+ }
21
+ export declare class ServicesRetainedRecipientConsumer {
22
+ private readonly clientConfig;
23
+ private readonly runtimeKey;
24
+ private readonly present;
25
+ private readonly logger;
26
+ private readonly endpointNonce;
27
+ private readonly retryDelayMs;
28
+ private readonly minimumHeartbeatDelayMs;
29
+ private endpoint;
30
+ private presentedOffer;
31
+ private acquirePermanentlyBlocked;
32
+ private consumptionPermanentlyBlocked;
33
+ private consecutiveHeartbeatNotFound;
34
+ private stopped;
35
+ private activationPromise;
36
+ private drainPromise;
37
+ private drainRequested;
38
+ private activationRetryTimer;
39
+ private drainRetryTimer;
40
+ private heartbeatTimer;
41
+ private stopPromise;
42
+ constructor(options: ServicesRetainedRecipientOptions);
43
+ start(): Promise<void>;
44
+ stop(): Promise<void>;
45
+ requestDrain(): Promise<void>;
46
+ private activate;
47
+ private drain;
48
+ private consumePresentedOffer;
49
+ private scheduleHeartbeat;
50
+ private heartbeat;
51
+ private recoverEndpoint;
52
+ private scheduleActivationRetry;
53
+ private scheduleDrainRetry;
54
+ private clearActivationRetryTimer;
55
+ private clearDrainRetryTimer;
56
+ private clearHeartbeatTimer;
57
+ }
58
+ //# sourceMappingURL=services-retained-recipient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"services-retained-recipient.d.ts","sourceRoot":"","sources":["../src/services-retained-recipient.ts"],"names":[],"mappings":"AACA,OAAO,EAML,KAAK,wCAAwC,EAI9C,MAAM,2CAA2C,CAAC;AAKnD,MAAM,WAAW,qCAAqC;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,wCAAwC,CAAC;CACpD;AAED,MAAM,WAAW,+BAA+B;IAC9C,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC/C;AAED,MAAM,WAAW,gCAAgC;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,CAAC,OAAO,EAAE,qCAAqC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9E,MAAM,EAAE,+BAA+B,CAAC;IACxC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAaD,qBAAa,iCAAiC;IAC5C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA6B;IAC1D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8C;IACtE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkC;IACzD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAS;IAEjD,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,yBAAyB,CAAS;IAC1C,OAAO,CAAC,6BAA6B,CAAS;IAC9C,OAAO,CAAC,4BAA4B,CAAK;IACzC,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,iBAAiB,CAA8B;IACvD,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,oBAAoB,CAA8C;IAC1E,OAAO,CAAC,eAAe,CAA8C;IACrE,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,WAAW,CAA8B;gBAErC,OAAO,EAAE,gCAAgC;IAc/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAStB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiCrB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;YAerB,QAAQ;YAoDR,KAAK;YA0EL,qBAAqB;IAwDnC,OAAO,CAAC,iBAAiB;YAcX,SAAS;IAgCvB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,yBAAyB;IAKjC,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,mBAAmB;CAI5B"}
@@ -0,0 +1,324 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { acquireServicesRetainedNodeRecipientMessage, consumeServicesRetainedNodeRecipientMessage, heartbeatRuntimeEndpoint, RuntimeAdapterApiError, registerRuntimeEndpoint, unregisterRuntimeEndpoint, } from "./_vendor/runtime-adapter-client/index.js";
3
+ const RETRY_DELAY_MS = 5_000;
4
+ const MIN_HEARTBEAT_DELAY_MS = 1_000;
5
+ export class ServicesRetainedRecipientConsumer {
6
+ clientConfig;
7
+ runtimeKey;
8
+ present;
9
+ logger;
10
+ endpointNonce;
11
+ retryDelayMs;
12
+ minimumHeartbeatDelayMs;
13
+ endpoint = null;
14
+ presentedOffer = null;
15
+ acquirePermanentlyBlocked = false;
16
+ consumptionPermanentlyBlocked = false;
17
+ consecutiveHeartbeatNotFound = 0;
18
+ stopped = true;
19
+ activationPromise = null;
20
+ drainPromise = null;
21
+ drainRequested = false;
22
+ activationRetryTimer = null;
23
+ drainRetryTimer = null;
24
+ heartbeatTimer = null;
25
+ stopPromise = null;
26
+ constructor(options) {
27
+ this.clientConfig = {
28
+ apiHost: options.apiHost,
29
+ ...(options.fetch && { fetch: options.fetch }),
30
+ };
31
+ this.runtimeKey = options.runtimeKey;
32
+ this.present = options.present;
33
+ this.logger = options.logger;
34
+ this.endpointNonce = options.endpointNonce ?? randomUUID();
35
+ this.retryDelayMs = options.retryDelayMs ?? RETRY_DELAY_MS;
36
+ this.minimumHeartbeatDelayMs =
37
+ options.minimumHeartbeatDelayMs ?? MIN_HEARTBEAT_DELAY_MS;
38
+ }
39
+ async start() {
40
+ if (!this.stopped)
41
+ return this.activationPromise ?? Promise.resolve();
42
+ const pendingStop = this.stopPromise;
43
+ if (pendingStop)
44
+ await pendingStop;
45
+ if (!this.stopped)
46
+ return this.activationPromise ?? Promise.resolve();
47
+ this.stopped = false;
48
+ await this.activate();
49
+ }
50
+ async stop() {
51
+ if (this.stopPromise)
52
+ return this.stopPromise;
53
+ this.stopped = true;
54
+ this.drainRequested = false;
55
+ this.clearActivationRetryTimer();
56
+ this.clearDrainRetryTimer();
57
+ this.clearHeartbeatTimer();
58
+ const endpoint = this.endpoint;
59
+ this.endpoint = null;
60
+ this.stopPromise = (async () => {
61
+ await this.activationPromise?.catch(() => undefined);
62
+ await this.drainPromise?.catch(() => undefined);
63
+ if (!endpoint)
64
+ return;
65
+ try {
66
+ await unregisterRuntimeEndpoint(this.clientConfig, this.runtimeKey, endpoint.id);
67
+ }
68
+ catch (error) {
69
+ this.logger.warn(`failed to withdraw Services-retained endpoint ${endpoint.id}: ${errorText(error)}`);
70
+ }
71
+ })().finally(() => {
72
+ this.stopPromise = null;
73
+ this.presentedOffer = null;
74
+ this.acquirePermanentlyBlocked = false;
75
+ this.consumptionPermanentlyBlocked = false;
76
+ });
77
+ return this.stopPromise;
78
+ }
79
+ async requestDrain() {
80
+ if (this.stopped)
81
+ return;
82
+ this.drainRequested = true;
83
+ if (!this.endpoint) {
84
+ await this.activate();
85
+ return;
86
+ }
87
+ if (this.drainPromise)
88
+ return this.drainPromise;
89
+ this.drainPromise = this.drain().finally(() => {
90
+ this.drainPromise = null;
91
+ if (this.drainRequested && !this.stopped)
92
+ void this.requestDrain();
93
+ });
94
+ return this.drainPromise;
95
+ }
96
+ async activate() {
97
+ if (this.stopped || this.endpoint)
98
+ return;
99
+ if (this.activationPromise)
100
+ return this.activationPromise;
101
+ this.activationPromise = (async () => {
102
+ try {
103
+ const response = await registerRuntimeEndpoint(this.clientConfig, this.runtimeKey, {
104
+ runtime_kind: "openclaw",
105
+ endpoint_nonce: this.endpointNonce,
106
+ display_label: "OpenClaw",
107
+ runtime_capabilities: ["node_recipient_admission_v1"],
108
+ node_recipient_admission_contract_id: "target-node-recipient-services-retained-v1",
109
+ });
110
+ if (this.stopped) {
111
+ await unregisterRuntimeEndpoint(this.clientConfig, this.runtimeKey, response.endpoint_id).catch(() => undefined);
112
+ return;
113
+ }
114
+ this.endpoint = {
115
+ id: response.endpoint_id,
116
+ nonce: this.endpointNonce,
117
+ heartbeatAfterSeconds: response.heartbeat_after_seconds,
118
+ };
119
+ this.consecutiveHeartbeatNotFound = 0;
120
+ this.acquirePermanentlyBlocked = false;
121
+ this.clearActivationRetryTimer();
122
+ this.logger.info(`Services-retained recipient active endpoint=${response.endpoint_id}`);
123
+ this.scheduleHeartbeat();
124
+ this.drainRequested = true;
125
+ }
126
+ catch (error) {
127
+ this.logger.error("Services-retained recipient declaration failed; live Connector participation remains available", error);
128
+ this.scheduleActivationRetry();
129
+ }
130
+ })().finally(() => {
131
+ this.activationPromise = null;
132
+ });
133
+ await this.activationPromise;
134
+ if (this.endpoint && this.drainRequested)
135
+ await this.requestDrain();
136
+ }
137
+ async drain() {
138
+ while (!this.stopped && this.endpoint) {
139
+ this.drainRequested = false;
140
+ const endpoint = this.endpoint;
141
+ if (this.presentedOffer) {
142
+ if (this.consumptionPermanentlyBlocked)
143
+ return;
144
+ if (!(await this.consumePresentedOffer(endpoint)))
145
+ return;
146
+ continue;
147
+ }
148
+ if (this.acquirePermanentlyBlocked)
149
+ return;
150
+ let acquired;
151
+ try {
152
+ acquired = await acquireServicesRetainedNodeRecipientMessage(this.clientConfig, this.runtimeKey, {
153
+ endpoint_id: endpoint.id,
154
+ endpoint_nonce: endpoint.nonce,
155
+ });
156
+ }
157
+ catch (error) {
158
+ this.logger.error("Services-retained acquire failed", error);
159
+ this.scheduleDrainRetry();
160
+ return;
161
+ }
162
+ if (this.stopped || this.endpoint !== endpoint)
163
+ return;
164
+ if (acquired.status === "empty")
165
+ return;
166
+ if (acquired.status === "rejected") {
167
+ this.logger.warn(`Services-retained acquire rejected reason=${acquired.reason} retryable=${String(acquired.retryable)}`);
168
+ if (acquired.reason === "endpoint_unavailable") {
169
+ this.recoverEndpoint(endpoint);
170
+ }
171
+ else {
172
+ this.acquirePermanentlyBlocked = true;
173
+ this.logger.error(`Services-retained acquire stopped on permanent provider rejection reason=${acquired.reason}`);
174
+ }
175
+ return;
176
+ }
177
+ let presented = false;
178
+ try {
179
+ presented = await this.present({
180
+ contentType: acquired.content_type,
181
+ envelope: acquired.envelope,
182
+ });
183
+ }
184
+ catch (error) {
185
+ this.logger.error(`OpenClaw inbound presentation failed message_ref=${acquired.message_ref}`, error);
186
+ }
187
+ if (!presented) {
188
+ this.scheduleDrainRetry();
189
+ return;
190
+ }
191
+ this.presentedOffer = {
192
+ messageRef: acquired.message_ref,
193
+ offerRef: acquired.offer_ref,
194
+ };
195
+ this.consumptionPermanentlyBlocked = false;
196
+ if (!(await this.consumePresentedOffer(endpoint)))
197
+ return;
198
+ }
199
+ }
200
+ async consumePresentedOffer(endpoint) {
201
+ const offer = this.presentedOffer;
202
+ if (!offer)
203
+ return true;
204
+ const outcome = await consumeServicesRetainedNodeRecipientMessage(this.clientConfig, this.runtimeKey, {
205
+ type: "runtime_node_recipient_message_consumption",
206
+ version: 1,
207
+ endpoint_id: endpoint.id,
208
+ endpoint_nonce: endpoint.nonce,
209
+ message_ref: offer.messageRef,
210
+ offer_ref: offer.offerRef,
211
+ });
212
+ if (outcome.ok) {
213
+ this.logger.info(`Services-retained message consumed message_ref=${offer.messageRef} status=${outcome.status}`);
214
+ this.presentedOffer = null;
215
+ this.consumptionPermanentlyBlocked = false;
216
+ this.drainRequested = true;
217
+ return true;
218
+ }
219
+ const reason = "providerStatus" in outcome ? outcome.reason : undefined;
220
+ this.logger.warn(`Services-retained consumption failed message_ref=${offer.messageRef} terminal=${String(outcome.terminal)}${reason ? ` reason=${reason}` : ""}`);
221
+ if (reason === "endpoint_unavailable") {
222
+ this.recoverEndpoint(endpoint);
223
+ return false;
224
+ }
225
+ if (reason === "processing_conflict" || !outcome.terminal) {
226
+ this.scheduleDrainRetry();
227
+ return false;
228
+ }
229
+ if (reason === "message_terminal") {
230
+ this.presentedOffer = null;
231
+ this.consumptionPermanentlyBlocked = false;
232
+ this.drainRequested = true;
233
+ return true;
234
+ }
235
+ this.consumptionPermanentlyBlocked = true;
236
+ this.logger.error(`Services-retained consumption stopped on permanent rejection message_ref=${offer.messageRef}${reason ? ` reason=${reason}` : ""}`);
237
+ return false;
238
+ }
239
+ scheduleHeartbeat() {
240
+ this.clearHeartbeatTimer();
241
+ if (this.stopped || !this.endpoint)
242
+ return;
243
+ const delay = Math.max(this.minimumHeartbeatDelayMs, this.endpoint.heartbeatAfterSeconds * 1_000);
244
+ this.heartbeatTimer = setTimeout(() => {
245
+ this.heartbeatTimer = null;
246
+ void this.heartbeat();
247
+ }, delay);
248
+ this.heartbeatTimer.unref?.();
249
+ }
250
+ async heartbeat() {
251
+ const endpoint = this.endpoint;
252
+ if (this.stopped || !endpoint)
253
+ return;
254
+ try {
255
+ await heartbeatRuntimeEndpoint(this.clientConfig, this.runtimeKey, endpoint.id);
256
+ this.consecutiveHeartbeatNotFound = 0;
257
+ void this.requestDrain();
258
+ }
259
+ catch (error) {
260
+ this.logger.warn(`Services-retained endpoint heartbeat failed endpoint=${endpoint.id}: ${errorText(error)}`);
261
+ if (error instanceof RuntimeAdapterApiError &&
262
+ error.status === 404 &&
263
+ error.code === "not_found") {
264
+ this.consecutiveHeartbeatNotFound++;
265
+ if (this.consecutiveHeartbeatNotFound >= 2) {
266
+ this.recoverEndpoint(endpoint);
267
+ }
268
+ }
269
+ else {
270
+ this.consecutiveHeartbeatNotFound = 0;
271
+ }
272
+ }
273
+ finally {
274
+ if (this.endpoint === endpoint)
275
+ this.scheduleHeartbeat();
276
+ }
277
+ }
278
+ recoverEndpoint(endpoint) {
279
+ if (this.stopped || this.endpoint !== endpoint)
280
+ return;
281
+ this.endpoint = null;
282
+ this.acquirePermanentlyBlocked = false;
283
+ this.consumptionPermanentlyBlocked = false;
284
+ this.consecutiveHeartbeatNotFound = 0;
285
+ this.clearHeartbeatTimer();
286
+ this.scheduleActivationRetry();
287
+ }
288
+ scheduleActivationRetry() {
289
+ if (this.stopped || this.activationRetryTimer)
290
+ return;
291
+ this.activationRetryTimer = setTimeout(() => {
292
+ this.activationRetryTimer = null;
293
+ void this.activate();
294
+ }, this.retryDelayMs);
295
+ this.activationRetryTimer.unref?.();
296
+ }
297
+ scheduleDrainRetry() {
298
+ if (this.stopped || this.drainRetryTimer)
299
+ return;
300
+ this.drainRetryTimer = setTimeout(() => {
301
+ this.drainRetryTimer = null;
302
+ void this.requestDrain();
303
+ }, this.retryDelayMs);
304
+ this.drainRetryTimer.unref?.();
305
+ }
306
+ clearActivationRetryTimer() {
307
+ if (this.activationRetryTimer)
308
+ clearTimeout(this.activationRetryTimer);
309
+ this.activationRetryTimer = null;
310
+ }
311
+ clearDrainRetryTimer() {
312
+ if (this.drainRetryTimer)
313
+ clearTimeout(this.drainRetryTimer);
314
+ this.drainRetryTimer = null;
315
+ }
316
+ clearHeartbeatTimer() {
317
+ if (this.heartbeatTimer)
318
+ clearTimeout(this.heartbeatTimer);
319
+ this.heartbeatTimer = null;
320
+ }
321
+ }
322
+ function errorText(error) {
323
+ return error instanceof Error ? error.message : String(error);
324
+ }
@@ -1,9 +1,13 @@
1
1
  import type { OwnerSignal } from "./owner-session-state.js";
2
2
  export declare function setCurrentTurnSender(contact: string | null): void;
3
+ export declare function setCurrentTurnSenderAddress(address: string | null): void;
4
+ export declare function setCurrentTurnReplyRoute(route: "routable" | "unavailable"): void;
3
5
  export declare function setCurrentTurnChannelId(channelId: string | null): void;
4
6
  export declare function setCurrentTurnIsOwnerForTools(value: boolean): void;
5
7
  export declare function setCurrentTurnOwnerSignalForTools(signal: OwnerSignal): void;
6
8
  export declare function consumeCurrentTurnSender(): string | null;
9
+ export declare function consumeCurrentTurnSenderAddress(): string | null;
10
+ export declare function consumeCurrentTurnReplyRoute(): "routable" | "unavailable" | null;
7
11
  export declare function getCurrentTurnChannelId(): string | null;
8
12
  export declare function getCurrentTurnIsOwnerNonConsuming(): boolean;
9
13
  export declare function getCurrentTurnOwnerSignal(): OwnerSignal;
@@ -1 +1 @@
1
- {"version":3,"file":"turn-context.d.ts","sourceRoot":"","sources":["../src/turn-context.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAwC5D,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEjE;AAOD,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEtE;AAOD,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAElE;AAUD,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAE3E;AAUD,wBAAgB,wBAAwB,IAAI,MAAM,GAAG,IAAI,CAIxD;AAMD,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAEvD;AAMD,wBAAgB,iCAAiC,IAAI,OAAO,CAE3D;AAOD,wBAAgB,yBAAyB,IAAI,WAAW,CAEvD"}
1
+ {"version":3,"file":"turn-context.d.ts","sourceRoot":"","sources":["../src/turn-context.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AA0C5D,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEjE;AAUD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAExE;AAGD,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,UAAU,GAAG,aAAa,GAChC,IAAI,CAEN;AAOD,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEtE;AAOD,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAElE;AAUD,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAE3E;AAUD,wBAAgB,wBAAwB,IAAI,MAAM,GAAG,IAAI,CAIxD;AAGD,wBAAgB,+BAA+B,IAAI,MAAM,GAAG,IAAI,CAI/D;AAGD,wBAAgB,4BAA4B,IACxC,UAAU,GACV,aAAa,GACb,IAAI,CAIP;AAMD,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAEvD;AAMD,wBAAgB,iCAAiC,IAAI,OAAO,CAE3D;AAOD,wBAAgB,yBAAyB,IAAI,WAAW,CAEvD"}
@@ -1,10 +1,18 @@
1
1
  let currentTurnSender = null;
2
+ let currentTurnSenderAddress = null;
3
+ let currentTurnReplyRoute = null;
2
4
  let currentTurnChannelId = null;
3
5
  let currentTurnIsOwner = false;
4
6
  let currentTurnOwnerSignalForTools = null;
5
7
  export function setCurrentTurnSender(contact) {
6
8
  currentTurnSender = contact;
7
9
  }
10
+ export function setCurrentTurnSenderAddress(address) {
11
+ currentTurnSenderAddress = address;
12
+ }
13
+ export function setCurrentTurnReplyRoute(route) {
14
+ currentTurnReplyRoute = route;
15
+ }
8
16
  export function setCurrentTurnChannelId(channelId) {
9
17
  currentTurnChannelId = channelId;
10
18
  }
@@ -19,6 +27,16 @@ export function consumeCurrentTurnSender() {
19
27
  currentTurnSender = null;
20
28
  return value;
21
29
  }
30
+ export function consumeCurrentTurnSenderAddress() {
31
+ const value = currentTurnSenderAddress;
32
+ currentTurnSenderAddress = null;
33
+ return value;
34
+ }
35
+ export function consumeCurrentTurnReplyRoute() {
36
+ const value = currentTurnReplyRoute;
37
+ currentTurnReplyRoute = null;
38
+ return value;
39
+ }
22
40
  export function getCurrentTurnChannelId() {
23
41
  return currentTurnChannelId;
24
42
  }