@prometheus-ai/agent 0.5.3 → 0.5.4
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/CHANGELOG.md +9 -0
- package/dist/types/cli/gateway-cli.d.ts +4 -0
- package/dist/types/commands/gateway.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/gateway/adapters/telegram/access.d.ts +4 -1
- package/dist/types/gateway/adapters/telegram/setup-api.d.ts +1 -1
- package/dist/types/gateway/adapters/telegram/webhook.d.ts +1 -1
- package/dist/types/gateway/types.d.ts +1 -1
- package/package.json +9 -9
- package/src/cli/gateway-cli.ts +32 -2
- package/src/commands/gateway.ts +4 -0
- package/src/config/settings-schema.ts +9 -0
- package/src/gateway/adapters/telegram/access.ts +39 -4
- package/src/gateway/adapters/telegram/normalize.ts +16 -1
- package/src/gateway/adapters/telegram/setup-api.ts +7 -1
- package/src/gateway/adapters/telegram/webhook.ts +26 -5
- package/src/gateway/context.ts +9 -1
- package/src/gateway/types.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/modes/setup-wizard/scenes/telegram.ts +77 -22
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.5.4] - 2026-06-14
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Route Telegram `edited_message`, `channel_post`, and `edited_channel_post`
|
|
10
|
+
update envelopes through the native gateway message path.
|
|
11
|
+
- Add a Telegram `allowedChats` allowlist so senderless channel posts can be
|
|
12
|
+
authorized without weakening user-id access checks.
|
|
13
|
+
|
|
5
14
|
## [0.5.3] - 2026-06-13
|
|
6
15
|
|
|
7
16
|
### Changed
|
|
@@ -27,6 +27,7 @@ export interface GatewayCommandFlags {
|
|
|
27
27
|
parseMode?: string;
|
|
28
28
|
disableNotification?: boolean;
|
|
29
29
|
telegramAllowedUsers?: string;
|
|
30
|
+
telegramAllowedChats?: string;
|
|
30
31
|
telegramExpressionCatalog?: string;
|
|
31
32
|
}
|
|
32
33
|
export interface GatewayBindConfig {
|
|
@@ -48,6 +49,7 @@ export interface TelegramGatewayCliConfig {
|
|
|
48
49
|
disableNotification?: boolean;
|
|
49
50
|
preferLargeMedia?: boolean;
|
|
50
51
|
allowedUserIds?: string[];
|
|
52
|
+
allowedChatIds?: string[];
|
|
51
53
|
expressionEnabled?: boolean;
|
|
52
54
|
expressionMinScore?: number;
|
|
53
55
|
expressionCatalog?: TelegramExpressionCatalog;
|
|
@@ -62,6 +64,7 @@ export interface TelegramGatewaySettingsConfig {
|
|
|
62
64
|
publicBaseUrl?: string;
|
|
63
65
|
webhookPath?: string;
|
|
64
66
|
allowedUsers?: string;
|
|
67
|
+
allowedChats?: string;
|
|
65
68
|
groupScope?: string;
|
|
66
69
|
threadScope?: string;
|
|
67
70
|
parseMode?: TelegramGatewayParseModeSetting;
|
|
@@ -91,5 +94,6 @@ export declare function runGatewayCommand(cmd: GatewayCommandArgs): Promise<void
|
|
|
91
94
|
export declare function buildGatewaySessionDir(rootSessionDir: string, sessionKey: string): string;
|
|
92
95
|
export declare function encodeGatewaySessionKeySegment(sessionKey: string): string;
|
|
93
96
|
export declare function parseTelegramAllowedUserIds(value: string | undefined): string[] | undefined;
|
|
97
|
+
export declare function parseTelegramAllowedChatIds(value: string | undefined): string[] | undefined;
|
|
94
98
|
export declare function normalizeTelegramBotUsername(value: string | undefined): string | undefined;
|
|
95
99
|
export {};
|
|
@@ -39,6 +39,9 @@ export default class Gateway extends Command {
|
|
|
39
39
|
"telegram-allowed-users": import("@prometheus-ai/utils/cli").FlagDescriptor<"string"> & {
|
|
40
40
|
description: string;
|
|
41
41
|
};
|
|
42
|
+
"telegram-allowed-chats": import("@prometheus-ai/utils/cli").FlagDescriptor<"string"> & {
|
|
43
|
+
description: string;
|
|
44
|
+
};
|
|
42
45
|
"telegram-expression-catalog": import("@prometheus-ai/utils/cli").FlagDescriptor<"string"> & {
|
|
43
46
|
description: string;
|
|
44
47
|
};
|
|
@@ -181,6 +181,15 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
181
181
|
readonly description: "Comma-separated Telegram user IDs allowed to use the gateway";
|
|
182
182
|
};
|
|
183
183
|
};
|
|
184
|
+
readonly "gateway.telegram.allowedChats": {
|
|
185
|
+
readonly type: "string";
|
|
186
|
+
readonly default: undefined;
|
|
187
|
+
readonly ui: {
|
|
188
|
+
readonly tab: "channels";
|
|
189
|
+
readonly label: "Telegram Allowed Chats";
|
|
190
|
+
readonly description: "Comma-separated Telegram chat IDs allowed to use the gateway";
|
|
191
|
+
};
|
|
192
|
+
};
|
|
184
193
|
readonly "gateway.telegram.groupScope": {
|
|
185
194
|
readonly type: "enum";
|
|
186
195
|
readonly values: readonly ["shared", "per-user"];
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { GatewayEvent } from "../../types";
|
|
2
2
|
export interface TelegramGatewayAccessPolicy {
|
|
3
3
|
allowedUserIds: string[];
|
|
4
|
+
allowedChatIds?: string[];
|
|
4
5
|
}
|
|
5
6
|
export type TelegramGatewayAuthorizer = (event: GatewayEvent) => boolean | Promise<boolean>;
|
|
6
|
-
export declare function createTelegramUserAllowlistPolicy(userIds: Iterable<string>): TelegramGatewayAccessPolicy | undefined;
|
|
7
|
+
export declare function createTelegramUserAllowlistPolicy(userIds: Iterable<string>, chatIds?: Iterable<string>): TelegramGatewayAccessPolicy | undefined;
|
|
7
8
|
export declare function isTelegramGatewayAccessRestricted(policy: TelegramGatewayAccessPolicy | undefined): boolean;
|
|
8
9
|
export declare function isTelegramGatewayEventAuthorized(event: GatewayEvent, policy: TelegramGatewayAccessPolicy | undefined): boolean;
|
|
9
10
|
export declare function normalizeTelegramUserIds(userIds: Iterable<string>): string[];
|
|
11
|
+
export declare function normalizeTelegramChatIds(chatIds: Iterable<string>): string[];
|
|
10
12
|
export declare function normalizeTelegramPositiveIntegerId(value: string, label: string): string;
|
|
13
|
+
export declare function normalizeTelegramSignedIntegerId(value: string, label: string): string;
|
|
11
14
|
export declare function toTelegramPositiveIntegerId(value: string, label: string): number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type TelegramFetch } from "./send-message";
|
|
2
2
|
import type { TelegramUpdate, TelegramUser } from "./types";
|
|
3
|
-
export declare const TELEGRAM_GATEWAY_ALLOWED_UPDATES: readonly ["message", "callback_query"];
|
|
3
|
+
export declare const TELEGRAM_GATEWAY_ALLOWED_UPDATES: readonly ["message", "edited_message", "channel_post", "edited_channel_post", "callback_query"];
|
|
4
4
|
export interface TelegramSetupApiOptions {
|
|
5
5
|
apiBaseUrl?: string;
|
|
6
6
|
fetch?: TelegramFetch;
|
|
@@ -4,7 +4,7 @@ import type { TelegramUpdate } from "./types";
|
|
|
4
4
|
export declare const TELEGRAM_WEBHOOK_SECRET_HEADER = "X-Telegram-Bot-Api-Secret-Token";
|
|
5
5
|
export declare const TELEGRAM_UPDATE_TYPES: readonly ["message", "edited_message", "channel_post", "edited_channel_post", "business_connection", "business_message", "edited_business_message", "deleted_business_messages", "message_reaction", "message_reaction_count", "inline_query", "chosen_inline_result", "callback_query", "shipping_query", "pre_checkout_query", "purchased_paid_media", "poll", "poll_answer", "my_chat_member", "chat_member", "chat_join_request", "chat_boost", "removed_chat_boost", "managed_bot", "guest_message"];
|
|
6
6
|
export type TelegramUpdateType = (typeof TELEGRAM_UPDATE_TYPES)[number];
|
|
7
|
-
export type TelegramRoutableUpdateType = "message" | "callback_query";
|
|
7
|
+
export type TelegramRoutableUpdateType = "message" | "edited_message" | "channel_post" | "edited_channel_post" | "callback_query";
|
|
8
8
|
export type TelegramWebhookIgnoreReason = "unsupported_update_type" | "unknown_update_type" | "unroutable_message" | "unroutable_callback_query" | "duplicate_update";
|
|
9
9
|
type TelegramWebhookErrorStatus = 400 | 401 | 405 | 500;
|
|
10
10
|
export interface TelegramWebhookOptions extends TelegramNormalizeOptions {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type GatewayPlatform = "telegram";
|
|
2
2
|
export type GatewayChatType = "direct" | "group" | "supergroup" | "channel";
|
|
3
3
|
export type GatewayParticipantScope = "shared" | "per-user";
|
|
4
|
-
export type GatewayEventKind = "message" | "callback_query";
|
|
4
|
+
export type GatewayEventKind = "message" | "edited_message" | "channel_post" | "edited_channel_post" | "callback_query";
|
|
5
5
|
export interface GatewayCallbackInteraction {
|
|
6
6
|
type: "callback_query";
|
|
7
7
|
id: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@prometheus-ai/agent",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.4",
|
|
5
5
|
"description": "Prometheus coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://prometheus.trivlab.com",
|
|
7
7
|
"author": "Uttam Trivedi",
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
"@agentclientprotocol/sdk": "0.22.1",
|
|
45
45
|
"@babel/parser": "^7.29.7",
|
|
46
46
|
"@mozilla/readability": "^0.6.0",
|
|
47
|
-
"@prometheus-ai/hashline": "0.5.
|
|
48
|
-
"@prometheus-ai/stats": "0.5.
|
|
49
|
-
"@prometheus-ai/agent-core": "0.5.
|
|
50
|
-
"@prometheus-ai/ai": "0.5.
|
|
51
|
-
"@prometheus-ai/memory": "0.5.
|
|
52
|
-
"@prometheus-ai/natives": "0.5.
|
|
53
|
-
"@prometheus-ai/tui": "0.5.
|
|
54
|
-
"@prometheus-ai/utils": "0.5.
|
|
47
|
+
"@prometheus-ai/hashline": "0.5.4",
|
|
48
|
+
"@prometheus-ai/stats": "0.5.4",
|
|
49
|
+
"@prometheus-ai/agent-core": "0.5.4",
|
|
50
|
+
"@prometheus-ai/ai": "0.5.4",
|
|
51
|
+
"@prometheus-ai/memory": "0.5.4",
|
|
52
|
+
"@prometheus-ai/natives": "0.5.4",
|
|
53
|
+
"@prometheus-ai/tui": "0.5.4",
|
|
54
|
+
"@prometheus-ai/utils": "0.5.4",
|
|
55
55
|
"@opentelemetry/api": "^1.9.1",
|
|
56
56
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
57
57
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
package/src/cli/gateway-cli.ts
CHANGED
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { APP_NAME } from "@prometheus-ai/utils";
|
|
10
10
|
import { isSettingsInitialized, Settings, settings } from "../config/settings";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
createTelegramUserAllowlistPolicy,
|
|
13
|
+
normalizeTelegramChatIds,
|
|
14
|
+
normalizeTelegramUserIds,
|
|
15
|
+
} from "../gateway/adapters/telegram/access";
|
|
12
16
|
import {
|
|
13
17
|
parseTelegramExpressionSettingsCatalog,
|
|
14
18
|
type TelegramExpressionCatalog,
|
|
@@ -62,6 +66,7 @@ export interface GatewayCommandFlags {
|
|
|
62
66
|
parseMode?: string;
|
|
63
67
|
disableNotification?: boolean;
|
|
64
68
|
telegramAllowedUsers?: string;
|
|
69
|
+
telegramAllowedChats?: string;
|
|
65
70
|
telegramExpressionCatalog?: string;
|
|
66
71
|
}
|
|
67
72
|
|
|
@@ -85,6 +90,7 @@ export interface TelegramGatewayCliConfig {
|
|
|
85
90
|
disableNotification?: boolean;
|
|
86
91
|
preferLargeMedia?: boolean;
|
|
87
92
|
allowedUserIds?: string[];
|
|
93
|
+
allowedChatIds?: string[];
|
|
88
94
|
expressionEnabled?: boolean;
|
|
89
95
|
expressionMinScore?: number;
|
|
90
96
|
expressionCatalog?: TelegramExpressionCatalog;
|
|
@@ -101,6 +107,7 @@ export interface TelegramGatewaySettingsConfig {
|
|
|
101
107
|
publicBaseUrl?: string;
|
|
102
108
|
webhookPath?: string;
|
|
103
109
|
allowedUsers?: string;
|
|
110
|
+
allowedChats?: string;
|
|
104
111
|
groupScope?: string;
|
|
105
112
|
threadScope?: string;
|
|
106
113
|
parseMode?: TelegramGatewayParseModeSetting;
|
|
@@ -159,6 +166,7 @@ export function resolveTelegramGatewayConfig(
|
|
|
159
166
|
flags.telegramWebhookSecret !== undefined ||
|
|
160
167
|
flags.telegramWebhookPath !== undefined ||
|
|
161
168
|
flags.telegramAllowedUsers !== undefined ||
|
|
169
|
+
flags.telegramAllowedChats !== undefined ||
|
|
162
170
|
flags.telegramExpressionCatalog !== undefined;
|
|
163
171
|
if (!hasTelegramFlag && !hasTelegramSpecificFlags) {
|
|
164
172
|
throw new Error(
|
|
@@ -249,6 +257,15 @@ export function resolveTelegramGatewayConfig(
|
|
|
249
257
|
configured.allowedUsers,
|
|
250
258
|
),
|
|
251
259
|
),
|
|
260
|
+
allowedChatIds: parseTelegramAllowedChatIds(
|
|
261
|
+
firstNonEmpty(
|
|
262
|
+
flags.telegramAllowedChats,
|
|
263
|
+
env.PROMETHEUS_TELEGRAM_ALLOWED_CHAT_IDS,
|
|
264
|
+
env.PROMETHEUS_TELEGRAM_ALLOWED_CHATS,
|
|
265
|
+
env.TELEGRAM_ALLOWED_CHAT_IDS,
|
|
266
|
+
configured.allowedChats,
|
|
267
|
+
),
|
|
268
|
+
),
|
|
252
269
|
expressionCatalogPath,
|
|
253
270
|
expressionEnabled,
|
|
254
271
|
expressionMinScore: configured.expressions?.minScore,
|
|
@@ -264,6 +281,7 @@ export function readTelegramGatewaySettings(settingsInstance: Settings): Telegra
|
|
|
264
281
|
publicBaseUrl: settingsInstance.get("gateway.telegram.publicBaseUrl"),
|
|
265
282
|
webhookPath: settingsInstance.get("gateway.telegram.webhookPath"),
|
|
266
283
|
allowedUsers: settingsInstance.get("gateway.telegram.allowedUsers"),
|
|
284
|
+
allowedChats: settingsInstance.get("gateway.telegram.allowedChats"),
|
|
267
285
|
groupScope: settingsInstance.get("gateway.telegram.groupScope"),
|
|
268
286
|
threadScope: settingsInstance.get("gateway.telegram.threadScope"),
|
|
269
287
|
parseMode: settingsInstance.get("gateway.telegram.parseMode"),
|
|
@@ -415,7 +433,10 @@ async function runGatewayServe(flags: GatewayCommandFlags): Promise<void> {
|
|
|
415
433
|
try {
|
|
416
434
|
const fileCommands = await discoverSlashCommands(gatewayCwd);
|
|
417
435
|
const botCommands = buildTelegramNativeBotCommands({ fileCommands });
|
|
418
|
-
const accessPolicy = createTelegramUserAllowlistPolicy(
|
|
436
|
+
const accessPolicy = createTelegramUserAllowlistPolicy(
|
|
437
|
+
telegram.allowedUserIds ?? [],
|
|
438
|
+
telegram.allowedChatIds ?? [],
|
|
439
|
+
);
|
|
419
440
|
const expressionCatalog = await resolveTelegramExpressionCatalog(telegram, readTelegramExpressionCatalog);
|
|
420
441
|
const commandRegistration = await registerTelegramNativeBotCommands(telegram.botToken, botCommands, {
|
|
421
442
|
accessPolicy,
|
|
@@ -631,6 +652,15 @@ export function parseTelegramAllowedUserIds(value: string | undefined): string[]
|
|
|
631
652
|
return normalizeTelegramUserIds(parts);
|
|
632
653
|
}
|
|
633
654
|
|
|
655
|
+
export function parseTelegramAllowedChatIds(value: string | undefined): string[] | undefined {
|
|
656
|
+
if (value === undefined || value.trim() === "") return undefined;
|
|
657
|
+
const parts = value
|
|
658
|
+
.split(",")
|
|
659
|
+
.map(part => part.trim())
|
|
660
|
+
.filter(Boolean);
|
|
661
|
+
return normalizeTelegramChatIds(parts);
|
|
662
|
+
}
|
|
663
|
+
|
|
634
664
|
export function normalizeTelegramBotUsername(value: string | undefined): string | undefined {
|
|
635
665
|
const normalized = value?.trim().replace(/^@/u, "");
|
|
636
666
|
return normalized ? normalized : undefined;
|
package/src/commands/gateway.ts
CHANGED
|
@@ -42,6 +42,9 @@ export default class Gateway extends Command {
|
|
|
42
42
|
"telegram-allowed-users": Flags.string({
|
|
43
43
|
description: "Comma-separated Telegram user IDs allowed to use this gateway",
|
|
44
44
|
}),
|
|
45
|
+
"telegram-allowed-chats": Flags.string({
|
|
46
|
+
description: "Comma-separated Telegram chat IDs allowed to use this gateway",
|
|
47
|
+
}),
|
|
45
48
|
"telegram-expression-catalog": Flags.string({
|
|
46
49
|
description: "Optional JSON file with Telegram expression intents and sticker/GIF variants",
|
|
47
50
|
}),
|
|
@@ -88,6 +91,7 @@ export default class Gateway extends Command {
|
|
|
88
91
|
telegramWebhookSecret: flags["telegram-webhook-secret"],
|
|
89
92
|
telegramWebhookPath: flags["telegram-webhook-path"],
|
|
90
93
|
telegramAllowedUsers: flags["telegram-allowed-users"],
|
|
94
|
+
telegramAllowedChats: flags["telegram-allowed-chats"],
|
|
91
95
|
telegramExpressionCatalog: flags["telegram-expression-catalog"],
|
|
92
96
|
allowBotMessages: flags["allow-bot-messages"],
|
|
93
97
|
includeRaw: flags["include-raw"],
|
|
@@ -335,6 +335,15 @@ export const SETTINGS_SCHEMA = {
|
|
|
335
335
|
description: "Comma-separated Telegram user IDs allowed to use the gateway",
|
|
336
336
|
},
|
|
337
337
|
},
|
|
338
|
+
"gateway.telegram.allowedChats": {
|
|
339
|
+
type: "string",
|
|
340
|
+
default: undefined,
|
|
341
|
+
ui: {
|
|
342
|
+
tab: "channels",
|
|
343
|
+
label: "Telegram Allowed Chats",
|
|
344
|
+
description: "Comma-separated Telegram chat IDs allowed to use the gateway",
|
|
345
|
+
},
|
|
346
|
+
},
|
|
338
347
|
"gateway.telegram.groupScope": {
|
|
339
348
|
type: "enum",
|
|
340
349
|
values: ["shared", "per-user"] as const,
|
|
@@ -2,17 +2,26 @@ import type { GatewayEvent } from "../../types";
|
|
|
2
2
|
|
|
3
3
|
export interface TelegramGatewayAccessPolicy {
|
|
4
4
|
allowedUserIds: string[];
|
|
5
|
+
allowedChatIds?: string[];
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export type TelegramGatewayAuthorizer = (event: GatewayEvent) => boolean | Promise<boolean>;
|
|
8
9
|
|
|
9
|
-
export function createTelegramUserAllowlistPolicy(
|
|
10
|
+
export function createTelegramUserAllowlistPolicy(
|
|
11
|
+
userIds: Iterable<string>,
|
|
12
|
+
chatIds: Iterable<string> = [],
|
|
13
|
+
): TelegramGatewayAccessPolicy | undefined {
|
|
10
14
|
const allowedUserIds = normalizeTelegramUserIds(userIds);
|
|
11
|
-
|
|
15
|
+
const allowedChatIds = normalizeTelegramChatIds(chatIds);
|
|
16
|
+
if (allowedUserIds.length === 0 && allowedChatIds.length === 0) return undefined;
|
|
17
|
+
return {
|
|
18
|
+
allowedUserIds,
|
|
19
|
+
...(allowedChatIds.length > 0 ? { allowedChatIds } : {}),
|
|
20
|
+
};
|
|
12
21
|
}
|
|
13
22
|
|
|
14
23
|
export function isTelegramGatewayAccessRestricted(policy: TelegramGatewayAccessPolicy | undefined): boolean {
|
|
15
|
-
return (policy?.allowedUserIds.length ?? 0) > 0;
|
|
24
|
+
return (policy?.allowedUserIds.length ?? 0) > 0 || (policy?.allowedChatIds?.length ?? 0) > 0;
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
export function isTelegramGatewayEventAuthorized(
|
|
@@ -21,7 +30,9 @@ export function isTelegramGatewayEventAuthorized(
|
|
|
21
30
|
): boolean {
|
|
22
31
|
if (!policy || !isTelegramGatewayAccessRestricted(policy)) return true;
|
|
23
32
|
const userId = event.source.userId?.trim();
|
|
24
|
-
|
|
33
|
+
if (userId !== undefined && policy.allowedUserIds.includes(userId)) return true;
|
|
34
|
+
const chatId = event.source.chatId.trim();
|
|
35
|
+
return chatId.length > 0 && (policy.allowedChatIds?.includes(chatId) ?? false);
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
export function normalizeTelegramUserIds(userIds: Iterable<string>): string[] {
|
|
@@ -36,6 +47,18 @@ export function normalizeTelegramUserIds(userIds: Iterable<string>): string[] {
|
|
|
36
47
|
return normalized;
|
|
37
48
|
}
|
|
38
49
|
|
|
50
|
+
export function normalizeTelegramChatIds(chatIds: Iterable<string>): string[] {
|
|
51
|
+
const normalized: string[] = [];
|
|
52
|
+
const seen = new Set<string>();
|
|
53
|
+
for (const chatId of chatIds) {
|
|
54
|
+
const value = normalizeTelegramSignedIntegerId(chatId, "Telegram chat id");
|
|
55
|
+
if (seen.has(value)) continue;
|
|
56
|
+
seen.add(value);
|
|
57
|
+
normalized.push(value);
|
|
58
|
+
}
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
39
62
|
export function normalizeTelegramPositiveIntegerId(value: string, label: string): string {
|
|
40
63
|
const trimmed = value.trim();
|
|
41
64
|
if (!/^[0-9]+$/u.test(trimmed)) {
|
|
@@ -48,6 +71,18 @@ export function normalizeTelegramPositiveIntegerId(value: string, label: string)
|
|
|
48
71
|
return String(numeric);
|
|
49
72
|
}
|
|
50
73
|
|
|
74
|
+
export function normalizeTelegramSignedIntegerId(value: string, label: string): string {
|
|
75
|
+
const trimmed = value.trim();
|
|
76
|
+
if (!/^-?[0-9]+$/u.test(trimmed)) {
|
|
77
|
+
throw new Error(`${label} must be an integer`);
|
|
78
|
+
}
|
|
79
|
+
const numeric = Number(trimmed);
|
|
80
|
+
if (!Number.isSafeInteger(numeric) || numeric === 0) {
|
|
81
|
+
throw new Error(`${label} must be a non-zero safe integer`);
|
|
82
|
+
}
|
|
83
|
+
return String(numeric);
|
|
84
|
+
}
|
|
85
|
+
|
|
51
86
|
export function toTelegramPositiveIntegerId(value: string, label: string): number {
|
|
52
87
|
return Number(normalizeTelegramPositiveIntegerId(value, label));
|
|
53
88
|
}
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
GatewayChatType,
|
|
4
4
|
GatewayEvent,
|
|
5
5
|
GatewayEventFeatures,
|
|
6
|
+
GatewayEventKind,
|
|
6
7
|
GatewayLocation,
|
|
7
8
|
GatewayMediaAttachment,
|
|
8
9
|
GatewayMediaVariant,
|
|
@@ -19,7 +20,8 @@ export function normalizeTelegramUpdate(
|
|
|
19
20
|
update: TelegramUpdate,
|
|
20
21
|
options: TelegramNormalizeOptions = {},
|
|
21
22
|
): GatewayEvent | undefined {
|
|
22
|
-
const
|
|
23
|
+
const messageUpdate = getTelegramMessageUpdate(update);
|
|
24
|
+
const message = messageUpdate?.message;
|
|
23
25
|
if (!message) return undefined;
|
|
24
26
|
const messageUser = message.from ?? message.direct_messages_topic?.user;
|
|
25
27
|
if (messageUser?.is_bot && !options.allowBotMessages) return undefined;
|
|
@@ -47,6 +49,7 @@ export function normalizeTelegramUpdate(
|
|
|
47
49
|
text,
|
|
48
50
|
receivedAt,
|
|
49
51
|
};
|
|
52
|
+
if (messageUpdate.kind !== "message") event.kind = messageUpdate.kind;
|
|
50
53
|
if (media.length > 0) event.media = media;
|
|
51
54
|
if (features !== undefined) event.features = features;
|
|
52
55
|
|
|
@@ -72,6 +75,18 @@ export function normalizeTelegramUpdate(
|
|
|
72
75
|
return event;
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
function getTelegramMessageUpdate(
|
|
79
|
+
update: TelegramUpdate,
|
|
80
|
+
): { kind: Exclude<GatewayEventKind, "callback_query">; message: TelegramMessage } | undefined {
|
|
81
|
+
if (update.message !== undefined) return { kind: "message", message: update.message };
|
|
82
|
+
if (update.edited_message !== undefined) return { kind: "edited_message", message: update.edited_message };
|
|
83
|
+
if (update.channel_post !== undefined) return { kind: "channel_post", message: update.channel_post };
|
|
84
|
+
if (update.edited_channel_post !== undefined) {
|
|
85
|
+
return { kind: "edited_channel_post", message: update.edited_channel_post };
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
export function normalizeTelegramCallbackQuery(
|
|
76
91
|
update: TelegramUpdate,
|
|
77
92
|
options: TelegramNormalizeOptions = {},
|
|
@@ -6,7 +6,13 @@ import {
|
|
|
6
6
|
} from "./send-message";
|
|
7
7
|
import type { TelegramUpdate, TelegramUser } from "./types";
|
|
8
8
|
|
|
9
|
-
export const TELEGRAM_GATEWAY_ALLOWED_UPDATES = [
|
|
9
|
+
export const TELEGRAM_GATEWAY_ALLOWED_UPDATES = [
|
|
10
|
+
"message",
|
|
11
|
+
"edited_message",
|
|
12
|
+
"channel_post",
|
|
13
|
+
"edited_channel_post",
|
|
14
|
+
"callback_query",
|
|
15
|
+
] as const;
|
|
10
16
|
|
|
11
17
|
export interface TelegramSetupApiOptions {
|
|
12
18
|
apiBaseUrl?: string;
|
|
@@ -33,7 +33,12 @@ export const TELEGRAM_UPDATE_TYPES = [
|
|
|
33
33
|
] as const;
|
|
34
34
|
|
|
35
35
|
export type TelegramUpdateType = (typeof TELEGRAM_UPDATE_TYPES)[number];
|
|
36
|
-
export type TelegramRoutableUpdateType =
|
|
36
|
+
export type TelegramRoutableUpdateType =
|
|
37
|
+
| "message"
|
|
38
|
+
| "edited_message"
|
|
39
|
+
| "channel_post"
|
|
40
|
+
| "edited_channel_post"
|
|
41
|
+
| "callback_query";
|
|
37
42
|
|
|
38
43
|
export type TelegramWebhookIgnoreReason =
|
|
39
44
|
| "unsupported_update_type"
|
|
@@ -170,7 +175,7 @@ export async function processTelegramUpdate(
|
|
|
170
175
|
}
|
|
171
176
|
event = normalizeTelegramCallbackQuery(update, options);
|
|
172
177
|
} else {
|
|
173
|
-
if (!hasTelegramMessageEnvelope(update)) {
|
|
178
|
+
if (!hasTelegramMessageEnvelope(update, updateType)) {
|
|
174
179
|
return rejectWebhook(400, "invalid_json_update");
|
|
175
180
|
}
|
|
176
181
|
event = normalizeTelegramUpdate(update, options);
|
|
@@ -223,7 +228,13 @@ export function getTelegramUpdateType(update: TelegramUpdate): TelegramUpdateTyp
|
|
|
223
228
|
export function isRoutableUpdateType(
|
|
224
229
|
updateType: TelegramUpdateType | undefined,
|
|
225
230
|
): updateType is TelegramRoutableUpdateType {
|
|
226
|
-
return
|
|
231
|
+
return (
|
|
232
|
+
updateType === "message" ||
|
|
233
|
+
updateType === "edited_message" ||
|
|
234
|
+
updateType === "channel_post" ||
|
|
235
|
+
updateType === "edited_channel_post" ||
|
|
236
|
+
updateType === "callback_query"
|
|
237
|
+
);
|
|
227
238
|
}
|
|
228
239
|
|
|
229
240
|
export function isValidTelegramWebhookSecretToken(secretToken: string): boolean {
|
|
@@ -256,8 +267,8 @@ function getTelegramCallbackQueryId(update: TelegramUpdate): string | undefined
|
|
|
256
267
|
return normalized ? normalized : undefined;
|
|
257
268
|
}
|
|
258
269
|
|
|
259
|
-
function hasTelegramMessageEnvelope(update: TelegramUpdate): boolean {
|
|
260
|
-
const message = (update
|
|
270
|
+
function hasTelegramMessageEnvelope(update: TelegramUpdate, updateType: TelegramRoutableUpdateType): boolean {
|
|
271
|
+
const message = getTelegramMessageEnvelope(update, updateType);
|
|
261
272
|
if (!isRecord(message)) return false;
|
|
262
273
|
|
|
263
274
|
const chat = message.chat;
|
|
@@ -273,6 +284,16 @@ function hasTelegramMessageEnvelope(update: TelegramUpdate): boolean {
|
|
|
273
284
|
);
|
|
274
285
|
}
|
|
275
286
|
|
|
287
|
+
function getTelegramMessageEnvelope(update: TelegramUpdate, updateType: TelegramRoutableUpdateType): unknown {
|
|
288
|
+
if (updateType === "message") return (update as unknown as Record<string, unknown>).message;
|
|
289
|
+
if (updateType === "edited_message") return (update as unknown as Record<string, unknown>).edited_message;
|
|
290
|
+
if (updateType === "channel_post") return (update as unknown as Record<string, unknown>).channel_post;
|
|
291
|
+
if (updateType === "edited_channel_post") {
|
|
292
|
+
return (update as unknown as Record<string, unknown>).edited_channel_post;
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
276
297
|
function isValidTelegramChatId(value: unknown): boolean {
|
|
277
298
|
if (typeof value === "number") return Number.isSafeInteger(value);
|
|
278
299
|
if (typeof value === "string") return value.trim().length > 0;
|
package/src/gateway/context.ts
CHANGED
|
@@ -72,10 +72,18 @@ export function renderGatewayEventPrompt(event: GatewayEvent): string {
|
|
|
72
72
|
if (event.text.trim().length === 0) {
|
|
73
73
|
throw new Error("Gateway event text cannot be empty");
|
|
74
74
|
}
|
|
75
|
-
const label = event.kind
|
|
75
|
+
const label = getGatewayPromptLabel(event.kind);
|
|
76
76
|
return `${renderGatewayEventContext(event)}\n\n${label}:\n${event.text}`;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function getGatewayPromptLabel(kind: GatewayEvent["kind"]): string {
|
|
80
|
+
if (kind === "callback_query") return "Gateway user action";
|
|
81
|
+
if (kind === "edited_message") return "Gateway edited user message";
|
|
82
|
+
if (kind === "channel_post") return "Gateway channel post";
|
|
83
|
+
if (kind === "edited_channel_post") return "Gateway edited channel post";
|
|
84
|
+
return "Gateway user message";
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
function addInteraction(
|
|
80
88
|
payload: GatewayEventContextPayload,
|
|
81
89
|
interaction: GatewayEvent["interaction"] | undefined,
|
package/src/gateway/types.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type GatewayChatType = "direct" | "group" | "supergroup" | "channel";
|
|
|
4
4
|
|
|
5
5
|
export type GatewayParticipantScope = "shared" | "per-user";
|
|
6
6
|
|
|
7
|
-
export type GatewayEventKind = "message" | "callback_query";
|
|
7
|
+
export type GatewayEventKind = "message" | "edited_message" | "channel_post" | "edited_channel_post" | "callback_query";
|
|
8
8
|
|
|
9
9
|
export interface GatewayCallbackInteraction {
|
|
10
10
|
type: "callback_query";
|
|
@@ -42,9 +42,9 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
|
42
42
|
"notebook-tool-runtime.md": "# Notebook file runtime internals\n\nThis document describes current `.ipynb` handling in `coding-agent` and its relationship to the kernel-backed Python runtime.\n\nThe critical distinction: **notebook support is file conversion/editing, not notebook execution**. `.ipynb` files are exposed as editable cell-marked text through `read` and the edit pipeline; no notebook-specific tool starts or talks to a Python kernel.\n\n## Implementation files\n\n- [`src/edit/notebook.ts`](../packages/coding-agent/src/edit/notebook.ts)\n- [`src/edit/read-file.ts`](../packages/coding-agent/src/edit/read-file.ts)\n- [`src/tools/read.ts`](../packages/coding-agent/src/tools/read.ts)\n- [`src/tools/eval.ts`](../packages/coding-agent/src/tools/eval.ts)\n- [`src/eval/py/executor.ts`](../packages/coding-agent/src/eval/py/executor.ts)\n- [`src/eval/py/kernel.ts`](../packages/coding-agent/src/eval/py/kernel.ts)\n- [`src/session/streaming-output.ts`](../packages/coding-agent/src/session/streaming-output.ts)\n\n## 1) Runtime boundary: editing vs executing\n\n## `.ipynb` file conversion (`src/edit/notebook.ts`)\n\n- `read` treats `.ipynb` files as notebooks unless the selector is `:raw`.\n- The default notebook view is editable text with markers:\n - `# %% [code] cell:N`\n - `# %% [markdown] cell:N`\n - `# %% [raw] cell:N`\n- Line selectors and multi-range selectors operate on that virtual text.\n- Edit/write paths round-trip virtual text back to notebook JSON through `serializeEditedNotebookText(...)`.\n- Existing notebook metadata is preserved when a marker references an existing `cell:N`; new cells get fresh empty metadata.\n- Missing notebooks edited through this path start from an empty nbformat 4.5 notebook.\n\nNo kernel lifecycle exists in this path:\n\n- no kernel session ID\n- no code execution\n- no stream chunks from Python\n- no rich display capture\n- no output artifact pipeline from execution\n\n## Kernel-backed execution path (`src/tools/eval.ts` + `src/eval/py/*`)\n\nWhen the agent needs to run cell-style Python code (sequential cells, persistent state, rich displays), that goes through the **`eval` tool** with per-cell `language: \"py\"`, not through notebook file handling.\n\nThat path is where Python subprocess lifecycle, reset/cancel behavior, chunk streaming, rich displays, and output artifact truncation live.\n\n## 2) Notebook cell handling semantics\n\n## Source normalization\n\nNotebook JSON `source` is converted to virtual text by joining source arrays. When virtual text is serialized back, cell source is split with newline preservation:\n\n- each line ending in `\\n` stays as a separate source entry with the newline\n- a final non-newline-terminated line is stored without forcing a trailing newline\n- empty content becomes an empty `source` array\n\nThis mirrors notebook JSON conventions and avoids accidental line concatenation on later edits.\n\n## Marker parsing and cell preservation\n\n- The first representation line must be a marker; text before the first marker, including a blank line, is rejected.\n- Markers must match `# %% [code|markdown|raw]` with optional `cell:N`.\n- If `cell:N` points at an unused existing cell, that cell is cloned, its `cell_type` and `source` are updated, and unrelated metadata is preserved.\n- If no valid unused original index is present, a new cell is created.\n- Code cells ensure `execution_count` exists and `outputs` exists.\n- Markdown/raw cells remove `execution_count` and `outputs`.\n\n## Error surfaces\n\nHard failures are thrown for:\n\n- missing notebook on read\n- invalid JSON\n- missing/non-array `cells`\n- invalid cell objects or cell types\n- invalid editable representation (for example, text before the first cell marker)\n\nThese surface through the caller (`read`, edit, or `write`) as normal tool errors.\n\n## 3) Kernel session semantics (where they actually exist)\n\nKernel semantics are implemented in `executePython` / `PythonKernel` and apply to the Python backend of the `eval` tool.\n\n## Modes\n\n`PythonKernelMode`:\n\n- `session` (default)\n - kernels are cached by `(session id, cwd)`\n - multiple owners can share a retained kernel for the same key\n - execution is serialized by the tool's exclusive concurrency and backend execution path\n - dead kernels are replaced before execution\n- `per-call`\n - creates a subprocess for the request\n - executes\n - always shuts down the subprocess in `finally`\n\n## Reset behavior\n\nEach eval cell has its own optional `reset` flag. `reset: true` resets the selected Python session before that cell executes; it is not a top-level tool parameter.\n\n## Kernel death / restart / retry\n\nIn session mode:\n\n- if the retained subprocess is not alive before execution, it is replaced\n- if execution fails because the subprocess died, the kernel is replaced and the code is retried once\n- explicit `reset` is rejected while another reset for the same session key is already in progress\n\n## 4) Environment/session variable injection\n\nKernel startup and per-execution environment patching can receive:\n\n- `PROMETHEUS_SESSION_FILE`\n- `PROMETHEUS_ARTIFACTS_DIR`\n- `PROMETHEUS_TOOL_BRIDGE_URL`\n- `PROMETHEUS_TOOL_BRIDGE_TOKEN`\n- `PROMETHEUS_TOOL_BRIDGE_SESSION`\n\nThe runner initializes process state so code executes in the requested cwd, managed env entries are reflected in `os.environ`, and cwd is available on `sys.path`.\n\n## 5) Streaming/chunk and display handling (kernel-backed path)\n\nThe Python backend uses an NDJSON subprocess runner. The host processes frames per execution:\n\n- `stdout` / `stderr` -> text chunks to `onChunk`\n- `display` / `result` -> MIME bundle rendering\n- `error` -> traceback text and structured error metadata\n- `done` -> final status, execution count, cancellation state\n\nDisplay text MIME precedence:\n\n1. `text/markdown`\n2. `text/plain`\n3. converted `text/html`\n\nStructured outputs captured separately include:\n\n- `application/json` -> JSON display output\n- `image/png` / `image/jpeg` -> image output\n- `application/x-prometheus-status` -> status event\n\nCancellation/timeout:\n\n- abort/timeout sends `SIGINT` to the runner\n- if the runner does not settle after the interrupt grace window, shutdown escalates and the kernel is recreated on the next call\n- timeout output is annotated with a timeout message\n\n## 6) Truncation and artifact behavior\n\n`OutputSink` in `src/session/streaming-output.ts` is used by kernel execution paths:\n\n- sanitizes every chunk\n- tracks total/output lines and bytes\n- optionally spills full output to an artifact file\n- keeps a UTF-8-safe in-memory tail buffer when output exceeds the configured threshold\n\n`eval` converts this metadata into result truncation notices and TUI warnings.\n\nNotebook file conversion does **not** use `OutputSink`; it has no stream/artifact truncation pipeline because it does not execute code.\n\n## 7) Renderer assumptions and formatting\n\n## Read/edit notebook representation\n\nNotebook files are rendered to the model as text. The visible cell markers are part of the editable representation, not comments that are ignored during serialization.\n\n## Python renderer (for actual execution output)\n\nKernel-backed execution rendering expects:\n\n- per-cell status transitions (`pending` / `running` / `complete` / `error`)\n- optional structured status events\n- optional JSON output trees\n- image outputs\n- truncation warnings + optional `artifact://<id>` pointer\n\nThis renderer behavior is unrelated to notebook JSON editing except that both reuse shared TUI primitives.\n\n## 8) Practical workflow\n\nIf a workflow needs both notebook mutation and execution:\n\n1. read or edit the `.ipynb` file through the normal file tools\n2. copy the desired cell source into `eval` cells with `language: \"py\"` to execute it\n3. write resulting source changes back to the notebook if needed\n\nCurrent implementation does not provide a single tool that both mutates `.ipynb` and executes notebook cells through kernel context.\n",
|
|
43
43
|
"plugin-manager-installer-plumbing.md": "# Plugin manager and installer plumbing\n\nThis document describes how `prometheus plugin` npm/link operations mutate plugin state on disk and how installed npm/link plugins become runtime capabilities (tools and extensions today, hooks/commands path resolution available). Marketplace installs use separate marketplace registries and cache plumbing; see `docs/marketplace.md`.\n\n## Scope and architecture\n\nThere are two plugin-management implementations in the codebase:\n\n1. **Active path used by CLI commands**: `PluginManager` (`src/extensibility/plugins/manager.ts`)\n2. **Legacy helper module**: installer functions (`src/extensibility/plugins/installer.ts`)\n\n`prometheus plugin` npm/link actions go through `PluginManager`; marketplace actions go through `MarketplaceManager`.\n\n`installer.ts` still documents important safety checks and filesystem behavior, but it is not the path used by `src/commands/plugin.ts` + `src/cli/plugin-cli.ts`.\n\n## Lifecycle: from CLI invocation to runtime availability\n\n```text\nprometheus plugin <npm/link action> ...\n -> src/commands/plugin.ts\n -> runPluginCommand(...) in src/cli/plugin-cli.ts\n -> PluginManager method (install/list/uninstall/link/...)\n -> mutate ~/.prometheus/plugins/{package.json,node_modules,prometheus-plugins.lock.json}\n -> runtime discovery: discoverAndLoadCustomTools(...) and discoverAndLoadExtensions(...)\n -> getAllPluginToolPaths(cwd) / getAllPluginExtensionPaths(cwd)\n -> custom tool loader imports tool modules; extension loader imports extension modules\n\nprometheus plugin install name@marketplace / prometheus install name@marketplace\n -> MarketplaceManager\n -> mutate ~/.prometheus/marketplaces.json, ~/.prometheus/plugins/installed_plugins.json, cache dirs\n -> installed marketplace plugin cache is surfaced as plugin roots/capabilities\n```\n\n### Command entrypoints\n\n- `src/commands/plugin.ts` defines command/flags and forwards to `runPluginCommand`.\n- `src/cli/plugin-cli.ts` maps npm/link subcommands to `PluginManager` methods:\n - `install`, `uninstall`, `list`, `link`, `doctor`, `features`, `config`, `enable`, `disable`\n- `discover`, `upgrade`, and `marketplace ...` subcommands use `MarketplaceManager`.\n- No explicit npm-plugin `update` action exists; update is done by re-running `install` with a new package/version spec.\n\n## On-disk model\n\nGlobal plugin state lives under `~/.prometheus/plugins`:\n\n- `package.json` — dependency manifest used by `bun install`/`bun uninstall` for npm-installed plugins\n- `node_modules/` — installed npm plugin packages or symlinks\n- `prometheus-plugins.lock.json` — runtime state for npm/link plugins:\n - enabled/disabled per plugin\n - selected feature set per plugin\n - persisted plugin settings\n\nProject-local overrides live at:\n\n- `<cwd>/.prometheus/plugin-overrides.json`\n\nOverrides are read-only from manager/loader perspective (no write path here) and can disable plugins or override features/settings for this project.\n\nMarketplace registries live separately:\n\n- `~/.prometheus/marketplaces.json` — configured marketplace catalogs\n- `~/.prometheus/plugins/installed_plugins.json` — user-scoped marketplace installs\n- `<cwd>/.prometheus/plugins/installed_plugins.json` — project-scoped marketplace installs when available\n- `~/.prometheus/plugins/cache/{marketplaces,plugins}/` — cached catalogs and plugin directories\n\n## Plugin spec parsing and metadata interpretation\n\n## Install spec grammar\n\n`parsePluginSpec` (`parser.ts`) supports:\n\n- `pkg` -> `features: null` (defaults behavior)\n- `pkg[*]` -> enable all manifest features\n- `pkg[]` -> enable no optional features\n- `pkg[a,b]` -> enable named features\n- `@scope/pkg@1.2.3[feat]` -> scoped + versioned package with explicit feature selection\n\n`extractPackageName` strips version suffix for on-disk path lookup after install.\n\n## Manifest source and required fields\n\nManifest is resolved from package fields as:\n\n1. `prometheus`\n2. legacy fallback `pi`\n3. fallback `{ version: package.version }`\n\nImplications:\n\n- There is no strict schema validation in manager/loader.\n- A package missing `prometheus` or legacy `pi` is still installable and listable.\n- Runtime plugin loading (`getEnabledPlugins`) skips packages without a `prometheus` or legacy `pi` manifest.\n- `manifest.version` is always overwritten from package `version`.\n\nMalformed `package.json` JSON is a hard failure at read time; malformed manifest shape may fail later only when specific fields are consumed.\n\n## Install/update flow (`PluginManager.install`)\n\n1. Parse feature bracket syntax from install spec.\n2. Validate package name against regex + shell-metacharacter denylist.\n3. Ensure plugin `package.json` exists (`prometheus-plugins`, private dependencies map).\n4. Run `bun install <packageSpec>` in `~/.prometheus/plugins`.\n5. Read installed package `node_modules/<name>/package.json`.\n6. Resolve manifest and compute `enabledFeatures`:\n - `[*]`: all declared features (or `null` if no feature map)\n - `[a,b]`: validates each feature exists in manifest features map\n - `[]`: empty feature list\n - bare spec: `null` (use defaults policy later in loader)\n7. Upsert lockfile runtime state: `{ version, enabledFeatures, enabled: true }`.\n\n### Update semantics\n\nBecause update is install-driven:\n\n- `prometheus plugin install pkg@newVersion` updates dependency and lockfile version.\n- Existing settings are preserved; state entry is overwritten for version/features/enabled.\n- No separate “check updates” or transactional migration logic exists.\n\n## Remove flow (`PluginManager.uninstall`)\n\n1. Validate package name.\n2. Run `bun uninstall <name>` in plugin dir.\n3. Remove plugin runtime state from lockfile:\n - `config.plugins[name]`\n - `config.settings[name]`\n\nIf uninstall command fails, runtime state is not changed.\n\n## List flow (`PluginManager.list`)\n\n1. Read plugin dependency map from `~/.prometheus/plugins/package.json`.\n2. Load lockfile runtime config (missing file -> empty defaults).\n3. Load project overrides (`<cwd>/.prometheus/plugin-overrides.json`, parse/read errors -> empty object with warning).\n4. For each dependency with a resolvable package.json:\n - build `InstalledPlugin` record\n - merge feature/enable state:\n - base from lockfile (or defaults)\n - project overrides can replace feature selection\n - project `disabled` list masks plugin as disabled\n\nThis is the effective state used by CLI status output and settings/features operations.\n\n## Link flow (`PluginManager.link`)\n\n`link` supports local plugin development by symlinking a local package into `~/.prometheus/plugins/node_modules/<pkg.name>`.\n\nBehavior:\n\n1. Resolve `localPath` against manager cwd.\n2. Require local `package.json` and `name` field.\n3. Ensure plugin dirs exist.\n4. For scoped names, create scope directory.\n5. Remove existing path at target link location.\n6. Create symlink.\n7. Add runtime lockfile entry enabled with default features (`null`).\n\nCaveat: current `PluginManager.link` does not enforce the `cwd` path-boundary check present in legacy `installer.ts` (`normalizedPath.startsWith(normalizedCwd)`), so trust is the caller’s responsibility.\n\n## Runtime loading: from installed plugin to callable capabilities\n\n## Discovery gate\n\n`getEnabledPlugins(cwd)` (`plugins/loader.ts`) reads:\n\n- plugin dependency manifest (`package.json`)\n- lockfile runtime state\n- project overrides via `getConfigDirPaths(\"plugin-overrides.json\", { user: false, cwd })`\n\nFiltering:\n\n- skip if no plugin package.json\n- skip if manifest (`prometheus`/`pi`) absent\n- skip if globally disabled in lockfile\n- skip if project-disabled\n\n## Capability path resolution\n\nFor each enabled plugin:\n\n- `resolvePluginExtensionPaths(plugin)`\n- `resolvePluginToolPaths(plugin)`\n- `resolvePluginHookPaths(plugin)`\n- `resolvePluginCommandPaths(plugin)`\n\nEach resolver includes base entries plus feature entries:\n\n- base entries are always included\n- explicit feature list -> only selected features\n- `enabledFeatures === null` -> enable features marked `default: true`\n\nManifest entries may point to a file or to a directory containing `index.ts`, `index.js`, `index.mjs`, or `index.cjs`. Missing files are silently skipped (`existsSync` guard).\n\n## Current runtime wiring differences\n\n- **Tools are wired into runtime today** via `discoverAndLoadCustomTools` (`custom-tools/loader.ts`), which calls `getAllPluginToolPaths(cwd)`.\n- **Extensions are wired into runtime today** via `discoverAndLoadExtensions` (`extensions/loader.ts`), which calls `getAllPluginExtensionPaths(cwd)`.\n- Paths are de-duplicated by resolved absolute path in custom tool and extension discovery (`seen` set, first path wins).\n- **Hooks/commands resolvers exist** and are exported, but this code path does not currently wire them into a runtime registry in the same way tools and extensions are wired.\n\n## Lock/state management details\n\n`PluginManager` caches runtime config in memory per instance (`#runtimeConfig`) and lazily loads once.\n\nLoad behavior:\n\n- lockfile missing -> `{ plugins: {}, settings: {} }`\n- lockfile read/parse failure -> warning + same empty defaults\n\nSave behavior:\n\n- writes full lockfile JSON pretty-printed each mutation\n\nNo cross-process locking or merge strategy exists; concurrent writers can overwrite each other.\n\n## Safety checks and trust boundaries\n\n## Input/package validation\n\nActive manager path enforces package-name validation:\n\n- regex for scoped/unscoped package specs (optionally with version)\n- explicit shell metacharacter denylist (`[;&|`$(){}[]<>\\\\]`)\n\nThis limits command-injection risk when invoking `bun install/uninstall`.\n\n## Filesystem trust boundary\n\n- Plugin code executes in-process when custom tool modules are imported; no sandboxing.\n- Manifest relative paths are joined against plugin package directory and only existence-checked.\n- The plugin package itself is trusted code once installed.\n\n## Legacy installer-only checks\n\n`installer.ts` includes additional link-time checks not mirrored in `PluginManager.link`:\n\n- local path must resolve inside project cwd\n- extra package name/path traversal guards for symlink target naming\n\nBecause CLI uses `PluginManager`, these stricter link guards are not currently on the main path.\n\n## Failure, partial success, and rollback behavior\n\nThe plugin manager is not transactional.\n\n| Operation stage | Failure behavior | Rollback |\n| -------------------------------------------------------- | -------------------------- | ----------------------------------------------------------------------------- |\n| `bun install` fails | install aborts with stderr | N/A (no state writes yet) |\n| Install succeeds, then manifest/feature validation fails | command fails | No uninstall rollback; dependency may remain in `node_modules`/`package.json` |\n| Install succeeds, then lockfile write fails | command fails | No rollback of installed package |\n| `bun uninstall` succeeds, lockfile write fails | command fails | Package removed, stale runtime state may remain |\n| `link` removes old target then symlink creation fails | command fails | No restoration of previous link/dir |\n\nOperationally, `doctor --fix` can repair some drift (`bun install`, orphaned config cleanup, invalid-feature cleanup), but it is best-effort.\n\n## Malformed/missing manifest behavior summary\n\n- Missing `prometheus`/`pi` field:\n - install/list: tolerated (minimal manifest)\n - runtime enabled-plugin discovery: skipped as non-plugin\n- Missing feature referenced by install spec or `features --set/--enable`: hard error with available feature list\n- Invalid `plugin-overrides.json`: ignored with fallback to `{}` in both manager and loader paths\n- Missing tool/hook/command file paths referenced by manifest: silently ignored during resolver expansion; flagged as errors only by `doctor`\n\n## Mode differences and precedence\n\n- `--dry-run` (install): returns synthetic install result, no filesystem/network/state writes.\n- `--json`: output formatting only, no behavior change.\n- Project overrides always take precedence over global lockfile for feature/settings view.\n- Effective enablement is `runtimeEnabled && !projectDisabled`.\n\n## Implementation files\n\n- [`src/commands/plugin.ts`](../packages/coding-agent/src/commands/plugin.ts) — CLI command declaration and flag mapping\n- [`src/cli/plugin-cli.ts`](../packages/coding-agent/src/cli/plugin-cli.ts) — action dispatch, user-facing command handlers\n- [`src/extensibility/plugins/manager.ts`](../packages/coding-agent/src/extensibility/plugins/manager.ts) — active install/remove/list/link/state/doctor implementation\n- [`src/extensibility/plugins/installer.ts`](../packages/coding-agent/src/extensibility/plugins/installer.ts) — legacy installer helpers and additional link safety checks\n- [`src/extensibility/plugins/loader.ts`](../packages/coding-agent/src/extensibility/plugins/loader.ts) — enabled-plugin discovery and tool/hook/command path resolution\n- [`src/extensibility/plugins/parser.ts`](../packages/coding-agent/src/extensibility/plugins/parser.ts) — install spec and package-name parsing helpers\n- [`src/extensibility/plugins/types.ts`](../packages/coding-agent/src/extensibility/plugins/types.ts) — manifest/runtime/override type contracts\n- [`src/extensibility/custom-tools/loader.ts`](../packages/coding-agent/src/extensibility/custom-tools/loader.ts) — runtime wiring for plugin-provided tool modules\n- [`src/extensibility/extensions/loader.ts`](../packages/coding-agent/src/extensibility/extensions/loader.ts) — runtime wiring for plugin-provided extension modules\n",
|
|
44
44
|
"porting-to-natives.md": "# Porting to prometheus-natives (N-API) — Field Notes\n\nThis is a practical guide for moving hot paths into `crates/prometheus-natives` and wiring them through the generated native package entrypoint. It exists to avoid the same failures happening twice.\n\n## When to port\n\nPort when any of these are true:\n\n- The hot path runs in render loops, tight UI updates, or large batches.\n- JS allocations dominate (string churn, regex backtracking, large arrays).\n- You already have a JS baseline and can benchmark both versions side by side.\n- The work is CPU-bound or blocking I/O that can run on the libuv thread pool.\n- The work is async I/O that can run on Tokio's runtime (for example shell execution).\n\nAvoid ports that depend on JS-only state or dynamic imports. N-API exports should be data-in/data-out. Long-running work should go through `task::blocking` (CPU-bound/blocking I/O) or `task::future` (async I/O) with cancellation where the caller needs `timeoutMs` or `AbortSignal`.\n\n## Current package shape\n\n`@prometheus-ai/natives` no longer has a `packages/natives/src/<module>` TypeScript wrapper layer. The package root points at generated native artifacts:\n\n- runtime entry/export wrapper: `packages/natives/native/index.js`\n- types entry: `packages/natives/native/index.d.ts`\n- loader helpers: `packages/natives/native/loader-state.js`\n- embedded manifest: `packages/natives/native/embedded-addon.js`\n\nConsumers import directly from `@prometheus-ai/natives`. The generated declarations and explicit ESM exports are produced during `bun --cwd=packages/natives run build`.\n\n## Anatomy of a native export\n\n**Rust side:**\n\n- Implementation lives in `crates/prometheus-natives/src/<module>.rs`.\n- If you add a new module, register it in `crates/prometheus-natives/src/lib.rs`.\n- Export with `#[napi]`; snake_case exports are converted to camelCase automatically. Use explicit JS names only for true aliases/non-default names. Use `#[napi(object)]` for object-shaped structs.\n- For CPU-bound or blocking work, use `task::blocking(tag, cancel_token, work)`.\n- For async work that needs Tokio, use `task::future(env, tag, work)`.\n- Pass a `CancelToken` when the API exposes `timeoutMs` or `AbortSignal`, and call `heartbeat()` inside long loops.\n\n**Package/build side:**\n\n- `packages/natives/scripts/build-native.ts` runs napi-rs, installs the `.node` artifact, copies generated `index.d.ts`, and regenerates explicit ESM class/function exports plus enum runtime exports in the checked-in `native/index.js`.\n- `packages/natives/native/index.js` is the ESM entrypoint that calls the loader, exposes named exports, and rejects install/compiled `.node` files that do not expose the package-version sentinel.\n- `packages/natives/package.json` exposes only the package root (`@prometheus-ai/natives`) as the import surface. At publish time the binaries are split out: the core ships the loader only (no `.node`), and each platform's `.node` is published as an optional-dependency leaf package `@prometheus-ai/natives-<tag>` (`scripts/ci-release-publish.ts` + `packages/natives/scripts/gen-npm-packages.ts`). This is transparent to importers — you still `import` from `@prometheus-ai/natives`.\n\n**Consumer side:**\n\n- Update direct imports/callsites in `packages/coding-agent` or `packages/tui` when the new export replaces a JS implementation.\n- Keep higher-level policy in consumers unless it belongs in the native primitive itself.\n\n## Porting checklist\n\n1. **Add the Rust implementation**\n\n- Put the core logic in a plain Rust function.\n- If it is a new module, add it to `crates/prometheus-natives/src/lib.rs`.\n- Expose it with `#[napi]` so the default snake_case -> camelCase mapping stays consistent.\n- Keep signatures owned and simple: `String`, `Vec<String>`, `Uint8Array`, `Either<JsString, Uint8Array>`, or `#[napi(object)]` structs.\n- For CPU-bound or blocking work, use `task::blocking`; for async work, use `task::future`.\n- If exposing cancellation, include `timeout_ms: Option<u32>` and `signal: Option<Unknown<'env>>` in options, create `CancelToken::new(...)`, and heartbeat in long loops.\n\n2. **Build generated bindings**\n\n- Run `bun --cwd=packages/natives run build`.\n- Confirm the generated `packages/natives/native/index.d.ts` includes the new export with the intended JS name/signature.\n- Confirm `packages/natives/native/index.js` has generated explicit ESM exports for the new class/function and enum objects when enum changes are involved.\n\n3. **Update consumers**\n\n- Import the new export directly from `@prometheus-ai/natives`.\n- Replace only callsites where the native implementation is faster/equivalent and preserves behavior.\n- Remove obsolete JS implementation code in the same change when the native path becomes canonical.\n\n4. **Add benchmarks**\n\n- Put benchmarks next to the owning package (`packages/tui/bench`, `packages/natives/bench`, or `packages/coding-agent/bench`).\n- Include a JS baseline and native version in the same run.\n- Use `Bun.nanoseconds()` and a fixed iteration count.\n- Keep benchmark inputs realistic for the hot path.\n\n5. **Run focused verification**\n\n- Build the native package.\n- Run the benchmark.\n- Run the narrow tests or scenario covering the changed export/callsites.\n\n## Pain points and how to avoid them\n\n### 1) Stale platform/variant artifacts\n\nThe loader probes platform-tagged artifacts in deterministic order. For x64, selected variant candidates are tried before the unsuffixed default fallback:\n\n- `modern`: `prometheus_natives.<tag>-modern.node`, then `...-baseline.node`, then `prometheus_natives.<tag>.node`.\n- `baseline`: `prometheus_natives.<tag>-baseline.node`, then `prometheus_natives.<tag>.node`.\n\nNon-x64 uses `prometheus_natives.<tag>.node`.\n\nCompiled binaries also probe `<getNativesDir()>/<version>/...` and a legacy user-data directory before package/executable locations. Windows `node_modules` installs stage leaf/core addons into the same versioned directory before probing. If any earlier candidate is stale, a new export may appear missing unless the version sentinel rejects it first.\n\n**Fix:** remove stale candidate/cache files and rebuild.\n\n```bash\nrm packages/natives/native/prometheus_natives.<platform>-<arch>.node\nrm packages/natives/native/prometheus_natives.<platform>-<arch>-modern.node\nrm packages/natives/native/prometheus_natives.<platform>-<arch>-baseline.node\nbun --cwd=packages/natives run build\n```\n\nFor compiled binaries or Windows staging, delete the versioned addon cache shown in the loader error (normally under `~/.prometheus/natives/<version>` unless `$XDG_DATA_HOME/prometheus` is used).\n\n### 2) Generated types do not match loaded binary\n\nThis can happen when `native/index.d.ts` was regenerated but the `.node` file being loaded is stale, same-version incomplete, or from a different platform/variant. Different-version install/compiled binaries should be rejected by the version sentinel during loading.\n\nVerify the loaded export set from the actual candidate path reported by the loader:\n\n```bash\nbun -e 'import { createRequire } from \"node:module\"; const require = createRequire(import.meta.url); const mod = require(process.argv[2]); console.log(Object.keys(mod).sort())' -- /path/from/loader/error/prometheus_natives.<tag>[-variant].node\n```\n\nFix the build/candidate mismatch. Do not paper over it with optional consumer checks if the export is required.\n\n### 3) Rust signature mismatch\n\nKeep N-API signatures simple and owned. Avoid borrowed references like `&str` in public exports. If you need structured data, use `#[napi(object)]` structs. If you need callbacks, use napi-rs `ThreadsafeFunction` and keep callback error/value behavior explicit.\n\n### 4) Enum runtime exports and ESM named exports\n\nnapi-rs declarations alone are not enough for JS callers that import named symbols or use enum objects at runtime. `scripts/gen-enums.ts` reads `native/index.d.ts`, writes explicit `export const ... = nativeBindings...` entries for public classes/functions, and emits enum objects in `native/index.js`. If you add or change a native export, verify both `native/index.d.ts` and the generated export block in `native/index.js`.\n\n### 5) Benchmarking mistakes\n\n- Do not compare different inputs or allocations.\n- Keep JS and native using identical input arrays.\n- Run both in the same benchmark file to avoid skew.\n- Include enough iterations to smooth startup noise, but keep inputs realistic.\n\n## Benchmark template\n\n```ts\nconst ITERATIONS = 2000;\n\nfunction bench(name: string, fn: () => void): number {\n const start = Bun.nanoseconds();\n for (let i = 0; i < ITERATIONS; i++) fn();\n const elapsed = (Bun.nanoseconds() - start) / 1e6;\n console.log(\n `${name}: ${elapsed.toFixed(2)}ms total (${(elapsed / ITERATIONS).toFixed(6)}ms/op)`,\n );\n return elapsed;\n}\n\nbench(\"feature/js\", () => {\n jsImpl(sample);\n});\n\nbench(\"feature/native\", () => {\n nativeImpl(sample);\n});\n```\n\n## Verification checklist\n\n- Generated `native/index.d.ts` includes the new export and intended TS signature.\n- `native/index.js` includes the generated named export; enum objects are present when the change adds/changes enums.\n- The loaded `.node` file's `Object.keys(require(candidate))` includes the new export and the package-version sentinel.\n- Bench numbers are recorded in the PR/notes.\n- Call sites are updated only if native is faster/equal and behavior-compatible.\n- Obsolete JS code is removed when the native implementation becomes canonical.\n\n## Rule of thumb\n\n- If native is slower, do not switch callsites. Keep or remove the export based on whether it has a near-term owner.\n- If native is faster and behavior-compatible, switch callsites and keep a benchmark to catch regressions.\n",
|
|
45
|
-
"prometheus-gateway-telegram-architecture.md": "# Prometheus Gateway And Telegram Architecture\n\nThis document defines the Prometheus-native gateway shape for Telegram and later\ncommunication surfaces. It is an architecture contract for the next\nimplementation slice, not a complete runtime spec.\n\n## Decision\n\nBuild a small platform-neutral gateway layer inside Prometheus, then place Telegram\nbehind that layer.\n\nDo not port Hermes gateway code. Hermes is useful as a reference for product\nbehavior: long-running messaging access, per-chat sessions, topic-aware routing,\nbackground delivery, and safe maintenance commands. In Prometheus those behaviors\nshould be implemented through existing Prometheus primitives:\n\n- `AgentSession` for turn execution.\n- `SessionManager` for persisted conversation state.\n- slash-command/custom-command handling for commands.\n- `CustomMessageEntry` for gateway context injected into a session.\n- memory URLs and the local memory backend for durable user/project memory.\n- the existing update CLI for `/update`.\n- Prometheus settings/config instead of Hermes YAML/environment conventions.\n\n## Confirmed Sources\n\n- Prometheus source and docs: this repository.\n- Prometheus local source reviewed in this checkout:\n - `packages/coding-agent/src/session/agent-session.ts`\n - `packages/coding-agent/src/session/session-manager.ts`\n - `packages/coding-agent/src/modes/print-mode.ts`\n - `packages/coding-agent/src/modes/rpc/`\n - `packages/coding-agent/src/cli/update-cli.ts`\n- Prometheus auth gateway design reference:\n - `docs/auth-broker-gateway.md`\n - `packages/ai/src/auth-gateway/server.ts`\n - `packages/coding-agent/src/cli/auth-gateway-cli.ts`\n- Telegram Bot API:\n - https://core.telegram.org/bots/api\n- Hermes docs:\n - https://hermes-agent.nousresearch.com/docs/user-guide/messaging\n - https://hermes-agent.nousresearch.com/docs/user-guide/features/memory/\n - https://hermes-agent.nousresearch.com/docs/user-guide/features/curator\n- Hermes source modules reviewed as reference only:\n - `gateway/session.py`\n - `gateway/run.py`\n - `gateway/platforms/base.py`\n - `gateway/platforms/telegram.py`\n - `cron/scheduler.py`\n\n## Source-Confirmed Constraints\n\nTelegram:\n\n- Webhook mode can validate `X-Telegram-Bot-Api-Secret-Token` against the\n `secret_token` supplied to `setWebhook`.\n- Polling mode must remove any active webhook before calling `getUpdates`,\n because Telegram does not allow long polling while a webhook is configured.\n- Telegram topic/forum routing uses `message_thread_id` on messages and send\n methods.\n- Telegram sends are bounded by Bot API message limits, so the connector must\n split or summarize long responses before sending.\n- Bot tokens and webhook secrets must be configured outside source control.\n\nHermes reference:\n\n- Hermes normalizes platform messages into a source/context object before\n routing to sessions.\n- Hermes includes `thread_id` in source, session key, sends, cron delivery, and\n diagnostics. The important lesson is that thread identity must survive the\n full path, not only inbound parsing.\n- Hermes runs gateway and cron as long-lived services, but Prometheus should not copy\n the Python process model.\n\nPrometheus source:\n\n- `AgentSession.prompt()` already routes normal user input, slash commands,\n prompt templates, streaming behavior, and session persistence.\n- `AgentSession.runEphemeralTurn()` provides a native side-channel turn that\n uses the current model, system prompt, and session snapshot without mutating\n persisted conversation history. This is the first gateway dispatch surface.\n- `SessionManager.create/open/continueRecent` can provide separate persisted\n sessions when given a deterministic session directory or file path.\n- `SessionManager.appendCustomMessage()` can persist gateway context that\n participates in LLM context without pretending to be a user message.\n- Prometheus RPC already exposes a process-isolated control surface for `prompt`,\n `steer`, `follow_up`, `new_session`, session state, messages, and handoff.\n- The auth gateway already demonstrates Prometheus's preferred HTTP-service style:\n small CLI wrapper, `Bun.serve`, bearer/secret checks, health route, and\n centralized request handlers.\n\n## Gateway Core\n\nThe gateway core should be independent from Telegram. It should define a small\ncontract that any adapter can target.\n\nCandidate files:\n\n- `packages/coding-agent/src/gateway/types.ts`\n- `packages/coding-agent/src/gateway/session-key.ts`\n- `packages/coding-agent/src/gateway/context.ts`\n- `packages/coding-agent/src/gateway/router.ts`\n- `packages/coding-agent/test/gateway/session-key.test.ts`\n- `packages/coding-agent/test/gateway/context.test.ts`\n- `packages/coding-agent/test/gateway/router.test.ts`\n\nCore types:\n\n```ts\nexport type GatewayPlatform = \"telegram\";\n\nexport type GatewayChatType =\n\t| \"direct\"\n\t| \"group\"\n\t| \"supergroup\"\n\t| \"channel\";\n\nexport interface GatewaySource {\n\tplatform: GatewayPlatform;\n\tchatId: string;\n\tchatType: GatewayChatType;\n\tuserId?: string;\n\tuserName?: string;\n\tmessageId?: string;\n\tthreadId?: string;\n\treplyToMessageId?: string;\n}\n\nexport interface GatewayEvent {\n\tsource: GatewaySource;\n\ttext: string;\n\treceivedAt: string;\n\traw?: unknown;\n}\n```\n\nThe first implementation should keep `raw` out of prompts and logs by default.\nIt is for tests and adapter debugging only.\n\n## Session Key Policy\n\nThe session key must be deterministic and explicit. It should never collapse a\ndirect chat, group chat, and topic into the same session.\n\nRecommended default:\n\n```text\ngateway:<platform>:direct:<chatId>\ngateway:<platform>:direct:<chatId>:thread:<threadId>\ngateway:<platform>:group:<chatId>\ngateway:<platform>:group:<chatId>:thread:<threadId>\n```\n\nOptional modes can be added later:\n\n```text\ngateway:<platform>:group:<chatId>:user:<userId>\ngateway:<platform>:group:<chatId>:thread:<threadId>:user:<userId>\n```\n\nPhase 4 should implement only the pure builder and tests. It should not call the\nTelegram network.\n\nRequired tests:\n\n- Telegram direct chat without a thread.\n- Telegram direct chat with a topic/thread.\n- Telegram group without a thread.\n- Telegram group with topic/thread.\n- Telegram group per-user mode.\n- Telegram topic per-user mode.\n- Empty/undefined thread ID does not create a topic session.\n- Direct and group sessions with the same numeric ID do not collide.\n\n## Gateway Context Injection\n\nEach gateway session should get a hidden custom message that tells the model\nwhere the current conversation came from.\n\nCandidate shape:\n\n```text\nYou are responding through the Prometheus gateway.\nPlatform: telegram\nChat type: group\nChat ID: <redacted or stable local id>\nThread/topic ID: <present only when applicable>\nUser ID: <present only when needed>\n\nPreserve the conversation boundary. If sending a reply, it must go back through\nthe same gateway source unless the user explicitly asks otherwise.\n```\n\nThe pure gateway core should first render this as structured context that can be\ntested without a live `AgentSession`. Production session integration should use\n`SessionManager.appendCustomMessage()` or `AgentSession.sendCustomMessage()`\nrather than mixing this into `AGENTS.md`, `USER.md`, `SOUL.md`, or memory\nartifacts.\n\nPII should be minimized. The stable session key may include raw platform IDs on\ndisk initially if that matches Prometheus session naming constraints, but prompt text\nshould prefer redacted or local display IDs unless raw IDs are needed for a\ntool.\n\n## Runner Shape\n\nThe pure core keeps the first runner testable:\n\n```text\nGatewayEvent\n -> buildGatewaySessionKey(source, options)\n -> renderGatewayEventPrompt(event)\n -> dispatch to an injected prompt/session target\n```\n\nThe first real service uses `AgentSession.runEphemeralTurn()` through a\nsession-keyed dispatch pool. This is intentionally Prometheus-native: the gateway does\nnot create a Hermes-like session loop, does not call TUI controllers, and does\nnot duplicate model/system prompt behavior.\n\nCurrent service flow:\n\n```text\nTelegram update\n -> Telegram adapter normalize()\n -> in-memory update_id dedupe in the long-lived handler\n -> fast webhook acknowledgement\n -> GatewayEvent\n -> gateway session key\n -> gateway command short-circuit when applicable (/status first)\n -> SessionManager.continueRecent() in the key's gateway session directory\n -> render gateway prompt\n -> AgentSession.runEphemeralTurn()\n -> chunked adapter delivery back to chat/thread with bounded retry_after backoff\n```\n\nA real service now exists as:\n\n```text\nprometheus gateway serve --telegram\n```\n\nIt follows the existing auth gateway style: small CLI wrapper, explicit\ntoken/secret setup, `Bun.serve` for HTTP mode, `/health`, and no secret\nlogging.\n\nFuture production hardening should stay Prometheus-native first: use the existing\nsession, slash-command, and async/background job paths unless source review\nproves a real gap. Richer command policy and possibly RPC process isolation can\nstill sit behind the same dispatch interface later. A separate gateway queue or\nscheduler should be added only when native Prometheus async delivery cannot meet a\nspecific production requirement.\n\n## Telegram Connector\n\nCandidate files:\n\n- `packages/coding-agent/src/gateway/adapters/telegram/types.ts`\n- `packages/coding-agent/src/gateway/adapters/telegram/normalize.ts`\n- `packages/coding-agent/src/gateway/adapters/telegram/send-message.ts`\n- `packages/coding-agent/src/gateway/adapters/telegram/client.ts`\n- `packages/coding-agent/src/gateway/adapters/telegram/webhook.ts`\n- `packages/coding-agent/test/gateway/telegram-normalize.test.ts`\n\nCapability map:\n\n- `docs/prometheus-telegram-capability-matrix.md`\n\nTelegram must be treated as a native interaction platform, not only a text\ntransport. The adapter should be able to grow into the full Bot API surface\nthrough typed capability modules while keeping the first webhook path small and\nsafe.\n\nInbound normalization:\n\n- Accept `message` in the first slice, and later `edited_message`,\n `channel_post`, `business_message`, `guest_message`, `inline_query`, or\n reaction/payment/admin updates only if we choose a clear policy.\n- Ignore bot-authored messages unless explicitly configured.\n- Extract `chat.id`, `chat.type`, `from.id`, `message_id`, text/caption, and\n `message_thread_id`.\n- Preserve `message_thread_id` only when it is present and finite.\n- Convert Telegram payloads into `GatewayEvent`; do not let Telegram-specific\n fields leak into gateway core except through `raw` in tests.\n- Keep room in the adapter route identity for channel direct-message topics,\n business connection IDs, guest query IDs, inline query IDs, callback query IDs,\n and inline message IDs before those capabilities are enabled.\n\nMedia handoff:\n\n- Normalize media metadata first. Telegram `photo`, `animation`/GIF, `video`,\n `document`, `live_photo`, `voice`, media groups, spoilers,\n caption-above-media, protected content, paid-post/do-not-delete metadata, and\n auto-delete timer service messages should be visible in `<gateway_event>`.\n- Download binaries only after the Telegram authorization gate. Blocked users\n must not trigger Telegram file downloads, session loads, tool work, or model\n calls.\n- Use Telegram `getFile` plus the official file download path for bytes. Do not\n derive download URLs directly from `file_id`.\n- Pass image-like media through Prometheus's existing `ImageContent` path. This\n includes photos, GIF/image animations, and image documents when the downloaded\n bytes are a supported image. Keep non-image videos and documents as metadata\n until a native file-reference handoff is implemented.\n- Do not expose Prometheus backend mechanics as identity. The gateway should\n make media feel natural to the user while remaining a thin bridge into native\n Prometheus capabilities.\n- Do not invent Telegram features. The Bot API does not expose a general\n view-once media send flag or a universal HD send flag. Preserve timer/protected\n metadata and map \"HD previews\" only to official `prefer_large_media` link\n preview behavior plus best-available inbound image selection.\n\nOutbound delivery:\n\n- Reply to the same `chat_id`.\n- Include `message_thread_id` when the source has a valid thread/topic ID.\n- Later include `direct_messages_topic_id`, `business_connection_id`, or\n `guest_query_id` only through the correct Bot API method for that surface.\n- Split long messages before sending.\n- Redact secrets before sending user-visible errors.\n- Keep a small Telegram client wrapper around `fetch` so tests can assert the\n Bot API method, payload, and topic fields without network calls.\n\nCapability policy:\n\n- Text, commands, callbacks, replies, topics, and webhook security are the v1\n hot path.\n- Media metadata and image-like media handoff are v1; broad non-image file\n processing, polls, reply keyboards, selectors, inline mode, deep links, and\n channel direct-message topics are v2 feature modules.\n- Mini Apps, payments, Stars, paid media, gifts, business-account actions,\n managed bots, chat-admin mutation, stories, and bot-to-bot workflows are\n disabled by default until an explicit product/security policy exists.\n- Any feature that spends money, mutates chat/account state, deletes data, or\n speaks as a business account requires explicit config and approval.\n\nSecurity:\n\n- Webhook mode must validate `X-Telegram-Bot-Api-Secret-Token`.\n- Token config must resolve from environment/config/secret storage, never from\n committed files.\n- Telegram user allowlist support lives in the gateway layer, before gateway\n commands, session lookup, prompt dispatch, memory loading, tools, async jobs,\n or model calls.\n- When `--telegram-allowed-users` or `PROMETHEUS_TELEGRAM_ALLOWED_USER_IDS` is set,\n default Telegram bot commands are cleared and Prometheus's native slash-command menu\n is registered only for authorized user scopes. Private chats get\n `MenuButtonCommands`; group/supergroup member scopes are registered lazily\n after an authorized user is seen in that chat.\n- With no allowlist configured, the gateway stays open for local/dev\n compatibility and registers the default native Prometheus command menu.\n\n## Commands\n\nGateway commands should enter Prometheus through a command router, not through\nspecial Telegram-only agent prompts. Native Prometheus slash commands remain the\nsource of truth for general command behavior and should be projected into\nTelegram rather than reimplemented in the gateway.\n\nInitial commands:\n\n- `/status`: report gateway health, current session scope, active session count,\n and Telegram delivery counters.\n- `/start`: return scope and status.\n- `/new`: create a new session for the current gateway session key.\n- `/memory`: route through native Prometheus command dispatch.\n\nLater command:\n\n- `/update`: spawn the existing `prometheus update` CLI in a controlled subprocess and\n stream status. Do not call update internals directly from the long-running\n gateway process. Add this only after gateway status, restart/lifecycle\n reporting, and pending-operation recovery are designed.\n\nThe command router should receive `GatewayEvent` and return a delivery action.\nOnly non-command text should go to `AgentSession.prompt()`.\n\n## Self-Improvement Boundary\n\nGateway work should not introduce autonomous mutation.\n\nAllowed early behavior:\n\n- trigger memory consolidation or show memory status\n- show proposed `USER.md` updates\n- show proposed `SOUL.md` patches\n- create dry-run self-improvement reports\n\nNot allowed in early gateway work:\n\n- automatic `SOUL.md` writes\n- automatic package update/restart without explicit command\n- deleting or rewriting user-authored skills\n- mutating skills without a report and opt-in command\n\n## Implementation Sequence\n\n1. Add gateway core types and session key tests. Done.\n2. Add gateway context rendering tests. Done.\n3. Add a test-only runner around a mocked `AgentSession` or minimal session\n facade. Done as an injected prompt dispatch facade.\n4. Add Telegram normalization fixtures and tests. Done for inbound `message`\n updates.\n5. Add Telegram client payload tests for topic and non-topic replies. Done for\n `sendMessage`.\n6. Add webhook-secret validation and `Update` variant router tests.\n7. Add callback-query acknowledgement and action routing tests.\n8. Add `prometheus gateway serve --telegram` only after the core and adapter contracts\n are stable.\n9. Add `/status` lifecycle plumbing before `/update`. Done for gateway health,\n current scope, active sessions, and Telegram delivery counters.\n10. Add bounded `retry_after` retry for Telegram sends. Done for in-process\n adapter delivery. Further reliability work should first reuse Prometheus's native\n async/background job delivery before adding gateway-specific queueing.\n11. Add front-door Telegram user allowlist plus scoped command/menu\n registration. Done with `--telegram-allowed-users` /\n `PROMETHEUS_TELEGRAM_ALLOWED_USER_IDS`, pre-session dispatch denial, empty default\n commands, authorized private command/menu scopes, and lazy group\n `chat_member` scopes.\n\n## Review Checklist\n\n- The gateway core imports no Telegram client code.\n- Telegram normalization does not call network APIs.\n- Session-key tests cover topic and non-topic collisions.\n- Production gateway execution goes through RPC unless source review proves RPC\n cannot support the required behavior.\n- Prompt context never includes raw Telegram update JSON.\n- Webhook secret validation is tested.\n- Gateway operational commands short-circuit before model dispatch.\n- Unauthorized Telegram users do not reach gateway commands, session lookup,\n prompt dispatch, memory loading, tools, async jobs, or model calls.\n- Telegram delivery honors `ResponseParameters.retry_after` when configured.\n- `/update` stays subprocess-based.\n- `SOUL.md` remains approval-gated.\n- No bot token, webhook secret, or Authorization header is committed.\n",
|
|
45
|
+
"prometheus-gateway-telegram-architecture.md": "# Prometheus Gateway And Telegram Architecture\n\nThis document defines the Prometheus-native gateway shape for Telegram and later\ncommunication surfaces. It is an architecture contract for the next\nimplementation slice, not a complete runtime spec.\n\n## Decision\n\nBuild a small platform-neutral gateway layer inside Prometheus, then place Telegram\nbehind that layer.\n\nDo not port Hermes gateway code. Hermes is useful as a reference for product\nbehavior: long-running messaging access, per-chat sessions, topic-aware routing,\nbackground delivery, and safe maintenance commands. In Prometheus those behaviors\nshould be implemented through existing Prometheus primitives:\n\n- `AgentSession` for turn execution.\n- `SessionManager` for persisted conversation state.\n- slash-command/custom-command handling for commands.\n- `CustomMessageEntry` for gateway context injected into a session.\n- memory URLs and the local memory backend for durable user/project memory.\n- the existing update CLI for `/update`.\n- Prometheus settings/config instead of Hermes YAML/environment conventions.\n\n## Confirmed Sources\n\n- Prometheus source and docs: this repository.\n- Prometheus local source reviewed in this checkout:\n - `packages/coding-agent/src/session/agent-session.ts`\n - `packages/coding-agent/src/session/session-manager.ts`\n - `packages/coding-agent/src/modes/print-mode.ts`\n - `packages/coding-agent/src/modes/rpc/`\n - `packages/coding-agent/src/cli/update-cli.ts`\n- Prometheus auth gateway design reference:\n - `docs/auth-broker-gateway.md`\n - `packages/ai/src/auth-gateway/server.ts`\n - `packages/coding-agent/src/cli/auth-gateway-cli.ts`\n- Telegram Bot API:\n - https://core.telegram.org/bots/api\n- Hermes docs:\n - https://hermes-agent.nousresearch.com/docs/user-guide/messaging\n - https://hermes-agent.nousresearch.com/docs/user-guide/features/memory/\n - https://hermes-agent.nousresearch.com/docs/user-guide/features/curator\n- Hermes source modules reviewed as reference only:\n - `gateway/session.py`\n - `gateway/run.py`\n - `gateway/platforms/base.py`\n - `gateway/platforms/telegram.py`\n - `cron/scheduler.py`\n\n## Source-Confirmed Constraints\n\nTelegram:\n\n- Webhook mode can validate `X-Telegram-Bot-Api-Secret-Token` against the\n `secret_token` supplied to `setWebhook`.\n- Polling mode must remove any active webhook before calling `getUpdates`,\n because Telegram does not allow long polling while a webhook is configured.\n- Telegram topic/forum routing uses `message_thread_id` on messages and send\n methods.\n- Telegram sends are bounded by Bot API message limits, so the connector must\n split or summarize long responses before sending.\n- Bot tokens and webhook secrets must be configured outside source control.\n\nHermes reference:\n\n- Hermes normalizes platform messages into a source/context object before\n routing to sessions.\n- Hermes includes `thread_id` in source, session key, sends, cron delivery, and\n diagnostics. The important lesson is that thread identity must survive the\n full path, not only inbound parsing.\n- Hermes runs gateway and cron as long-lived services, but Prometheus should not copy\n the Python process model.\n\nPrometheus source:\n\n- `AgentSession.prompt()` already routes normal user input, slash commands,\n prompt templates, streaming behavior, and session persistence.\n- `AgentSession.runEphemeralTurn()` provides a native side-channel turn that\n uses the current model, system prompt, and session snapshot without mutating\n persisted conversation history. This is the first gateway dispatch surface.\n- `SessionManager.create/open/continueRecent` can provide separate persisted\n sessions when given a deterministic session directory or file path.\n- `SessionManager.appendCustomMessage()` can persist gateway context that\n participates in LLM context without pretending to be a user message.\n- Prometheus RPC already exposes a process-isolated control surface for `prompt`,\n `steer`, `follow_up`, `new_session`, session state, messages, and handoff.\n- The auth gateway already demonstrates Prometheus's preferred HTTP-service style:\n small CLI wrapper, `Bun.serve`, bearer/secret checks, health route, and\n centralized request handlers.\n\n## Gateway Core\n\nThe gateway core should be independent from Telegram. It should define a small\ncontract that any adapter can target.\n\nCandidate files:\n\n- `packages/coding-agent/src/gateway/types.ts`\n- `packages/coding-agent/src/gateway/session-key.ts`\n- `packages/coding-agent/src/gateway/context.ts`\n- `packages/coding-agent/src/gateway/router.ts`\n- `packages/coding-agent/test/gateway/session-key.test.ts`\n- `packages/coding-agent/test/gateway/context.test.ts`\n- `packages/coding-agent/test/gateway/router.test.ts`\n\nCore types:\n\n```ts\nexport type GatewayPlatform = \"telegram\";\n\nexport type GatewayChatType =\n\t| \"direct\"\n\t| \"group\"\n\t| \"supergroup\"\n\t| \"channel\";\n\nexport interface GatewaySource {\n\tplatform: GatewayPlatform;\n\tchatId: string;\n\tchatType: GatewayChatType;\n\tuserId?: string;\n\tuserName?: string;\n\tmessageId?: string;\n\tthreadId?: string;\n\treplyToMessageId?: string;\n}\n\nexport interface GatewayEvent {\n\tsource: GatewaySource;\n\ttext: string;\n\treceivedAt: string;\n\traw?: unknown;\n}\n```\n\nThe first implementation should keep `raw` out of prompts and logs by default.\nIt is for tests and adapter debugging only.\n\n## Session Key Policy\n\nThe session key must be deterministic and explicit. It should never collapse a\ndirect chat, group chat, and topic into the same session.\n\nRecommended default:\n\n```text\ngateway:<platform>:direct:<chatId>\ngateway:<platform>:direct:<chatId>:thread:<threadId>\ngateway:<platform>:group:<chatId>\ngateway:<platform>:group:<chatId>:thread:<threadId>\n```\n\nOptional modes can be added later:\n\n```text\ngateway:<platform>:group:<chatId>:user:<userId>\ngateway:<platform>:group:<chatId>:thread:<threadId>:user:<userId>\n```\n\nPhase 4 should implement only the pure builder and tests. It should not call the\nTelegram network.\n\nRequired tests:\n\n- Telegram direct chat without a thread.\n- Telegram direct chat with a topic/thread.\n- Telegram group without a thread.\n- Telegram group with topic/thread.\n- Telegram group per-user mode.\n- Telegram topic per-user mode.\n- Empty/undefined thread ID does not create a topic session.\n- Direct and group sessions with the same numeric ID do not collide.\n\n## Gateway Context Injection\n\nEach gateway session should get a hidden custom message that tells the model\nwhere the current conversation came from.\n\nCandidate shape:\n\n```text\nYou are responding through the Prometheus gateway.\nPlatform: telegram\nChat type: group\nChat ID: <redacted or stable local id>\nThread/topic ID: <present only when applicable>\nUser ID: <present only when needed>\n\nPreserve the conversation boundary. If sending a reply, it must go back through\nthe same gateway source unless the user explicitly asks otherwise.\n```\n\nThe pure gateway core should first render this as structured context that can be\ntested without a live `AgentSession`. Production session integration should use\n`SessionManager.appendCustomMessage()` or `AgentSession.sendCustomMessage()`\nrather than mixing this into `AGENTS.md`, `USER.md`, `SOUL.md`, or memory\nartifacts.\n\nPII should be minimized. The stable session key may include raw platform IDs on\ndisk initially if that matches Prometheus session naming constraints, but prompt text\nshould prefer redacted or local display IDs unless raw IDs are needed for a\ntool.\n\n## Runner Shape\n\nThe pure core keeps the first runner testable:\n\n```text\nGatewayEvent\n -> buildGatewaySessionKey(source, options)\n -> renderGatewayEventPrompt(event)\n -> dispatch to an injected prompt/session target\n```\n\nThe first real service uses `AgentSession.runEphemeralTurn()` through a\nsession-keyed dispatch pool. This is intentionally Prometheus-native: the gateway does\nnot create a Hermes-like session loop, does not call TUI controllers, and does\nnot duplicate model/system prompt behavior.\n\nCurrent service flow:\n\n```text\nTelegram update\n -> Telegram adapter normalize()\n -> in-memory update_id dedupe in the long-lived handler\n -> fast webhook acknowledgement\n -> GatewayEvent\n -> gateway session key\n -> gateway command short-circuit when applicable (/status first)\n -> SessionManager.continueRecent() in the key's gateway session directory\n -> render gateway prompt\n -> AgentSession.runEphemeralTurn()\n -> chunked adapter delivery back to chat/thread with bounded retry_after backoff\n```\n\nA real service now exists as:\n\n```text\nprometheus gateway serve --telegram\n```\n\nIt follows the existing auth gateway style: small CLI wrapper, explicit\ntoken/secret setup, `Bun.serve` for HTTP mode, `/health`, and no secret\nlogging.\n\nFuture production hardening should stay Prometheus-native first: use the existing\nsession, slash-command, and async/background job paths unless source review\nproves a real gap. Richer command policy and possibly RPC process isolation can\nstill sit behind the same dispatch interface later. A separate gateway queue or\nscheduler should be added only when native Prometheus async delivery cannot meet a\nspecific production requirement.\n\n## Telegram Connector\n\nCandidate files:\n\n- `packages/coding-agent/src/gateway/adapters/telegram/types.ts`\n- `packages/coding-agent/src/gateway/adapters/telegram/normalize.ts`\n- `packages/coding-agent/src/gateway/adapters/telegram/send-message.ts`\n- `packages/coding-agent/src/gateway/adapters/telegram/client.ts`\n- `packages/coding-agent/src/gateway/adapters/telegram/webhook.ts`\n- `packages/coding-agent/test/gateway/telegram-normalize.test.ts`\n\nCapability map:\n\n- `docs/prometheus-telegram-capability-matrix.md`\n\nTelegram must be treated as a native interaction platform, not only a text\ntransport. The adapter should be able to grow into the full Bot API surface\nthrough typed capability modules while keeping the first webhook path small and\nsafe.\n\nInbound normalization:\n\n- Accept `message`, `edited_message`, `channel_post`, and\n `edited_channel_post` through the native message path.\n- Add `business_message`, `guest_message`, `inline_query`, or\n reaction/payment/admin updates only if we choose a clear policy.\n- Ignore bot-authored messages unless explicitly configured.\n- Extract `chat.id`, `chat.type`, `from.id`, `message_id`, text/caption, and\n `message_thread_id`.\n- Enforce access before model dispatch. User-authored updates use the\n `allowedUsers` user-id allowlist; senderless channel posts require the\n `allowedChats` chat-id allowlist.\n- Preserve `message_thread_id` only when it is present and finite.\n- Convert Telegram payloads into `GatewayEvent`; do not let Telegram-specific\n fields leak into gateway core except through `raw` in tests.\n- Keep room in the adapter route identity for channel direct-message topics,\n business connection IDs, guest query IDs, inline query IDs, callback query IDs,\n and inline message IDs before those capabilities are enabled.\n\nMedia handoff:\n\n- Normalize media metadata first. Telegram `photo`, `animation`/GIF, `video`,\n `document`, `live_photo`, `voice`, media groups, spoilers,\n caption-above-media, protected content, paid-post/do-not-delete metadata, and\n auto-delete timer service messages should be visible in `<gateway_event>`.\n- Download binaries only after the Telegram authorization gate. Blocked users\n must not trigger Telegram file downloads, session loads, tool work, or model\n calls.\n- Use Telegram `getFile` plus the official file download path for bytes. Do not\n derive download URLs directly from `file_id`.\n- Pass image-like media through Prometheus's existing `ImageContent` path. This\n includes photos, GIF/image animations, and image documents when the downloaded\n bytes are a supported image. Keep non-image videos and documents as metadata\n until a native file-reference handoff is implemented.\n- Do not expose Prometheus backend mechanics as identity. The gateway should\n make media feel natural to the user while remaining a thin bridge into native\n Prometheus capabilities.\n- Do not invent Telegram features. The Bot API does not expose a general\n view-once media send flag or a universal HD send flag. Preserve timer/protected\n metadata and map \"HD previews\" only to official `prefer_large_media` link\n preview behavior plus best-available inbound image selection.\n\nOutbound delivery:\n\n- Reply to the same `chat_id`.\n- Include `message_thread_id` when the source has a valid thread/topic ID.\n- Later include `direct_messages_topic_id`, `business_connection_id`, or\n `guest_query_id` only through the correct Bot API method for that surface.\n- Split long messages before sending.\n- Redact secrets before sending user-visible errors.\n- Keep a small Telegram client wrapper around `fetch` so tests can assert the\n Bot API method, payload, and topic fields without network calls.\n\nCapability policy:\n\n- Text, commands, callbacks, replies, topics, and webhook security are the v1\n hot path.\n- Media metadata and image-like media handoff are v1; broad non-image file\n processing, polls, reply keyboards, selectors, inline mode, deep links, and\n channel direct-message topics are v2 feature modules.\n- Mini Apps, payments, Stars, paid media, gifts, business-account actions,\n managed bots, chat-admin mutation, stories, and bot-to-bot workflows are\n disabled by default until an explicit product/security policy exists.\n- Any feature that spends money, mutates chat/account state, deletes data, or\n speaks as a business account requires explicit config and approval.\n\nSecurity:\n\n- Webhook mode must validate `X-Telegram-Bot-Api-Secret-Token`.\n- Token config must resolve from environment/config/secret storage, never from\n committed files.\n- Telegram user allowlist support lives in the gateway layer, before gateway\n commands, session lookup, prompt dispatch, memory loading, tools, async jobs,\n or model calls.\n- When `--telegram-allowed-users` or `PROMETHEUS_TELEGRAM_ALLOWED_USER_IDS` is set,\n default Telegram bot commands are cleared and Prometheus's native slash-command menu\n is registered only for authorized user scopes. Private chats get\n `MenuButtonCommands`; group/supergroup member scopes are registered lazily\n after an authorized user is seen in that chat.\n- With no allowlist configured, the gateway stays open for local/dev\n compatibility and registers the default native Prometheus command menu.\n\n## Commands\n\nGateway commands should enter Prometheus through a command router, not through\nspecial Telegram-only agent prompts. Native Prometheus slash commands remain the\nsource of truth for general command behavior and should be projected into\nTelegram rather than reimplemented in the gateway.\n\nInitial commands:\n\n- `/status`: report gateway health, current session scope, active session count,\n and Telegram delivery counters.\n- `/start`: return scope and status.\n- `/new`: create a new session for the current gateway session key.\n- `/memory`: route through native Prometheus command dispatch.\n\nLater command:\n\n- `/update`: spawn the existing `prometheus update` CLI in a controlled subprocess and\n stream status. Do not call update internals directly from the long-running\n gateway process. Add this only after gateway status, restart/lifecycle\n reporting, and pending-operation recovery are designed.\n\nThe command router should receive `GatewayEvent` and return a delivery action.\nOnly non-command text should go to `AgentSession.prompt()`.\n\n## Self-Improvement Boundary\n\nGateway work should not introduce autonomous mutation.\n\nAllowed early behavior:\n\n- trigger memory consolidation or show memory status\n- show proposed `USER.md` updates\n- show proposed `SOUL.md` patches\n- create dry-run self-improvement reports\n\nNot allowed in early gateway work:\n\n- automatic `SOUL.md` writes\n- automatic package update/restart without explicit command\n- deleting or rewriting user-authored skills\n- mutating skills without a report and opt-in command\n\n## Implementation Sequence\n\n1. Add gateway core types and session key tests. Done.\n2. Add gateway context rendering tests. Done.\n3. Add a test-only runner around a mocked `AgentSession` or minimal session\n facade. Done as an injected prompt dispatch facade.\n4. Add Telegram normalization fixtures and tests. Done for inbound `message`\n updates.\n5. Add Telegram client payload tests for topic and non-topic replies. Done for\n `sendMessage`.\n6. Add webhook-secret validation and `Update` variant router tests.\n7. Add callback-query acknowledgement and action routing tests.\n8. Add `prometheus gateway serve --telegram` only after the core and adapter contracts\n are stable.\n9. Add `/status` lifecycle plumbing before `/update`. Done for gateway health,\n current scope, active sessions, and Telegram delivery counters.\n10. Add bounded `retry_after` retry for Telegram sends. Done for in-process\n adapter delivery. Further reliability work should first reuse Prometheus's native\n async/background job delivery before adding gateway-specific queueing.\n11. Add front-door Telegram user allowlist plus scoped command/menu\n registration. Done with `--telegram-allowed-users` /\n `PROMETHEUS_TELEGRAM_ALLOWED_USER_IDS`, pre-session dispatch denial, empty default\n commands, authorized private command/menu scopes, and lazy group\n `chat_member` scopes.\n\n## Review Checklist\n\n- The gateway core imports no Telegram client code.\n- Telegram normalization does not call network APIs.\n- Session-key tests cover topic and non-topic collisions.\n- Production gateway execution goes through RPC unless source review proves RPC\n cannot support the required behavior.\n- Prompt context never includes raw Telegram update JSON.\n- Webhook secret validation is tested.\n- Gateway operational commands short-circuit before model dispatch.\n- Unauthorized Telegram users do not reach gateway commands, session lookup,\n prompt dispatch, memory loading, tools, async jobs, or model calls.\n- Telegram delivery honors `ResponseParameters.retry_after` when configured.\n- `/update` stays subprocess-based.\n- `SOUL.md` remains approval-gated.\n- No bot token, webhook secret, or Authorization header is committed.\n",
|
|
46
46
|
"prometheus-memory-backend.md": "# Prometheus Memory backend\n\nPrometheus can use `@prometheus-ai/memory` as a local long-term memory backend.\n\nSet:\n\n```yaml\nmemory:\n backend: prometheus-memory\n```\n\nExample:\n\n```yaml\nmemory:\n backend: prometheus-memory\nprometheusMemory:\n scoping: per-project-tagged\n```\n\nWith this backend enabled, the coding agent:\n\n1. Opens one or more local Prometheus Memory SQLite databases according to the configured bank scoping.\n2. Recalls relevant memories into a `<memories>` block for the first model turn of a session and refreshes the base prompt if recall happens from the `agent_start` listener.\n3. Retains completed conversation turns into the retain bank after agent turns, no more often than `prometheusMemory.retainEveryNTurns`.\n4. Adds recalled memory as extra compaction context when compaction asks the memory backend for `preCompactionContext`.\n5. Uses the normal `/memory view`, `/memory stats`, `/memory diagnose`, `/memory clear`, and `/memory enqueue` commands through the shared memory backend interface.\n\nRecalled memory is background context, not instructions. Current user messages and tool output take precedence when they conflict.\n\n## Settings\n\n| Setting | Default | Description |\n| ------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `memory.backend` | `off` | Set to `prometheus-memory` to enable this backend. |\n| `prometheusMemory.dbPath` | agent memories dir | Optional SQLite database path. |\n| `prometheusMemory.bank` | project directory name | Base bank name passed to Prometheus Memory; the coding-agent wrapper scopes from this base according to `prometheusMemory.scoping`. |\n| `prometheusMemory.scoping` | `per-project` | Memory visibility mode: `global` = one shared bank, `per-project` = isolated project memory, `per-project-tagged` = project-local writes plus global recall visibility. |\n| `prometheusMemory.autoRecall` | `true` | Recall memory on the first turn of a session. |\n| `prometheusMemory.autoRetain` | `true` | Retain completed turns automatically. |\n| `prometheusMemory.retainEveryNTurns` | `4` | Minimum user turns between automatic retain writes. |\n| `prometheusMemory.recallLimit` | `8` | Maximum recalled memories in the prompt block. |\n| `prometheusMemory.recallContextTurns` | `3` | Prior user-bounded turns included in recall queries. |\n| `prometheusMemory.recallMaxQueryChars` | `4000` | Maximum composed recall query length. |\n| `prometheusMemory.injectionTokenLimit` | `5000` | Approximate token budget for memory prompt injection. |\n| `prometheusMemory.debug` | `false` | Enable debug logging for backend failures. |\n| `prometheusMemory.noEmbeddings` | `false` | Force FTS-only recall. |\n| `prometheusMemory.embeddingModel` | env/default | Embedding model passed to Prometheus Memory. |\n| `prometheusMemory.embeddingApiUrl` | env/default | OpenAI-compatible embedding endpoint passed to Prometheus Memory. |\n| `prometheusMemory.embeddingApiKey` | env/default | Embedding API key passed to Prometheus Memory. |\n| `prometheusMemory.llmMode` | `smol` | `smol` uses the configured Prometheus smol model, `remote` uses the settings below, and `none` disables LLM calls. |\n| `prometheusMemory.llmBaseUrl` | env/default | OpenAI-compatible LLM endpoint for `llmMode: remote`. |\n| `prometheusMemory.llmApiKey` | env/default | LLM API key for `llmMode: remote`. |\n| `prometheusMemory.llmModel` | env/default | LLM model id for `llmMode: remote`. |\n\n## Scoping\n\nThe coding-agent wrapper applies scoping on top of the underlying Prometheus Memory package:\n\n- `global` uses one shared bank for recall and writes.\n- `per-project` writes to and recalls from a bank derived from the current git repository root (or cwd) plus a stable hash.\n- `per-project-tagged` writes to the project-local bank and recalls from both the project-local bank and the shared global bank, with duplicate recall results merged.\n\nThe combined project-plus-global behavior lives in the wrapper. The `@prometheus-ai/memory` package itself still exposes banks and constructor options directly, including `bank` for selecting a bank name. Project-local banks other than the shared bank are stored as sibling bank databases managed by Prometheus Memory's bank manager.\n\n## LLM and embeddings\n\nThe backend passes these settings to the Prometheus Memory package; if a setting is omitted, it falls back to its `PROMETHEUS_MEMORY_*` environment defaults. The backend does not download or run a local GGUF LLM. LLM-dependent paths use a configured Prometheus model, a dynamic completion function, a remote OpenAI-compatible endpoint, or deterministic no-LLM fallbacks.\n\nFTS-only:\n\n```yaml\nmemory:\n backend: prometheus-memory\nprometheusMemory:\n noEmbeddings: true\n```\n\nEquivalent constructor shape:\n\n```ts\nnew PrometheusMemory({ noEmbeddings: true });\n```\n\nRemote embeddings:\n\n```yaml\nprometheusMemory:\n embeddingModel: text-embedding-3-small\n embeddingApiUrl: https://api.openai.com/v1\n embeddingApiKey: ${OPENAI_API_KEY}\n```\n\nEquivalent constructor shape:\n\n```ts\nnew PrometheusMemory({\n embeddingModel: \"text-embedding-3-small\",\n embeddingApiUrl: \"https://api.openai.com/v1\",\n embeddingApiKey,\n});\n```\n\nRemote LLM:\n\n```yaml\nprometheusMemory:\n llmMode: remote\n llmBaseUrl: https://api.openai.com/v1\n llmApiKey: ${OPENAI_API_KEY}\n llmModel: gpt-4.1-mini\n```\n\nEquivalent constructor shapes:\n\n```ts\nnew PrometheusMemory({ llm: { baseUrl, apiKey, model } });\nnew PrometheusMemory({ llmBaseUrl: baseUrl, llmApiKey: apiKey, llmModel: model });\n```\n\nDynamic function LLM for rotating OAuth tokens:\n\n```ts\nnew PrometheusMemory({\n llm: async (prompt, opts) => {\n const token = await getFreshOauthToken();\n return await completeWithPrometheusAi(prompt, {\n token,\n maxTokens: opts?.maxTokens,\n temperature: opts?.temperature,\n });\n },\n});\n```\n\nPrometheus smol model LLM:\n\n```yaml\nprometheusMemory:\n llmMode: smol\n```\n\nThe coding agent resolves its configured smol role and passes a dynamic completion function so every Prometheus Memory LLM call can fetch the current provider credentials at call time:\n\n```ts\nnew PrometheusMemory({\n llm: async (prompt, opts) => completeSmolWithCurrentAuth(prompt, opts),\n});\n```\n\n## Operational notes\n\n- The default shared database lives under the agent memories directory in `prometheus-memory/prometheus-memory.db`; project-scoped banks use sibling database paths under that Prometheus Memory directory.\n- `/memory clear` removes every scoped Prometheus Memory SQLite database and sidecar WAL/SHM files for the active configuration.\n- `/memory enqueue` forces retention of the current session, flushes pending fact extractions, and runs Prometheus Memory sleep/consolidation.\n- `/memory stats` and `/memory diagnose` render backend-specific bank statistics/diagnostics when the Prometheus Memory backend is active.\n- Subagents do not own separate Prometheus Memory retain loops; they alias the parent state when a parent Prometheus Memory state exists, and otherwise remain inert.\n",
|
|
47
|
-
"prometheus-telegram-capability-matrix.md": "# Prometheus Telegram Capability Matrix\n\nThis document records the Telegram-native capability target for the Prometheus gateway.\nIt is based on official Telegram documentation checked on 2026-06-09. The\nlatest official release found during this pass is Bot API 10.0 from 2026-05-08.\n\nThe goal is not to enable every feature on day one. The goal is to make the\nconnector capable by design: each Telegram feature should have a typed ingress,\ntyped outbound method, routing identity, permission policy, and tests before a\nworkflow uses it.\n\n## Official Sources\n\n- Bot API changelog: https://core.telegram.org/bots/api-changelog\n- Bot API reference: https://core.telegram.org/bots/api\n- Bot features: https://core.telegram.org/bots/features\n- Mini Apps: https://core.telegram.org/bots/webapps\n- Payments: https://core.telegram.org/bots/payments\n- Inline bots: https://core.telegram.org/bots/inline\n- Webhook IP ranges: https://core.telegram.org/bots/webhooks\n\n## Design Rule\n\nTelegram is not just a text transport. Treat it as a native interaction\nplatform.\n\nThe Prometheus gateway should therefore model four layers separately:\n\n1. Ingress: every supported `Update` kind is parsed into a typed gateway event.\n2. Identity: sessions are keyed by the real Telegram conversation boundary.\n3. Capability: outbound calls are exposed as explicit adapter capabilities.\n4. Policy: high-risk capabilities require configuration, rights checks, and\n explicit workflow approval.\n\nDo not hide all Telegram behavior behind `sendMessage(text)`. Text reply is only\none capability.\n\n## Routing Identity Model\n\nEvery Telegram event should carry a route key that can distinguish these\nsurfaces:\n\n- normal private chat\n- normal group or supergroup\n- forum topic in a supergroup\n- private chat topic for bots with topic mode enabled\n- channel post\n- channel direct-message topic\n- business connection chat\n- guest query\n- inline query\n- callback query tied to an existing message or inline message\n\nRecommended internal fields:\n\n```ts\ninterface TelegramRouteIdentity {\n\tupdateType: string;\n\tchatId?: string;\n\tchatType?: \"private\" | \"group\" | \"supergroup\" | \"channel\";\n\tuserId?: string;\n\tmessageId?: string;\n\tmessageThreadId?: string;\n\tdirectMessagesTopicId?: string;\n\tbusinessConnectionId?: string;\n\tguestQueryId?: string;\n\tinlineQueryId?: string;\n\tcallbackQueryId?: string;\n\tinlineMessageId?: string;\n}\n```\n\nThe gateway session key should be derived from the smallest stable conversation\nboundary, not from a loose chat ID. For example, a supergroup topic and a channel\ndirect-message topic must not collapse into the same session.\n\n## Capability Matrix\n\n| Capability group | Official anchor | Prometheus implication | Target |\n| --- | --- | --- | --- |\n| Update envelope | `Update` in Bot API reference | Route by the single update variant present: `message`, `edited_message`, `callback_query`, `business_message`, `guest_message`, reactions, payments, boosts, managed bot updates, and others. | v1 base |\n| Ingress transport | `setWebhook`, `deleteWebhook`, `getUpdates` | Use webhook mode for production/tunnels and polling mode as an explicit setup option for local or always-on hosts without public HTTPS. Webhook mode validates `X-Telegram-Bot-Api-Secret-Token`; polling mode calls `deleteWebhook` before long polling. Both modes configure `allowed_updates`, dedupe `update_id`, and route through the same adapter path. | v1 |\n| Rate limits and retries | Bot API FAQ, `ResponseParameters` | Centralize retry/backoff. Honor `retry_after`, throttle per chat/group/broadcast, and avoid paid broadcast unless spend controls exist. | v1 |\n| Normal chats | `Chat`, `Message` | Support private, group, supergroup, and channel IDs as strings. Keep text, caption, entities, sender, reply metadata, and message IDs. | v1 |\n| Forum and private topics | `message_thread_id`, forum topic methods | Preserve `message_thread_id` on ingress and outbound replies. Later expose topic create/edit/close/delete as explicit admin capabilities. | v1 route, v2 manage |\n| Channel direct-message topics | `DirectMessagesTopic`, `direct_messages_topic_id` | Model separately from forum topics. Outbound methods need `direct_messages_topic_id` where official methods require it. | v2 |\n| Replies and quotes | `ReplyParameters`, `ExternalReplyInfo` | Preserve reply targets and expose reply builders. Do not assume replies always refer to messages in the same chat/topic. | v1 |\n| Text messages | `sendMessage` | Keep current outbound text builder. Split or summarize over-limit text before send. | v1 |\n| Draft/streaming status | `sendMessageDraft`, `sendChatAction` | Use `sendChatAction` for simple typing/upload indicators and `sendMessageDraft` later for richer \"thinking\" previews in private chats. | v1 action, v2 draft |\n| Commands and menus | `BotCommand`, `BotCommandScope`, bot features commands | Use scoped menus for UX only. Enforce command validity and authorization server-side. | v1 |\n| Inline keyboards | `InlineKeyboardMarkup`, `CallbackQuery` | Treat callbacks as authenticated UI actions, answer every callback query, and bind callback payloads to session/action state. | v1 |\n| Reply keyboards and selectors | `ReplyKeyboardMarkup`, request users/chats/location/contact/poll | Expose as UI capabilities for workflows that need structured answers. | v2 |\n| Media and files | send/get file methods and `Message` media fields | Normalize media metadata and support download/upload helpers for photos, video, audio, voice, documents, animation, stickers, video notes, and live photos. Store reusable `file_id`; download binaries only when a workflow needs them. Current native handoff downloads image-like media only and passes it through Prometheus `ImageContent`; non-image videos/documents remain metadata until a native file-reference handoff is added. | v1 metadata + native image handoff, v2 broad media |\n| Albums/media groups | `sendMediaGroup`, media group IDs | Preserve album grouping and route grouped media as one logical user input where possible. | v2 |\n| Polls/quizzes/checklists | `Poll`, `sendPoll`, checklist fields | Support poll results and later poll creation. Media polls and checklist task replies need explicit typed support. | v2 |\n| Reactions | `MessageReactionUpdated`, `setMessageReaction`, delete reaction methods | Optional workflows for acknowledgement/moderation. Requires explicit `allowed_updates` and rights. | v2/defer |\n| Inline mode | inline bot docs, `InlineQuery`, `ChosenInlineResult` | Add a separate query/answer path; this is not a chat session until a result creates one. | v2 |\n| Deep links | bot features deep linking | Map `/start <payload>` to safe onboarding/session/action routes. Validate payloads. | v1/v2 |\n| Telegram Login/OIDC | Telegram Login widget / OIDC docs | Use only for a web portal or Mini App auth flow. Validate tokens server-side, enforce redirect allowlists, and keep client secrets out of the bot runtime. | v2 if portal exists |\n| Mini Apps/Web Apps | Mini Apps docs, `WebAppInfo`, `answerWebAppQuery` | Treat Mini Apps as a separate frontend surface with init-data validation. Never trust `initDataUnsafe`; validate HMAC/signature server-side and reject stale `auth_date`. | v2/defer |\n| Payments and Stars | payments docs, invoice/payment updates, Stars payments | Support only after product policy is defined. Digital goods use Stars (`currency=XTR`), pre-checkout handling, successful payment persistence, charge IDs, refunds, subscription state, and audit logs. | v2/defer |\n| Paid media and gifts | paid media/gift Bot API objects | Preserve paid-media metadata and the official paid-post do-not-delete constraint. Track entitlements, purchase events, Star spend, gift inventory, and refund/support state only after a product policy exists. These are monetization/account actions, not core agent chat. | metadata now, defer actions |\n| Business/secretary bots | bot features secretary bots, `BusinessConnection` | Namespace sessions by `business_connection_id`; process business connections/messages/edits/deletes. Persist `BusinessBotRights` and enforce the 24-hour managed-chat window before replies. | v2, earlier if product requires |\n| Guest bots | Bot API 10.0 guest mode, `answerGuestQuery` | Support summoned one-shot interactions without assuming chat membership or history. | v2 |\n| Bot-to-bot communication | bot features bot-to-bot | Useful for agent swarms only with loop prevention, sender trust policy, and rate controls. | defer |\n| Chat/admin management | chat member, permissions, boosts, topic/admin methods | Expose only as admin-gated capabilities; never let normal chat prompts mutate chat state directly. | v2/defer |\n| Managed bots | `managed_bot` update and management methods | Treat as a separate operator workflow, not normal chat. | defer |\n| Stories/profile/account features | story/profile/photo/gift methods | Business/admin only, approval-gated. | defer |\n\n## Prometheus-Native Adapter Shape\n\nThe Telegram adapter should grow as modules, not as one giant client:\n\n```text\nsrc/gateway/adapters/telegram/\n types.ts # current and future Bot API payload types\n normalize.ts # update -> gateway event/capability event\n send-message.ts # text send capability\n webhook.ts # request validation and update parsing\n callbacks.ts # callback query action routing\n commands.ts # Telegram command parsing/menu helpers\n media.ts # media upload/download helpers\n topics.ts # forum/private/direct-message topic helpers\n rate-limit.ts # retry_after/backoff and per-route throttles\n business.ts # business connection helpers\n inline.ts # inline query answer helpers\n payments.ts # invoice/payment helpers\n web-app.ts # Mini App init-data and answer helpers\n```\n\nGateway core must stay Telegram-free. Telegram-specific fields belong inside the\nadapter and should be projected into platform-neutral gateway events only when\nthe core needs them.\n\n## Capability Policy\n\nEach outbound capability should declare:\n\n- method name\n- required route identity fields\n- required bot rights or BotFather configuration\n- whether it is safe in private chat, group, supergroup, channel, business chat,\n guest query, or inline mode\n- whether it can spend money, mutate chat state, delete data, or act as a\n business account\n- test fixtures and official source link\n\nHigh-risk classes:\n\n- money movement: invoices, refunds, Stars, subscriptions, paid media, gifts\n- account mutation: profile, username, stories, business account changes\n- chat mutation: bans, permissions, topic deletion, message deletion, reactions\n- bot management: managed bot creation/token changes\n- autonomous bot-to-bot messaging\n\nHigh-risk capabilities must be disabled by default and require explicit config\nplus a Prometheus confirmation/approval flow.\n\n## Implementation Order\n\n1. Webhook validation and `Update` variant router.\n2. Text/caption messages, topics, replies, commands, and callbacks.\n3. Message actions: typing/upload indicators, message edits, delete own message,\n answer callback query.\n4. Basic media metadata ingestion for voice, photo, document, animation/GIF,\n video, and live-photo messages; pass image-like Telegram files through\n Prometheus's native image input path after authorization.\n5. Reply keyboards, selectors, polls, and structured chat UI.\n6. Inline mode and deep-link handoffs.\n7. Channel direct-message topics.\n8. Business/secretary bot support.\n9. Mini Apps/Web Apps and Telegram Login/OIDC.\n10. Payments, Stars, paid media, gifts, managed bots, and bot-to-bot workflows.\n\n## Current Status\n\nThe v1 gateway slice now covers webhook validation, update routing,\ntopic-aware text replies, callback acknowledgement, fast production webhook\nacknowledgement, in-memory `update_id` dedupe, chunked text sends, bounded\n`retry_after` send retry, official `sendChatAction` indicators with opt-in\n`business_connection_id`, typed `editMessageText`, `editMessageCaption`, and\n`editMessageReplyMarkup` adapter support, typed adapter-only\n`deleteMessage`/`deleteMessages` support, channel direct-message topic route\npreservation, photo/voice/document/animation/GIF/video/live-photo ingress for\nrouteable text/caption messages, spoiler/caption-above/protected-content,\npaid-post/do-not-delete, media-group, paid-media star count, and auto-delete\ntimer metadata, adapter `getFile` metadata and official file URL helpers,\nauthorized image-like media download, native Prometheus `ImageContent` handoff\nfor photos/GIFs/image documents, per-route Prometheus session pooling,\nTelegram native command menu projection, gateway `/status`, and a runnable CLI\nsurface:\n\n```text\nprometheus gateway serve --telegram\n```\n\nThe gateway also supports a native Telegram expression catalog for user-owned\nstickers and GIF animations. Catalog assets can be stored in native settings or\n`~/.prometheus/agent/telegram-expressions.json`, searched with broad expression intent\naliases, selected by Prometheus with a hidden trailing marker, and delivered\nthrough the existing `sendSticker`/`sendAnimation` adapter path without exposing\nthe marker to Telegram users.\n\nImplemented tests cover:\n\n- rejects non-POST webhook requests\n- rejects wrong or missing `X-Telegram-Bot-Api-Secret-Token`\n- parses valid JSON update bodies\n- routes `message` updates through `normalizeTelegramUpdate`\n- ignores unsupported update kinds with an explicit result\n- preserves `message_thread_id` in the resulting gateway event\n- preserves `direct_messages_topic.topic_id` as a separate channel direct-message\n route identity\n- sends replies back to the same Telegram chat/topic\n- sends channel direct-message replies with `direct_messages_topic_id`\n- sends supported `sendChatAction` indicators, serializes optional\n `business_connection_id`, and skips unsupported channel/direct-message\n chat-action routes\n- builds and sends typed `editMessageText`, `editMessageCaption`, and\n `editMessageReplyMarkup` payloads for chat and inline message targets\n- builds and sends typed adapter-only `deleteMessage` and `deleteMessages`\n payloads without wiring prompt-level deletion behavior\n- preserves photo, voice, document, animation/GIF, video, and live-photo metadata\n in gateway context and routes non-audio media-only messages into prompt\n dispatch with generated intent text\n- preserves spoiler, caption-above, protected-content, paid-post/do-not-delete,\n media-group, paid-media star count, and auto-delete timer metadata without\n inventing unsupported Bot API flags\n- builds and sends typed adapter-only `getFile` payloads, constructs official\n Telegram file download URLs, downloads authorized image-like media, and hands\n supported images to Prometheus as native image blocks rather than prompt text\n- treats true view-once media and universal HD media sending as unsupported by\n the official Bot API. \"HD previews\" is an Prometheus policy mapped only to Telegram's\n supported `prefer_large_media` link-preview option and best-available inbound\n image variant selection.\n- answers callback queries before routing callback actions\n- acknowledges production webhooks before long model/reply work completes\n- dedupes repeated `update_id` values inside a long-lived handler\n- splits over-limit text replies before `sendMessage`\n- retries `sendMessage` after official `retry_after` flood-control responses\n- routes direct chats, group chats, topics, and channel direct-message topics\n into distinct Prometheus sessions\n- handles `/status` before Prometheus prompt dispatch and returns current gateway scope\n- validates CLI bind, webhook path, token/secret config, and command\n registration\n\n## Current Next Slice\n\nSimplify the gateway around native Prometheus behavior before adding more runtime\nmachinery:\n\n- keep Telegram command registration as a projection of native Prometheus slash\n commands\n- keep authorized-user filtering at the Telegram front door so blocked users do\n not create/load Prometheus sessions or reach model/tool work\n- keep Telegram dispatch on the normal Prometheus session/prompt path\n- use Prometheus's native async/background job system for long-running Prometheus work\n- add new gateway queueing or schedulers only after a proven gap in the native\n path\n\nDo not add Mini Apps, payments, business rights, broad non-image media downloads,\nor admin chat mutation in this next slice. The adapter should remain capable of\nthese features later, but v1 should keep the hot path reliable.\n",
|
|
47
|
+
"prometheus-telegram-capability-matrix.md": "# Prometheus Telegram Capability Matrix\n\nThis document records the Telegram-native capability target for the Prometheus gateway.\nIt is based on official Telegram documentation checked on 2026-06-14. The\nlatest official release found during this pass is Bot API 10.1 from 2026-06-11.\n\nThe goal is not to enable every feature on day one. The goal is to make the\nconnector capable by design: each Telegram feature should have a typed ingress,\ntyped outbound method, routing identity, permission policy, and tests before a\nworkflow uses it.\n\n## Official Sources\n\n- Bot API changelog: https://core.telegram.org/bots/api-changelog\n- Bot API reference: https://core.telegram.org/bots/api\n- Bot features: https://core.telegram.org/bots/features\n- Mini Apps: https://core.telegram.org/bots/webapps\n- Payments: https://core.telegram.org/bots/payments\n- Inline bots: https://core.telegram.org/bots/inline\n- Webhook IP ranges: https://core.telegram.org/bots/webhooks\n\n## Design Rule\n\nTelegram is not just a text transport. Treat it as a native interaction\nplatform.\n\nThe Prometheus gateway should therefore model four layers separately:\n\n1. Ingress: every supported `Update` kind is parsed into a typed gateway event.\n2. Identity: sessions are keyed by the real Telegram conversation boundary.\n3. Capability: outbound calls are exposed as explicit adapter capabilities.\n4. Policy: high-risk capabilities require configuration, rights checks, and\n explicit workflow approval.\n\nDo not hide all Telegram behavior behind `sendMessage(text)`. Text reply is only\none capability.\n\n## Routing Identity Model\n\nEvery Telegram event should carry a route key that can distinguish these\nsurfaces:\n\n- normal private chat\n- normal group or supergroup\n- forum topic in a supergroup\n- private chat topic for bots with topic mode enabled\n- channel post\n- channel direct-message topic\n- business connection chat\n- guest query\n- inline query\n- callback query tied to an existing message or inline message\n\nRecommended internal fields:\n\n```ts\ninterface TelegramRouteIdentity {\n\tupdateType: string;\n\tchatId?: string;\n\tchatType?: \"private\" | \"group\" | \"supergroup\" | \"channel\";\n\tuserId?: string;\n\tmessageId?: string;\n\tmessageThreadId?: string;\n\tdirectMessagesTopicId?: string;\n\tbusinessConnectionId?: string;\n\tguestQueryId?: string;\n\tinlineQueryId?: string;\n\tcallbackQueryId?: string;\n\tinlineMessageId?: string;\n}\n```\n\nThe gateway session key should be derived from the smallest stable conversation\nboundary, not from a loose chat ID. For example, a supergroup topic and a channel\ndirect-message topic must not collapse into the same session.\n\n## Capability Matrix\n\n| Capability group | Official anchor | Prometheus implication | Target |\n| --- | --- | --- | --- |\n| Update envelope | `Update` in Bot API reference | Route by the single update variant present: `message`, `edited_message`, `callback_query`, `business_message`, `guest_message`, reactions, payments, boosts, managed bot updates, and others. | v1 base |\n| Rich messages | Bot API 10.1 rich message classes and `sendRichMessage` | Treat rich messages as a future typed send/content capability. Do not collapse them into ad hoc Markdown; wait for a policy and renderer design. | v2/defer |\n| Ingress transport | `setWebhook`, `deleteWebhook`, `getUpdates` | Use webhook mode for production/tunnels and polling mode as an explicit setup option for local or always-on hosts without public HTTPS. Webhook mode validates `X-Telegram-Bot-Api-Secret-Token`; polling mode calls `deleteWebhook` before long polling. Both modes configure `allowed_updates`, dedupe `update_id`, and route through the same adapter path. | v1 |\n| Rate limits and retries | Bot API FAQ, `ResponseParameters` | Centralize retry/backoff. Honor `retry_after`, throttle per chat/group/broadcast, and avoid paid broadcast unless spend controls exist. | v1 |\n| Normal chats | `Chat`, `Message` | Support private, group, supergroup, and channel IDs as strings. Keep text, caption, entities, sender, reply metadata, and message IDs. | v1 |\n| Forum and private topics | `message_thread_id`, forum topic methods | Preserve `message_thread_id` on ingress and outbound replies. Later expose topic create/edit/close/delete as explicit admin capabilities. | v1 route, v2 manage |\n| Channel direct-message topics | `DirectMessagesTopic`, `direct_messages_topic_id` | Model separately from forum topics. Outbound methods need `direct_messages_topic_id` where official methods require it. | v2 |\n| Replies and quotes | `ReplyParameters`, `ExternalReplyInfo` | Preserve reply targets and expose reply builders. Do not assume replies always refer to messages in the same chat/topic. | v1 |\n| Text messages | `sendMessage` | Keep current outbound text builder. Split or summarize over-limit text before send. | v1 |\n| Draft/streaming status | `sendMessageDraft`, `sendChatAction` | Use `sendChatAction` for simple typing/upload indicators and `sendMessageDraft` later for richer \"thinking\" previews in private chats. | v1 action, v2 draft |\n| Commands and menus | `BotCommand`, `BotCommandScope`, bot features commands | Use scoped menus for UX only. Enforce command validity and authorization server-side. | v1 |\n| Inline keyboards | `InlineKeyboardMarkup`, `CallbackQuery` | Treat callbacks as authenticated UI actions, answer every callback query, and bind callback payloads to session/action state. | v1 |\n| Reply keyboards and selectors | `ReplyKeyboardMarkup`, request users/chats/location/contact/poll | Expose as UI capabilities for workflows that need structured answers. | v2 |\n| Media and files | send/get file methods and `Message` media fields | Normalize media metadata and support download/upload helpers for photos, video, audio, voice, documents, animation, stickers, video notes, and live photos. Store reusable `file_id`; download binaries only when a workflow needs them. Current native handoff downloads image-like media only and passes it through Prometheus `ImageContent`; non-image videos/documents remain metadata until a native file-reference handoff is added. | v1 metadata + native image handoff, v2 broad media |\n| Albums/media groups | `sendMediaGroup`, media group IDs | Preserve album grouping and route grouped media as one logical user input where possible. | v2 |\n| Polls/quizzes/checklists | `Poll`, `sendPoll`, checklist fields | Support poll results and later poll creation. Media polls and checklist task replies need explicit typed support. | v2 |\n| Reactions | `MessageReactionUpdated`, `setMessageReaction`, delete reaction methods | Optional workflows for acknowledgement/moderation. Requires explicit `allowed_updates` and rights. | v2/defer |\n| Inline mode | inline bot docs, `InlineQuery`, `ChosenInlineResult` | Add a separate query/answer path; this is not a chat session until a result creates one. | v2 |\n| Deep links | bot features deep linking | Map `/start <payload>` to safe onboarding/session/action routes. Validate payloads. | v1/v2 |\n| Telegram Login/OIDC | Telegram Login widget / OIDC docs | Use only for a web portal or Mini App auth flow. Validate tokens server-side, enforce redirect allowlists, and keep client secrets out of the bot runtime. | v2 if portal exists |\n| Mini Apps/Web Apps | Mini Apps docs, `WebAppInfo`, `answerWebAppQuery` | Treat Mini Apps as a separate frontend surface with init-data validation. Never trust `initDataUnsafe`; validate HMAC/signature server-side and reject stale `auth_date`. | v2/defer |\n| Payments and Stars | payments docs, invoice/payment updates, Stars payments | Support only after product policy is defined. Digital goods use Stars (`currency=XTR`), pre-checkout handling, successful payment persistence, charge IDs, refunds, subscription state, and audit logs. | v2/defer |\n| Paid media and gifts | paid media/gift Bot API objects | Preserve paid-media metadata and the official paid-post do-not-delete constraint. Track entitlements, purchase events, Star spend, gift inventory, and refund/support state only after a product policy exists. These are monetization/account actions, not core agent chat. | metadata now, defer actions |\n| Business/secretary bots | bot features secretary bots, `BusinessConnection` | Namespace sessions by `business_connection_id`; process business connections/messages/edits/deletes. Persist `BusinessBotRights` and enforce the 24-hour managed-chat window before replies. | v2, earlier if product requires |\n| Guest bots | Bot API 10.0 guest mode, `answerGuestQuery` | Support summoned one-shot interactions without assuming chat membership or history. | v2 |\n| Bot-to-bot communication | bot features bot-to-bot | Useful for agent swarms only with loop prevention, sender trust policy, and rate controls. | defer |\n| Chat/admin management | chat member, permissions, boosts, topic/admin methods | Expose only as admin-gated capabilities; never let normal chat prompts mutate chat state directly. | v2/defer |\n| Managed bots | `managed_bot` update and management methods | Treat as a separate operator workflow, not normal chat. | defer |\n| Stories/profile/account features | story/profile/photo/gift methods | Business/admin only, approval-gated. | defer |\n\n## Prometheus-Native Adapter Shape\n\nThe Telegram adapter should grow as modules, not as one giant client:\n\n```text\nsrc/gateway/adapters/telegram/\n types.ts # current and future Bot API payload types\n normalize.ts # update -> gateway event/capability event\n send-message.ts # text send capability\n webhook.ts # request validation and update parsing\n callbacks.ts # callback query action routing\n commands.ts # Telegram command parsing/menu helpers\n media.ts # media upload/download helpers\n topics.ts # forum/private/direct-message topic helpers\n rate-limit.ts # retry_after/backoff and per-route throttles\n business.ts # business connection helpers\n inline.ts # inline query answer helpers\n payments.ts # invoice/payment helpers\n web-app.ts # Mini App init-data and answer helpers\n```\n\nGateway core must stay Telegram-free. Telegram-specific fields belong inside the\nadapter and should be projected into platform-neutral gateway events only when\nthe core needs them.\n\n## Capability Policy\n\nEach outbound capability should declare:\n\n- method name\n- required route identity fields\n- required bot rights or BotFather configuration\n- whether it is safe in private chat, group, supergroup, channel, business chat,\n guest query, or inline mode\n- whether it can spend money, mutate chat state, delete data, or act as a\n business account\n- test fixtures and official source link\n\nHigh-risk classes:\n\n- money movement: invoices, refunds, Stars, subscriptions, paid media, gifts\n- account mutation: profile, username, stories, business account changes\n- chat mutation: bans, permissions, topic deletion, message deletion, reactions\n- bot management: managed bot creation/token changes\n- autonomous bot-to-bot messaging\n\nHigh-risk capabilities must be disabled by default and require explicit config\nplus a Prometheus confirmation/approval flow.\n\n## Implementation Order\n\n1. Webhook validation and `Update` variant router.\n2. Text/caption messages, topics, replies, commands, and callbacks.\n3. Message actions: typing/upload indicators, message edits, delete own message,\n answer callback query.\n4. Basic media metadata ingestion for voice, photo, document, animation/GIF,\n video, and live-photo messages; pass image-like Telegram files through\n Prometheus's native image input path after authorization.\n5. Reply keyboards, selectors, polls, and structured chat UI.\n6. Inline mode and deep-link handoffs.\n7. Channel direct-message topics.\n8. Business/secretary bot support.\n9. Mini Apps/Web Apps and Telegram Login/OIDC.\n10. Payments, Stars, paid media, gifts, managed bots, and bot-to-bot workflows.\n\n## Current Status\n\nThe v1 gateway slice now covers webhook validation, update routing for\n`message`, `edited_message`, `channel_post`, `edited_channel_post`, and\n`callback_query`,\nuser-id and chat-id allowlists before model dispatch, topic-aware text replies,\ncallback acknowledgement, fast production webhook\nacknowledgement, in-memory `update_id` dedupe, chunked text sends, bounded\n`retry_after` send retry, official `sendChatAction` indicators with opt-in\n`business_connection_id`, typed `editMessageText`, `editMessageCaption`, and\n`editMessageReplyMarkup` adapter support, typed adapter-only\n`deleteMessage`/`deleteMessages` support, channel direct-message topic route\npreservation, photo/voice/document/animation/GIF/video/live-photo ingress for\nrouteable text/caption messages, spoiler/caption-above/protected-content,\npaid-post/do-not-delete, media-group, paid-media star count, and auto-delete\ntimer metadata, adapter `getFile` metadata and official file URL helpers,\nauthorized image-like media download, native Prometheus `ImageContent` handoff\nfor photos/GIFs/image documents, per-route Prometheus session pooling,\nTelegram native command menu projection, gateway `/status`, and a runnable CLI\nsurface:\n\n```text\nprometheus gateway serve --telegram\n```\n\nThe gateway also supports a native Telegram expression catalog for user-owned\nstickers and GIF animations. Catalog assets can be stored in native settings or\n`~/.prometheus/agent/telegram-expressions.json`, searched with broad expression intent\naliases, selected by Prometheus with a hidden trailing marker, and delivered\nthrough the existing `sendSticker`/`sendAnimation` adapter path without exposing\nthe marker to Telegram users.\n\nImplemented tests cover:\n\n- rejects non-POST webhook requests\n- rejects wrong or missing `X-Telegram-Bot-Api-Secret-Token`\n- parses valid JSON update bodies\n- routes `message` updates through `normalizeTelegramUpdate`\n- routes `edited_message`, `channel_post`, and `edited_channel_post` update\n envelopes through the same native message normalizer with explicit event kinds\n- ignores unsupported update kinds with an explicit result\n- preserves `message_thread_id` in the resulting gateway event\n- preserves `direct_messages_topic.topic_id` as a separate channel direct-message\n route identity\n- sends replies back to the same Telegram chat/topic\n- sends channel direct-message replies with `direct_messages_topic_id`\n- sends supported `sendChatAction` indicators, serializes optional\n `business_connection_id`, and skips unsupported channel/direct-message\n chat-action routes\n- builds and sends typed `editMessageText`, `editMessageCaption`, and\n `editMessageReplyMarkup` payloads for chat and inline message targets\n- builds and sends typed adapter-only `deleteMessage` and `deleteMessages`\n payloads without wiring prompt-level deletion behavior\n- preserves photo, voice, document, animation/GIF, video, and live-photo metadata\n in gateway context and routes non-audio media-only messages into prompt\n dispatch with generated intent text\n- preserves spoiler, caption-above, protected-content, paid-post/do-not-delete,\n media-group, paid-media star count, and auto-delete timer metadata without\n inventing unsupported Bot API flags\n- builds and sends typed adapter-only `getFile` payloads, constructs official\n Telegram file download URLs, downloads authorized image-like media, and hands\n supported images to Prometheus as native image blocks rather than prompt text\n- treats true view-once media and universal HD media sending as unsupported by\n the official Bot API. \"HD previews\" is an Prometheus policy mapped only to Telegram's\n supported `prefer_large_media` link-preview option and best-available inbound\n image variant selection.\n- answers callback queries before routing callback actions\n- acknowledges production webhooks before long model/reply work completes\n- dedupes repeated `update_id` values inside a long-lived handler\n- splits over-limit text replies before `sendMessage`\n- retries `sendMessage` after official `retry_after` flood-control responses\n- routes direct chats, group chats, topics, and channel direct-message topics\n into distinct Prometheus sessions\n- handles `/status` before Prometheus prompt dispatch and returns current gateway scope\n- validates CLI bind, webhook path, token/secret config, and command\n registration\n\n## Current Next Slice\n\nSimplify the gateway around native Prometheus behavior before adding more runtime\nmachinery:\n\n- keep Telegram command registration as a projection of native Prometheus slash\n commands\n- keep authorized-user filtering at the Telegram front door so blocked users do\n not create/load Prometheus sessions or reach model/tool work\n- keep Telegram dispatch on the normal Prometheus session/prompt path\n- use Prometheus's native async/background job system for long-running Prometheus work\n- add new gateway queueing or schedulers only after a proven gap in the native\n path\n\nDo not add Mini Apps, payments, business rights, broad non-image media downloads,\nor admin chat mutation in this next slice. The adapter should remain capable of\nthese features later, but v1 should keep the hot path reliable.\n",
|
|
48
48
|
"prometheus-telegram-expression-catalog.md": "# Prometheus Telegram Expression Catalog\n\nPrometheus can attach Telegram-native stickers and GIF animations to replies\nwhen the agent explicitly selects a media kind and expression from a local\ncatalog.\nThis keeps the choice expressive without inventing a separate media workflow.\n\n## Native Rule\n\nThe catalog stores expression groups with reusable Telegram media references\nunderneath them:\n\n- `sticker` assets are sent with Telegram `sendSticker`.\n- `animation` assets are sent with Telegram `sendAnimation`.\n- The selection order is media kind first (`sticker` or `gif`), then expression,\n then one matching media variant.\n- `media` should usually be a Telegram `file_id` captured for this bot.\n- HTTP URLs are allowed by Telegram for animations and static `.WEBP` stickers,\n but `file_id` is preferred because it is stable for the bot and avoids\n downloading third-party media at send time.\n\nDo not store bot tokens, webhook secrets, Authorization headers, or private user\ndata in the catalog.\n\nOfficial Telegram references:\n\n- https://core.telegram.org/bots/api#sendsticker\n- https://core.telegram.org/bots/api#sendanimation\n- https://core.telegram.org/bots/api#sending-files\n\n## Search And Selection\n\nThe gateway privately shows matching media-kind and expression pairs to\nPrometheus inside the gateway handoff prompt. Prometheus sends one only by\nappending a hidden marker at the end of its reply:\n\n```text\nThat was painfully awkward. {{telegram_expression:sticker:awkward}}\nThat was painfully awkward. {{telegram_expression:gif:awkward}}\n```\n\nThe gateway strips the marker before delivery, picks one matching Telegram media\nvariant for that media kind and expression, and sends it through the existing\nTelegram response path. Slash-command replies never attach expressions.\n\n## Catalog Location\n\nPrometheus resolves the catalog in this order:\n\n1. `--telegram-expression-catalog` or `PROMETHEUS_TELEGRAM_EXPRESSION_CATALOG_PATH`\n2. Native setup/config setting `gateway.telegram.expressionCatalogPath`\n3. Native setup/config record `gateway.telegram.expressions.catalog`\n4. Default file `~/.prometheus/agent/telegram-expressions.json`\n\n`gateway.telegram.expressions.enabled: false` disables all expression catalog\nloading.\n\n## Example\n\n```json\n{\n \"enabled\": true,\n \"minScore\": 2,\n \"expressions\": {\n \"awkward\": {\n \"title\": \"Awkward\",\n \"tags\": [\"cringe\", \"yikes\", \"facepalm\"],\n \"variants\": [\n {\n \"type\": \"animation\",\n \"media\": \"CgACAgQAAxkBAA...\",\n \"tags\": [\"gif\", \"pause\"]\n },\n {\n \"type\": \"sticker\",\n \"media\": \"CAACAgUAAxkBAA...\",\n \"emoji\": \"😬\",\n \"tags\": [\"sticker\", \"grimace\"]\n }\n ]\n }\n }\n}\n```\n\nThe older flat `assets` form and expression-only markers still work for\ncompatibility, but new catalogs should use grouped `expressions` and new prompt\nmarkers should use `sticker:<expression>` or `gif:<expression>`.\n\n## Wide Intent Tags\n\nUse a few canonical tags per asset. Prometheus expands these with built-in\naliases during search:\n\n```text\nacknowledge, admire, agree, angry, approve, awkward, bored, calm,\ncelebrate, chaos, checking, confused, curious, disagree, done, dramatic,\nembarrassed, excited, fast, funny, greeting, help, love, no, ok, panic,\nproud, sad, savage, shocked, sleepy, sorry, success, suspicious, thanks,\nthinking, wait, welcome, yes\n```\n\nGood starter groups:\n\n- `awkward`, `cringe`, `yikes`, `facepalm`\n- `funny`, `lol`, `meme`, `joke`\n- `celebrate`, `success`, `done`, `win`\n- `thinking`, `checking`, `wait`, `curious`\n- `approve`, `agree`, `ok`, `yes`\n- `sad`, `sorry`, `calm`, `love`, `thanks`\n",
|
|
49
49
|
"provider-streaming-internals.md": "# Provider streaming internals\n\nThis document explains how token/tool streaming is normalized in `@prometheus-ai/ai`, then propagated through `@prometheus-ai/agent-core` and `coding-agent` session events.\n\n## End-to-end flow\n\n1. `streamSimple()` (`packages/ai/src/stream.ts`) maps generic options and dispatches to a provider stream function.\n2. Provider stream functions translate provider-native stream events into the unified `AssistantMessageEvent` sequence. Current built-ins include Anthropic, OpenAI Responses/Completions/Codex/Azure Responses, Google Gemini/Gemini CLI/Vertex, Bedrock Converse, Ollama, Cursor, prometheus-native gateway transport, plus GitLab Duo/Kimi/Synthetic wrappers and extension-registered custom APIs.\n3. Each provider pushes events into `AssistantMessageEventStream` (`packages/ai/src/utils/event-stream.ts`), which throttles delta events and exposes:\n - async iteration for incremental updates\n - `result()` for final `AssistantMessage`\n4. `agentLoop` (`packages/agent/src/agent-loop.ts`) consumes those events, mutates in-flight assistant state, and emits `message_update` events carrying the raw `assistantMessageEvent`.\n5. `AgentSession` (`packages/coding-agent/src/session/agent-session.ts`) subscribes to agent events, persists messages, drives extension hooks, and applies session behaviors (retry, compaction, TTSR, streaming-edit abort checks).\n\n## Unified stream contract in `@prometheus-ai/ai`\n\nAll providers emit the same shape (`AssistantMessageEvent` in `packages/ai/src/types.ts`):\n\n- `start`\n- content block lifecycle triplets:\n - text: `text_start` → `text_delta`\\* → `text_end`\n - thinking: `thinking_start` → `thinking_delta`\\* → `thinking_end`\n - tool call: `toolcall_start` → `toolcall_delta`\\* → `toolcall_end`\n- terminal event:\n - `done` with `reason: \"stop\" | \"length\" | \"toolUse\"`\n - or `error` with `reason: \"aborted\" | \"error\"`\n\n`AssistantMessageEventStream` guarantees:\n\n- final result is resolved by terminal event (`done` or `error`)\n- deltas are batched/throttled (~50ms)\n- buffered deltas are flushed before non-delta events and before completion\n\n## Delta throttling and harmonization behavior\n\n`AssistantMessageEventStream` treats `text_delta`, `thinking_delta`, and `toolcall_delta` as mergeable events:\n\n- buffered deltas are merged only when **type + contentIndex** match\n- merge keeps the latest `partial` snapshot\n- non-delta events force immediate flush\n\nThis smooths high-frequency provider streams for TUI/event consumers, but is not provider backpressure: providers still produce at full speed, while the local stream buffers.\n\n## Provider normalization details\n\n## Anthropic (`anthropic-messages`)\n\nSource: `packages/ai/src/providers/anthropic.ts`\n\nNormalization points:\n\n- `message_start` initializes usage (input/output/cache tokens)\n- `content_block_start` maps to text/thinking/toolcall starts\n- `content_block_delta` maps:\n - `text_delta` → `text_delta`\n - `thinking_delta` → `thinking_delta`\n - `input_json_delta` → `toolcall_delta`\n - `signature_delta` updates `thinkingSignature` only (no event)\n- `content_block_stop` emits corresponding `*_end`\n- `message_delta.stop_reason` maps via `mapStopReason()`\n\nTool-call argument streaming:\n\n- each tool block carries internal `partialJson`\n- every JSON delta appends to `partialJson`\n- `arguments` are reparsed on each delta via `parseStreamingJson()`\n- `toolcall_end` reparses once more, then strips `partialJson`\n\n## OpenAI Responses family (`openai-responses`, `openai-codex-responses`, `azure-openai-responses`)\n\nSources: `packages/ai/src/providers/openai-responses.ts`, `openai-codex-responses.ts`, and `azure-openai-responses.ts`\n\nNormalization points:\n\n- `response.output_item.added` starts reasoning/text/function-call/custom-tool blocks\n- reasoning summary events (`response.reasoning_summary_text.delta`) and raw reasoning events (`response.reasoning_text.delta`) become `thinking_delta`\n- output/refusal deltas become `text_delta`\n- `response.function_call_arguments.delta` and `response.custom_tool_call_input.delta` become `toolcall_delta`\n- `response.output_item.done` emits `thinking_end` / `text_end` / `toolcall_end`\n- `response.completed` maps status to stop reason and usage; `response.failed` / SDK `error` events throw into the wrapper's terminal `error` path\n\nTool-call argument streaming:\n\n- same `partialJson` accumulation pattern as Anthropic for function-call JSON arguments\n- custom tools stream raw string input and expose final arguments as `{ input: <raw> }`\n- providers that send only `response.function_call_arguments.done` still populate final args\n- tool call IDs are normalized as `\"<call_id>|<item_id>\"`\n\n## Google Generative AI (`google-generative-ai`)\n\nSource: `packages/ai/src/providers/google.ts`\n\nNormalization points:\n\n- iterates `candidate.content.parts`\n- text parts are split into thinking vs text by `isThinkingPart(part)`\n- block transitions close previous block before starting a new one\n- `part.functionCall` is treated as a complete tool call (start/delta/end emitted immediately)\n- finish reason mapped by `mapStopReason()` from `google-shared.ts`\n\nTool-call argument streaming:\n\n- function call args arrive as structured object, not incremental JSON text\n- implementation emits one synthetic `toolcall_delta` containing `JSON.stringify(arguments)`\n- no partial JSON parser needed for Google in this path\n\n## Partial tool-call JSON accumulation and recovery\n\nShared behavior for Anthropic/OpenAI Responses uses `parseStreamingJson()` (`packages/ai/src/utils/json-parse.ts`):\n\n1. try `JSON.parse`\n2. fallback to `partial-json` parser for incomplete fragments\n3. if both fail, return `{}`\n\nImplications:\n\n- malformed or truncated argument deltas do not crash stream processing immediately\n- in-progress `arguments` may temporarily be `{}`\n- later valid deltas can recover structured arguments because parsing is retried on every append\n- final `toolcall_end` performs one more parse attempt before emission\n\n## Stop reasons vs transport/runtime errors\n\nProvider stop reasons are mapped to normalized `stopReason`:\n\n- Anthropic: `end_turn`→`stop`, `max_tokens`→`length`, `tool_use`→`toolUse`, safety/refusal cases→`error`\n- OpenAI Responses: `completed`→`stop`, `incomplete`→`length`, `failed/cancelled`→`error`\n- Google: `STOP`→`stop`, `MAX_TOKENS`→`length`, safety/prohibited/malformed-function-call classes→`error`\n\nError semantics are split in two stages:\n\n1. **Model completion semantics** (provider reported finish reason/status)\n2. **Transport/runtime failure** (network/client/parser/abort exceptions)\n\nIf provider stream throws or signals failure, each provider wrapper catches and emits terminal `error` event with:\n\n- `stopReason = \"aborted\"` when abort signal is set\n- otherwise `stopReason = \"error\"`\n- `errorMessage = formatErrorMessageWithRetryAfter(error)`\n\n## Malformed chunk / SSE parse failure behavior\n\nMost provider paths delegate chunk/SSE framing to vendor SDK streams (Anthropic SDK, OpenAI SDK, Google SDK). The Codex SSE fallback uses `readSseJson()` directly, and websocket Codex frames are normalized through the same event handler.\n\nObserved behavior in current implementation:\n\n- malformed SDK stream parsing surfaces as an exception or stream `error` event\n- malformed Codex SSE JSON/framing throws from the local SSE reader\n- provider wrapper converts failures into unified terminal `error` events\n- no provider-specific resume/retry inside the stream function itself, except Codex websocket-to-SSE transport fallback before replay-unsafe output is emitted\n- higher-level retries are handled in `AgentSession` auto-retry logic (message-level retry, not stream-chunk replay)\n\n## Cancellation boundaries\n\nCancellation is layered:\n\n- AI provider request: `options.signal` is passed into provider client stream call.\n- Provider wrapper: after stream loop, aborted signal forces error path (`\"Request was aborted\"`).\n- Agent loop: checks `signal.aborted` before handling each provider event and can synthesize an aborted assistant message from the latest partial.\n- Session/agent controls: `AgentSession.abort()` -> `agent.abort()` -> shared abort controller cancellation.\n\nTool execution cancellation is separate from model stream cancellation:\n\n- tool runners use `AbortSignal.any([agentSignal, steeringAbortSignal])`\n- steering interrupts can abort remaining tool execution while preserving already-produced tool results\n\n## Backpressure boundaries\n\nThere is no hard backpressure mechanism between provider SDK stream and downstream consumers:\n\n- `EventStream` uses in-memory queues with no max size\n- throttling reduces UI update rate but does not slow provider intake\n- if consumers lag significantly, queued events can grow until completion\n\nCurrent design favors responsiveness and simple ordering over bounded-buffer flow control.\n\n## How stream events surface as agent/session events\n\n`agentLoop.streamAssistantResponse()` bridges `AssistantMessageEvent` to `AgentEvent`:\n\n- on `start`: pushes placeholder assistant message and emits `message_start`\n- on block events (`text_*`, `thinking_*`, `toolcall_*`): updates last assistant message, emits `message_update` with raw `assistantMessageEvent`\n- on terminal (`done`/`error`): resolves final message from `response.result()`, emits `message_end`\n\n`AgentSession` then consumes those events for session-level behaviors:\n\n- TTSR watches `message_update.assistantMessageEvent` for `text_delta`, `thinking_delta`, and `toolcall_delta`\n- streaming edit guard inspects `toolcall_delta`/`toolcall_end` on `edit` calls and can abort early\n- persistence writes finalized messages at `message_end`\n- auto-retry examines assistant `stopReason === \"error\"` plus `errorMessage` heuristics\n\n## Unified vs provider-specific responsibilities\n\nUnified (common contract):\n\n- event shape (`AssistantMessageEvent`)\n- final result extraction (`done`/`error`)\n- delta throttling + merge rules\n- agent/session event propagation model\n\nProvider-specific (not fully abstracted):\n\n- upstream event taxonomies and mapping logic\n- stop-reason translation tables\n- tool-call ID conventions\n- reasoning/thinking block semantics and signatures\n- usage token semantics and availability timing\n- message conversion constraints per API\n\n## Implementation files\n\n- [`../../ai/src/stream.ts`](../packages/ai/src/stream.ts) — provider dispatch, option mapping, API key/session plumbing, custom API dispatch, and provider-specific credential handling.\n- [`../../ai/src/utils/event-stream.ts`](../packages/ai/src/utils/event-stream.ts) — generic stream queue + assistant delta throttling.\n- [`../../ai/src/utils/json-parse.ts`](../packages/ai/src/utils/json-parse.ts) — partial JSON parsing for streamed tool arguments.\n- [`../../ai/src/providers/anthropic.ts`](../packages/ai/src/providers/anthropic.ts) — Anthropic event translation and tool JSON delta accumulation.\n- [`../../ai/src/providers/openai-responses.ts`](../packages/ai/src/providers/openai-responses.ts), [`openai-responses-shared.ts`](../packages/ai/src/providers/openai-responses-shared.ts), [`openai-codex-responses.ts`](../packages/ai/src/providers/openai-codex-responses.ts), [`azure-openai-responses.ts`](../packages/ai/src/providers/azure-openai-responses.ts) — Responses-family event translation and status mapping.\n- [`../../ai/src/providers/google.ts`](../packages/ai/src/providers/google.ts), [`google-gemini-cli.ts`](../packages/ai/src/providers/google-gemini-cli.ts), [`google-vertex.ts`](../packages/ai/src/providers/google-vertex.ts) — Gemini stream chunk-to-block translation variants.\n- [`../../ai/src/providers/google-shared.ts`](../packages/ai/src/providers/google-shared.ts) — Gemini finish-reason mapping and shared conversion rules.\n- [`../../ai/src/providers/amazon-bedrock.ts`](../packages/ai/src/providers/amazon-bedrock.ts), [`openai-completions.ts`](../packages/ai/src/providers/openai-completions.ts), [`ollama.ts`](../packages/ai/src/providers/ollama.ts), [`cursor.ts`](../packages/ai/src/providers/cursor.ts), [`prometheus-native-client.ts`](../packages/ai/src/providers/prometheus-native-client.ts) — additional built-in stream adapters using the same event contract.\n- [`../../agent/src/agent-loop.ts`](../packages/agent/src/agent-loop.ts) — provider stream consumption and `message_update` bridging.\n- [`../src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts) — session-level handling of streaming updates, abort, retry, and persistence.\n",
|
|
50
50
|
"python-repl.md": "# Eval Tool Python Backend\n\nThis document describes the Python execution stack in `packages/coding-agent`.\nIt covers tool behavior, runner lifecycle, environment handling, execution semantics, output rendering, supported magics, and operational failure modes.\n\n## Scope and Key Files\n\n- Tool surface: `src/tools/eval.ts`\n- Session/per-call kernel orchestration: `src/eval/py/executor.ts`\n- Subprocess kernel client: `src/eval/py/kernel.ts`\n- Python wrapper / NDJSON server: `src/eval/py/runner.py`\n- Prelude helpers loaded into every kernel: `src/eval/py/prelude.py`\n- Host-side subagent helper bridge: `src/eval/agent-bridge.ts`\n- MIME bundle renderer (text + structured outputs): `src/eval/py/display.ts`\n- Interactive-mode renderer for user-triggered Python runs: `src/modes/components/eval-execution.ts`\n- Runtime/env filtering and Python resolution: `src/eval/py/runtime.ts`\n\n## What eval's Python backend is\n\nThe `eval` tool executes one or more Python cells inside a retained `python` subprocess that speaks NDJSON over stdin/stdout. No Jupyter gateway and no extra pip dependencies are required — a vanilla Python 3.8+ interpreter is enough. Rich `display()` output (PIL, pandas, plotly, matplotlib figures) keeps working because the wrapper implements MIME-bundle dispatch.\n\nTool params:\n\n```ts\n{\n cells: Array<{\n language: \"py\" | \"js\";\n code: string;\n title?: string;\n timeout?: number; // seconds, clamped to 1..600, default 30. Inactivity budget — see \"Cell timeout\".\n reset?: boolean; // reset this cell's selected runtime before execution\n }>;\n}\n```\n\nThe tool is `concurrency = \"exclusive\"` for a session, so calls do not overlap.\n\n## Kernel lifecycle\n\nEach Python kernel is a single subprocess: `<resolved-python> -u <runner.py>`. The runner is bundled with the host binary (Bun text import), written to an `prometheus-python-runner` cache under the OS temp directory once per script hash, and reused by subsequent spawns.\n\nKernel startup sequence:\n\n1. Availability check (`checkPythonKernelAvailability`) — verifies that a Python interpreter resolves and runs.\n2. Spawn `python -u runner.py` with filtered env and `cwd`.\n3. Send an init request that runs `os.chdir(cwd)`, injects env entries, and adds `cwd` to `sys.path`.\n4. Execute `PYTHON_PRELUDE` (idempotent — only initializes once per process).\n\nKernel shutdown:\n\n- Send `{\"type\": \"exit\"}` over stdin.\n- Wait for process exit with `SHUTDOWN_GRACE_MS` budget.\n- Escalate to `SIGTERM` and finally `SIGKILL` if the process does not exit in time.\n\n## Wire protocol (NDJSON, host ↔ runner)\n\nOne JSON object per line, UTF-8, `\\n` terminated.\n\nHost → runner:\n\n```jsonc\n{\"id\": \"<reqId>\", \"code\": \"<source>\", \"silent\": false, \"storeHistory\": true}\n{\"type\": \"exit\"}\n```\n\nRunner → host:\n\n```jsonc\n{\"type\": \"started\", \"id\": \"<reqId>\"}\n{\"type\": \"stdout\", \"id\": \"<reqId>\", \"data\": \"...\"}\n{\"type\": \"stderr\", \"id\": \"<reqId>\", \"data\": \"...\"}\n{\"type\": \"display\", \"id\": \"<reqId>\", \"bundle\": {<mime>: <value>}}\n{\"type\": \"result\", \"id\": \"<reqId>\", \"bundle\": {<mime>: <value>}}\n{\"type\": \"error\", \"id\": \"<reqId>\", \"ename\": \"...\", \"evalue\": \"...\", \"traceback\": [\"...\"]}\n{\"type\": \"done\", \"id\": \"<reqId>\", \"status\": \"ok\"|\"error\", \"executionCount\": N, \"cancelled\": false}\n```\n\nStatus events the prelude emits (e.g. `_emit_status(\"find\", count=…)`) ship inside display bundles under `application/x-prometheus-status` so the existing TUI status renderer keeps working.\n\n## Magics\n\nThe runner's source transformer rewrites IPython-style magics to plain Python calls before parsing. Supported set:\n\n| Magic | Effect |\n| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `%pip <args>` | `python -m pip <args>` with live streaming output. Newly installed packages are evicted from `sys.modules` so the next `import` picks up the fresh install. |\n| `%cd <path>` | `os.chdir(path)` (with `~` expansion); emits status event. |\n| `%pwd` | Returns `os.getcwd()`. |\n| `%ls [path]` | Returns `sorted(os.listdir(path))`. |\n| `%env [KEY[=VAL]]` | List, read, or set env vars (matches prelude `env()` semantics). |\n| `%set_env KEY VALUE` | Set `os.environ[KEY]`. |\n| `%time <expr>` / `%timeit <expr>` | Time the expression; emits status event with elapsed ms. |\n| `%who` / `%whos` | List user-namespace names. |\n| `%reset` | Clear user globals and re-inject prelude. |\n| `%load <path>` | Read a file into a fresh cell and execute. |\n| `%run <path>` | `runpy.run_path` and merge globals back. |\n| `%%bash` / `%%sh` | Run the cell body via `bash`/`sh`. |\n| `%%capture [name]` | Run body with stdout/stderr captured into `name`. |\n| `%%timeit` | Time the cell body. |\n| `%%writefile <path>` | Write body to file. |\n| `!cmd` / `var = !cmd` | Run command via subprocess shell; returns an SList-style result with `.n` / `.s` helpers. |\n| `var = %name args` | Assignment forms work for line magics and `!cmd`. |\n\nUnknown magic names raise `NameError: UsageError: ...` inside the cell.\n\n## Session persistence semantics\n\n`python.kernelMode` controls retained kernel reuse:\n\n- `session` (default)\n - Reuses kernel sessions keyed by namespaced eval session id plus cwd.\n - Multiple owners can share the same retained kernel for that key.\n - Calls through the tool are exclusive, so tool invocations do not overlap.\n - A dead retained subprocess is replaced before execution.\n - If the subprocess dies during execution, it is replaced and the cell is retried once.\n- `per-call`\n - Spawns a fresh subprocess for each request.\n - Shuts the subprocess down after the request.\n - No cross-call state persistence.\n\n### Multi-cell behavior in a single tool call\n\nPython cells run sequentially in the same selected Python kernel instance for that tool call.\n\nIf an intermediate cell fails:\n\n- Earlier cell state remains in memory.\n- Tool returns a targeted error indicating which cell failed.\n- Later cells are not executed.\n\n`reset=true` is per cell and resets that language runtime before the cell executes.\n\n## Environment filtering and runtime resolution\n\nEnvironment is filtered before launching the runner:\n\n- Allowlist includes core vars like `PATH`, `HOME`, locale vars, `VIRTUAL_ENV`, `PYTHONPATH`, etc.\n- Allow-prefixes: `LC_`, `XDG_`, `PROMETHEUS_`\n- Denylist strips common API keys (OpenAI/Anthropic/Gemini/etc.)\n\nRuntime selection order:\n\n1. Active/located venv (`VIRTUAL_ENV`, then `<cwd>/.venv`, `<cwd>/venv`)\n2. Managed venv at `~/.prometheus/python-env`\n3. `python` or `python3` on PATH\n\nWhen a venv is selected, its bin/Scripts path is prepended to `PATH`.\n\nThe runner additionally receives `PYTHONUNBUFFERED=1` and `PYTHONIOENCODING=utf-8` so streamed output reaches the host promptly.\n\n## Tool availability and mode selection\n\n`eval.py` / `eval.js` (both default `true`) plus optional boolean env flags `PROMETHEUS_PY` / `PROMETHEUS_JS` control eval backend exposure:\n\n- Python backend only (`eval.py=true`, `eval.js=false`, or `PROMETHEUS_PY=1 PROMETHEUS_JS=0`)\n- JavaScript backend only (`eval.py=false`, `eval.js=true`, or `PROMETHEUS_PY=0 PROMETHEUS_JS=1`)\n- both backends (`eval.py=true`, `eval.js=true`, or `PROMETHEUS_PY=1 PROMETHEUS_JS=1`)\n\n`PROMETHEUS_PY` and `PROMETHEUS_JS` use normal boolean flag parsing. If either env var is set, the env pair overrides the per-key settings; an unset member of the pair defaults to enabled.\n\nIf Python preflight fails and `eval.js` is enabled, `eval` remains available for `js` cells; `py` cells fail with a Python-backend availability error.\n\nPython prelude helpers include `agent(prompt, *, agent_type=\"task\", model=None, context=None, label=None, schema=None)`. It synchronously calls the host bridge, runs one subagent through the task executor, and returns the final text. When `schema` is supplied, the helper parses the subagent's JSON output and returns the object.\n\n## Execution flow and cancellation/timeout\n\n### Cell timeout\n\nEach eval cell `timeout` is in seconds, defaults to 30, and is clamped to `1..600`. It is a **wall-clock budget on the cell's own work** that the watchdog (`IdleTimeout`, `src/eval/idle-timeout.ts`) enforces, **but it is paused while a host-side `agent()`/`parallel()`/`llm()` bridge call is in flight**: those calls pump a heartbeat (`withBridgeHeartbeat`, `src/eval/heartbeat.ts`) that re-arms the watchdog, so a long fanout or a slow completion runs to completion instead of being killed mid-stream.\n\nThe heartbeat is the **sole** signal that extends the budget. Everything else the cell does — compute, `stdout`/`stderr`, `log()`/`phase()`, and ordinary (non-agent) tool calls — counts against `timeout`, so a cell that is not delegating to an agent/llm is bounded by a plain wall-clock timeout. The tool combines the caller abort signal, the session abort signal, and the watchdog's signal with `AbortSignal.any(...)`; no wall-clock deadline is passed to the backend, so neither runtime arms a competing fixed timer.\n\n### Kernel execution cancellation\n\nOn abort/timeout:\n\n- The host sends `kill(\"SIGINT\")` to the runner subprocess.\n- The runner's exec-time signal handler raises `KeyboardInterrupt` inside the user code.\n- Result includes `cancelled=true`; the timeout path annotates output as `Command timed out after <n> seconds`.\n- Between requests the runner installs `SIG_IGN` for SIGINT so a stray cancel does not tear down the kernel.\n\nIf a second cancel is required (runner stuck in C code), the host escalates to `SIGTERM` and the session restarts on the next call.\n\n### stdin behavior\n\nInteractive stdin is not supported. The runner does not forward `input()` prompts; user code that calls `input()` blocks until cancellation.\n\n## Output capture and rendering\n\n### Captured output classes\n\nFrom runner frames:\n\n- `stdout` / `stderr` → plain text chunks\n- `display` / `result` → rich display handling (MIME bundle)\n- `error` → traceback text\n- `application/x-prometheus-status` MIME inside `display` → structured status events\n\nDisplay MIME precedence:\n\n1. `text/markdown`\n2. `text/plain`\n3. `text/html` (converted to basic markdown)\n\nAdditionally captured as structured outputs:\n\n- `application/json` → JSON tree data\n- `image/png` / `image/jpeg` → image payloads\n- `application/x-prometheus-status` → status events\n\n### Matplotlib\n\nThe runner sets `MPLBACKEND=Agg` as an environ default so figures render off-screen. After every cell, `pyplot.get_fignums()` is iterated; each figure is saved to PNG, emitted as an `image/png` display, and closed.\n\n### Storage and truncation\n\nOutput is streamed through `OutputSink` and may be persisted to artifact storage. Tool results can include truncation metadata and `artifact://<id>` for full output recovery.\n\n### Renderer behavior\n\n- Tool renderer (`eval.ts`):\n - shows code-cell blocks with per-cell status\n - collapsed preview defaults to 10 lines\n - supports expanded mode for all output retained in the tool result\n- Interactive renderer (`eval-execution.ts`):\n - used for user-triggered Python execution in TUI\n - collapsed preview defaults to 20 lines\n - clamps very long individual lines to 4000 chars for display safety\n - shows cancellation/error/truncation notices\n\n## Operational troubleshooting\n\n- **Python backend not available** — Check `eval.py`, `PROMETHEUS_PY`, and that `python`/`python3` is on PATH. If preflight fails and `eval.js` is enabled, use a `js` cell.\n- **No Python on PATH** — Install a system Python 3.8+ or place a venv at `~/.prometheus/python-env`. `prometheus setup python --check` reports the resolved interpreter.\n- **Execution hangs then times out** — Increase tool `timeout` (max 600s) if workload is legitimate. For stuck native code, cancellation triggers `SIGINT` first then escalates; the session restarts on the next request.\n- **stdin/input prompts in Python code** — `input()` is not supported; pass data programmatically.\n- **Working directory errors** — Tool validates `cwd` exists and is a directory before execution.\n\n## Relevant environment variables\n\n- `PROMETHEUS_PY` / `PROMETHEUS_JS` — eval backend exposure overrides\n- `PROMETHEUS_PYTHON_SKIP_CHECK=1` — bypass Python preflight/warm checks\n- `PROMETHEUS_PYTHON_INTEGRATION=1` — enable gated integration tests that spawn a real Python\n- `PROMETHEUS_PYTHON_IPC_TRACE=1` — log NDJSON frames exchanged with the runner subprocess\n",
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
DEFAULT_TELEGRAM_WEBHOOK_PATH,
|
|
4
4
|
normalizeTelegramBotUsername,
|
|
5
5
|
normalizeWebhookPath,
|
|
6
|
+
parseTelegramAllowedChatIds,
|
|
6
7
|
parseTelegramAllowedUserIds,
|
|
7
8
|
type TelegramGatewayMode,
|
|
8
9
|
} from "../../../cli/gateway-cli";
|
|
@@ -25,7 +26,7 @@ import { shortenPath } from "../../../tools/render-utils";
|
|
|
25
26
|
import { getSelectListTheme, theme } from "../../theme/theme";
|
|
26
27
|
import type { SetupSceneHost, SetupTab } from "./types";
|
|
27
28
|
|
|
28
|
-
const MAX_VISIBLE =
|
|
29
|
+
const MAX_VISIBLE = 16;
|
|
29
30
|
const PARSE_MODES = ["none", "HTML", "Markdown", "MarkdownV2"] as const;
|
|
30
31
|
|
|
31
32
|
type TelegramFieldId =
|
|
@@ -36,6 +37,7 @@ type TelegramFieldId =
|
|
|
36
37
|
| "public-base-url"
|
|
37
38
|
| "webhook-path"
|
|
38
39
|
| "allowed-users"
|
|
40
|
+
| "allowed-chats"
|
|
39
41
|
| "group-scope"
|
|
40
42
|
| "thread-scope"
|
|
41
43
|
| "parse-mode"
|
|
@@ -45,7 +47,13 @@ type TelegramFieldId =
|
|
|
45
47
|
| "register-webhook"
|
|
46
48
|
| "finish";
|
|
47
49
|
|
|
48
|
-
type InputFieldId =
|
|
50
|
+
type InputFieldId =
|
|
51
|
+
| "bot-token"
|
|
52
|
+
| "bot-username"
|
|
53
|
+
| "public-base-url"
|
|
54
|
+
| "webhook-path"
|
|
55
|
+
| "allowed-users"
|
|
56
|
+
| "allowed-chats";
|
|
49
57
|
|
|
50
58
|
interface ActiveInput {
|
|
51
59
|
field: InputFieldId;
|
|
@@ -161,6 +169,7 @@ export class TelegramTab implements SetupTab {
|
|
|
161
169
|
const publicBaseUrl = this.host.ctx.settings.get("gateway.telegram.publicBaseUrl");
|
|
162
170
|
const webhookPath = this.host.ctx.settings.get("gateway.telegram.webhookPath") ?? DEFAULT_TELEGRAM_WEBHOOK_PATH;
|
|
163
171
|
const allowedUsers = this.host.ctx.settings.get("gateway.telegram.allowedUsers");
|
|
172
|
+
const allowedChats = this.host.ctx.settings.get("gateway.telegram.allowedChats");
|
|
164
173
|
const groupScope = this.host.ctx.settings.get("gateway.telegram.groupScope");
|
|
165
174
|
const threadScope = this.host.ctx.settings.get("gateway.telegram.threadScope");
|
|
166
175
|
const parseMode = this.host.ctx.settings.get("gateway.telegram.parseMode");
|
|
@@ -202,7 +211,13 @@ export class TelegramTab implements SetupTab {
|
|
|
202
211
|
{
|
|
203
212
|
value: "allowed-users",
|
|
204
213
|
label: "Allowed users",
|
|
205
|
-
description:
|
|
214
|
+
description:
|
|
215
|
+
allowedUsers || (allowedChats ? "Optional per-user gate" : "Required before activating Telegram"),
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
value: "allowed-chats",
|
|
219
|
+
label: "Allowed chats",
|
|
220
|
+
description: allowedChats || "Optional group/channel chat IDs",
|
|
206
221
|
},
|
|
207
222
|
{
|
|
208
223
|
value: "group-scope",
|
|
@@ -266,6 +281,7 @@ export class TelegramTab implements SetupTab {
|
|
|
266
281
|
case "public-base-url":
|
|
267
282
|
case "webhook-path":
|
|
268
283
|
case "allowed-users":
|
|
284
|
+
case "allowed-chats":
|
|
269
285
|
this.#startInput(id);
|
|
270
286
|
return;
|
|
271
287
|
case "group-scope":
|
|
@@ -373,20 +389,18 @@ export class TelegramTab implements SetupTab {
|
|
|
373
389
|
}
|
|
374
390
|
case "allowed-users": {
|
|
375
391
|
const allowedUsers = parseTelegramAllowedUserIds(value);
|
|
376
|
-
if (!allowedUsers || allowedUsers.length === 0) {
|
|
377
|
-
this.#status = [];
|
|
378
|
-
if (
|
|
379
|
-
!this.#disableGateway(
|
|
380
|
-
"Telegram allowlist is empty; add an allowed user and activate Telegram again.",
|
|
381
|
-
)
|
|
382
|
-
) {
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
392
|
this.#setAllowedUsers(allowedUsers);
|
|
393
|
+
this.#disableGatewayIfAllowlistEmpty();
|
|
387
394
|
this.#status.unshift(theme.fg("success", `${theme.status.success} Telegram allowlist saved`));
|
|
388
395
|
break;
|
|
389
396
|
}
|
|
397
|
+
case "allowed-chats": {
|
|
398
|
+
const allowedChats = parseTelegramAllowedChatIds(value);
|
|
399
|
+
this.#setAllowedChats(allowedChats);
|
|
400
|
+
this.#disableGatewayIfAllowlistEmpty();
|
|
401
|
+
this.#status.unshift(theme.fg("success", `${theme.status.success} Telegram chat allowlist saved`));
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
390
404
|
default:
|
|
391
405
|
field satisfies never;
|
|
392
406
|
}
|
|
@@ -414,6 +428,29 @@ export class TelegramTab implements SetupTab {
|
|
|
414
428
|
}
|
|
415
429
|
}
|
|
416
430
|
|
|
431
|
+
#setAllowedChats(allowedChats: string[] | undefined): void {
|
|
432
|
+
const value = allowedChats?.join(", ") ?? undefined;
|
|
433
|
+
this.host.ctx.settings.set("gateway.telegram.allowedChats", value);
|
|
434
|
+
const effectiveValue = this.host.ctx.settings.get("gateway.telegram.allowedChats");
|
|
435
|
+
if (value) {
|
|
436
|
+
if (effectiveValue !== value) {
|
|
437
|
+
this.host.ctx.settings.override("gateway.telegram.allowedChats", value);
|
|
438
|
+
}
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (effectiveValue) {
|
|
442
|
+
this.host.ctx.settings.override("gateway.telegram.allowedChats", "");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
#disableGatewayIfAllowlistEmpty(): boolean {
|
|
447
|
+
if (this.#hasAllowedPrincipal()) return true;
|
|
448
|
+
this.#status = [];
|
|
449
|
+
return this.#disableGateway(
|
|
450
|
+
"Telegram allowlist is empty; add an allowed user or chat and activate Telegram again.",
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
417
454
|
#setGatewayEnabled(enabled: boolean): boolean {
|
|
418
455
|
this.host.ctx.settings.set("gateway.telegram.enabled", enabled);
|
|
419
456
|
if (this.host.ctx.settings.get("gateway.telegram.enabled") !== enabled) {
|
|
@@ -525,8 +562,8 @@ export class TelegramTab implements SetupTab {
|
|
|
525
562
|
this.host.requestRender();
|
|
526
563
|
return;
|
|
527
564
|
}
|
|
528
|
-
const
|
|
529
|
-
if (!
|
|
565
|
+
const allowedPrincipals = this.#requireAllowedPrincipals();
|
|
566
|
+
if (!allowedPrincipals) return;
|
|
530
567
|
|
|
531
568
|
await this.#runBusy(async signal => {
|
|
532
569
|
await deleteTelegramWebhook(token, { dropPendingUpdates: false, signal, fetch: this.deps.fetch });
|
|
@@ -538,7 +575,7 @@ export class TelegramTab implements SetupTab {
|
|
|
538
575
|
}
|
|
539
576
|
this.#status = [
|
|
540
577
|
theme.fg("success", `${theme.status.success} Telegram polling activated`),
|
|
541
|
-
theme.fg("dim",
|
|
578
|
+
theme.fg("dim", this.#formatAllowedPrincipals(allowedPrincipals)),
|
|
542
579
|
theme.fg("dim", "Gateway serve will use getUpdates."),
|
|
543
580
|
];
|
|
544
581
|
});
|
|
@@ -557,8 +594,8 @@ export class TelegramTab implements SetupTab {
|
|
|
557
594
|
this.host.requestRender();
|
|
558
595
|
return;
|
|
559
596
|
}
|
|
560
|
-
const
|
|
561
|
-
if (!
|
|
597
|
+
const allowedPrincipals = this.#requireAllowedPrincipals();
|
|
598
|
+
if (!allowedPrincipals) return;
|
|
562
599
|
|
|
563
600
|
await this.#runBusy(async signal => {
|
|
564
601
|
const webhookPath = normalizeWebhookPath(this.host.ctx.settings.get("gateway.telegram.webhookPath"));
|
|
@@ -578,7 +615,7 @@ export class TelegramTab implements SetupTab {
|
|
|
578
615
|
}
|
|
579
616
|
this.#status = [
|
|
580
617
|
theme.fg("success", `${theme.status.success} Telegram webhook registered`),
|
|
581
|
-
theme.fg("dim",
|
|
618
|
+
theme.fg("dim", this.#formatAllowedPrincipals(allowedPrincipals)),
|
|
582
619
|
theme.fg("dim", `URL: ${info.url || webhookUrl}`),
|
|
583
620
|
theme.fg("dim", `Pending updates: ${info.pending_update_count}`),
|
|
584
621
|
];
|
|
@@ -627,12 +664,15 @@ export class TelegramTab implements SetupTab {
|
|
|
627
664
|
this.host.requestRender();
|
|
628
665
|
}
|
|
629
666
|
|
|
630
|
-
#
|
|
667
|
+
#requireAllowedPrincipals(): { users: string[]; chats: string[] } | undefined {
|
|
631
668
|
try {
|
|
632
669
|
const allowedUsers = parseTelegramAllowedUserIds(this.host.ctx.settings.get("gateway.telegram.allowedUsers"));
|
|
633
|
-
|
|
670
|
+
const allowedChats = parseTelegramAllowedChatIds(this.host.ctx.settings.get("gateway.telegram.allowedChats"));
|
|
671
|
+
if ((allowedUsers?.length ?? 0) > 0 || (allowedChats?.length ?? 0) > 0) {
|
|
672
|
+
return { users: allowedUsers ?? [], chats: allowedChats ?? [] };
|
|
673
|
+
}
|
|
634
674
|
this.#status = [
|
|
635
|
-
theme.fg("warning", `${theme.status.pending} Add at least one allowed Telegram user ID first`),
|
|
675
|
+
theme.fg("warning", `${theme.status.pending} Add at least one allowed Telegram user or chat ID first`),
|
|
636
676
|
];
|
|
637
677
|
} catch (error) {
|
|
638
678
|
this.#status = [theme.fg("error", `${theme.status.error} ${formatError(error)}`)];
|
|
@@ -641,6 +681,17 @@ export class TelegramTab implements SetupTab {
|
|
|
641
681
|
return undefined;
|
|
642
682
|
}
|
|
643
683
|
|
|
684
|
+
#hasAllowedPrincipal(): boolean {
|
|
685
|
+
return (
|
|
686
|
+
(this.host.ctx.settings.get("gateway.telegram.allowedUsers")?.trim().length ?? 0) > 0 ||
|
|
687
|
+
(this.host.ctx.settings.get("gateway.telegram.allowedChats")?.trim().length ?? 0) > 0
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
#formatAllowedPrincipals(principals: { users: string[]; chats: string[] }): string {
|
|
692
|
+
return `Authorized users: ${principals.users.length}; chats: ${principals.chats.length}`;
|
|
693
|
+
}
|
|
694
|
+
|
|
644
695
|
async #refreshSecretStatus(): Promise<void> {
|
|
645
696
|
const [botToken, webhookSecret] = await Promise.all([
|
|
646
697
|
readTelegramGatewaySecret("bot-token"),
|
|
@@ -665,6 +716,8 @@ export class TelegramTab implements SetupTab {
|
|
|
665
716
|
return this.host.ctx.settings.get("gateway.telegram.webhookPath") ?? DEFAULT_TELEGRAM_WEBHOOK_PATH;
|
|
666
717
|
case "allowed-users":
|
|
667
718
|
return this.host.ctx.settings.get("gateway.telegram.allowedUsers") ?? "";
|
|
719
|
+
case "allowed-chats":
|
|
720
|
+
return this.host.ctx.settings.get("gateway.telegram.allowedChats") ?? "";
|
|
668
721
|
default:
|
|
669
722
|
field satisfies never;
|
|
670
723
|
return "";
|
|
@@ -683,6 +736,8 @@ export class TelegramTab implements SetupTab {
|
|
|
683
736
|
return "webhook path";
|
|
684
737
|
case "allowed-users":
|
|
685
738
|
return "allowed Telegram user IDs";
|
|
739
|
+
case "allowed-chats":
|
|
740
|
+
return "allowed Telegram chat IDs";
|
|
686
741
|
default:
|
|
687
742
|
field satisfies never;
|
|
688
743
|
return "value";
|