@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.
- package/dist/_vendor/runtime-adapter-client/conformance.d.ts +8 -0
- package/dist/_vendor/runtime-adapter-client/conformance.d.ts.map +1 -0
- package/dist/_vendor/runtime-adapter-client/conformance.js +75 -0
- package/dist/_vendor/runtime-adapter-client/icm.d.ts +6 -0
- package/dist/_vendor/runtime-adapter-client/icm.d.ts.map +1 -0
- package/dist/_vendor/runtime-adapter-client/icm.js +86 -0
- package/dist/_vendor/runtime-adapter-client/index.d.ts +7 -0
- package/dist/_vendor/runtime-adapter-client/index.d.ts.map +1 -0
- package/dist/_vendor/runtime-adapter-client/index.js +6 -0
- package/dist/_vendor/runtime-adapter-client/runtime-adapter-api.d.ts +300 -0
- package/dist/_vendor/runtime-adapter-client/runtime-adapter-api.d.ts.map +1 -0
- package/dist/_vendor/runtime-adapter-client/runtime-adapter-api.js +3112 -0
- package/dist/_vendor/runtime-adapter-client/send-admission.d.ts +89 -0
- package/dist/_vendor/runtime-adapter-client/send-admission.d.ts.map +1 -0
- package/dist/_vendor/runtime-adapter-client/send-admission.js +169 -0
- package/dist/_vendor/runtime-adapter-client/types.d.ts +632 -0
- package/dist/_vendor/runtime-adapter-client/types.d.ts.map +1 -0
- package/dist/_vendor/runtime-adapter-client/types.js +33 -0
- package/dist/_vendor/runtime-adapter-client/work-target.d.ts +5 -0
- package/dist/_vendor/runtime-adapter-client/work-target.d.ts.map +1 -0
- package/dist/_vendor/runtime-adapter-client/work-target.js +47 -0
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +103 -31
- package/dist/connector-client.d.ts +2 -4
- package/dist/connector-client.d.ts.map +1 -1
- package/dist/connector-client.js +9 -62
- package/dist/connector-url-boundary.d.ts +0 -5
- package/dist/connector-url-boundary.d.ts.map +1 -1
- package/dist/connector-url-boundary.js +0 -13
- package/dist/conversation-manager.d.ts +2 -2
- package/dist/conversation-manager.d.ts.map +1 -1
- package/dist/conversation-manager.js +24 -16
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +26 -13
- package/dist/services-retained-recipient.d.ts +58 -0
- package/dist/services-retained-recipient.d.ts.map +1 -0
- package/dist/services-retained-recipient.js +324 -0
- package/dist/turn-context.d.ts +4 -0
- package/dist/turn-context.d.ts.map +1 -1
- package/dist/turn-context.js +18 -0
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
172
|
-
if (
|
|
173
|
-
|
|
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: {
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"
|
|
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 =
|
|
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 =
|
|
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
|
|
69
|
-
|
|
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
|
-
|
|
207
|
-
"
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
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
|
+
}
|
package/dist/turn-context.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/turn-context.js
CHANGED
|
@@ -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
|
}
|