@relaymessenger/openclaw-plugin 0.4.7-staging.40 → 0.4.7-staging.41

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.
@@ -7,8 +7,8 @@
7
7
  },
8
8
  "relaySdk": {
9
9
  "package": "@relaymessenger/sdk",
10
- "version": "0.3.6-staging.47",
11
- "integrity": "sha512-CI67lgtiuYyhSwuttqXF6x07OC9fDwTIpT3eCZPiqHGxWDo6Tx/zNmAAUzEDcw9ExoGOHP2VXnIlcDFMlQrslw==",
10
+ "version": "0.3.6-staging.48",
11
+ "integrity": "sha512-1IZ3eLkdNGi6u8PzRBGET40IwxiDLBb3tuQG9V+RXUfyizw5AiWKF1vWg0pm2gz7jQsSFwyMWlyGynNOZZOnOQ==",
12
12
  "operationsSha256": "3e09d345c249709d942d2efb89ed7e84934c19552767d8f4438eb086dd917e1c",
13
13
  "workspaceOpenapiSha256": "44d3202de07206707c8914c37029abc365c144be4245e677574210d2a1cf6f41",
14
14
  "usedOperations": [
@@ -60,8 +60,10 @@ async function readReplyTarget(params) {
60
60
  * OpenClaw's own reply context, `supplemental.quote`, which it renders to the
61
61
  * model as "Reply target of current user message" (id, sender, body), as its
62
62
  * Telegram channel fills it from Telegram's `reply_to_message`. A reply names
63
- * one bubble: when the target has more than one part, only the swiped part is
64
- * quoted, the rule Relay's iOS app uses to draw the quote.
63
+ * one bubble: a multipart target is narrowed to the swiped part, the rule
64
+ * Relay's iOS app uses to draw the quote (the SDK's `replyTargetParts`); a
65
+ * tap or a selection answer names a part with no words, so it keeps the
66
+ * whole Message.
65
67
  */
66
68
  export function relayReplyQuote(facts, target) {
67
69
  if (!target || target.chat_id !== facts.chatId)
@@ -70,7 +72,7 @@ export function relayReplyQuote(facts, target) {
70
72
  const swiped = parts.length > 1 && facts.replyToPartIndex !== undefined
71
73
  ? parts[facts.replyToPartIndex]
72
74
  : undefined;
73
- const body = renderRelayMessageParts(swiped ? [swiped] : parts);
75
+ const body = renderRelayMessageParts(swiped && swiped.type !== "buttons" && swiped.type !== "selection" ? [swiped] : parts);
74
76
  const sender = target.from_handle?.display_name?.trim()
75
77
  || target.from_handle?.handle
76
78
  || target.from
@@ -82,6 +84,16 @@ export function relayReplyQuote(facts, target) {
82
84
  senderAllowed: true,
83
85
  };
84
86
  }
87
+ /**
88
+ * Whether the part a reply names may itself be replied to. A tap names the
89
+ * agent's buttons part and a selection answer its selection part; no reply may
90
+ * point at those (Relay v1 ReplyTo.part_index), so a person's tap keeps its
91
+ * own Message as the reply target.
92
+ */
93
+ function repliable(target, partIndex) {
94
+ const part = target?.parts?.[partIndex ?? 0];
95
+ return part !== undefined && part.type !== "buttons" && part.type !== "selection";
96
+ }
85
97
  /**
86
98
  * The answer to another agent names the Message it answers: the model's own
87
99
  * reply target when it chose one, else the agent's Message. Where no reply may
@@ -202,18 +214,23 @@ export async function dispatchRelayEvent(params) {
202
214
  params.warn?.(`relay: Contact @${facts.handle} did not pass OpenClaw ingress (${access.ingress.decision}:${access.ingress.reasonCode})`);
203
215
  return;
204
216
  }
205
- // An agent's reply target is only its own Message (agentReplyLink); a
206
- // person's is the Message they replied from, as before.
217
+ // An agent's reply target is only its own Message (agentReplyLink). A
218
+ // person's is the Message they replied to, OpenClaw's ReplyToId, as its
219
+ // Telegram channel sets `reply.replyToId` to Telegram's reply_to_message:
220
+ // the prompt names it beside the quote. A tap or a selection answer names a
221
+ // part no reply may target, so it keeps the person's own Message.
222
+ const quote = relayReplyQuote(facts, repliedTo);
207
223
  const replyTarget = facts.fromAgent
208
224
  ? facts.agentReplyLink
209
- : facts.replyAnchorId ?? facts.replyToId;
225
+ : quote && repliable(repliedTo, facts.replyToPartIndex)
226
+ ? quote.id
227
+ : facts.replyAnchorId ?? facts.replyToId;
210
228
  const body = buildEnvelope({
211
229
  channel: "Relay",
212
230
  from: `${facts.displayName} (@${facts.handle})`,
213
231
  ...(facts.timestamp ? { timestamp: facts.timestamp } : {}),
214
232
  body: facts.text,
215
233
  });
216
- const quote = relayReplyQuote(facts, repliedTo);
217
234
  const ctxPayload = buildChannelInboundEventContext({
218
235
  channel: "relay",
219
236
  accountId: route.accountId ?? params.account.accountId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relaymessenger/openclaw-plugin",
3
- "version": "0.4.7-staging.40",
3
+ "version": "0.4.7-staging.41",
4
4
  "description": "Native Relay channel plugin for OpenClaw",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "prepack": "npm run build"
38
38
  },
39
39
  "dependencies": {
40
- "@relaymessenger/sdk": "0.3.6-staging.47"
40
+ "@relaymessenger/sdk": "0.3.6-staging.48"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.6.2",
package/src/dispatch.ts CHANGED
@@ -114,8 +114,10 @@ async function readReplyTarget(params: {
114
114
  * OpenClaw's own reply context, `supplemental.quote`, which it renders to the
115
115
  * model as "Reply target of current user message" (id, sender, body), as its
116
116
  * Telegram channel fills it from Telegram's `reply_to_message`. A reply names
117
- * one bubble: when the target has more than one part, only the swiped part is
118
- * quoted, the rule Relay's iOS app uses to draw the quote.
117
+ * one bubble: a multipart target is narrowed to the swiped part, the rule
118
+ * Relay's iOS app uses to draw the quote (the SDK's `replyTargetParts`); a
119
+ * tap or a selection answer names a part with no words, so it keeps the
120
+ * whole Message.
119
121
  */
120
122
  export function relayReplyQuote(
121
123
  facts: Pick<RelayInboundFacts, "chatId" | "replyToPartIndex">,
@@ -126,7 +128,9 @@ export function relayReplyQuote(
126
128
  const swiped = parts.length > 1 && facts.replyToPartIndex !== undefined
127
129
  ? parts[facts.replyToPartIndex]
128
130
  : undefined;
129
- const body = renderRelayMessageParts(swiped ? [swiped] : parts);
131
+ const body = renderRelayMessageParts(
132
+ swiped && swiped.type !== "buttons" && swiped.type !== "selection" ? [swiped] : parts,
133
+ );
130
134
  const sender = target.from_handle?.display_name?.trim()
131
135
  || target.from_handle?.handle
132
136
  || target.from
@@ -139,6 +143,17 @@ export function relayReplyQuote(
139
143
  };
140
144
  }
141
145
 
146
+ /**
147
+ * Whether the part a reply names may itself be replied to. A tap names the
148
+ * agent's buttons part and a selection answer its selection part; no reply may
149
+ * point at those (Relay v1 ReplyTo.part_index), so a person's tap keeps its
150
+ * own Message as the reply target.
151
+ */
152
+ function repliable(target: Message | undefined, partIndex: number | undefined): boolean {
153
+ const part = target?.parts?.[partIndex ?? 0];
154
+ return part !== undefined && part.type !== "buttons" && part.type !== "selection";
155
+ }
156
+
142
157
  /**
143
158
  * The answer to another agent names the Message it answers: the model's own
144
159
  * reply target when it chose one, else the agent's Message. Where no reply may
@@ -281,18 +296,23 @@ export async function dispatchRelayEvent(params: {
281
296
  return;
282
297
  }
283
298
 
284
- // An agent's reply target is only its own Message (agentReplyLink); a
285
- // person's is the Message they replied from, as before.
299
+ // An agent's reply target is only its own Message (agentReplyLink). A
300
+ // person's is the Message they replied to, OpenClaw's ReplyToId, as its
301
+ // Telegram channel sets `reply.replyToId` to Telegram's reply_to_message:
302
+ // the prompt names it beside the quote. A tap or a selection answer names a
303
+ // part no reply may target, so it keeps the person's own Message.
304
+ const quote = relayReplyQuote(facts, repliedTo);
286
305
  const replyTarget = facts.fromAgent
287
306
  ? facts.agentReplyLink
288
- : facts.replyAnchorId ?? facts.replyToId;
307
+ : quote && repliable(repliedTo, facts.replyToPartIndex)
308
+ ? quote.id
309
+ : facts.replyAnchorId ?? facts.replyToId;
289
310
  const body = buildEnvelope({
290
311
  channel: "Relay",
291
312
  from: `${facts.displayName} (@${facts.handle})`,
292
313
  ...(facts.timestamp ? { timestamp: facts.timestamp } : {}),
293
314
  body: facts.text,
294
315
  });
295
- const quote = relayReplyQuote(facts, repliedTo);
296
316
  const ctxPayload = buildChannelInboundEventContext({
297
317
  channel: "relay",
298
318
  accountId: route.accountId ?? params.account.accountId,