clawgram 2.19.3 → 2.20.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
@@ -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");
@@ -200,9 +201,25 @@ exports.CORE_ACTION_SYNONYMS = {
200
201
  "channel-info": "chatInfo",
201
202
  "member-info": "participants",
202
203
  "download-file": "fetch-media",
204
+ // Chat management. `kick` was already accepted; the rest were advertised
205
+ // under names core does not know and were therefore never callable from the
206
+ // tool at all — 2.19.4 gives them core's nearest name. `transferOwnership`
207
+ // and `inviteLink` have no counterpart in that vocabulary and stay
208
+ // gateway-only, as does `joins`.
209
+ "channel-create": "createGroup",
210
+ addParticipant: "addMembers",
211
+ kick: "removeMember",
212
+ "role-add": "promoteAdmin",
213
+ "role-remove": "demoteAdmin",
203
214
  };
204
215
  /** Canonical management action for every accepted spelling. */
205
216
  const MANAGE_ACTION_ALIASES = {
217
+ // Core's spellings first — these are the only ones the agent's tool can
218
+ // reach; see CORE_ACTION_SYNONYMS.
219
+ "channel-create": "createGroup",
220
+ addParticipant: "addMembers",
221
+ "role-add": "promoteAdmin",
222
+ "role-remove": "demoteAdmin",
206
223
  createGroup: "createGroup",
207
224
  createChat: "createGroup",
208
225
  "create-group": "createGroup",
@@ -429,12 +446,12 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
429
446
  "Explicit targets may be @username, numeric Telegram user id, phone/contact resolvable by Telegram, group chat ids, or clawgram:<target>.",
430
447
  "For Telegram forum topics, send to the group chat id and pass the topic id separately as `threadId`.",
431
448
  "Use the `react` action to acknowledge a message with an emoji instead of sending a reply; pass an empty `emoji` (or `remove: true`) to take the reaction back.",
432
- "Use the `channel-info` action (clawgram also answers to `chatInfo`) to learn what a chat is — title, type, member count, description, pinned message — instead of guessing from its id.",
449
+ "Use the `channel-info` action to learn what a chat is — title, type, member count, description, pinned message — instead of guessing from its id. Name the chat with `chatId` and do not pass `target`: core refuses it for this action, and the descriptive spelling `chatInfo` is not callable from this tool at all.",
433
450
  "Use the `thread-list` action to list a forum's topics by name (optional `query` narrows by title); that is where a `threadId` comes from when someone names a topic instead of quoting a message in it. Name the chat with `chatId` and do not pass `target` — core refuses it for this action. `topics` is the same call under a name core does not know, and is only reachable through the gateway RPC.",
434
451
  "Pass that `threadId` to `read` as well: without it a forum read returns every topic interleaved rather than the one that was asked about.",
435
- "Use the `download-file` action (or its alias `fetch-media`) to fetch the attachment on a message `read` reported. Name the chat with `chatId` and the message with `messageId`; do not pass `target` — core refuses it for this action: `mode: \"read\"` returns a description of an image or a transcript of a voice note, `\"file\"` returns a path to reuse, `\"both\"` (default) returns both. `read` only says an attachment exists; this is what brings it.",
436
- "Use the `channel-list` action (clawgram also answers to `dialogs`) to find out which group chats this account is actually in — including ones nobody has configured yet. It reports id, title and type only, never direct chats, and only when the account enables `discoverChats`.",
437
- "Use `member-info` with a `chatId` to list who is in a chat; `joins` has no name in core's vocabulary and is reachable only through the gateway RPC.",
452
+ "Use the `download-file` action to fetch the attachment on a message `read` reported. Name the chat with `chatId` and the message with `messageId`; do not pass `target` — core refuses it for this action: `mode: \"read\"` returns a description of an image or a transcript of a voice note, `\"file\"` returns a path to reuse, `\"both\"` (default) returns both. `read` only says an attachment exists; this is what brings it.",
453
+ "Use the `channel-list` action to find out which group chats this account is actually in — including ones nobody has configured yet. It reports id, title and type only, never direct chats, and only when the account enables `discoverChats`.",
454
+ "Use `member-info` with a `chatId` to list who is in a chat, and `kick` with a `chatId` and `userId` to remove someone from a managed chat. The rest of the chat-management family and `joins` have no name core knows, so they are reachable only through the gateway RPC, not from this tool.",
438
455
  "Use `createGroup` (title, optional about, optional users) to create a new Telegram supergroup; `addMembers`/`removeMember` change who is in a managed chat, `promoteAdmin`/`demoteAdmin` grant or revoke admin rights, `transferOwnership` hands the chat over, `inviteLink` issues an invite link for people Telegram refused to add directly.",
439
456
  ],
440
457
  messageToolCapabilities: () => [
@@ -442,10 +459,10 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
442
459
  "clawgram can send text messages to direct chats and groups from the connected personal account.",
443
460
  "clawgram supports Telegram forum topics via the `threadId` parameter on group sends.",
444
461
  "clawgram can add and clear emoji reactions on messages. A plain Telegram account holds one reaction per message, so a new emoji replaces the previous one.",
445
- "clawgram can describe a chat via `chatInfo`: title, type (direct/group/supergroup/channel), member count, description, whether it is a forum, and the pinned message id.",
446
- "clawgram can list the topics of a forum supergroup via `topics`: id, title, last message, and whether a topic is closed, hidden or pinned.",
447
- "clawgram can fetch the attachment on any message inside its read scope via `fetch-media`: images come back described, voice notes transcribed, and either can be returned as a file path for reuse.",
448
- "clawgram can list the group chats the account belongs to via `dialogs`, when the account sets discoverChats. Metadata only, no direct chats — it answers \"where am I\", not \"what was said\".",
462
+ "clawgram can describe a chat via `channel-info`: title, type (direct/group/supergroup/channel), member count, description, whether it is a forum, and the pinned message id.",
463
+ "clawgram can list the topics of a forum supergroup via `thread-list`: id, title, last message, and whether a topic is closed, hidden or pinned.",
464
+ "clawgram can fetch the attachment on any message inside its read scope via `download-file`: images come back described, voice notes transcribed, and either can be returned as a file path for reuse.",
465
+ "clawgram can list the group chats the account belongs to via `channel-list`, when the account sets discoverChats. Metadata only, no direct chats — it answers \"where am I\", not \"what was said\".",
449
466
  "clawgram can manage chats where the account's manageChats config allows it: create supergroups, add and remove members, promote and demote admins, transfer ownership, and export invite links.",
450
467
  ],
451
468
  },
@@ -1412,32 +1429,40 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
1412
1429
  // Leaving it out does not degrade to a text send: the agent simply
1413
1430
  // never sees a way to send the file, announces it in words, and the
1414
1431
  // file stays on disk. That is exactly what happened on 2026-08-07.
1432
+ // Only names core already knows. An action outside
1433
+ // `CHANNEL_MESSAGE_ACTION_NAMES` cannot be called from the agent's
1434
+ // `message` tool at all — it is simultaneously "requires a target"
1435
+ // and "does not accept a target" — so advertising one is handing the
1436
+ // agent a trap. It cost a broken reply in a live chat on 2026-08-31:
1437
+ // the agent picked the descriptive `chatInfo`, got both halves of
1438
+ // the contradiction, and the turn ended in `✉️ Message failed`.
1439
+ //
1440
+ // The descriptive spellings (`topics`, `dialogs`, `chatInfo`,
1441
+ // `participants`, `joins`, `fetch-media`, the manage family) still
1442
+ // work in `handleAction`, so gateway RPC and existing skills keep
1443
+ // calling them — RPC does not consult this list. They are simply not
1444
+ // offered to the agent, which has no way to use them.
1445
+ //
1446
+ // `upload-file` is what core dispatches when an agent has an
1447
+ // attachment to deliver — a generated image is the common case.
1448
+ // Leaving it out does not degrade to a text send: the agent simply
1449
+ // never sees a way to send the file, announces it in words, and the
1450
+ // file stays on disk. That is exactly what happened on 2026-08-07.
1451
+ //
1452
+ // `kick` is core's name for `removeMember`; the rest of the manage
1453
+ // family has no core equivalent and stays gateway-only until it gets
1454
+ // one. `joins` likewise.
1415
1455
  actions: [
1416
- "send", "read", "participants", "joins", "react", "chatInfo", "topics", "dialogs", "upload-file",
1417
- // Core's own names for four of the above. Without them the
1418
- // action is advertised and unreachable — see CORE_ACTION_SYNONYMS.
1419
- "thread-list", "channel-list", "channel-info", "member-info",
1456
+ "send", "read", "react", "upload-file",
1420
1457
  // Reading an attachment that is already in a chat. `read` reports
1421
1458
  // that a photo exists; this is what turns it into something the
1422
1459
  // agent can look at or pass on.
1423
- //
1424
- // Two names on purpose. `download-file` is core's own vocabulary
1425
- // (`CHANNEL_MESSAGE_ACTION_NAMES`), and core's target policy is
1426
- // keyed by that vocabulary: an action it does not know is both
1427
- // "requires a target" (`MESSAGE_ACTION_TARGET_MODE[action] !==
1428
- // "none"` is true for `undefined`) and "does not accept a target"
1429
- // (the same lookup defaults to `"none"` when a target is passed).
1430
- // That contradiction is unresolvable from the caller's side —
1431
- // measured on 2026-08-24, when the agent tried every combination
1432
- // and got one of the two errors each time. `download-file` is
1433
- // mapped to `"none"`, so it has no such contradiction;
1434
- // `fetch-media` stays as the descriptive name and is made usable
1435
- // by the alias declaration below.
1436
- "fetch-media", "download-file",
1460
+ "download-file",
1461
+ // Core's names for the chat-shaped reads see CORE_ACTION_SYNONYMS.
1462
+ "thread-list", "channel-list", "channel-info", "member-info",
1437
1463
  // Chat management (2.12.0) — gated by the account's manageChats
1438
1464
  // scope; without it every one of these is refused.
1439
- "createGroup", "addMembers", "removeMember",
1440
- "promoteAdmin", "demoteAdmin", "transferOwnership", "inviteLink",
1465
+ "channel-create", "addParticipant", "kick", "role-add", "role-remove",
1441
1466
  ],
1442
1467
  capabilities: [],
1443
1468
  mediaSourceParams: {
@@ -2381,6 +2406,24 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
2381
2406
  });
2382
2407
  return { skipped: "silent" };
2383
2408
  }
2409
+ // Core's operational chatter (tool-error warnings, fallback notices)
2410
+ // stays out of group chats: it is telemetry for the operator, not a
2411
+ // reply to the room, and it has already been seen carrying shell
2412
+ // commands with secret-store paths. DMs keep it. The text itself is
2413
+ // never logged — see system-notice.ts for why.
2414
+ const suppressedNotice = (0, system_notice_1.shouldSuppressGroupSystemNotice)({
2415
+ targetKind: (0, helpers_1.inferOutboundTargetKind)(ctx.to),
2416
+ text: ctx.text,
2417
+ });
2418
+ if (suppressedNotice) {
2419
+ actionLog.warn("clawgram suppressing system notice in group", {
2420
+ accountId: ctx.accountId,
2421
+ rawTo: ctx.to,
2422
+ noticeKind: suppressedNotice,
2423
+ textLength: ctx.text.length,
2424
+ });
2425
+ return { skipped: "system-notice" };
2426
+ }
2384
2427
  const gram = runtimes.get(ctx.accountId);
2385
2428
  if (!gram) {
2386
2429
  throw new Error(`clawgram: runtime not found for account ${ctx.accountId}`);
@@ -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
+ }
@@ -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.19.3",
5
+ "version": "2.20.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.19.3",
3
+ "version": "2.20.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": {