clawgram 2.19.4 → 2.20.2
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 +34 -1
- package/dist/helpers.js +42 -10
- package/dist/system-notice.js +70 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/channel.js
CHANGED
|
@@ -70,6 +70,7 @@ const joins_1 = require("./joins");
|
|
|
70
70
|
const reactions_1 = require("./reactions");
|
|
71
71
|
const manage_1 = require("./manage");
|
|
72
72
|
const silent_reaction_1 = require("./silent-reaction");
|
|
73
|
+
const system_notice_1 = require("./system-notice");
|
|
73
74
|
const chat_info_1 = require("./chat-info");
|
|
74
75
|
const topics_1 = require("./topics");
|
|
75
76
|
const dialogs_1 = require("./dialogs");
|
|
@@ -800,7 +801,11 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
800
801
|
text,
|
|
801
802
|
message: rawMessage,
|
|
802
803
|
});
|
|
803
|
-
|
|
804
|
+
// One fetch serves two needs: the reply-to-self gate below and
|
|
805
|
+
// the parent's text for the agent (ReplyToBody), which a plain
|
|
806
|
+
// reply does not carry on its own.
|
|
807
|
+
const replyParent = await (0, helpers_1.resolveReplyParent)(rawMessage, { selfId, selfLabel });
|
|
808
|
+
const wasReplyToSelf = replyParent.isSelf;
|
|
804
809
|
const mentionDecision = (0, channel_inbound_1.resolveInboundMentionDecision)({
|
|
805
810
|
facts: {
|
|
806
811
|
canDetectMention: true,
|
|
@@ -874,6 +879,10 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
874
879
|
// bare text, and the fragment the person pointed at is lost.
|
|
875
880
|
ReplyToQuoteText: normalized.replyQuoteText,
|
|
876
881
|
ReplyToIsQuote: normalized.replyIsQuote,
|
|
882
|
+
// A plain reply has no highlight; core then falls back to the
|
|
883
|
+
// parent's body, which only exists if the channel fetched it.
|
|
884
|
+
ReplyToBody: replyParent.body,
|
|
885
|
+
ReplyToSender: replyParent.sender,
|
|
877
886
|
MessageThreadId: normalized.messageThreadId,
|
|
878
887
|
NativeChannelId: normalized.chatId,
|
|
879
888
|
// Trusted per-group prompt block from `groups.<id>.systemPrompt`.
|
|
@@ -1178,6 +1187,10 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1178
1187
|
});
|
|
1179
1188
|
return;
|
|
1180
1189
|
}
|
|
1190
|
+
// Same fetch as the group path. In a DM the parent is as often
|
|
1191
|
+
// the agent's own message as the person's — the owner answers a
|
|
1192
|
+
// notice she sent — and neither text is available any other way.
|
|
1193
|
+
const replyParent = await (0, helpers_1.resolveReplyParent)(rawMessage, { selfId, selfLabel });
|
|
1181
1194
|
await gram.withTyping(conversationTarget, async () => {
|
|
1182
1195
|
await (0, direct_dm_1.dispatchInboundDirectDmWithRuntime)({
|
|
1183
1196
|
cfg,
|
|
@@ -1209,6 +1222,8 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1209
1222
|
// direct messages too, and the fragment is not part of the text.
|
|
1210
1223
|
ReplyToQuoteText: normalized.replyQuoteText,
|
|
1211
1224
|
ReplyToIsQuote: normalized.replyIsQuote,
|
|
1225
|
+
ReplyToBody: replyParent.body,
|
|
1226
|
+
ReplyToSender: replyParent.sender,
|
|
1212
1227
|
NativeChannelId: normalized.chatId,
|
|
1213
1228
|
},
|
|
1214
1229
|
deliver: async (payload) => {
|
|
@@ -2405,6 +2420,24 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
2405
2420
|
});
|
|
2406
2421
|
return { skipped: "silent" };
|
|
2407
2422
|
}
|
|
2423
|
+
// Core's operational chatter (tool-error warnings, fallback notices)
|
|
2424
|
+
// stays out of group chats: it is telemetry for the operator, not a
|
|
2425
|
+
// reply to the room, and it has already been seen carrying shell
|
|
2426
|
+
// commands with secret-store paths. DMs keep it. The text itself is
|
|
2427
|
+
// never logged — see system-notice.ts for why.
|
|
2428
|
+
const suppressedNotice = (0, system_notice_1.shouldSuppressGroupSystemNotice)({
|
|
2429
|
+
targetKind: (0, helpers_1.inferOutboundTargetKind)(ctx.to),
|
|
2430
|
+
text: ctx.text,
|
|
2431
|
+
});
|
|
2432
|
+
if (suppressedNotice) {
|
|
2433
|
+
actionLog.warn("clawgram suppressing system notice in group", {
|
|
2434
|
+
accountId: ctx.accountId,
|
|
2435
|
+
rawTo: ctx.to,
|
|
2436
|
+
noticeKind: suppressedNotice,
|
|
2437
|
+
textLength: ctx.text.length,
|
|
2438
|
+
});
|
|
2439
|
+
return { skipped: "system-notice" };
|
|
2440
|
+
}
|
|
2408
2441
|
const gram = runtimes.get(ctx.accountId);
|
|
2409
2442
|
if (!gram) {
|
|
2410
2443
|
throw new Error(`clawgram: runtime not found for account ${ctx.accountId}`);
|
package/dist/helpers.js
CHANGED
|
@@ -36,6 +36,7 @@ exports.prefixReplyTextToAddress = prefixReplyTextToAddress;
|
|
|
36
36
|
exports.resolveReplyTarget = resolveReplyTarget;
|
|
37
37
|
exports.resolveChatTarget = resolveChatTarget;
|
|
38
38
|
exports.isReplyToSelfMessage = isReplyToSelfMessage;
|
|
39
|
+
exports.resolveReplyParent = resolveReplyParent;
|
|
39
40
|
exports.resolveSenderProfile = resolveSenderProfile;
|
|
40
41
|
exports.resolveSenderProfileWithTimeout = resolveSenderProfileWithTimeout;
|
|
41
42
|
exports.normalizeParseMode = normalizeParseMode;
|
|
@@ -549,27 +550,58 @@ async function resolveChatTarget(message) {
|
|
|
549
550
|
}
|
|
550
551
|
return message?.inputChat ?? message?._inputChat ?? message?.chat ?? message?._chat ?? message?.peerId;
|
|
551
552
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
553
|
+
/**
|
|
554
|
+
* The message a reply points at, fetched once.
|
|
555
|
+
*
|
|
556
|
+
* Telegram does not put the parent's text into the reply; a highlight
|
|
557
|
+
* (`quoteText`) is the only fragment that travels with it, and most replies
|
|
558
|
+
* have none. Core renders `[Replying to: …]` from the highlight or, failing
|
|
559
|
+
* that, from `ReplyToBody` — so without this fetch a plain reply reaches the
|
|
560
|
+
* agent as a bare parent id. The case that exposed it: the owner answered, in
|
|
561
|
+
* a DM, the agent's own notice about an unknown sender; the notice had been
|
|
562
|
+
* sent from another session, DMs are outside `readChats`, and the agent had
|
|
563
|
+
* no way to learn what "reply to #1011" referred to.
|
|
564
|
+
*
|
|
565
|
+
* Failure degrades to "no context" on purpose: a parent that cannot be
|
|
566
|
+
* fetched (deleted, flood-waited, transport hiccup) must not cost the
|
|
567
|
+
* message itself.
|
|
568
|
+
*/
|
|
569
|
+
async function resolveReplyParent(message, input) {
|
|
556
570
|
const replyToMessageId = message?.replyTo?.replyToMsgId ?? message?.replyToMsgId;
|
|
557
571
|
if (!replyToMessageId) {
|
|
558
|
-
return false;
|
|
572
|
+
return { isSelf: false };
|
|
559
573
|
}
|
|
560
574
|
const replied = typeof message?.getReplyMessage === "function"
|
|
561
575
|
? await message.getReplyMessage().catch(() => undefined)
|
|
562
576
|
: undefined;
|
|
563
577
|
if (!replied) {
|
|
564
|
-
return false;
|
|
565
|
-
}
|
|
566
|
-
if (replied.out === true) {
|
|
567
|
-
return true;
|
|
578
|
+
return { isSelf: false };
|
|
568
579
|
}
|
|
569
580
|
const replySenderId = replied.senderId ??
|
|
570
581
|
replied.fromId?.userId ??
|
|
571
582
|
replied.fromId?.channelId;
|
|
572
|
-
|
|
583
|
+
const isSelf = replied.out === true ||
|
|
584
|
+
(input.selfId !== undefined && replySenderId !== undefined && String(replySenderId) === input.selfId);
|
|
585
|
+
const rawText = typeof replied.message === "string" ? replied.message :
|
|
586
|
+
typeof replied.text === "string" ? replied.text :
|
|
587
|
+
"";
|
|
588
|
+
const body = rawText.trim() ? rawText : undefined;
|
|
589
|
+
const source = replied.sender ?? replied._sender;
|
|
590
|
+
const sender = isSelf
|
|
591
|
+
? (input.selfLabel ?? (input.selfId !== undefined ? input.selfId : undefined))
|
|
592
|
+
: toDisplayName({
|
|
593
|
+
firstName: typeof source?.firstName === "string" ? source.firstName : undefined,
|
|
594
|
+
lastName: typeof source?.lastName === "string" ? source.lastName : undefined,
|
|
595
|
+
username: undefined,
|
|
596
|
+
fallback: resolveActiveUsername(source) ? `@${resolveActiveUsername(source)}` : (replySenderId !== undefined ? String(replySenderId) : undefined),
|
|
597
|
+
});
|
|
598
|
+
return { isSelf, body, sender: sender || undefined };
|
|
599
|
+
}
|
|
600
|
+
async function isReplyToSelfMessage(message, selfId) {
|
|
601
|
+
if (!selfId) {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
return (await resolveReplyParent(message, { selfId })).isSelf;
|
|
573
605
|
}
|
|
574
606
|
async function resolveSenderProfile(message, input) {
|
|
575
607
|
const pickProfile = (source) => {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Core's operational chatter — and why a group chat never sees it.
|
|
4
|
+
*
|
|
5
|
+
* When a tool call fails or the model silently degrades, core appends a
|
|
6
|
+
* status line to the turn's outbound payloads: `⚠️ 🛠️ Exec failed: …`,
|
|
7
|
+
* `⚠️ ✉️ Message failed`, `↪️ Model Fallback: …`. In a DM with the owner that
|
|
8
|
+
* is legitimate telemetry. In a group it is the assistant narrating its own
|
|
9
|
+
* kitchen to an audience the message was never for — measured three days in a
|
|
10
|
+
* row in the owner's work chat (2026-08-30 … 09-01): a jq stack trace with
|
|
11
|
+
* server paths, a bare "Message failed", an exec step list. The people in the
|
|
12
|
+
* chat cannot act on any of it, and the owner reads it as the assistant
|
|
13
|
+
* being broken.
|
|
14
|
+
*
|
|
15
|
+
* Detection is by core's own exact prefixes, not by keyword: the assistant is
|
|
16
|
+
* allowed to *say* "⚠️" or discuss a failure in its own words — only core's
|
|
17
|
+
* machine-built notices match. The list mirrors what core actually emits
|
|
18
|
+
* (`isCronToolWarning` matches `⚠️ 🛠️ ` verbatim; the message-failed check is
|
|
19
|
+
* the same normalized comparison core uses; fallback notices are built by
|
|
20
|
+
* `buildFallbackNotice`/`buildFallbackClearedNotice`).
|
|
21
|
+
*
|
|
22
|
+
* Nothing is lost by dropping them here: the same failures live in the run's
|
|
23
|
+
* diagnostics, the cron job's `lastError` and the gateway log. The drop is
|
|
24
|
+
* logged with the notice's class and length — never its text, which has
|
|
25
|
+
* already been seen carrying secret-store paths and full shell commands.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.classifySystemNotice = classifySystemNotice;
|
|
29
|
+
exports.shouldSuppressGroupSystemNotice = shouldSuppressGroupSystemNotice;
|
|
30
|
+
const TOOL_WARNING_PREFIX = "⚠️ 🛠️ ";
|
|
31
|
+
const MESSAGE_FAILED_PREFIX = "⚠️ ✉️ message failed";
|
|
32
|
+
const FALLBACK_NOTICE_PREFIX = "↪️ model fallback";
|
|
33
|
+
/**
|
|
34
|
+
* Classifies core's operational status lines. `undefined` means the text is a
|
|
35
|
+
* real reply and must be delivered untouched.
|
|
36
|
+
*/
|
|
37
|
+
function classifySystemNotice(text) {
|
|
38
|
+
const trimmed = text.trim();
|
|
39
|
+
if (!trimmed) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
if (trimmed.startsWith(TOOL_WARNING_PREFIX)) {
|
|
43
|
+
return "tool-warning";
|
|
44
|
+
}
|
|
45
|
+
const lower = trimmed.toLowerCase();
|
|
46
|
+
if (lower === MESSAGE_FAILED_PREFIX || lower.startsWith(`${MESSAGE_FAILED_PREFIX}:`)) {
|
|
47
|
+
return "message-failed";
|
|
48
|
+
}
|
|
49
|
+
// Covers both "↪️ Model Fallback: …" and "↪️ Model Fallback cleared: …".
|
|
50
|
+
if (lower.startsWith(FALLBACK_NOTICE_PREFIX)) {
|
|
51
|
+
return "model-fallback";
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A notice glued to a real reply is not a notice: `classifySystemNotice` looks
|
|
57
|
+
* at the start of the text on purpose, so an answer that quotes or discusses a
|
|
58
|
+
* warning still goes out. Only a payload that IS the notice — the whole text,
|
|
59
|
+
* possibly with the group-address prefix core adds — is suppressible.
|
|
60
|
+
*
|
|
61
|
+
* The address prefix ("@name, ") is applied by the channel after this check,
|
|
62
|
+
* so the text seen here is core's payload verbatim.
|
|
63
|
+
*/
|
|
64
|
+
function shouldSuppressGroupSystemNotice(params) {
|
|
65
|
+
// DMs keep the telemetry: there the reader is the person running the agent.
|
|
66
|
+
if (params.targetKind !== "group" && params.targetKind !== "channel") {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return classifySystemNotice(params.text);
|
|
70
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "2.20.2",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED