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
|
@@ -1,11 +1,39 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { tasksFile } from "../../runtime/paths.js";
|
|
5
5
|
|
|
6
|
+
const DEFAULT_RETRY = Object.freeze({
|
|
7
|
+
maxAttempts: 3,
|
|
8
|
+
baseDelaySeconds: 30,
|
|
9
|
+
maxDelaySeconds: 900,
|
|
10
|
+
multiplier: 2
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const TERMINAL_STATUSES = new Set(["done", "failed", "outcome_uncertain"]);
|
|
14
|
+
const TERMINAL_PAYLOAD_KEYS = ["chatId", "toolName", "resourceId", "artifactId"];
|
|
15
|
+
|
|
16
|
+
const taskFileOperations = new Map();
|
|
17
|
+
|
|
18
|
+
async function serializeTaskFileOperation(operation) {
|
|
19
|
+
const previous = taskFileOperations.get(tasksFile) || Promise.resolve();
|
|
20
|
+
const current = previous.catch(() => {}).then(operation);
|
|
21
|
+
taskFileOperations.set(tasksFile, current);
|
|
22
|
+
try {
|
|
23
|
+
return await current;
|
|
24
|
+
} finally {
|
|
25
|
+
if (taskFileOperations.get(tasksFile) === current) taskFileOperations.delete(tasksFile);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function waitForTaskFileOperations() {
|
|
30
|
+
await (taskFileOperations.get(tasksFile) || Promise.resolve()).catch(() => {});
|
|
31
|
+
}
|
|
32
|
+
|
|
6
33
|
async function loadTasksFile() {
|
|
7
34
|
try {
|
|
8
|
-
|
|
35
|
+
const parsed = JSON.parse(await readFile(tasksFile, "utf8"));
|
|
36
|
+
return Array.isArray(parsed) ? parsed.map(migrateTask) : [];
|
|
9
37
|
} catch {
|
|
10
38
|
return [];
|
|
11
39
|
}
|
|
@@ -13,173 +41,435 @@ async function loadTasksFile() {
|
|
|
13
41
|
|
|
14
42
|
async function saveTasksFile(tasks) {
|
|
15
43
|
await mkdir(path.dirname(tasksFile), { recursive: true });
|
|
16
|
-
|
|
44
|
+
const temporaryFile = `${tasksFile}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
45
|
+
try {
|
|
46
|
+
await writeFile(temporaryFile, `${JSON.stringify(tasks, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
47
|
+
await rename(temporaryFile, tasksFile);
|
|
48
|
+
} finally {
|
|
49
|
+
await rm(temporaryFile, { force: true }).catch(() => {});
|
|
50
|
+
}
|
|
17
51
|
}
|
|
18
52
|
|
|
19
53
|
function taskId() {
|
|
20
54
|
return crypto.randomUUID();
|
|
21
55
|
}
|
|
22
56
|
|
|
23
|
-
function
|
|
57
|
+
function boundedPositiveInteger(value, fallback, maximum) {
|
|
58
|
+
const parsed = Number(value);
|
|
59
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? Math.min(parsed, maximum) : fallback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function boundedPositiveNumber(value, fallback, maximum) {
|
|
63
|
+
const parsed = Number(value);
|
|
64
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, maximum) : fallback;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeRetry(retry = {}) {
|
|
68
|
+
const value = retry && typeof retry === "object" && !Array.isArray(retry) ? retry : {};
|
|
24
69
|
return {
|
|
70
|
+
maxAttempts: boundedPositiveInteger(value.maxAttempts, DEFAULT_RETRY.maxAttempts, 10),
|
|
71
|
+
baseDelaySeconds: boundedPositiveNumber(value.baseDelaySeconds, DEFAULT_RETRY.baseDelaySeconds, 3_600),
|
|
72
|
+
maxDelaySeconds: boundedPositiveNumber(value.maxDelaySeconds, DEFAULT_RETRY.maxDelaySeconds, 86_400),
|
|
73
|
+
multiplier: boundedPositiveNumber(value.multiplier, DEFAULT_RETRY.multiplier, 10)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function legacyTelegramRoute(telegramContext) {
|
|
78
|
+
if (!telegramContext?.transportChatId) return null;
|
|
79
|
+
return {
|
|
80
|
+
transport: "telegram",
|
|
81
|
+
destination: {
|
|
82
|
+
chatId: telegramContext.transportChatId,
|
|
83
|
+
...(telegramContext.messageThreadId ? { threadId: telegramContext.messageThreadId } : {})
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function migrateTask(task = {}) {
|
|
89
|
+
const payload = { ...(task.payload || {}) };
|
|
90
|
+
const route = task.route || legacyTelegramRoute(payload.telegramContext) || null;
|
|
91
|
+
delete payload.telegramContext;
|
|
92
|
+
const migrated = {
|
|
93
|
+
...task,
|
|
94
|
+
payload,
|
|
95
|
+
route,
|
|
96
|
+
attempts: Number.isSafeInteger(task.attempts) && task.attempts >= 0 ? task.attempts : 0
|
|
97
|
+
};
|
|
98
|
+
if (!TERMINAL_STATUSES.has(migrated.status)) migrated.retry = normalizeRetry(task.retry);
|
|
99
|
+
return migrated;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function compactTerminalPayload(payload = {}) {
|
|
103
|
+
return Object.fromEntries(TERMINAL_PAYLOAD_KEYS.flatMap((key) => {
|
|
104
|
+
const value = payload[key];
|
|
105
|
+
return ["string", "number", "boolean"].includes(typeof value) ? [[key, value]] : [];
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function compactTerminalTask(task) {
|
|
110
|
+
if (!TERMINAL_STATUSES.has(task.status)) return false;
|
|
111
|
+
const payload = compactTerminalPayload(task.payload);
|
|
112
|
+
const changed = !task.payloadCompacted
|
|
113
|
+
|| JSON.stringify(task.payload || {}) !== JSON.stringify(payload)
|
|
114
|
+
|| Object.hasOwn(task, "retry")
|
|
115
|
+
|| Object.hasOwn(task, "recurrence");
|
|
116
|
+
task.payload = payload;
|
|
117
|
+
task.payloadCompacted = true;
|
|
118
|
+
delete task.retry;
|
|
119
|
+
delete task.recurrence;
|
|
120
|
+
return changed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeTask(task, defaults = {}) {
|
|
124
|
+
const mergedPayload = {
|
|
125
|
+
...(defaults.payload || {}),
|
|
126
|
+
...(task.payload || {})
|
|
127
|
+
};
|
|
128
|
+
const route = task.route
|
|
129
|
+
|| defaults.route
|
|
130
|
+
|| legacyTelegramRoute(mergedPayload.telegramContext)
|
|
131
|
+
|| null;
|
|
132
|
+
delete mergedPayload.telegramContext;
|
|
133
|
+
const normalized = migrateTask({
|
|
25
134
|
id: task.id || taskId(),
|
|
26
135
|
status: task.status || "pending",
|
|
27
136
|
createdAt: task.createdAt || new Date().toISOString(),
|
|
28
137
|
updatedAt: new Date().toISOString(),
|
|
29
138
|
kind: task.kind,
|
|
30
139
|
runAt: task.runAt || new Date().toISOString(),
|
|
31
|
-
payload:
|
|
32
|
-
|
|
33
|
-
...(task.payload || {})
|
|
34
|
-
},
|
|
140
|
+
payload: mergedPayload,
|
|
141
|
+
route,
|
|
35
142
|
recurrence: task.recurrence || defaults.recurrence || null,
|
|
36
143
|
source: {
|
|
37
144
|
...(defaults.source || {}),
|
|
38
145
|
...(task.source || {})
|
|
39
|
-
}
|
|
40
|
-
|
|
146
|
+
},
|
|
147
|
+
...(task.startedAt ? { startedAt: task.startedAt } : {}),
|
|
148
|
+
...(task.claimedAt ? { claimedAt: task.claimedAt } : {}),
|
|
149
|
+
...(task.executionStartedAt ? { executionStartedAt: task.executionStartedAt } : {}),
|
|
150
|
+
attempts: task.attempts || 0,
|
|
151
|
+
retry: task.retry || defaults.retry
|
|
152
|
+
});
|
|
153
|
+
compactTerminalTask(normalized);
|
|
154
|
+
return normalized;
|
|
41
155
|
}
|
|
42
156
|
|
|
43
|
-
function computeNextRunAt(task) {
|
|
157
|
+
function computeNextRunAt(task, now = Date.now()) {
|
|
44
158
|
if (task.recurrence?.type === "interval" && Number(task.recurrence.everySeconds) > 0) {
|
|
45
|
-
return new Date(
|
|
159
|
+
return new Date(now + (Number(task.recurrence.everySeconds) * 1000)).toISOString();
|
|
46
160
|
}
|
|
47
161
|
return "";
|
|
48
162
|
}
|
|
49
163
|
|
|
164
|
+
function retryDelayMs(task) {
|
|
165
|
+
const exponent = Math.max(0, Number(task.attempts || 1) - 1);
|
|
166
|
+
const seconds = Math.min(
|
|
167
|
+
task.retry.maxDelaySeconds,
|
|
168
|
+
task.retry.baseDelaySeconds * (task.retry.multiplier ** exponent)
|
|
169
|
+
);
|
|
170
|
+
return Math.max(1, Math.round(seconds * 1000));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function failTask(task, error) {
|
|
174
|
+
const failedAt = new Date().toISOString();
|
|
175
|
+
task.status = "failed";
|
|
176
|
+
task.error = error instanceof Error ? error.message : String(error);
|
|
177
|
+
task.lastError = task.error;
|
|
178
|
+
task.failedAt = failedAt;
|
|
179
|
+
task.updatedAt = failedAt;
|
|
180
|
+
compactTerminalTask(task);
|
|
181
|
+
return structuredClone(task);
|
|
182
|
+
}
|
|
183
|
+
|
|
50
184
|
export class TaskStore {
|
|
51
185
|
constructor() {
|
|
52
186
|
this.tasks = null;
|
|
53
187
|
}
|
|
54
188
|
|
|
55
189
|
async init() {
|
|
56
|
-
if (!this.tasks) this.
|
|
190
|
+
if (!this.tasks) await this.reload();
|
|
57
191
|
}
|
|
58
192
|
|
|
59
193
|
async reload() {
|
|
194
|
+
await waitForTaskFileOperations();
|
|
60
195
|
this.tasks = await loadTasksFile();
|
|
61
196
|
}
|
|
62
197
|
|
|
198
|
+
async mutate(operation) {
|
|
199
|
+
return serializeTaskFileOperation(async () => {
|
|
200
|
+
this.tasks = await loadTasksFile();
|
|
201
|
+
const { result, changed = true } = await operation(this.tasks);
|
|
202
|
+
if (changed) await saveTasksFile(this.tasks);
|
|
203
|
+
return result;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
63
207
|
async save() {
|
|
64
|
-
|
|
208
|
+
const tasks = structuredClone(this.tasks || []);
|
|
209
|
+
return serializeTaskFileOperation(async () => {
|
|
210
|
+
this.tasks = tasks;
|
|
211
|
+
await saveTasksFile(tasks);
|
|
212
|
+
});
|
|
65
213
|
}
|
|
66
214
|
|
|
67
215
|
async add(task, defaults = {}) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
216
|
+
return this.mutate(async (tasks) => {
|
|
217
|
+
const normalized = normalizeTask(task, defaults);
|
|
218
|
+
tasks.push(normalized);
|
|
219
|
+
return { result: structuredClone(normalized) };
|
|
220
|
+
});
|
|
73
221
|
}
|
|
74
222
|
|
|
75
|
-
async addMany(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
223
|
+
async addMany(tasksToAdd = [], defaults = {}) {
|
|
224
|
+
return this.mutate(async (tasks) => {
|
|
225
|
+
const created = tasksToAdd.map((task) => normalizeTask(task, defaults));
|
|
226
|
+
tasks.push(...created);
|
|
227
|
+
return { result: structuredClone(created), changed: created.length > 0 };
|
|
228
|
+
});
|
|
81
229
|
}
|
|
82
230
|
|
|
83
231
|
async claimDue(limit = 10) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
due.push({ ...task });
|
|
96
|
-
}
|
|
232
|
+
return this.mutate(async (tasks) => {
|
|
233
|
+
const now = Date.now();
|
|
234
|
+
const due = tasks
|
|
235
|
+
.filter((task) => task.status === "pending"
|
|
236
|
+
&& task.runAt
|
|
237
|
+
&& !Number.isNaN(Date.parse(task.runAt))
|
|
238
|
+
&& Date.parse(task.runAt) <= now)
|
|
239
|
+
.sort((left, right) => Date.parse(left.runAt) - Date.parse(right.runAt)
|
|
240
|
+
|| Date.parse(left.createdAt || 0) - Date.parse(right.createdAt || 0)
|
|
241
|
+
|| String(left.id).localeCompare(String(right.id)))
|
|
242
|
+
.slice(0, limit);
|
|
97
243
|
|
|
98
|
-
|
|
99
|
-
|
|
244
|
+
const claimedAt = new Date(now).toISOString();
|
|
245
|
+
for (const task of due) {
|
|
246
|
+
task.status = "running";
|
|
247
|
+
task.attempts += 1;
|
|
248
|
+
task.startedAt = claimedAt;
|
|
249
|
+
task.claimedAt = claimedAt;
|
|
250
|
+
delete task.executionStartedAt;
|
|
251
|
+
task.updatedAt = claimedAt;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { result: structuredClone(due), changed: due.length > 0 };
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async markExecutionStarted(taskId) {
|
|
259
|
+
return this.mutate(async (tasks) => {
|
|
260
|
+
const task = tasks.find((item) => item.id === taskId);
|
|
261
|
+
if (!task || task.status !== "running") return { result: null, changed: false };
|
|
262
|
+
task.executionStartedAt = new Date().toISOString();
|
|
263
|
+
task.updatedAt = task.executionStartedAt;
|
|
264
|
+
return { result: structuredClone(task) };
|
|
265
|
+
});
|
|
100
266
|
}
|
|
101
267
|
|
|
102
268
|
async recoverInterrupted() {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
269
|
+
return this.mutate(async (tasks) => {
|
|
270
|
+
const recovered = [];
|
|
271
|
+
let compacted = false;
|
|
272
|
+
const now = Date.now();
|
|
106
273
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
274
|
+
for (const task of tasks) {
|
|
275
|
+
if (task.status === "running") {
|
|
276
|
+
const interruptedAt = new Date(now).toISOString();
|
|
277
|
+
if (task.claimedAt && !task.executionStartedAt) {
|
|
278
|
+
task.status = "pending";
|
|
279
|
+
task.attempts = Math.max(0, Number(task.attempts || 0) - 1);
|
|
280
|
+
task.lastError = "execution interrupted before start";
|
|
281
|
+
task.lastFailedAt = interruptedAt;
|
|
282
|
+
task.updatedAt = interruptedAt;
|
|
283
|
+
task.lastClaimedAt = task.claimedAt;
|
|
284
|
+
delete task.startedAt;
|
|
285
|
+
delete task.claimedAt;
|
|
286
|
+
recovered.push(structuredClone(task));
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
task.lastError = "execution interrupted before confirmation";
|
|
290
|
+
task.lastFailedAt = interruptedAt;
|
|
291
|
+
task.updatedAt = interruptedAt;
|
|
292
|
+
if (task.claimedAt) task.lastClaimedAt = task.claimedAt;
|
|
293
|
+
if (task.executionStartedAt) task.lastExecutionStartedAt = task.executionStartedAt;
|
|
294
|
+
delete task.claimedAt;
|
|
295
|
+
delete task.executionStartedAt;
|
|
296
|
+
if (task.kind === "poll_tool") {
|
|
297
|
+
task.status = "pending";
|
|
298
|
+
task.runAt = new Date(now + retryDelayMs(task)).toISOString();
|
|
299
|
+
delete task.startedAt;
|
|
300
|
+
} else {
|
|
301
|
+
const nextRunAt = computeNextRunAt(task, now);
|
|
302
|
+
if (nextRunAt) {
|
|
303
|
+
task.status = "pending";
|
|
304
|
+
task.runAt = nextRunAt;
|
|
305
|
+
task.attempts = 0;
|
|
306
|
+
task.lastOutcome = "outcome_uncertain";
|
|
307
|
+
delete task.startedAt;
|
|
308
|
+
} else {
|
|
309
|
+
task.status = "outcome_uncertain";
|
|
310
|
+
task.error = task.lastError;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
compactTerminalTask(task);
|
|
314
|
+
recovered.push(structuredClone(task));
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (compactTerminalTask(task)) compacted = true;
|
|
318
|
+
}
|
|
113
319
|
|
|
114
|
-
|
|
115
|
-
|
|
320
|
+
return { result: recovered, changed: recovered.length > 0 || compacted };
|
|
321
|
+
});
|
|
116
322
|
}
|
|
117
323
|
|
|
118
324
|
async complete(taskId) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
325
|
+
return this.mutate(async (tasks) => {
|
|
326
|
+
const task = tasks.find((item) => item.id === taskId);
|
|
327
|
+
if (!task) return { result: null, changed: false };
|
|
328
|
+
|
|
329
|
+
const now = Date.now();
|
|
330
|
+
const completedAt = new Date(now).toISOString();
|
|
331
|
+
const nextRunAt = computeNextRunAt(task, now);
|
|
332
|
+
task.lastCompletedAt = completedAt;
|
|
333
|
+
task.lastRunAt = completedAt;
|
|
334
|
+
if (task.claimedAt) task.lastClaimedAt = task.claimedAt;
|
|
335
|
+
if (task.executionStartedAt) task.lastExecutionStartedAt = task.executionStartedAt;
|
|
336
|
+
delete task.lastError;
|
|
337
|
+
delete task.error;
|
|
338
|
+
delete task.lastOutcome;
|
|
339
|
+
delete task.consecutiveFailures;
|
|
340
|
+
delete task.claimedAt;
|
|
341
|
+
delete task.executionStartedAt;
|
|
342
|
+
if (nextRunAt) {
|
|
343
|
+
task.status = "pending";
|
|
344
|
+
task.runAt = nextRunAt;
|
|
345
|
+
task.attempts = 0;
|
|
346
|
+
delete task.startedAt;
|
|
347
|
+
} else {
|
|
348
|
+
task.status = "done";
|
|
349
|
+
task.completedAt = completedAt;
|
|
350
|
+
}
|
|
351
|
+
task.updatedAt = completedAt;
|
|
352
|
+
compactTerminalTask(task);
|
|
353
|
+
return { result: structuredClone(task) };
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async retryOrFail(taskId, error, { retryable = true, outcomeUncertain = false } = {}) {
|
|
358
|
+
return this.mutate(async (tasks) => {
|
|
359
|
+
const task = tasks.find((item) => item.id === taskId);
|
|
360
|
+
if (!task) return { result: null, changed: false };
|
|
361
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
362
|
+
if (outcomeUncertain) {
|
|
363
|
+
const now = Date.now();
|
|
364
|
+
const uncertainAt = new Date(now).toISOString();
|
|
365
|
+
const nextRunAt = computeNextRunAt(task, now);
|
|
366
|
+
task.lastError = message;
|
|
367
|
+
task.lastFailedAt = uncertainAt;
|
|
368
|
+
task.updatedAt = uncertainAt;
|
|
369
|
+
if (task.claimedAt) task.lastClaimedAt = task.claimedAt;
|
|
370
|
+
if (task.executionStartedAt) task.lastExecutionStartedAt = task.executionStartedAt;
|
|
371
|
+
delete task.claimedAt;
|
|
372
|
+
delete task.executionStartedAt;
|
|
373
|
+
if (nextRunAt) {
|
|
374
|
+
task.status = "pending";
|
|
375
|
+
task.runAt = nextRunAt;
|
|
376
|
+
task.attempts = 0;
|
|
377
|
+
task.lastOutcome = "outcome_uncertain";
|
|
378
|
+
delete task.startedAt;
|
|
379
|
+
delete task.error;
|
|
380
|
+
return { result: { ...structuredClone(task), terminalFailure: true } };
|
|
381
|
+
}
|
|
382
|
+
task.status = "outcome_uncertain";
|
|
383
|
+
task.error = message;
|
|
384
|
+
task.uncertainAt = uncertainAt;
|
|
385
|
+
compactTerminalTask(task);
|
|
386
|
+
return { result: structuredClone(task) };
|
|
387
|
+
}
|
|
388
|
+
if (!retryable) return { result: failTask(task, message) };
|
|
389
|
+
if (task.attempts >= task.retry.maxAttempts) {
|
|
390
|
+
const now = Date.now();
|
|
391
|
+
const nextRunAt = computeNextRunAt(task, now);
|
|
392
|
+
if (!nextRunAt) return { result: failTask(task, message) };
|
|
393
|
+
|
|
394
|
+
const failedAt = new Date(now).toISOString();
|
|
395
|
+
task.status = "pending";
|
|
396
|
+
task.runAt = nextRunAt;
|
|
397
|
+
task.attempts = 0;
|
|
398
|
+
task.lastOutcome = "failed";
|
|
399
|
+
task.lastError = message;
|
|
400
|
+
task.lastFailedAt = failedAt;
|
|
401
|
+
task.consecutiveFailures = Number(task.consecutiveFailures || 0) + 1;
|
|
402
|
+
task.updatedAt = failedAt;
|
|
403
|
+
if (task.claimedAt) task.lastClaimedAt = task.claimedAt;
|
|
404
|
+
if (task.executionStartedAt) task.lastExecutionStartedAt = task.executionStartedAt;
|
|
405
|
+
delete task.startedAt;
|
|
406
|
+
delete task.claimedAt;
|
|
407
|
+
delete task.executionStartedAt;
|
|
408
|
+
delete task.error;
|
|
409
|
+
return { result: { ...structuredClone(task), terminalFailure: true } };
|
|
410
|
+
}
|
|
122
411
|
|
|
123
|
-
|
|
124
|
-
if (nextRunAt) {
|
|
412
|
+
const now = Date.now();
|
|
125
413
|
task.status = "pending";
|
|
126
|
-
task.runAt =
|
|
127
|
-
task.
|
|
128
|
-
|
|
129
|
-
task.
|
|
130
|
-
task.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
414
|
+
task.runAt = new Date(now + retryDelayMs(task)).toISOString();
|
|
415
|
+
task.lastError = message;
|
|
416
|
+
task.lastFailedAt = new Date(now).toISOString();
|
|
417
|
+
task.updatedAt = task.lastFailedAt;
|
|
418
|
+
if (task.claimedAt) task.lastClaimedAt = task.claimedAt;
|
|
419
|
+
if (task.executionStartedAt) task.lastExecutionStartedAt = task.executionStartedAt;
|
|
420
|
+
delete task.startedAt;
|
|
421
|
+
delete task.claimedAt;
|
|
422
|
+
delete task.executionStartedAt;
|
|
423
|
+
return { result: structuredClone(task) };
|
|
424
|
+
});
|
|
135
425
|
}
|
|
136
426
|
|
|
137
427
|
async fail(taskId, error) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await this.save();
|
|
145
|
-
return task;
|
|
428
|
+
return this.mutate(async (tasks) => {
|
|
429
|
+
const task = tasks.find((item) => item.id === taskId);
|
|
430
|
+
return task
|
|
431
|
+
? { result: failTask(task, error) }
|
|
432
|
+
: { result: null, changed: false };
|
|
433
|
+
});
|
|
146
434
|
}
|
|
147
435
|
|
|
148
436
|
async list(filter = {}) {
|
|
149
437
|
await this.reload();
|
|
150
438
|
return this.tasks.filter((task) => {
|
|
151
|
-
if (filter.chatId && task.payload?.chatId !== filter.chatId) return false;
|
|
439
|
+
if (filter.chatId && String(task.payload?.chatId) !== String(filter.chatId)) return false;
|
|
152
440
|
if (filter.status && task.status !== filter.status) return false;
|
|
153
441
|
if (filter.kind && task.kind !== filter.kind) return false;
|
|
154
442
|
return true;
|
|
155
|
-
});
|
|
443
|
+
}).map((task) => structuredClone(task));
|
|
156
444
|
}
|
|
157
445
|
|
|
158
446
|
async get(taskId) {
|
|
159
447
|
await this.reload();
|
|
160
|
-
|
|
448
|
+
const task = this.tasks.find((item) => item.id === taskId);
|
|
449
|
+
return task ? structuredClone(task) : null;
|
|
161
450
|
}
|
|
162
451
|
|
|
163
452
|
async cancel(taskId) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
453
|
+
return this.mutate(async (tasks) => {
|
|
454
|
+
const index = tasks.findIndex((item) => item.id === taskId);
|
|
455
|
+
if (index === -1) return { result: null, changed: false };
|
|
456
|
+
const [task] = tasks.splice(index, 1);
|
|
457
|
+
return { result: structuredClone(task) };
|
|
458
|
+
});
|
|
170
459
|
}
|
|
171
460
|
|
|
172
461
|
async cancelAll(filter = {}) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
462
|
+
return this.mutate(async (tasks) => {
|
|
463
|
+
const removed = [];
|
|
464
|
+
const remaining = tasks.filter((task) => {
|
|
465
|
+
if (filter.chatId && String(task.payload?.chatId) !== String(filter.chatId)) return true;
|
|
466
|
+
if (filter.status && task.status !== filter.status) return true;
|
|
467
|
+
if (["done", "failed", "outcome_uncertain"].includes(task.status)) return true;
|
|
468
|
+
removed.push(structuredClone(task));
|
|
469
|
+
return false;
|
|
470
|
+
});
|
|
471
|
+
tasks.splice(0, tasks.length, ...remaining);
|
|
472
|
+
return { result: removed, changed: removed.length > 0 };
|
|
181
473
|
});
|
|
182
|
-
if (removed.length) await this.save();
|
|
183
|
-
return removed;
|
|
184
474
|
}
|
|
185
475
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { taskWithoutCallerRouting } from "../tasks/task-routing.js";
|
|
3
4
|
|
|
4
5
|
export async function materializeToolOutput({ result, name, chatId, artifactStore, taskStore, taskContext = null }) {
|
|
5
6
|
const chatArtifactStore = artifactStore.forChat(chatId);
|
|
@@ -27,11 +28,10 @@ export async function materializeToolOutput({ result, name, chatId, artifactStor
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
if (result.asyncTask || result.asyncTasks?.length) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
},
|
|
31
|
+
const requestedTasks = (result.asyncTasks || [result.asyncTask]).map(taskWithoutCallerRouting);
|
|
32
|
+
result.asyncTasks = await taskStore.addMany(requestedTasks, {
|
|
33
|
+
payload: { chatId },
|
|
34
|
+
...(taskContext ? { route: taskContext } : {}),
|
|
35
35
|
source: { type: "tool", toolName: name, chatId }
|
|
36
36
|
});
|
|
37
37
|
delete result.asyncTask;
|
|
@@ -11,6 +11,7 @@ import { daemonConfigDefaults } from "../config/config-defaults.js";
|
|
|
11
11
|
import { SkillRegistry } from "../skills/skill-registry.js";
|
|
12
12
|
import { ToolUsageStore } from "./tool-usage-store.js";
|
|
13
13
|
import { inspectToolDependencies, normalizeToolDependencies } from "./tool-dependencies.js";
|
|
14
|
+
import { normalizeToolExecution, WeightedResourceGovernor } from "./weighted-resource-governor.js";
|
|
14
15
|
|
|
15
16
|
function toolEnv() {
|
|
16
17
|
return { ...process.env, ARISA_PACKAGE_DIR: arisaPackageDir, ARISA_IPC_SOCKET: arisaIpcSocketFile };
|
|
@@ -272,7 +273,9 @@ export class ToolRegistry {
|
|
|
272
273
|
resolveOfficialToolNames = readOfficialToolNames,
|
|
273
274
|
helpTimeoutMs = defaultToolHelpTimeoutMs,
|
|
274
275
|
runTimeoutMs = defaultToolRunTimeoutMs,
|
|
275
|
-
killGraceMs = defaultToolKillGraceMs
|
|
276
|
+
killGraceMs = defaultToolKillGraceMs,
|
|
277
|
+
executionPolicy,
|
|
278
|
+
executionGovernor
|
|
276
279
|
} = {}) {
|
|
277
280
|
this.logger = logger;
|
|
278
281
|
this.helpTimeoutMs = positiveDuration(helpTimeoutMs, defaultToolHelpTimeoutMs);
|
|
@@ -282,6 +285,10 @@ export class ToolRegistry {
|
|
|
282
285
|
this.skillRegistry = new SkillRegistry();
|
|
283
286
|
this.usageStore = usageStore;
|
|
284
287
|
this.resolveOfficialToolNames = resolveOfficialToolNames;
|
|
288
|
+
this.executionGovernor = executionGovernor || new WeightedResourceGovernor({
|
|
289
|
+
policy: executionPolicy,
|
|
290
|
+
logger
|
|
291
|
+
});
|
|
285
292
|
}
|
|
286
293
|
|
|
287
294
|
async buildSnapshot() {
|
|
@@ -308,6 +315,7 @@ export class ToolRegistry {
|
|
|
308
315
|
snapshot.set(manifest.name, {
|
|
309
316
|
...manifest,
|
|
310
317
|
toolDependencies: normalizeToolDependencies(manifest.toolDependencies),
|
|
318
|
+
execution: normalizeToolExecution(manifest.execution),
|
|
311
319
|
category: normalizeCategory(manifest.category),
|
|
312
320
|
keywords: normalizeKeywords(manifest.keywords),
|
|
313
321
|
skillHints,
|
|
@@ -450,6 +458,10 @@ export class ToolRegistry {
|
|
|
450
458
|
return inspectToolDependencies(this.tools, name);
|
|
451
459
|
}
|
|
452
460
|
|
|
461
|
+
executionDiagnostic() {
|
|
462
|
+
return this.executionGovernor.snapshot();
|
|
463
|
+
}
|
|
464
|
+
|
|
453
465
|
async usage(chatId) {
|
|
454
466
|
const [counts, officialNames] = await Promise.all([
|
|
455
467
|
this.usageStore.counts(chatId),
|
|
@@ -475,14 +487,16 @@ export class ToolRegistry {
|
|
|
475
487
|
await this.usageStore.record(chatId, name).catch((error) => {
|
|
476
488
|
this.logger?.error("tools", `could not record ${name} usage: ${error?.message || String(error)}`);
|
|
477
489
|
});
|
|
478
|
-
this.logger?.log("tools", `running ${name}`);
|
|
479
490
|
const tmpDir = chatId != null ? getChatToolTmpDir(chatId, name) : getToolTmpDir(name);
|
|
480
|
-
await mkdir(tmpDir, { recursive: true });
|
|
481
491
|
const requestFile = path.join(tmpDir, `.request-${Date.now()}-${randomUUID()}.json`);
|
|
482
|
-
|
|
483
|
-
const enrichedRequest = { ...request, chatId, skills };
|
|
492
|
+
let lease = null;
|
|
484
493
|
let result;
|
|
485
494
|
try {
|
|
495
|
+
lease = await this.executionGovernor.acquire(tool.execution, name);
|
|
496
|
+
this.logger?.log("tools", `running ${name}`);
|
|
497
|
+
await mkdir(tmpDir, { recursive: true });
|
|
498
|
+
const skills = await this.resolveSkills(name);
|
|
499
|
+
const enrichedRequest = { ...request, chatId, skills };
|
|
486
500
|
if (tool.daemon?.protocol === "arisa-daemon-v1") {
|
|
487
501
|
const scope = tool.daemon.scope === "chat"
|
|
488
502
|
? { type: "chat", chatId }
|
|
@@ -538,6 +552,7 @@ export class ToolRegistry {
|
|
|
538
552
|
error: error?.message || `Invalid tool response for ${name}`
|
|
539
553
|
});
|
|
540
554
|
} finally {
|
|
555
|
+
lease?.release();
|
|
541
556
|
await unlink(requestFile).catch(() => {});
|
|
542
557
|
await rmdir(tmpDir).catch(() => {});
|
|
543
558
|
if (chatId != null) {
|