clawgram 2.17.1 → 2.18.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/README.md +24 -1
- package/dist/channel.js +38 -9
- package/dist/gramjs-client.js +22 -0
- package/dist/helpers.js +49 -14
- package/dist/silent-reaction.js +33 -4
- package/openclaw.plugin.json +13 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -236,19 +236,42 @@ reconnect), not the whole Gateway.
|
|
|
236
236
|
| `manageChats` | string[] | unset | Chats the assistant may **manage** — see [Chat management](#chat-management). Absent or empty = management off; `["*"]` = every chat |
|
|
237
237
|
| `replyParseMode` | `"html"` \| `"markdown"` \| `"none"` | unset | Outbound format for replies, core-delivered text, captions and `send` calls that omit `parseMode` — see [Message formatting](#message-formatting) |
|
|
238
238
|
| `twoFaPassword` | string \| SecretRef | unset | The account's Telegram 2FA password; read only by `transferOwnership` |
|
|
239
|
+
| `reactionModel` | string | unset | Model ref or alias for the emoji pick on a silent mention. Unset = the agent's own model. Needs `plugins.entries.clawgram.llm.allowModelOverride: true` in the gateway config; without it the override is refused and the pick quietly falls back to the default model |
|
|
239
240
|
|
|
240
241
|
Group config fields:
|
|
241
242
|
|
|
242
243
|
| Field | Type | Default | Description |
|
|
243
244
|
|---|---|---|---|
|
|
244
245
|
| `enabled` | boolean | `true` | Enables or disables replies in the group |
|
|
245
|
-
| `groupPolicy` | `"open"` \| `"mention"`
|
|
246
|
+
| `groupPolicy` | `"open"` \| `"mention"` \| `"tag"` | `"mention"` | What wakes the agent here — see [What wakes the agent in a group](#what-wakes-the-agent-in-a-group) |
|
|
246
247
|
| `allowFrom` | string[] | `["*"]` | Allowed sender IDs/usernames inside that group |
|
|
247
248
|
| `tools` | object | unset | `{ allow?, alsoAllow?, deny? }` — tool policy for this group; see [Per-group tools, skills and system prompt](#per-group-tools-skills-and-system-prompt) |
|
|
248
249
|
| `toolsBySender` | object | unset | Per-sender tool policy inside this group, keys `id:<id>`, `username:<handle>`, `name:<display>` or `*` |
|
|
249
250
|
| `skills` | string[] | unset | Skill allowlist for this group; `[]` = no skills here, unset = the agent's skills |
|
|
250
251
|
| `systemPrompt` | string | unset | Trusted prompt block appended for messages from this group |
|
|
251
252
|
|
|
253
|
+
### What wakes the agent in a group
|
|
254
|
+
|
|
255
|
+
`groupPolicy` decides which messages start a turn at all. It is a ladder, widest
|
|
256
|
+
first, and the rung is chosen per group:
|
|
257
|
+
|
|
258
|
+
| Rung | Wakes on | Use it when |
|
|
259
|
+
|---|---|---|
|
|
260
|
+
| `open` | every message in the chat | the agent works as a member of the team and an address may carry no name at all |
|
|
261
|
+
| `mention` | the name it answers to, an `@username`, or a reply to it | the default: the agent is a participant, not a fixture |
|
|
262
|
+
| `tag` | an `@username` or a reply to it — **never the name** | the name occurs in conversation constantly, as it does in a large community |
|
|
263
|
+
|
|
264
|
+
Two things are worth knowing before reaching for `open`:
|
|
265
|
+
|
|
266
|
+
- it spends a **full turn on every message**, chatter included. Whether words
|
|
267
|
+
are owed is then the agent's decision, and most of the time the answer is no;
|
|
268
|
+
- the typing indicator is shown only for messages that actually addressed the
|
|
269
|
+
agent. Under `open` the room would otherwise watch it "type" through
|
|
270
|
+
conversations it is merely reading, with nothing following.
|
|
271
|
+
|
|
272
|
+
Emoji reactions are unaffected by the rung: the channel leaves one only where
|
|
273
|
+
the agent was genuinely addressed, so background reading stays unmarked.
|
|
274
|
+
|
|
252
275
|
### Per-group tools, skills and system prompt
|
|
253
276
|
|
|
254
277
|
Since 2.17.0 a group entry can narrow what the assistant does *in that chat*
|
package/dist/channel.js
CHANGED
|
@@ -80,6 +80,22 @@ function readAccountReactionLevel(cfg, accountId) {
|
|
|
80
80
|
}
|
|
81
81
|
return cfg?.channels?.[constants_1.CHANNEL_ID]?.accounts?.[resolvedAccountId]?.reactionLevel;
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Model ref for the emoji pick, when the account names one.
|
|
85
|
+
*
|
|
86
|
+
* Picking one emoji out of a fixed list of 68 is the cheapest judgement this
|
|
87
|
+
* channel makes and the only model call it makes on its own; running it on the
|
|
88
|
+
* agent's own head spends the expensive quota on a decision a small model
|
|
89
|
+
* makes just as well.
|
|
90
|
+
*/
|
|
91
|
+
function readAccountReactionModel(cfg, accountId) {
|
|
92
|
+
const resolvedAccountId = (0, helpers_1.resolveConfiguredAccountId)(cfg, accountId);
|
|
93
|
+
if (!resolvedAccountId) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const raw = cfg?.channels?.[constants_1.CHANNEL_ID]?.accounts?.[resolvedAccountId]?.reactionModel;
|
|
97
|
+
return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
|
|
98
|
+
}
|
|
83
99
|
/**
|
|
84
100
|
* Wires `reactToSilentMention` to this account's runtime, config and log.
|
|
85
101
|
*
|
|
@@ -96,6 +112,7 @@ async function reactToSilentMentionForAccount(params) {
|
|
|
96
112
|
}
|
|
97
113
|
await (0, silent_reaction_1.reactToSilentMention)({
|
|
98
114
|
appetite: (0, reactions_1.resolveAgentReactionGuidance)(readAccountReactionLevel(params.cfg, params.accountId)),
|
|
115
|
+
model: readAccountReactionModel(params.cfg, params.accountId),
|
|
99
116
|
wasMentioned: params.wasMentioned,
|
|
100
117
|
chatId: params.chatId,
|
|
101
118
|
messageId: params.messageId,
|
|
@@ -696,13 +713,19 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
696
713
|
// channelRuntime comes from the untyped ctx, so the generic route type falls
|
|
697
714
|
// back to the minimal RouteLike. The runtime value is a ResolvedAgentRoute.
|
|
698
715
|
const route = inboundRoute;
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
message: rawMessage
|
|
705
|
-
|
|
716
|
+
// Under `tag` the name is not an address: in a chat of a thousand
|
|
717
|
+
// people it occurs in conversation constantly and is aimed at her
|
|
718
|
+
// almost never. Only the `@` counts, and it is the same fact the
|
|
719
|
+
// stricter rung of the ladder is named after.
|
|
720
|
+
const wasMentioned = groupConfig.groupPolicy === "tag"
|
|
721
|
+
? (0, helpers_1.hasExplicitTelegramMention)({ selfUsername, text, message: rawMessage })
|
|
722
|
+
: (0, helpers_1.hasTelegramMention)({
|
|
723
|
+
cfg,
|
|
724
|
+
agentId: route.agentId,
|
|
725
|
+
selfUsername,
|
|
726
|
+
text,
|
|
727
|
+
message: rawMessage,
|
|
728
|
+
});
|
|
706
729
|
const wasReplyToSelf = await (0, helpers_1.isReplyToSelfMessage)(rawMessage, selfId);
|
|
707
730
|
const mentionDecision = (0, channel_inbound_1.resolveInboundMentionDecision)({
|
|
708
731
|
facts: {
|
|
@@ -712,7 +735,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
712
735
|
},
|
|
713
736
|
policy: {
|
|
714
737
|
isGroup: true,
|
|
715
|
-
requireMention: groupConfig.groupPolicy
|
|
738
|
+
requireMention: groupConfig.groupPolicy !== "open",
|
|
716
739
|
allowTextCommands: false,
|
|
717
740
|
hasControlCommand: false,
|
|
718
741
|
commandAuthorized: true,
|
|
@@ -723,6 +746,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
723
746
|
chatId: normalized.chatId,
|
|
724
747
|
messageId: normalized.messageId,
|
|
725
748
|
selfUsername,
|
|
749
|
+
groupPolicy: groupConfig.groupPolicy,
|
|
726
750
|
mentionedFlag: rawMessage?.mentioned === true,
|
|
727
751
|
hasEntities: Array.isArray(rawMessage?.entities) ? rawMessage.entities.length : 0,
|
|
728
752
|
wasMentioned,
|
|
@@ -730,7 +754,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
730
754
|
shouldSkip: mentionDecision.shouldSkip,
|
|
731
755
|
text,
|
|
732
756
|
});
|
|
733
|
-
if (groupConfig.groupPolicy
|
|
757
|
+
if (groupConfig.groupPolicy !== "open" && mentionDecision.shouldSkip && !wasReplyToSelf) {
|
|
734
758
|
log?.info?.("clawgram skipping group message without mention", {
|
|
735
759
|
accountId,
|
|
736
760
|
chatId: normalized.chatId,
|
|
@@ -993,6 +1017,11 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
993
1017
|
}, {
|
|
994
1018
|
readMessageId: Number(normalized.messageId),
|
|
995
1019
|
messageThreadId,
|
|
1020
|
+
// The indicator is a promise of an answer, and it is owed only
|
|
1021
|
+
// to someone who addressed her. Under `open` the turn runs on
|
|
1022
|
+
// every message in the chat, so without this the whole room
|
|
1023
|
+
// watches her "type" through conversations she is only reading.
|
|
1024
|
+
typing: mentionDecision.effectiveWasMentioned || wasReplyToSelf,
|
|
996
1025
|
});
|
|
997
1026
|
log?.info?.("clawgram group inbound handled", {
|
|
998
1027
|
accountId,
|
package/dist/gramjs-client.js
CHANGED
|
@@ -565,7 +565,29 @@ class GramJsClientManager {
|
|
|
565
565
|
}
|
|
566
566
|
await this.client.markAsRead(resolved.peer, messageId).catch(() => undefined);
|
|
567
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* Runs `fn` while the chat shows "typing", and marks the message read.
|
|
570
|
+
*
|
|
571
|
+
* `typing: false` keeps the read receipt and drops the indicator. The two
|
|
572
|
+
* are separable because they promise different things: reading is what the
|
|
573
|
+
* agent did, typing is a promise that words are coming. Under
|
|
574
|
+
* `groupPolicy: "open"` every message starts a turn, and most of those turns
|
|
575
|
+
* end in silence — on 2026-08-17 the management chat watched «Тина
|
|
576
|
+
* печатает…» for 20–26 seconds on each of four messages that were never
|
|
577
|
+
* addressed to her, and nothing followed. An indicator that is not owed to
|
|
578
|
+
* anyone is worse than no indicator.
|
|
579
|
+
*/
|
|
568
580
|
async withTyping(target, fn, options) {
|
|
581
|
+
if (options?.typing === false) {
|
|
582
|
+
// Still a read receipt: she did read it, and the chat may show that.
|
|
583
|
+
const resolvedPeer = await this.resolvePeer(target).then((r) => r.peer).catch(() => undefined);
|
|
584
|
+
if (resolvedPeer) {
|
|
585
|
+
await this.markRead(resolvedPeer, options?.readMessageId, {
|
|
586
|
+
messageThreadId: options?.messageThreadId,
|
|
587
|
+
}).catch(() => undefined);
|
|
588
|
+
}
|
|
589
|
+
return await fn();
|
|
590
|
+
}
|
|
569
591
|
let peer;
|
|
570
592
|
let readMarked = false;
|
|
571
593
|
let stopped = false;
|
package/dist/helpers.js
CHANGED
|
@@ -27,6 +27,7 @@ exports.resolveActiveUsername = resolveActiveUsername;
|
|
|
27
27
|
exports.normalizeAllowEntry = normalizeAllowEntry;
|
|
28
28
|
exports.isSenderAllowed = isSenderAllowed;
|
|
29
29
|
exports.hasTelegramMention = hasTelegramMention;
|
|
30
|
+
exports.hasExplicitTelegramMention = hasExplicitTelegramMention;
|
|
30
31
|
exports.toDisplayName = toDisplayName;
|
|
31
32
|
exports.withTimeout = withTimeout;
|
|
32
33
|
exports.stripSilentReplyToken = stripSilentReplyToken;
|
|
@@ -271,8 +272,17 @@ function resolveAllowFrom(value) {
|
|
|
271
272
|
const entries = value.map((entry) => String(entry).trim()).filter(Boolean);
|
|
272
273
|
return entries.length > 0 ? entries : ["*"];
|
|
273
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Three rungs, widest first: `open` wakes on every message, `mention` on the
|
|
277
|
+
* name or an `@`, `tag` on the `@` alone. Anything unrecognised lands on
|
|
278
|
+
* `mention`, the rung this channel has always defaulted to.
|
|
279
|
+
*/
|
|
274
280
|
function resolveGroupPolicy(value) {
|
|
275
|
-
|
|
281
|
+
if (value === "open")
|
|
282
|
+
return "open";
|
|
283
|
+
if (value === "tag")
|
|
284
|
+
return "tag";
|
|
285
|
+
return "mention";
|
|
276
286
|
}
|
|
277
287
|
/**
|
|
278
288
|
* Per-group `skills` → core `replyOptions.skillFilter`, `systemPrompt` →
|
|
@@ -342,19 +352,27 @@ function isSenderAllowed(input) {
|
|
|
342
352
|
].filter((value) => Boolean(value)).map(normalizeAllowEntry);
|
|
343
353
|
return input.allowFrom.map(normalizeAllowEntry).some((entry) => senderIds.includes(entry));
|
|
344
354
|
}
|
|
345
|
-
|
|
355
|
+
/**
|
|
356
|
+
* Whether the message tags this account with `@username` — nothing else.
|
|
357
|
+
*
|
|
358
|
+
* The name the agent answers to (`Тина`, and whatever else core's mention
|
|
359
|
+
* regexes carry) is deliberately NOT consulted. In a thousand-person chat the
|
|
360
|
+
* name occurs in conversation constantly and almost never as an address; the
|
|
361
|
+
* `@` is the one form that is unambiguously aimed at her. This is the whole of
|
|
362
|
+
* `groupPolicy: "tag"`.
|
|
363
|
+
*
|
|
364
|
+
* `message.mentioned` is Telegram's own flag and stays in: the client sets it
|
|
365
|
+
* for an @-mention and for a reply to her, and both are addresses.
|
|
366
|
+
*/
|
|
367
|
+
function hasExplicitTelegramMention(input) {
|
|
346
368
|
const normalizedText = input.text.trim();
|
|
347
369
|
const message = input.message;
|
|
348
|
-
const mentionRegexes = (0, channel_inbound_1.buildMentionRegexes)(input.cfg, input.agentId);
|
|
349
370
|
const selfUsername = input.selfUsername?.replace(/^@/, "").trim();
|
|
371
|
+
if (!selfUsername) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
350
374
|
const entities = Array.isArray(message?.entities) ? message.entities : [];
|
|
351
|
-
const
|
|
352
|
-
entities.some((entity) => {
|
|
353
|
-
const kind = typeof entity?.className === "string" ? entity.className : entity?.type;
|
|
354
|
-
return kind === "MessageEntityMention" || kind === "mention" || kind === "MessageEntityMentionName" || kind === "InputMessageEntityMentionName";
|
|
355
|
-
}) ||
|
|
356
|
-
/(^|\s)@[a-zA-Z0-9_]{5,}\b/.test(normalizedText);
|
|
357
|
-
const entityExplicitMention = Boolean(selfUsername) && entities.some((entity) => {
|
|
375
|
+
const entityExplicitMention = entities.some((entity) => {
|
|
358
376
|
const kind = typeof entity?.className === "string" ? entity.className : entity?.type;
|
|
359
377
|
if (kind !== "MessageEntityMention" && kind !== "mention") {
|
|
360
378
|
return false;
|
|
@@ -366,10 +384,27 @@ function hasTelegramMention(input) {
|
|
|
366
384
|
}
|
|
367
385
|
return normalizedText.slice(offset, offset + length).replace(/^@/, "").trim().toLowerCase() === selfUsername.toLowerCase();
|
|
368
386
|
});
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
387
|
+
return message?.mentioned === true ||
|
|
388
|
+
entityExplicitMention ||
|
|
389
|
+
new RegExp(`(^|\\s)@${selfUsername.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i").test(normalizedText);
|
|
390
|
+
}
|
|
391
|
+
function hasTelegramMention(input) {
|
|
392
|
+
const normalizedText = input.text.trim();
|
|
393
|
+
const message = input.message;
|
|
394
|
+
const mentionRegexes = (0, channel_inbound_1.buildMentionRegexes)(input.cfg, input.agentId);
|
|
395
|
+
const selfUsername = input.selfUsername?.replace(/^@/, "").trim();
|
|
396
|
+
const entities = Array.isArray(message?.entities) ? message.entities : [];
|
|
397
|
+
const hasAnyMention = Boolean(message?.mentioned) ||
|
|
398
|
+
entities.some((entity) => {
|
|
399
|
+
const kind = typeof entity?.className === "string" ? entity.className : entity?.type;
|
|
400
|
+
return kind === "MessageEntityMention" || kind === "mention" || kind === "MessageEntityMentionName" || kind === "InputMessageEntityMentionName";
|
|
401
|
+
}) ||
|
|
402
|
+
/(^|\s)@[a-zA-Z0-9_]{5,}\b/.test(normalizedText);
|
|
403
|
+
const explicitlyMentioned = hasExplicitTelegramMention({
|
|
404
|
+
selfUsername: input.selfUsername,
|
|
405
|
+
text: input.text,
|
|
406
|
+
message,
|
|
407
|
+
});
|
|
373
408
|
return (0, channel_inbound_1.matchesMentionWithExplicit)({
|
|
374
409
|
text: normalizedText,
|
|
375
410
|
mentionRegexes,
|
package/dist/silent-reaction.js
CHANGED
|
@@ -151,6 +151,33 @@ function shouldReactToSilentTurn(params) {
|
|
|
151
151
|
}
|
|
152
152
|
/** The message text is truncated before it reaches the model; a mood needs no more. */
|
|
153
153
|
const MAX_JUDGED_CHARS = 2000;
|
|
154
|
+
/**
|
|
155
|
+
* Asks for the emoji on the configured model, falling back to the default one.
|
|
156
|
+
*
|
|
157
|
+
* The fallback is not defensive habit. Core refuses a plugin's model override
|
|
158
|
+
* unless `plugins.entries.clawgram.llm.allowModelOverride` is set, and the
|
|
159
|
+
* refusal is a throw — which this feature swallows by design. Without the
|
|
160
|
+
* retry, pointing `reactionModel` at a cheap model in a config that never
|
|
161
|
+
* granted the permission would not make reactions cheaper, it would make them
|
|
162
|
+
* disappear, and the log would say nothing about why.
|
|
163
|
+
*/
|
|
164
|
+
async function completeEmoji(params) {
|
|
165
|
+
const request = {
|
|
166
|
+
messages: [{ role: "user", content: params.content }],
|
|
167
|
+
systemPrompt: params.systemPrompt,
|
|
168
|
+
maxTokens: 8,
|
|
169
|
+
purpose: "clawgram: emoji reaction for a silent mention",
|
|
170
|
+
};
|
|
171
|
+
if (!params.model) {
|
|
172
|
+
return { answer: await params.deps.complete(request), fellBack: false };
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
return { answer: await params.deps.complete({ ...request, model: params.model }), fellBack: false };
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return { answer: await params.deps.complete(request), fellBack: true };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
154
181
|
/**
|
|
155
182
|
* Leaves an emoji on a message the agent was named in but chose not to answer.
|
|
156
183
|
*
|
|
@@ -187,11 +214,11 @@ async function reactToSilentMention(params) {
|
|
|
187
214
|
params.deps.onDecision?.({ messageId, appetite, chose: "none", allowedCount: 0 });
|
|
188
215
|
return undefined;
|
|
189
216
|
}
|
|
190
|
-
const answer = await
|
|
191
|
-
|
|
217
|
+
const { answer, fellBack } = await completeEmoji({
|
|
218
|
+
deps: params.deps,
|
|
219
|
+
model: params.model,
|
|
192
220
|
systemPrompt: buildEmojiSystemPrompt(appetite, allowed),
|
|
193
|
-
|
|
194
|
-
purpose: "clawgram: emoji reaction for a silent mention",
|
|
221
|
+
content: String(params.messageText ?? "").slice(0, MAX_JUDGED_CHARS),
|
|
195
222
|
});
|
|
196
223
|
const emoji = parseEmojiChoice(answer?.text, allowed);
|
|
197
224
|
params.deps.onDecision?.({
|
|
@@ -200,6 +227,8 @@ async function reactToSilentMention(params) {
|
|
|
200
227
|
chose: emoji ? "emoji" : "none",
|
|
201
228
|
emoji,
|
|
202
229
|
allowedCount: allowed?.length,
|
|
230
|
+
...params.model ? { model: params.model } : {},
|
|
231
|
+
...fellBack ? { modelFellBack: true } : {},
|
|
203
232
|
});
|
|
204
233
|
if (!emoji) {
|
|
205
234
|
return undefined;
|
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.18.0",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
|
@@ -53,9 +53,11 @@
|
|
|
53
53
|
"type": "string",
|
|
54
54
|
"enum": [
|
|
55
55
|
"open",
|
|
56
|
-
"mention"
|
|
56
|
+
"mention",
|
|
57
|
+
"tag"
|
|
57
58
|
],
|
|
58
|
-
"default": "mention"
|
|
59
|
+
"default": "mention",
|
|
60
|
+
"description": "What wakes the agent in this chat (2.18.0). `mention` \u2014 the name, an @mention or a reply to it; `open` \u2014 every message, the agent decides for itself whether words are owed; `tag` \u2014 only an explicit @username or a reply, for chats where the name occurs in conversation constantly."
|
|
59
61
|
},
|
|
60
62
|
"allowFrom": {
|
|
61
63
|
"type": "array",
|
|
@@ -358,9 +360,11 @@
|
|
|
358
360
|
"type": "string",
|
|
359
361
|
"enum": [
|
|
360
362
|
"open",
|
|
361
|
-
"mention"
|
|
363
|
+
"mention",
|
|
364
|
+
"tag"
|
|
362
365
|
],
|
|
363
|
-
"default": "mention"
|
|
366
|
+
"default": "mention",
|
|
367
|
+
"description": "What wakes the agent in this chat (2.18.0). `mention` \u2014 the name, an @mention or a reply to it; `open` \u2014 every message; `tag` \u2014 only an explicit @username or a reply."
|
|
364
368
|
},
|
|
365
369
|
"allowFrom": {
|
|
366
370
|
"type": "array",
|
|
@@ -507,6 +511,10 @@
|
|
|
507
511
|
],
|
|
508
512
|
"description": "How freely the agent may leave emoji reactions (2.8.0). Core injects its `## Reactions` prompt section only for `minimal` and `extensive`; `off` and `ack` leave the prompt silent about reactions, and the agent then effectively never reacts on its own. Absent means `minimal`. Mirrors the bundled Telegram channel's reactionLevel."
|
|
509
513
|
},
|
|
514
|
+
"reactionModel": {
|
|
515
|
+
"type": "string",
|
|
516
|
+
"description": "Model ref or alias for the emoji pick on a silent mention (2.18.0). Absent = the agent's own model, which is what every version before this used. Requires `plugins.entries.clawgram.llm.allowModelOverride: true` in the gateway config; without it core refuses the override and the pick falls back to the default model rather than losing the reaction."
|
|
517
|
+
},
|
|
510
518
|
"replyParseMode": {
|
|
511
519
|
"type": "string",
|
|
512
520
|
"enum": [
|
package/package.json
CHANGED