clawgram 2.3.3 → 2.4.1

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/channel.js CHANGED
@@ -470,6 +470,12 @@ const createChannelPlugin = (runtimes) => {
470
470
  MessageSidFull: normalized.messageId,
471
471
  Timestamp: normalized.timestamp,
472
472
  ReplyToId: normalized.replyToMessageId,
473
+ // Core renders these itself as `[Replying to: "…"]` ahead of the
474
+ // user body — it keys off Provider being "telegram", which is set
475
+ // below. Without them a highlighted reply reaches the agent as
476
+ // bare text, and the fragment the person pointed at is lost.
477
+ ReplyToQuoteText: normalized.replyQuoteText,
478
+ ReplyToIsQuote: normalized.replyIsQuote,
473
479
  MessageThreadId: normalized.messageThreadId,
474
480
  NativeChannelId: normalized.chatId,
475
481
  OriginatingChannel: "clawgram",
@@ -530,6 +536,10 @@ const createChannelPlugin = (runtimes) => {
530
536
  channel: "clawgram",
531
537
  accountId: route.accountId ?? accountId,
532
538
  });
539
+ // Boundary for the transcript fallback below: only replies
540
+ // written after this instant may be salvaged. Same clock as
541
+ // the transcript writer — both live in this process.
542
+ const dispatchStartedAt = Date.now();
533
543
  const dispatchResult = await dispatchBase.dispatchReplyWithBufferedBlockDispatcher({
534
544
  ctx: ctxPayload,
535
545
  cfg,
@@ -598,7 +608,7 @@ const createChannelPlugin = (runtimes) => {
598
608
  (dispatchCounts.block ?? 0) === 0 &&
599
609
  (dispatchCounts.final ?? 0) === 0;
600
610
  if (nothingDelivered) {
601
- const fallbackText = (0, helpers_1.readLatestAssistantFallbackFromTranscript)(route.sessionKey, storePath);
611
+ const fallbackText = (0, helpers_1.readLatestAssistantFallbackFromTranscript)(route.sessionKey, storePath, dispatchStartedAt);
602
612
  // A suppressed silent reply legitimately delivers nothing, so
603
613
  // this fallback fires right after it. Without the same check
604
614
  // the token would be read back from the transcript and sent.
@@ -746,6 +756,10 @@ const createChannelPlugin = (runtimes) => {
746
756
  SenderUsername: normalized.senderUsername,
747
757
  SenderName: normalized.senderDisplay,
748
758
  ReplyToId: normalized.replyToMessageId,
759
+ // Same reason as the group path: highlighted replies happen in
760
+ // direct messages too, and the fragment is not part of the text.
761
+ ReplyToQuoteText: normalized.replyQuoteText,
762
+ ReplyToIsQuote: normalized.replyIsQuote,
749
763
  NativeChannelId: normalized.chatId,
750
764
  },
751
765
  deliver: async (payload) => {
package/dist/helpers.js CHANGED
@@ -110,7 +110,17 @@ function resolveTranscriptPathFromStoreEntry(input) {
110
110
  return undefined;
111
111
  }
112
112
  }
113
- function readLatestAssistantFallbackFromTranscript(sessionKey, storePath) {
113
+ /**
114
+ * Salvages a reply that reached the transcript but not stdout.
115
+ *
116
+ * `notBeforeMs` is the start of the current dispatch. Only entries stamped at
117
+ * or after it count: on a turn that aborted with zero output, the newest
118
+ * assistant entry is by definition from an earlier turn, and re-sending it
119
+ * addresses an old answer to a new question (the 2026-08-06 duplicates,
120
+ * note 0054 in the control repo). An entry with no parseable timestamp cannot
121
+ * prove it is fresh, so it does not count either.
122
+ */
123
+ function readLatestAssistantFallbackFromTranscript(sessionKey, storePath, notBeforeMs) {
114
124
  if (!storePath?.trim()) {
115
125
  return undefined;
116
126
  }
@@ -135,6 +145,14 @@ function readLatestAssistantFallbackFromTranscript(sessionKey, storePath) {
135
145
  if (entry?.type !== "message" || entry?.message?.role !== "assistant" || !Array.isArray(entry.message.content)) {
136
146
  continue;
137
147
  }
148
+ if (notBeforeMs !== undefined) {
149
+ const stamped = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : NaN;
150
+ if (!Number.isFinite(stamped) || stamped < notBeforeMs) {
151
+ // Entries are appended in order; everything below this one is
152
+ // older still. Stop instead of walking further into the past.
153
+ return undefined;
154
+ }
155
+ }
138
156
  const textPart = entry.message.content.find((part) => part?.type === "text" && typeof part.text === "string" && part.text.trim());
139
157
  if (!textPart?.text) {
140
158
  continue;
package/dist/history.js CHANGED
@@ -227,6 +227,14 @@ function resolveSenderDisplay(msg) {
227
227
  return joined;
228
228
  return readSender(msg, "title") ?? readSender(msg, "username");
229
229
  }
230
+ /**
231
+ * Same rule as the inbound path (`normalize.ts`): the text is the evidence,
232
+ * not the `quote` flag, and blank text means no highlight.
233
+ */
234
+ function resolveHistoryReplyQuote(msg) {
235
+ const raw = msg?.replyTo?.quoteText ?? msg?.quoteText;
236
+ return typeof raw === "string" && raw.trim() ? raw : undefined;
237
+ }
230
238
  /**
231
239
  * Deliberately drops `raw`. Inbound events carry it because the runtime may need
232
240
  * the original object; history is read straight into a model's context, where an
@@ -258,6 +266,7 @@ function normalizeHistoryMessage(msg, fallbackChatId) {
258
266
  timestamp,
259
267
  sentAt: timestamp === undefined ? undefined : new Date(timestamp * 1000).toISOString(),
260
268
  replyToMessageId: toStringId(msg?.replyTo?.replyToMsgId) ?? toStringId(msg?.replyToMsgId),
269
+ replyQuoteText: resolveHistoryReplyQuote(msg),
261
270
  messageThreadId: toStringId(msg?.replyTo?.replyToTopId) ?? toStringId(msg?.replyToTopId),
262
271
  media: (0, media_1.describeMedia)(msg?.media),
263
272
  isOutgoing: msg?.out === true,
package/dist/normalize.js CHANGED
@@ -66,6 +66,21 @@ function resolveMessageThreadId(msg) {
66
66
  return (toStringId(msg?.replyTo?.replyToMsgId) ??
67
67
  toStringId(msg?.replyToMsgId));
68
68
  }
69
+ /**
70
+ * The highlighted fragment of a reply, when there is one.
71
+ *
72
+ * Telegram sends the flag and the text separately, and clients in the wild are
73
+ * not consistent about the flag. The text is the evidence: if `quoteText` has
74
+ * content, a fragment was highlighted regardless of what `quote` says. Blank
75
+ * text is treated as no highlight rather than as an empty one.
76
+ */
77
+ function resolveReplyQuote(msg) {
78
+ const raw = msg?.replyTo?.quoteText ?? msg?.quoteText;
79
+ const text = typeof raw === "string" && raw.trim() ? raw : undefined;
80
+ if (!text)
81
+ return {};
82
+ return { text, isQuote: true };
83
+ }
69
84
  function normalizeTelegramEvent(event, accountId) {
70
85
  const msg = event?.message;
71
86
  if (!msg)
@@ -84,6 +99,7 @@ function normalizeTelegramEvent(event, accountId) {
84
99
  const replyToMessageId = toStringId(msg.replyTo?.replyToMsgId) ??
85
100
  toStringId(msg.replyToMsgId);
86
101
  const messageThreadId = resolveMessageThreadId(msg);
102
+ const replyQuote = resolveReplyQuote(msg);
87
103
  const text = typeof msg.message === "string"
88
104
  ? msg.message
89
105
  : typeof msg.text === "string"
@@ -116,6 +132,8 @@ function normalizeTelegramEvent(event, accountId) {
116
132
  messageId,
117
133
  text,
118
134
  replyToMessageId,
135
+ replyQuoteText: replyQuote.text,
136
+ replyIsQuote: replyQuote.isQuote,
119
137
  chatType,
120
138
  timestamp: toTimestamp(msg.date),
121
139
  isOutgoing: Boolean(msg.out),
@@ -2,7 +2,7 @@
2
2
  "id": "clawgram",
3
3
  "name": "Clawgram",
4
4
  "description": "Clawgram — personal Telegram (MTProto userbot) channel for OpenClaw. Your AI assistant reads and responds as you.",
5
- "version": "2.3.3",
5
+ "version": "2.4.1",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawgram",
3
- "version": "2.3.3",
3
+ "version": "2.4.1",
4
4
  "description": "Clawgram — personal Telegram (MTProto userbot) channel for OpenClaw. Your AI assistant reads and responds as you.",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {