arisa 5.1.49 → 5.1.60
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 +0 -2
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +39 -489
- package/src/core/agent/agent-session-lifecycle.js +181 -0
- package/src/core/agent/pi-capability-tools.js +183 -0
- package/src/core/artifacts/artifact-store.js +73 -17
- package/src/core/capabilities/capability-service.js +340 -0
- package/src/core/tasks/task-routing.js +7 -0
- package/src/core/tasks/task-runner.js +53 -0
- package/src/core/tasks/task-store.js +316 -92
- package/src/core/tools/tool-output-materializer.js +5 -5
- package/src/official-tools.lock.json +7 -4
- package/src/runtime/arisa-capabilities.js +51 -242
- package/src/runtime/create-app.js +10 -1
- package/src/runtime/create-headless-app.js +5 -2
- package/src/transport/telegram/bot.js +112 -368
- package/src/transport/telegram/chat-queue.js +72 -6
- package/src/transport/telegram/prompt-builders.js +9 -0
- package/src/transport/telegram/task-dispatcher.js +73 -36
- package/src/transport/telegram/telegram-auth-controller.js +180 -0
- package/src/transport/telegram/telegram-session-bridge.js +170 -0
- package/src/transport/telegram/telegram-tools-command.js +28 -0
- package/src/transport/telegram/telegram-workspace-controller.js +66 -0
- package/test/agent-session-lifecycle.test.js +58 -0
- package/test/artifact-store.test.js +38 -2
- package/test/capabilities-security.test.js +58 -0
- package/test/context-and-task-bounds.test.js +76 -1
- package/test/device-code-message.test.js +9 -0
- package/test/media-caption.test.js +1 -1
- package/test/pi-capability-tools.test.js +65 -0
- package/test/session-start-operational-notes.test.js +1 -1
- package/test/task-idempotency.test.js +40 -0
- package/test/task-routing.test.js +62 -0
- package/test/task-store.test.js +178 -6
- package/test/telegram-task-dispatcher.test.js +99 -23
- package/test/telegram-text-artifact.test.js +13 -2
- package/test/telegram-tools-command.test.js +47 -0
|
@@ -6,6 +6,8 @@ export function createChatStateStore() {
|
|
|
6
6
|
processing: false,
|
|
7
7
|
pendingPrompts: [],
|
|
8
8
|
pendingPromptContexts: [],
|
|
9
|
+
pendingPromptReceipts: [],
|
|
10
|
+
pendingPromptCoalescible: [],
|
|
9
11
|
continueAfterClose: false,
|
|
10
12
|
historyRevision: 0,
|
|
11
13
|
beforeNextPrompt: null,
|
|
@@ -29,20 +31,59 @@ export function createChatStateStore() {
|
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
export function
|
|
34
|
+
export function createPromptExecutionReceipt() {
|
|
35
|
+
let resolve;
|
|
36
|
+
let reject;
|
|
37
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
38
|
+
resolve = resolvePromise;
|
|
39
|
+
reject = rejectPromise;
|
|
40
|
+
});
|
|
41
|
+
return { promise, resolve, reject };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rejectQueuedReceipts(chatState, error) {
|
|
45
|
+
for (const receipt of chatState.pendingPromptReceipts || []) receipt?.reject(error);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const COALESCED_PROMPT_SEPARATOR = "\n\n--- next direct message ---\n\n";
|
|
49
|
+
|
|
50
|
+
function coalesceLastQueuedPrompt(chatState, prompt, ctx) {
|
|
51
|
+
const coalescible = chatState.pendingPromptCoalescible ||= [];
|
|
52
|
+
const index = chatState.pendingPrompts.length - 1;
|
|
53
|
+
if (index < 0 || !coalescible[index]) return false;
|
|
54
|
+
chatState.pendingPrompts[index] += `${COALESCED_PROMPT_SEPARATOR}${prompt}`;
|
|
55
|
+
(chatState.pendingPromptContexts ||= [])[index] = ctx;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function queueChatPrompt(chatState, prompt, {
|
|
60
|
+
replace = false,
|
|
61
|
+
ctx = null,
|
|
62
|
+
receipt = null,
|
|
63
|
+
coalescible = false
|
|
64
|
+
} = {}) {
|
|
33
65
|
chatState.pendingPromptContexts ||= [];
|
|
66
|
+
chatState.pendingPromptReceipts ||= [];
|
|
67
|
+
chatState.pendingPromptCoalescible ||= [];
|
|
34
68
|
if (replace) {
|
|
69
|
+
rejectQueuedReceipts(chatState, Object.assign(new Error("Queued prompt was superseded"), { code: "PROMPT_SUPERSEDED" }));
|
|
35
70
|
chatState.pendingPrompts = [];
|
|
36
71
|
chatState.pendingPromptContexts = [];
|
|
72
|
+
chatState.pendingPromptReceipts = [];
|
|
73
|
+
chatState.pendingPromptCoalescible = [];
|
|
37
74
|
}
|
|
38
75
|
chatState.pendingPrompts.push(prompt);
|
|
39
76
|
chatState.pendingPromptContexts.push(ctx);
|
|
77
|
+
chatState.pendingPromptReceipts.push(receipt);
|
|
78
|
+
chatState.pendingPromptCoalescible.push(coalescible);
|
|
40
79
|
}
|
|
41
80
|
|
|
42
81
|
function takeQueuedPrompt(chatState) {
|
|
82
|
+
(chatState.pendingPromptCoalescible ||= []).shift();
|
|
43
83
|
return {
|
|
44
84
|
prompt: chatState.pendingPrompts.shift() || "",
|
|
45
|
-
ctx: (chatState.pendingPromptContexts ||= []).shift() || null
|
|
85
|
+
ctx: (chatState.pendingPromptContexts ||= []).shift() || null,
|
|
86
|
+
receipt: (chatState.pendingPromptReceipts ||= []).shift() || null
|
|
46
87
|
};
|
|
47
88
|
}
|
|
48
89
|
|
|
@@ -52,7 +93,19 @@ export function resolveTelegramBusyMessageMode(config, chatId) {
|
|
|
52
93
|
return mode === "steer" ? "steer" : "queue";
|
|
53
94
|
}
|
|
54
95
|
|
|
55
|
-
export async function routeBusyPrompt({
|
|
96
|
+
export async function routeBusyPrompt({
|
|
97
|
+
chatState,
|
|
98
|
+
prompt,
|
|
99
|
+
mode = "queue",
|
|
100
|
+
replaceQueued = false,
|
|
101
|
+
ctx = null,
|
|
102
|
+
receipt = null,
|
|
103
|
+
coalesceQueued = false
|
|
104
|
+
}) {
|
|
105
|
+
if (coalesceQueued && !replaceQueued && !receipt && coalesceLastQueuedPrompt(chatState, prompt, ctx)) {
|
|
106
|
+
return { disposition: "coalesced" };
|
|
107
|
+
}
|
|
108
|
+
|
|
56
109
|
const session = chatState.activeSession;
|
|
57
110
|
if (
|
|
58
111
|
mode === "steer"
|
|
@@ -61,17 +114,23 @@ export async function routeBusyPrompt({ chatState, prompt, mode = "queue", repla
|
|
|
61
114
|
&& !chatState.beforeNextPrompt
|
|
62
115
|
&& session?.isStreaming
|
|
63
116
|
&& typeof session.steer === "function"
|
|
117
|
+
&& !receipt
|
|
64
118
|
) {
|
|
65
119
|
try {
|
|
66
120
|
await session.steer(prompt);
|
|
67
121
|
return { disposition: "steered" };
|
|
68
122
|
} catch (error) {
|
|
69
|
-
queueChatPrompt(chatState, prompt, { ctx });
|
|
123
|
+
queueChatPrompt(chatState, prompt, { ctx, receipt, coalescible: coalesceQueued });
|
|
70
124
|
return { disposition: "queued", steerError: error };
|
|
71
125
|
}
|
|
72
126
|
}
|
|
73
127
|
|
|
74
|
-
queueChatPrompt(chatState, prompt, {
|
|
128
|
+
queueChatPrompt(chatState, prompt, {
|
|
129
|
+
replace: replaceQueued,
|
|
130
|
+
ctx,
|
|
131
|
+
receipt,
|
|
132
|
+
coalescible: coalesceQueued
|
|
133
|
+
});
|
|
75
134
|
return { disposition: "queued" };
|
|
76
135
|
}
|
|
77
136
|
|
|
@@ -84,6 +143,7 @@ export async function drainChatPromptQueue({
|
|
|
84
143
|
chatState,
|
|
85
144
|
initialPrompt,
|
|
86
145
|
initialCtx = null,
|
|
146
|
+
initialReceipt = null,
|
|
87
147
|
processPrompt,
|
|
88
148
|
onPromptFailure,
|
|
89
149
|
onPromptInterrupted,
|
|
@@ -91,6 +151,7 @@ export async function drainChatPromptQueue({
|
|
|
91
151
|
}) {
|
|
92
152
|
let currentPrompt = initialPrompt;
|
|
93
153
|
let currentCtx = initialCtx;
|
|
154
|
+
let currentReceipt = initialReceipt;
|
|
94
155
|
|
|
95
156
|
try {
|
|
96
157
|
await beforeInitialPrompt?.();
|
|
@@ -104,11 +165,14 @@ export async function drainChatPromptQueue({
|
|
|
104
165
|
const queued = takeQueuedPrompt(chatState);
|
|
105
166
|
currentPrompt = queued.prompt;
|
|
106
167
|
currentCtx = queued.ctx;
|
|
168
|
+
currentReceipt = queued.receipt;
|
|
107
169
|
chatState.continueAfterClose = false;
|
|
108
170
|
}
|
|
109
171
|
try {
|
|
110
|
-
await processPrompt({ prompt: currentPrompt, ctx: currentCtx });
|
|
172
|
+
await processPrompt({ prompt: currentPrompt, ctx: currentCtx, receipt: currentReceipt });
|
|
173
|
+
currentReceipt?.resolve({ status: "completed" });
|
|
111
174
|
} catch (error) {
|
|
175
|
+
currentReceipt?.reject(error);
|
|
112
176
|
if (chatState.continueAfterClose && chatState.pendingPrompts.length) {
|
|
113
177
|
await onPromptInterrupted?.(error);
|
|
114
178
|
} else {
|
|
@@ -117,11 +181,13 @@ export async function drainChatPromptQueue({
|
|
|
117
181
|
}
|
|
118
182
|
} finally {
|
|
119
183
|
currentCtx = null;
|
|
184
|
+
currentReceipt = null;
|
|
120
185
|
}
|
|
121
186
|
|
|
122
187
|
const queued = takeQueuedPrompt(chatState);
|
|
123
188
|
currentPrompt = queued.prompt;
|
|
124
189
|
currentCtx = queued.ctx;
|
|
190
|
+
currentReceipt = queued.receipt;
|
|
125
191
|
chatState.continueAfterClose = false;
|
|
126
192
|
}
|
|
127
193
|
} finally {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatLocationText } from "./media.js";
|
|
2
2
|
import { normalizeArtifactForReasoning, shouldNormalizeArtifactToText } from "../../core/artifacts/normalize-for-reasoning.js";
|
|
3
|
+
import { clampModelSpeed } from "../../core/agent/model-speed.js";
|
|
3
4
|
|
|
4
5
|
const slowPromptNoticeMs = 300_000;
|
|
5
6
|
|
|
@@ -169,6 +170,14 @@ export function isScheduledTaskPrompt(prompt) {
|
|
|
169
170
|
return String(prompt || "").startsWith("Scheduled task fired.\n");
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
export function scheduledPromptSpeedOptions({ prompt, session, speedController, configuredSpeed }) {
|
|
174
|
+
return {
|
|
175
|
+
speedController,
|
|
176
|
+
speed: isScheduledTaskPrompt(prompt) ? 1 : undefined,
|
|
177
|
+
restoreSpeed: () => clampModelSpeed(session.model, configuredSpeed)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
172
181
|
export async function withPromptSpeed({ speedController, speed, restoreSpeed }, work) {
|
|
173
182
|
if (!speedController || speed === undefined) return work();
|
|
174
183
|
speedController.setSpeed(speed);
|
|
@@ -1,9 +1,48 @@
|
|
|
1
|
+
import { NonRetryableTaskError, createTaskRunner } from "../../core/tasks/task-runner.js";
|
|
1
2
|
import { buildAsyncEventPrompt, buildAsyncTaskPrompt } from "./prompt-builders.js";
|
|
2
3
|
|
|
3
4
|
function errorMessage(error) {
|
|
4
5
|
return error instanceof Error ? error.message : String(error);
|
|
5
6
|
}
|
|
6
7
|
|
|
8
|
+
function requireChatId(task) {
|
|
9
|
+
const chatId = task.payload?.chatId;
|
|
10
|
+
if (chatId == null || chatId === "") {
|
|
11
|
+
throw new NonRetryableTaskError(`Task missing chatId: ${task.kind}`);
|
|
12
|
+
}
|
|
13
|
+
return chatId;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function safeErrorSummary(error) {
|
|
17
|
+
return errorMessage(error)
|
|
18
|
+
.replace(/(bearer\s+)[^\s]+/gi, "$1[redacted]")
|
|
19
|
+
.replace(/((?:api[_-]?key|token|secret|password)\s*[=:]\s*)[^\s,;]+/gi, "$1[redacted]")
|
|
20
|
+
.replace(/\s+/g, " ")
|
|
21
|
+
.trim()
|
|
22
|
+
.slice(0, 300) || "Unknown error";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function failureDestination(task) {
|
|
26
|
+
const destination = task.route?.transport === "telegram" ? task.route.destination : null;
|
|
27
|
+
return {
|
|
28
|
+
chatId: destination?.chatId || task.payload?.chatId,
|
|
29
|
+
threadId: destination?.threadId || null
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildFailureNotice({ task, result, error }) {
|
|
34
|
+
const uncertain = result?.status === "outcome_uncertain";
|
|
35
|
+
const recurring = result?.terminalFailure === true && result?.status === "pending";
|
|
36
|
+
const lines = [
|
|
37
|
+
uncertain ? "⚠️ Arisa task outcome is uncertain" : "⚠️ Arisa task failed",
|
|
38
|
+
`Task: ${task.kind || "unknown"} (${task.id})`,
|
|
39
|
+
`Error: ${safeErrorSummary(error)}`
|
|
40
|
+
];
|
|
41
|
+
if (recurring) lines.push(`Next run: ${result.runAt}`);
|
|
42
|
+
else lines.push("No further retries are scheduled.");
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
7
46
|
export function createTelegramTaskDispatcher({
|
|
8
47
|
taskStore,
|
|
9
48
|
sendMessage,
|
|
@@ -15,21 +54,18 @@ export function createTelegramTaskDispatcher({
|
|
|
15
54
|
logger
|
|
16
55
|
}) {
|
|
17
56
|
async function dispatchAgentTask(task, chatId) {
|
|
18
|
-
if (!task.payload.prompt)
|
|
19
|
-
await taskStore.fail(task.id, "agent_task missing prompt");
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
57
|
+
if (!task.payload.prompt) throw new NonRetryableTaskError("agent_task missing prompt");
|
|
22
58
|
logger?.log("tasks", `running task ${task.id} for chat ${chatId}`);
|
|
23
59
|
await enqueueAsyncPrompt({
|
|
24
60
|
chatId,
|
|
25
61
|
prompt: await buildAsyncTaskPrompt({ task, artifactStore, toolRegistry, resourceNotes, logger }),
|
|
26
62
|
label: `scheduled task ${task.id}`,
|
|
27
|
-
|
|
63
|
+
route: task.route
|
|
28
64
|
});
|
|
29
|
-
await taskStore.complete(task.id);
|
|
30
65
|
}
|
|
31
66
|
|
|
32
67
|
async function dispatchAgentEvent(task, chatId) {
|
|
68
|
+
if (!task.payload?.prompt) throw new NonRetryableTaskError("agent_event missing prompt");
|
|
33
69
|
logger?.log("tasks", `agent event ${task.id} for chat ${chatId}`);
|
|
34
70
|
const acknowledgement = String(task.payload?.acknowledgement || "").trim();
|
|
35
71
|
if (acknowledgement) {
|
|
@@ -43,52 +79,53 @@ export function createTelegramTaskDispatcher({
|
|
|
43
79
|
chatId,
|
|
44
80
|
prompt: await buildAsyncEventPrompt(task, resourceNotes),
|
|
45
81
|
label: `agent event ${task.id}`,
|
|
46
|
-
|
|
82
|
+
route: task.route
|
|
47
83
|
});
|
|
48
|
-
await taskStore.complete(task.id);
|
|
49
84
|
}
|
|
50
85
|
|
|
51
86
|
async function dispatchPollTool(task, chatId) {
|
|
52
87
|
const toolName = task.payload?.toolName;
|
|
53
|
-
if (!toolName)
|
|
54
|
-
await taskStore.fail(task.id, "poll_tool missing toolName");
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
88
|
+
if (!toolName) throw new NonRetryableTaskError("poll_tool missing toolName");
|
|
57
89
|
logger?.log("tasks", `polling tool ${toolName} (task ${task.id}) for chat ${chatId}`);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
90
|
+
const result = await agentManager.runTool({
|
|
91
|
+
name: toolName,
|
|
92
|
+
request: { args: task.payload.args || {} },
|
|
93
|
+
chatId
|
|
94
|
+
});
|
|
95
|
+
if (result?.ok === false) {
|
|
96
|
+
const error = new Error(result.error || `poll_tool ${toolName} failed`);
|
|
97
|
+
if (result.status === "needs_config") error.retryable = false;
|
|
98
|
+
if (result.status === "outcome_uncertain") {
|
|
99
|
+
error.retryable = false;
|
|
100
|
+
error.outcomeUncertain = true;
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
66
103
|
}
|
|
67
|
-
await taskStore.complete(task.id);
|
|
68
104
|
}
|
|
69
105
|
|
|
70
106
|
async function dispatchTask(task) {
|
|
71
|
-
const chatId = task
|
|
72
|
-
if (!chatId) {
|
|
73
|
-
await taskStore.fail(task.id, `Task missing chatId: ${task.kind}`);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
107
|
+
const chatId = requireChatId(task);
|
|
76
108
|
if (task.kind === "agent_task") return dispatchAgentTask(task, chatId);
|
|
77
109
|
if (task.kind === "agent_event") return dispatchAgentEvent(task, chatId);
|
|
78
110
|
if (task.kind === "poll_tool") return dispatchPollTool(task, chatId);
|
|
79
|
-
|
|
111
|
+
throw new NonRetryableTaskError(`Unsupported task: ${task.kind}`);
|
|
80
112
|
}
|
|
81
113
|
|
|
82
|
-
async function
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
} catch (error) {
|
|
88
|
-
await taskStore.fail(task.id, errorMessage(error));
|
|
89
|
-
}
|
|
114
|
+
async function notifyTerminalFailure(details) {
|
|
115
|
+
const destination = failureDestination(details.task);
|
|
116
|
+
if (!destination.chatId) {
|
|
117
|
+
logger?.log("tasks", `task ${details.task.id} has no Telegram failure-notification destination`);
|
|
118
|
+
return;
|
|
90
119
|
}
|
|
120
|
+
const options = destination.threadId ? { message_thread_id: destination.threadId } : undefined;
|
|
121
|
+
await sendMessage(destination.chatId, buildFailureNotice(details), options);
|
|
91
122
|
}
|
|
92
123
|
|
|
93
|
-
|
|
124
|
+
const runner = createTaskRunner({
|
|
125
|
+
taskStore,
|
|
126
|
+
dispatch: dispatchTask,
|
|
127
|
+
onTerminalFailure: notifyTerminalFailure,
|
|
128
|
+
logger
|
|
129
|
+
});
|
|
130
|
+
return { dispatchTask, dispatchDueTasks: runner.dispatchDueTasks, runClaimedTask: runner.runClaimedTask };
|
|
94
131
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildPiAuthRecoveryBlockedMessage,
|
|
3
|
+
buildPiAuthTelegramMessage,
|
|
4
|
+
getErrorMessage,
|
|
5
|
+
getPiAuthIssue,
|
|
6
|
+
getPiAuthStatus
|
|
7
|
+
} from "../../core/agent/auth-flow.js";
|
|
8
|
+
import { createPiOAuthLogin } from "../../core/agent/pi-auth-login.js";
|
|
9
|
+
import { buildDeviceCodeTelegramMessage } from "./device-code-message.js";
|
|
10
|
+
import { getIncomingMessageText } from "./prompt-builders.js";
|
|
11
|
+
|
|
12
|
+
function chatKey(chatId) {
|
|
13
|
+
return String(chatId);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function selectTelegramLoginOption(options = []) {
|
|
17
|
+
return options.find((option) => /device/i.test(`${option.id} ${option.label}`))
|
|
18
|
+
|| options.find((option) => /browser|oauth|web/i.test(`${option.id} ${option.label}`))
|
|
19
|
+
|| options[0]
|
|
20
|
+
|| null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createTelegramAuthController({
|
|
24
|
+
config,
|
|
25
|
+
api,
|
|
26
|
+
agentManager,
|
|
27
|
+
logger,
|
|
28
|
+
markPromptErrorNotified = () => {}
|
|
29
|
+
}) {
|
|
30
|
+
const renewals = new Map();
|
|
31
|
+
let issue = null;
|
|
32
|
+
|
|
33
|
+
function rememberIssue(error) {
|
|
34
|
+
const detected = getPiAuthIssue(error);
|
|
35
|
+
if (detected) issue = detected;
|
|
36
|
+
return detected;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function rememberValidationFailure(error) {
|
|
40
|
+
const detected = rememberIssue(error) || {
|
|
41
|
+
kind: "validation-failed",
|
|
42
|
+
message: getErrorMessage(error)
|
|
43
|
+
};
|
|
44
|
+
issue = detected;
|
|
45
|
+
return detected;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function notifyIssueIfNeeded(chatId, error) {
|
|
49
|
+
const detected = rememberIssue(error);
|
|
50
|
+
if (!detected) return false;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, issue: detected }));
|
|
54
|
+
markPromptErrorNotified(error);
|
|
55
|
+
return true;
|
|
56
|
+
} catch (notifyError) {
|
|
57
|
+
logger?.error("telegram", `auth issue notice failed for chat ${chatId}: ${getErrorMessage(notifyError)}`);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function finishRenewal(chatId, renewal) {
|
|
63
|
+
try {
|
|
64
|
+
await renewal.promise;
|
|
65
|
+
await agentManager.validateAgent();
|
|
66
|
+
agentManager.clearSessionCache(chatId);
|
|
67
|
+
issue = null;
|
|
68
|
+
logger?.log("telegram", `Pi auth renewal completed for chat ${chatId}`);
|
|
69
|
+
await api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, verified: true }));
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const detected = rememberValidationFailure(error);
|
|
72
|
+
logger?.error("telegram", `Pi auth renewal failed for chat ${chatId}: ${getErrorMessage(error)}`);
|
|
73
|
+
await api.sendMessage(chatId, buildPiAuthTelegramMessage({ config, chatId, issue: detected })).catch((notifyError) => {
|
|
74
|
+
logger?.error("telegram", `auth renewal failure notice failed for chat ${chatId}: ${getErrorMessage(notifyError)}`);
|
|
75
|
+
});
|
|
76
|
+
} finally {
|
|
77
|
+
renewals.delete(chatKey(chatId));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function startRenewal(chatId) {
|
|
82
|
+
const key = chatKey(chatId);
|
|
83
|
+
const existing = renewals.get(key);
|
|
84
|
+
if (existing) return { started: false, renewal: existing };
|
|
85
|
+
|
|
86
|
+
const renewal = createPiOAuthLogin({
|
|
87
|
+
provider: config.pi.provider,
|
|
88
|
+
onSelect: async ({ message, options }) => {
|
|
89
|
+
const selected = selectTelegramLoginOption(options);
|
|
90
|
+
if (!selected) return undefined;
|
|
91
|
+
logger?.log("telegram", `Pi auth option for chat ${chatId}: ${selected.id}`);
|
|
92
|
+
await api.sendMessage(chatId, `${message}\nUsing: ${selected.label || selected.id}`);
|
|
93
|
+
return selected.id;
|
|
94
|
+
},
|
|
95
|
+
onAuth: async ({ url, instructions }) => {
|
|
96
|
+
await api.sendMessage(chatId, [
|
|
97
|
+
instructions || "Open this URL to continue Pi authentication:",
|
|
98
|
+
url,
|
|
99
|
+
"After login, paste the full redirect URL back here."
|
|
100
|
+
].join("\n"));
|
|
101
|
+
},
|
|
102
|
+
onDeviceCode: async ({ userCode, verificationUri, expiresInSeconds }) => {
|
|
103
|
+
const payload = buildDeviceCodeTelegramMessage({ userCode, verificationUri, expiresInSeconds });
|
|
104
|
+
const { text, ...options } = payload;
|
|
105
|
+
await api.sendMessage(chatId, text, options);
|
|
106
|
+
},
|
|
107
|
+
onPrompt: async ({ message, controller }) => {
|
|
108
|
+
await api.sendMessage(chatId, `${message}\nReply here with the value.`);
|
|
109
|
+
return controller.waitForManualCode();
|
|
110
|
+
},
|
|
111
|
+
onProgress: (message) => {
|
|
112
|
+
if (message) logger?.log("telegram", `Pi auth progress for chat ${chatId}: ${message}`);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
renewals.set(key, renewal);
|
|
117
|
+
finishRenewal(chatId, renewal);
|
|
118
|
+
return { started: true, renewal };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function submitRenewalInput(ctx) {
|
|
122
|
+
const renewal = renewals.get(chatKey(ctx.chat.id));
|
|
123
|
+
const text = getIncomingMessageText(ctx.message).trim();
|
|
124
|
+
if (!renewal || !renewal.manualInputRequested || !text) return false;
|
|
125
|
+
if (!renewal.submitManualCode(text)) return false;
|
|
126
|
+
await ctx.reply("Got it. Finishing Pi login now...");
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildBlockedMessage(chatId) {
|
|
131
|
+
return buildPiAuthRecoveryBlockedMessage({
|
|
132
|
+
config,
|
|
133
|
+
chatId,
|
|
134
|
+
issue,
|
|
135
|
+
renewalActive: renewals.has(chatKey(chatId))
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function handleCommand(ctx, { authorize, withTyping }) {
|
|
140
|
+
const authorization = await authorize(ctx);
|
|
141
|
+
if (!authorization.ok) return;
|
|
142
|
+
|
|
143
|
+
const status = getPiAuthStatus(config, ctx.chat.id);
|
|
144
|
+
if (status.hasApiKey || !status.supportsOAuth) {
|
|
145
|
+
await withTyping(ctx, async () => {
|
|
146
|
+
try {
|
|
147
|
+
await agentManager.validateAgent();
|
|
148
|
+
agentManager.clearSessionCache(ctx.chat.id);
|
|
149
|
+
issue = null;
|
|
150
|
+
await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, verified: true }));
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const detected = rememberValidationFailure(error);
|
|
153
|
+
await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue: detected }));
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const { started } = await startRenewal(ctx.chat.id);
|
|
161
|
+
await ctx.reply(started
|
|
162
|
+
? "Starting Pi login from Telegram..."
|
|
163
|
+
: "Pi login is already in progress. Paste the redirect URL or code here when you have it.");
|
|
164
|
+
} catch (error) {
|
|
165
|
+
const detected = rememberValidationFailure(error);
|
|
166
|
+
await ctx.reply(buildPiAuthTelegramMessage({ config, chatId: ctx.chat.id, issue: detected }));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
buildBlockedMessage,
|
|
172
|
+
getIssue: () => issue,
|
|
173
|
+
handleCommand,
|
|
174
|
+
hasActiveRenewal: (chatId) => renewals.has(chatKey(chatId)),
|
|
175
|
+
notifyIssueIfNeeded,
|
|
176
|
+
rememberIssue,
|
|
177
|
+
startRenewal,
|
|
178
|
+
submitRenewalInput
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { InputFile } from "grammy";
|
|
3
|
+
import { cancelRestartReceipt, prepareRestartReceipt } from "../../runtime/restart-receipt.js";
|
|
4
|
+
import { isSilentReply } from "./prompt-builders.js";
|
|
5
|
+
import { renderTelegramHtml } from "./text-format.js";
|
|
6
|
+
import { resolveTelegramWorkspaceRoute, topicSessionId } from "./workspace-group.js";
|
|
7
|
+
|
|
8
|
+
function deliveryMethod(artifact, method) {
|
|
9
|
+
if (method) return method;
|
|
10
|
+
if (artifact.metadata?.delivery?.method) return artifact.metadata.delivery.method;
|
|
11
|
+
if (artifact.kind === "audio" || artifact.mimeType?.startsWith("audio/")) return "audio";
|
|
12
|
+
if (artifact.kind === "image" || artifact.mimeType?.startsWith("image/")) return "photo";
|
|
13
|
+
if (artifact.kind === "video" || artifact.mimeType?.startsWith("video/")) return "video";
|
|
14
|
+
return "document";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function safeCaption(caption) {
|
|
18
|
+
return caption && !/(^|\s)(\/[^\s]|[A-Za-z]:[\\/])/.test(caption) ? caption : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createTelegramSessionBridgeController({
|
|
22
|
+
config,
|
|
23
|
+
api,
|
|
24
|
+
agentManager,
|
|
25
|
+
artifactStore,
|
|
26
|
+
sessionSeeds,
|
|
27
|
+
getChatState,
|
|
28
|
+
buildTopicInitializationHandoff,
|
|
29
|
+
logger
|
|
30
|
+
}) {
|
|
31
|
+
function createWorkspaceAccessGuard(route) {
|
|
32
|
+
return async () => {
|
|
33
|
+
if (!route.workspace) return;
|
|
34
|
+
const current = await resolveTelegramWorkspaceRoute({
|
|
35
|
+
config,
|
|
36
|
+
api,
|
|
37
|
+
ctx: {
|
|
38
|
+
chat: { id: route.transportChatId, type: "supergroup", is_forum: true },
|
|
39
|
+
from: { id: route.ownerChatId },
|
|
40
|
+
message: { message_thread_id: route.threadId }
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
if (!current.ok) throw new Error("Owner-only workspace access is paused.");
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createSessionBridge(route) {
|
|
48
|
+
const messageOptions = (extra = {}) => route.workspace && route.threadId
|
|
49
|
+
? { ...extra, message_thread_id: route.threadId }
|
|
50
|
+
: extra;
|
|
51
|
+
const initializeForumTopic = async ({ messageThreadId, name, context }) => {
|
|
52
|
+
if (!route.workspace) throw new Error("Telegram topic initialization is only available from the owner workspace forum.");
|
|
53
|
+
await createWorkspaceAccessGuard(route)();
|
|
54
|
+
const initializedSessionId = topicSessionId({
|
|
55
|
+
ownerChatId: route.ownerChatId,
|
|
56
|
+
groupChatId: route.transportChatId,
|
|
57
|
+
threadId: messageThreadId,
|
|
58
|
+
generalTopicId: route.generalTopicId
|
|
59
|
+
});
|
|
60
|
+
if (initializedSessionId === String(route.ownerChatId)) {
|
|
61
|
+
throw new Error("The General topic already uses the owner's private session and cannot be reinitialized here.");
|
|
62
|
+
}
|
|
63
|
+
const handoff = buildTopicInitializationHandoff({ name, context });
|
|
64
|
+
await sessionSeeds.set(initializedSessionId, handoff);
|
|
65
|
+
agentManager.resetSession(initializedSessionId, { handoff });
|
|
66
|
+
await agentManager.waitForSessionClose(initializedSessionId);
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
chatId: route.transportChatId,
|
|
70
|
+
messageThreadId,
|
|
71
|
+
sessionId: initializedSessionId,
|
|
72
|
+
name,
|
|
73
|
+
initialized: true
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
sendMedia: async (filePath, { method = "audio", caption, filename } = {}) => {
|
|
78
|
+
logger?.log("telegram", `sending ${method} reply for chat ${route.transportChatId}`);
|
|
79
|
+
const input = new InputFile(filePath, filename || undefined);
|
|
80
|
+
const options = messageOptions({ caption });
|
|
81
|
+
if (method === "voice") return api.sendVoice(route.transportChatId, input, options);
|
|
82
|
+
if (method === "document") return api.sendDocument(route.transportChatId, input, options);
|
|
83
|
+
if (method === "photo" || method === "image") return api.sendPhoto(route.transportChatId, input, options);
|
|
84
|
+
if (method === "video") return api.sendVideo(route.transportChatId, input, options);
|
|
85
|
+
return api.sendAudio(route.transportChatId, input, options);
|
|
86
|
+
},
|
|
87
|
+
createForumTopic: async (name, context) => {
|
|
88
|
+
if (!route.workspace) throw new Error("Telegram topic creation is only available from the owner workspace forum.");
|
|
89
|
+
await createWorkspaceAccessGuard(route)();
|
|
90
|
+
const topic = await api.createForumTopic(route.transportChatId, name);
|
|
91
|
+
return initializeForumTopic({
|
|
92
|
+
messageThreadId: topic.message_thread_id,
|
|
93
|
+
name: topic.name,
|
|
94
|
+
context
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
initializeForumTopic,
|
|
98
|
+
prepareRestartReceipt: (summary) => prepareRestartReceipt({
|
|
99
|
+
transportChatId: route.transportChatId,
|
|
100
|
+
threadId: route.threadId
|
|
101
|
+
}, { reason: String(summary || "Agent-requested restart").trim() }),
|
|
102
|
+
cancelRestartReceipt,
|
|
103
|
+
getTaskContext: () => route.workspace ? {
|
|
104
|
+
transport: "telegram",
|
|
105
|
+
destination: {
|
|
106
|
+
chatId: route.transportChatId,
|
|
107
|
+
threadId: route.topicThreadId
|
|
108
|
+
}
|
|
109
|
+
} : null
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function sendTextReply({ sendText, sendDocument, chatId, artifactChatId = chatId, text }) {
|
|
114
|
+
const maxInlineReplyLength = 3500;
|
|
115
|
+
if (isSilentReply(text)) {
|
|
116
|
+
logger?.log("telegram", `suppressing silent reply for chat ${chatId}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (text.length > maxInlineReplyLength) {
|
|
121
|
+
logger?.log("telegram", `sending long reply as markdown attachment for chat ${chatId}`);
|
|
122
|
+
const chatArtifactStore = artifactStore.forChat(artifactChatId);
|
|
123
|
+
const artifact = await chatArtifactStore.createGeneratedFile({
|
|
124
|
+
fileName: `reply-${Date.now()}.md`,
|
|
125
|
+
content: text,
|
|
126
|
+
kind: "document",
|
|
127
|
+
mimeType: "text/markdown",
|
|
128
|
+
source: { type: "assistant", chatId },
|
|
129
|
+
metadata: { delivery: "telegram-document" }
|
|
130
|
+
});
|
|
131
|
+
await sendDocument(new InputFile(artifact.path, path.basename(artifact.path)), {
|
|
132
|
+
caption: "Response attached as Markdown."
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
logger?.log("telegram", `sending text reply for chat ${chatId}`);
|
|
138
|
+
const sent = await sendText(renderTelegramHtml(text), { parse_mode: "HTML" });
|
|
139
|
+
if (sent?.message_id) {
|
|
140
|
+
const messages = getChatState(chatId).assistantMessages;
|
|
141
|
+
messages.set(sent.message_id, text);
|
|
142
|
+
while (messages.size > 50) messages.delete(messages.keys().next().value);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function installArtifactDeliveryHandler() {
|
|
147
|
+
agentManager.setArtifactDeliveryHandler?.(async ({ chatId, artifact, caption, method }) => {
|
|
148
|
+
const resolvedMethod = deliveryMethod(artifact, method);
|
|
149
|
+
await createSessionBridge({
|
|
150
|
+
workspace: false,
|
|
151
|
+
sessionId: String(chatId),
|
|
152
|
+
scopeChatId: chatId,
|
|
153
|
+
transportChatId: chatId,
|
|
154
|
+
threadId: null
|
|
155
|
+
}).sendMedia(artifact.path, {
|
|
156
|
+
method: resolvedMethod,
|
|
157
|
+
caption: safeCaption(caption),
|
|
158
|
+
filename: path.basename(artifact.path)
|
|
159
|
+
});
|
|
160
|
+
return { ok: true, artifactId: artifact.id, method: resolvedMethod };
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
createSessionBridge,
|
|
166
|
+
createWorkspaceAccessGuard,
|
|
167
|
+
installArtifactDeliveryHandler,
|
|
168
|
+
sendTextReply
|
|
169
|
+
};
|
|
170
|
+
}
|