arisa 3.0.11 → 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.11",
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
  }
@@ -21,6 +22,10 @@ export class AgentManager {
21
22
  this.sessions.clear();
22
23
  }
23
24
 
25
+ resetSession(chatId) {
26
+ this.sessions.delete(chatId);
27
+ }
28
+
24
29
  async validatePiAgent() {
25
30
  this.logger?.log("agent", "validating Pi session");
26
31
  const { authStorage, modelRegistry } = createPiRuntime({
@@ -61,7 +66,7 @@ export class AgentManager {
61
66
  }
62
67
 
63
68
  this.logger?.log("agent", `creating session for chat ${chatId}`);
64
- const customTools = this.createTools(telegram);
69
+ const customTools = this.createTools(telegram, chatId);
65
70
  const { session } = await createAgentSession({
66
71
  cwd: arisaInstallDir,
67
72
  agentDir: arisaHomeDir,
@@ -83,7 +88,7 @@ export class AgentManager {
83
88
  return ctx;
84
89
  }
85
90
 
86
- createTools(telegram) {
91
+ createTools(telegram, chatId) {
87
92
  return [
88
93
  defineTool({
89
94
  name: "list_tools",
@@ -171,12 +176,72 @@ export class AgentManager {
171
176
  await unlink(result.output.filePath).catch(() => {});
172
177
  }
173
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
+
174
191
  return {
175
192
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
176
193
  details: result
177
194
  };
178
195
  }
179
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
+ }),
180
245
  defineTool({
181
246
  name: "send_media_reply",
182
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);
@@ -240,10 +305,19 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
240
305
  return ctx.reply(auth.firstTime ? "This chat is now authorized for Arisa." : "Arisa is ready.");
241
306
  });
242
307
 
308
+ bot.command("new", async (ctx) => {
309
+ const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
310
+ if (!auth.ok) return;
311
+ await handleNewCommand(ctx);
312
+ });
313
+
243
314
  bot.on("message", async (ctx) => {
244
315
  const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
245
316
  if (!auth.ok) return;
246
317
 
318
+ const command = getTelegramCommand(ctx);
319
+ if (command) return;
320
+
247
321
  try {
248
322
  await enqueueOrProcess(ctx);
249
323
  } catch (error) {
@@ -261,16 +335,6 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
261
335
  try {
262
336
  logger?.log("telegram", `generating startup message for chat ${chatId}`);
263
337
  const chatMeta = config.telegram.chatMeta[chatId] || {};
264
- const telegram = {
265
- sendMedia: async (filePath, { method = "audio", caption } = {}) => {
266
- logger?.log("telegram", `sending ${method} reply for chat ${chatId}`);
267
- const input = new InputFile(filePath);
268
- if (method === "voice") return bot.api.sendVoice(chatId, input, { caption });
269
- if (method === "document") return bot.api.sendDocument(chatId, input, { caption });
270
- return bot.api.sendAudio(chatId, input, { caption });
271
- }
272
- };
273
- const { session } = await agentManager.getSessionContext(chatId, telegram);
274
338
  const welcomePrompt = [
275
339
  "System event: Arisa has just started.",
276
340
  `chatId: ${chatId}`,
@@ -282,21 +346,36 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
282
346
  "Use the user's Telegram language when possible.",
283
347
  "Do not mention internal implementation details."
284
348
  ].filter(Boolean).join("\n");
285
- const text = await collectText(session, welcomePrompt);
286
- if (text) {
287
- await sendTextReply({
288
- sendText: (message, extra) => bot.api.sendMessage(chatId, message, extra),
289
- sendDocument: (file, extra) => bot.api.sendDocument(chatId, file, extra),
290
- chatId,
291
- text
292
- });
293
- }
349
+ await enqueuePrompt({ chatId, prompt: welcomePrompt, label: "startup message" });
294
350
  } catch (error) {
295
351
  logger?.log("telegram", `startup message failed for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`);
296
352
  }
297
353
  }
354
+ await bot.api.setMyCommands([
355
+ { command: "new", description: "Start a new chat context" }
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();
298
377
  logger?.log("telegram", "bot polling started");
299
- await bot.start();
378
+ await bot.start({ drop_pending_updates: true });
300
379
  }
301
380
  };
302
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
  });