arisa 4.3.5 → 5.1.2
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 +21 -19
- package/README.md +30 -9
- package/package.json +6 -2
- package/pnpm-workspace.yaml +1 -0
- package/src/core/agent/agent-manager.js +288 -29
- package/src/core/agent/auth-flow.js +12 -8
- package/src/core/agent/model-selection.js +54 -14
- package/src/core/agent/model-speed.js +59 -0
- package/src/core/config/config-defaults.js +56 -4
- package/src/core/config/config-store.js +5 -1
- package/src/core/conversation/conversation-history-store.js +142 -0
- package/src/core/tasks/task-store.js +16 -0
- package/src/core/tools/daemon-health.js +11 -2
- package/src/core/tools/daemon-processes.js +92 -2
- package/src/core/tools/daemon-runtime.js +4 -2
- package/src/core/tools/ipc-client.js +15 -3
- package/src/core/tools/tool-registry.js +42 -2
- package/src/core/tools/tool-usage-store.js +59 -0
- package/src/index.js +61 -6
- package/src/runtime/arisa-capabilities.js +45 -1
- package/src/runtime/bootstrap.js +3 -2
- package/src/runtime/create-app.js +49 -11
- package/src/runtime/doctor.js +365 -0
- package/src/runtime/log-viewer.js +165 -0
- package/src/runtime/paths.js +8 -1
- package/src/runtime/report-format.js +51 -0
- package/src/runtime/service-manager.js +106 -8
- package/src/runtime/tool-process-supervisor.js +107 -10
- package/src/runtime/tool-usage-report.js +10 -0
- package/src/runtime/update-manager.js +206 -0
- package/src/transport/telegram/bot.js +633 -91
- package/src/transport/telegram/model-picker.js +28 -2
- package/test/agent-tool-policy.test.js +26 -1
- package/test/auth-flow.test.js +28 -2
- package/test/capabilities-security.test.js +37 -0
- package/test/context-and-task-bounds.test.js +280 -0
- package/test/daemon-runtime.test.js +130 -2
- package/test/dependency-warnings.test.js +17 -0
- package/test/doctor.test.js +111 -0
- package/test/log-viewer.test.js +90 -0
- package/test/model-selection.test.js +125 -2
- package/test/paths.test.js +16 -0
- package/test/pi-compaction.test.js +43 -0
- package/test/service-manager.test.js +237 -0
- package/test/task-store.test.js +31 -0
- package/test/telegram-text-artifact.test.js +36 -1
- package/test/tool-registry-run.test.js +21 -0
- package/test/tool-usage.test.js +37 -0
- package/test/update-manager.test.js +87 -0
|
@@ -3,16 +3,61 @@ import path from "node:path";
|
|
|
3
3
|
import { authorizeChat } from "./auth.js";
|
|
4
4
|
import { captureIncomingArtifact, formatLocationText } from "./media.js";
|
|
5
5
|
import { buildDeviceCodeTelegramMessage } from "./device-code-message.js";
|
|
6
|
-
import { buildEffortPicker, buildModelPicker, parseEffortPickerAction, parseModelPickerAction } from "./model-picker.js";
|
|
6
|
+
import { buildEffortPicker, buildModelPicker, buildSpeedPicker, parseEffortPickerAction, parseModelPickerAction, parseSpeedPickerAction, reverseModelOrder } from "./model-picker.js";
|
|
7
7
|
import { renderTelegramHtml } from "./text-format.js";
|
|
8
8
|
import { buildPiAuthRecoveryBlockedMessage, buildPiAuthTelegramMessage, getErrorMessage, getPiAuthIssue, getPiAuthStatus } from "../../core/agent/auth-flow.js";
|
|
9
9
|
import { createPiOAuthLogin } from "../../core/agent/pi-auth-login.js";
|
|
10
|
-
import { resolveChatModel, resolveChatThinkingLevel, selectChatModel, selectChatThinkingLevel } from "../../core/agent/model-selection.js";
|
|
10
|
+
import { getAgentConfig, resolveChatModel, resolveChatSpeed, resolveChatThinkingLevel, selectChatModel, selectChatSpeed, selectChatThinkingLevel } from "../../core/agent/model-selection.js";
|
|
11
11
|
import { clampModelThinkingLevel, createPiRuntime, listModelThinkingLevels, listProviderModels, modelSupportsThinking } from "../../core/agent/pi-runtime.js";
|
|
12
|
+
import { clampModelSpeed, MODEL_SPEEDS, modelSupportsSpeed } from "../../core/agent/model-speed.js";
|
|
12
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";
|
|
16
|
+
import { formatDoctorReport } from "../../runtime/doctor.js";
|
|
17
|
+
import { formatToolUsageReport } from "../../runtime/tool-usage-report.js";
|
|
13
18
|
|
|
14
19
|
const slowPromptNoticeMs = 300_000;
|
|
15
20
|
|
|
21
|
+
export const telegramCommands = Object.freeze([
|
|
22
|
+
{ command: "new", description: "New chat context" },
|
|
23
|
+
{ command: "restart", description: "Restart Arisa" },
|
|
24
|
+
{ command: "doctor", description: "Check runtime health" },
|
|
25
|
+
{ command: "update", description: "Check for updates" },
|
|
26
|
+
{ command: "tools", description: "Tool usage counts" },
|
|
27
|
+
{ command: "model", description: "Choose chat model" },
|
|
28
|
+
{ command: "effort", description: "Choose reasoning effort" },
|
|
29
|
+
{ command: "speed", description: "Choose model speed" },
|
|
30
|
+
{ command: "auth", description: "Authentication status" }
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export function createTelegramRestartHandler({ authorize, requestRestart, logger }) {
|
|
34
|
+
if (typeof authorize !== "function" || typeof requestRestart !== "function") {
|
|
35
|
+
throw new Error("Telegram restart requires authorization and restart handoff functions");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let restartRequested = false;
|
|
39
|
+
return async (ctx) => {
|
|
40
|
+
const auth = await authorize(ctx);
|
|
41
|
+
if (!auth.ok) return;
|
|
42
|
+
|
|
43
|
+
if (restartRequested) {
|
|
44
|
+
await ctx.reply("An Arisa restart is already in progress.");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
restartRequested = true;
|
|
49
|
+
try {
|
|
50
|
+
await ctx.reply("Arisa is restarting. I'll be back shortly.");
|
|
51
|
+
const handoff = await requestRestart();
|
|
52
|
+
logger?.log("telegram", `restart handed off to process ${handoff.pid}`);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
restartRequested = false;
|
|
55
|
+
logger?.error("telegram", `restart handoff failed: ${getErrorMessage(error)}`);
|
|
56
|
+
await ctx.reply(`Arisa could not be restarted: ${getErrorMessage(error)}`);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
16
61
|
function quotedMessageSummary(message) {
|
|
17
62
|
if (!message) return [];
|
|
18
63
|
|
|
@@ -53,6 +98,67 @@ function getIncomingMessageText(message) {
|
|
|
53
98
|
return message?.text || message?.caption || formatLocationText(message) || "";
|
|
54
99
|
}
|
|
55
100
|
|
|
101
|
+
function telegramDisplayName(entity = {}) {
|
|
102
|
+
if (entity.username) return `@${entity.username}`;
|
|
103
|
+
return [entity.first_name, entity.last_name].filter(Boolean).join(" ") || entity.title || "unknown";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function forwardedMessageSummary(message) {
|
|
107
|
+
const origin = message?.forward_origin;
|
|
108
|
+
if (!origin) return [];
|
|
109
|
+
|
|
110
|
+
const parts = ["forwarded: true", `forwardedOriginType: ${origin.type}`];
|
|
111
|
+
if (origin.type === "user") parts.push(`forwardedFrom: ${telegramDisplayName(origin.sender_user)}`);
|
|
112
|
+
if (origin.type === "hidden_user") parts.push(`forwardedFrom: ${origin.sender_user_name}`);
|
|
113
|
+
if (origin.type === "chat" || origin.type === "channel") {
|
|
114
|
+
parts.push(`forwardedFrom: ${telegramDisplayName(origin.chat)}`);
|
|
115
|
+
}
|
|
116
|
+
if (origin.type === "channel" && origin.message_id) parts.push(`forwardedMessageId: ${origin.message_id}`);
|
|
117
|
+
if (origin.author_signature) parts.push(`forwardedAuthorSignature: ${origin.author_signature}`);
|
|
118
|
+
if (origin.date) parts.push(`forwardedAt: ${new Date(origin.date * 1000).toISOString()}`);
|
|
119
|
+
return parts;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function reactionLabel(reaction = {}) {
|
|
123
|
+
if (reaction.type === "emoji") return reaction.emoji || "emoji";
|
|
124
|
+
if (reaction.type === "custom_emoji") return `custom:${reaction.custom_emoji_id || "unknown"}`;
|
|
125
|
+
if (reaction.type === "paid") return "paid";
|
|
126
|
+
return reaction.type || "unknown";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function reactionDifference(left = [], right = []) {
|
|
130
|
+
const remaining = right.map(reactionLabel);
|
|
131
|
+
return left.map(reactionLabel).filter((label) => {
|
|
132
|
+
const index = remaining.indexOf(label);
|
|
133
|
+
if (index < 0) return true;
|
|
134
|
+
remaining.splice(index, 1);
|
|
135
|
+
return false;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function buildReactionPrompt({ reaction, reactedMessageText = "" }) {
|
|
140
|
+
const oldReactions = reaction.old_reaction || [];
|
|
141
|
+
const newReactions = reaction.new_reaction || [];
|
|
142
|
+
const added = reactionDifference(newReactions, oldReactions);
|
|
143
|
+
const removed = reactionDifference(oldReactions, newReactions);
|
|
144
|
+
const actor = reaction.user || reaction.actor_chat || {};
|
|
145
|
+
const actorId = reaction.user?.id || reaction.actor_chat?.id || "unknown";
|
|
146
|
+
|
|
147
|
+
return [
|
|
148
|
+
"Incoming Telegram reaction.",
|
|
149
|
+
`chatId: ${reaction.chat.id}`,
|
|
150
|
+
`userId: ${actorId}`,
|
|
151
|
+
`username: ${reaction.user?.username || "(no username)"}`,
|
|
152
|
+
`reactedMessageId: ${reaction.message_id}`,
|
|
153
|
+
reactedMessageText ? `reactedMessageText: ${reactedMessageText}` : null,
|
|
154
|
+
added.length ? `addedReactions: ${added.join(" ")}` : null,
|
|
155
|
+
removed.length ? `removedReactions: ${removed.join(" ")}` : null,
|
|
156
|
+
`currentReactions: ${newReactions.map(reactionLabel).join(" ") || "none"}`,
|
|
157
|
+
`actor: ${telegramDisplayName(actor)}`,
|
|
158
|
+
"Treat this as lightweight feedback on the referenced message. Respond only if the reaction clearly requests action; otherwise stay silent."
|
|
159
|
+
].filter(Boolean).join("\n");
|
|
160
|
+
}
|
|
161
|
+
|
|
56
162
|
function baseMimeType(mimeType = "") {
|
|
57
163
|
return mimeType.split(";")[0].trim().toLowerCase();
|
|
58
164
|
}
|
|
@@ -80,6 +186,7 @@ export function buildPrompt({ ctx, artifact, transcript, toolResult }) {
|
|
|
80
186
|
|
|
81
187
|
const messageText = getIncomingMessageText(ctx.message);
|
|
82
188
|
if (messageText) parts.push(`text: ${messageText}`);
|
|
189
|
+
parts.push(...forwardedMessageSummary(ctx.message));
|
|
83
190
|
parts.push(...quotedMessageSummary(ctx.message?.reply_to_message));
|
|
84
191
|
if (shouldIncludeArtifactReference({ artifact, messageText })) {
|
|
85
192
|
if (artifact?.path) parts.push(`artifactPath: ${artifact.path}`);
|
|
@@ -219,12 +326,13 @@ function buildStartupMessage(chatMeta = {}) {
|
|
|
219
326
|
return "Arisa is back online.";
|
|
220
327
|
}
|
|
221
328
|
|
|
222
|
-
async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {}) {
|
|
329
|
+
export async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {}) {
|
|
223
330
|
let text = "";
|
|
224
331
|
let assistantErrorMessage = "";
|
|
225
332
|
let shouldSeparateAssistantMessage = false;
|
|
226
333
|
let slowPromptTimer = null;
|
|
227
334
|
const unsubscribe = session.subscribe((event) => {
|
|
335
|
+
if (event.arisaPromptScoped === false) return;
|
|
228
336
|
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
229
337
|
shouldSeparateAssistantMessage = text.trim().length > 0;
|
|
230
338
|
}
|
|
@@ -235,8 +343,13 @@ async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {
|
|
|
235
343
|
}
|
|
236
344
|
text += event.assistantMessageEvent.delta;
|
|
237
345
|
}
|
|
238
|
-
if (event.type === "message_end" && event.message?.
|
|
239
|
-
|
|
346
|
+
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
347
|
+
if (event.message.stopReason === "error") {
|
|
348
|
+
assistantErrorMessage = event.message.errorMessage || "assistant message ended with error";
|
|
349
|
+
} else if (event.message.stopReason !== "aborted") {
|
|
350
|
+
// Auto-compaction and retry can emit a transient error before a successful continuation.
|
|
351
|
+
assistantErrorMessage = "";
|
|
352
|
+
}
|
|
240
353
|
}
|
|
241
354
|
const logMessage = sessionEventLogMessage(event);
|
|
242
355
|
if (logMessage) logger?.log("agent", `chat ${chatId} ${logMessage}`);
|
|
@@ -265,6 +378,32 @@ async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {
|
|
|
265
378
|
return text.trim();
|
|
266
379
|
}
|
|
267
380
|
|
|
381
|
+
export function isSilentReply(text) {
|
|
382
|
+
return /^(?:NO_REPLY|No reply needed\.|No action needed\.)(?:\s+(?:NO_REPLY|No reply needed\.|No action needed\.))*$/.test(String(text || "").trim());
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function buildSessionHandoffPrompt() {
|
|
386
|
+
return [
|
|
387
|
+
"Prepare a concise handoff for the next Arisa session.",
|
|
388
|
+
"Review the entire active session, including any previous compaction summaries and the latest messages.",
|
|
389
|
+
"Keep only durable context: current goals or projects, decisions, user preferences, unresolved tasks, and important facts needed to continue.",
|
|
390
|
+
"Use at most 8 short bullets and at most 1600 characters.",
|
|
391
|
+
"Exclude secrets, tokens, passwords, cookies, API keys, private file paths, full transcripts, and stale chatter.",
|
|
392
|
+
"Do not take actions, call tools, send messages, or explain the process.",
|
|
393
|
+
"Return only the handoff."
|
|
394
|
+
].join("\n");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function sanitizeSessionHandoff(text) {
|
|
398
|
+
const sanitized = String(text || "")
|
|
399
|
+
.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi, "[redacted private key]")
|
|
400
|
+
.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]")
|
|
401
|
+
.replace(/(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|client[_ -]?secret|password|cookie|secret)\s*[:=]\s*[^\s,;]+/gi, "[redacted credential]")
|
|
402
|
+
.trim();
|
|
403
|
+
if (sanitized.length <= 4000) return sanitized;
|
|
404
|
+
return `${sanitized.slice(0, 3997).trim()}...`;
|
|
405
|
+
}
|
|
406
|
+
|
|
268
407
|
async function withTyping(ctx, work) {
|
|
269
408
|
await ctx.api.sendChatAction(ctx.chat.id, "typing");
|
|
270
409
|
const timer = setInterval(() => {
|
|
@@ -278,14 +417,152 @@ async function withTyping(ctx, work) {
|
|
|
278
417
|
}
|
|
279
418
|
}
|
|
280
419
|
|
|
281
|
-
export
|
|
420
|
+
export function createChatStateStore() {
|
|
421
|
+
const states = new Map();
|
|
422
|
+
|
|
423
|
+
function reset(chatId) {
|
|
424
|
+
const state = {
|
|
425
|
+
processing: false,
|
|
426
|
+
pendingPrompts: [],
|
|
427
|
+
continueAfterClose: false,
|
|
428
|
+
historyRevision: 0,
|
|
429
|
+
beforeNextPrompt: null,
|
|
430
|
+
activeSession: null,
|
|
431
|
+
activeSteers: [],
|
|
432
|
+
assistantMessages: new Map()
|
|
433
|
+
};
|
|
434
|
+
states.set(String(chatId), state);
|
|
435
|
+
return state;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
get(chatId) {
|
|
440
|
+
const key = String(chatId);
|
|
441
|
+
return states.get(key) || reset(key);
|
|
442
|
+
},
|
|
443
|
+
reset,
|
|
444
|
+
anyProcessing() {
|
|
445
|
+
return [...states.values()].some((state) => state.processing);
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function queueChatPrompt(chatState, prompt, { replace = false } = {}) {
|
|
451
|
+
if (replace) chatState.pendingPrompts = [];
|
|
452
|
+
chatState.pendingPrompts.push(prompt);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function takeQueuedPrompt(chatState) {
|
|
456
|
+
return chatState.pendingPrompts.shift() || "";
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function resolveTelegramBusyMessageMode(config, chatId) {
|
|
460
|
+
const chatMode = config.telegram?.chatMeta?.[String(chatId)]?.busyMessageMode;
|
|
461
|
+
const mode = chatMode || config.telegram?.busyMessageMode;
|
|
462
|
+
return mode === "steer" ? "steer" : "queue";
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function routeBusyPrompt({ chatState, prompt, mode = "queue", replaceQueued = false }) {
|
|
466
|
+
const session = chatState.activeSession;
|
|
467
|
+
if (
|
|
468
|
+
mode === "steer"
|
|
469
|
+
&& !replaceQueued
|
|
470
|
+
&& !chatState.continueAfterClose
|
|
471
|
+
&& !chatState.beforeNextPrompt
|
|
472
|
+
&& session?.isStreaming
|
|
473
|
+
&& typeof session.steer === "function"
|
|
474
|
+
) {
|
|
475
|
+
try {
|
|
476
|
+
await session.steer(prompt);
|
|
477
|
+
chatState.activeSteers.push(prompt);
|
|
478
|
+
return { disposition: "steered" };
|
|
479
|
+
} catch (error) {
|
|
480
|
+
queueChatPrompt(chatState, prompt);
|
|
481
|
+
return { disposition: "queued", steerError: error };
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
queueChatPrompt(chatState, prompt, { replace: replaceQueued });
|
|
486
|
+
return { disposition: "queued" };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export async function drainChatPromptQueue({
|
|
490
|
+
chatState,
|
|
491
|
+
initialPrompt,
|
|
492
|
+
initialCtx = null,
|
|
493
|
+
processPrompt,
|
|
494
|
+
onPromptFailure,
|
|
495
|
+
onPromptInterrupted,
|
|
496
|
+
beforeInitialPrompt
|
|
497
|
+
}) {
|
|
498
|
+
let currentPrompt = initialPrompt;
|
|
499
|
+
let currentCtx = initialCtx;
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
await beforeInitialPrompt?.();
|
|
503
|
+
while (currentPrompt) {
|
|
504
|
+
while (chatState.beforeNextPrompt) {
|
|
505
|
+
const gate = chatState.beforeNextPrompt;
|
|
506
|
+
await gate;
|
|
507
|
+
if (chatState.beforeNextPrompt === gate) chatState.beforeNextPrompt = null;
|
|
508
|
+
}
|
|
509
|
+
if (chatState.continueAfterClose && chatState.pendingPrompts.length) {
|
|
510
|
+
currentPrompt = takeQueuedPrompt(chatState);
|
|
511
|
+
chatState.continueAfterClose = false;
|
|
512
|
+
currentCtx = null;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
await processPrompt({ prompt: currentPrompt, ctx: currentCtx });
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (chatState.continueAfterClose && chatState.pendingPrompts.length) {
|
|
518
|
+
await onPromptInterrupted?.(error);
|
|
519
|
+
} else {
|
|
520
|
+
await onPromptFailure?.(error);
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
} finally {
|
|
524
|
+
currentCtx = null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
currentPrompt = takeQueuedPrompt(chatState);
|
|
528
|
+
chatState.continueAfterClose = false;
|
|
529
|
+
}
|
|
530
|
+
} finally {
|
|
531
|
+
chatState.processing = false;
|
|
532
|
+
chatState.activeSession = null;
|
|
533
|
+
chatState.activeSteers = [];
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export async function closeModelPicker(ctx, { messageText, callbackText }) {
|
|
538
|
+
await ctx.api.editMessageText(
|
|
539
|
+
ctx.chat.id,
|
|
540
|
+
ctx.callbackQuery.message.message_id,
|
|
541
|
+
messageText
|
|
542
|
+
);
|
|
543
|
+
await ctx.answerCallbackQuery({ text: callbackText });
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, doctor, checkUpdates, requestRestart, logger }) {
|
|
282
547
|
const bot = new Bot(config.telegram.token);
|
|
283
|
-
const perChatState =
|
|
548
|
+
const perChatState = createChatStateStore();
|
|
549
|
+
const conversationHistory = new ConversationHistoryStore();
|
|
284
550
|
const notifiedPromptErrors = new WeakSet();
|
|
285
551
|
const authRenewals = new Map();
|
|
286
552
|
let piAuthIssue = null;
|
|
287
553
|
let taskTimer = null;
|
|
288
554
|
|
|
555
|
+
const handleRestartCommand = createTelegramRestartHandler({
|
|
556
|
+
authorize: (ctx) => authorizeChat({
|
|
557
|
+
config,
|
|
558
|
+
chatId: ctx.chat.id,
|
|
559
|
+
saveConfig,
|
|
560
|
+
chatMeta: getIncomingChatMeta(ctx)
|
|
561
|
+
}),
|
|
562
|
+
requestRestart,
|
|
563
|
+
logger
|
|
564
|
+
});
|
|
565
|
+
|
|
289
566
|
function chatKey(chatId) {
|
|
290
567
|
return String(chatId);
|
|
291
568
|
}
|
|
@@ -309,7 +586,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
309
586
|
if (!issue) return false;
|
|
310
587
|
|
|
311
588
|
try {
|
|
312
|
-
await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, issue }));
|
|
589
|
+
await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, issue }));
|
|
313
590
|
markPromptErrorNotified(error);
|
|
314
591
|
return true;
|
|
315
592
|
} catch (notifyError) {
|
|
@@ -328,16 +605,16 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
328
605
|
async function finishAuthRenewal(chatId, renewal) {
|
|
329
606
|
try {
|
|
330
607
|
await renewal.promise;
|
|
331
|
-
await agentManager.
|
|
608
|
+
await agentManager.validateAgent();
|
|
332
609
|
agentManager.clearSessionCache(chatId);
|
|
333
610
|
piAuthIssue = null;
|
|
334
611
|
logger?.log("telegram", `Pi auth renewal completed for chat ${chatId}`);
|
|
335
|
-
await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, verified: true }));
|
|
612
|
+
await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, verified: true }));
|
|
336
613
|
} catch (error) {
|
|
337
614
|
const issue = rememberPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
|
|
338
615
|
piAuthIssue = issue;
|
|
339
616
|
logger?.error("telegram", `Pi auth renewal failed for chat ${chatId}: ${getErrorMessage(error)}`);
|
|
340
|
-
await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, issue })).catch((notifyError) => {
|
|
617
|
+
await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, issue })).catch((notifyError) => {
|
|
341
618
|
logger?.error("telegram", `auth renewal failure notice failed for chat ${chatId}: ${getErrorMessage(notifyError)}`);
|
|
342
619
|
});
|
|
343
620
|
} finally {
|
|
@@ -407,26 +684,25 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
407
684
|
}
|
|
408
685
|
|
|
409
686
|
function getChatState(chatId) {
|
|
410
|
-
if (!perChatState.has(chatId)) {
|
|
411
|
-
perChatState.set(chatId, { processing: false, nextPrompt: "" });
|
|
412
|
-
}
|
|
413
687
|
return perChatState.get(chatId);
|
|
414
688
|
}
|
|
415
689
|
|
|
416
|
-
function getProviderModels() {
|
|
690
|
+
async function getProviderModels(chatId) {
|
|
417
691
|
const runtime = createPiRuntime({
|
|
418
692
|
provider: config.pi.provider,
|
|
419
693
|
apiKey: config.pi.apiKey
|
|
420
694
|
});
|
|
421
|
-
return listProviderModels(config.pi.provider, runtime);
|
|
695
|
+
return reverseModelOrder(listProviderModels(config.pi.provider, runtime));
|
|
422
696
|
}
|
|
423
697
|
|
|
424
698
|
async function showModelPicker(ctx, page = 0) {
|
|
699
|
+
const agentConfig = getAgentConfig(config);
|
|
425
700
|
const picker = buildModelPicker({
|
|
426
|
-
provider:
|
|
427
|
-
models: getProviderModels(),
|
|
701
|
+
provider: agentConfig.provider,
|
|
702
|
+
models: await getProviderModels(ctx.chat.id),
|
|
428
703
|
selectedModelId: resolveChatModel(config, ctx.chat.id),
|
|
429
704
|
selectedThinkingLevel: resolveChatThinkingLevel(config, ctx.chat.id),
|
|
705
|
+
selectedSpeed: resolveChatSpeed(config, ctx.chat.id),
|
|
430
706
|
page,
|
|
431
707
|
pageSize: config.telegram.modelPickerPageSize
|
|
432
708
|
});
|
|
@@ -439,10 +715,11 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
439
715
|
}
|
|
440
716
|
|
|
441
717
|
async function showEffortPicker(ctx, { model, modelIndex, selectedThinkingLevel } = {}) {
|
|
442
|
-
const
|
|
718
|
+
const agentConfig = getAgentConfig(config);
|
|
719
|
+
const models = await getProviderModels(ctx.chat.id);
|
|
443
720
|
const resolvedModel = model || models.find((item) => item.id === resolveChatModel(config, ctx.chat.id));
|
|
444
721
|
if (!resolvedModel) {
|
|
445
|
-
throw new Error(`Model not found for provider ${
|
|
722
|
+
throw new Error(`Model not found for provider ${agentConfig.provider}`);
|
|
446
723
|
}
|
|
447
724
|
if (!modelSupportsThinking(resolvedModel)) {
|
|
448
725
|
const text = `${resolvedModel.provider}/${resolvedModel.id} does not support effort levels.`;
|
|
@@ -468,20 +745,46 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
468
745
|
return ctx.reply(picker.text, extra);
|
|
469
746
|
}
|
|
470
747
|
|
|
748
|
+
async function showSpeedPicker(ctx) {
|
|
749
|
+
const agentConfig = getAgentConfig(config);
|
|
750
|
+
const models = await getProviderModels(ctx.chat.id);
|
|
751
|
+
const model = models.find((item) => item.id === resolveChatModel(config, ctx.chat.id));
|
|
752
|
+
if (!model) throw new Error(`Model not found for provider ${agentConfig.provider}`);
|
|
753
|
+
if (!modelSupportsSpeed(model)) {
|
|
754
|
+
const text = `${model.provider}/${model.id} does not support speed 1.5x.`;
|
|
755
|
+
if (ctx.callbackQuery?.message?.message_id) {
|
|
756
|
+
return ctx.api.editMessageText(ctx.chat.id, ctx.callbackQuery.message.message_id, text);
|
|
757
|
+
}
|
|
758
|
+
return ctx.reply(text);
|
|
759
|
+
}
|
|
760
|
+
const picker = buildSpeedPicker({
|
|
761
|
+
provider: model.provider,
|
|
762
|
+
modelId: model.id,
|
|
763
|
+
speeds: MODEL_SPEEDS,
|
|
764
|
+
selectedSpeed: resolveChatSpeed(config, ctx.chat.id)
|
|
765
|
+
});
|
|
766
|
+
const extra = { reply_markup: picker.replyMarkup };
|
|
767
|
+
const messageId = ctx.callbackQuery?.message?.message_id;
|
|
768
|
+
if (messageId) return ctx.api.editMessageText(ctx.chat.id, messageId, picker.text, extra);
|
|
769
|
+
return ctx.reply(picker.text, extra);
|
|
770
|
+
}
|
|
771
|
+
|
|
471
772
|
async function persistChatModel(chatId, model, thinkingLevel) {
|
|
773
|
+
const agentConfig = getAgentConfig(config);
|
|
472
774
|
const key = chatKey(chatId);
|
|
473
|
-
const hadSelections = Boolean(
|
|
474
|
-
const previousSelection =
|
|
775
|
+
const hadSelections = Boolean(agentConfig.chatModels);
|
|
776
|
+
const previousSelection = agentConfig.chatModels?.[key];
|
|
475
777
|
const level = clampModelThinkingLevel(model, thinkingLevel ?? resolveChatThinkingLevel(config, chatId));
|
|
476
|
-
|
|
778
|
+
const speed = clampModelSpeed(model, resolveChatSpeed(config, chatId));
|
|
779
|
+
selectChatModel(config, chatId, model, { thinkingLevel: level, speed });
|
|
477
780
|
try {
|
|
478
781
|
await saveConfig(config);
|
|
479
782
|
} catch (error) {
|
|
480
783
|
if (previousSelection) {
|
|
481
|
-
|
|
784
|
+
agentConfig.chatModels[key] = previousSelection;
|
|
482
785
|
} else {
|
|
483
|
-
delete
|
|
484
|
-
if (!hadSelections) delete
|
|
786
|
+
delete agentConfig.chatModels[key];
|
|
787
|
+
if (!hadSelections) delete agentConfig.chatModels;
|
|
485
788
|
}
|
|
486
789
|
throw error;
|
|
487
790
|
}
|
|
@@ -490,20 +793,44 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
490
793
|
}
|
|
491
794
|
|
|
492
795
|
async function persistChatEffort(chatId, model, thinkingLevel) {
|
|
796
|
+
const agentConfig = getAgentConfig(config);
|
|
493
797
|
const key = chatKey(chatId);
|
|
494
|
-
const hadSelections = Boolean(
|
|
495
|
-
const previousSelection =
|
|
798
|
+
const hadSelections = Boolean(agentConfig.chatModels);
|
|
799
|
+
const previousSelection = agentConfig.chatModels?.[key];
|
|
496
800
|
const level = clampModelThinkingLevel(model, thinkingLevel);
|
|
497
801
|
selectChatThinkingLevel(config, chatId, level);
|
|
498
802
|
try {
|
|
499
803
|
await saveConfig(config);
|
|
500
804
|
} catch (error) {
|
|
501
805
|
if (previousSelection) {
|
|
502
|
-
|
|
806
|
+
agentConfig.chatModels[key] = previousSelection;
|
|
807
|
+
} else {
|
|
808
|
+
delete agentConfig.chatModels[key];
|
|
809
|
+
if (!hadSelections) delete agentConfig.chatModels;
|
|
810
|
+
}
|
|
811
|
+
throw error;
|
|
812
|
+
}
|
|
813
|
+
return level;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async function persistChatSpeed(chatId, model, speed) {
|
|
817
|
+
const agentConfig = getAgentConfig(config);
|
|
818
|
+
const key = chatKey(chatId);
|
|
819
|
+
const hadSelections = Boolean(agentConfig.chatModels);
|
|
820
|
+
const previousSelection = agentConfig.chatModels?.[key];
|
|
821
|
+
const level = clampModelSpeed(model, speed);
|
|
822
|
+
await agentManager.setModelSpeed(chatId, level);
|
|
823
|
+
selectChatSpeed(config, chatId, level);
|
|
824
|
+
try {
|
|
825
|
+
await saveConfig(config);
|
|
826
|
+
} catch (error) {
|
|
827
|
+
if (previousSelection) {
|
|
828
|
+
agentConfig.chatModels[key] = previousSelection;
|
|
503
829
|
} else {
|
|
504
|
-
delete
|
|
505
|
-
if (!hadSelections) delete
|
|
830
|
+
delete agentConfig.chatModels[key];
|
|
831
|
+
if (!hadSelections) delete agentConfig.chatModels;
|
|
506
832
|
}
|
|
833
|
+
agentManager.clearSessionCache(chatId);
|
|
507
834
|
throw error;
|
|
508
835
|
}
|
|
509
836
|
return level;
|
|
@@ -526,6 +853,11 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
526
853
|
async function sendTextReply({ sendText, sendDocument, chatId, text }) {
|
|
527
854
|
const maxInlineReplyLength = 3500;
|
|
528
855
|
|
|
856
|
+
if (isSilentReply(text)) {
|
|
857
|
+
logger?.log("telegram", `suppressing silent reply for chat ${chatId}`);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
529
861
|
if (text.length > maxInlineReplyLength) {
|
|
530
862
|
logger?.log("telegram", `sending long reply as markdown attachment for chat ${chatId}`);
|
|
531
863
|
const chatArtifactStore = artifactStore.forChat(chatId);
|
|
@@ -544,7 +876,12 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
544
876
|
}
|
|
545
877
|
|
|
546
878
|
logger?.log("telegram", `sending text reply for chat ${chatId}`);
|
|
547
|
-
await sendText(renderTelegramHtml(text), { parse_mode: "HTML" });
|
|
879
|
+
const sent = await sendText(renderTelegramHtml(text), { parse_mode: "HTML" });
|
|
880
|
+
if (sent?.message_id) {
|
|
881
|
+
const messages = getChatState(chatId).assistantMessages;
|
|
882
|
+
messages.set(sent.message_id, text);
|
|
883
|
+
while (messages.size > 50) messages.delete(messages.keys().next().value);
|
|
884
|
+
}
|
|
548
885
|
}
|
|
549
886
|
|
|
550
887
|
function createTelegramSessionBridge(chatId) {
|
|
@@ -561,10 +898,35 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
561
898
|
};
|
|
562
899
|
}
|
|
563
900
|
|
|
901
|
+
agentManager.setArtifactDeliveryHandler?.(async ({ chatId, artifact, caption, method }) => {
|
|
902
|
+
const resolvedMethod = method
|
|
903
|
+
|| artifact.metadata?.delivery?.method
|
|
904
|
+
|| (artifact.kind === "audio" || artifact.mimeType?.startsWith("audio/") ? "audio"
|
|
905
|
+
: artifact.kind === "image" || artifact.mimeType?.startsWith("image/") ? "photo"
|
|
906
|
+
: artifact.kind === "video" || artifact.mimeType?.startsWith("video/") ? "video"
|
|
907
|
+
: "document");
|
|
908
|
+
const safeCaption = caption && !/(^|\s)(\/[^\s]|[A-Za-z]:[\\/])/.test(caption) ? caption : undefined;
|
|
909
|
+
await createTelegramSessionBridge(chatId).sendMedia(artifact.path, {
|
|
910
|
+
method: resolvedMethod,
|
|
911
|
+
caption: safeCaption,
|
|
912
|
+
filename: path.basename(artifact.path)
|
|
913
|
+
});
|
|
914
|
+
return { ok: true, artifactId: artifact.id, method: resolvedMethod };
|
|
915
|
+
});
|
|
916
|
+
|
|
564
917
|
async function processPromptForChat({ chatId, prompt, ctx = null }) {
|
|
565
918
|
const work = async () => {
|
|
566
919
|
const { session } = await agentManager.getSessionContext(chatId, createTelegramSessionBridge(chatId));
|
|
920
|
+
const historyRevision = getChatState(chatId).historyRevision;
|
|
921
|
+
await conversationHistory.ensureSeed(chatId, {
|
|
922
|
+
runtime: "pi",
|
|
923
|
+
history: formatPortableSessionHistory(session.messages)
|
|
924
|
+
});
|
|
567
925
|
let text = "";
|
|
926
|
+
let steeredPrompts = [];
|
|
927
|
+
const chatState = getChatState(chatId);
|
|
928
|
+
chatState.activeSession = session;
|
|
929
|
+
chatState.activeSteers = [];
|
|
568
930
|
try {
|
|
569
931
|
text = await collectText(session, prompt, {
|
|
570
932
|
logger,
|
|
@@ -577,6 +939,20 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
577
939
|
} catch (error) {
|
|
578
940
|
agentManager.resetSession(chatId);
|
|
579
941
|
throw error;
|
|
942
|
+
} finally {
|
|
943
|
+
steeredPrompts = [...chatState.activeSteers];
|
|
944
|
+
if (chatState.activeSession === session) chatState.activeSession = null;
|
|
945
|
+
chatState.activeSteers = [];
|
|
946
|
+
}
|
|
947
|
+
if (getChatState(chatId).historyRevision === historyRevision) {
|
|
948
|
+
const historyPrompt = steeredPrompts.length
|
|
949
|
+
? [prompt, ...steeredPrompts.map((message) => `[Steering message]\n${message}`)].join("\n\n")
|
|
950
|
+
: prompt;
|
|
951
|
+
await conversationHistory.appendTurn(chatId, {
|
|
952
|
+
runtime: "pi",
|
|
953
|
+
prompt: historyPrompt,
|
|
954
|
+
response: text
|
|
955
|
+
});
|
|
580
956
|
}
|
|
581
957
|
if (text) {
|
|
582
958
|
await sendTextReply({
|
|
@@ -592,46 +968,53 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
592
968
|
return work();
|
|
593
969
|
}
|
|
594
970
|
|
|
595
|
-
async function enqueuePrompt({ chatId, prompt, label, ctx = null }) {
|
|
971
|
+
async function enqueuePrompt({ chatId, prompt, label, ctx = null, replaceQueued = false, busyMessageMode = "queue" }) {
|
|
596
972
|
const chatState = getChatState(chatId);
|
|
597
973
|
|
|
598
974
|
if (chatState.processing) {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
:
|
|
975
|
+
const routed = await routeBusyPrompt({
|
|
976
|
+
chatState,
|
|
977
|
+
prompt,
|
|
978
|
+
mode: busyMessageMode,
|
|
979
|
+
replaceQueued
|
|
980
|
+
});
|
|
981
|
+
if (routed.disposition === "steered") {
|
|
982
|
+
logger?.log("telegram", `chat ${chatId} busy, steering ${label}`);
|
|
983
|
+
} else {
|
|
984
|
+
logger?.log("telegram", `chat ${chatId} busy, queueing ${label}`);
|
|
985
|
+
if (routed.steerError) {
|
|
986
|
+
logger?.log("telegram", `steer failed for chat ${chatId}, queued instead: ${getErrorMessage(routed.steerError)}`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (replaceQueued) chatState.continueAfterClose = true;
|
|
603
990
|
return;
|
|
604
991
|
}
|
|
605
992
|
|
|
606
993
|
chatState.processing = true;
|
|
607
994
|
logger?.log("telegram", `processing ${label} in chat ${chatId}`);
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
try {
|
|
612
|
-
while (currentPrompt) {
|
|
613
|
-
try {
|
|
614
|
-
logger?.log("telegram", `prompt dispatch for chat ${chatId}`);
|
|
615
|
-
await processPromptForChat({ chatId, prompt: currentPrompt, ctx: currentCtx });
|
|
616
|
-
} catch (error) {
|
|
617
|
-
const message = getErrorMessage(error);
|
|
618
|
-
logger?.error("telegram", `${label} failed for chat ${chatId}: ${message}`);
|
|
619
|
-
await notifyPiAuthIssueIfNeeded(chatId, error);
|
|
620
|
-
throw error;
|
|
621
|
-
} finally {
|
|
622
|
-
currentCtx = null;
|
|
623
|
-
}
|
|
995
|
+
return processChatPromptQueue({ chatId, prompt, label, ctx });
|
|
996
|
+
}
|
|
624
997
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
998
|
+
function processChatPromptQueue({ chatId, prompt, label, ctx = null, beforeInitialPrompt }) {
|
|
999
|
+
const chatState = getChatState(chatId);
|
|
1000
|
+
return drainChatPromptQueue({
|
|
1001
|
+
chatState,
|
|
1002
|
+
initialPrompt: prompt,
|
|
1003
|
+
initialCtx: ctx,
|
|
1004
|
+
beforeInitialPrompt,
|
|
1005
|
+
processPrompt: ({ prompt: currentPrompt, ctx: currentCtx }) => {
|
|
1006
|
+
logger?.log("telegram", `prompt dispatch for chat ${chatId}`);
|
|
1007
|
+
return processPromptForChat({ chatId, prompt: currentPrompt, ctx: currentCtx });
|
|
1008
|
+
},
|
|
1009
|
+
onPromptInterrupted: (error) => {
|
|
1010
|
+
logger?.log("telegram", `${label} interrupted by queued /new for chat ${chatId}: ${getErrorMessage(error)}`);
|
|
1011
|
+
},
|
|
1012
|
+
onPromptFailure: async (error) => {
|
|
1013
|
+
const message = getErrorMessage(error);
|
|
1014
|
+
logger?.error("telegram", `${label} failed for chat ${chatId}: ${message}`);
|
|
1015
|
+
await notifyPiAuthIssueIfNeeded(chatId, error);
|
|
631
1016
|
}
|
|
632
|
-
}
|
|
633
|
-
chatState.processing = false;
|
|
634
|
-
}
|
|
1017
|
+
});
|
|
635
1018
|
}
|
|
636
1019
|
|
|
637
1020
|
async function enqueueOrProcess(ctx) {
|
|
@@ -639,10 +1022,14 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
639
1022
|
|
|
640
1023
|
if (chatState.processing) {
|
|
641
1024
|
const incomingPrompt = await buildIncomingPrompt(ctx);
|
|
1025
|
+
const busyMessageMode = typeof ctx.message?.text === "string"
|
|
1026
|
+
? resolveTelegramBusyMessageMode(config, ctx.chat.id)
|
|
1027
|
+
: "queue";
|
|
642
1028
|
return enqueuePrompt({
|
|
643
1029
|
chatId: ctx.chat.id,
|
|
644
1030
|
prompt: incomingPrompt,
|
|
645
|
-
label: `message ${ctx.msg.message_id}
|
|
1031
|
+
label: `message ${ctx.msg.message_id}`,
|
|
1032
|
+
busyMessageMode
|
|
646
1033
|
});
|
|
647
1034
|
}
|
|
648
1035
|
|
|
@@ -747,14 +1134,61 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
747
1134
|
}
|
|
748
1135
|
}
|
|
749
1136
|
|
|
1137
|
+
async function summarizeSessionBeforeReset(chatId) {
|
|
1138
|
+
try {
|
|
1139
|
+
const context = await agentManager.getSessionContext(chatId, createTelegramSessionBridge(chatId));
|
|
1140
|
+
const parentSession = context.session.sessionFile || "";
|
|
1141
|
+
if (!context.session.messages.length) return { handoff: "", parentSession: "" };
|
|
1142
|
+
|
|
1143
|
+
const summary = await collectText(context.session, buildSessionHandoffPrompt(), { logger, chatId });
|
|
1144
|
+
return { handoff: sanitizeSessionHandoff(summary), parentSession };
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
logger?.log("agent", `session handoff summary failed for chat ${chatId}: ${getErrorMessage(error)}`);
|
|
1147
|
+
return { handoff: "", parentSession: "" };
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
750
1151
|
async function handleNewCommand(ctx) {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1152
|
+
const chatState = getChatState(ctx.chat.id);
|
|
1153
|
+
const wasProcessing = chatState.processing;
|
|
1154
|
+
chatState.historyRevision += 1;
|
|
1155
|
+
const commandRevision = chatState.historyRevision;
|
|
1156
|
+
const prompt = buildNewSessionPrompt(ctx);
|
|
1157
|
+
|
|
1158
|
+
if (wasProcessing) {
|
|
1159
|
+
logger?.log("telegram", `chat ${ctx.chat.id} busy, queueing new-session command`);
|
|
1160
|
+
queueChatPrompt(chatState, prompt, { replace: true });
|
|
1161
|
+
chatState.continueAfterClose = true;
|
|
1162
|
+
const reset = (async () => {
|
|
1163
|
+
await conversationHistory.reset(ctx.chat.id, { runtime: "pi" });
|
|
1164
|
+
agentManager.resetSession(ctx.chat.id);
|
|
1165
|
+
})();
|
|
1166
|
+
chatState.beforeNextPrompt = reset;
|
|
1167
|
+
try {
|
|
1168
|
+
await reset;
|
|
1169
|
+
} finally {
|
|
1170
|
+
if (chatState.beforeNextPrompt === reset) chatState.beforeNextPrompt = null;
|
|
1171
|
+
}
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
chatState.processing = true;
|
|
1176
|
+
logger?.log("telegram", `processing new-session command in chat ${ctx.chat.id}`);
|
|
1177
|
+
await processChatPromptQueue({
|
|
754
1178
|
chatId: ctx.chat.id,
|
|
755
|
-
prompt
|
|
1179
|
+
prompt,
|
|
756
1180
|
label: "new-session command",
|
|
757
|
-
ctx
|
|
1181
|
+
ctx,
|
|
1182
|
+
beforeInitialPrompt: async () => {
|
|
1183
|
+
const handoff = await withTyping(ctx, () => summarizeSessionBeforeReset(ctx.chat.id));
|
|
1184
|
+
if (chatState.historyRevision !== commandRevision) return;
|
|
1185
|
+
await conversationHistory.reset(ctx.chat.id, {
|
|
1186
|
+
runtime: "pi",
|
|
1187
|
+
history: handoff.handoff
|
|
1188
|
+
});
|
|
1189
|
+
if (chatState.historyRevision !== commandRevision) return;
|
|
1190
|
+
agentManager.resetSession(ctx.chat.id, handoff);
|
|
1191
|
+
}
|
|
758
1192
|
});
|
|
759
1193
|
}
|
|
760
1194
|
|
|
@@ -775,6 +1209,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
775
1209
|
if (piAuthIssue) {
|
|
776
1210
|
await ctx.reply(buildPiAuthRecoveryBlockedMessage({
|
|
777
1211
|
config,
|
|
1212
|
+
chatId: ctx.chat.id,
|
|
778
1213
|
issue: piAuthIssue,
|
|
779
1214
|
renewalActive: authRenewals.has(chatKey(ctx.chat.id))
|
|
780
1215
|
}));
|
|
@@ -783,6 +1218,48 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
783
1218
|
await handleNewCommand(ctx);
|
|
784
1219
|
});
|
|
785
1220
|
|
|
1221
|
+
bot.command("restart", handleRestartCommand);
|
|
1222
|
+
|
|
1223
|
+
bot.command("doctor", async (ctx) => {
|
|
1224
|
+
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
1225
|
+
if (!auth.ok) return;
|
|
1226
|
+
const pending = await ctx.reply(renderTelegramHtml("```text\nRunning Arisa Doctor…\n```"), { parse_mode: "HTML" });
|
|
1227
|
+
try {
|
|
1228
|
+
await ctx.api.editMessageText(
|
|
1229
|
+
ctx.chat.id,
|
|
1230
|
+
pending.message_id,
|
|
1231
|
+
renderTelegramHtml(formatDoctorReport(await doctor())),
|
|
1232
|
+
{ parse_mode: "HTML" }
|
|
1233
|
+
);
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
logger?.error("doctor", `doctor command failed: ${getErrorMessage(error)}`);
|
|
1236
|
+
await ctx.api.editMessageText(ctx.chat.id, pending.message_id, `Arisa Doctor failed: ${getErrorMessage(error)}`);
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
bot.command("update", async (ctx) => {
|
|
1241
|
+
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
1242
|
+
if (!auth.ok) return;
|
|
1243
|
+
const pending = await ctx.reply(renderTelegramHtml("```text\nChecking Arisa and official tool updates…\n```"), { parse_mode: "HTML" });
|
|
1244
|
+
try {
|
|
1245
|
+
await ctx.api.editMessageText(
|
|
1246
|
+
ctx.chat.id,
|
|
1247
|
+
pending.message_id,
|
|
1248
|
+
renderTelegramHtml(await checkUpdates(ctx.chat.id)),
|
|
1249
|
+
{ parse_mode: "HTML" }
|
|
1250
|
+
);
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
logger?.error("update", `update check failed: ${getErrorMessage(error)}`);
|
|
1253
|
+
await ctx.api.editMessageText(ctx.chat.id, pending.message_id, `Arisa update check failed: ${getErrorMessage(error)}`);
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
bot.command("tools", async (ctx) => {
|
|
1258
|
+
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
1259
|
+
if (!auth.ok) return;
|
|
1260
|
+
await ctx.reply(renderTelegramHtml(formatToolUsageReport(await toolRegistry.usage(ctx.chat.id))), { parse_mode: "HTML" });
|
|
1261
|
+
});
|
|
1262
|
+
|
|
786
1263
|
bot.command("model", async (ctx) => {
|
|
787
1264
|
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
788
1265
|
if (!auth.ok) return;
|
|
@@ -795,22 +1272,28 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
795
1272
|
await showEffortPicker(ctx);
|
|
796
1273
|
});
|
|
797
1274
|
|
|
1275
|
+
bot.command("speed", async (ctx) => {
|
|
1276
|
+
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
1277
|
+
if (!auth.ok) return;
|
|
1278
|
+
await showSpeedPicker(ctx);
|
|
1279
|
+
});
|
|
1280
|
+
|
|
798
1281
|
bot.command("auth", async (ctx) => {
|
|
799
1282
|
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
800
1283
|
if (!auth.ok) return;
|
|
801
1284
|
|
|
802
|
-
const status = getPiAuthStatus(config);
|
|
1285
|
+
const status = getPiAuthStatus(config, ctx.chat.id);
|
|
803
1286
|
if (status.hasApiKey || !status.supportsOAuth) {
|
|
804
1287
|
await withTyping(ctx, async () => {
|
|
805
1288
|
try {
|
|
806
|
-
await agentManager.
|
|
1289
|
+
await agentManager.validateAgent();
|
|
807
1290
|
agentManager.clearSessionCache(ctx.chat.id);
|
|
808
1291
|
piAuthIssue = null;
|
|
809
|
-
await ctx.reply(buildPiAuthTelegramMessage({ config, verified: true }));
|
|
1292
|
+
await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, verified: true }));
|
|
810
1293
|
} catch (error) {
|
|
811
1294
|
const issue = rememberPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
|
|
812
1295
|
piAuthIssue = issue;
|
|
813
|
-
await ctx.reply(buildPiAuthTelegramMessage({ config, issue }));
|
|
1296
|
+
await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue }));
|
|
814
1297
|
}
|
|
815
1298
|
});
|
|
816
1299
|
return;
|
|
@@ -824,14 +1307,15 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
824
1307
|
} catch (error) {
|
|
825
1308
|
const issue = rememberPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
|
|
826
1309
|
piAuthIssue = issue;
|
|
827
|
-
await ctx.reply(buildPiAuthTelegramMessage({ config, issue }));
|
|
1310
|
+
await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue }));
|
|
828
1311
|
}
|
|
829
1312
|
});
|
|
830
1313
|
|
|
831
1314
|
bot.on("callback_query:data", async (ctx, next) => {
|
|
832
1315
|
const modelAction = parseModelPickerAction(ctx.callbackQuery.data);
|
|
833
1316
|
const effortAction = modelAction ? null : parseEffortPickerAction(ctx.callbackQuery.data);
|
|
834
|
-
const
|
|
1317
|
+
const speedAction = modelAction || effortAction ? null : parseSpeedPickerAction(ctx.callbackQuery.data);
|
|
1318
|
+
const action = modelAction || effortAction || speedAction;
|
|
835
1319
|
if (!action) return next();
|
|
836
1320
|
if (action.type === "noop") {
|
|
837
1321
|
await ctx.answerCallbackQuery();
|
|
@@ -851,7 +1335,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
851
1335
|
return;
|
|
852
1336
|
}
|
|
853
1337
|
|
|
854
|
-
const models = getProviderModels();
|
|
1338
|
+
const models = await getProviderModels(ctx.chat.id);
|
|
855
1339
|
const chatBusy = getChatState(ctx.chat.id).processing;
|
|
856
1340
|
|
|
857
1341
|
if (action.type === "select") {
|
|
@@ -886,7 +1370,10 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
886
1370
|
const currentModelId = resolveChatModel(config, ctx.chat.id);
|
|
887
1371
|
const currentEffort = resolveChatThinkingLevel(config, ctx.chat.id);
|
|
888
1372
|
if (model.id === currentModelId && currentEffort === "off") {
|
|
889
|
-
await ctx
|
|
1373
|
+
await closeModelPicker(ctx, {
|
|
1374
|
+
messageText: `Already using ${model.provider}/${model.id}.`,
|
|
1375
|
+
callbackText: `Already using ${model.id}.`
|
|
1376
|
+
});
|
|
890
1377
|
return;
|
|
891
1378
|
}
|
|
892
1379
|
|
|
@@ -921,7 +1408,10 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
921
1408
|
const currentModelId = resolveChatModel(config, ctx.chat.id);
|
|
922
1409
|
const currentEffort = resolveChatThinkingLevel(config, ctx.chat.id);
|
|
923
1410
|
if (model.id === currentModelId && action.level === currentEffort) {
|
|
924
|
-
await ctx
|
|
1411
|
+
await closeModelPicker(ctx, {
|
|
1412
|
+
messageText: `Already using ${model.provider}/${model.id} (effort: ${action.level}).`,
|
|
1413
|
+
callbackText: `Already using ${model.id} at ${action.level}.`
|
|
1414
|
+
});
|
|
925
1415
|
return;
|
|
926
1416
|
}
|
|
927
1417
|
|
|
@@ -981,7 +1471,10 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
981
1471
|
}
|
|
982
1472
|
const currentEffort = resolveChatThinkingLevel(config, ctx.chat.id);
|
|
983
1473
|
if (action.level === currentEffort) {
|
|
984
|
-
await ctx
|
|
1474
|
+
await closeModelPicker(ctx, {
|
|
1475
|
+
messageText: `Already using effort ${action.level} for ${model.provider}/${model.id}.`,
|
|
1476
|
+
callbackText: `Already using effort ${action.level}.`
|
|
1477
|
+
});
|
|
985
1478
|
return;
|
|
986
1479
|
}
|
|
987
1480
|
await persistChatEffort(ctx.chat.id, model, action.level);
|
|
@@ -991,16 +1484,69 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
991
1484
|
`Effort set to ${action.level} for ${model.provider}/${model.id}.`
|
|
992
1485
|
);
|
|
993
1486
|
await ctx.answerCallbackQuery({ text: `Effort: ${action.level}.` });
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
if (action.type === "speed") {
|
|
1491
|
+
const model = models.find((item) => item.id === resolveChatModel(config, ctx.chat.id));
|
|
1492
|
+
if (!model) {
|
|
1493
|
+
await ctx.answerCallbackQuery({
|
|
1494
|
+
text: "Current model is unavailable. Run /model again.",
|
|
1495
|
+
show_alert: true
|
|
1496
|
+
});
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
if (!modelSupportsSpeed(model)) {
|
|
1500
|
+
await ctx.answerCallbackQuery({
|
|
1501
|
+
text: "This model does not support speed 1.5x.",
|
|
1502
|
+
show_alert: true
|
|
1503
|
+
});
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
const currentSpeed = resolveChatSpeed(config, ctx.chat.id);
|
|
1507
|
+
if (action.speed === currentSpeed) {
|
|
1508
|
+
await closeModelPicker(ctx, {
|
|
1509
|
+
messageText: `Already using speed ${action.speed.toFixed(1)}x for ${model.provider}/${model.id}.`,
|
|
1510
|
+
callbackText: `Already using speed ${action.speed.toFixed(1)}x.`
|
|
1511
|
+
});
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
await persistChatSpeed(ctx.chat.id, model, action.speed);
|
|
1515
|
+
await ctx.api.editMessageText(
|
|
1516
|
+
ctx.chat.id,
|
|
1517
|
+
ctx.callbackQuery.message.message_id,
|
|
1518
|
+
`Speed set to ${action.speed.toFixed(1)}x for ${model.provider}/${model.id}.`
|
|
1519
|
+
);
|
|
1520
|
+
await ctx.answerCallbackQuery({ text: `Speed: ${action.speed.toFixed(1)}x.` });
|
|
994
1521
|
}
|
|
995
1522
|
} catch (error) {
|
|
996
1523
|
logger?.error("telegram", `model selection failed for chat ${ctx.chat.id}: ${getErrorMessage(error)}`);
|
|
997
1524
|
await ctx.answerCallbackQuery({
|
|
998
|
-
text: "Could not change the model or
|
|
1525
|
+
text: "Could not change the model, effort, or speed.",
|
|
999
1526
|
show_alert: true
|
|
1000
1527
|
}).catch(() => {});
|
|
1001
1528
|
}
|
|
1002
1529
|
});
|
|
1003
1530
|
|
|
1531
|
+
bot.on("message_reaction", async (ctx) => {
|
|
1532
|
+
const reaction = ctx.messageReaction;
|
|
1533
|
+
const chatId = reaction.chat.id;
|
|
1534
|
+
const auth = await authorizeChat({ config, chatId, saveConfig });
|
|
1535
|
+
if (!auth.ok || piAuthIssue) return;
|
|
1536
|
+
|
|
1537
|
+
const reactedMessageText = getChatState(chatId).assistantMessages.get(reaction.message_id) || "";
|
|
1538
|
+
const prompt = buildReactionPrompt({ reaction, reactedMessageText });
|
|
1539
|
+
enqueuePrompt({
|
|
1540
|
+
chatId,
|
|
1541
|
+
prompt,
|
|
1542
|
+
label: `reaction to message ${reaction.message_id}`,
|
|
1543
|
+
busyMessageMode: "queue"
|
|
1544
|
+
}).catch((error) => {
|
|
1545
|
+
getChatState(chatId).processing = false;
|
|
1546
|
+
logger?.error("telegram", `reaction handling failed for chat ${chatId}: ${getErrorMessage(error)}`);
|
|
1547
|
+
});
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1004
1550
|
bot.on("message", async (ctx) => {
|
|
1005
1551
|
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
1006
1552
|
if (!auth.ok) return;
|
|
@@ -1013,34 +1559,30 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1013
1559
|
if (piAuthIssue) {
|
|
1014
1560
|
await ctx.reply(buildPiAuthRecoveryBlockedMessage({
|
|
1015
1561
|
config,
|
|
1562
|
+
chatId: ctx.chat.id,
|
|
1016
1563
|
issue: piAuthIssue,
|
|
1017
1564
|
renewalActive: authRenewals.has(chatKey(ctx.chat.id))
|
|
1018
1565
|
}));
|
|
1019
1566
|
return;
|
|
1020
1567
|
}
|
|
1021
1568
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1569
|
+
// grammY long polling awaits each middleware. Keep prompt execution in the background so
|
|
1570
|
+
// the next Telegram update can reach the active session as a steer or queued message.
|
|
1571
|
+
enqueueOrProcess(ctx).catch(async (error) => {
|
|
1025
1572
|
const chatState = getChatState(ctx.chat.id);
|
|
1026
1573
|
chatState.processing = false;
|
|
1027
1574
|
if (wasPromptErrorNotified(error)) return;
|
|
1028
1575
|
const issue = getPiAuthIssue(error);
|
|
1029
1576
|
await ctx.reply(issue
|
|
1030
|
-
? buildPiAuthTelegramMessage({ config, issue })
|
|
1577
|
+
? buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue })
|
|
1031
1578
|
: getErrorMessage(error));
|
|
1032
|
-
}
|
|
1579
|
+
});
|
|
1033
1580
|
});
|
|
1034
1581
|
|
|
1035
1582
|
return {
|
|
1036
1583
|
async start({ skipAgentStartupPrompts = false } = {}) {
|
|
1037
1584
|
config.telegram.chatMeta ||= {};
|
|
1038
|
-
await bot.api.setMyCommands(
|
|
1039
|
-
{ command: "new", description: "Start a new chat context" },
|
|
1040
|
-
{ command: "model", description: "Choose the model for this chat" },
|
|
1041
|
-
{ command: "effort", description: "Choose reasoning effort for this chat" },
|
|
1042
|
-
{ command: "auth", description: "Show Pi authentication status" }
|
|
1043
|
-
]);
|
|
1585
|
+
await bot.api.setMyCommands(telegramCommands);
|
|
1044
1586
|
if (!taskTimer) {
|
|
1045
1587
|
taskTimer = setInterval(() => {
|
|
1046
1588
|
dispatchDueTasks().catch((error) => {
|
|
@@ -1052,7 +1594,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1052
1594
|
await bot.api.deleteWebhook({ drop_pending_updates: true });
|
|
1053
1595
|
logger?.log("telegram", "bot polling started");
|
|
1054
1596
|
scheduleStartupMessages({ skipAgentStartupPrompts });
|
|
1055
|
-
await bot.start();
|
|
1597
|
+
await bot.start({ allowed_updates: ["message", "callback_query", "message_reaction"] });
|
|
1056
1598
|
},
|
|
1057
1599
|
|
|
1058
1600
|
async stop() {
|