comisai 1.0.27 → 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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/agent",
3
3
  "private": true,
4
- "version": "1.0.27",
4
+ "version": "1.0.28",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "AI agent executor, budget control, and session management for Comis",
@@ -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.debug({
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
- }, "Auto-reply suppressed");
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);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/channels",
3
3
  "private": true,
4
- "version": "1.0.27",
4
+ "version": "1.0.28",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Chat platform adapters — Discord, Telegram, Slack, WhatsApp, Signal, iMessage, IRC, LINE",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/cli",
3
3
  "private": true,
4
- "version": "1.0.27",
4
+ "version": "1.0.28",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Command-line interface for the Comis AI agent platform",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/core",
3
3
  "private": true,
4
- "version": "1.0.27",
4
+ "version": "1.0.28",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Core domain types, ports, event bus, security, and config for Comis",
@@ -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: { isRestartContinuation: true, mcpStatusLine: mcpStatusLine ?? null },
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
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/daemon",
3
3
  "private": true,
4
- "version": "1.0.27",
4
+ "version": "1.0.28",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Background daemon and orchestrator for the Comis platform",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/gateway",
3
3
  "private": true,
4
- "version": "1.0.27",
4
+ "version": "1.0.28",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "HTTP, JSON-RPC, and WebSocket gateway for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/infra",
3
3
  "private": true,
4
- "version": "1.0.27",
4
+ "version": "1.0.28",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Structured logging infrastructure for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/memory",
3
3
  "private": true,
4
- "version": "1.0.27",
4
+ "version": "1.0.28",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "SQLite memory, embeddings, and RAG storage for Comis agents",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/scheduler",
3
3
  "private": true,
4
- "version": "1.0.27",
4
+ "version": "1.0.28",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Task scheduling and cron management for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/shared",
3
3
  "private": true,
4
- "version": "1.0.27",
4
+ "version": "1.0.28",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Shared types and utilities for the Comis platform",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/skills",
3
3
  "private": true,
4
- "version": "1.0.27",
4
+ "version": "1.0.28",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Skill system, MCP integration, and tool sandbox for Comis agents",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comis/web",
3
- "version": "1.0.27",
3
+ "version": "1.0.28",
4
4
  "description": "Web dashboard SPA for Comis agent management",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comisai",
3
- "version": "1.0.27",
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.27",
115
- "@comis/core": "1.0.27",
116
- "@comis/infra": "1.0.27",
117
- "@comis/memory": "1.0.27",
118
- "@comis/gateway": "1.0.27",
119
- "@comis/skills": "1.0.27",
120
- "@comis/scheduler": "1.0.27",
121
- "@comis/agent": "1.0.27",
122
- "@comis/channels": "1.0.27",
123
- "@comis/cli": "1.0.27",
124
- "@comis/daemon": "1.0.27",
125
- "@comis/web": "1.0.27",
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",