arisa 5.1.49 → 5.1.64
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/README.md +9 -0
- package/package.json +1 -1
- package/src/core/agent/agent-manager.js +49 -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/config/config-defaults.js +28 -1
- package/src/core/tasks/task-routing.js +7 -0
- package/src/core/tasks/task-runner.js +68 -0
- package/src/core/tasks/task-store.js +382 -92
- package/src/core/tools/tool-output-materializer.js +5 -5
- package/src/core/tools/tool-registry.js +20 -5
- package/src/core/tools/weighted-resource-governor.js +153 -0
- package/src/index.js +20 -0
- package/src/official-tools.lock.json +62 -45
- package/src/runtime/arisa-capabilities.js +51 -242
- package/src/runtime/create-app.js +11 -2
- package/src/runtime/create-headless-app.js +7 -4
- package/src/runtime/paths.js +4 -0
- package/src/runtime/service-manager.js +3 -1
- package/src/runtime/service-supervisor.js +98 -0
- package/src/transport/telegram/bot.js +186 -374
- package/src/transport/telegram/chat-queue.js +83 -6
- package/src/transport/telegram/prompt-builders.js +9 -0
- package/src/transport/telegram/reply-topic-routing.js +111 -0
- package/src/transport/telegram/task-dispatcher.js +96 -36
- package/src/transport/telegram/telegram-auth-controller.js +180 -0
- package/src/transport/telegram/telegram-session-bridge.js +177 -0
- package/src/transport/telegram/telegram-tools-command.js +28 -0
- package/src/transport/telegram/telegram-workspace-controller.js +66 -0
- package/src/transport/telegram/workspace-topic-store.js +228 -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/chat-queue.test.js +32 -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/model-selection.test.js +9 -1
- package/test/official-tool-dependencies.test.js +1 -1
- package/test/paths.test.js +8 -0
- package/test/pi-capability-tools.test.js +65 -0
- package/test/service-manager.test.js +48 -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 +231 -7
- package/test/telegram-reply-topic-routing.test.js +94 -0
- package/test/telegram-task-dispatcher.test.js +150 -23
- package/test/telegram-text-artifact.test.js +13 -2
- package/test/telegram-tools-command.test.js +47 -0
- package/test/telegram-workspace-topic-store.test.js +124 -0
- package/test/tool-registry-run.test.js +41 -0
- package/test/weighted-resource-governor.test.js +95 -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,69 @@ export function createChatStateStore() {
|
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
export function
|
|
34
|
+
export function createPromptExecutionReceipt(onStart = null) {
|
|
35
|
+
let resolve;
|
|
36
|
+
let reject;
|
|
37
|
+
let started = false;
|
|
38
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
39
|
+
resolve = resolvePromise;
|
|
40
|
+
reject = rejectPromise;
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
promise,
|
|
44
|
+
resolve,
|
|
45
|
+
reject,
|
|
46
|
+
start() {
|
|
47
|
+
if (started) return;
|
|
48
|
+
started = true;
|
|
49
|
+
onStart?.({ resolve, reject, promise });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function rejectQueuedReceipts(chatState, error) {
|
|
55
|
+
for (const receipt of chatState.pendingPromptReceipts || []) receipt?.reject(error);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const COALESCED_PROMPT_SEPARATOR = "\n\n--- next direct message ---\n\n";
|
|
59
|
+
|
|
60
|
+
function coalesceLastQueuedPrompt(chatState, prompt, ctx) {
|
|
61
|
+
const coalescible = chatState.pendingPromptCoalescible ||= [];
|
|
62
|
+
const index = chatState.pendingPrompts.length - 1;
|
|
63
|
+
if (index < 0 || !coalescible[index]) return false;
|
|
64
|
+
chatState.pendingPrompts[index] += `${COALESCED_PROMPT_SEPARATOR}${prompt}`;
|
|
65
|
+
(chatState.pendingPromptContexts ||= [])[index] = ctx;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function queueChatPrompt(chatState, prompt, {
|
|
70
|
+
replace = false,
|
|
71
|
+
ctx = null,
|
|
72
|
+
receipt = null,
|
|
73
|
+
coalescible = false
|
|
74
|
+
} = {}) {
|
|
33
75
|
chatState.pendingPromptContexts ||= [];
|
|
76
|
+
chatState.pendingPromptReceipts ||= [];
|
|
77
|
+
chatState.pendingPromptCoalescible ||= [];
|
|
34
78
|
if (replace) {
|
|
79
|
+
rejectQueuedReceipts(chatState, Object.assign(new Error("Queued prompt was superseded"), { code: "PROMPT_SUPERSEDED" }));
|
|
35
80
|
chatState.pendingPrompts = [];
|
|
36
81
|
chatState.pendingPromptContexts = [];
|
|
82
|
+
chatState.pendingPromptReceipts = [];
|
|
83
|
+
chatState.pendingPromptCoalescible = [];
|
|
37
84
|
}
|
|
38
85
|
chatState.pendingPrompts.push(prompt);
|
|
39
86
|
chatState.pendingPromptContexts.push(ctx);
|
|
87
|
+
chatState.pendingPromptReceipts.push(receipt);
|
|
88
|
+
chatState.pendingPromptCoalescible.push(coalescible);
|
|
40
89
|
}
|
|
41
90
|
|
|
42
91
|
function takeQueuedPrompt(chatState) {
|
|
92
|
+
(chatState.pendingPromptCoalescible ||= []).shift();
|
|
43
93
|
return {
|
|
44
94
|
prompt: chatState.pendingPrompts.shift() || "",
|
|
45
|
-
ctx: (chatState.pendingPromptContexts ||= []).shift() || null
|
|
95
|
+
ctx: (chatState.pendingPromptContexts ||= []).shift() || null,
|
|
96
|
+
receipt: (chatState.pendingPromptReceipts ||= []).shift() || null
|
|
46
97
|
};
|
|
47
98
|
}
|
|
48
99
|
|
|
@@ -52,7 +103,19 @@ export function resolveTelegramBusyMessageMode(config, chatId) {
|
|
|
52
103
|
return mode === "steer" ? "steer" : "queue";
|
|
53
104
|
}
|
|
54
105
|
|
|
55
|
-
export async function routeBusyPrompt({
|
|
106
|
+
export async function routeBusyPrompt({
|
|
107
|
+
chatState,
|
|
108
|
+
prompt,
|
|
109
|
+
mode = "queue",
|
|
110
|
+
replaceQueued = false,
|
|
111
|
+
ctx = null,
|
|
112
|
+
receipt = null,
|
|
113
|
+
coalesceQueued = false
|
|
114
|
+
}) {
|
|
115
|
+
if (coalesceQueued && !replaceQueued && !receipt && coalesceLastQueuedPrompt(chatState, prompt, ctx)) {
|
|
116
|
+
return { disposition: "coalesced" };
|
|
117
|
+
}
|
|
118
|
+
|
|
56
119
|
const session = chatState.activeSession;
|
|
57
120
|
if (
|
|
58
121
|
mode === "steer"
|
|
@@ -61,17 +124,23 @@ export async function routeBusyPrompt({ chatState, prompt, mode = "queue", repla
|
|
|
61
124
|
&& !chatState.beforeNextPrompt
|
|
62
125
|
&& session?.isStreaming
|
|
63
126
|
&& typeof session.steer === "function"
|
|
127
|
+
&& !receipt
|
|
64
128
|
) {
|
|
65
129
|
try {
|
|
66
130
|
await session.steer(prompt);
|
|
67
131
|
return { disposition: "steered" };
|
|
68
132
|
} catch (error) {
|
|
69
|
-
queueChatPrompt(chatState, prompt, { ctx });
|
|
133
|
+
queueChatPrompt(chatState, prompt, { ctx, receipt, coalescible: coalesceQueued });
|
|
70
134
|
return { disposition: "queued", steerError: error };
|
|
71
135
|
}
|
|
72
136
|
}
|
|
73
137
|
|
|
74
|
-
queueChatPrompt(chatState, prompt, {
|
|
138
|
+
queueChatPrompt(chatState, prompt, {
|
|
139
|
+
replace: replaceQueued,
|
|
140
|
+
ctx,
|
|
141
|
+
receipt,
|
|
142
|
+
coalescible: coalesceQueued
|
|
143
|
+
});
|
|
75
144
|
return { disposition: "queued" };
|
|
76
145
|
}
|
|
77
146
|
|
|
@@ -84,6 +153,7 @@ export async function drainChatPromptQueue({
|
|
|
84
153
|
chatState,
|
|
85
154
|
initialPrompt,
|
|
86
155
|
initialCtx = null,
|
|
156
|
+
initialReceipt = null,
|
|
87
157
|
processPrompt,
|
|
88
158
|
onPromptFailure,
|
|
89
159
|
onPromptInterrupted,
|
|
@@ -91,6 +161,7 @@ export async function drainChatPromptQueue({
|
|
|
91
161
|
}) {
|
|
92
162
|
let currentPrompt = initialPrompt;
|
|
93
163
|
let currentCtx = initialCtx;
|
|
164
|
+
let currentReceipt = initialReceipt;
|
|
94
165
|
|
|
95
166
|
try {
|
|
96
167
|
await beforeInitialPrompt?.();
|
|
@@ -104,11 +175,15 @@ export async function drainChatPromptQueue({
|
|
|
104
175
|
const queued = takeQueuedPrompt(chatState);
|
|
105
176
|
currentPrompt = queued.prompt;
|
|
106
177
|
currentCtx = queued.ctx;
|
|
178
|
+
currentReceipt = queued.receipt;
|
|
107
179
|
chatState.continueAfterClose = false;
|
|
108
180
|
}
|
|
109
181
|
try {
|
|
110
|
-
|
|
182
|
+
currentReceipt?.start?.();
|
|
183
|
+
await processPrompt({ prompt: currentPrompt, ctx: currentCtx, receipt: currentReceipt });
|
|
184
|
+
currentReceipt?.resolve({ status: "completed" });
|
|
111
185
|
} catch (error) {
|
|
186
|
+
currentReceipt?.reject(error);
|
|
112
187
|
if (chatState.continueAfterClose && chatState.pendingPrompts.length) {
|
|
113
188
|
await onPromptInterrupted?.(error);
|
|
114
189
|
} else {
|
|
@@ -117,11 +192,13 @@ export async function drainChatPromptQueue({
|
|
|
117
192
|
}
|
|
118
193
|
} finally {
|
|
119
194
|
currentCtx = null;
|
|
195
|
+
currentReceipt = null;
|
|
120
196
|
}
|
|
121
197
|
|
|
122
198
|
const queued = takeQueuedPrompt(chatState);
|
|
123
199
|
currentPrompt = queued.prompt;
|
|
124
200
|
currentCtx = queued.ctx;
|
|
201
|
+
currentReceipt = queued.receipt;
|
|
125
202
|
chatState.continueAfterClose = false;
|
|
126
203
|
}
|
|
127
204
|
} 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);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const replyTopicMarkerPattern = /\s*\[\[ARISA_REPLY_TOPIC:(\d+)\]\]\s*$/u;
|
|
2
|
+
const proposalMarkerPattern = /\s*\[\[ARISA_PROPOSE_TOPIC:([^\]\r\n]{1,80})\]\]\s*$/u;
|
|
3
|
+
const anyRoutingMarkerPattern = /\s*\[\[ARISA_(?:REPLY_TOPIC|PROPOSE_TOPIC):[^\]]*\]\]\s*$/u;
|
|
4
|
+
|
|
5
|
+
function singleLine(value, maxLength) {
|
|
6
|
+
return String(value || "").replace(/\s+/g, " ").trim().slice(0, maxLength);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isGeneralWorkspaceRoute(route) {
|
|
10
|
+
return Boolean(route?.workspace
|
|
11
|
+
&& route.threadId == null
|
|
12
|
+
&& route.topicThreadId === route.generalTopicId);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function activeTopics(topics = []) {
|
|
16
|
+
return topics
|
|
17
|
+
.map((topic) => ({
|
|
18
|
+
threadId: Number(topic?.threadId),
|
|
19
|
+
name: singleLine(topic?.name, 80),
|
|
20
|
+
description: singleLine(topic?.description, 240)
|
|
21
|
+
}))
|
|
22
|
+
.filter((topic) => Number.isSafeInteger(topic.threadId) && topic.threadId > 0 && topic.name)
|
|
23
|
+
.sort((left, right) => left.threadId - right.threadId)
|
|
24
|
+
.slice(0, 32);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildGeneralReplyRoutingInstruction(topics = [], recentProposals = []) {
|
|
28
|
+
const available = activeTopics(topics);
|
|
29
|
+
const options = available.length
|
|
30
|
+
? available.map((topic) => [
|
|
31
|
+
`- ${topic.threadId}: ${topic.name}`,
|
|
32
|
+
topic.description ? ` — ${topic.description}` : ""
|
|
33
|
+
].join(""))
|
|
34
|
+
: ["- No named topics are registered yet."];
|
|
35
|
+
const proposals = recentProposals
|
|
36
|
+
.map((proposal) => singleLine(proposal?.name, 80))
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.slice(0, 12);
|
|
39
|
+
return [
|
|
40
|
+
"Telegram General-topic management instruction:",
|
|
41
|
+
"This applies only because the incoming message was written in General inside an owner workspace supergroup. It never applies to a private chat with the bot.",
|
|
42
|
+
"The original message must stay in General and the conversation remains in the General session.",
|
|
43
|
+
"If, and only if, your final response clearly belongs to exactly one registered topic below, append [[ARISA_REPLY_TOPIC:<id>]] as the final line. Do not mention this routing marker.",
|
|
44
|
+
"If the relationship is weak, ambiguous, or no topic fits, do not append a reply marker and the response will remain in General.",
|
|
45
|
+
"You may occasionally propose a new topic only when the General conversation shows a substantial theme recurring across multiple turns and no registered topic fits. Ask for explicit confirmation in the visible response and append [[ARISA_PROPOSE_TOPIC:<short name>]] as the final line. Never create a topic without explicit confirmation.",
|
|
46
|
+
proposals.length ? `Do not repeat these recent topic proposals: ${proposals.join(", ")}.` : null,
|
|
47
|
+
"Registered reply topics:",
|
|
48
|
+
...options
|
|
49
|
+
].filter(Boolean).join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function appendGeneralReplyRoutingInstruction(prompt, topics = [], recentProposals = []) {
|
|
53
|
+
return `${String(prompt || "")}\n\n${buildGeneralReplyRoutingInstruction(topics, recentProposals)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function extractReplyTopicMetadata(text, topics = []) {
|
|
57
|
+
let cleanedText = String(text || "");
|
|
58
|
+
let requestedThreadId = null;
|
|
59
|
+
let proposal = "";
|
|
60
|
+
let changed = true;
|
|
61
|
+
while (changed) {
|
|
62
|
+
changed = false;
|
|
63
|
+
const replyMatch = cleanedText.match(replyTopicMarkerPattern);
|
|
64
|
+
if (replyMatch) {
|
|
65
|
+
requestedThreadId ??= Number(replyMatch[1]);
|
|
66
|
+
cleanedText = cleanedText.replace(replyTopicMarkerPattern, "");
|
|
67
|
+
changed = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const proposalMatch = cleanedText.match(proposalMarkerPattern);
|
|
71
|
+
if (proposalMatch) {
|
|
72
|
+
proposal ||= singleLine(proposalMatch[1], 80);
|
|
73
|
+
cleanedText = cleanedText.replace(proposalMarkerPattern, "");
|
|
74
|
+
changed = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (anyRoutingMarkerPattern.test(cleanedText)) {
|
|
78
|
+
cleanedText = cleanedText.replace(anyRoutingMarkerPattern, "");
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const available = activeTopics(topics);
|
|
84
|
+
const allowed = available.some((topic) => topic.threadId === requestedThreadId);
|
|
85
|
+
return {
|
|
86
|
+
text: cleanedText.trimEnd(),
|
|
87
|
+
threadId: allowed ? requestedThreadId : null,
|
|
88
|
+
proposal
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function routeGeneralWorkspaceReply({ route, text, topics = [] }) {
|
|
93
|
+
const generalWorkspaceRoute = isGeneralWorkspaceRoute(route);
|
|
94
|
+
const extracted = extractReplyTopicMetadata(text, generalWorkspaceRoute ? topics : []);
|
|
95
|
+
if (!generalWorkspaceRoute) {
|
|
96
|
+
return { text: extracted.text, route, topic: null, proposal: "" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const topic = activeTopics(topics).find((item) => item.threadId === extracted.threadId) || null;
|
|
100
|
+
if (!topic) return { text: extracted.text, route, topic: null, proposal: extracted.proposal };
|
|
101
|
+
return {
|
|
102
|
+
text: extracted.text,
|
|
103
|
+
route: {
|
|
104
|
+
...route,
|
|
105
|
+
threadId: topic.threadId,
|
|
106
|
+
topicThreadId: topic.threadId
|
|
107
|
+
},
|
|
108
|
+
topic,
|
|
109
|
+
proposal: ""
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -1,9 +1,55 @@
|
|
|
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 boundedTimeout(value, fallback) {
|
|
9
|
+
const parsed = Number(value);
|
|
10
|
+
return Number.isFinite(parsed) && parsed > 0
|
|
11
|
+
? Math.min(Math.round(parsed), 60 * 60_000)
|
|
12
|
+
: fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function requireChatId(task) {
|
|
16
|
+
const chatId = task.payload?.chatId;
|
|
17
|
+
if (chatId == null || chatId === "") {
|
|
18
|
+
throw new NonRetryableTaskError(`Task missing chatId: ${task.kind}`);
|
|
19
|
+
}
|
|
20
|
+
return chatId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function safeErrorSummary(error) {
|
|
24
|
+
return errorMessage(error)
|
|
25
|
+
.replace(/(bearer\s+)[^\s]+/gi, "$1[redacted]")
|
|
26
|
+
.replace(/((?:api[_-]?key|token|secret|password)\s*[=:]\s*)[^\s,;]+/gi, "$1[redacted]")
|
|
27
|
+
.replace(/\s+/g, " ")
|
|
28
|
+
.trim()
|
|
29
|
+
.slice(0, 300) || "Unknown error";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function failureDestination(task) {
|
|
33
|
+
const destination = task.route?.transport === "telegram" ? task.route.destination : null;
|
|
34
|
+
return {
|
|
35
|
+
chatId: destination?.chatId || task.payload?.chatId,
|
|
36
|
+
threadId: destination?.threadId || null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildFailureNotice({ task, result, error }) {
|
|
41
|
+
const uncertain = result?.status === "outcome_uncertain" || result?.lastOutcome === "outcome_uncertain";
|
|
42
|
+
const recurring = result?.terminalFailure === true && result?.status === "pending";
|
|
43
|
+
const lines = [
|
|
44
|
+
uncertain ? "⚠️ Arisa task outcome is uncertain" : "⚠️ Arisa task failed",
|
|
45
|
+
`Task: ${task.kind || "unknown"} (${task.id})`,
|
|
46
|
+
`Error: ${safeErrorSummary(error)}`
|
|
47
|
+
];
|
|
48
|
+
if (recurring) lines.push(`Next run: ${result.runAt}`);
|
|
49
|
+
else lines.push("No further retries are scheduled.");
|
|
50
|
+
return lines.join("\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
7
53
|
export function createTelegramTaskDispatcher({
|
|
8
54
|
taskStore,
|
|
9
55
|
sendMessage,
|
|
@@ -12,24 +58,26 @@ export function createTelegramTaskDispatcher({
|
|
|
12
58
|
toolRegistry,
|
|
13
59
|
resourceNotes,
|
|
14
60
|
agentManager,
|
|
61
|
+
taskTimeouts = {},
|
|
15
62
|
logger
|
|
16
63
|
}) {
|
|
64
|
+
const agentTimeoutMs = boundedTimeout(taskTimeouts.agentTimeoutMs, 15 * 60_000);
|
|
65
|
+
const eventTimeoutMs = boundedTimeout(taskTimeouts.eventTimeoutMs, 5 * 60_000);
|
|
66
|
+
|
|
17
67
|
async function dispatchAgentTask(task, chatId) {
|
|
18
|
-
if (!task.payload.prompt)
|
|
19
|
-
await taskStore.fail(task.id, "agent_task missing prompt");
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
68
|
+
if (!task.payload.prompt) throw new NonRetryableTaskError("agent_task missing prompt");
|
|
22
69
|
logger?.log("tasks", `running task ${task.id} for chat ${chatId}`);
|
|
23
70
|
await enqueueAsyncPrompt({
|
|
24
71
|
chatId,
|
|
25
72
|
prompt: await buildAsyncTaskPrompt({ task, artifactStore, toolRegistry, resourceNotes, logger }),
|
|
26
73
|
label: `scheduled task ${task.id}`,
|
|
27
|
-
|
|
74
|
+
route: task.route,
|
|
75
|
+
timeoutMs: agentTimeoutMs
|
|
28
76
|
});
|
|
29
|
-
await taskStore.complete(task.id);
|
|
30
77
|
}
|
|
31
78
|
|
|
32
79
|
async function dispatchAgentEvent(task, chatId) {
|
|
80
|
+
if (!task.payload?.prompt) throw new NonRetryableTaskError("agent_event missing prompt");
|
|
33
81
|
logger?.log("tasks", `agent event ${task.id} for chat ${chatId}`);
|
|
34
82
|
const acknowledgement = String(task.payload?.acknowledgement || "").trim();
|
|
35
83
|
if (acknowledgement) {
|
|
@@ -43,52 +91,64 @@ export function createTelegramTaskDispatcher({
|
|
|
43
91
|
chatId,
|
|
44
92
|
prompt: await buildAsyncEventPrompt(task, resourceNotes),
|
|
45
93
|
label: `agent event ${task.id}`,
|
|
46
|
-
|
|
94
|
+
route: task.route,
|
|
95
|
+
timeoutMs: eventTimeoutMs
|
|
47
96
|
});
|
|
48
|
-
await taskStore.complete(task.id);
|
|
49
97
|
}
|
|
50
98
|
|
|
51
99
|
async function dispatchPollTool(task, chatId) {
|
|
52
100
|
const toolName = task.payload?.toolName;
|
|
53
|
-
if (!toolName)
|
|
54
|
-
await taskStore.fail(task.id, "poll_tool missing toolName");
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
101
|
+
if (!toolName) throw new NonRetryableTaskError("poll_tool missing toolName");
|
|
57
102
|
logger?.log("tasks", `polling tool ${toolName} (task ${task.id}) for chat ${chatId}`);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
103
|
+
const result = await agentManager.runTool({
|
|
104
|
+
name: toolName,
|
|
105
|
+
request: { args: task.payload.args || {} },
|
|
106
|
+
chatId
|
|
107
|
+
});
|
|
108
|
+
if (result?.ok === false) {
|
|
109
|
+
const error = new Error(result.error || `poll_tool ${toolName} failed`);
|
|
110
|
+
if (result.status === "needs_config") error.retryable = false;
|
|
111
|
+
if (result.status === "outcome_uncertain") {
|
|
112
|
+
error.retryable = false;
|
|
113
|
+
error.outcomeUncertain = true;
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
66
116
|
}
|
|
67
|
-
await taskStore.complete(task.id);
|
|
68
117
|
}
|
|
69
118
|
|
|
70
119
|
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
|
-
}
|
|
120
|
+
const chatId = requireChatId(task);
|
|
76
121
|
if (task.kind === "agent_task") return dispatchAgentTask(task, chatId);
|
|
77
122
|
if (task.kind === "agent_event") return dispatchAgentEvent(task, chatId);
|
|
78
123
|
if (task.kind === "poll_tool") return dispatchPollTool(task, chatId);
|
|
79
|
-
|
|
124
|
+
throw new NonRetryableTaskError(`Unsupported task: ${task.kind}`);
|
|
80
125
|
}
|
|
81
126
|
|
|
82
|
-
async function
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
} catch (error) {
|
|
88
|
-
await taskStore.fail(task.id, errorMessage(error));
|
|
89
|
-
}
|
|
127
|
+
async function notifyTerminalFailure(details) {
|
|
128
|
+
const destination = failureDestination(details.task);
|
|
129
|
+
if (!destination.chatId) {
|
|
130
|
+
logger?.log("tasks", `task ${details.task.id} has no Telegram failure-notification destination`);
|
|
131
|
+
return;
|
|
90
132
|
}
|
|
133
|
+
const options = destination.threadId ? { message_thread_id: destination.threadId } : undefined;
|
|
134
|
+
await sendMessage(destination.chatId, buildFailureNotice(details), options);
|
|
91
135
|
}
|
|
92
136
|
|
|
93
|
-
|
|
137
|
+
const runner = createTaskRunner({
|
|
138
|
+
taskStore,
|
|
139
|
+
dispatch: dispatchTask,
|
|
140
|
+
laneKey(task) {
|
|
141
|
+
if (task.kind === "poll_tool") {
|
|
142
|
+
return `poll:${task.payload?.chatId}:${task.payload?.toolName || task.id}`;
|
|
143
|
+
}
|
|
144
|
+
const destination = task.route?.transport === "telegram" ? task.route.destination : null;
|
|
145
|
+
if (!destination?.chatId || Number(destination.threadId) === 1) {
|
|
146
|
+
return `agent:${task.payload?.chatId}`;
|
|
147
|
+
}
|
|
148
|
+
return `agent:${destination.chatId}:${destination.threadId || 0}`;
|
|
149
|
+
},
|
|
150
|
+
onTerminalFailure: notifyTerminalFailure,
|
|
151
|
+
logger
|
|
152
|
+
});
|
|
153
|
+
return { dispatchTask, dispatchDueTasks: runner.dispatchDueTasks, runClaimedTask: runner.runClaimedTask };
|
|
94
154
|
}
|
|
@@ -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
|
+
}
|