arisa 4.3.4 → 5.0.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 +18 -17
- 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 +27 -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 +47 -11
- package/src/runtime/doctor.js +307 -0
- package/src/runtime/log-viewer.js +165 -0
- package/src/runtime/paths.js +4 -1
- package/src/runtime/service-manager.js +106 -8
- package/src/runtime/tool-process-supervisor.js +107 -10
- package/src/transport/telegram/bot.js +533 -99
- 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 +279 -0
- package/test/daemon-runtime.test.js +130 -2
- package/test/dependency-warnings.test.js +17 -0
- package/test/doctor.test.js +90 -0
- package/test/log-viewer.test.js +90 -0
- package/test/model-selection.test.js +125 -2
- package/test/paths.test.js +8 -0
- package/test/pi-compaction.test.js +43 -0
- package/test/service-manager.test.js +234 -0
- package/test/task-store.test.js +31 -0
|
@@ -3,16 +3,58 @@ 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";
|
|
13
17
|
|
|
14
18
|
const slowPromptNoticeMs = 300_000;
|
|
15
19
|
|
|
20
|
+
export const telegramCommands = Object.freeze([
|
|
21
|
+
{ command: "new", description: "Start a new chat context" },
|
|
22
|
+
{ command: "restart", description: "Restart the Arisa service" },
|
|
23
|
+
{ command: "doctor", description: "Check and repair Arisa runtime health" },
|
|
24
|
+
{ command: "model", description: "Choose the model for this chat" },
|
|
25
|
+
{ command: "effort", description: "Choose reasoning effort for this chat" },
|
|
26
|
+
{ command: "speed", description: "Choose model speed for this chat" },
|
|
27
|
+
{ command: "auth", description: "Show authentication status" }
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export function createTelegramRestartHandler({ authorize, requestRestart, logger }) {
|
|
31
|
+
if (typeof authorize !== "function" || typeof requestRestart !== "function") {
|
|
32
|
+
throw new Error("Telegram restart requires authorization and restart handoff functions");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let restartRequested = false;
|
|
36
|
+
return async (ctx) => {
|
|
37
|
+
const auth = await authorize(ctx);
|
|
38
|
+
if (!auth.ok) return;
|
|
39
|
+
|
|
40
|
+
if (restartRequested) {
|
|
41
|
+
await ctx.reply("An Arisa restart is already in progress.");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
restartRequested = true;
|
|
46
|
+
try {
|
|
47
|
+
await ctx.reply("Arisa is restarting. I'll be back shortly.");
|
|
48
|
+
const handoff = await requestRestart();
|
|
49
|
+
logger?.log("telegram", `restart handed off to process ${handoff.pid}`);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
restartRequested = false;
|
|
52
|
+
logger?.error("telegram", `restart handoff failed: ${getErrorMessage(error)}`);
|
|
53
|
+
await ctx.reply(`Arisa could not be restarted: ${getErrorMessage(error)}`);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
16
58
|
function quotedMessageSummary(message) {
|
|
17
59
|
if (!message) return [];
|
|
18
60
|
|
|
@@ -219,12 +261,13 @@ function buildStartupMessage(chatMeta = {}) {
|
|
|
219
261
|
return "Arisa is back online.";
|
|
220
262
|
}
|
|
221
263
|
|
|
222
|
-
async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {}) {
|
|
264
|
+
export async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {}) {
|
|
223
265
|
let text = "";
|
|
224
266
|
let assistantErrorMessage = "";
|
|
225
267
|
let shouldSeparateAssistantMessage = false;
|
|
226
268
|
let slowPromptTimer = null;
|
|
227
269
|
const unsubscribe = session.subscribe((event) => {
|
|
270
|
+
if (event.arisaPromptScoped === false) return;
|
|
228
271
|
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
229
272
|
shouldSeparateAssistantMessage = text.trim().length > 0;
|
|
230
273
|
}
|
|
@@ -235,8 +278,13 @@ async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {
|
|
|
235
278
|
}
|
|
236
279
|
text += event.assistantMessageEvent.delta;
|
|
237
280
|
}
|
|
238
|
-
if (event.type === "message_end" && event.message?.
|
|
239
|
-
|
|
281
|
+
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
282
|
+
if (event.message.stopReason === "error") {
|
|
283
|
+
assistantErrorMessage = event.message.errorMessage || "assistant message ended with error";
|
|
284
|
+
} else if (event.message.stopReason !== "aborted") {
|
|
285
|
+
// Auto-compaction and retry can emit a transient error before a successful continuation.
|
|
286
|
+
assistantErrorMessage = "";
|
|
287
|
+
}
|
|
240
288
|
}
|
|
241
289
|
const logMessage = sessionEventLogMessage(event);
|
|
242
290
|
if (logMessage) logger?.log("agent", `chat ${chatId} ${logMessage}`);
|
|
@@ -265,6 +313,32 @@ async function collectText(session, prompt, { logger, chatId, onSlowPrompt } = {
|
|
|
265
313
|
return text.trim();
|
|
266
314
|
}
|
|
267
315
|
|
|
316
|
+
export function isSilentReply(text) {
|
|
317
|
+
return /^(?:NO_REPLY|No reply needed\.|No action needed\.)(?:\s+(?:NO_REPLY|No reply needed\.|No action needed\.))*$/.test(String(text || "").trim());
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function buildSessionHandoffPrompt() {
|
|
321
|
+
return [
|
|
322
|
+
"Prepare a concise handoff for the next Arisa session.",
|
|
323
|
+
"Review the entire active session, including any previous compaction summaries and the latest messages.",
|
|
324
|
+
"Keep only durable context: current goals or projects, decisions, user preferences, unresolved tasks, and important facts needed to continue.",
|
|
325
|
+
"Use at most 8 short bullets and at most 1600 characters.",
|
|
326
|
+
"Exclude secrets, tokens, passwords, cookies, API keys, private file paths, full transcripts, and stale chatter.",
|
|
327
|
+
"Do not take actions, call tools, send messages, or explain the process.",
|
|
328
|
+
"Return only the handoff."
|
|
329
|
+
].join("\n");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function sanitizeSessionHandoff(text) {
|
|
333
|
+
const sanitized = String(text || "")
|
|
334
|
+
.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi, "[redacted private key]")
|
|
335
|
+
.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]")
|
|
336
|
+
.replace(/(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|client[_ -]?secret|password|cookie|secret)\s*[:=]\s*[^\s,;]+/gi, "[redacted credential]")
|
|
337
|
+
.trim();
|
|
338
|
+
if (sanitized.length <= 4000) return sanitized;
|
|
339
|
+
return `${sanitized.slice(0, 3997).trim()}...`;
|
|
340
|
+
}
|
|
341
|
+
|
|
268
342
|
async function withTyping(ctx, work) {
|
|
269
343
|
await ctx.api.sendChatAction(ctx.chat.id, "typing");
|
|
270
344
|
const timer = setInterval(() => {
|
|
@@ -278,14 +352,151 @@ async function withTyping(ctx, work) {
|
|
|
278
352
|
}
|
|
279
353
|
}
|
|
280
354
|
|
|
281
|
-
export
|
|
355
|
+
export function createChatStateStore() {
|
|
356
|
+
const states = new Map();
|
|
357
|
+
|
|
358
|
+
function reset(chatId) {
|
|
359
|
+
const state = {
|
|
360
|
+
processing: false,
|
|
361
|
+
pendingPrompts: [],
|
|
362
|
+
continueAfterClose: false,
|
|
363
|
+
historyRevision: 0,
|
|
364
|
+
beforeNextPrompt: null,
|
|
365
|
+
activeSession: null,
|
|
366
|
+
activeSteers: []
|
|
367
|
+
};
|
|
368
|
+
states.set(String(chatId), state);
|
|
369
|
+
return state;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
get(chatId) {
|
|
374
|
+
const key = String(chatId);
|
|
375
|
+
return states.get(key) || reset(key);
|
|
376
|
+
},
|
|
377
|
+
reset,
|
|
378
|
+
anyProcessing() {
|
|
379
|
+
return [...states.values()].some((state) => state.processing);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function queueChatPrompt(chatState, prompt, { replace = false } = {}) {
|
|
385
|
+
if (replace) chatState.pendingPrompts = [];
|
|
386
|
+
chatState.pendingPrompts.push(prompt);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function takeQueuedPrompt(chatState) {
|
|
390
|
+
return chatState.pendingPrompts.shift() || "";
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function resolveTelegramBusyMessageMode(config, chatId) {
|
|
394
|
+
const chatMode = config.telegram?.chatMeta?.[String(chatId)]?.busyMessageMode;
|
|
395
|
+
const mode = chatMode || config.telegram?.busyMessageMode;
|
|
396
|
+
return mode === "steer" ? "steer" : "queue";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export async function routeBusyPrompt({ chatState, prompt, mode = "queue", replaceQueued = false }) {
|
|
400
|
+
const session = chatState.activeSession;
|
|
401
|
+
if (
|
|
402
|
+
mode === "steer"
|
|
403
|
+
&& !replaceQueued
|
|
404
|
+
&& !chatState.continueAfterClose
|
|
405
|
+
&& !chatState.beforeNextPrompt
|
|
406
|
+
&& session?.isStreaming
|
|
407
|
+
&& typeof session.steer === "function"
|
|
408
|
+
) {
|
|
409
|
+
try {
|
|
410
|
+
await session.steer(prompt);
|
|
411
|
+
chatState.activeSteers.push(prompt);
|
|
412
|
+
return { disposition: "steered" };
|
|
413
|
+
} catch (error) {
|
|
414
|
+
queueChatPrompt(chatState, prompt);
|
|
415
|
+
return { disposition: "queued", steerError: error };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
queueChatPrompt(chatState, prompt, { replace: replaceQueued });
|
|
420
|
+
return { disposition: "queued" };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export async function drainChatPromptQueue({
|
|
424
|
+
chatState,
|
|
425
|
+
initialPrompt,
|
|
426
|
+
initialCtx = null,
|
|
427
|
+
processPrompt,
|
|
428
|
+
onPromptFailure,
|
|
429
|
+
onPromptInterrupted,
|
|
430
|
+
beforeInitialPrompt
|
|
431
|
+
}) {
|
|
432
|
+
let currentPrompt = initialPrompt;
|
|
433
|
+
let currentCtx = initialCtx;
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
await beforeInitialPrompt?.();
|
|
437
|
+
while (currentPrompt) {
|
|
438
|
+
while (chatState.beforeNextPrompt) {
|
|
439
|
+
const gate = chatState.beforeNextPrompt;
|
|
440
|
+
await gate;
|
|
441
|
+
if (chatState.beforeNextPrompt === gate) chatState.beforeNextPrompt = null;
|
|
442
|
+
}
|
|
443
|
+
if (chatState.continueAfterClose && chatState.pendingPrompts.length) {
|
|
444
|
+
currentPrompt = takeQueuedPrompt(chatState);
|
|
445
|
+
chatState.continueAfterClose = false;
|
|
446
|
+
currentCtx = null;
|
|
447
|
+
}
|
|
448
|
+
try {
|
|
449
|
+
await processPrompt({ prompt: currentPrompt, ctx: currentCtx });
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (chatState.continueAfterClose && chatState.pendingPrompts.length) {
|
|
452
|
+
await onPromptInterrupted?.(error);
|
|
453
|
+
} else {
|
|
454
|
+
await onPromptFailure?.(error);
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
} finally {
|
|
458
|
+
currentCtx = null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
currentPrompt = takeQueuedPrompt(chatState);
|
|
462
|
+
chatState.continueAfterClose = false;
|
|
463
|
+
}
|
|
464
|
+
} finally {
|
|
465
|
+
chatState.processing = false;
|
|
466
|
+
chatState.activeSession = null;
|
|
467
|
+
chatState.activeSteers = [];
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export async function closeModelPicker(ctx, { messageText, callbackText }) {
|
|
472
|
+
await ctx.api.editMessageText(
|
|
473
|
+
ctx.chat.id,
|
|
474
|
+
ctx.callbackQuery.message.message_id,
|
|
475
|
+
messageText
|
|
476
|
+
);
|
|
477
|
+
await ctx.answerCallbackQuery({ text: callbackText });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, doctor, requestRestart, logger }) {
|
|
282
481
|
const bot = new Bot(config.telegram.token);
|
|
283
|
-
const perChatState =
|
|
482
|
+
const perChatState = createChatStateStore();
|
|
483
|
+
const conversationHistory = new ConversationHistoryStore();
|
|
284
484
|
const notifiedPromptErrors = new WeakSet();
|
|
285
485
|
const authRenewals = new Map();
|
|
286
486
|
let piAuthIssue = null;
|
|
287
487
|
let taskTimer = null;
|
|
288
488
|
|
|
489
|
+
const handleRestartCommand = createTelegramRestartHandler({
|
|
490
|
+
authorize: (ctx) => authorizeChat({
|
|
491
|
+
config,
|
|
492
|
+
chatId: ctx.chat.id,
|
|
493
|
+
saveConfig,
|
|
494
|
+
chatMeta: getIncomingChatMeta(ctx)
|
|
495
|
+
}),
|
|
496
|
+
requestRestart,
|
|
497
|
+
logger
|
|
498
|
+
});
|
|
499
|
+
|
|
289
500
|
function chatKey(chatId) {
|
|
290
501
|
return String(chatId);
|
|
291
502
|
}
|
|
@@ -309,7 +520,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
309
520
|
if (!issue) return false;
|
|
310
521
|
|
|
311
522
|
try {
|
|
312
|
-
await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, issue }));
|
|
523
|
+
await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, issue }));
|
|
313
524
|
markPromptErrorNotified(error);
|
|
314
525
|
return true;
|
|
315
526
|
} catch (notifyError) {
|
|
@@ -328,16 +539,16 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
328
539
|
async function finishAuthRenewal(chatId, renewal) {
|
|
329
540
|
try {
|
|
330
541
|
await renewal.promise;
|
|
331
|
-
await agentManager.
|
|
542
|
+
await agentManager.validateAgent();
|
|
332
543
|
agentManager.clearSessionCache(chatId);
|
|
333
544
|
piAuthIssue = null;
|
|
334
545
|
logger?.log("telegram", `Pi auth renewal completed for chat ${chatId}`);
|
|
335
|
-
await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, verified: true }));
|
|
546
|
+
await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, verified: true }));
|
|
336
547
|
} catch (error) {
|
|
337
548
|
const issue = rememberPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
|
|
338
549
|
piAuthIssue = issue;
|
|
339
550
|
logger?.error("telegram", `Pi auth renewal failed for chat ${chatId}: ${getErrorMessage(error)}`);
|
|
340
|
-
await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, issue })).catch((notifyError) => {
|
|
551
|
+
await bot.api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, issue })).catch((notifyError) => {
|
|
341
552
|
logger?.error("telegram", `auth renewal failure notice failed for chat ${chatId}: ${getErrorMessage(notifyError)}`);
|
|
342
553
|
});
|
|
343
554
|
} finally {
|
|
@@ -407,26 +618,25 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
407
618
|
}
|
|
408
619
|
|
|
409
620
|
function getChatState(chatId) {
|
|
410
|
-
if (!perChatState.has(chatId)) {
|
|
411
|
-
perChatState.set(chatId, { processing: false, nextPrompt: "" });
|
|
412
|
-
}
|
|
413
621
|
return perChatState.get(chatId);
|
|
414
622
|
}
|
|
415
623
|
|
|
416
|
-
function getProviderModels() {
|
|
624
|
+
async function getProviderModels(chatId) {
|
|
417
625
|
const runtime = createPiRuntime({
|
|
418
626
|
provider: config.pi.provider,
|
|
419
627
|
apiKey: config.pi.apiKey
|
|
420
628
|
});
|
|
421
|
-
return listProviderModels(config.pi.provider, runtime);
|
|
629
|
+
return reverseModelOrder(listProviderModels(config.pi.provider, runtime));
|
|
422
630
|
}
|
|
423
631
|
|
|
424
632
|
async function showModelPicker(ctx, page = 0) {
|
|
633
|
+
const agentConfig = getAgentConfig(config);
|
|
425
634
|
const picker = buildModelPicker({
|
|
426
|
-
provider:
|
|
427
|
-
models: getProviderModels(),
|
|
635
|
+
provider: agentConfig.provider,
|
|
636
|
+
models: await getProviderModels(ctx.chat.id),
|
|
428
637
|
selectedModelId: resolveChatModel(config, ctx.chat.id),
|
|
429
638
|
selectedThinkingLevel: resolveChatThinkingLevel(config, ctx.chat.id),
|
|
639
|
+
selectedSpeed: resolveChatSpeed(config, ctx.chat.id),
|
|
430
640
|
page,
|
|
431
641
|
pageSize: config.telegram.modelPickerPageSize
|
|
432
642
|
});
|
|
@@ -439,10 +649,11 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
439
649
|
}
|
|
440
650
|
|
|
441
651
|
async function showEffortPicker(ctx, { model, modelIndex, selectedThinkingLevel } = {}) {
|
|
442
|
-
const
|
|
652
|
+
const agentConfig = getAgentConfig(config);
|
|
653
|
+
const models = await getProviderModels(ctx.chat.id);
|
|
443
654
|
const resolvedModel = model || models.find((item) => item.id === resolveChatModel(config, ctx.chat.id));
|
|
444
655
|
if (!resolvedModel) {
|
|
445
|
-
throw new Error(`Model not found for provider ${
|
|
656
|
+
throw new Error(`Model not found for provider ${agentConfig.provider}`);
|
|
446
657
|
}
|
|
447
658
|
if (!modelSupportsThinking(resolvedModel)) {
|
|
448
659
|
const text = `${resolvedModel.provider}/${resolvedModel.id} does not support effort levels.`;
|
|
@@ -468,20 +679,46 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
468
679
|
return ctx.reply(picker.text, extra);
|
|
469
680
|
}
|
|
470
681
|
|
|
682
|
+
async function showSpeedPicker(ctx) {
|
|
683
|
+
const agentConfig = getAgentConfig(config);
|
|
684
|
+
const models = await getProviderModels(ctx.chat.id);
|
|
685
|
+
const model = models.find((item) => item.id === resolveChatModel(config, ctx.chat.id));
|
|
686
|
+
if (!model) throw new Error(`Model not found for provider ${agentConfig.provider}`);
|
|
687
|
+
if (!modelSupportsSpeed(model)) {
|
|
688
|
+
const text = `${model.provider}/${model.id} does not support speed 1.5x.`;
|
|
689
|
+
if (ctx.callbackQuery?.message?.message_id) {
|
|
690
|
+
return ctx.api.editMessageText(ctx.chat.id, ctx.callbackQuery.message.message_id, text);
|
|
691
|
+
}
|
|
692
|
+
return ctx.reply(text);
|
|
693
|
+
}
|
|
694
|
+
const picker = buildSpeedPicker({
|
|
695
|
+
provider: model.provider,
|
|
696
|
+
modelId: model.id,
|
|
697
|
+
speeds: MODEL_SPEEDS,
|
|
698
|
+
selectedSpeed: resolveChatSpeed(config, ctx.chat.id)
|
|
699
|
+
});
|
|
700
|
+
const extra = { reply_markup: picker.replyMarkup };
|
|
701
|
+
const messageId = ctx.callbackQuery?.message?.message_id;
|
|
702
|
+
if (messageId) return ctx.api.editMessageText(ctx.chat.id, messageId, picker.text, extra);
|
|
703
|
+
return ctx.reply(picker.text, extra);
|
|
704
|
+
}
|
|
705
|
+
|
|
471
706
|
async function persistChatModel(chatId, model, thinkingLevel) {
|
|
707
|
+
const agentConfig = getAgentConfig(config);
|
|
472
708
|
const key = chatKey(chatId);
|
|
473
|
-
const hadSelections = Boolean(
|
|
474
|
-
const previousSelection =
|
|
709
|
+
const hadSelections = Boolean(agentConfig.chatModels);
|
|
710
|
+
const previousSelection = agentConfig.chatModels?.[key];
|
|
475
711
|
const level = clampModelThinkingLevel(model, thinkingLevel ?? resolveChatThinkingLevel(config, chatId));
|
|
476
|
-
|
|
712
|
+
const speed = clampModelSpeed(model, resolveChatSpeed(config, chatId));
|
|
713
|
+
selectChatModel(config, chatId, model, { thinkingLevel: level, speed });
|
|
477
714
|
try {
|
|
478
715
|
await saveConfig(config);
|
|
479
716
|
} catch (error) {
|
|
480
717
|
if (previousSelection) {
|
|
481
|
-
|
|
718
|
+
agentConfig.chatModels[key] = previousSelection;
|
|
482
719
|
} else {
|
|
483
|
-
delete
|
|
484
|
-
if (!hadSelections) delete
|
|
720
|
+
delete agentConfig.chatModels[key];
|
|
721
|
+
if (!hadSelections) delete agentConfig.chatModels;
|
|
485
722
|
}
|
|
486
723
|
throw error;
|
|
487
724
|
}
|
|
@@ -490,20 +727,44 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
490
727
|
}
|
|
491
728
|
|
|
492
729
|
async function persistChatEffort(chatId, model, thinkingLevel) {
|
|
730
|
+
const agentConfig = getAgentConfig(config);
|
|
493
731
|
const key = chatKey(chatId);
|
|
494
|
-
const hadSelections = Boolean(
|
|
495
|
-
const previousSelection =
|
|
732
|
+
const hadSelections = Boolean(agentConfig.chatModels);
|
|
733
|
+
const previousSelection = agentConfig.chatModels?.[key];
|
|
496
734
|
const level = clampModelThinkingLevel(model, thinkingLevel);
|
|
497
735
|
selectChatThinkingLevel(config, chatId, level);
|
|
498
736
|
try {
|
|
499
737
|
await saveConfig(config);
|
|
500
738
|
} catch (error) {
|
|
501
739
|
if (previousSelection) {
|
|
502
|
-
|
|
740
|
+
agentConfig.chatModels[key] = previousSelection;
|
|
741
|
+
} else {
|
|
742
|
+
delete agentConfig.chatModels[key];
|
|
743
|
+
if (!hadSelections) delete agentConfig.chatModels;
|
|
744
|
+
}
|
|
745
|
+
throw error;
|
|
746
|
+
}
|
|
747
|
+
return level;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function persistChatSpeed(chatId, model, speed) {
|
|
751
|
+
const agentConfig = getAgentConfig(config);
|
|
752
|
+
const key = chatKey(chatId);
|
|
753
|
+
const hadSelections = Boolean(agentConfig.chatModels);
|
|
754
|
+
const previousSelection = agentConfig.chatModels?.[key];
|
|
755
|
+
const level = clampModelSpeed(model, speed);
|
|
756
|
+
await agentManager.setModelSpeed(chatId, level);
|
|
757
|
+
selectChatSpeed(config, chatId, level);
|
|
758
|
+
try {
|
|
759
|
+
await saveConfig(config);
|
|
760
|
+
} catch (error) {
|
|
761
|
+
if (previousSelection) {
|
|
762
|
+
agentConfig.chatModels[key] = previousSelection;
|
|
503
763
|
} else {
|
|
504
|
-
delete
|
|
505
|
-
if (!hadSelections) delete
|
|
764
|
+
delete agentConfig.chatModels[key];
|
|
765
|
+
if (!hadSelections) delete agentConfig.chatModels;
|
|
506
766
|
}
|
|
767
|
+
agentManager.clearSessionCache(chatId);
|
|
507
768
|
throw error;
|
|
508
769
|
}
|
|
509
770
|
return level;
|
|
@@ -526,6 +787,11 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
526
787
|
async function sendTextReply({ sendText, sendDocument, chatId, text }) {
|
|
527
788
|
const maxInlineReplyLength = 3500;
|
|
528
789
|
|
|
790
|
+
if (isSilentReply(text)) {
|
|
791
|
+
logger?.log("telegram", `suppressing silent reply for chat ${chatId}`);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
529
795
|
if (text.length > maxInlineReplyLength) {
|
|
530
796
|
logger?.log("telegram", `sending long reply as markdown attachment for chat ${chatId}`);
|
|
531
797
|
const chatArtifactStore = artifactStore.forChat(chatId);
|
|
@@ -561,10 +827,35 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
561
827
|
};
|
|
562
828
|
}
|
|
563
829
|
|
|
830
|
+
agentManager.setArtifactDeliveryHandler?.(async ({ chatId, artifact, caption, method }) => {
|
|
831
|
+
const resolvedMethod = method
|
|
832
|
+
|| artifact.metadata?.delivery?.method
|
|
833
|
+
|| (artifact.kind === "audio" || artifact.mimeType?.startsWith("audio/") ? "audio"
|
|
834
|
+
: artifact.kind === "image" || artifact.mimeType?.startsWith("image/") ? "photo"
|
|
835
|
+
: artifact.kind === "video" || artifact.mimeType?.startsWith("video/") ? "video"
|
|
836
|
+
: "document");
|
|
837
|
+
const safeCaption = caption && !/(^|\s)(\/[^\s]|[A-Za-z]:[\\/])/.test(caption) ? caption : undefined;
|
|
838
|
+
await createTelegramSessionBridge(chatId).sendMedia(artifact.path, {
|
|
839
|
+
method: resolvedMethod,
|
|
840
|
+
caption: safeCaption,
|
|
841
|
+
filename: path.basename(artifact.path)
|
|
842
|
+
});
|
|
843
|
+
return { ok: true, artifactId: artifact.id, method: resolvedMethod };
|
|
844
|
+
});
|
|
845
|
+
|
|
564
846
|
async function processPromptForChat({ chatId, prompt, ctx = null }) {
|
|
565
847
|
const work = async () => {
|
|
566
848
|
const { session } = await agentManager.getSessionContext(chatId, createTelegramSessionBridge(chatId));
|
|
849
|
+
const historyRevision = getChatState(chatId).historyRevision;
|
|
850
|
+
await conversationHistory.ensureSeed(chatId, {
|
|
851
|
+
runtime: "pi",
|
|
852
|
+
history: formatPortableSessionHistory(session.messages)
|
|
853
|
+
});
|
|
567
854
|
let text = "";
|
|
855
|
+
let steeredPrompts = [];
|
|
856
|
+
const chatState = getChatState(chatId);
|
|
857
|
+
chatState.activeSession = session;
|
|
858
|
+
chatState.activeSteers = [];
|
|
568
859
|
try {
|
|
569
860
|
text = await collectText(session, prompt, {
|
|
570
861
|
logger,
|
|
@@ -577,6 +868,20 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
577
868
|
} catch (error) {
|
|
578
869
|
agentManager.resetSession(chatId);
|
|
579
870
|
throw error;
|
|
871
|
+
} finally {
|
|
872
|
+
steeredPrompts = [...chatState.activeSteers];
|
|
873
|
+
if (chatState.activeSession === session) chatState.activeSession = null;
|
|
874
|
+
chatState.activeSteers = [];
|
|
875
|
+
}
|
|
876
|
+
if (getChatState(chatId).historyRevision === historyRevision) {
|
|
877
|
+
const historyPrompt = steeredPrompts.length
|
|
878
|
+
? [prompt, ...steeredPrompts.map((message) => `[Steering message]\n${message}`)].join("\n\n")
|
|
879
|
+
: prompt;
|
|
880
|
+
await conversationHistory.appendTurn(chatId, {
|
|
881
|
+
runtime: "pi",
|
|
882
|
+
prompt: historyPrompt,
|
|
883
|
+
response: text
|
|
884
|
+
});
|
|
580
885
|
}
|
|
581
886
|
if (text) {
|
|
582
887
|
await sendTextReply({
|
|
@@ -592,46 +897,53 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
592
897
|
return work();
|
|
593
898
|
}
|
|
594
899
|
|
|
595
|
-
async function enqueuePrompt({ chatId, prompt, label, ctx = null }) {
|
|
900
|
+
async function enqueuePrompt({ chatId, prompt, label, ctx = null, replaceQueued = false, busyMessageMode = "queue" }) {
|
|
596
901
|
const chatState = getChatState(chatId);
|
|
597
902
|
|
|
598
903
|
if (chatState.processing) {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
:
|
|
904
|
+
const routed = await routeBusyPrompt({
|
|
905
|
+
chatState,
|
|
906
|
+
prompt,
|
|
907
|
+
mode: busyMessageMode,
|
|
908
|
+
replaceQueued
|
|
909
|
+
});
|
|
910
|
+
if (routed.disposition === "steered") {
|
|
911
|
+
logger?.log("telegram", `chat ${chatId} busy, steering ${label}`);
|
|
912
|
+
} else {
|
|
913
|
+
logger?.log("telegram", `chat ${chatId} busy, queueing ${label}`);
|
|
914
|
+
if (routed.steerError) {
|
|
915
|
+
logger?.log("telegram", `steer failed for chat ${chatId}, queued instead: ${getErrorMessage(routed.steerError)}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (replaceQueued) chatState.continueAfterClose = true;
|
|
603
919
|
return;
|
|
604
920
|
}
|
|
605
921
|
|
|
606
922
|
chatState.processing = true;
|
|
607
923
|
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
|
-
}
|
|
924
|
+
return processChatPromptQueue({ chatId, prompt, label, ctx });
|
|
925
|
+
}
|
|
624
926
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
927
|
+
function processChatPromptQueue({ chatId, prompt, label, ctx = null, beforeInitialPrompt }) {
|
|
928
|
+
const chatState = getChatState(chatId);
|
|
929
|
+
return drainChatPromptQueue({
|
|
930
|
+
chatState,
|
|
931
|
+
initialPrompt: prompt,
|
|
932
|
+
initialCtx: ctx,
|
|
933
|
+
beforeInitialPrompt,
|
|
934
|
+
processPrompt: ({ prompt: currentPrompt, ctx: currentCtx }) => {
|
|
935
|
+
logger?.log("telegram", `prompt dispatch for chat ${chatId}`);
|
|
936
|
+
return processPromptForChat({ chatId, prompt: currentPrompt, ctx: currentCtx });
|
|
937
|
+
},
|
|
938
|
+
onPromptInterrupted: (error) => {
|
|
939
|
+
logger?.log("telegram", `${label} interrupted by queued /new for chat ${chatId}: ${getErrorMessage(error)}`);
|
|
940
|
+
},
|
|
941
|
+
onPromptFailure: async (error) => {
|
|
942
|
+
const message = getErrorMessage(error);
|
|
943
|
+
logger?.error("telegram", `${label} failed for chat ${chatId}: ${message}`);
|
|
944
|
+
await notifyPiAuthIssueIfNeeded(chatId, error);
|
|
631
945
|
}
|
|
632
|
-
}
|
|
633
|
-
chatState.processing = false;
|
|
634
|
-
}
|
|
946
|
+
});
|
|
635
947
|
}
|
|
636
948
|
|
|
637
949
|
async function enqueueOrProcess(ctx) {
|
|
@@ -639,10 +951,14 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
639
951
|
|
|
640
952
|
if (chatState.processing) {
|
|
641
953
|
const incomingPrompt = await buildIncomingPrompt(ctx);
|
|
954
|
+
const busyMessageMode = typeof ctx.message?.text === "string"
|
|
955
|
+
? resolveTelegramBusyMessageMode(config, ctx.chat.id)
|
|
956
|
+
: "queue";
|
|
642
957
|
return enqueuePrompt({
|
|
643
958
|
chatId: ctx.chat.id,
|
|
644
959
|
prompt: incomingPrompt,
|
|
645
|
-
label: `message ${ctx.msg.message_id}
|
|
960
|
+
label: `message ${ctx.msg.message_id}`,
|
|
961
|
+
busyMessageMode
|
|
646
962
|
});
|
|
647
963
|
}
|
|
648
964
|
|
|
@@ -747,14 +1063,61 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
747
1063
|
}
|
|
748
1064
|
}
|
|
749
1065
|
|
|
1066
|
+
async function summarizeSessionBeforeReset(chatId) {
|
|
1067
|
+
try {
|
|
1068
|
+
const context = await agentManager.getSessionContext(chatId, createTelegramSessionBridge(chatId));
|
|
1069
|
+
const parentSession = context.session.sessionFile || "";
|
|
1070
|
+
if (!context.session.messages.length) return { handoff: "", parentSession: "" };
|
|
1071
|
+
|
|
1072
|
+
const summary = await collectText(context.session, buildSessionHandoffPrompt(), { logger, chatId });
|
|
1073
|
+
return { handoff: sanitizeSessionHandoff(summary), parentSession };
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
logger?.log("agent", `session handoff summary failed for chat ${chatId}: ${getErrorMessage(error)}`);
|
|
1076
|
+
return { handoff: "", parentSession: "" };
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
750
1080
|
async function handleNewCommand(ctx) {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1081
|
+
const chatState = getChatState(ctx.chat.id);
|
|
1082
|
+
const wasProcessing = chatState.processing;
|
|
1083
|
+
chatState.historyRevision += 1;
|
|
1084
|
+
const commandRevision = chatState.historyRevision;
|
|
1085
|
+
const prompt = buildNewSessionPrompt(ctx);
|
|
1086
|
+
|
|
1087
|
+
if (wasProcessing) {
|
|
1088
|
+
logger?.log("telegram", `chat ${ctx.chat.id} busy, queueing new-session command`);
|
|
1089
|
+
queueChatPrompt(chatState, prompt, { replace: true });
|
|
1090
|
+
chatState.continueAfterClose = true;
|
|
1091
|
+
const reset = (async () => {
|
|
1092
|
+
await conversationHistory.reset(ctx.chat.id, { runtime: "pi" });
|
|
1093
|
+
agentManager.resetSession(ctx.chat.id);
|
|
1094
|
+
})();
|
|
1095
|
+
chatState.beforeNextPrompt = reset;
|
|
1096
|
+
try {
|
|
1097
|
+
await reset;
|
|
1098
|
+
} finally {
|
|
1099
|
+
if (chatState.beforeNextPrompt === reset) chatState.beforeNextPrompt = null;
|
|
1100
|
+
}
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
chatState.processing = true;
|
|
1105
|
+
logger?.log("telegram", `processing new-session command in chat ${ctx.chat.id}`);
|
|
1106
|
+
await processChatPromptQueue({
|
|
754
1107
|
chatId: ctx.chat.id,
|
|
755
|
-
prompt
|
|
1108
|
+
prompt,
|
|
756
1109
|
label: "new-session command",
|
|
757
|
-
ctx
|
|
1110
|
+
ctx,
|
|
1111
|
+
beforeInitialPrompt: async () => {
|
|
1112
|
+
const handoff = await withTyping(ctx, () => summarizeSessionBeforeReset(ctx.chat.id));
|
|
1113
|
+
if (chatState.historyRevision !== commandRevision) return;
|
|
1114
|
+
await conversationHistory.reset(ctx.chat.id, {
|
|
1115
|
+
runtime: "pi",
|
|
1116
|
+
history: handoff.handoff
|
|
1117
|
+
});
|
|
1118
|
+
if (chatState.historyRevision !== commandRevision) return;
|
|
1119
|
+
agentManager.resetSession(ctx.chat.id, handoff);
|
|
1120
|
+
}
|
|
758
1121
|
});
|
|
759
1122
|
}
|
|
760
1123
|
|
|
@@ -775,6 +1138,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
775
1138
|
if (piAuthIssue) {
|
|
776
1139
|
await ctx.reply(buildPiAuthRecoveryBlockedMessage({
|
|
777
1140
|
config,
|
|
1141
|
+
chatId: ctx.chat.id,
|
|
778
1142
|
issue: piAuthIssue,
|
|
779
1143
|
renewalActive: authRenewals.has(chatKey(ctx.chat.id))
|
|
780
1144
|
}));
|
|
@@ -783,6 +1147,21 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
783
1147
|
await handleNewCommand(ctx);
|
|
784
1148
|
});
|
|
785
1149
|
|
|
1150
|
+
bot.command("restart", handleRestartCommand);
|
|
1151
|
+
|
|
1152
|
+
bot.command("doctor", async (ctx) => {
|
|
1153
|
+
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
1154
|
+
if (!auth.ok) return;
|
|
1155
|
+
await withTyping(ctx, async () => {
|
|
1156
|
+
try {
|
|
1157
|
+
await ctx.reply(formatDoctorReport(await doctor()));
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
logger?.error("doctor", `doctor command failed: ${getErrorMessage(error)}`);
|
|
1160
|
+
await ctx.reply(`Arisa Doctor failed: ${getErrorMessage(error)}`);
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
});
|
|
1164
|
+
|
|
786
1165
|
bot.command("model", async (ctx) => {
|
|
787
1166
|
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
788
1167
|
if (!auth.ok) return;
|
|
@@ -795,22 +1174,28 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
795
1174
|
await showEffortPicker(ctx);
|
|
796
1175
|
});
|
|
797
1176
|
|
|
1177
|
+
bot.command("speed", async (ctx) => {
|
|
1178
|
+
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
1179
|
+
if (!auth.ok) return;
|
|
1180
|
+
await showSpeedPicker(ctx);
|
|
1181
|
+
});
|
|
1182
|
+
|
|
798
1183
|
bot.command("auth", async (ctx) => {
|
|
799
1184
|
const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
|
|
800
1185
|
if (!auth.ok) return;
|
|
801
1186
|
|
|
802
|
-
const status = getPiAuthStatus(config);
|
|
1187
|
+
const status = getPiAuthStatus(config, ctx.chat.id);
|
|
803
1188
|
if (status.hasApiKey || !status.supportsOAuth) {
|
|
804
1189
|
await withTyping(ctx, async () => {
|
|
805
1190
|
try {
|
|
806
|
-
await agentManager.
|
|
1191
|
+
await agentManager.validateAgent();
|
|
807
1192
|
agentManager.clearSessionCache(ctx.chat.id);
|
|
808
1193
|
piAuthIssue = null;
|
|
809
|
-
await ctx.reply(buildPiAuthTelegramMessage({ config, verified: true }));
|
|
1194
|
+
await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, verified: true }));
|
|
810
1195
|
} catch (error) {
|
|
811
1196
|
const issue = rememberPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
|
|
812
1197
|
piAuthIssue = issue;
|
|
813
|
-
await ctx.reply(buildPiAuthTelegramMessage({ config, issue }));
|
|
1198
|
+
await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue }));
|
|
814
1199
|
}
|
|
815
1200
|
});
|
|
816
1201
|
return;
|
|
@@ -824,14 +1209,15 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
824
1209
|
} catch (error) {
|
|
825
1210
|
const issue = rememberPiAuthIssue(error) || { kind: "validation-failed", message: getErrorMessage(error) };
|
|
826
1211
|
piAuthIssue = issue;
|
|
827
|
-
await ctx.reply(buildPiAuthTelegramMessage({ config, issue }));
|
|
1212
|
+
await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue }));
|
|
828
1213
|
}
|
|
829
1214
|
});
|
|
830
1215
|
|
|
831
1216
|
bot.on("callback_query:data", async (ctx, next) => {
|
|
832
1217
|
const modelAction = parseModelPickerAction(ctx.callbackQuery.data);
|
|
833
1218
|
const effortAction = modelAction ? null : parseEffortPickerAction(ctx.callbackQuery.data);
|
|
834
|
-
const
|
|
1219
|
+
const speedAction = modelAction || effortAction ? null : parseSpeedPickerAction(ctx.callbackQuery.data);
|
|
1220
|
+
const action = modelAction || effortAction || speedAction;
|
|
835
1221
|
if (!action) return next();
|
|
836
1222
|
if (action.type === "noop") {
|
|
837
1223
|
await ctx.answerCallbackQuery();
|
|
@@ -851,17 +1237,8 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
851
1237
|
return;
|
|
852
1238
|
}
|
|
853
1239
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
text: action.type === "effort" || action.type === "model-effort"
|
|
857
|
-
? "Wait for the current response before changing effort."
|
|
858
|
-
: "Wait for the current response before changing models.",
|
|
859
|
-
show_alert: true
|
|
860
|
-
});
|
|
861
|
-
return;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
const models = getProviderModels();
|
|
1240
|
+
const models = await getProviderModels(ctx.chat.id);
|
|
1241
|
+
const chatBusy = getChatState(ctx.chat.id).processing;
|
|
865
1242
|
|
|
866
1243
|
if (action.type === "select") {
|
|
867
1244
|
const model = models[action.value];
|
|
@@ -873,6 +1250,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
873
1250
|
return;
|
|
874
1251
|
}
|
|
875
1252
|
|
|
1253
|
+
// Reasoning models open the effort picker only — no session reset yet.
|
|
876
1254
|
if (modelSupportsThinking(model)) {
|
|
877
1255
|
await showEffortPicker(ctx, {
|
|
878
1256
|
model,
|
|
@@ -883,10 +1261,21 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
883
1261
|
return;
|
|
884
1262
|
}
|
|
885
1263
|
|
|
1264
|
+
if (chatBusy) {
|
|
1265
|
+
await ctx.answerCallbackQuery({
|
|
1266
|
+
text: "Wait for the current response before changing models.",
|
|
1267
|
+
show_alert: true
|
|
1268
|
+
});
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
886
1272
|
const currentModelId = resolveChatModel(config, ctx.chat.id);
|
|
887
1273
|
const currentEffort = resolveChatThinkingLevel(config, ctx.chat.id);
|
|
888
1274
|
if (model.id === currentModelId && currentEffort === "off") {
|
|
889
|
-
await ctx
|
|
1275
|
+
await closeModelPicker(ctx, {
|
|
1276
|
+
messageText: `Already using ${model.provider}/${model.id}.`,
|
|
1277
|
+
callbackText: `Already using ${model.id}.`
|
|
1278
|
+
});
|
|
890
1279
|
return;
|
|
891
1280
|
}
|
|
892
1281
|
|
|
@@ -921,10 +1310,14 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
921
1310
|
const currentModelId = resolveChatModel(config, ctx.chat.id);
|
|
922
1311
|
const currentEffort = resolveChatThinkingLevel(config, ctx.chat.id);
|
|
923
1312
|
if (model.id === currentModelId && action.level === currentEffort) {
|
|
924
|
-
await ctx
|
|
1313
|
+
await closeModelPicker(ctx, {
|
|
1314
|
+
messageText: `Already using ${model.provider}/${model.id} (effort: ${action.level}).`,
|
|
1315
|
+
callbackText: `Already using ${model.id} at ${action.level}.`
|
|
1316
|
+
});
|
|
925
1317
|
return;
|
|
926
1318
|
}
|
|
927
1319
|
|
|
1320
|
+
// Effort-only updates do not reset the session, so they are safe while busy.
|
|
928
1321
|
if (model.id === currentModelId) {
|
|
929
1322
|
await persistChatEffort(ctx.chat.id, model, action.level);
|
|
930
1323
|
await ctx.api.editMessageText(
|
|
@@ -936,6 +1329,14 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
936
1329
|
return;
|
|
937
1330
|
}
|
|
938
1331
|
|
|
1332
|
+
if (chatBusy) {
|
|
1333
|
+
await ctx.answerCallbackQuery({
|
|
1334
|
+
text: "Wait for the current response before changing models.",
|
|
1335
|
+
show_alert: true
|
|
1336
|
+
});
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
939
1340
|
await persistChatModel(ctx.chat.id, model, action.level);
|
|
940
1341
|
await ctx.api.editMessageText(
|
|
941
1342
|
ctx.chat.id,
|
|
@@ -972,7 +1373,10 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
972
1373
|
}
|
|
973
1374
|
const currentEffort = resolveChatThinkingLevel(config, ctx.chat.id);
|
|
974
1375
|
if (action.level === currentEffort) {
|
|
975
|
-
await ctx
|
|
1376
|
+
await closeModelPicker(ctx, {
|
|
1377
|
+
messageText: `Already using effort ${action.level} for ${model.provider}/${model.id}.`,
|
|
1378
|
+
callbackText: `Already using effort ${action.level}.`
|
|
1379
|
+
});
|
|
976
1380
|
return;
|
|
977
1381
|
}
|
|
978
1382
|
await persistChatEffort(ctx.chat.id, model, action.level);
|
|
@@ -982,11 +1386,45 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
982
1386
|
`Effort set to ${action.level} for ${model.provider}/${model.id}.`
|
|
983
1387
|
);
|
|
984
1388
|
await ctx.answerCallbackQuery({ text: `Effort: ${action.level}.` });
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
if (action.type === "speed") {
|
|
1393
|
+
const model = models.find((item) => item.id === resolveChatModel(config, ctx.chat.id));
|
|
1394
|
+
if (!model) {
|
|
1395
|
+
await ctx.answerCallbackQuery({
|
|
1396
|
+
text: "Current model is unavailable. Run /model again.",
|
|
1397
|
+
show_alert: true
|
|
1398
|
+
});
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
if (!modelSupportsSpeed(model)) {
|
|
1402
|
+
await ctx.answerCallbackQuery({
|
|
1403
|
+
text: "This model does not support speed 1.5x.",
|
|
1404
|
+
show_alert: true
|
|
1405
|
+
});
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
const currentSpeed = resolveChatSpeed(config, ctx.chat.id);
|
|
1409
|
+
if (action.speed === currentSpeed) {
|
|
1410
|
+
await closeModelPicker(ctx, {
|
|
1411
|
+
messageText: `Already using speed ${action.speed.toFixed(1)}x for ${model.provider}/${model.id}.`,
|
|
1412
|
+
callbackText: `Already using speed ${action.speed.toFixed(1)}x.`
|
|
1413
|
+
});
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
await persistChatSpeed(ctx.chat.id, model, action.speed);
|
|
1417
|
+
await ctx.api.editMessageText(
|
|
1418
|
+
ctx.chat.id,
|
|
1419
|
+
ctx.callbackQuery.message.message_id,
|
|
1420
|
+
`Speed set to ${action.speed.toFixed(1)}x for ${model.provider}/${model.id}.`
|
|
1421
|
+
);
|
|
1422
|
+
await ctx.answerCallbackQuery({ text: `Speed: ${action.speed.toFixed(1)}x.` });
|
|
985
1423
|
}
|
|
986
1424
|
} catch (error) {
|
|
987
1425
|
logger?.error("telegram", `model selection failed for chat ${ctx.chat.id}: ${getErrorMessage(error)}`);
|
|
988
1426
|
await ctx.answerCallbackQuery({
|
|
989
|
-
text: "Could not change the model or
|
|
1427
|
+
text: "Could not change the model, effort, or speed.",
|
|
990
1428
|
show_alert: true
|
|
991
1429
|
}).catch(() => {});
|
|
992
1430
|
}
|
|
@@ -1004,34 +1442,30 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, t
|
|
|
1004
1442
|
if (piAuthIssue) {
|
|
1005
1443
|
await ctx.reply(buildPiAuthRecoveryBlockedMessage({
|
|
1006
1444
|
config,
|
|
1445
|
+
chatId: ctx.chat.id,
|
|
1007
1446
|
issue: piAuthIssue,
|
|
1008
1447
|
renewalActive: authRenewals.has(chatKey(ctx.chat.id))
|
|
1009
1448
|
}));
|
|
1010
1449
|
return;
|
|
1011
1450
|
}
|
|
1012
1451
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1452
|
+
// grammY long polling awaits each middleware. Keep prompt execution in the background so
|
|
1453
|
+
// the next Telegram update can reach the active session as a steer or queued message.
|
|
1454
|
+
enqueueOrProcess(ctx).catch(async (error) => {
|
|
1016
1455
|
const chatState = getChatState(ctx.chat.id);
|
|
1017
1456
|
chatState.processing = false;
|
|
1018
1457
|
if (wasPromptErrorNotified(error)) return;
|
|
1019
1458
|
const issue = getPiAuthIssue(error);
|
|
1020
1459
|
await ctx.reply(issue
|
|
1021
|
-
? buildPiAuthTelegramMessage({ config, issue })
|
|
1460
|
+
? buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue })
|
|
1022
1461
|
: getErrorMessage(error));
|
|
1023
|
-
}
|
|
1462
|
+
});
|
|
1024
1463
|
});
|
|
1025
1464
|
|
|
1026
1465
|
return {
|
|
1027
1466
|
async start({ skipAgentStartupPrompts = false } = {}) {
|
|
1028
1467
|
config.telegram.chatMeta ||= {};
|
|
1029
|
-
await bot.api.setMyCommands(
|
|
1030
|
-
{ command: "new", description: "Start a new chat context" },
|
|
1031
|
-
{ command: "model", description: "Choose the model for this chat" },
|
|
1032
|
-
{ command: "effort", description: "Choose reasoning effort for this chat" },
|
|
1033
|
-
{ command: "auth", description: "Show Pi authentication status" }
|
|
1034
|
-
]);
|
|
1468
|
+
await bot.api.setMyCommands(telegramCommands);
|
|
1035
1469
|
if (!taskTimer) {
|
|
1036
1470
|
taskTimer = setInterval(() => {
|
|
1037
1471
|
dispatchDueTasks().catch((error) => {
|