@relaymessenger/openclaw-plugin 0.4.7-staging.1 → 0.4.7-staging.11

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.
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "relayServer": {
3
3
  "repository": "RelayMessenger/Relay-Server",
4
- "commit": "55e12f23fdb559e23e54e4f386a77fd397293834",
4
+ "commit": "eb83978b6b2c625da82471e4af16acad8de0e618",
5
5
  "openapiPath": "contracts/developer/openapi.yaml",
6
- "sha256": "de33237b05b09414c1994446f746795ab8bf410cb2c8f1422259103775cfc182"
6
+ "sha256": "27698655d12500fb9cd2e10dbf1c94025fbc64c288df6151db673a7649877111"
7
7
  },
8
8
  "relaySdk": {
9
9
  "package": "@relaymessenger/sdk",
10
- "version": "0.3.6-staging.1",
11
- "integrity": "sha512-aGHnNGlKR2M7d6BiRChQ8bG2FBYKm/lSAeBL/9cKJ8O7gwKCrysDWuFhQExoSg+riIT0DiOlNh2hVdHfSWLOKQ==",
12
- "operationsSha256": "7b131c1d40779b204fb3cf3ebc0cc6c6f2df19555618958fd17b0c81209452b9",
13
- "workspaceOpenapiSha256": "de33237b05b09414c1994446f746795ab8bf410cb2c8f1422259103775cfc182",
10
+ "version": "0.3.6-staging.9",
11
+ "integrity": "sha512-3tqlPQV4G3mkHvENP2j1gPKg9Rtstuc+yrSUk0aWRK+cXy4110Pr5LaNy8ea7nPgGpkF/1JoUnxjHtA0L7HHzA==",
12
+ "operationsSha256": "04e5e1f1f626a2b0607fb0d091f12a4205a9e298fdb6f56e5bfcc446c9fe5ee3",
13
+ "workspaceOpenapiSha256": "27698655d12500fb9cd2e10dbf1c94025fbc64c288df6151db673a7649877111",
14
14
  "usedOperations": [
15
15
  {
16
16
  "method": "GET",
@@ -249,6 +249,7 @@ export const relayChannelPlugin = createChatChannelPlugin({
249
249
  deliveryQueueId: ctx.deliveryQueueId,
250
250
  deliveryPartIndex: ctx.deliveryPartIndex,
251
251
  }),
252
+ onButtonsError: (error) => ctx.log?.warn?.(`relay: buttons block left as text: ${error}`),
252
253
  ...(ctx.onPlatformSendDispatch
253
254
  ? { onPlatformSendDispatch: ctx.onPlatformSendDispatch }
254
255
  : {}),
@@ -170,7 +170,9 @@ export async function dispatchRelayEvent(params) {
170
170
  reply: {
171
171
  to: facts.chatId,
172
172
  originatingTo: facts.chatId,
173
- ...(facts.replyToId ? { replyToId: facts.replyToId } : {}),
173
+ ...((facts.replyAnchorId ?? facts.replyToId)
174
+ ? { replyToId: facts.replyAnchorId ?? facts.replyToId }
175
+ : {}),
174
176
  },
175
177
  message: {
176
178
  inboundEventKind: "user_request",
@@ -8,6 +8,10 @@ function renderPart(part) {
8
8
  return `[Attachment: ${part.filename} (${part.mime_type})] ${part.url}`;
9
9
  case "system":
10
10
  return part.value;
11
+ // The agent's own buttons part reads as nothing, as on the server; its
12
+ // question is the text beside it. A tap arrives as ordinary text.
13
+ case "buttons":
14
+ return undefined;
11
15
  }
12
16
  }
13
17
  export function renderRelayMessageParts(parts) {
@@ -61,7 +65,14 @@ export function buildRelayInboundFacts(event) {
61
65
  ? { ownerHandle: event.data.chat.owner_handle }
62
66
  : {}),
63
67
  ...(event.data.reply_to?.message_id
64
- ? { replyToId: event.data.reply_to.message_id }
68
+ ? {
69
+ replyToId: event.data.reply_to.message_id,
70
+ // The answer quotes the person's message, the one it answers (a bot's
71
+ // reply_to in Telegram and Discord names the person's message). A
72
+ // tap's reply_to names the agent's buttons part, which no reply may
73
+ // target, so this is also what keeps a tap answerable.
74
+ replyAnchorId: event.data.id,
75
+ }
65
76
  : {}),
66
77
  ...(Number.isFinite(timestamp) ? { timestamp } : {}),
67
78
  };
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { Relay, RelayAPIError, } from "@relaymessenger/sdk";
2
+ import { answerMessages, indexedIdempotencyKey, Relay, RelayAPIError, } from "@relaymessenger/sdk";
3
3
  export const RELAY_TEXT_CHUNK_LIMIT = 10_000;
4
4
  const IDEMPOTENCY_KEY_MAX_LENGTH = 255;
5
5
  export function createRelaySdkClient(account) {
@@ -17,17 +17,35 @@ export function deriveRelayIdempotencyKey(params) {
17
17
  ? raw
18
18
  : `relay-openclaw:sha256:${createHash("sha256").update(raw).digest("hex")}`;
19
19
  }
20
+ /**
21
+ * OpenClaw hands the agent's words as text, so buttons ride in them as the
22
+ * SDK's fenced block, lifted here into the buttons part, and a link written
23
+ * alone on a line goes out as its own link Message. A block that cannot be
24
+ * read stays in the words and is reported through `onButtonsError`. Without
25
+ * a block or a link line the words go exactly as OpenClaw handed them, in one
26
+ * Message; the response is the first Message's, the one the reply anchors to.
27
+ */
20
28
  export async function sendRelayText(params) {
21
29
  await params.onPlatformSendDispatch?.();
22
- return await params.relay.chats.messages.send(params.chatId, {
23
- message: {
24
- parts: [{ type: "text", value: params.text }],
25
- idempotency_key: params.idempotencyKey,
26
- ...(params.replyToId
27
- ? { reply_to: { message_id: params.replyToId } }
28
- : {}),
29
- },
30
- }, params.signal ? { signal: params.signal } : undefined);
30
+ const { messages, error } = answerMessages(params.text);
31
+ if (error)
32
+ params.onButtonsError?.(error);
33
+ if (messages.length === 0)
34
+ messages.push([{ type: "text", value: params.text }]);
35
+ let first;
36
+ for (const [index, parts] of messages.entries()) {
37
+ const response = await params.relay.chats.messages.send(params.chatId, {
38
+ message: {
39
+ parts,
40
+ idempotency_key: indexedIdempotencyKey(params.idempotencyKey, index),
41
+ ...(index === 0 && params.replyToId
42
+ ? { reply_to: { message_id: params.replyToId } }
43
+ : {}),
44
+ },
45
+ }, params.signal ? { signal: params.signal } : undefined);
46
+ first ??= response;
47
+ }
48
+ return first;
31
49
  }
32
50
  export function classifyUnknownRelaySend(error) {
33
51
  if (!(error instanceof RelayAPIError)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relaymessenger/openclaw-plugin",
3
- "version": "0.4.7-staging.1",
3
+ "version": "0.4.7-staging.11",
4
4
  "description": "Native Relay channel plugin for OpenClaw",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,11 +37,11 @@
37
37
  "prepack": "npm run build"
38
38
  },
39
39
  "dependencies": {
40
- "@relaymessenger/sdk": "0.3.6-staging.1"
40
+ "@relaymessenger/sdk": "0.3.6-staging.9"
41
41
  },
42
42
  "devDependencies": {
43
- "@types/node": "^26.0.0",
44
- "openclaw": "2026.8.1",
43
+ "@types/node": "^26.6.2",
44
+ "openclaw": "2026.9.5",
45
45
  "typescript": "^7.0.2",
46
46
  "vitest": "^4.1.10"
47
47
  },
@@ -164,7 +164,7 @@
164
164
  "pluginApi": ">=2026.8.1 <2026.10.0"
165
165
  },
166
166
  "build": {
167
- "openclawVersion": "2026.8.1"
167
+ "openclawVersion": "2026.9.5"
168
168
  }
169
169
  }
170
170
  }
package/src/channel.ts CHANGED
@@ -329,6 +329,10 @@ export const relayChannelPlugin: ChannelPlugin<ResolvedRelayAccount> =
329
329
  deliveryQueueId: ctx.deliveryQueueId,
330
330
  deliveryPartIndex: ctx.deliveryPartIndex,
331
331
  }),
332
+ onButtonsError: (error) =>
333
+ (ctx as { log?: { warn?: (message: string) => void } }).log?.warn?.(
334
+ `relay: buttons block left as text: ${error}`,
335
+ ),
332
336
  ...(ctx.onPlatformSendDispatch
333
337
  ? { onPlatformSendDispatch: ctx.onPlatformSendDispatch }
334
338
  : {}),
package/src/dispatch.ts CHANGED
@@ -236,7 +236,9 @@ export async function dispatchRelayEvent(params: {
236
236
  reply: {
237
237
  to: facts.chatId,
238
238
  originatingTo: facts.chatId,
239
- ...(facts.replyToId ? { replyToId: facts.replyToId } : {}),
239
+ ...((facts.replyAnchorId ?? facts.replyToId)
240
+ ? { replyToId: facts.replyAnchorId ?? facts.replyToId }
241
+ : {}),
240
242
  },
241
243
  message: {
242
244
  inboundEventKind: "user_request",
package/src/inbound.ts CHANGED
@@ -17,6 +17,10 @@ function renderPart(part: MessagePartResponse): string | undefined {
17
17
  return `[Attachment: ${part.filename} (${part.mime_type})] ${part.url}`;
18
18
  case "system":
19
19
  return part.value;
20
+ // The agent's own buttons part reads as nothing, as on the server; its
21
+ // question is the text beside it. A tap arrives as ordinary text.
22
+ case "buttons":
23
+ return undefined;
20
24
  }
21
25
  }
22
26
 
@@ -84,7 +88,14 @@ export function buildRelayInboundFacts(
84
88
  ? { ownerHandle: event.data.chat.owner_handle }
85
89
  : {}),
86
90
  ...(event.data.reply_to?.message_id
87
- ? { replyToId: event.data.reply_to.message_id }
91
+ ? {
92
+ replyToId: event.data.reply_to.message_id,
93
+ // The answer quotes the person's message, the one it answers (a bot's
94
+ // reply_to in Telegram and Discord names the person's message). A
95
+ // tap's reply_to names the agent's buttons part, which no reply may
96
+ // target, so this is also what keeps a tap answerable.
97
+ replyAnchorId: event.data.id,
98
+ }
88
99
  : {}),
89
100
  ...(Number.isFinite(timestamp) ? { timestamp } : {}),
90
101
  };
package/src/outbound.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import {
3
+ answerMessages,
4
+ indexedIdempotencyKey,
3
5
  Relay,
4
6
  RelayAPIError,
5
7
  type MessageSendResponse,
@@ -32,6 +34,14 @@ export function deriveRelayIdempotencyKey(params: {
32
34
  : `relay-openclaw:sha256:${createHash("sha256").update(raw).digest("hex")}`;
33
35
  }
34
36
 
37
+ /**
38
+ * OpenClaw hands the agent's words as text, so buttons ride in them as the
39
+ * SDK's fenced block, lifted here into the buttons part, and a link written
40
+ * alone on a line goes out as its own link Message. A block that cannot be
41
+ * read stays in the words and is reported through `onButtonsError`. Without
42
+ * a block or a link line the words go exactly as OpenClaw handed them, in one
43
+ * Message; the response is the first Message's, the one the reply anchors to.
44
+ */
35
45
  export async function sendRelayText(params: {
36
46
  relay: Pick<Relay, "chats">;
37
47
  chatId: string;
@@ -40,21 +50,30 @@ export async function sendRelayText(params: {
40
50
  idempotencyKey: string;
41
51
  signal?: AbortSignal;
42
52
  onPlatformSendDispatch?: () => Promise<void>;
53
+ onButtonsError?: (error: string) => void;
43
54
  }): Promise<MessageSendResponse> {
44
55
  await params.onPlatformSendDispatch?.();
45
- return await params.relay.chats.messages.send(
46
- params.chatId,
47
- {
48
- message: {
49
- parts: [{ type: "text", value: params.text }],
50
- idempotency_key: params.idempotencyKey,
51
- ...(params.replyToId
52
- ? { reply_to: { message_id: params.replyToId } }
53
- : {}),
56
+ const { messages, error } = answerMessages(params.text);
57
+ if (error) params.onButtonsError?.(error);
58
+ if (messages.length === 0) messages.push([{ type: "text", value: params.text }]);
59
+ let first: MessageSendResponse | undefined;
60
+ for (const [index, parts] of messages.entries()) {
61
+ const response = await params.relay.chats.messages.send(
62
+ params.chatId,
63
+ {
64
+ message: {
65
+ parts,
66
+ idempotency_key: indexedIdempotencyKey(params.idempotencyKey, index),
67
+ ...(index === 0 && params.replyToId
68
+ ? { reply_to: { message_id: params.replyToId } }
69
+ : {}),
70
+ },
54
71
  },
55
- },
56
- params.signal ? { signal: params.signal } : undefined,
57
- );
72
+ params.signal ? { signal: params.signal } : undefined,
73
+ );
74
+ first ??= response;
75
+ }
76
+ return first!;
58
77
  }
59
78
 
60
79
  export function classifyUnknownRelaySend(error: unknown): {
package/src/types.ts CHANGED
@@ -72,5 +72,11 @@ export type RelayInboundFacts = {
72
72
  mentionHandles: string[];
73
73
  ownerHandle?: ChatHandle;
74
74
  replyToId?: string;
75
+ /**
76
+ * The Message an outbound reply should quote when the person's Message
77
+ * was itself a reply: the person's Message. A tap's reply_to names the
78
+ * agent's buttons part, which no reply may target.
79
+ */
80
+ replyAnchorId?: string;
75
81
  timestamp?: number;
76
82
  };