arisa 3.0.12 → 3.0.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "3.0.12",
3
+ "version": "3.0.14",
4
4
  "description": "Telegram + Pi Agent modular assistant",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -30,8 +30,8 @@
30
30
  "pi.dev",
31
31
  "clasen"
32
32
  ],
33
- "author": "",
34
- "license": "GLP",
33
+ "author": "Martin Clasen",
34
+ "license": "MIT",
35
35
  "packageManager": "pnpm@10.32.1",
36
36
  "dependencies": {
37
37
  "@mariozechner/pi-coding-agent": "^0.65.0",
@@ -8,10 +8,11 @@ import { arisaInstallDir, buildAgentRuntimeContext } from "./runtime-context.js"
8
8
  import { arisaHomeDir } from "../../runtime/paths.js";
9
9
 
10
10
  export class AgentManager {
11
- constructor({ config, artifactStore, toolRegistry, logger }) {
11
+ constructor({ config, artifactStore, toolRegistry, taskStore, logger }) {
12
12
  this.config = config;
13
13
  this.artifactStore = artifactStore;
14
14
  this.toolRegistry = toolRegistry;
15
+ this.taskStore = taskStore;
15
16
  this.logger = logger;
16
17
  this.sessions = new Map();
17
18
  }
@@ -65,7 +66,7 @@ export class AgentManager {
65
66
  }
66
67
 
67
68
  this.logger?.log("agent", `creating session for chat ${chatId}`);
68
- const customTools = this.createTools(telegram);
69
+ const customTools = this.createTools(telegram, chatId);
69
70
  const { session } = await createAgentSession({
70
71
  cwd: arisaInstallDir,
71
72
  agentDir: arisaHomeDir,
@@ -87,7 +88,7 @@ export class AgentManager {
87
88
  return ctx;
88
89
  }
89
90
 
90
- createTools(telegram) {
91
+ createTools(telegram, chatId) {
91
92
  return [
92
93
  defineTool({
93
94
  name: "list_tools",
@@ -175,12 +176,72 @@ export class AgentManager {
175
176
  await unlink(result.output.filePath).catch(() => {});
176
177
  }
177
178
 
179
+ if (result.asyncTask || result.asyncTasks?.length) {
180
+ const scheduled = await this.taskStore.addMany(
181
+ result.asyncTasks || [result.asyncTask],
182
+ {
183
+ payload: { chatId },
184
+ source: { type: "tool", toolName: params.name, chatId }
185
+ }
186
+ );
187
+ result.asyncTasks = scheduled;
188
+ delete result.asyncTask;
189
+ }
190
+
178
191
  return {
179
192
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
180
193
  details: result
181
194
  };
182
195
  }
183
196
  }),
197
+ defineTool({
198
+ name: "list_scheduled_tasks",
199
+ label: "List scheduled tasks",
200
+ description: "List scheduled async tasks for the current Telegram chat.",
201
+ parameters: Type.Object({
202
+ status: Type.Optional(Type.String())
203
+ }),
204
+ execute: async (_id, params) => {
205
+ const tasks = await this.taskStore.list({ chatId, status: params.status });
206
+ return {
207
+ content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }],
208
+ details: { tasks }
209
+ };
210
+ }
211
+ }),
212
+ defineTool({
213
+ name: "cancel_scheduled_task",
214
+ label: "Cancel scheduled task",
215
+ description: "Cancel one scheduled async task by id for the current Telegram chat.",
216
+ parameters: Type.Object({ id: Type.String() }),
217
+ execute: async (_id, params) => {
218
+ const existing = await this.taskStore.get(params.id);
219
+ if (!existing || existing.payload?.chatId !== chatId) {
220
+ return {
221
+ content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Task not found" }) }],
222
+ details: { ok: false, error: "Task not found" }
223
+ };
224
+ }
225
+ const task = await this.taskStore.cancel(params.id);
226
+ return {
227
+ content: [{ type: "text", text: JSON.stringify({ ok: true, task }, null, 2) }],
228
+ details: { ok: true, task }
229
+ };
230
+ }
231
+ }),
232
+ defineTool({
233
+ name: "cancel_all_scheduled_tasks",
234
+ label: "Cancel all scheduled tasks",
235
+ description: "Cancel all pending or running async tasks for the current Telegram chat.",
236
+ parameters: Type.Object({}),
237
+ execute: async () => {
238
+ const tasks = await this.taskStore.cancelAll({ chatId });
239
+ return {
240
+ content: [{ type: "text", text: JSON.stringify({ ok: true, cancelled: tasks.length }, null, 2) }],
241
+ details: { ok: true, tasks }
242
+ };
243
+ }
244
+ }),
184
245
  defineTool({
185
246
  name: "send_media_reply",
186
247
  label: "Send media reply",
@@ -0,0 +1,165 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { tasksFile } from "../../runtime/paths.js";
5
+
6
+ async function loadTasksFile() {
7
+ try {
8
+ return JSON.parse(await readFile(tasksFile, "utf8"));
9
+ } catch {
10
+ return [];
11
+ }
12
+ }
13
+
14
+ async function saveTasksFile(tasks) {
15
+ await mkdir(path.dirname(tasksFile), { recursive: true });
16
+ await writeFile(tasksFile, `${JSON.stringify(tasks, null, 2)}\n`, "utf8");
17
+ }
18
+
19
+ function taskId() {
20
+ return crypto.randomUUID();
21
+ }
22
+
23
+ function normalizeTask(task, defaults = {}) {
24
+ return {
25
+ id: task.id || taskId(),
26
+ status: task.status || "pending",
27
+ createdAt: task.createdAt || new Date().toISOString(),
28
+ updatedAt: new Date().toISOString(),
29
+ kind: task.kind,
30
+ runAt: task.runAt,
31
+ payload: {
32
+ ...(defaults.payload || {}),
33
+ ...(task.payload || {})
34
+ },
35
+ recurrence: task.recurrence || defaults.recurrence || null,
36
+ source: {
37
+ ...(defaults.source || {}),
38
+ ...(task.source || {})
39
+ }
40
+ };
41
+ }
42
+
43
+ function computeNextRunAt(task) {
44
+ if (task.recurrence?.type === "interval" && Number(task.recurrence.everySeconds) > 0) {
45
+ return new Date(Date.now() + (Number(task.recurrence.everySeconds) * 1000)).toISOString();
46
+ }
47
+ return "";
48
+ }
49
+
50
+ export class TaskStore {
51
+ constructor() {
52
+ this.tasks = null;
53
+ }
54
+
55
+ async init() {
56
+ if (!this.tasks) this.tasks = await loadTasksFile();
57
+ }
58
+
59
+ async save() {
60
+ await saveTasksFile(this.tasks || []);
61
+ }
62
+
63
+ async add(task, defaults = {}) {
64
+ await this.init();
65
+ const normalized = normalizeTask(task, defaults);
66
+ this.tasks.push(normalized);
67
+ await this.save();
68
+ return normalized;
69
+ }
70
+
71
+ async addMany(tasks = [], defaults = {}) {
72
+ const created = [];
73
+ for (const task of tasks) {
74
+ created.push(await this.add(task, defaults));
75
+ }
76
+ return created;
77
+ }
78
+
79
+ async claimDue(limit = 10) {
80
+ await this.init();
81
+ const now = Date.now();
82
+ const due = [];
83
+
84
+ for (const task of this.tasks) {
85
+ if (due.length >= limit) break;
86
+ if (task.status !== "pending") continue;
87
+ if (!task.runAt || Number.isNaN(Date.parse(task.runAt))) continue;
88
+ if (Date.parse(task.runAt) > now) continue;
89
+ task.status = "running";
90
+ task.updatedAt = new Date().toISOString();
91
+ due.push({ ...task });
92
+ }
93
+
94
+ if (due.length) await this.save();
95
+ return due;
96
+ }
97
+
98
+ async complete(taskId) {
99
+ await this.init();
100
+ const task = this.tasks.find((item) => item.id === taskId);
101
+ if (!task) return null;
102
+
103
+ const nextRunAt = computeNextRunAt(task);
104
+ if (nextRunAt) {
105
+ task.status = "pending";
106
+ task.runAt = nextRunAt;
107
+ task.lastRunAt = new Date().toISOString();
108
+ } else {
109
+ task.status = "done";
110
+ task.completedAt = new Date().toISOString();
111
+ }
112
+ task.updatedAt = new Date().toISOString();
113
+ await this.save();
114
+ return task;
115
+ }
116
+
117
+ async fail(taskId, error) {
118
+ await this.init();
119
+ const task = this.tasks.find((item) => item.id === taskId);
120
+ if (!task) return null;
121
+ task.status = "failed";
122
+ task.error = error;
123
+ task.updatedAt = new Date().toISOString();
124
+ await this.save();
125
+ return task;
126
+ }
127
+
128
+ async list(filter = {}) {
129
+ await this.init();
130
+ return this.tasks.filter((task) => {
131
+ if (filter.chatId && task.payload?.chatId !== filter.chatId) return false;
132
+ if (filter.status && task.status !== filter.status) return false;
133
+ if (filter.kind && task.kind !== filter.kind) return false;
134
+ return true;
135
+ });
136
+ }
137
+
138
+ async get(taskId) {
139
+ await this.init();
140
+ return this.tasks.find((item) => item.id === taskId) || null;
141
+ }
142
+
143
+ async cancel(taskId) {
144
+ await this.init();
145
+ const index = this.tasks.findIndex((item) => item.id === taskId);
146
+ if (index === -1) return null;
147
+ const [task] = this.tasks.splice(index, 1);
148
+ await this.save();
149
+ return task;
150
+ }
151
+
152
+ async cancelAll(filter = {}) {
153
+ await this.init();
154
+ const removed = [];
155
+ this.tasks = this.tasks.filter((task) => {
156
+ if (filter.chatId && task.payload?.chatId !== filter.chatId) return true;
157
+ if (filter.status && task.status !== filter.status) return true;
158
+ if (task.status === "done" || task.status === "failed") return true;
159
+ removed.push({ ...task });
160
+ return false;
161
+ });
162
+ if (removed.length) await this.save();
163
+ return removed;
164
+ }
165
+ }
@@ -119,7 +119,11 @@ export class ToolRegistry {
119
119
  try {
120
120
  const parsed = JSON.parse(result.stdout || result.stderr);
121
121
  const normalized = normalizeToolResult(name, parsed);
122
- this.logger?.log("tools", `${name} -> ${normalized.ok === false ? normalized.status || "error" : "ok"}`);
122
+ if (normalized.ok === false) {
123
+ this.logger?.log("tools", `${name} -> ${normalized.status || "error"}: ${normalized.error || "unknown error"}`);
124
+ } else {
125
+ this.logger?.log("tools", `${name} -> ok`);
126
+ }
123
127
  return normalized;
124
128
  } catch {
125
129
  return normalizeToolResult(name, {
@@ -1,6 +1,7 @@
1
1
  import { loadConfig, saveConfig, updateConfig } from "../core/config/config-store.js";
2
2
  import { ArtifactStore } from "../core/artifacts/artifact-store.js";
3
3
  import { ToolRegistry } from "../core/tools/tool-registry.js";
4
+ import { TaskStore } from "../core/tasks/task-store.js";
4
5
  import { AgentManager } from "../core/agent/agent-manager.js";
5
6
  import { createTelegramBot } from "../transport/telegram/bot.js";
6
7
 
@@ -9,11 +10,12 @@ export async function createApp({ logger } = {}) {
9
10
  const config = await loadConfig();
10
11
  const artifactStore = new ArtifactStore();
11
12
  const toolRegistry = new ToolRegistry({ logger });
13
+ const taskStore = new TaskStore();
12
14
  await toolRegistry.load();
13
15
  logger?.log("app", `loaded ${toolRegistry.list().length} tools`);
14
16
 
15
- const agentManager = new AgentManager({ config, artifactStore, toolRegistry, logger });
16
- const bot = await createTelegramBot({ config, artifactStore, toolRegistry, agentManager, saveConfig, updateConfig, logger });
17
+ const agentManager = new AgentManager({ config, artifactStore, toolRegistry, taskStore, logger });
18
+ const bot = await createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger });
17
19
 
18
20
  return {
19
21
  async start() {
@@ -9,6 +9,7 @@ export const servicePidFile = path.join(stateDir, "arisa.pid");
9
9
  export const serviceLogFile = path.join(stateDir, "arisa.log");
10
10
  export const artifactsDir = path.join(arisaHomeDir, "artifacts");
11
11
  export const artifactsIndexFile = path.join(stateDir, "artifacts.json");
12
+ export const tasksFile = path.join(stateDir, "tasks.json");
12
13
  export const toolsDir = path.join(arisaHomeDir, "tools");
13
14
 
14
15
  export function getToolDir(toolName) {
@@ -32,14 +32,20 @@ function quotedMessageSummary(message) {
32
32
  return parts;
33
33
  }
34
34
 
35
+ function getTelegramCommand(ctx) {
36
+ const text = ctx.message?.text || "";
37
+ const entity = ctx.message?.entities?.[0];
38
+ if (entity?.type !== "bot_command" || entity.offset !== 0 || !text.startsWith("/")) return "";
39
+ return text.slice(1, entity.length).split("@")[0].trim().toLowerCase();
40
+ }
41
+
35
42
  function buildPrompt({ ctx, artifact, transcript, toolResult }) {
36
43
  const parts = [
37
- `New Telegram message.`,
44
+ `New Session..`,
38
45
  `chatId: ${ctx.chat.id}`,
39
46
  `userId: ${ctx.from.id}`,
40
47
  `username: ${ctx.from.username || "(no username)"}`,
41
- `messageId: ${ctx.msg.message_id}`,
42
- `preferredTelegramLanguageCode: ${ctx.from?.language_code || "unknown"}`
48
+ `messageId: ${ctx.msg.message_id}`
43
49
  ];
44
50
 
45
51
  if (ctx.message?.text) parts.push(`text: ${ctx.message.text}`);
@@ -64,6 +70,26 @@ function buildPrompt({ ctx, artifact, transcript, toolResult }) {
64
70
  return parts.join("\n");
65
71
  }
66
72
 
73
+ function buildNewSessionPrompt(ctx) {
74
+ return [
75
+ "System event: /new requested.",
76
+ "Session was reset.",
77
+ `preferredTelegramLanguageCode: ${ctx.from?.language_code || "unknown"}`,
78
+ "Reply with a brief, warm confirmation in the user's language."
79
+ ].join("\n");
80
+ }
81
+
82
+ function buildAsyncTaskPrompt(task) {
83
+ return [
84
+ "Scheduled task fired.",
85
+ `taskId: ${task.id}`,
86
+ `chatId: ${task.payload.chatId}`,
87
+ task.payload.prompt ? `text: ${task.payload.prompt}` : null,
88
+ "Treat this as a new request for the chat and fulfill it now.",
89
+ "If needed, use tools."
90
+ ].filter(Boolean).join("\n");
91
+ }
92
+
67
93
  async function maybeTranscribeIncomingAudio({ artifact, toolRegistry, artifactStore }) {
68
94
  if (!artifact || artifact.kind !== "audio") return { transcript: null };
69
95
 
@@ -117,7 +143,7 @@ async function withTyping(ctx, work) {
117
143
  }
118
144
  }
119
145
 
120
- export async function createTelegramBot({ config, artifactStore, toolRegistry, agentManager, saveConfig, updateConfig, logger }) {
146
+ export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger }) {
121
147
  const bot = new Bot(config.telegram.apiKey);
122
148
  const perChatState = new Map();
123
149
 
@@ -172,51 +198,58 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
172
198
  await sendText(renderTelegramHtml(text), { parse_mode: "HTML" });
173
199
  }
174
200
 
175
- async function processPrompt(ctx, prompt) {
176
- const telegram = {
201
+ function createTelegramSessionBridge(chatId) {
202
+ return {
177
203
  sendMedia: async (filePath, { method = "audio", caption } = {}) => {
178
- logger?.log("telegram", `sending ${method} reply for chat ${ctx.chat.id}`);
204
+ logger?.log("telegram", `sending ${method} reply for chat ${chatId}`);
179
205
  const input = new InputFile(filePath);
180
- if (method === "voice") return ctx.replyWithVoice(input, { caption });
181
- if (method === "document") return ctx.replyWithDocument(input, { caption });
182
- return ctx.replyWithAudio(input, { caption });
206
+ if (method === "voice") return bot.api.sendVoice(chatId, input, { caption });
207
+ if (method === "document") return bot.api.sendDocument(chatId, input, { caption });
208
+ return bot.api.sendAudio(chatId, input, { caption });
183
209
  }
184
210
  };
185
- return withTyping(ctx, async () => {
186
- const { session } = await agentManager.getSessionContext(ctx.chat.id, telegram);
211
+ }
212
+
213
+ async function processPromptForChat({ chatId, prompt, ctx = null }) {
214
+ const work = async () => {
215
+ const { session } = await agentManager.getSessionContext(chatId, createTelegramSessionBridge(chatId));
187
216
  const text = await collectText(session, prompt);
188
217
  if (text) {
189
218
  await sendTextReply({
190
- sendText: (message, extra) => ctx.reply(message, extra),
191
- sendDocument: (file, extra) => ctx.replyWithDocument(file, extra),
192
- chatId: ctx.chat.id,
219
+ sendText: (message, extra) => bot.api.sendMessage(chatId, message, extra),
220
+ sendDocument: (file, extra) => bot.api.sendDocument(chatId, file, extra),
221
+ chatId,
193
222
  text
194
223
  });
195
224
  }
196
- });
225
+ };
226
+
227
+ if (ctx) return withTyping(ctx, work);
228
+ return work();
197
229
  }
198
230
 
199
- async function enqueueOrProcess(ctx) {
200
- const chatState = getChatState(ctx.chat.id);
201
- const incomingPrompt = await buildIncomingPrompt(ctx);
231
+ async function enqueuePrompt({ chatId, prompt, label, ctx = null }) {
232
+ const chatState = getChatState(chatId);
202
233
 
203
234
  if (chatState.processing) {
204
- logger?.log("telegram", `chat ${ctx.chat.id} busy, queueing message ${ctx.msg.message_id}`);
235
+ logger?.log("telegram", `chat ${chatId} busy, queueing ${label}`);
205
236
  chatState.nextPrompt = chatState.nextPrompt
206
- ? `${chatState.nextPrompt}\n\n${incomingPrompt}`
207
- : incomingPrompt;
208
- return ctx.reply("Queued. I will process this right after the current task finishes.");
237
+ ? `${chatState.nextPrompt}\n\n${prompt}`
238
+ : prompt;
239
+ return;
209
240
  }
210
241
 
211
242
  chatState.processing = true;
212
- logger?.log("telegram", `processing message ${ctx.msg.message_id} in chat ${ctx.chat.id}`);
213
- let currentPrompt = incomingPrompt;
243
+ logger?.log("telegram", `processing ${label} in chat ${chatId}`);
244
+ let currentPrompt = prompt;
245
+ let currentCtx = ctx;
214
246
 
215
247
  while (currentPrompt) {
216
248
  try {
217
- logger?.log("telegram", `prompt dispatch for chat ${ctx.chat.id}`);
218
- await processPrompt(ctx, currentPrompt);
249
+ logger?.log("telegram", `prompt dispatch for chat ${chatId}`);
250
+ await processPromptForChat({ chatId, prompt: currentPrompt, ctx: currentCtx });
219
251
  } finally {
252
+ currentCtx = null;
220
253
  if (chatState.nextPrompt) {
221
254
  currentPrompt = chatState.nextPrompt;
222
255
  chatState.nextPrompt = "";
@@ -229,6 +262,38 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
229
262
  chatState.processing = false;
230
263
  }
231
264
 
265
+ async function enqueueOrProcess(ctx) {
266
+ const chatState = getChatState(ctx.chat.id);
267
+
268
+ if (chatState.processing) {
269
+ const incomingPrompt = await buildIncomingPrompt(ctx);
270
+ return enqueuePrompt({
271
+ chatId: ctx.chat.id,
272
+ prompt: incomingPrompt,
273
+ label: `message ${ctx.msg.message_id}`
274
+ });
275
+ }
276
+
277
+ const incomingPrompt = await buildIncomingPrompt(ctx);
278
+ return enqueuePrompt({
279
+ chatId: ctx.chat.id,
280
+ prompt: incomingPrompt,
281
+ label: `message ${ctx.msg.message_id}`,
282
+ ctx
283
+ });
284
+ }
285
+
286
+ async function handleNewCommand(ctx) {
287
+ agentManager.resetSession(ctx.chat.id);
288
+ perChatState.set(ctx.chat.id, { processing: false, nextPrompt: "" });
289
+ await enqueuePrompt({
290
+ chatId: ctx.chat.id,
291
+ prompt: buildNewSessionPrompt(ctx),
292
+ label: "new-session command",
293
+ ctx
294
+ });
295
+ }
296
+
232
297
  bot.catch((error) => {
233
298
  logger?.error("telegram", `bot error: ${error instanceof Error ? error.message : String(error)}`);
234
299
  console.error("Telegram bot error:", error);
@@ -243,15 +308,16 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
243
308
  bot.command("new", async (ctx) => {
244
309
  const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
245
310
  if (!auth.ok) return;
246
- agentManager.resetSession(ctx.chat.id);
247
- perChatState.set(ctx.chat.id, { processing: false, nextPrompt: "" });
248
- return ctx.reply("Started a new chat context.");
311
+ await handleNewCommand(ctx);
249
312
  });
250
313
 
251
314
  bot.on("message", async (ctx) => {
252
315
  const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
253
316
  if (!auth.ok) return;
254
317
 
318
+ const command = getTelegramCommand(ctx);
319
+ if (command) return;
320
+
255
321
  try {
256
322
  await enqueueOrProcess(ctx);
257
323
  } catch (error) {
@@ -269,16 +335,6 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
269
335
  try {
270
336
  logger?.log("telegram", `generating startup message for chat ${chatId}`);
271
337
  const chatMeta = config.telegram.chatMeta[chatId] || {};
272
- const telegram = {
273
- sendMedia: async (filePath, { method = "audio", caption } = {}) => {
274
- logger?.log("telegram", `sending ${method} reply for chat ${chatId}`);
275
- const input = new InputFile(filePath);
276
- if (method === "voice") return bot.api.sendVoice(chatId, input, { caption });
277
- if (method === "document") return bot.api.sendDocument(chatId, input, { caption });
278
- return bot.api.sendAudio(chatId, input, { caption });
279
- }
280
- };
281
- const { session } = await agentManager.getSessionContext(chatId, telegram);
282
338
  const welcomePrompt = [
283
339
  "System event: Arisa has just started.",
284
340
  `chatId: ${chatId}`,
@@ -290,15 +346,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
290
346
  "Use the user's Telegram language when possible.",
291
347
  "Do not mention internal implementation details."
292
348
  ].filter(Boolean).join("\n");
293
- const text = await collectText(session, welcomePrompt);
294
- if (text) {
295
- await sendTextReply({
296
- sendText: (message, extra) => bot.api.sendMessage(chatId, message, extra),
297
- sendDocument: (file, extra) => bot.api.sendDocument(chatId, file, extra),
298
- chatId,
299
- text
300
- });
301
- }
349
+ await enqueuePrompt({ chatId, prompt: welcomePrompt, label: "startup message" });
302
350
  } catch (error) {
303
351
  logger?.log("telegram", `startup message failed for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`);
304
352
  }
@@ -306,8 +354,28 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
306
354
  await bot.api.setMyCommands([
307
355
  { command: "new", description: "Start a new chat context" }
308
356
  ]);
357
+ setInterval(async () => {
358
+ const tasks = await taskStore.claimDue(10);
359
+ for (const task of tasks) {
360
+ try {
361
+ if (task.kind !== "agent_task" || !task.payload?.chatId || !task.payload?.prompt) {
362
+ await taskStore.fail(task.id, `Unsupported task: ${task.kind}`);
363
+ continue;
364
+ }
365
+ logger?.log("tasks", `running task ${task.id} for chat ${task.payload.chatId}`);
366
+ await enqueuePrompt({
367
+ chatId: task.payload.chatId,
368
+ prompt: buildAsyncTaskPrompt(task),
369
+ label: `scheduled task ${task.id}`
370
+ });
371
+ await taskStore.complete(task.id);
372
+ } catch (error) {
373
+ await taskStore.fail(task.id, error instanceof Error ? error.message : String(error));
374
+ }
375
+ }
376
+ }, 1000).unref();
309
377
  logger?.log("telegram", "bot polling started");
310
- await bot.start();
378
+ await bot.start({ drop_pending_updates: true });
311
379
  }
312
380
  };
313
381
  }
@@ -0,0 +1 @@
1
+ export default {};
@@ -0,0 +1,68 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { toolError, toolOk } from "../../src/core/tools/tool-result.js";
3
+
4
+ function printHelp() {
5
+ console.log(`schedule-agent-task\n\nUsage:\n node index.js --help\n node index.js run --request-file <json>\n\nExpected input:\n {\n "text": "tell me the temperature in Toronto",\n "artifact": { "text": "tell me the temperature in Toronto" },\n "args": {\n "prompt": "tell me the temperature in Toronto",\n "runAt": "2026-04-07T14:00:00.000Z",\n "delaySeconds": "30",\n "intervalSeconds": "3600"\n }\n }\n\nBehavior:\n - schedules a future agent task for the current chat\n - provide either args.runAt or args.delaySeconds\n - optional args.intervalSeconds makes the task recurring\n`);
6
+ }
7
+
8
+ function firstNonEmpty(...values) {
9
+ return values.find((value) => String(value || "").trim()) || "";
10
+ }
11
+
12
+ function buildRunAt(args = {}) {
13
+ const runAtValue = firstNonEmpty(args.runAt, args.at, args.when);
14
+ if (runAtValue) {
15
+ const parsed = Date.parse(runAtValue);
16
+ if (Number.isNaN(parsed)) return "";
17
+ return new Date(parsed).toISOString();
18
+ }
19
+
20
+ const delaySeconds = Number(firstNonEmpty(args.delaySeconds, args.delay, args.seconds));
21
+ if (Number.isFinite(delaySeconds) && delaySeconds > 0) {
22
+ return new Date(Date.now() + (delaySeconds * 1000)).toISOString();
23
+ }
24
+
25
+ return "";
26
+ }
27
+
28
+ async function run(requestFile) {
29
+ const request = JSON.parse(await readFile(requestFile, "utf8"));
30
+ const args = request.args || {};
31
+ const prompt = firstNonEmpty(args.prompt, args.message, args.task, request.text, request.artifact?.text);
32
+ const runAt = buildRunAt(args);
33
+ const intervalSeconds = Number(firstNonEmpty(args.intervalSeconds, args.interval, args.everySeconds));
34
+
35
+ if (!prompt.trim()) {
36
+ console.log(JSON.stringify(toolError("prompt/message/task, text, or artifact.text is required")));
37
+ return;
38
+ }
39
+
40
+ if (!runAt) {
41
+ console.log(JSON.stringify(toolError("args.runAt/at/when or args.delaySeconds/delay/seconds is required")));
42
+ return;
43
+ }
44
+
45
+ const asyncTask = {
46
+ kind: "agent_task",
47
+ runAt,
48
+ payload: { prompt },
49
+ recurrence: Number.isFinite(intervalSeconds) && intervalSeconds > 0
50
+ ? { type: "interval", everySeconds: intervalSeconds }
51
+ : null
52
+ };
53
+
54
+ console.log(JSON.stringify(toolOk({ runAt }, {
55
+ status: "scheduled",
56
+ asyncTask
57
+ })));
58
+ }
59
+
60
+ const args = process.argv.slice(2);
61
+ if (!args.length || args.includes("--help") || args[0] === "help") {
62
+ printHelp();
63
+ } else if (args[0] === "run") {
64
+ const fileIndex = args.indexOf("--request-file");
65
+ await run(args[fileIndex + 1]);
66
+ } else {
67
+ printHelp();
68
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "schedule-agent-task-cli",
3
+ "private": true,
4
+ "type": "module",
5
+ "version": "1.0.0"
6
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "schedule-agent-task",
3
+ "description": "Schedule a future Pi Agent task for the current chat.",
4
+ "entry": "index.js",
5
+ "input": ["text/plain"],
6
+ "output": ["application/json"],
7
+ "configSchema": {}
8
+ }
@@ -47,7 +47,7 @@ async function fetchText(url) {
47
47
  const response = await fetch(url, {
48
48
  headers: {
49
49
  "user-agent": "Mozilla/5.0",
50
- "accept-language": "es-AR,es;q=0.9,en;q=0.8"
50
+ "accept-language": "en-US,en;q=0.9"
51
51
  },
52
52
  redirect: "follow"
53
53
  });