clawgram 2.23.0 → 2.24.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.
package/dist/channel.js CHANGED
@@ -54,22 +54,14 @@ const fetch_media_1 = require("./fetch-media");
54
54
  const channel_runtime_1 = require("openclaw/plugin-sdk/channel-runtime");
55
55
  const param_readers_1 = require("openclaw/plugin-sdk/param-readers");
56
56
  const tool_send_1 = require("openclaw/plugin-sdk/tool-send");
57
- const direct_dm_1 = require("openclaw/plugin-sdk/direct-dm");
58
- const channel_inbound_1 = require("openclaw/plugin-sdk/channel-inbound");
59
- const channel_reply_pipeline_1 = require("openclaw/plugin-sdk/channel-reply-pipeline");
60
- const inbound_envelope_1 = require("openclaw/plugin-sdk/inbound-envelope");
61
- const inbound_reply_dispatch_1 = require("openclaw/plugin-sdk/inbound-reply-dispatch");
62
57
  const channel_pairing_1 = require("openclaw/plugin-sdk/channel-pairing");
63
58
  const events_1 = require("telegram/events");
64
- const constants_1 = require("./constants");
65
59
  const gramjs_client_1 = require("./gramjs-client");
66
- const normalize_1 = require("./normalize");
67
60
  const history_1 = require("./history");
68
61
  const send_scope_1 = require("./send-scope");
69
62
  const joins_1 = require("./joins");
70
63
  const reactions_1 = require("./reactions");
71
64
  const manage_1 = require("./manage");
72
- const silent_reaction_1 = require("./silent-reaction");
73
65
  const system_notice_1 = require("./system-notice");
74
66
  const state_dir_1 = require("./state-dir");
75
67
  const chat_info_1 = require("./chat-info");
@@ -82,68 +74,8 @@ const group_reply_address_1 = require("./group-reply-address");
82
74
  const group_visible_reply_guard_1 = require("./group-visible-reply-guard");
83
75
  const helpers_1 = require("./helpers");
84
76
  const proxy_config_1 = require("./proxy-config");
85
- const constants_2 = require("./constants");
77
+ const constants_1 = require("./constants");
86
78
  const actionLog = (0, core_1.createSubsystemLogger)("channels/clawgram");
87
- /** Reads the configured reaction level for an account, tolerating a missing config. */
88
- function readAccountReactionLevel(cfg, accountId) {
89
- const resolvedAccountId = (0, helpers_1.resolveConfiguredAccountId)(cfg, accountId);
90
- if (!resolvedAccountId) {
91
- return undefined;
92
- }
93
- return cfg?.channels?.[constants_2.CHANNEL_ID]?.accounts?.[resolvedAccountId]?.reactionLevel;
94
- }
95
- /**
96
- * Model ref for the emoji pick, when the account names one.
97
- *
98
- * Picking one emoji out of a fixed list of 68 is the cheapest judgement this
99
- * channel makes and the only model call it makes on its own; running it on the
100
- * agent's own head spends the expensive quota on a decision a small model
101
- * makes just as well.
102
- */
103
- function readAccountReactionModel(cfg, accountId) {
104
- const resolvedAccountId = (0, helpers_1.resolveConfiguredAccountId)(cfg, accountId);
105
- if (!resolvedAccountId) {
106
- return undefined;
107
- }
108
- const raw = cfg?.channels?.[constants_2.CHANNEL_ID]?.accounts?.[resolvedAccountId]?.reactionModel;
109
- return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
110
- }
111
- /**
112
- * Wires `reactToSilentMention` to this account's runtime, config and log.
113
- *
114
- * The decision itself lives in `silent-reaction.ts`, testable without a
115
- * Telegram connection; everything here is lookup. Missing pieces — no
116
- * connected client, no model access — resolve to no reaction rather than to
117
- * an error, because by this point the agent has already declined to reply.
118
- */
119
- async function reactToSilentMentionForAccount(params) {
120
- const gram = params.gram;
121
- const llm = params.pluginRuntime?.llm;
122
- if (!gram || typeof llm?.complete !== "function") {
123
- return;
124
- }
125
- await (0, silent_reaction_1.reactToSilentMention)({
126
- appetite: (0, reactions_1.resolveAgentReactionGuidance)(readAccountReactionLevel(params.cfg, params.accountId)),
127
- model: readAccountReactionModel(params.cfg, params.accountId),
128
- wasMentioned: params.wasMentioned,
129
- chatId: params.chatId,
130
- messageId: params.messageId,
131
- messageText: params.messageText,
132
- deps: {
133
- // Bound rather than destructured: the SDK may implement this as a
134
- // method that needs its receiver.
135
- complete: (args) => llm.complete(args),
136
- sendReaction: (args) => gram.sendReaction(args),
137
- allowedReactions: gram.getAllowedReactions
138
- ? () => gram.getAllowedReactions(params.chatId)
139
- : undefined,
140
- onDecision: (info) => actionLog.info("clawgram silent-mention reaction", {
141
- accountId: params.accountId,
142
- ...info,
143
- }),
144
- },
145
- });
146
- }
147
79
  /**
148
80
  * Read scope as configured for the account. Left `undefined` when the key is
149
81
  * absent so `isChatReadable` can tell "not configured" from "configured empty" —
@@ -242,6 +174,7 @@ const actions_1 = require("./actions");
242
174
  Object.defineProperty(exports, "CORE_ACTION_SYNONYMS", { enumerable: true, get: function () { return actions_1.CORE_ACTION_SYNONYMS; } });
243
175
  Object.defineProperty(exports, "canonicalAction", { enumerable: true, get: function () { return actions_1.canonicalAction; } });
244
176
  const outbound_1 = require("./outbound");
177
+ const inbound_pipeline_1 = require("./inbound-pipeline");
245
178
  /**
246
179
  * Turns an inbound attachment into text the agent can read.
247
180
  *
@@ -454,808 +387,10 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
454
387
  });
455
388
  const client = gram.getClient();
456
389
  const eventBuilder = new events_1.NewMessage({});
457
- const eventHandler = async (event) => {
458
- try {
459
- const rawMessage = event?.message;
460
- const rawPeerUserId = rawMessage?.peerId?.userId;
461
- const rawPeerChatId = rawMessage?.peerId?.chatId;
462
- const rawPeerChannelId = rawMessage?.peerId?.channelId;
463
- const directLike = rawPeerUserId !== undefined ||
464
- (typeof rawMessage?.chatId === "number" && rawMessage.chatId > 0);
465
- if (directLike) {
466
- log?.info?.("clawgram raw direct-like event", {
467
- accountId,
468
- messageId: String(rawMessage?.id ?? ""),
469
- chatId: String(rawMessage?.chatId ?? ""),
470
- peerUserId: String(rawPeerUserId ?? ""),
471
- peerChatId: String(rawPeerChatId ?? ""),
472
- peerChannelId: String(rawPeerChannelId ?? ""),
473
- senderId: String(rawMessage?.senderId ?? rawMessage?.fromId?.userId ?? ""),
474
- out: rawMessage?.out === true,
475
- textLength: typeof rawMessage?.message === "string" ? rawMessage.message.length : typeof rawMessage?.text === "string" ? rawMessage.text.length : 0,
476
- });
477
- }
478
- const normalized = (0, normalize_1.normalizeTelegramEvent)(event, accountId);
479
- if (!normalized) {
480
- if (directLike) {
481
- log?.info?.("clawgram normalize returned null", {
482
- accountId,
483
- messageId: String(rawMessage?.id ?? ""),
484
- chatId: String(rawMessage?.chatId ?? ""),
485
- peerUserId: String(rawPeerUserId ?? ""),
486
- });
487
- }
488
- return;
489
- }
490
- const directReplyTarget = normalized.chatType === "direct"
491
- ? undefined
492
- : await (0, helpers_1.resolveReplyTarget)(rawMessage);
493
- const senderProfile = normalized.chatType === "direct"
494
- ? await (0, helpers_1.resolveSenderProfileWithTimeout)(rawMessage, {
495
- senderId: normalized.senderId,
496
- client,
497
- }, 1500)
498
- : await (0, helpers_1.resolveSenderProfile)(rawMessage, {
499
- senderId: normalized.senderId,
500
- client,
501
- });
502
- const replyTarget = normalized.chatType === "direct"
503
- ? normalized.chatId
504
- : await (0, helpers_1.resolveChatTarget)(rawMessage);
505
- if (replyTarget) {
506
- normalized.replyTarget = replyTarget;
507
- }
508
- if (!normalized.senderUsername && senderProfile.username) {
509
- normalized.senderUsername = senderProfile.username;
510
- }
511
- if (!normalized.senderDisplay && senderProfile.display) {
512
- normalized.senderDisplay = senderProfile.display;
513
- }
514
- if (normalized.isOutgoing) {
515
- if (normalized.chatType === "direct") {
516
- log?.info?.("clawgram skipping outgoing direct event", {
517
- accountId,
518
- chatId: normalized.chatId,
519
- messageId: normalized.messageId,
520
- senderId: normalized.senderId,
521
- });
522
- }
523
- return;
524
- }
525
- if (normalized.chatType === "channel") {
526
- log?.info?.("clawgram skipping channel inbound", {
527
- accountId,
528
- chatId: normalized.chatId,
529
- chatType: normalized.chatType,
530
- messageId: normalized.messageId,
531
- });
532
- return;
533
- }
534
- let text = normalized.text?.trim();
535
- // Whether this sender may reach the agent at all — decided before
536
- // the attachment is fetched.
537
- //
538
- // Reading an attachment downloads up to 25 MB and then spends a
539
- // transcription or vision call on it. That used to happen for
540
- // every photo and voice note from anyone in any group the account
541
- // sits in, and only afterwards was the sender checked against
542
- // `allowFrom`. A stranger could therefore spend the owner's model
543
- // budget at will. None of these checks depend on the message text,
544
- // so they cost nothing to run first.
545
- const inboundSenderId = normalized.senderId ?? normalized.chatId;
546
- const inboundScopes = (0, helpers_1.resolveAccountScopes)(cfg, accountId);
547
- const inboundGroupConfig = normalized.chatType === "group"
548
- ? (0, helpers_1.resolveGroupConfig)(inboundScopes.groups, normalized.chatId)
549
- : undefined;
550
- const senderMayReachAgent = normalized.chatType === "group"
551
- ? Boolean(inboundGroupConfig
552
- && inboundGroupConfig.enabled !== false
553
- && (0, helpers_1.isSenderAllowed)({
554
- allowFrom: inboundGroupConfig.allowFrom,
555
- senderId: inboundSenderId,
556
- senderUsername: normalized.senderUsername,
557
- }))
558
- : (0, helpers_1.isSenderAllowed)({
559
- allowFrom: inboundScopes.allowFrom,
560
- senderId: inboundSenderId,
561
- senderUsername: normalized.senderUsername,
562
- });
563
- // An attachment carries no text of its own, and dropping it as
564
- // "empty" is how the assistant used to go silent on being spoken
565
- // to or shown something. Read it into the body instead: for a
566
- // voice note and a screenshot alike, the attachment *is* the
567
- // message. A caption is kept and the reading appended, because
568
- // "look at this" plus the picture is one thought, not two.
569
- const attachment = senderMayReachAgent ? await (0, attachments_1.readInboundAttachment)({
570
- gram,
571
- event,
572
- cfg,
573
- runtime: pluginRuntime,
574
- log,
575
- accountId,
576
- chatId: normalized.chatId,
577
- messageId: normalized.messageId,
578
- }) : undefined;
579
- if (attachment) {
580
- const marker = attachment.understanding === "transcript" ? "голосовое" : "изображение";
581
- const read = `[${marker}] ${attachment.text}`;
582
- text = text ? `${text}\n\n${read}` : read;
583
- }
584
- // What the mention gate is allowed to read.
585
- //
586
- // A transcript is the sender's own speech, so "Тина, посмотри"
587
- // said aloud addresses the agent exactly as typing it would. A
588
- // description is not: it is a vision model reading somebody
589
- // else's content, and a screenshot of a chat where a third party
590
- // wrote "@tina_bot" is not an address to her. Feeding the whole
591
- // body to the gate made every such screenshot wake her up.
592
- const addressableText = (0, helpers_1.resolveAddressableText)({
593
- messageText: normalized.text,
594
- bodyText: text,
595
- understanding: attachment?.understanding,
596
- });
597
- if (!text) {
598
- log?.info?.("clawgram skipping empty inbound text", {
599
- accountId,
600
- chatId: normalized.chatId,
601
- messageId: normalized.messageId,
602
- });
603
- return;
604
- }
605
- const senderId = normalized.senderId ?? normalized.chatId;
606
- const isTelegramServiceDirect = normalized.chatType === "direct" &&
607
- (normalized.chatId === constants_1.TELEGRAM_SERVICE_CHAT_ID || senderId === constants_1.TELEGRAM_SERVICE_CHAT_ID);
608
- const isSavedMessagesDirect = normalized.chatType === "direct" &&
609
- Boolean(selfId) &&
610
- normalized.chatId === selfId &&
611
- senderId === selfId;
612
- if (isTelegramServiceDirect) {
613
- log?.info?.("clawgram skipping Telegram service direct chat", {
614
- accountId,
615
- chatId: normalized.chatId,
616
- messageId: normalized.messageId,
617
- senderId,
618
- });
619
- return;
620
- }
621
- if (isSavedMessagesDirect) {
622
- log?.info?.("clawgram skipping Saved Messages direct chat", {
623
- accountId,
624
- chatId: normalized.chatId,
625
- messageId: normalized.messageId,
626
- senderId,
627
- selfId,
628
- });
629
- return;
630
- }
631
- const senderUsername = normalized.senderUsername;
632
- const senderLabel = normalized.senderDisplay || normalized.senderUsername || senderId;
633
- const conversationTarget = normalized.chatType === "direct"
634
- ? normalized.chatId
635
- : normalized.replyTarget ?? normalized.chatId;
636
- const conversationFallbackTargets = [
637
- normalized.chatType === "direct" ? directReplyTarget : undefined,
638
- normalized.chatType === "direct" ? normalized.replyTarget : undefined,
639
- normalized.chatType === "direct" && normalized.senderUsername ? `@${normalized.senderUsername}` : undefined,
640
- normalized.chatId,
641
- ].filter((target, index, items) => {
642
- if (!target || target === conversationTarget) {
643
- return false;
644
- }
645
- return items.findIndex((candidate) => candidate === target) === index;
646
- });
647
- const sendTextToConversation = async (args) => {
648
- const targets = [conversationTarget, ...conversationFallbackTargets];
649
- // Replies have no per-call parseMode slot — the format is an
650
- // account setting (2.3.1); absent keeps the GramJS default
651
- // (its markdown parser — not plain text, see 2.15.0 notes).
652
- const replyParseMode = gram.replyParseMode;
653
- let lastError;
654
- for (const target of targets) {
655
- try {
656
- return await gram.sendText({
657
- target,
658
- text: args.text,
659
- replyToMessageId: args.replyToMessageId,
660
- messageThreadId: args.messageThreadId,
661
- parseMode: replyParseMode,
662
- });
663
- }
664
- catch (error) {
665
- lastError = error;
666
- }
667
- }
668
- throw lastError;
669
- };
670
- // Resolved once, above, before the attachment fetch that depends on
671
- // the answer — and by the same resolver `resolveAccount` uses, so the
672
- // gate applied here is the one the account was started with.
673
- const { allowFrom: directAllowFrom } = inboundScopes;
674
- const dmPolicy = "open";
675
- if (normalized.chatType === "group") {
676
- const groupConfig = inboundGroupConfig;
677
- if (!groupConfig) {
678
- log?.info?.("clawgram skipping group not present in groups config", {
679
- accountId,
680
- chatId: normalized.chatId,
681
- messageId: normalized.messageId,
682
- });
683
- return;
684
- }
685
- if (groupConfig.enabled === false) {
686
- log?.info?.("clawgram skipping disabled group", {
687
- accountId,
688
- chatId: normalized.chatId,
689
- messageId: normalized.messageId,
690
- });
691
- return;
692
- }
693
- if (!(0, helpers_1.isSenderAllowed)({
694
- allowFrom: groupConfig.allowFrom,
695
- senderId,
696
- senderUsername: normalized.senderUsername,
697
- })) {
698
- log?.info?.("clawgram blocking inbound group sender by allowFrom", {
699
- accountId,
700
- chatId: normalized.chatId,
701
- messageId: normalized.messageId,
702
- senderId,
703
- username: normalized.senderUsername,
704
- allowFrom: groupConfig.allowFrom,
705
- });
706
- return;
707
- }
708
- const scopedGroupPeerId = (0, helpers_1.buildScopedGroupPeerId)(accountId, normalized.chatId);
709
- const { route: inboundRoute, buildEnvelope } = (0, inbound_envelope_1.resolveInboundRouteEnvelopeBuilderWithRuntime)({
710
- cfg,
711
- channel: "clawgram",
712
- accountId,
713
- peer: {
714
- kind: "group",
715
- id: scopedGroupPeerId,
716
- },
717
- runtime: channelRuntime,
718
- sessionStore: cfg?.session?.store,
719
- });
720
- // channelRuntime comes from the untyped ctx, so the generic route type falls
721
- // back to the minimal RouteLike. The runtime value is a ResolvedAgentRoute.
722
- const route = inboundRoute;
723
- // Under `tag` the name is not an address: in a chat of a thousand
724
- // people it occurs in conversation constantly and is aimed at her
725
- // almost never. Only the `@` counts, and it is the same fact the
726
- // stricter rung of the ladder is named after.
727
- const wasMentioned = groupConfig.groupPolicy === "tag"
728
- ? (0, helpers_1.hasExplicitTelegramMention)({ selfUsername, text: addressableText, message: rawMessage })
729
- : (0, helpers_1.hasTelegramMention)({
730
- cfg,
731
- agentId: route.agentId,
732
- selfUsername,
733
- text: addressableText,
734
- message: rawMessage,
735
- });
736
- // One fetch serves two needs: the reply-to-self gate below and
737
- // the parent's text for the agent (ReplyToBody), which a plain
738
- // reply does not carry on its own.
739
- const replyParent = await (0, helpers_1.resolveReplyParent)(rawMessage, { selfId, selfLabel });
740
- const wasReplyToSelf = replyParent.isSelf;
741
- const mentionDecision = (0, channel_inbound_1.resolveInboundMentionDecision)({
742
- facts: {
743
- canDetectMention: true,
744
- wasMentioned,
745
- hasAnyMention: /(^|\s)@[a-zA-Z0-9_]{5,}\b/.test(addressableText),
746
- },
747
- policy: {
748
- isGroup: true,
749
- requireMention: groupConfig.groupPolicy !== "open",
750
- allowTextCommands: false,
751
- hasControlCommand: false,
752
- commandAuthorized: true,
753
- },
754
- });
755
- log?.info?.("clawgram group mention gate", {
756
- accountId,
757
- chatId: normalized.chatId,
758
- messageId: normalized.messageId,
759
- selfUsername,
760
- groupPolicy: groupConfig.groupPolicy,
761
- mentionedFlag: rawMessage?.mentioned === true,
762
- hasEntities: Array.isArray(rawMessage?.entities) ? rawMessage.entities.length : 0,
763
- wasMentioned,
764
- wasReplyToSelf,
765
- shouldSkip: mentionDecision.shouldSkip,
766
- textLength: text.length,
767
- });
768
- if (groupConfig.groupPolicy !== "open" && mentionDecision.shouldSkip && !wasReplyToSelf) {
769
- log?.info?.("clawgram skipping group message without mention", {
770
- accountId,
771
- chatId: normalized.chatId,
772
- messageId: normalized.messageId,
773
- senderId,
774
- });
775
- return;
776
- }
777
- const { storePath, body } = buildEnvelope({
778
- channel: "Telegram",
779
- from: senderLabel,
780
- body: text,
781
- timestamp: normalized.timestamp,
782
- });
783
- const conversationRouteTarget = (0, helpers_1.buildConversationTarget)(normalized.chatId);
784
- const ctxPayload = channelRuntime.reply.finalizeInboundContext({
785
- Body: body,
786
- BodyForAgent: text,
787
- RawBody: text,
788
- CommandBody: text,
789
- From: conversationRouteTarget,
790
- To: conversationRouteTarget,
791
- SessionKey: route.sessionKey,
792
- AccountId: route.accountId ?? accountId,
793
- ChatType: "group",
794
- ConversationLabel: senderLabel,
795
- SenderId: senderId,
796
- SenderUsername: normalized.senderUsername,
797
- SenderName: normalized.senderDisplay,
798
- GroupId: normalized.chatId,
799
- GroupSubject: normalized.chatId,
800
- WasMentioned: mentionDecision.effectiveWasMentioned || wasReplyToSelf,
801
- WasReplyToSelf: wasReplyToSelf,
802
- Provider: "telegram",
803
- Surface: "clawgram",
804
- MessageSid: normalized.messageId,
805
- MessageSidFull: normalized.messageId,
806
- Timestamp: normalized.timestamp,
807
- ReplyToId: normalized.replyToMessageId,
808
- // Core renders these itself as `[Replying to: "…"]` ahead of the
809
- // user body — it keys off Provider being "telegram", which is set
810
- // below. Without them a highlighted reply reaches the agent as
811
- // bare text, and the fragment the person pointed at is lost.
812
- ReplyToQuoteText: normalized.replyQuoteText,
813
- ReplyToIsQuote: normalized.replyIsQuote,
814
- // A plain reply has no highlight; core then falls back to the
815
- // parent's body, which only exists if the channel fetched it.
816
- ReplyToBody: replyParent.body,
817
- ReplyToSender: replyParent.sender,
818
- MessageThreadId: normalized.messageThreadId,
819
- NativeChannelId: normalized.chatId,
820
- // Trusted per-group prompt block from `groups.<id>.systemPrompt`.
821
- // Core normalizes it (`normalizeTrustedTextField`) and appends
822
- // it to the system prompt for this turn. Undefined = no block.
823
- GroupSystemPrompt: groupConfig.systemPrompt,
824
- OriginatingChannel: "clawgram",
825
- OriginatingTo: conversationRouteTarget,
826
- });
827
- const groupReplyAddress = (0, group_reply_address_1.buildGroupReplyAddress)({
828
- senderUsername: normalized.senderUsername,
829
- senderDisplay: normalized.senderDisplay,
830
- senderId,
831
- });
832
- (0, group_reply_address_1.rememberGroupReplyAddress)({
833
- accountId: route.accountId ?? accountId,
834
- chatId: normalized.chatId,
835
- replyToId: normalized.messageId,
836
- address: groupReplyAddress,
837
- });
838
- const messageThreadId = (0, helpers_1.parseOptionalThreadId)(normalized.messageThreadId);
839
- const groupTypingTarget = normalized.chatId;
840
- await gram.withTyping(groupTypingTarget, async () => {
841
- log?.info?.("clawgram dispatching group reply", {
842
- accountId,
843
- chatId: normalized.chatId,
844
- messageId: normalized.messageId,
845
- routeSessionKey: route.sessionKey,
846
- storePath,
847
- });
848
- await channelRuntime.session.recordInboundSession({
849
- storePath,
850
- sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
851
- ctx: ctxPayload,
852
- updateLastRoute: {
853
- sessionKey: route.sessionKey,
854
- channel: constants_2.CHANNEL_ID,
855
- to: conversationRouteTarget,
856
- accountId: route.accountId ?? accountId,
857
- },
858
- onRecordError: (err) => {
859
- log?.info?.("clawgram failed to update group last route", {
860
- accountId,
861
- chatId: normalized.chatId,
862
- messageId: normalized.messageId,
863
- error: String(err),
864
- });
865
- },
866
- });
867
- const dispatchBase = (0, inbound_reply_dispatch_1.buildInboundReplyDispatchBase)({
868
- cfg,
869
- channel: "clawgram",
870
- accountId: route.accountId ?? accountId,
871
- route,
872
- storePath,
873
- ctxPayload,
874
- core: { channel: channelRuntime },
875
- });
876
- const { onModelSelected, ...replyPipeline } = (0, channel_reply_pipeline_1.createChannelReplyPipeline)({
877
- cfg,
878
- agentId: route.agentId,
879
- channel: "clawgram",
880
- accountId: route.accountId ?? accountId,
881
- });
882
- // Boundary for the transcript fallback below: only replies
883
- // written after this instant may be salvaged. Same clock as
884
- // the transcript writer — both live in this process.
885
- const dispatchStartedAt = Date.now();
886
- const dispatchResult = await dispatchBase.dispatchReplyWithBufferedBlockDispatcher({
887
- ctx: ctxPayload,
888
- cfg,
889
- dispatcherOptions: {
890
- ...replyPipeline,
891
- deliver: async (payload) => {
892
- const outboundText = typeof payload.text === "string" ? payload.text.trim() : "";
893
- log?.info?.("clawgram deliver group payload", {
894
- accountId,
895
- chatId: normalized.chatId,
896
- messageId: normalized.messageId,
897
- payloadTextLength: outboundText.length,
898
- payloadReplyToId: payload.replyToId ?? null,
899
- });
900
- if (!outboundText) {
901
- return;
902
- }
903
- // The agent may decline to answer by returning the shared
904
- // silent token. Drop it before addressing: otherwise the
905
- // reply-address prefix turns it into a visible message.
906
- const visibleText = (0, helpers_1.stripSilentReplyToken)(outboundText);
907
- if (!visibleText) {
908
- log?.info?.("clawgram suppressing silent group reply", {
909
- accountId,
910
- chatId: normalized.chatId,
911
- messageId: normalized.messageId,
912
- });
913
- return;
914
- }
915
- // Ядро подклеивает свою телеметрию к полезной нагрузке
916
- // хода, и сюда она приходит тем же путём, что ответ.
917
- // Проверка стояла только в `outbound.sendText`, то есть
918
- // класс инцидента 30.08–01.09 был закрыт для рассылок и
919
- // открыт для обычного ответа на упоминание (A5-10).
920
- const groupNotice = (0, system_notice_1.shouldSuppressGroupSystemNotice)({
921
- targetKind: "group",
922
- text: visibleText,
923
- });
924
- if (groupNotice) {
925
- log?.warn?.("clawgram suppressing system notice in group reply", {
926
- accountId,
927
- chatId: normalized.chatId,
928
- messageId: normalized.messageId,
929
- noticeKind: groupNotice,
930
- textLength: visibleText.length,
931
- });
932
- return;
933
- }
934
- const replyToMessageId = payload.replyToId ? Number(payload.replyToId) : Number(normalized.messageId);
935
- const rememberedAddress = (0, group_reply_address_1.consumeGroupReplyAddress)({
936
- accountId: route.accountId ?? accountId,
937
- chatId: normalized.chatId,
938
- replyToId: payload.replyToId ?? normalized.messageId,
939
- });
940
- await sendTextToConversation({
941
- text: (0, helpers_1.prefixReplyTextToAddress)(visibleText, rememberedAddress ?? groupReplyAddress),
942
- replyToMessageId,
943
- messageThreadId,
944
- });
945
- },
946
- onError: (err, info) => {
947
- log?.error?.("clawgram failed to dispatch group reply", {
948
- accountId,
949
- chatId: normalized.chatId,
950
- messageId: normalized.messageId,
951
- kind: info.kind,
952
- error: String(err),
953
- });
954
- },
955
- },
956
- replyOptions: {
957
- onModelSelected,
958
- // `groups.<id>.skills` → core's per-turn skill allowlist.
959
- // Undefined = inherit the agent's skills; [] = none here.
960
- skillFilter: groupConfig.skillFilter,
961
- },
962
- });
963
- log?.info?.("clawgram group dispatch completed", {
964
- accountId,
965
- chatId: normalized.chatId,
966
- messageId: normalized.messageId,
967
- queuedFinal: dispatchResult?.queuedFinal ?? null,
968
- counts: dispatchResult?.counts ?? null,
969
- });
970
- const dispatchCounts = dispatchResult?.counts ?? { tool: 0, block: 0, final: 0 };
971
- const nothingDelivered = dispatchResult?.queuedFinal !== true &&
972
- (dispatchCounts.tool ?? 0) === 0 &&
973
- (dispatchCounts.block ?? 0) === 0 &&
974
- (dispatchCounts.final ?? 0) === 0;
975
- if (nothingDelivered) {
976
- const fallbackText = (0, helpers_1.readLatestAssistantFallbackFromTranscript)(route.sessionKey, storePath, dispatchStartedAt);
977
- // A suppressed silent reply legitimately delivers nothing, so
978
- // this fallback fires right after it. Without the same check
979
- // the token would be read back from the transcript and sent.
980
- //
981
- // TTS markup needs the same treatment for the same reason:
982
- // core strips it on the normal reply path, but this text comes
983
- // straight out of the transcript. On 2026-08-08 a group got
984
- // `[[tts:text]]Привет, Вася!…[[/tts:text]]` verbatim. The
985
- // spoken words are kept — a synthesis that did not happen
986
- // should degrade to readable text, not to markup.
987
- const rawFallback = fallbackText
988
- ? (0, helpers_1.stripTtsDirectives)((0, helpers_1.stripSilentReplyToken)(fallbackText))
989
- : "";
990
- // Тот же фильтр и здесь: последняя реплика в стенограмме
991
- // вполне может оказаться именно уведомлением об ошибке.
992
- const fallbackNotice = rawFallback
993
- ? (0, system_notice_1.shouldSuppressGroupSystemNotice)({ targetKind: "group", text: rawFallback })
994
- : undefined;
995
- if (fallbackNotice) {
996
- log?.warn?.("clawgram suppressing system notice in transcript fallback", {
997
- accountId,
998
- chatId: normalized.chatId,
999
- messageId: normalized.messageId,
1000
- noticeKind: fallbackNotice,
1001
- textLength: rawFallback.length,
1002
- });
1003
- }
1004
- const visibleFallbackText = fallbackNotice ? "" : rawFallback;
1005
- if (!visibleFallbackText) {
1006
- if (fallbackText) {
1007
- log?.info?.("clawgram skipping silent transcript fallback", {
1008
- accountId,
1009
- chatId: normalized.chatId,
1010
- messageId: normalized.messageId,
1011
- routeSessionKey: route.sessionKey,
1012
- });
1013
- }
1014
- else {
1015
- log?.warn?.("clawgram transcript fallback unavailable", {
1016
- accountId,
1017
- chatId: normalized.chatId,
1018
- messageId: normalized.messageId,
1019
- routeSessionKey: route.sessionKey,
1020
- });
1021
- }
1022
- // Named, and nothing came back: leave a reaction so the
1023
- // decision is visible instead of reading as her ignoring
1024
- // people. The condition is her silence, not the shape of
1025
- // the transcript — a turn that wrote no entry at all is
1026
- // just as silent as one that wrote the NO_REPLY token.
1027
- //
1028
- // Never allowed to disturb the turn: the reply is already
1029
- // settled by this point, so a failure here stays silent.
1030
- await reactToSilentMentionForAccount({
1031
- cfg,
1032
- accountId,
1033
- gram: runtimes.get(accountId),
1034
- pluginRuntime,
1035
- chatId: normalized.chatId,
1036
- messageId: normalized.messageId,
1037
- messageText: normalized.text,
1038
- // Same sense of "addressed" the agent was given for this
1039
- // turn on line 817: a reply to her own message counts as
1040
- // being spoken to, mention or not.
1041
- wasMentioned: mentionDecision.effectiveWasMentioned || wasReplyToSelf,
1042
- }).catch((err) => {
1043
- log?.info?.("clawgram silent-mention reaction failed", {
1044
- accountId,
1045
- chatId: normalized.chatId,
1046
- messageId: normalized.messageId,
1047
- error: String(err),
1048
- });
1049
- });
1050
- }
1051
- else {
1052
- log?.warn?.("clawgram using transcript fallback reply", {
1053
- accountId,
1054
- chatId: normalized.chatId,
1055
- messageId: normalized.messageId,
1056
- routeSessionKey: route.sessionKey,
1057
- fallbackTextLength: visibleFallbackText.length,
1058
- });
1059
- await sendTextToConversation({
1060
- text: (0, helpers_1.prefixReplyTextToAddress)(visibleFallbackText, groupReplyAddress),
1061
- replyToMessageId: Number(normalized.messageId),
1062
- messageThreadId,
1063
- });
1064
- }
1065
- }
1066
- }, {
1067
- readMessageId: Number(normalized.messageId),
1068
- messageThreadId,
1069
- // The indicator is a promise of an answer, and it is owed only
1070
- // to someone who addressed her. Under `open` the turn runs on
1071
- // every message in the chat, so without this the whole room
1072
- // watches her "type" through conversations she is only reading.
1073
- typing: mentionDecision.effectiveWasMentioned || wasReplyToSelf,
1074
- });
1075
- log?.info?.("clawgram group inbound handled", {
1076
- accountId,
1077
- chatId: normalized.chatId,
1078
- messageId: normalized.messageId,
1079
- senderId,
1080
- senderLabel,
1081
- wasMentioned: mentionDecision.effectiveWasMentioned,
1082
- wasReplyToSelf,
1083
- });
1084
- return;
1085
- }
1086
- if (!(0, helpers_1.isSenderAllowed)({
1087
- allowFrom: directAllowFrom,
1088
- senderId,
1089
- senderUsername: normalized.senderUsername,
1090
- })) {
1091
- log?.info?.("clawgram direct allowFrom mismatch", {
1092
- accountId,
1093
- senderId,
1094
- senderUsername: normalized.senderUsername,
1095
- allowFrom: directAllowFrom,
1096
- });
1097
- return;
1098
- }
1099
- const access = await (0, direct_dm_1.resolveInboundDirectDmAccessWithRuntime)({
1100
- cfg,
1101
- channel: "clawgram",
1102
- accountId,
1103
- dmPolicy,
1104
- allowFrom: directAllowFrom,
1105
- senderId,
1106
- rawBody: text,
1107
- runtime: channelRuntime.commands,
1108
- isSenderAllowed: (_candidateSenderId, allowEntries) => (0, helpers_1.isSenderAllowed)({
1109
- allowFrom: allowEntries,
1110
- senderId,
1111
- senderUsername,
1112
- }),
1113
- readStoreAllowFrom: pairing.readStoreForDmPolicy,
1114
- });
1115
- if (access.access.decision === "block") {
1116
- log?.info?.("clawgram blocking inbound direct message", {
1117
- accountId,
1118
- chatId: normalized.chatId,
1119
- messageId: normalized.messageId,
1120
- senderId,
1121
- reason: access.access.reason,
1122
- reasonCode: access.access.reasonCode,
1123
- });
1124
- return;
1125
- }
1126
- if (access.access.decision === "pairing") {
1127
- await pairing.issueChallenge({
1128
- senderId,
1129
- senderIdLine: `Your Telegram user id: ${senderId}`,
1130
- meta: {
1131
- username: normalized.senderUsername,
1132
- name: normalized.senderDisplay,
1133
- },
1134
- sendPairingReply: async (pairingText) => {
1135
- await sendTextToConversation({
1136
- text: pairingText,
1137
- });
1138
- },
1139
- onReplyError: (err) => {
1140
- log?.info?.("clawgram pairing reply failed", {
1141
- accountId,
1142
- chatId: normalized.chatId,
1143
- senderId,
1144
- error: String(err),
1145
- });
1146
- },
1147
- });
1148
- log?.info?.("clawgram pairing required for inbound direct message", {
1149
- accountId,
1150
- chatId: normalized.chatId,
1151
- messageId: normalized.messageId,
1152
- senderId,
1153
- });
1154
- return;
1155
- }
1156
- // Same fetch as the group path. In a DM the parent is as often
1157
- // the agent's own message as the person's — the owner answers a
1158
- // notice she sent — and neither text is available any other way.
1159
- const replyParent = await (0, helpers_1.resolveReplyParent)(rawMessage, { selfId, selfLabel });
1160
- await gram.withTyping(conversationTarget, async () => {
1161
- await (0, direct_dm_1.dispatchInboundDirectDmWithRuntime)({
1162
- cfg,
1163
- runtime: { channel: channelRuntime },
1164
- channel: "clawgram",
1165
- channelLabel: "Telegram",
1166
- accountId,
1167
- peer: {
1168
- kind: "direct",
1169
- id: senderId,
1170
- },
1171
- senderId,
1172
- senderAddress: `telegram:${senderId}`,
1173
- recipientAddress: selfId ? `telegram:${selfId}` : `telegram:${accountId}`,
1174
- conversationLabel: senderLabel,
1175
- rawBody: text,
1176
- messageId: normalized.messageId,
1177
- timestamp: normalized.timestamp,
1178
- commandAuthorized: access.commandAuthorized,
1179
- provider: "telegram",
1180
- surface: "clawgram",
1181
- originatingChannel: "clawgram",
1182
- originatingTo: senderId,
1183
- extraContext: {
1184
- SenderUsername: normalized.senderUsername,
1185
- SenderName: normalized.senderDisplay,
1186
- ReplyToId: normalized.replyToMessageId,
1187
- // Same reason as the group path: highlighted replies happen in
1188
- // direct messages too, and the fragment is not part of the text.
1189
- ReplyToQuoteText: normalized.replyQuoteText,
1190
- ReplyToIsQuote: normalized.replyIsQuote,
1191
- ReplyToBody: replyParent.body,
1192
- ReplyToSender: replyParent.sender,
1193
- NativeChannelId: normalized.chatId,
1194
- },
1195
- deliver: async (payload) => {
1196
- const outboundText = typeof payload.text === "string" ? payload.text.trim() : "";
1197
- if (!outboundText) {
1198
- return;
1199
- }
1200
- const visibleText = (0, helpers_1.stripSilentReplyToken)(outboundText);
1201
- if (!visibleText) {
1202
- log?.info?.("clawgram suppressing silent direct reply", {
1203
- accountId,
1204
- chatId: normalized.chatId,
1205
- messageId: normalized.messageId,
1206
- });
1207
- return;
1208
- }
1209
- await sendTextToConversation({
1210
- text: visibleText,
1211
- replyToMessageId: payload.replyToId ? Number(payload.replyToId) : undefined,
1212
- });
1213
- },
1214
- onRecordError: (err) => {
1215
- log?.info?.("clawgram failed to record inbound session", {
1216
- accountId,
1217
- chatId: normalized.chatId,
1218
- messageId: normalized.messageId,
1219
- error: String(err),
1220
- });
1221
- },
1222
- onDispatchError: (err, info) => {
1223
- log?.info?.("clawgram failed to dispatch reply", {
1224
- accountId,
1225
- chatId: normalized.chatId,
1226
- messageId: normalized.messageId,
1227
- kind: info.kind,
1228
- error: String(err),
1229
- });
1230
- },
1231
- });
1232
- }, {
1233
- readMessageId: Number(normalized.messageId),
1234
- });
1235
- log?.info?.("clawgram inbound handled", {
1236
- accountId,
1237
- chatId: normalized.chatId,
1238
- messageId: normalized.messageId,
1239
- senderId,
1240
- senderLabel,
1241
- });
1242
- }
1243
- catch (error) {
1244
- const rawMessage = event?.message;
1245
- log?.error?.("clawgram inbound handling failed", {
1246
- accountId,
1247
- chatId: String(rawMessage?.chatId ?? rawMessage?.peerId?.userId ?? rawMessage?.peerId?.chatId ?? rawMessage?.peerId?.channelId ?? ""),
1248
- messageId: String(rawMessage?.id ?? ""),
1249
- error: String(error),
1250
- });
1251
- log?.info?.("clawgram inbound preflight failed", {
1252
- accountId,
1253
- chatId: String(rawMessage?.chatId ?? rawMessage?.peerId?.userId ?? rawMessage?.peerId?.chatId ?? rawMessage?.peerId?.channelId ?? ""),
1254
- messageId: String(rawMessage?.id ?? ""),
1255
- error: String(error),
1256
- });
1257
- }
1258
- };
390
+ const eventHandler = async (event) => (0, inbound_pipeline_1.handleInboundEvent)(event, {
391
+ accountId, cfg, channelRuntime, client, gram, log, pairing,
392
+ pluginRuntime, runtimes, selfId, selfLabel, selfUsername,
393
+ });
1259
394
  client.addEventHandler(eventHandler, eventBuilder);
1260
395
  // Being added to a chat arrives as a service message, which `NewMessage`
1261
396
  // drops — so joins are observed on the raw update stream instead. Only
@@ -1304,7 +439,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
1304
439
  },
1305
440
  },
1306
441
  messaging: {
1307
- targetPrefixes: [constants_2.CHANNEL_ID, "tguserbot", "telegram", "tg"],
442
+ targetPrefixes: [constants_1.CHANNEL_ID, "tguserbot", "telegram", "tg"],
1308
443
  normalizeTarget(raw) {
1309
444
  const normalized = (0, helpers_1.normalizeOutboundTarget)(raw);
1310
445
  return normalized || undefined;
@@ -1376,7 +511,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
1376
511
  return (0, core_1.buildChannelOutboundSessionRoute)({
1377
512
  cfg: params.cfg,
1378
513
  agentId: params.agentId,
1379
- channel: constants_2.CHANNEL_ID,
514
+ channel: constants_1.CHANNEL_ID,
1380
515
  accountId,
1381
516
  peer: {
1382
517
  kind: (0, helpers_1.routeKindFromChatType)(chatType),