comisai 1.0.26 → 1.0.28
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/node_modules/@comis/agent/package.json +1 -1
- package/node_modules/@comis/channels/dist/shared/inbound-gate.js +21 -3
- package/node_modules/@comis/channels/dist/telegram/message-mapper.d.ts +18 -1
- package/node_modules/@comis/channels/dist/telegram/message-mapper.js +95 -1
- package/node_modules/@comis/channels/dist/telegram/telegram-adapter.js +7 -1
- package/node_modules/@comis/channels/package.json +1 -1
- package/node_modules/@comis/cli/package.json +1 -1
- package/node_modules/@comis/core/package.json +1 -1
- package/node_modules/@comis/daemon/dist/daemon.js +23 -1
- package/node_modules/@comis/daemon/dist/wiring/restart-continuation.d.ts +10 -0
- package/node_modules/@comis/daemon/package.json +1 -1
- package/node_modules/@comis/gateway/package.json +1 -1
- package/node_modules/@comis/infra/package.json +1 -1
- package/node_modules/@comis/memory/package.json +1 -1
- package/node_modules/@comis/scheduler/package.json +1 -1
- package/node_modules/@comis/shared/package.json +1 -1
- package/node_modules/@comis/skills/package.json +1 -1
- package/node_modules/@comis/web/package.json +1 -1
- package/package.json +14 -14
|
@@ -65,6 +65,18 @@ export async function evaluateInboundGate(deps, adapter, processedMsg, sessionKe
|
|
|
65
65
|
injectedAsHistory: true,
|
|
66
66
|
timestamp: Date.now(),
|
|
67
67
|
});
|
|
68
|
+
deps.logger.info({
|
|
69
|
+
channelType: adapter.channelType,
|
|
70
|
+
chatId: msg.channelId,
|
|
71
|
+
senderId: msg.senderId,
|
|
72
|
+
reason: decision.reason,
|
|
73
|
+
activationMode: arConfig.groupActivation,
|
|
74
|
+
isBotMentioned: msg.metadata?.isBotMentioned === true,
|
|
75
|
+
replyToBot: msg.metadata?.replyToBot === true,
|
|
76
|
+
action: "inject-history",
|
|
77
|
+
hint: "Group activation policy did not match — message saved as history context only. Set autoReplyEngine.groupActivation=always to respond to all group messages, or @-mention/reply to the bot to activate it.",
|
|
78
|
+
errorKind: "config",
|
|
79
|
+
}, "Group message did not activate agent");
|
|
68
80
|
// Push to group history ring buffer for context injection
|
|
69
81
|
if (deps.groupHistoryBuffer) {
|
|
70
82
|
deps.groupHistoryBuffer.push(formatSessionKey(sessionKey), msg);
|
|
@@ -100,12 +112,18 @@ export async function evaluateInboundGate(deps, adapter, processedMsg, sessionKe
|
|
|
100
112
|
injectedAsHistory: false,
|
|
101
113
|
timestamp: Date.now(),
|
|
102
114
|
});
|
|
103
|
-
deps.logger.
|
|
104
|
-
step: "auto-reply-suppressed",
|
|
115
|
+
deps.logger.info({
|
|
105
116
|
channelType: adapter.channelType,
|
|
106
117
|
chatId: msg.channelId,
|
|
118
|
+
senderId: msg.senderId,
|
|
107
119
|
reason: decision.reason,
|
|
108
|
-
|
|
120
|
+
activationMode: arConfig.groupActivation,
|
|
121
|
+
isBotMentioned: msg.metadata?.isBotMentioned === true,
|
|
122
|
+
replyToBot: msg.metadata?.replyToBot === true,
|
|
123
|
+
action: "ignore",
|
|
124
|
+
hint: "Group activation policy did not match and history injection is disabled. Set autoReplyEngine.groupActivation=always or autoReplyEngine.historyInjection=true to change.",
|
|
125
|
+
errorKind: "config",
|
|
126
|
+
}, "Group message ignored");
|
|
109
127
|
return { action: "skip" };
|
|
110
128
|
}
|
|
111
129
|
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { NormalizedMessage } from "@comis/core";
|
|
2
2
|
import type { Message } from "grammy/types";
|
|
3
|
+
/**
|
|
4
|
+
* Identifying details of the bot account, used to detect addressing in
|
|
5
|
+
* inbound Telegram messages (mentions, replies, bot_command targets).
|
|
6
|
+
*
|
|
7
|
+
* Sourced from `bot.api.getMe()` after token validation in the adapter.
|
|
8
|
+
*/
|
|
9
|
+
export interface TelegramBotIdentity {
|
|
10
|
+
id: number;
|
|
11
|
+
username: string;
|
|
12
|
+
}
|
|
3
13
|
/**
|
|
4
14
|
* Map a Grammy Message object to a NormalizedMessage.
|
|
5
15
|
*
|
|
@@ -14,8 +24,15 @@ import type { Message } from "grammy/types";
|
|
|
14
24
|
* - Media -> attachments via `buildAttachments()`
|
|
15
25
|
* - Platform metadata preserved in `metadata` field
|
|
16
26
|
*
|
|
27
|
+
* When `bot` is provided, message entities and `reply_to_message` are
|
|
28
|
+
* inspected to populate `metadata.isBotMentioned`, `metadata.replyToBot`,
|
|
29
|
+
* and `metadata.isBotCommand` so that the inbound gate's mention-gated
|
|
30
|
+
* activation policy can correctly route addressed group messages to the
|
|
31
|
+
* agent. Omitting `bot` preserves prior behavior (no addressing flags).
|
|
32
|
+
*
|
|
17
33
|
* @param msg - A plain Telegram Message object
|
|
18
34
|
* @param chatId - The chat ID (used as channelId)
|
|
35
|
+
* @param bot - Optional bot identity for addressing detection
|
|
19
36
|
* @returns A fully populated NormalizedMessage
|
|
20
37
|
*/
|
|
21
|
-
export declare function mapGrammyToNormalized(msg: Message, chatId: number): NormalizedMessage;
|
|
38
|
+
export declare function mapGrammyToNormalized(msg: Message, chatId: number, bot?: TelegramBotIdentity): NormalizedMessage;
|
|
@@ -2,6 +2,81 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { buildAttachments } from "./media-handler.js";
|
|
3
3
|
import { normalizeLocation } from "../shared/location-normalizer.js";
|
|
4
4
|
import { resolveTelegramThreadContext } from "./thread-context.js";
|
|
5
|
+
/**
|
|
6
|
+
* Inspect a Telegram message for any signal that the bot is being addressed:
|
|
7
|
+
*
|
|
8
|
+
* - `mention` entity (`@username`) matching the bot's username
|
|
9
|
+
* - `text_mention` entity referencing the bot's user id (no public username
|
|
10
|
+
* required; this is what private bots receive)
|
|
11
|
+
* - `bot_command` entity — bare `/cmd` (privacy-off DM) or `/cmd@<botUsername>`
|
|
12
|
+
* (group with privacy-on)
|
|
13
|
+
* - `reply_to_message` whose author is the bot itself
|
|
14
|
+
*
|
|
15
|
+
* Mentions of *other* users / `text_mention` of *other* users / commands
|
|
16
|
+
* targeted at *other* bots do not flip any flag.
|
|
17
|
+
*/
|
|
18
|
+
function detectBotAddressing(msg, bot) {
|
|
19
|
+
const result = {
|
|
20
|
+
isBotMentioned: false,
|
|
21
|
+
replyToBot: false,
|
|
22
|
+
isBotCommand: false,
|
|
23
|
+
};
|
|
24
|
+
// Reply-to detection: a reply to a message authored by the bot is treated
|
|
25
|
+
// as addressing, mirroring the convention used by other channels.
|
|
26
|
+
if (msg.reply_to_message?.from?.id === bot.id) {
|
|
27
|
+
result.replyToBot = true;
|
|
28
|
+
}
|
|
29
|
+
// Entities live on text or caption depending on whether the message is
|
|
30
|
+
// a plain message or a media-with-caption.
|
|
31
|
+
const entities = msg.entities ?? msg.caption_entities ?? [];
|
|
32
|
+
if (entities.length === 0) {
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
const source = msg.text ?? msg.caption ?? "";
|
|
36
|
+
const expectedMention = `@${bot.username.toLowerCase()}`;
|
|
37
|
+
for (const ent of entities) {
|
|
38
|
+
if (ent.type === "mention") {
|
|
39
|
+
// `mention` entity covers `@username` text — slice and case-insensitive compare.
|
|
40
|
+
const slice = source.slice(ent.offset, ent.offset + ent.length).toLowerCase();
|
|
41
|
+
if (slice === expectedMention) {
|
|
42
|
+
result.isBotMentioned = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (ent.type === "text_mention") {
|
|
46
|
+
// `text_mention` entity carries a `user` payload — used for bots without
|
|
47
|
+
// a public username, or when Telegram resolves the mention server-side.
|
|
48
|
+
const tm = ent;
|
|
49
|
+
if (tm.user?.id === bot.id) {
|
|
50
|
+
result.isBotMentioned = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (ent.type === "bot_command") {
|
|
54
|
+
// Slash command targeting: `/cmd` (no target — DM/privacy-off) or
|
|
55
|
+
// `/cmd@<botUsername>` (group with privacy-on). Either form addressed
|
|
56
|
+
// to this bot activates it; commands targeted at *other* bots do not.
|
|
57
|
+
const slice = source.slice(ent.offset, ent.offset + ent.length);
|
|
58
|
+
const atIdx = slice.indexOf("@");
|
|
59
|
+
if (atIdx === -1) {
|
|
60
|
+
// Bare /cmd — only meaningful in DMs or privacy-off groups, where
|
|
61
|
+
// Telegram delivers it to us in the first place.
|
|
62
|
+
result.isBotCommand = true;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const target = slice.slice(atIdx + 1).toLowerCase();
|
|
66
|
+
if (target === bot.username.toLowerCase()) {
|
|
67
|
+
result.isBotCommand = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// A bot_command entity for this bot implies activation — surface it as a
|
|
73
|
+
// mention so downstream gates (which key off `isBotMentioned`) treat it
|
|
74
|
+
// identically to an explicit @mention.
|
|
75
|
+
if (result.isBotCommand) {
|
|
76
|
+
result.isBotMentioned = true;
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
5
80
|
/**
|
|
6
81
|
* Map a Grammy Message object to a NormalizedMessage.
|
|
7
82
|
*
|
|
@@ -16,11 +91,18 @@ import { resolveTelegramThreadContext } from "./thread-context.js";
|
|
|
16
91
|
* - Media -> attachments via `buildAttachments()`
|
|
17
92
|
* - Platform metadata preserved in `metadata` field
|
|
18
93
|
*
|
|
94
|
+
* When `bot` is provided, message entities and `reply_to_message` are
|
|
95
|
+
* inspected to populate `metadata.isBotMentioned`, `metadata.replyToBot`,
|
|
96
|
+
* and `metadata.isBotCommand` so that the inbound gate's mention-gated
|
|
97
|
+
* activation policy can correctly route addressed group messages to the
|
|
98
|
+
* agent. Omitting `bot` preserves prior behavior (no addressing flags).
|
|
99
|
+
*
|
|
19
100
|
* @param msg - A plain Telegram Message object
|
|
20
101
|
* @param chatId - The chat ID (used as channelId)
|
|
102
|
+
* @param bot - Optional bot identity for addressing detection
|
|
21
103
|
* @returns A fully populated NormalizedMessage
|
|
22
104
|
*/
|
|
23
|
-
export function mapGrammyToNormalized(msg, chatId) {
|
|
105
|
+
export function mapGrammyToNormalized(msg, chatId, bot) {
|
|
24
106
|
const metadata = {
|
|
25
107
|
telegramMessageId: msg.message_id,
|
|
26
108
|
telegramChatType: msg.chat.type,
|
|
@@ -42,6 +124,18 @@ export function mapGrammyToNormalized(msg, chatId) {
|
|
|
42
124
|
metadata.telegramIsForum = isForum;
|
|
43
125
|
metadata.telegramThreadScope = threadCtx.scope;
|
|
44
126
|
}
|
|
127
|
+
// Bot addressing detection — only when bot identity is supplied. The
|
|
128
|
+
// adapter populates `bot` after token validation; tests that exercise
|
|
129
|
+
// the pure mapper without a bot identity continue to omit these flags.
|
|
130
|
+
if (bot) {
|
|
131
|
+
const addressing = detectBotAddressing(msg, bot);
|
|
132
|
+
if (addressing.isBotMentioned)
|
|
133
|
+
metadata.isBotMentioned = true;
|
|
134
|
+
if (addressing.replyToBot)
|
|
135
|
+
metadata.replyToBot = true;
|
|
136
|
+
if (addressing.isBotCommand)
|
|
137
|
+
metadata.isBotCommand = true;
|
|
138
|
+
}
|
|
45
139
|
// Extract text from message body or caption
|
|
46
140
|
let text = msg.text ?? msg.caption ?? "";
|
|
47
141
|
// GPS location extraction from venue and location messages
|
|
@@ -125,6 +125,9 @@ export function createTelegramAdapter(deps) {
|
|
|
125
125
|
const handlers = [];
|
|
126
126
|
let _channelId = "telegram-pending";
|
|
127
127
|
let runnerHandle = null;
|
|
128
|
+
// Bot identity is populated after token validation in start(). Used by the
|
|
129
|
+
// message mapper to detect mentions/replies/bot_commands aimed at this bot.
|
|
130
|
+
let botIdentity;
|
|
128
131
|
// Health tracking
|
|
129
132
|
let _connected = false;
|
|
130
133
|
let _startedAt;
|
|
@@ -140,7 +143,7 @@ export function createTelegramAdapter(deps) {
|
|
|
140
143
|
return;
|
|
141
144
|
}
|
|
142
145
|
_lastMessageAt = Date.now();
|
|
143
|
-
const normalized = mapGrammyToNormalized(msg, chatId);
|
|
146
|
+
const normalized = mapGrammyToNormalized(msg, chatId, botIdentity);
|
|
144
147
|
deps.logger.info({ channelType: "telegram", messageId: normalized.id, chatId: String(chatId), previewLen: (normalized.text ?? "").length }, "Inbound message");
|
|
145
148
|
for (const handler of handlers) {
|
|
146
149
|
// Fire-and-forget: don't block Grammy middleware
|
|
@@ -175,6 +178,9 @@ export function createTelegramAdapter(deps) {
|
|
|
175
178
|
}
|
|
176
179
|
const botInfo = tokenResult.value;
|
|
177
180
|
_channelId = `telegram-${botInfo.id}`;
|
|
181
|
+
// Populate identity now that getMe() has succeeded — message mapper
|
|
182
|
+
// uses this to detect mentions/replies/bot_commands aimed at us.
|
|
183
|
+
botIdentity = { id: botInfo.id, username: botInfo.username };
|
|
178
184
|
// Validate webhook secret if provided
|
|
179
185
|
if (deps.webhookSecret) {
|
|
180
186
|
const secretResult = validateWebhookSecret(deps.webhookSecret);
|
|
@@ -656,11 +656,18 @@ export async function main(overrides = {}) {
|
|
|
656
656
|
onTaskExtraction: extractFromConversation,
|
|
657
657
|
// Restart continuation: track recently-active sessions for SIGUSR2 replay
|
|
658
658
|
onMessageProcessed: (msg, channelType) => {
|
|
659
|
+
// Preserve channel-native chat type so post-restart synthetic messages
|
|
660
|
+
// can frame group sessions correctly. Without this, group inbounds are
|
|
661
|
+
// mis-framed as DMs on first turn after restart.
|
|
662
|
+
const chatType = typeof msg.metadata?.telegramChatType === "string"
|
|
663
|
+
? msg.metadata.telegramChatType
|
|
664
|
+
: undefined;
|
|
659
665
|
continuationTracker.track({
|
|
660
666
|
agentId: defaultAgentId,
|
|
661
667
|
channelType,
|
|
662
668
|
channelId: msg.channelId,
|
|
663
669
|
userId: msg.senderId,
|
|
670
|
+
chatType,
|
|
664
671
|
tenantId: container.config.tenantId,
|
|
665
672
|
timestamp: Date.now(),
|
|
666
673
|
});
|
|
@@ -1168,6 +1175,21 @@ export async function main(overrides = {}) {
|
|
|
1168
1175
|
daemonLogger.debug({ channelType: record.channelType, channelId: record.channelId }, "Skipping continuation replay: session already active this cycle");
|
|
1169
1176
|
continue;
|
|
1170
1177
|
}
|
|
1178
|
+
// Rehydrate chat-type metadata so downstream resolveChatType /
|
|
1179
|
+
// isGroupMessage classify the resumed session correctly. Without this,
|
|
1180
|
+
// a synthetic restart message for a group is mis-framed as a DM on the
|
|
1181
|
+
// first post-restart turn.
|
|
1182
|
+
const syntheticMetadata = {
|
|
1183
|
+
isRestartContinuation: true,
|
|
1184
|
+
mcpStatusLine: mcpStatusLine ?? null,
|
|
1185
|
+
};
|
|
1186
|
+
if (record.channelType === "telegram" && record.chatType) {
|
|
1187
|
+
syntheticMetadata.telegramChatType = record.chatType;
|
|
1188
|
+
}
|
|
1189
|
+
if (record.chatType === "group" || record.chatType === "supergroup") {
|
|
1190
|
+
// Channel-agnostic flag mirrored by other adapters (e.g. WhatsApp).
|
|
1191
|
+
syntheticMetadata.isGroup = true;
|
|
1192
|
+
}
|
|
1171
1193
|
const syntheticMsg = {
|
|
1172
1194
|
id: randomUUID(),
|
|
1173
1195
|
channelId: record.channelId,
|
|
@@ -1176,7 +1198,7 @@ export async function main(overrides = {}) {
|
|
|
1176
1198
|
text: mcpStatusLine ? `${baseText}\n${mcpStatusLine}` : baseText,
|
|
1177
1199
|
timestamp: Date.now(),
|
|
1178
1200
|
attachments: [],
|
|
1179
|
-
metadata:
|
|
1201
|
+
metadata: syntheticMetadata,
|
|
1180
1202
|
};
|
|
1181
1203
|
channelManager.injectMessage(record.channelType, syntheticMsg).catch((injectErr) => {
|
|
1182
1204
|
daemonLogger.warn({ err: injectErr, channelType: record.channelType, channelId: record.channelId, hint: "Continuation replay failed; user can re-send to resume", errorKind: "internal" }, "Failed to replay continuation");
|
|
@@ -16,6 +16,16 @@ export interface ContinuationRecord {
|
|
|
16
16
|
peerId?: string;
|
|
17
17
|
guildId?: string;
|
|
18
18
|
threadId?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Channel-native chat type tag captured at track-time so the synthetic
|
|
21
|
+
* restart message can frame the resumed conversation correctly.
|
|
22
|
+
*
|
|
23
|
+
* For Telegram: `"private"`, `"group"`, `"supergroup"`, or `"channel"`
|
|
24
|
+
* (sourced from `metadata.telegramChatType`). Without this, group sessions
|
|
25
|
+
* are mis-framed as DMs on first turn after restart because the synthetic
|
|
26
|
+
* inbound carries no chat-type metadata.
|
|
27
|
+
*/
|
|
28
|
+
chatType?: string;
|
|
19
29
|
tenantId: string;
|
|
20
30
|
timestamp: number;
|
|
21
31
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comisai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"author": "Moshe Anconina",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "Security-first AI agent platform — connects AI agents to Discord, Telegram, Slack, WhatsApp, and more",
|
|
@@ -111,18 +111,18 @@
|
|
|
111
111
|
"@comis/web"
|
|
112
112
|
],
|
|
113
113
|
"dependencies": {
|
|
114
|
-
"@comis/shared": "1.0.
|
|
115
|
-
"@comis/core": "1.0.
|
|
116
|
-
"@comis/infra": "1.0.
|
|
117
|
-
"@comis/memory": "1.0.
|
|
118
|
-
"@comis/gateway": "1.0.
|
|
119
|
-
"@comis/skills": "1.0.
|
|
120
|
-
"@comis/scheduler": "1.0.
|
|
121
|
-
"@comis/agent": "1.0.
|
|
122
|
-
"@comis/channels": "1.0.
|
|
123
|
-
"@comis/cli": "1.0.
|
|
124
|
-
"@comis/daemon": "1.0.
|
|
125
|
-
"@comis/web": "1.0.
|
|
114
|
+
"@comis/shared": "1.0.28",
|
|
115
|
+
"@comis/core": "1.0.28",
|
|
116
|
+
"@comis/infra": "1.0.28",
|
|
117
|
+
"@comis/memory": "1.0.28",
|
|
118
|
+
"@comis/gateway": "1.0.28",
|
|
119
|
+
"@comis/skills": "1.0.28",
|
|
120
|
+
"@comis/scheduler": "1.0.28",
|
|
121
|
+
"@comis/agent": "1.0.28",
|
|
122
|
+
"@comis/channels": "1.0.28",
|
|
123
|
+
"@comis/cli": "1.0.28",
|
|
124
|
+
"@comis/daemon": "1.0.28",
|
|
125
|
+
"@comis/web": "1.0.28",
|
|
126
126
|
"@agentclientprotocol/sdk": "^0.19.0",
|
|
127
127
|
"@clack/core": "^1.1.0",
|
|
128
128
|
"@clack/prompts": "^1.1.0",
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"iconv-lite": "^0.7.2",
|
|
165
165
|
"ignore": "^7.0.5",
|
|
166
166
|
"imapflow": "^1.2.18",
|
|
167
|
-
"impit": "
|
|
167
|
+
"impit": "0.13.0",
|
|
168
168
|
"ipaddr.js": "^2.3.0",
|
|
169
169
|
"irc-framework": "^4.14.0",
|
|
170
170
|
"json-rpc-2.0": "^1.7.1",
|