arisa 5.1.13 → 5.1.49
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/AGENTS.md +4 -0
- package/README.md +2 -0
- package/package.json +8 -10
- package/src/core/agent/agent-manager.js +132 -70
- package/src/core/agent/pi-runtime.js +0 -8
- package/src/core/agent/system-shell-tool.js +13 -2
- package/src/core/artifacts/artifact-store.js +17 -18
- package/src/core/config/config-defaults.js +2 -2
- package/src/core/conversation/session-seed-store.js +85 -0
- package/src/core/tools/ipc-client.js +0 -2
- package/src/core/tools/official-tool-installer.js +78 -6
- package/src/core/tools/tool-dependencies.js +99 -0
- package/src/core/tools/tool-output-materializer.js +41 -0
- package/src/core/tools/tool-registry.js +145 -28
- package/src/official-tools.lock.json +209 -5
- package/src/runtime/arisa-capabilities.js +12 -1
- package/src/runtime/create-app.js +1 -0
- package/src/runtime/doctor.js +72 -23
- package/src/runtime/headless-tool-executor.js +2 -32
- package/src/runtime/paths.js +7 -1
- package/src/runtime/restart-receipt.js +90 -0
- package/src/runtime/tool-usage-report.js +25 -10
- package/src/transport/telegram/bot.js +403 -1015
- package/src/transport/telegram/chat-queue.js +132 -0
- package/src/transport/telegram/media.js +2 -2
- package/src/transport/telegram/model-callback.js +211 -0
- package/src/transport/telegram/model-controls.js +164 -0
- package/src/transport/telegram/prompt-builders.js +372 -0
- package/src/transport/telegram/task-dispatcher.js +94 -0
- package/src/transport/telegram/update-command.js +1 -1
- package/src/transport/telegram/workspace-group.js +83 -0
- package/test/agent-tool-policy.test.js +7 -1
- package/test/capabilities-security.test.js +21 -0
- package/test/context-and-task-bounds.test.js +33 -5
- package/test/doctor.test.js +57 -4
- package/test/model-selection.test.js +47 -1
- package/test/official-tool-dependencies.test.js +25 -0
- package/test/official-tool-installer.test.js +37 -9
- package/test/paths.test.js +4 -4
- package/test/restart-receipt.test.js +39 -0
- package/test/session-start-operational-notes.test.js +47 -0
- package/test/telegram-prompt-builders.test.js +33 -0
- package/test/telegram-task-dispatcher.test.js +102 -0
- package/test/telegram-workspace-group.test.js +76 -0
- package/test/tool-dependencies.test.js +53 -0
- package/test/tool-registry-run.test.js +81 -1
- package/test/tool-usage.test.js +26 -4
- package/test/topic-initialization.test.js +66 -0
- package/src/core/conversation/conversation-history-store.js +0 -142
|
@@ -1,25 +1,86 @@
|
|
|
1
1
|
import { Bot, InputFile } from "grammy";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { authorizeChat } from "./auth.js";
|
|
4
|
-
import { captureIncomingArtifact
|
|
4
|
+
import { captureIncomingArtifact } from "./media.js";
|
|
5
5
|
import { buildDeviceCodeTelegramMessage } from "./device-code-message.js";
|
|
6
|
-
import { buildEffortPicker, buildModelPicker, buildSpeedPicker, parseEffortPickerAction, parseModelPickerAction, parseSpeedPickerAction, reverseModelOrder } from "./model-picker.js";
|
|
7
6
|
import { renderTelegramHtml } from "./text-format.js";
|
|
8
7
|
import { buildPiAuthRecoveryBlockedMessage, buildPiAuthTelegramMessage, getErrorMessage, getPiAuthIssue, getPiAuthStatus } from "../../core/agent/auth-flow.js";
|
|
9
8
|
import { createPiOAuthLogin } from "../../core/agent/pi-auth-login.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { clampModelSpeed, MODEL_SPEEDS, modelSupportsSpeed } from "../../core/agent/model-speed.js";
|
|
13
|
-
import { normalizeArtifactForReasoning, shouldNormalizeArtifactToText } from "../../core/artifacts/normalize-for-reasoning.js";
|
|
14
|
-
import { formatPortableSessionHistory } from "../../core/agent/agent-manager.js";
|
|
15
|
-
import { ConversationHistoryStore } from "../../core/conversation/conversation-history-store.js";
|
|
9
|
+
import { resolveChatSpeed } from "../../core/agent/model-selection.js";
|
|
10
|
+
import { SessionSeedStore } from "../../core/conversation/session-seed-store.js";
|
|
16
11
|
import { formatDoctorReport } from "../../runtime/doctor.js";
|
|
17
12
|
import { formatToolUsageReport } from "../../runtime/tool-usage-report.js";
|
|
18
13
|
import { ToolResourceNoteStore } from "../../core/tools/tool-resource-note-store.js";
|
|
19
14
|
import { formatUpdateReport } from "../../runtime/update-manager.js";
|
|
15
|
+
import { cancelRestartReceipt, deliverRestartReceipt, prepareRestartReceipt } from "../../runtime/restart-receipt.js";
|
|
20
16
|
import { buildUpdatePicker, createTelegramUpdateCallbackHandler } from "./update-command.js";
|
|
17
|
+
import { createTelegramModelControls } from "./model-controls.js";
|
|
18
|
+
import { createTelegramModelCallbackHandler } from "./model-callback.js";
|
|
19
|
+
import { createTelegramTaskDispatcher } from "./task-dispatcher.js";
|
|
20
|
+
import { resolveTelegramWorkspaceRoute, topicSessionId } from "./workspace-group.js";
|
|
21
|
+
import {
|
|
22
|
+
buildNewSessionPrompt,
|
|
23
|
+
buildPrompt,
|
|
24
|
+
buildReactionPrompt,
|
|
25
|
+
buildSessionHandoffPrompt,
|
|
26
|
+
buildStartupMessage,
|
|
27
|
+
collectText,
|
|
28
|
+
getIncomingMessageText,
|
|
29
|
+
isScheduledTaskPrompt,
|
|
30
|
+
isSilentReply,
|
|
31
|
+
normalizeIncomingArtifact,
|
|
32
|
+
sanitizeSessionHandoff,
|
|
33
|
+
shouldIncludeArtifactReference,
|
|
34
|
+
withPromptSpeed
|
|
35
|
+
} from "./prompt-builders.js";
|
|
36
|
+
import {
|
|
37
|
+
createChatStateStore,
|
|
38
|
+
drainChatPromptQueue,
|
|
39
|
+
queueChatPrompt,
|
|
40
|
+
resolveTelegramBusyMessageMode,
|
|
41
|
+
routeBusyPrompt
|
|
42
|
+
} from "./chat-queue.js";
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
createChatStateStore,
|
|
46
|
+
drainChatPromptQueue,
|
|
47
|
+
queueChatPrompt,
|
|
48
|
+
resolveTelegramBusyMessageMode,
|
|
49
|
+
routeBusyPrompt
|
|
50
|
+
} from "./chat-queue.js";
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
buildAsyncTaskPrompt,
|
|
54
|
+
buildPrompt,
|
|
55
|
+
buildReactionPrompt,
|
|
56
|
+
collectText,
|
|
57
|
+
isScheduledTaskPrompt,
|
|
58
|
+
isSilentReply,
|
|
59
|
+
shouldIncludeArtifactReference,
|
|
60
|
+
withPromptSpeed
|
|
61
|
+
} from "./prompt-builders.js";
|
|
62
|
+
|
|
63
|
+
export function isProcessableTelegramMessage(message = {}) {
|
|
64
|
+
return Boolean(
|
|
65
|
+
String(message.text || "").trim()
|
|
66
|
+
|| message.voice
|
|
67
|
+
|| message.audio
|
|
68
|
+
|| message.video
|
|
69
|
+
|| message.document
|
|
70
|
+
|| message.photo?.length
|
|
71
|
+
|| message.location
|
|
72
|
+
|| message.venue
|
|
73
|
+
);
|
|
74
|
+
}
|
|
21
75
|
|
|
22
|
-
|
|
76
|
+
export function buildTopicInitializationHandoff({ name, context }) {
|
|
77
|
+
return [
|
|
78
|
+
`Telegram topic: ${String(name || "").trim()}`,
|
|
79
|
+
"This topic has an isolated conversation session.",
|
|
80
|
+
"Use the following as background context. Do not repeat it unless the user asks.",
|
|
81
|
+
String(context || "").trim()
|
|
82
|
+
].filter(Boolean).join("\n\n");
|
|
83
|
+
}
|
|
23
84
|
|
|
24
85
|
export const telegramCommands = Object.freeze([
|
|
25
86
|
{ command: "new", description: "New chat context" },
|
|
@@ -51,7 +112,7 @@ export function createTelegramRestartHandler({ authorize, requestRestart, logger
|
|
|
51
112
|
restartRequested = true;
|
|
52
113
|
try {
|
|
53
114
|
await ctx.reply("Arisa is restarting. I'll be back shortly.");
|
|
54
|
-
const handoff = await requestRestart();
|
|
115
|
+
const handoff = await requestRestart(ctx);
|
|
55
116
|
logger?.log("telegram", `restart handed off to process ${handoff.pid}`);
|
|
56
117
|
} catch (error) {
|
|
57
118
|
restartRequested = false;
|
|
@@ -61,35 +122,6 @@ export function createTelegramRestartHandler({ authorize, requestRestart, logger
|
|
|
61
122
|
};
|
|
62
123
|
}
|
|
63
124
|
|
|
64
|
-
function quotedMessageSummary(message) {
|
|
65
|
-
if (!message) return [];
|
|
66
|
-
|
|
67
|
-
const fromName = message.from?.username
|
|
68
|
-
? `@${message.from.username}`
|
|
69
|
-
: [message.from?.first_name, message.from?.last_name].filter(Boolean).join(" ") || "unknown";
|
|
70
|
-
|
|
71
|
-
const parts = [
|
|
72
|
-
`quotedMessageId: ${message.message_id}`,
|
|
73
|
-
`quotedFrom: ${fromName}`
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
if (message.text) parts.push(`quotedText: ${message.text}`);
|
|
77
|
-
if (message.caption) parts.push(`quotedCaption: ${message.caption}`);
|
|
78
|
-
if (message.voice) parts.push(`quotedKind: voice`);
|
|
79
|
-
if (message.audio) parts.push(`quotedKind: audio`);
|
|
80
|
-
if (message.photo?.length) parts.push(`quotedKind: image`);
|
|
81
|
-
if (message.document) parts.push(`quotedKind: document`);
|
|
82
|
-
if (message.video) parts.push(`quotedKind: video`);
|
|
83
|
-
if (message.sticker) parts.push(`quotedKind: sticker`);
|
|
84
|
-
if (message.location) parts.push(`quotedKind: location`, `quotedLocation: ${formatLocationText(message)}`);
|
|
85
|
-
|
|
86
|
-
if (!message.text && !message.caption) {
|
|
87
|
-
parts.push(`Important: this message replies to a Telegram message with no textual body available in the update. Use the quoted kind and metadata as context.`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return parts;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
125
|
function getTelegramCommand(ctx) {
|
|
94
126
|
const text = ctx.message?.text || "";
|
|
95
127
|
const entity = ctx.message?.entities?.[0];
|
|
@@ -97,513 +129,73 @@ function getTelegramCommand(ctx) {
|
|
|
97
129
|
return text.slice(1, entity.length).split("@")[0].trim().toLowerCase();
|
|
98
130
|
}
|
|
99
131
|
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (entity.username) return `@${entity.username}`;
|
|
106
|
-
return [entity.first_name, entity.last_name].filter(Boolean).join(" ") || entity.title || "unknown";
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function forwardedMessageSummary(message) {
|
|
110
|
-
const origin = message?.forward_origin;
|
|
111
|
-
if (!origin) return [];
|
|
112
|
-
|
|
113
|
-
const parts = ["forwarded: true", `forwardedOriginType: ${origin.type}`];
|
|
114
|
-
if (origin.type === "user") parts.push(`forwardedFrom: ${telegramDisplayName(origin.sender_user)}`);
|
|
115
|
-
if (origin.type === "hidden_user") parts.push(`forwardedFrom: ${origin.sender_user_name}`);
|
|
116
|
-
if (origin.type === "chat" || origin.type === "channel") {
|
|
117
|
-
parts.push(`forwardedFrom: ${telegramDisplayName(origin.chat)}`);
|
|
118
|
-
}
|
|
119
|
-
if (origin.type === "channel" && origin.message_id) parts.push(`forwardedMessageId: ${origin.message_id}`);
|
|
120
|
-
if (origin.author_signature) parts.push(`forwardedAuthorSignature: ${origin.author_signature}`);
|
|
121
|
-
if (origin.date) parts.push(`forwardedAt: ${new Date(origin.date * 1000).toISOString()}`);
|
|
122
|
-
return parts;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function reactionLabel(reaction = {}) {
|
|
126
|
-
if (reaction.type === "emoji") return reaction.emoji || "emoji";
|
|
127
|
-
if (reaction.type === "custom_emoji") return `custom:${reaction.custom_emoji_id || "unknown"}`;
|
|
128
|
-
if (reaction.type === "paid") return "paid";
|
|
129
|
-
return reaction.type || "unknown";
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function reactionDifference(left = [], right = []) {
|
|
133
|
-
const remaining = right.map(reactionLabel);
|
|
134
|
-
return left.map(reactionLabel).filter((label) => {
|
|
135
|
-
const index = remaining.indexOf(label);
|
|
136
|
-
if (index < 0) return true;
|
|
137
|
-
remaining.splice(index, 1);
|
|
138
|
-
return false;
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function buildReactionPrompt({ reaction, reactedMessageText = "" }) {
|
|
143
|
-
const oldReactions = reaction.old_reaction || [];
|
|
144
|
-
const newReactions = reaction.new_reaction || [];
|
|
145
|
-
const added = reactionDifference(newReactions, oldReactions);
|
|
146
|
-
const removed = reactionDifference(oldReactions, newReactions);
|
|
147
|
-
const actor = reaction.user || reaction.actor_chat || {};
|
|
148
|
-
const actorId = reaction.user?.id || reaction.actor_chat?.id || "unknown";
|
|
149
|
-
|
|
150
|
-
return [
|
|
151
|
-
"Incoming Telegram reaction.",
|
|
152
|
-
`chatId: ${reaction.chat.id}`,
|
|
153
|
-
`userId: ${actorId}`,
|
|
154
|
-
`username: ${reaction.user?.username || "(no username)"}`,
|
|
155
|
-
`reactedMessageId: ${reaction.message_id}`,
|
|
156
|
-
reactedMessageText ? `reactedMessageText: ${reactedMessageText}` : null,
|
|
157
|
-
added.length ? `addedReactions: ${added.join(" ")}` : null,
|
|
158
|
-
removed.length ? `removedReactions: ${removed.join(" ")}` : null,
|
|
159
|
-
`currentReactions: ${newReactions.map(reactionLabel).join(" ") || "none"}`,
|
|
160
|
-
`actor: ${telegramDisplayName(actor)}`,
|
|
161
|
-
"Treat this as lightweight feedback on the referenced message. Respond only if the reaction clearly requests action; otherwise stay silent."
|
|
162
|
-
].filter(Boolean).join("\n");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function baseMimeType(mimeType = "") {
|
|
166
|
-
return mimeType.split(";")[0].trim().toLowerCase();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function isInlineTextArtifact(artifact, messageText) {
|
|
170
|
-
return artifact?.kind === "text"
|
|
171
|
-
&& baseMimeType(artifact.mimeType) === "text/plain"
|
|
172
|
-
&& typeof artifact.text === "string"
|
|
173
|
-
&& artifact.text === messageText;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function shouldIncludeArtifactReference({ artifact, messageText = "" } = {}) {
|
|
177
|
-
if (!artifact) return false;
|
|
178
|
-
return !isInlineTextArtifact(artifact, messageText);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function buildPrompt({ ctx, artifact, transcript, toolResult }) {
|
|
182
|
-
const parts = [
|
|
183
|
-
`Incoming Telegram message.`,
|
|
184
|
-
`chatId: ${ctx.chat.id}`,
|
|
185
|
-
`userId: ${ctx.from.id}`,
|
|
186
|
-
`username: ${ctx.from.username || "(no username)"}`,
|
|
187
|
-
`messageId: ${ctx.msg.message_id}`
|
|
188
|
-
];
|
|
189
|
-
|
|
190
|
-
const messageText = getIncomingMessageText(ctx.message);
|
|
191
|
-
if (messageText) parts.push(`text: ${messageText}`);
|
|
192
|
-
parts.push(...forwardedMessageSummary(ctx.message));
|
|
193
|
-
parts.push(...quotedMessageSummary(ctx.message?.reply_to_message));
|
|
194
|
-
if (shouldIncludeArtifactReference({ artifact, messageText })) {
|
|
195
|
-
if (artifact?.path) parts.push(`artifactPath: ${artifact.path}`);
|
|
196
|
-
if (artifact?.id) parts.push(`artifactId: ${artifact.id}`);
|
|
197
|
-
if (artifact?.mimeType) parts.push(`mimeType: ${artifact.mimeType}`);
|
|
198
|
-
if (artifact?.kind) parts.push(`kind: ${artifact.kind}`);
|
|
199
|
-
}
|
|
200
|
-
if (transcript) {
|
|
201
|
-
parts.push(`transcriptArtifactId: ${transcript.id}`);
|
|
202
|
-
parts.push(`transcriptText: ${transcript.text}`);
|
|
203
|
-
parts.push(`Important: the incoming media has already been transcribed. Use the transcript as the user message content. Do not answer with a raw transcription unless the user explicitly asked for one.`);
|
|
204
|
-
}
|
|
205
|
-
if (shouldNormalizeArtifactToText(artifact) && !transcript && toolResult) {
|
|
206
|
-
parts.push(`mediaNormalizationResult: ${JSON.stringify(toolResult)}`);
|
|
207
|
-
parts.push(`Important: pre-reasoning media normalization could not be completed, so you do not have a transcript for this audio/video message.`);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
parts.push(`Use read/write/edit for file work in the active workspace, bash for bash-compatible commands, and system_shell for native system commands such as PowerShell on Windows.`);
|
|
211
|
-
parts.push(`If you need an Arisa modular CLI tool, use list_tools/tool_help/run_tool.`);
|
|
212
|
-
parts.push(`If a tool config is missing, ask the user naturally and then use set_tool_config.`);
|
|
213
|
-
parts.push(`To deliver a file to the chat: run_tool with deliver:true to generate and send in one step, or send_artifact with an existing artifactId (e.g. an inbound file).`);
|
|
214
|
-
return parts.join("\n");
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function buildNewSessionPrompt(ctx) {
|
|
218
|
-
return [
|
|
219
|
-
"System event: /new requested.",
|
|
220
|
-
"Session was reset.",
|
|
221
|
-
`preferredTelegramLanguageCode: ${ctx.from?.language_code || "unknown"}`,
|
|
222
|
-
"Reply with a brief, warm confirmation in the user's language."
|
|
223
|
-
].join("\n");
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export function isScheduledTaskPrompt(prompt) {
|
|
227
|
-
return String(prompt || "").startsWith("Scheduled task fired.\n");
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export async function withPromptSpeed({ speedController, speed, restoreSpeed }, work) {
|
|
231
|
-
if (!speedController || speed === undefined) return work();
|
|
232
|
-
speedController.setSpeed(speed);
|
|
233
|
-
try {
|
|
234
|
-
return await work();
|
|
235
|
-
} finally {
|
|
236
|
-
speedController.setSpeed(restoreSpeed());
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export async function buildAsyncTaskPrompt({ task, artifactStore, toolRegistry, resourceNotes, logger }) {
|
|
241
|
-
const taskText = task.payload.prompt || "";
|
|
242
|
-
const resourceId = String(task.source?.resourceId || task.payload?.resourceId || "").trim();
|
|
243
|
-
const resourceNote = resourceId && task.source?.toolName
|
|
244
|
-
? await resourceNotes.get(task.payload.chatId, task.source.toolName, resourceId)
|
|
245
|
-
: "";
|
|
246
|
-
const parts = [
|
|
247
|
-
"Scheduled task fired.",
|
|
248
|
-
`taskId: ${task.id}`,
|
|
249
|
-
`chatId: ${task.payload.chatId}`,
|
|
250
|
-
resourceNote ? `resourceNote: ${resourceNote}` : null,
|
|
251
|
-
taskText ? `text: ${taskText}` : null
|
|
252
|
-
];
|
|
253
|
-
|
|
254
|
-
if (task.payload.artifactId) {
|
|
255
|
-
const chatArtifactStore = artifactStore.forChat(task.payload.chatId);
|
|
256
|
-
const artifact = await chatArtifactStore.get(task.payload.artifactId);
|
|
257
|
-
if (artifact) {
|
|
258
|
-
if (shouldIncludeArtifactReference({ artifact, messageText: taskText })) {
|
|
259
|
-
parts.push(`artifactPath: ${artifact.path || ""}`);
|
|
260
|
-
parts.push(`artifactId: ${artifact.id}`);
|
|
261
|
-
parts.push(`mimeType: ${artifact.mimeType}`);
|
|
262
|
-
parts.push(`kind: ${artifact.kind}`);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const { normalizedArtifact, toolResult } = await normalizeArtifactForReasoning({
|
|
266
|
-
artifact,
|
|
267
|
-
desiredMimeType: "text/plain",
|
|
268
|
-
toolRegistry,
|
|
269
|
-
chatArtifactStore,
|
|
270
|
-
chatId: task.payload.chatId
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
if (normalizedArtifact) {
|
|
274
|
-
logger?.log("tasks", `artifact ${artifact.id} normalized to ${normalizedArtifact.id}`);
|
|
275
|
-
parts.push(`transcriptArtifactId: ${normalizedArtifact.id}`);
|
|
276
|
-
parts.push(`transcriptText: ${normalizedArtifact.text}`);
|
|
277
|
-
parts.push("Important: the attached media artifact has already been normalized for reasoning. Use the transcript as the message content.");
|
|
278
|
-
} else if (shouldNormalizeArtifactToText(artifact) && toolResult) {
|
|
279
|
-
parts.push(`mediaNormalizationResult: ${JSON.stringify(toolResult)}`);
|
|
280
|
-
parts.push("Important: pre-reasoning media normalization could not be completed, so you do not have a transcript for this audio/video artifact.");
|
|
281
|
-
}
|
|
282
|
-
} else {
|
|
283
|
-
parts.push(`artifactId: ${task.payload.artifactId}`);
|
|
284
|
-
parts.push("Important: referenced artifact was not found.");
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
parts.push("Treat this as a new request for the chat and fulfill it now.");
|
|
289
|
-
parts.push("If needed, use read/write/edit, bash, system_shell, or Arisa modular tools via run_tool.");
|
|
290
|
-
return parts.filter(Boolean).join("\n");
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
async function buildAsyncEventPrompt(task, resourceNotes) {
|
|
294
|
-
const resourceId = String(task.source?.resourceId || task.payload?.resourceId || "").trim();
|
|
295
|
-
const resourceNote = resourceId && task.source?.toolName
|
|
296
|
-
? await resourceNotes.get(task.payload.chatId, task.source.toolName, resourceId)
|
|
297
|
-
: "";
|
|
298
|
-
return [
|
|
299
|
-
"External event arrived.",
|
|
300
|
-
`taskId: ${task.id}`,
|
|
301
|
-
`chatId: ${task.payload.chatId}`,
|
|
302
|
-
resourceNote ? `resourceNote: ${resourceNote}` : null,
|
|
303
|
-
task.payload.prompt ? `event: ${task.payload.prompt}` : null,
|
|
304
|
-
"A polling checker detected this external event. Evaluate it and decide the next action.",
|
|
305
|
-
"If it warrants no action, you may stay silent.",
|
|
306
|
-
"If needed, use read/write/edit, bash, system_shell, or Arisa modular tools via run_tool."
|
|
307
|
-
].filter(Boolean).join("\n");
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
async function normalizeIncomingArtifact({ artifact, toolRegistry, chatArtifactStore, chatId }) {
|
|
311
|
-
if (!artifact) return { transcript: null, toolResult: null };
|
|
312
|
-
const { normalizedArtifact, toolResult } = await normalizeArtifactForReasoning({
|
|
313
|
-
artifact,
|
|
314
|
-
desiredMimeType: "text/plain",
|
|
315
|
-
toolRegistry,
|
|
316
|
-
chatArtifactStore,
|
|
317
|
-
chatId
|
|
318
|
-
});
|
|
319
|
-
return { transcript: normalizedArtifact, toolResult };
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function sessionEventLogMessage(event) {
|
|
323
|
-
if (event.type === "tool_execution_start") {
|
|
324
|
-
return `tool ${event.toolName} started`;
|
|
325
|
-
}
|
|
326
|
-
if (event.type === "tool_execution_end") {
|
|
327
|
-
return `tool ${event.toolName} ${event.isError ? "failed" : "finished"}`;
|
|
328
|
-
}
|
|
329
|
-
if (event.type === "auto_retry_start") {
|
|
330
|
-
return `auto retry ${event.attempt}/${event.maxAttempts} in ${event.delayMs}ms: ${event.errorMessage}`;
|
|
331
|
-
}
|
|
332
|
-
if (event.type === "auto_retry_end") {
|
|
333
|
-
return event.success
|
|
334
|
-
? `auto retry succeeded after ${event.attempt} attempt(s)`
|
|
335
|
-
: `auto retry failed after ${event.attempt} attempt(s): ${event.finalError || "unknown error"}`;
|
|
336
|
-
}
|
|
337
|
-
if (event.type === "compaction_start") {
|
|
338
|
-
return `compaction started (${event.reason})`;
|
|
339
|
-
}
|
|
340
|
-
if (event.type === "compaction_end") {
|
|
341
|
-
return `compaction ${event.aborted ? "aborted" : "finished"} (${event.reason})`;
|
|
342
|
-
}
|
|
343
|
-
if (event.type === "message_end" && event.message?.stopReason === "error") {
|
|
344
|
-
return `assistant message ended with error: ${event.message.errorMessage || "unknown error"}`;
|
|
345
|
-
}
|
|
346
|
-
return "";
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function buildStartupMessage(chatMeta = {}) {
|
|
350
|
-
const languageCode = String(chatMeta.languageCode || "").toLowerCase();
|
|
351
|
-
if (languageCode.startsWith("es")) return "Arisa esta en linea de nuevo.";
|
|
352
|
-
if (languageCode.startsWith("pt")) return "Arisa esta online de novo.";
|
|
353
|
-
return "Arisa is back online.";
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
export async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {}) {
|
|
357
|
-
const assistantMessages = [];
|
|
358
|
-
let assistantMessage = "";
|
|
359
|
-
let assistantErrorMessage = "";
|
|
360
|
-
let slowPromptTimer = null;
|
|
361
|
-
const finishAssistantMessage = () => {
|
|
362
|
-
if (assistantMessage && !isSilentReply(assistantMessage)) {
|
|
363
|
-
assistantMessages.push(assistantMessage);
|
|
364
|
-
}
|
|
365
|
-
assistantMessage = "";
|
|
366
|
-
};
|
|
367
|
-
const unsubscribe = session.subscribe((event) => {
|
|
368
|
-
if (event.arisaPromptScoped === false) return;
|
|
369
|
-
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
370
|
-
finishAssistantMessage();
|
|
371
|
-
}
|
|
372
|
-
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
373
|
-
assistantMessage += event.assistantMessageEvent.delta;
|
|
374
|
-
}
|
|
375
|
-
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
376
|
-
if (event.message.stopReason === "error") {
|
|
377
|
-
assistantErrorMessage = event.message.errorMessage || "assistant message ended with error";
|
|
378
|
-
} else if (event.message.stopReason !== "aborted") {
|
|
379
|
-
// Auto-compaction and retry can emit a transient error before a successful continuation.
|
|
380
|
-
assistantErrorMessage = "";
|
|
381
|
-
}
|
|
382
|
-
finishAssistantMessage();
|
|
383
|
-
}
|
|
384
|
-
const logMessage = sessionEventLogMessage(event);
|
|
385
|
-
if (logMessage) logger?.log("agent", `chat ${chatId} ${logMessage}`);
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
if (onSlowPrompt) {
|
|
389
|
-
slowPromptTimer = setTimeout(() => {
|
|
390
|
-
logger?.log("telegram", `prompt for chat ${chatId} is still running after ${slowPromptNoticeMs}ms`);
|
|
391
|
-
onSlowPrompt().catch((error) => {
|
|
392
|
-
logger?.error("telegram", `slow prompt notice failed for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
393
|
-
});
|
|
394
|
-
}, slowPromptNoticeMs);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
try {
|
|
398
|
-
await session.prompt(prompt);
|
|
399
|
-
} finally {
|
|
400
|
-
if (slowPromptTimer) clearTimeout(slowPromptTimer);
|
|
401
|
-
unsubscribe();
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (assistantErrorMessage) {
|
|
405
|
-
throw new Error(assistantErrorMessage);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
finishAssistantMessage();
|
|
409
|
-
return assistantMessages.join("\n\n").trim();
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
export function isSilentReply(text) {
|
|
413
|
-
return /^(?:NO_REPLY|No reply needed\.|No action needed\.)(?:\s+(?:NO_REPLY|No reply needed\.|No action needed\.))*$/.test(String(text || "").trim());
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function buildSessionHandoffPrompt() {
|
|
417
|
-
return [
|
|
418
|
-
"Prepare a concise handoff for the next Arisa session.",
|
|
419
|
-
"Review the entire active session, including any previous compaction summaries and the latest messages.",
|
|
420
|
-
"Keep only durable context: current goals or projects, decisions, user preferences, unresolved tasks, and important facts needed to continue.",
|
|
421
|
-
"Use at most 8 short bullets and at most 1600 characters.",
|
|
422
|
-
"Exclude secrets, tokens, passwords, cookies, API keys, private file paths, full transcripts, and stale chatter.",
|
|
423
|
-
"Do not take actions, call tools, send messages, or explain the process.",
|
|
424
|
-
"Return only the handoff."
|
|
425
|
-
].join("\n");
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function sanitizeSessionHandoff(text) {
|
|
429
|
-
const sanitized = String(text || "")
|
|
430
|
-
.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi, "[redacted private key]")
|
|
431
|
-
.replace(/\b(?:sk-[A-Za-z0-9_-]{12,}|gh[opsu]_[A-Za-z0-9_-]{12,}|Bearer\s+[A-Za-z0-9._-]+)\b/gi, "[redacted credential]")
|
|
432
|
-
.replace(/(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|client[_ -]?secret|password|cookie|secret)\s*[:=]\s*[^\s,;]+/gi, "[redacted credential]")
|
|
433
|
-
.trim();
|
|
434
|
-
if (sanitized.length <= 4000) return sanitized;
|
|
435
|
-
return `${sanitized.slice(0, 3997).trim()}...`;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
async function withTyping(ctx, work) {
|
|
439
|
-
await ctx.api.sendChatAction(ctx.chat.id, "typing");
|
|
132
|
+
export async function startTelegramTyping(ctx) {
|
|
133
|
+
const options = ctx.message?.message_thread_id
|
|
134
|
+
? { message_thread_id: ctx.message.message_thread_id }
|
|
135
|
+
: undefined;
|
|
136
|
+
await ctx.api.sendChatAction(ctx.chat.id, "typing", options).catch(() => {});
|
|
440
137
|
const timer = setInterval(() => {
|
|
441
|
-
ctx.api.sendChatAction(ctx.chat.id, "typing").catch(() => {});
|
|
138
|
+
ctx.api.sendChatAction(ctx.chat.id, "typing", options).catch(() => {});
|
|
442
139
|
}, 4000);
|
|
443
|
-
|
|
444
|
-
try {
|
|
445
|
-
return await work();
|
|
446
|
-
} finally {
|
|
447
|
-
clearInterval(timer);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
export function createChatStateStore() {
|
|
452
|
-
const states = new Map();
|
|
453
|
-
|
|
454
|
-
function reset(chatId) {
|
|
455
|
-
const state = {
|
|
456
|
-
processing: false,
|
|
457
|
-
pendingPrompts: [],
|
|
458
|
-
continueAfterClose: false,
|
|
459
|
-
historyRevision: 0,
|
|
460
|
-
beforeNextPrompt: null,
|
|
461
|
-
activeSession: null,
|
|
462
|
-
activeSteers: [],
|
|
463
|
-
assistantMessages: new Map()
|
|
464
|
-
};
|
|
465
|
-
states.set(String(chatId), state);
|
|
466
|
-
return state;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
return {
|
|
470
|
-
get(chatId) {
|
|
471
|
-
const key = String(chatId);
|
|
472
|
-
return states.get(key) || reset(key);
|
|
473
|
-
},
|
|
474
|
-
reset,
|
|
475
|
-
anyProcessing() {
|
|
476
|
-
return [...states.values()].some((state) => state.processing);
|
|
477
|
-
}
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
export function queueChatPrompt(chatState, prompt, { replace = false } = {}) {
|
|
482
|
-
if (replace) chatState.pendingPrompts = [];
|
|
483
|
-
chatState.pendingPrompts.push(prompt);
|
|
140
|
+
return () => clearInterval(timer);
|
|
484
141
|
}
|
|
485
142
|
|
|
486
|
-
function
|
|
487
|
-
|
|
143
|
+
export async function ensureQueuedTelegramTyping(chatState, ctx) {
|
|
144
|
+
if (chatState.stopQueuedTyping) return;
|
|
145
|
+
chatState.stopQueuedTyping = await startTelegramTyping(ctx);
|
|
488
146
|
}
|
|
489
147
|
|
|
490
|
-
export function
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return mode === "steer" ? "steer" : "queue";
|
|
148
|
+
export function stopQueuedTelegramTyping(chatState) {
|
|
149
|
+
chatState.stopQueuedTyping?.();
|
|
150
|
+
chatState.stopQueuedTyping = null;
|
|
494
151
|
}
|
|
495
152
|
|
|
496
|
-
|
|
497
|
-
const
|
|
498
|
-
if (
|
|
499
|
-
mode === "steer"
|
|
500
|
-
&& !replaceQueued
|
|
501
|
-
&& !chatState.continueAfterClose
|
|
502
|
-
&& !chatState.beforeNextPrompt
|
|
503
|
-
&& session?.isStreaming
|
|
504
|
-
&& typeof session.steer === "function"
|
|
505
|
-
) {
|
|
506
|
-
try {
|
|
507
|
-
await session.steer(prompt);
|
|
508
|
-
chatState.activeSteers.push(prompt);
|
|
509
|
-
return { disposition: "steered" };
|
|
510
|
-
} catch (error) {
|
|
511
|
-
queueChatPrompt(chatState, prompt);
|
|
512
|
-
return { disposition: "queued", steerError: error };
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
queueChatPrompt(chatState, prompt, { replace: replaceQueued });
|
|
517
|
-
return { disposition: "queued" };
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
export async function drainChatPromptQueue({
|
|
521
|
-
chatState,
|
|
522
|
-
initialPrompt,
|
|
523
|
-
initialCtx = null,
|
|
524
|
-
processPrompt,
|
|
525
|
-
onPromptFailure,
|
|
526
|
-
onPromptInterrupted,
|
|
527
|
-
beforeInitialPrompt
|
|
528
|
-
}) {
|
|
529
|
-
let currentPrompt = initialPrompt;
|
|
530
|
-
let currentCtx = initialCtx;
|
|
531
|
-
|
|
153
|
+
async function withTyping(ctx, work) {
|
|
154
|
+
const stopTyping = await startTelegramTyping(ctx);
|
|
532
155
|
try {
|
|
533
|
-
await
|
|
534
|
-
while (currentPrompt) {
|
|
535
|
-
while (chatState.beforeNextPrompt) {
|
|
536
|
-
const gate = chatState.beforeNextPrompt;
|
|
537
|
-
await gate;
|
|
538
|
-
if (chatState.beforeNextPrompt === gate) chatState.beforeNextPrompt = null;
|
|
539
|
-
}
|
|
540
|
-
if (chatState.continueAfterClose && chatState.pendingPrompts.length) {
|
|
541
|
-
currentPrompt = takeQueuedPrompt(chatState);
|
|
542
|
-
chatState.continueAfterClose = false;
|
|
543
|
-
currentCtx = null;
|
|
544
|
-
}
|
|
545
|
-
try {
|
|
546
|
-
await processPrompt({ prompt: currentPrompt, ctx: currentCtx });
|
|
547
|
-
} catch (error) {
|
|
548
|
-
if (chatState.continueAfterClose && chatState.pendingPrompts.length) {
|
|
549
|
-
await onPromptInterrupted?.(error);
|
|
550
|
-
} else {
|
|
551
|
-
await onPromptFailure?.(error);
|
|
552
|
-
throw error;
|
|
553
|
-
}
|
|
554
|
-
} finally {
|
|
555
|
-
currentCtx = null;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
currentPrompt = takeQueuedPrompt(chatState);
|
|
559
|
-
chatState.continueAfterClose = false;
|
|
560
|
-
}
|
|
156
|
+
return await work();
|
|
561
157
|
} finally {
|
|
562
|
-
|
|
563
|
-
chatState.activeSession = null;
|
|
564
|
-
chatState.activeSteers = [];
|
|
158
|
+
stopTyping();
|
|
565
159
|
}
|
|
566
160
|
}
|
|
567
161
|
|
|
568
|
-
export
|
|
569
|
-
await ctx.api.editMessageText(
|
|
570
|
-
ctx.chat.id,
|
|
571
|
-
ctx.callbackQuery.message.message_id,
|
|
572
|
-
messageText
|
|
573
|
-
);
|
|
574
|
-
await ctx.answerCallbackQuery({ text: callbackText });
|
|
575
|
-
}
|
|
162
|
+
export { closeModelPicker } from "./model-callback.js";
|
|
576
163
|
|
|
577
164
|
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, doctor, checkUpdates, updateCore, updateTools, requestRestart, logger }) {
|
|
578
165
|
const resourceNotes = new ToolResourceNoteStore();
|
|
579
166
|
const bot = new Bot(config.telegram.token);
|
|
580
167
|
const perChatState = createChatStateStore();
|
|
581
|
-
const
|
|
168
|
+
const sessionSeeds = new SessionSeedStore();
|
|
582
169
|
const notifiedPromptErrors = new WeakSet();
|
|
583
170
|
const authRenewals = new Map();
|
|
171
|
+
const workspaceRoutes = new WeakMap();
|
|
172
|
+
const workspaceGateStates = new Map();
|
|
584
173
|
let piAuthIssue = null;
|
|
585
174
|
let taskTimer = null;
|
|
586
175
|
|
|
176
|
+
const requestRestartWithReceipt = async (ctx, reason = "Telegram restart") => {
|
|
177
|
+
const route = contextRoute(ctx);
|
|
178
|
+
const receipt = await prepareRestartReceipt({
|
|
179
|
+
transportChatId: route.transportChatId,
|
|
180
|
+
threadId: route.threadId
|
|
181
|
+
}, { reason });
|
|
182
|
+
try {
|
|
183
|
+
return await requestRestart();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
await cancelRestartReceipt(receipt.id).catch(() => {});
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
587
189
|
const handleRestartCommand = createTelegramRestartHandler({
|
|
588
|
-
authorize:
|
|
589
|
-
|
|
590
|
-
chatId: ctx.chat.id,
|
|
591
|
-
saveConfig,
|
|
592
|
-
chatMeta: getIncomingChatMeta(ctx)
|
|
593
|
-
}),
|
|
594
|
-
requestRestart,
|
|
190
|
+
authorize: authorizeContext,
|
|
191
|
+
requestRestart: (ctx) => requestRestartWithReceipt(ctx, "Telegram /restart"),
|
|
595
192
|
logger
|
|
596
193
|
});
|
|
597
194
|
const handleUpdateCallback = createTelegramUpdateCallbackHandler({
|
|
598
|
-
authorize:
|
|
599
|
-
config,
|
|
600
|
-
chatId: ctx.chat.id,
|
|
601
|
-
saveConfig,
|
|
602
|
-
chatMeta: getIncomingChatMeta(ctx)
|
|
603
|
-
}),
|
|
195
|
+
authorize: authorizeContext,
|
|
604
196
|
updateCore,
|
|
605
197
|
updateTools,
|
|
606
|
-
requestRestart,
|
|
198
|
+
requestRestart: (ctx) => requestRestartWithReceipt(ctx, "Telegram update restart"),
|
|
607
199
|
logger
|
|
608
200
|
});
|
|
609
201
|
|
|
@@ -727,174 +319,97 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
727
319
|
};
|
|
728
320
|
}
|
|
729
321
|
|
|
730
|
-
function
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
provider: config.pi.provider,
|
|
737
|
-
apiKey: config.pi.apiKey
|
|
738
|
-
});
|
|
739
|
-
return reverseModelOrder(listProviderModels(config.pi.provider, runtime));
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
async function showModelPicker(ctx, page = 0) {
|
|
743
|
-
const agentConfig = getAgentConfig(config);
|
|
744
|
-
const picker = buildModelPicker({
|
|
745
|
-
provider: agentConfig.provider,
|
|
746
|
-
models: await getProviderModels(ctx.chat.id),
|
|
747
|
-
selectedModelId: resolveChatModel(config, ctx.chat.id),
|
|
748
|
-
selectedThinkingLevel: resolveChatThinkingLevel(config, ctx.chat.id),
|
|
749
|
-
selectedSpeed: resolveChatSpeed(config, ctx.chat.id),
|
|
750
|
-
page,
|
|
751
|
-
pageSize: config.telegram.modelPickerPageSize
|
|
752
|
-
});
|
|
753
|
-
const extra = { reply_markup: picker.replyMarkup };
|
|
754
|
-
const messageId = ctx.callbackQuery?.message?.message_id;
|
|
755
|
-
if (messageId) {
|
|
756
|
-
return ctx.api.editMessageText(ctx.chat.id, messageId, picker.text, extra);
|
|
322
|
+
async function authorizeContext(ctx) {
|
|
323
|
+
const route = await resolveTelegramWorkspaceRoute({ config, api: ctx.api, ctx });
|
|
324
|
+
if (!route.workspace) {
|
|
325
|
+
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
326
|
+
if (auth.ok) workspaceRoutes.set(ctx, route);
|
|
327
|
+
return auth;
|
|
757
328
|
}
|
|
758
|
-
return ctx.reply(picker.text, extra);
|
|
759
|
-
}
|
|
760
329
|
|
|
761
|
-
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
768
|
-
if (!modelSupportsThinking(resolvedModel)) {
|
|
769
|
-
const text = `${resolvedModel.provider}/${resolvedModel.id} does not support effort levels.`;
|
|
770
|
-
if (ctx.callbackQuery?.message?.message_id) {
|
|
771
|
-
return ctx.api.editMessageText(ctx.chat.id, ctx.callbackQuery.message.message_id, text);
|
|
330
|
+
const gateKey = String(ctx.chat.id);
|
|
331
|
+
const previous = workspaceGateStates.get(gateKey);
|
|
332
|
+
if (!route.ok) {
|
|
333
|
+
workspaceGateStates.set(gateKey, route.reason || "locked");
|
|
334
|
+
if (previous !== (route.reason || "locked")) {
|
|
335
|
+
await ctx.reply("Private workspace access is paused because this forum is no longer owner-only.").catch(() => {});
|
|
772
336
|
}
|
|
773
|
-
return
|
|
337
|
+
return { ok: false, reason: route.reason || "workspace-locked" };
|
|
774
338
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
modelIndex
|
|
783
|
-
});
|
|
784
|
-
const extra = { reply_markup: picker.replyMarkup };
|
|
785
|
-
const messageId = ctx.callbackQuery?.message?.message_id;
|
|
786
|
-
if (messageId) {
|
|
787
|
-
return ctx.api.editMessageText(ctx.chat.id, messageId, picker.text, extra);
|
|
339
|
+
if (!(config.telegram.authorizedChatIds || []).includes(route.ownerChatId)) {
|
|
340
|
+
return { ok: false, reason: "owner-not-authorized" };
|
|
341
|
+
}
|
|
342
|
+
workspaceRoutes.set(ctx, route);
|
|
343
|
+
workspaceGateStates.set(gateKey, "ready");
|
|
344
|
+
if (previous && previous !== "ready") {
|
|
345
|
+
await ctx.reply("Private workspace access restored.").catch(() => {});
|
|
788
346
|
}
|
|
789
|
-
return
|
|
347
|
+
return { ok: true, firstTime: false, workspace: true };
|
|
790
348
|
}
|
|
791
349
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}
|
|
802
|
-
return ctx.reply(text);
|
|
803
|
-
}
|
|
804
|
-
const picker = buildSpeedPicker({
|
|
805
|
-
provider: model.provider,
|
|
806
|
-
modelId: model.id,
|
|
807
|
-
speeds: MODEL_SPEEDS,
|
|
808
|
-
selectedSpeed: resolveChatSpeed(config, ctx.chat.id)
|
|
809
|
-
});
|
|
810
|
-
const extra = { reply_markup: picker.replyMarkup };
|
|
811
|
-
const messageId = ctx.callbackQuery?.message?.message_id;
|
|
812
|
-
if (messageId) return ctx.api.editMessageText(ctx.chat.id, messageId, picker.text, extra);
|
|
813
|
-
return ctx.reply(picker.text, extra);
|
|
350
|
+
function contextRoute(ctx) {
|
|
351
|
+
return workspaceRoutes.get(ctx) || {
|
|
352
|
+
ok: true,
|
|
353
|
+
workspace: false,
|
|
354
|
+
sessionId: String(ctx.chat.id),
|
|
355
|
+
scopeChatId: ctx.chat.id,
|
|
356
|
+
transportChatId: ctx.chat.id,
|
|
357
|
+
threadId: null
|
|
358
|
+
};
|
|
814
359
|
}
|
|
815
360
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const key = chatKey(chatId);
|
|
819
|
-
const hadSelections = Boolean(agentConfig.chatModels);
|
|
820
|
-
const previousSelection = agentConfig.chatModels?.[key];
|
|
821
|
-
const level = clampModelThinkingLevel(model, thinkingLevel ?? resolveChatThinkingLevel(config, chatId));
|
|
822
|
-
const speed = clampModelSpeed(model, resolveChatSpeed(config, chatId));
|
|
823
|
-
selectChatModel(config, chatId, model, { thinkingLevel: level, speed });
|
|
824
|
-
try {
|
|
825
|
-
await saveConfig(config);
|
|
826
|
-
} catch (error) {
|
|
827
|
-
if (previousSelection) {
|
|
828
|
-
agentConfig.chatModels[key] = previousSelection;
|
|
829
|
-
} else {
|
|
830
|
-
delete agentConfig.chatModels[key];
|
|
831
|
-
if (!hadSelections) delete agentConfig.chatModels;
|
|
832
|
-
}
|
|
833
|
-
throw error;
|
|
834
|
-
}
|
|
835
|
-
agentManager.resetSession(chatId);
|
|
836
|
-
return level;
|
|
361
|
+
function getChatState(chatId) {
|
|
362
|
+
return perChatState.get(chatId);
|
|
837
363
|
}
|
|
838
364
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
delete agentConfig.chatModels[key];
|
|
853
|
-
if (!hadSelections) delete agentConfig.chatModels;
|
|
854
|
-
}
|
|
855
|
-
throw error;
|
|
856
|
-
}
|
|
857
|
-
return level;
|
|
365
|
+
const {
|
|
366
|
+
getProviderModels,
|
|
367
|
+
showModelPicker,
|
|
368
|
+
showEffortPicker,
|
|
369
|
+
showSpeedPicker,
|
|
370
|
+
persistChatModel,
|
|
371
|
+
persistChatEffort,
|
|
372
|
+
persistChatSpeed
|
|
373
|
+
} = createTelegramModelControls({ config, saveConfig, agentManager, contextRoute });
|
|
374
|
+
|
|
375
|
+
function modelSelectionFor(chatId) {
|
|
376
|
+
const selection = config.pi.chatModels?.[chatKey(chatId)];
|
|
377
|
+
return selection?.provider === config.pi.provider ? selection : null;
|
|
858
378
|
}
|
|
859
379
|
|
|
860
|
-
async function
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
agentConfig.chatModels[key] = previousSelection;
|
|
873
|
-
} else {
|
|
874
|
-
delete agentConfig.chatModels[key];
|
|
875
|
-
if (!hadSelections) delete agentConfig.chatModels;
|
|
876
|
-
}
|
|
877
|
-
agentManager.clearSessionCache(chatId);
|
|
878
|
-
throw error;
|
|
879
|
-
}
|
|
880
|
-
return level;
|
|
380
|
+
async function ensureWorkspaceTopicModelSelection(route) {
|
|
381
|
+
if (!route.workspace || chatKey(route.sessionId) === chatKey(route.ownerChatId)) return;
|
|
382
|
+
if (modelSelectionFor(route.sessionId)) return;
|
|
383
|
+
const inherited = modelSelectionFor(route.scopeChatId) || modelSelectionFor(route.transportChatId);
|
|
384
|
+
if (!inherited) return;
|
|
385
|
+
config.pi.chatModels ||= {};
|
|
386
|
+
config.pi.chatModels[chatKey(route.sessionId)] = {
|
|
387
|
+
...inherited,
|
|
388
|
+
sessionRevision: 0
|
|
389
|
+
};
|
|
390
|
+
await saveConfig(config);
|
|
391
|
+
logger?.log("telegram", `inherited model ${inherited.model} for workspace topic session ${route.sessionId}`);
|
|
881
392
|
}
|
|
882
393
|
|
|
883
|
-
async function buildIncomingPrompt(ctx) {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
const
|
|
887
|
-
const artifact = await captureIncomingArtifact(ctx, artifactStore);
|
|
394
|
+
async function buildIncomingPrompt(ctx, route = contextRoute(ctx)) {
|
|
395
|
+
logger?.log("telegram", `message ${ctx.msg.message_id} in chat ${route.transportChatId} session ${route.sessionId}`);
|
|
396
|
+
const chatArtifactStore = artifactStore.forChat(route.scopeChatId);
|
|
397
|
+
const artifact = await captureIncomingArtifact(ctx, artifactStore, { storageChatId: route.scopeChatId });
|
|
888
398
|
if (artifact) logger?.log("telegram", `captured artifact ${artifact.kind}${artifact.id ? ` ${artifact.id}` : ""}`);
|
|
889
|
-
const { transcript, toolResult } = await normalizeIncomingArtifact({
|
|
399
|
+
const { transcript, toolResult, normalizationRequired } = await normalizeIncomingArtifact({
|
|
400
|
+
artifact,
|
|
401
|
+
toolRegistry,
|
|
402
|
+
chatArtifactStore,
|
|
403
|
+
chatId: route.scopeChatId
|
|
404
|
+
});
|
|
890
405
|
if (transcript) logger?.log("telegram", `media transcribed to artifact ${transcript.id}`);
|
|
891
|
-
if (
|
|
892
|
-
logger?.log("telegram", `media normalization unavailable for chat ${
|
|
406
|
+
if (normalizationRequired && !transcript) {
|
|
407
|
+
logger?.log("telegram", `media normalization unavailable for chat ${route.transportChatId}: ${toolResult?.error || toolResult?.missingConfig?.join(", ") || "unknown error"}`);
|
|
893
408
|
}
|
|
894
409
|
return buildPrompt({ ctx, artifact, transcript, toolResult });
|
|
895
410
|
}
|
|
896
411
|
|
|
897
|
-
async function sendTextReply({ sendText, sendDocument, chatId, text }) {
|
|
412
|
+
async function sendTextReply({ sendText, sendDocument, chatId, artifactChatId = chatId, text }) {
|
|
898
413
|
const maxInlineReplyLength = 3500;
|
|
899
414
|
|
|
900
415
|
if (isSilentReply(text)) {
|
|
@@ -904,7 +419,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
904
419
|
|
|
905
420
|
if (text.length > maxInlineReplyLength) {
|
|
906
421
|
logger?.log("telegram", `sending long reply as markdown attachment for chat ${chatId}`);
|
|
907
|
-
const chatArtifactStore = artifactStore.forChat(
|
|
422
|
+
const chatArtifactStore = artifactStore.forChat(artifactChatId);
|
|
908
423
|
const artifact = await chatArtifactStore.createGeneratedFile({
|
|
909
424
|
fileName: `reply-${Date.now()}.md`,
|
|
910
425
|
content: text,
|
|
@@ -928,17 +443,82 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
928
443
|
}
|
|
929
444
|
}
|
|
930
445
|
|
|
931
|
-
function
|
|
446
|
+
function createWorkspaceAccessGuard(route) {
|
|
447
|
+
return async () => {
|
|
448
|
+
if (!route.workspace) return;
|
|
449
|
+
const current = await resolveTelegramWorkspaceRoute({
|
|
450
|
+
config,
|
|
451
|
+
api: bot.api,
|
|
452
|
+
ctx: {
|
|
453
|
+
chat: { id: route.transportChatId, type: "supergroup", is_forum: true },
|
|
454
|
+
from: { id: route.ownerChatId },
|
|
455
|
+
message: { message_thread_id: route.threadId }
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
if (!current.ok) throw new Error("Owner-only workspace access is paused.");
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function createTelegramSessionBridge(route) {
|
|
463
|
+
const messageOptions = (extra = {}) => route.workspace && route.threadId
|
|
464
|
+
? { ...extra, message_thread_id: route.threadId }
|
|
465
|
+
: extra;
|
|
466
|
+
const initializeForumTopic = async ({ messageThreadId, name, context }) => {
|
|
467
|
+
if (!route.workspace) throw new Error("Telegram topic initialization is only available from the owner workspace forum.");
|
|
468
|
+
await createWorkspaceAccessGuard(route)();
|
|
469
|
+
const initializedSessionId = topicSessionId({
|
|
470
|
+
ownerChatId: route.ownerChatId,
|
|
471
|
+
groupChatId: route.transportChatId,
|
|
472
|
+
threadId: messageThreadId,
|
|
473
|
+
generalTopicId: route.generalTopicId
|
|
474
|
+
});
|
|
475
|
+
if (initializedSessionId === String(route.ownerChatId)) {
|
|
476
|
+
throw new Error("The General topic already uses the owner's private session and cannot be reinitialized here.");
|
|
477
|
+
}
|
|
478
|
+
const handoff = buildTopicInitializationHandoff({ name, context });
|
|
479
|
+
await sessionSeeds.set(initializedSessionId, handoff);
|
|
480
|
+
agentManager.resetSession(initializedSessionId, { handoff });
|
|
481
|
+
await agentManager.waitForSessionClose(initializedSessionId);
|
|
482
|
+
return {
|
|
483
|
+
ok: true,
|
|
484
|
+
chatId: route.transportChatId,
|
|
485
|
+
messageThreadId,
|
|
486
|
+
sessionId: initializedSessionId,
|
|
487
|
+
name,
|
|
488
|
+
initialized: true
|
|
489
|
+
};
|
|
490
|
+
};
|
|
932
491
|
return {
|
|
933
492
|
sendMedia: async (filePath, { method = "audio", caption, filename } = {}) => {
|
|
934
|
-
logger?.log("telegram", `sending ${method} reply for chat ${
|
|
493
|
+
logger?.log("telegram", `sending ${method} reply for chat ${route.transportChatId}`);
|
|
935
494
|
const input = new InputFile(filePath, filename || undefined);
|
|
936
|
-
|
|
937
|
-
if (method === "
|
|
938
|
-
if (method === "
|
|
939
|
-
if (method === "
|
|
940
|
-
return bot.api.
|
|
941
|
-
|
|
495
|
+
const options = messageOptions({ caption });
|
|
496
|
+
if (method === "voice") return bot.api.sendVoice(route.transportChatId, input, options);
|
|
497
|
+
if (method === "document") return bot.api.sendDocument(route.transportChatId, input, options);
|
|
498
|
+
if (method === "photo" || method === "image") return bot.api.sendPhoto(route.transportChatId, input, options);
|
|
499
|
+
if (method === "video") return bot.api.sendVideo(route.transportChatId, input, options);
|
|
500
|
+
return bot.api.sendAudio(route.transportChatId, input, options);
|
|
501
|
+
},
|
|
502
|
+
createForumTopic: async (name, context) => {
|
|
503
|
+
if (!route.workspace) throw new Error("Telegram topic creation is only available from the owner workspace forum.");
|
|
504
|
+
await createWorkspaceAccessGuard(route)();
|
|
505
|
+
const topic = await bot.api.createForumTopic(route.transportChatId, name);
|
|
506
|
+
return initializeForumTopic({
|
|
507
|
+
messageThreadId: topic.message_thread_id,
|
|
508
|
+
name: topic.name,
|
|
509
|
+
context
|
|
510
|
+
});
|
|
511
|
+
},
|
|
512
|
+
initializeForumTopic,
|
|
513
|
+
prepareRestartReceipt: (summary) => prepareRestartReceipt({
|
|
514
|
+
transportChatId: route.transportChatId,
|
|
515
|
+
threadId: route.threadId
|
|
516
|
+
}, { reason: String(summary || "Agent-requested restart").trim() }),
|
|
517
|
+
cancelRestartReceipt,
|
|
518
|
+
getTaskContext: () => route.workspace ? {
|
|
519
|
+
transportChatId: route.transportChatId,
|
|
520
|
+
messageThreadId: route.topicThreadId
|
|
521
|
+
} : null
|
|
942
522
|
};
|
|
943
523
|
}
|
|
944
524
|
|
|
@@ -950,7 +530,13 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
950
530
|
: artifact.kind === "video" || artifact.mimeType?.startsWith("video/") ? "video"
|
|
951
531
|
: "document");
|
|
952
532
|
const safeCaption = caption && !/(^|\s)(\/[^\s]|[A-Za-z]:[\\/])/.test(caption) ? caption : undefined;
|
|
953
|
-
await createTelegramSessionBridge(
|
|
533
|
+
await createTelegramSessionBridge({
|
|
534
|
+
workspace: false,
|
|
535
|
+
sessionId: String(chatId),
|
|
536
|
+
scopeChatId: chatId,
|
|
537
|
+
transportChatId: chatId,
|
|
538
|
+
threadId: null
|
|
539
|
+
}).sendMedia(artifact.path, {
|
|
954
540
|
method: resolvedMethod,
|
|
955
541
|
caption: safeCaption,
|
|
956
542
|
filename: path.basename(artifact.path)
|
|
@@ -959,54 +545,63 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
959
545
|
});
|
|
960
546
|
|
|
961
547
|
async function processPromptForChat({ chatId, prompt, ctx = null }) {
|
|
548
|
+
const route = ctx ? contextRoute(ctx) : {
|
|
549
|
+
workspace: false,
|
|
550
|
+
sessionId: String(chatId),
|
|
551
|
+
scopeChatId: chatId,
|
|
552
|
+
transportChatId: chatId,
|
|
553
|
+
threadId: null
|
|
554
|
+
};
|
|
555
|
+
const sessionId = route.sessionId;
|
|
556
|
+
const bridge = createTelegramSessionBridge(route);
|
|
557
|
+
const messageOptions = (extra = {}) => route.workspace && route.threadId
|
|
558
|
+
? { ...extra, message_thread_id: route.threadId }
|
|
559
|
+
: extra;
|
|
962
560
|
const work = async () => {
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
561
|
+
await ensureWorkspaceTopicModelSelection(route);
|
|
562
|
+
if (route.workspace && route.threadId) {
|
|
563
|
+
const handoff = await sessionSeeds.consume(sessionId);
|
|
564
|
+
if (handoff) {
|
|
565
|
+
agentManager.resetSession(sessionId, { handoff });
|
|
566
|
+
await agentManager.waitForSessionClose(sessionId);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const { session, speedController } = await agentManager.getSessionContext(sessionId, bridge, {
|
|
570
|
+
scopeChatId: route.scopeChatId,
|
|
571
|
+
accessGuard: createWorkspaceAccessGuard(route)
|
|
968
572
|
});
|
|
969
573
|
let text = "";
|
|
970
|
-
|
|
971
|
-
const chatState = getChatState(chatId);
|
|
574
|
+
const chatState = getChatState(sessionId);
|
|
972
575
|
chatState.activeSession = session;
|
|
973
|
-
chatState.
|
|
576
|
+
chatState.activeRoute = route;
|
|
974
577
|
try {
|
|
975
578
|
text = await withPromptSpeed({
|
|
976
579
|
speedController,
|
|
977
580
|
speed: isScheduledTaskPrompt(prompt) ? 1 : undefined,
|
|
978
|
-
restoreSpeed: () => clampModelSpeed(session.model, resolveChatSpeed(config,
|
|
581
|
+
restoreSpeed: () => clampModelSpeed(session.model, resolveChatSpeed(config, sessionId))
|
|
979
582
|
}, () => collectText(session, prompt, {
|
|
980
583
|
logger,
|
|
981
|
-
chatId,
|
|
584
|
+
chatId: sessionId,
|
|
982
585
|
onSlowPrompt: () => bot.api.sendMessage(
|
|
983
|
-
|
|
984
|
-
"This is taking longer than 5 minutes, so I will keep the current session running instead of starting over. Send /new if you want to abandon it and start fresh."
|
|
586
|
+
route.transportChatId,
|
|
587
|
+
"This is taking longer than 5 minutes, so I will keep the current session running instead of starting over. Send /new if you want to abandon it and start fresh.",
|
|
588
|
+
messageOptions()
|
|
985
589
|
)
|
|
986
590
|
}));
|
|
987
591
|
} catch (error) {
|
|
988
|
-
agentManager.resetSession(
|
|
592
|
+
agentManager.resetSession(sessionId);
|
|
989
593
|
throw error;
|
|
990
594
|
} finally {
|
|
991
|
-
steeredPrompts = [...chatState.activeSteers];
|
|
992
595
|
if (chatState.activeSession === session) chatState.activeSession = null;
|
|
993
|
-
chatState.
|
|
994
|
-
}
|
|
995
|
-
if (getChatState(chatId).historyRevision === historyRevision) {
|
|
996
|
-
const historyPrompt = steeredPrompts.length
|
|
997
|
-
? [prompt, ...steeredPrompts.map((message) => `[Steering message]\n${message}`)].join("\n\n")
|
|
998
|
-
: prompt;
|
|
999
|
-
await conversationHistory.appendTurn(chatId, {
|
|
1000
|
-
runtime: "pi",
|
|
1001
|
-
prompt: historyPrompt,
|
|
1002
|
-
response: text
|
|
1003
|
-
});
|
|
596
|
+
chatState.activeRoute = null;
|
|
1004
597
|
}
|
|
1005
598
|
if (text) {
|
|
599
|
+
await createWorkspaceAccessGuard(route)();
|
|
1006
600
|
await sendTextReply({
|
|
1007
|
-
sendText: (message, extra) => bot.api.sendMessage(
|
|
1008
|
-
sendDocument: (file, extra) => bot.api.sendDocument(
|
|
1009
|
-
chatId,
|
|
601
|
+
sendText: (message, extra) => bot.api.sendMessage(route.transportChatId, message, messageOptions(extra)),
|
|
602
|
+
sendDocument: (file, extra) => bot.api.sendDocument(route.transportChatId, file, messageOptions(extra)),
|
|
603
|
+
chatId: sessionId,
|
|
604
|
+
artifactChatId: route.scopeChatId,
|
|
1010
605
|
text
|
|
1011
606
|
});
|
|
1012
607
|
}
|
|
@@ -1020,11 +615,18 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1020
615
|
const chatState = getChatState(chatId);
|
|
1021
616
|
|
|
1022
617
|
if (chatState.processing) {
|
|
618
|
+
const incomingRoute = ctx ? contextRoute(ctx) : null;
|
|
619
|
+
const activeRoute = chatState.activeRoute;
|
|
620
|
+
const sameDelivery = !incomingRoute || !activeRoute || (
|
|
621
|
+
incomingRoute.transportChatId === activeRoute.transportChatId
|
|
622
|
+
&& incomingRoute.threadId === activeRoute.threadId
|
|
623
|
+
);
|
|
1023
624
|
const routed = await routeBusyPrompt({
|
|
1024
625
|
chatState,
|
|
1025
626
|
prompt,
|
|
1026
|
-
mode: busyMessageMode,
|
|
1027
|
-
replaceQueued
|
|
627
|
+
mode: sameDelivery ? busyMessageMode : "queue",
|
|
628
|
+
replaceQueued,
|
|
629
|
+
ctx
|
|
1028
630
|
});
|
|
1029
631
|
if (routed.disposition === "steered") {
|
|
1030
632
|
logger?.log("telegram", `chat ${chatId} busy, steering ${label}`);
|
|
@@ -1066,24 +668,27 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1066
668
|
}
|
|
1067
669
|
|
|
1068
670
|
async function enqueueOrProcess(ctx) {
|
|
1069
|
-
const
|
|
671
|
+
const route = contextRoute(ctx);
|
|
672
|
+
const chatState = getChatState(route.sessionId);
|
|
1070
673
|
|
|
1071
674
|
if (chatState.processing) {
|
|
1072
|
-
|
|
675
|
+
await ensureQueuedTelegramTyping(chatState, ctx);
|
|
676
|
+
const incomingPrompt = await buildIncomingPrompt(ctx, route);
|
|
1073
677
|
const busyMessageMode = typeof ctx.message?.text === "string"
|
|
1074
|
-
? resolveTelegramBusyMessageMode(config,
|
|
678
|
+
? resolveTelegramBusyMessageMode(config, route.sessionId)
|
|
1075
679
|
: "queue";
|
|
1076
680
|
return enqueuePrompt({
|
|
1077
|
-
chatId:
|
|
681
|
+
chatId: route.sessionId,
|
|
1078
682
|
prompt: incomingPrompt,
|
|
1079
683
|
label: `message ${ctx.msg.message_id}`,
|
|
1080
|
-
busyMessageMode
|
|
684
|
+
busyMessageMode,
|
|
685
|
+
ctx
|
|
1081
686
|
});
|
|
1082
687
|
}
|
|
1083
688
|
|
|
1084
|
-
const incomingPrompt = await buildIncomingPrompt(ctx);
|
|
689
|
+
const incomingPrompt = await buildIncomingPrompt(ctx, route);
|
|
1085
690
|
return enqueuePrompt({
|
|
1086
|
-
chatId:
|
|
691
|
+
chatId: route.sessionId,
|
|
1087
692
|
prompt: incomingPrompt,
|
|
1088
693
|
label: `message ${ctx.msg.message_id}`,
|
|
1089
694
|
ctx
|
|
@@ -1100,6 +705,12 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1100
705
|
logger?.log("telegram", `startup message failed for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1101
706
|
}
|
|
1102
707
|
}
|
|
708
|
+
try {
|
|
709
|
+
const result = await deliverRestartReceipt((chatId, text, options) => bot.api.sendMessage(chatId, text, options));
|
|
710
|
+
if (result) logger?.log("telegram", `delivered restart receipt ${result.receipt.id}`);
|
|
711
|
+
} catch (error) {
|
|
712
|
+
logger?.log("telegram", `restart receipt delivery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
713
|
+
}
|
|
1103
714
|
}
|
|
1104
715
|
|
|
1105
716
|
function scheduleStartupMessages({ skipAgentStartupPrompts = false } = {}) {
|
|
@@ -1115,76 +726,48 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1115
726
|
timer.unref?.();
|
|
1116
727
|
}
|
|
1117
728
|
|
|
1118
|
-
async function
|
|
1119
|
-
|
|
1120
|
-
if (
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
logger?.log("tasks", `running task ${task.id} for chat ${chatId}`);
|
|
1131
|
-
await enqueuePrompt({
|
|
1132
|
-
chatId,
|
|
1133
|
-
prompt: await buildAsyncTaskPrompt({ task, artifactStore, toolRegistry, resourceNotes, logger }),
|
|
1134
|
-
label: `scheduled task ${task.id}`
|
|
1135
|
-
});
|
|
1136
|
-
await taskStore.complete(task.id);
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
if (task.kind === "agent_event") {
|
|
1141
|
-
logger?.log("tasks", `agent event ${task.id} for chat ${chatId}`);
|
|
1142
|
-
await enqueuePrompt({
|
|
1143
|
-
chatId,
|
|
1144
|
-
prompt: await buildAsyncEventPrompt(task, resourceNotes),
|
|
1145
|
-
label: `agent event ${task.id}`
|
|
1146
|
-
});
|
|
1147
|
-
await taskStore.complete(task.id);
|
|
1148
|
-
return;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
if (task.kind === "poll_tool") {
|
|
1152
|
-
const toolName = task.payload?.toolName;
|
|
1153
|
-
if (!toolName) {
|
|
1154
|
-
await taskStore.fail(task.id, "poll_tool missing toolName");
|
|
1155
|
-
return;
|
|
1156
|
-
}
|
|
1157
|
-
logger?.log("tasks", `polling tool ${toolName} (task ${task.id}) for chat ${chatId}`);
|
|
1158
|
-
try {
|
|
1159
|
-
await agentManager.runTool({
|
|
1160
|
-
name: toolName,
|
|
1161
|
-
request: { args: task.payload.args || {} },
|
|
1162
|
-
chatId
|
|
1163
|
-
});
|
|
1164
|
-
} catch (error) {
|
|
1165
|
-
logger?.log("tasks", `poll_tool ${toolName} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1166
|
-
}
|
|
1167
|
-
await taskStore.complete(task.id);
|
|
1168
|
-
return;
|
|
729
|
+
async function enqueueAsyncPrompt({ chatId, prompt, label, telegramContext }) {
|
|
730
|
+
let ctx = { chat: { id: chatId }, api: bot.api };
|
|
731
|
+
if (telegramContext?.transportChatId && telegramContext?.messageThreadId) {
|
|
732
|
+
ctx = {
|
|
733
|
+
chat: { id: telegramContext.transportChatId, type: "supergroup", is_forum: true },
|
|
734
|
+
from: { id: chatId },
|
|
735
|
+
message: { message_thread_id: telegramContext.messageThreadId },
|
|
736
|
+
api: bot.api
|
|
737
|
+
};
|
|
738
|
+
const route = await resolveTelegramWorkspaceRoute({ config, api: bot.api, ctx });
|
|
739
|
+
if (!route.ok) throw new Error("Scheduled owner-workspace destination is unavailable.");
|
|
740
|
+
workspaceRoutes.set(ctx, route);
|
|
1169
741
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
742
|
+
const route = contextRoute(ctx);
|
|
743
|
+
const chatState = getChatState(route.sessionId);
|
|
744
|
+
if (chatState.processing) await ensureQueuedTelegramTyping(chatState, ctx);
|
|
745
|
+
return enqueuePrompt({ chatId: route.sessionId, prompt, label, ctx });
|
|
1172
746
|
}
|
|
1173
747
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
}
|
|
748
|
+
const { dispatchDueTasks } = createTelegramTaskDispatcher({
|
|
749
|
+
taskStore,
|
|
750
|
+
sendMessage: (chatId, text) => bot.api.sendMessage(chatId, text),
|
|
751
|
+
enqueueAsyncPrompt,
|
|
752
|
+
artifactStore,
|
|
753
|
+
toolRegistry,
|
|
754
|
+
resourceNotes,
|
|
755
|
+
agentManager,
|
|
756
|
+
logger
|
|
757
|
+
});
|
|
1184
758
|
|
|
1185
|
-
async function summarizeSessionBeforeReset(chatId
|
|
759
|
+
async function summarizeSessionBeforeReset(chatId, route = {
|
|
760
|
+
workspace: false,
|
|
761
|
+
sessionId: String(chatId),
|
|
762
|
+
scopeChatId: chatId,
|
|
763
|
+
transportChatId: chatId,
|
|
764
|
+
threadId: null
|
|
765
|
+
}) {
|
|
1186
766
|
try {
|
|
1187
|
-
const context = await agentManager.getSessionContext(chatId, createTelegramSessionBridge(
|
|
767
|
+
const context = await agentManager.getSessionContext(chatId, createTelegramSessionBridge(route), {
|
|
768
|
+
scopeChatId: route.scopeChatId,
|
|
769
|
+
accessGuard: createWorkspaceAccessGuard(route)
|
|
770
|
+
});
|
|
1188
771
|
const parentSession = context.session.sessionFile || "";
|
|
1189
772
|
if (!context.session.messages.length) return { handoff: "", parentSession: "" };
|
|
1190
773
|
|
|
@@ -1197,19 +780,21 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1197
780
|
}
|
|
1198
781
|
|
|
1199
782
|
async function handleNewCommand(ctx) {
|
|
1200
|
-
const
|
|
783
|
+
const route = contextRoute(ctx);
|
|
784
|
+
const sessionId = route.sessionId;
|
|
785
|
+
const chatState = getChatState(sessionId);
|
|
1201
786
|
const wasProcessing = chatState.processing;
|
|
1202
787
|
chatState.historyRevision += 1;
|
|
1203
788
|
const commandRevision = chatState.historyRevision;
|
|
1204
789
|
const prompt = buildNewSessionPrompt(ctx);
|
|
1205
790
|
|
|
1206
791
|
if (wasProcessing) {
|
|
1207
|
-
logger?.log("telegram", `chat ${
|
|
1208
|
-
queueChatPrompt(chatState, prompt, { replace: true });
|
|
792
|
+
logger?.log("telegram", `chat ${sessionId} busy, queueing new-session command`);
|
|
793
|
+
queueChatPrompt(chatState, prompt, { replace: true, ctx });
|
|
1209
794
|
chatState.continueAfterClose = true;
|
|
1210
795
|
const reset = (async () => {
|
|
1211
|
-
await
|
|
1212
|
-
agentManager.resetSession(
|
|
796
|
+
await sessionSeeds.clear(sessionId);
|
|
797
|
+
agentManager.resetSession(sessionId);
|
|
1213
798
|
})();
|
|
1214
799
|
chatState.beforeNextPrompt = reset;
|
|
1215
800
|
try {
|
|
@@ -1221,21 +806,22 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1221
806
|
}
|
|
1222
807
|
|
|
1223
808
|
chatState.processing = true;
|
|
1224
|
-
logger?.log("telegram", `processing new-session command in chat ${
|
|
809
|
+
logger?.log("telegram", `processing new-session command in chat ${sessionId}`);
|
|
1225
810
|
await processChatPromptQueue({
|
|
1226
|
-
chatId:
|
|
811
|
+
chatId: sessionId,
|
|
1227
812
|
prompt,
|
|
1228
813
|
label: "new-session command",
|
|
1229
814
|
ctx,
|
|
1230
815
|
beforeInitialPrompt: async () => {
|
|
1231
|
-
const handoff = await withTyping(ctx, () => summarizeSessionBeforeReset(
|
|
816
|
+
const handoff = await withTyping(ctx, () => summarizeSessionBeforeReset(sessionId, route));
|
|
1232
817
|
if (chatState.historyRevision !== commandRevision) return;
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
818
|
+
if (route.workspace && route.threadId) {
|
|
819
|
+
await sessionSeeds.set(sessionId, handoff.handoff);
|
|
820
|
+
} else {
|
|
821
|
+
await sessionSeeds.clear(sessionId);
|
|
822
|
+
}
|
|
1237
823
|
if (chatState.historyRevision !== commandRevision) return;
|
|
1238
|
-
agentManager.resetSession(
|
|
824
|
+
agentManager.resetSession(sessionId, handoff);
|
|
1239
825
|
}
|
|
1240
826
|
});
|
|
1241
827
|
}
|
|
@@ -1246,13 +832,13 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1246
832
|
});
|
|
1247
833
|
|
|
1248
834
|
bot.command("start", async (ctx) => {
|
|
1249
|
-
const auth = await
|
|
835
|
+
const auth = await authorizeContext(ctx);
|
|
1250
836
|
if (!auth.ok) return;
|
|
1251
837
|
return ctx.reply(auth.firstTime ? "This chat is now authorized for Arisa." : "Arisa is ready.");
|
|
1252
838
|
});
|
|
1253
839
|
|
|
1254
840
|
bot.command("new", async (ctx) => {
|
|
1255
|
-
const auth = await
|
|
841
|
+
const auth = await authorizeContext(ctx);
|
|
1256
842
|
if (!auth.ok) return;
|
|
1257
843
|
if (piAuthIssue) {
|
|
1258
844
|
await ctx.reply(buildPiAuthRecoveryBlockedMessage({
|
|
@@ -1269,7 +855,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1269
855
|
bot.command("restart", handleRestartCommand);
|
|
1270
856
|
|
|
1271
857
|
bot.command("doctor", async (ctx) => {
|
|
1272
|
-
const auth = await
|
|
858
|
+
const auth = await authorizeContext(ctx);
|
|
1273
859
|
if (!auth.ok) return;
|
|
1274
860
|
const pending = await ctx.reply(renderTelegramHtml("```text\nRunning Arisa Doctor…\n```"), { parse_mode: "HTML" });
|
|
1275
861
|
try {
|
|
@@ -1286,11 +872,11 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1286
872
|
});
|
|
1287
873
|
|
|
1288
874
|
bot.command("update", async (ctx) => {
|
|
1289
|
-
const auth = await
|
|
875
|
+
const auth = await authorizeContext(ctx);
|
|
1290
876
|
if (!auth.ok) return;
|
|
1291
877
|
const pending = await ctx.reply(renderTelegramHtml("```text\nChecking Arisa and official tool updates…\n```"), { parse_mode: "HTML" });
|
|
1292
878
|
try {
|
|
1293
|
-
const report = await checkUpdates(ctx.
|
|
879
|
+
const report = await checkUpdates(contextRoute(ctx).scopeChatId);
|
|
1294
880
|
const picker = buildUpdatePicker(report);
|
|
1295
881
|
await ctx.api.editMessageText(
|
|
1296
882
|
ctx.chat.id,
|
|
@@ -1305,31 +891,31 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1305
891
|
});
|
|
1306
892
|
|
|
1307
893
|
bot.command("tools", async (ctx) => {
|
|
1308
|
-
const auth = await
|
|
894
|
+
const auth = await authorizeContext(ctx);
|
|
1309
895
|
if (!auth.ok) return;
|
|
1310
|
-
await ctx.reply(renderTelegramHtml(formatToolUsageReport(await toolRegistry.usage(ctx.
|
|
896
|
+
await ctx.reply(renderTelegramHtml(formatToolUsageReport(await toolRegistry.usage(contextRoute(ctx).scopeChatId))), { parse_mode: "HTML" });
|
|
1311
897
|
});
|
|
1312
898
|
|
|
1313
899
|
bot.command("model", async (ctx) => {
|
|
1314
|
-
const auth = await
|
|
900
|
+
const auth = await authorizeContext(ctx);
|
|
1315
901
|
if (!auth.ok) return;
|
|
1316
902
|
await showModelPicker(ctx);
|
|
1317
903
|
});
|
|
1318
904
|
|
|
1319
905
|
bot.command("effort", async (ctx) => {
|
|
1320
|
-
const auth = await
|
|
906
|
+
const auth = await authorizeContext(ctx);
|
|
1321
907
|
if (!auth.ok) return;
|
|
1322
908
|
await showEffortPicker(ctx);
|
|
1323
909
|
});
|
|
1324
910
|
|
|
1325
911
|
bot.command("speed", async (ctx) => {
|
|
1326
|
-
const auth = await
|
|
912
|
+
const auth = await authorizeContext(ctx);
|
|
1327
913
|
if (!auth.ok) return;
|
|
1328
914
|
await showSpeedPicker(ctx);
|
|
1329
915
|
});
|
|
1330
916
|
|
|
1331
917
|
bot.command("auth", async (ctx) => {
|
|
1332
|
-
const auth = await
|
|
918
|
+
const auth = await authorizeContext(ctx);
|
|
1333
919
|
if (!auth.ok) return;
|
|
1334
920
|
|
|
1335
921
|
const status = getPiAuthStatus(config, ctx.chat.id);
|
|
@@ -1361,222 +947,23 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1361
947
|
}
|
|
1362
948
|
});
|
|
1363
949
|
|
|
950
|
+
const handleModelCallback = createTelegramModelCallbackHandler({
|
|
951
|
+
config,
|
|
952
|
+
authorizeContext,
|
|
953
|
+
contextRoute,
|
|
954
|
+
getChatState,
|
|
955
|
+
getProviderModels,
|
|
956
|
+
showModelPicker,
|
|
957
|
+
showEffortPicker,
|
|
958
|
+
persistChatModel,
|
|
959
|
+
persistChatEffort,
|
|
960
|
+
persistChatSpeed,
|
|
961
|
+
logger
|
|
962
|
+
});
|
|
963
|
+
|
|
1364
964
|
bot.on("callback_query:data", async (ctx, next) => {
|
|
1365
965
|
if (await handleUpdateCallback(ctx)) return;
|
|
1366
|
-
|
|
1367
|
-
const effortAction = modelAction ? null : parseEffortPickerAction(ctx.callbackQuery.data);
|
|
1368
|
-
const speedAction = modelAction || effortAction ? null : parseSpeedPickerAction(ctx.callbackQuery.data);
|
|
1369
|
-
const action = modelAction || effortAction || speedAction;
|
|
1370
|
-
if (!action) return next();
|
|
1371
|
-
if (action.type === "noop") {
|
|
1372
|
-
await ctx.answerCallbackQuery();
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig });
|
|
1377
|
-
if (!auth.ok) {
|
|
1378
|
-
await ctx.answerCallbackQuery({ text: "This chat is not authorized.", show_alert: true });
|
|
1379
|
-
return;
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
try {
|
|
1383
|
-
if (action.type === "page") {
|
|
1384
|
-
await showModelPicker(ctx, action.value);
|
|
1385
|
-
await ctx.answerCallbackQuery();
|
|
1386
|
-
return;
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
const models = await getProviderModels(ctx.chat.id);
|
|
1390
|
-
const chatBusy = getChatState(ctx.chat.id).processing;
|
|
1391
|
-
|
|
1392
|
-
if (action.type === "select") {
|
|
1393
|
-
const model = models[action.value];
|
|
1394
|
-
if (!model) {
|
|
1395
|
-
await ctx.answerCallbackQuery({
|
|
1396
|
-
text: "This model list is no longer current. Run /model again.",
|
|
1397
|
-
show_alert: true
|
|
1398
|
-
});
|
|
1399
|
-
return;
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
// Reasoning models open the effort picker only — no session reset yet.
|
|
1403
|
-
if (modelSupportsThinking(model)) {
|
|
1404
|
-
await showEffortPicker(ctx, {
|
|
1405
|
-
model,
|
|
1406
|
-
modelIndex: action.value,
|
|
1407
|
-
selectedThinkingLevel: clampModelThinkingLevel(model, resolveChatThinkingLevel(config, ctx.chat.id))
|
|
1408
|
-
});
|
|
1409
|
-
await ctx.answerCallbackQuery({ text: `Choose effort for ${model.id}.` });
|
|
1410
|
-
return;
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
if (chatBusy) {
|
|
1414
|
-
await ctx.answerCallbackQuery({
|
|
1415
|
-
text: "Wait for the current response before changing models.",
|
|
1416
|
-
show_alert: true
|
|
1417
|
-
});
|
|
1418
|
-
return;
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
const currentModelId = resolveChatModel(config, ctx.chat.id);
|
|
1422
|
-
const currentEffort = resolveChatThinkingLevel(config, ctx.chat.id);
|
|
1423
|
-
if (model.id === currentModelId && currentEffort === "off") {
|
|
1424
|
-
await closeModelPicker(ctx, {
|
|
1425
|
-
messageText: `Already using ${model.provider}/${model.id}.`,
|
|
1426
|
-
callbackText: `Already using ${model.id}.`
|
|
1427
|
-
});
|
|
1428
|
-
return;
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
await persistChatModel(ctx.chat.id, model, "off");
|
|
1432
|
-
await ctx.api.editMessageText(
|
|
1433
|
-
ctx.chat.id,
|
|
1434
|
-
ctx.callbackQuery.message.message_id,
|
|
1435
|
-
`Model changed to ${model.provider}/${model.id}.\nA new chat context will start with your next message.`
|
|
1436
|
-
);
|
|
1437
|
-
await ctx.answerCallbackQuery({ text: `Using ${model.id}.` });
|
|
1438
|
-
return;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
if (action.type === "model-effort") {
|
|
1442
|
-
const model = models[action.modelIndex];
|
|
1443
|
-
if (!model) {
|
|
1444
|
-
await ctx.answerCallbackQuery({
|
|
1445
|
-
text: "This model list is no longer current. Run /model again.",
|
|
1446
|
-
show_alert: true
|
|
1447
|
-
});
|
|
1448
|
-
return;
|
|
1449
|
-
}
|
|
1450
|
-
const levels = listModelThinkingLevels(model);
|
|
1451
|
-
if (!levels.includes(action.level)) {
|
|
1452
|
-
await ctx.answerCallbackQuery({
|
|
1453
|
-
text: "That effort level is not available for this model.",
|
|
1454
|
-
show_alert: true
|
|
1455
|
-
});
|
|
1456
|
-
return;
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
const currentModelId = resolveChatModel(config, ctx.chat.id);
|
|
1460
|
-
const currentEffort = resolveChatThinkingLevel(config, ctx.chat.id);
|
|
1461
|
-
if (model.id === currentModelId && action.level === currentEffort) {
|
|
1462
|
-
await closeModelPicker(ctx, {
|
|
1463
|
-
messageText: `Already using ${model.provider}/${model.id} (effort: ${action.level}).`,
|
|
1464
|
-
callbackText: `Already using ${model.id} at ${action.level}.`
|
|
1465
|
-
});
|
|
1466
|
-
return;
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
// Effort-only updates do not reset the session, so they are safe while busy.
|
|
1470
|
-
if (model.id === currentModelId) {
|
|
1471
|
-
await persistChatEffort(ctx.chat.id, model, action.level);
|
|
1472
|
-
await ctx.api.editMessageText(
|
|
1473
|
-
ctx.chat.id,
|
|
1474
|
-
ctx.callbackQuery.message.message_id,
|
|
1475
|
-
`Effort set to ${action.level} for ${model.provider}/${model.id}.`
|
|
1476
|
-
);
|
|
1477
|
-
await ctx.answerCallbackQuery({ text: `Effort: ${action.level}.` });
|
|
1478
|
-
return;
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
if (chatBusy) {
|
|
1482
|
-
await ctx.answerCallbackQuery({
|
|
1483
|
-
text: "Wait for the current response before changing models.",
|
|
1484
|
-
show_alert: true
|
|
1485
|
-
});
|
|
1486
|
-
return;
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
await persistChatModel(ctx.chat.id, model, action.level);
|
|
1490
|
-
await ctx.api.editMessageText(
|
|
1491
|
-
ctx.chat.id,
|
|
1492
|
-
ctx.callbackQuery.message.message_id,
|
|
1493
|
-
`Model changed to ${model.provider}/${model.id} (effort: ${action.level}).\nA new chat context will start with your next message.`
|
|
1494
|
-
);
|
|
1495
|
-
await ctx.answerCallbackQuery({ text: `Using ${model.id} / ${action.level}.` });
|
|
1496
|
-
return;
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
if (action.type === "effort") {
|
|
1500
|
-
const model = models.find((item) => item.id === resolveChatModel(config, ctx.chat.id));
|
|
1501
|
-
if (!model) {
|
|
1502
|
-
await ctx.answerCallbackQuery({
|
|
1503
|
-
text: "Current model is unavailable. Run /model again.",
|
|
1504
|
-
show_alert: true
|
|
1505
|
-
});
|
|
1506
|
-
return;
|
|
1507
|
-
}
|
|
1508
|
-
if (!modelSupportsThinking(model)) {
|
|
1509
|
-
await ctx.answerCallbackQuery({
|
|
1510
|
-
text: "This model does not support effort levels.",
|
|
1511
|
-
show_alert: true
|
|
1512
|
-
});
|
|
1513
|
-
return;
|
|
1514
|
-
}
|
|
1515
|
-
const levels = listModelThinkingLevels(model);
|
|
1516
|
-
if (!levels.includes(action.level)) {
|
|
1517
|
-
await ctx.answerCallbackQuery({
|
|
1518
|
-
text: "That effort level is not available for this model.",
|
|
1519
|
-
show_alert: true
|
|
1520
|
-
});
|
|
1521
|
-
return;
|
|
1522
|
-
}
|
|
1523
|
-
const currentEffort = resolveChatThinkingLevel(config, ctx.chat.id);
|
|
1524
|
-
if (action.level === currentEffort) {
|
|
1525
|
-
await closeModelPicker(ctx, {
|
|
1526
|
-
messageText: `Already using effort ${action.level} for ${model.provider}/${model.id}.`,
|
|
1527
|
-
callbackText: `Already using effort ${action.level}.`
|
|
1528
|
-
});
|
|
1529
|
-
return;
|
|
1530
|
-
}
|
|
1531
|
-
await persistChatEffort(ctx.chat.id, model, action.level);
|
|
1532
|
-
await ctx.api.editMessageText(
|
|
1533
|
-
ctx.chat.id,
|
|
1534
|
-
ctx.callbackQuery.message.message_id,
|
|
1535
|
-
`Effort set to ${action.level} for ${model.provider}/${model.id}.`
|
|
1536
|
-
);
|
|
1537
|
-
await ctx.answerCallbackQuery({ text: `Effort: ${action.level}.` });
|
|
1538
|
-
return;
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
if (action.type === "speed") {
|
|
1542
|
-
const model = models.find((item) => item.id === resolveChatModel(config, ctx.chat.id));
|
|
1543
|
-
if (!model) {
|
|
1544
|
-
await ctx.answerCallbackQuery({
|
|
1545
|
-
text: "Current model is unavailable. Run /model again.",
|
|
1546
|
-
show_alert: true
|
|
1547
|
-
});
|
|
1548
|
-
return;
|
|
1549
|
-
}
|
|
1550
|
-
if (!modelSupportsSpeed(model)) {
|
|
1551
|
-
await ctx.answerCallbackQuery({
|
|
1552
|
-
text: "This model does not support speed 1.5x.",
|
|
1553
|
-
show_alert: true
|
|
1554
|
-
});
|
|
1555
|
-
return;
|
|
1556
|
-
}
|
|
1557
|
-
const currentSpeed = resolveChatSpeed(config, ctx.chat.id);
|
|
1558
|
-
if (action.speed === currentSpeed) {
|
|
1559
|
-
await closeModelPicker(ctx, {
|
|
1560
|
-
messageText: `Already using speed ${action.speed.toFixed(1)}x for ${model.provider}/${model.id}.`,
|
|
1561
|
-
callbackText: `Already using speed ${action.speed.toFixed(1)}x.`
|
|
1562
|
-
});
|
|
1563
|
-
return;
|
|
1564
|
-
}
|
|
1565
|
-
await persistChatSpeed(ctx.chat.id, model, action.speed);
|
|
1566
|
-
await ctx.api.editMessageText(
|
|
1567
|
-
ctx.chat.id,
|
|
1568
|
-
ctx.callbackQuery.message.message_id,
|
|
1569
|
-
`Speed set to ${action.speed.toFixed(1)}x for ${model.provider}/${model.id}.`
|
|
1570
|
-
);
|
|
1571
|
-
await ctx.answerCallbackQuery({ text: `Speed: ${action.speed.toFixed(1)}x.` });
|
|
1572
|
-
}
|
|
1573
|
-
} catch (error) {
|
|
1574
|
-
logger?.error("telegram", `model selection failed for chat ${ctx.chat.id}: ${getErrorMessage(error)}`);
|
|
1575
|
-
await ctx.answerCallbackQuery({
|
|
1576
|
-
text: "Could not change the model, effort, or speed.",
|
|
1577
|
-
show_alert: true
|
|
1578
|
-
}).catch(() => {});
|
|
1579
|
-
}
|
|
966
|
+
return handleModelCallback(ctx, next);
|
|
1580
967
|
});
|
|
1581
968
|
|
|
1582
969
|
bot.on("message_reaction", async (ctx) => {
|
|
@@ -1599,8 +986,9 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1599
986
|
});
|
|
1600
987
|
|
|
1601
988
|
bot.on("message", async (ctx) => {
|
|
1602
|
-
const auth = await
|
|
989
|
+
const auth = await authorizeContext(ctx);
|
|
1603
990
|
if (!auth.ok) return;
|
|
991
|
+
if (!isProcessableTelegramMessage(ctx.message)) return;
|
|
1604
992
|
|
|
1605
993
|
const command = getTelegramCommand(ctx);
|
|
1606
994
|
if (command) return;
|