clawgram 2.26.1 → 2.27.0

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.
@@ -79,5 +79,10 @@ function buildGroupReplyAddress(input) {
79
79
  if (display && display !== "Telegram") {
80
80
  return display;
81
81
  }
82
- return input.senderId;
82
+ // No handle and no name means no greeting, not a numeric one. The id
83
+ // used to be the fallback, and in a basic group — where GramJS attaches
84
+ // no sender profile to the message — every reply of a day opened with
85
+ // «890975818, …» (07.09.2026). A person does not know their own
86
+ // telegram id and reads it as a malfunction.
87
+ return undefined;
83
88
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.visibleReplyText = visibleReplyText;
4
4
  exports.handleInboundEvent = handleInboundEvent;
5
+ exports.agentFacingGroupBody = agentFacingGroupBody;
5
6
  // Входящий контур: одно событие Telegram от нормализации до ответа.
6
7
  //
7
8
  // Вынесено из `channel.ts` — 856 строк внутри `gateway.startAccount`, самый
@@ -252,6 +253,13 @@ async function handleInboundEvent(event, ctx) {
252
253
  senderId: inboundSenderId,
253
254
  senderUsername: normalized.senderUsername,
254
255
  });
256
+ // The name of a direct-message sender who may reach the agent. The gate
257
+ // above stays where B5-04 put it — a blocked sender still costs no
258
+ // call. A group sender is looked up later, after the mention gate, for
259
+ // the same reason (see the group branch).
260
+ if (normalized.chatType === "direct" && senderMayReachAgent) {
261
+ await resolveNamelessSender(normalized, rawMessage, client);
262
+ }
255
263
  // An attachment carries no text of its own, and dropping it as
256
264
  // "empty" is how the assistant used to go silent on being spoken
257
265
  // to or shown something. Read it into the body instead: for a
@@ -485,16 +493,32 @@ async function handleInboundEvent(event, ctx) {
485
493
  });
486
494
  return;
487
495
  }
496
+ // Who is speaking, resolved only now — past the group gate and the
497
+ // mention gate, so a message the agent will not even read costs no
498
+ // call (B5-04). GramJS attaches `_sender` only from its entity cache,
499
+ // and in a basic group (or after a restart) that cache is empty: the
500
+ // turn then had nothing but the numeric id, the reply greeting fell
501
+ // back to it, and a management chat spent 07.09.2026 being addressed
502
+ // as «890975818, …». One source of truth for the name: the address
503
+ // the channel would prepend is also what the agent reads, so the
504
+ // model cannot greet «Вася Ш.» while the channel greets «@vasya».
505
+ await resolveNamelessSender(normalized, rawMessage, client);
506
+ const groupSenderLabel = normalized.senderDisplay || normalized.senderUsername || senderId;
507
+ const groupReplyAddress = (0, group_reply_address_1.buildGroupReplyAddress)({
508
+ senderUsername: normalized.senderUsername,
509
+ senderDisplay: normalized.senderDisplay,
510
+ senderId,
511
+ });
488
512
  const { storePath, body } = buildEnvelope({
489
513
  channel: "Telegram",
490
- from: senderLabel,
514
+ from: groupSenderLabel,
491
515
  body: text,
492
516
  timestamp: normalized.timestamp,
493
517
  });
494
518
  const conversationRouteTarget = (0, helpers_1.buildConversationTarget)(normalized.chatId);
495
519
  const ctxPayload = channelRuntime.reply.finalizeInboundContext({
496
520
  Body: body,
497
- BodyForAgent: text,
521
+ BodyForAgent: agentFacingGroupBody({ address: groupReplyAddress, senderId, text }),
498
522
  RawBody: text,
499
523
  CommandBody: text,
500
524
  From: conversationRouteTarget,
@@ -502,7 +526,7 @@ async function handleInboundEvent(event, ctx) {
502
526
  SessionKey: route.sessionKey,
503
527
  AccountId: route.accountId ?? accountId,
504
528
  ChatType: "group",
505
- ConversationLabel: senderLabel,
529
+ ConversationLabel: groupSenderLabel,
506
530
  SenderId: senderId,
507
531
  SenderUsername: normalized.senderUsername,
508
532
  SenderName: normalized.senderDisplay,
@@ -535,11 +559,6 @@ async function handleInboundEvent(event, ctx) {
535
559
  OriginatingChannel: "clawgram",
536
560
  OriginatingTo: conversationRouteTarget,
537
561
  });
538
- const groupReplyAddress = (0, group_reply_address_1.buildGroupReplyAddress)({
539
- senderUsername: normalized.senderUsername,
540
- senderDisplay: normalized.senderDisplay,
541
- senderId,
542
- });
543
562
  (0, group_reply_address_1.rememberGroupReplyAddress)({
544
563
  accountId: route.accountId ?? accountId,
545
564
  chatId: normalized.chatId,
@@ -897,3 +916,51 @@ async function handleInboundEvent(event, ctx) {
897
916
  });
898
917
  }
899
918
  }
919
+ /**
920
+ * A sender who arrived without a name or handle gets one profile lookup.
921
+ *
922
+ * GramJS attaches `_sender` only from its in-memory entity cache; a basic
923
+ * group's update carries no users, so after a restart the cache is empty
924
+ * until something else (a `participants` read) fills it. The lookup may
925
+ * therefore still come back empty — then the sender stays nameless, the
926
+ * greeting is omitted and the agent reads `id:<n>`. Callers decide *when*
927
+ * this runs: after every gate, never for traffic the agent will not read.
928
+ */
929
+ async function resolveNamelessSender(normalized, rawMessage, client) {
930
+ if (!normalized.senderId || normalized.senderDisplay || normalized.senderUsername) {
931
+ return;
932
+ }
933
+ const profile = await (0, helpers_1.resolveSenderProfileWithTimeout)(rawMessage, {
934
+ senderId: normalized.senderId,
935
+ client,
936
+ }, 1500);
937
+ if (profile.username) {
938
+ normalized.senderUsername = profile.username;
939
+ }
940
+ // `toDisplayName` answers "Telegram" when it knows nothing; that is not
941
+ // a name and must not become one here.
942
+ if (profile.display && profile.display !== "Telegram") {
943
+ normalized.senderDisplay = profile.display;
944
+ }
945
+ }
946
+ /**
947
+ * What the agent reads for a group message: `Адрес: текст`.
948
+ *
949
+ * Until 2.27.0 the turn received the bare text. Core's own Telegram channel
950
+ * prefixes the sender for groups (`formatInboundEnvelope`), and without that
951
+ * the agent could tell speakers apart only by the numeric id in metadata —
952
+ * which is exactly what it then used as an address. The prefix is the very
953
+ * address the channel would prepend to a reply (`buildGroupReplyAddress`),
954
+ * so what the model addresses and what the channel greets never differ —
955
+ * a display name in the body with a handle in the greeting would have
956
+ * produced «@vasya, Вася Ш., готово». No address known: `id:<n>`, marked
957
+ * so it is never mistaken for a name.
958
+ */
959
+ function agentFacingGroupBody(input) {
960
+ const address = input.address?.trim();
961
+ if (address) {
962
+ return `${address}: ${input.text}`;
963
+ }
964
+ const id = input.senderId !== undefined ? String(input.senderId).trim() : "";
965
+ return id ? `id:${id}: ${input.text}` : input.text;
966
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "clawgram",
3
- "version": "2.26.1",
3
+ "version": "2.27.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "clawgram",
9
- "version": "2.26.1",
9
+ "version": "2.27.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "json5": "2.2.3",
@@ -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.26.1",
5
+ "version": "2.27.0",
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.26.1",
3
+ "version": "2.27.0",
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": {