@goodfish/jarvy 0.1.0 → 0.3.0
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/CHANGELOG.md +65 -0
- package/README.md +2 -2
- package/README.zh-CN.md +3 -1
- package/dist/server/cli.js +541 -260
- package/dist/web/assets/figure-DBBkOPNl.js +1 -0
- package/dist/web/assets/index-C1Aq7FIn.js +505 -0
- package/dist/web/assets/index-CDOMm3pH.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -2
- package/dist/web/assets/figure-BOkXKeYO.js +0 -1
- package/dist/web/assets/index-B5lY7pJH.css +0 -1
- package/dist/web/assets/index-l6bxxXC5.js +0 -406
package/dist/server/cli.js
CHANGED
|
@@ -72,28 +72,41 @@ import { HTTPException as HTTPException2 } from "hono/http-exception";
|
|
|
72
72
|
|
|
73
73
|
// src/server/shared/runtime.ts
|
|
74
74
|
var RuntimeFault = class extends Error {
|
|
75
|
-
constructor(code, message, status = 409) {
|
|
75
|
+
constructor(code, message, status = 409, params) {
|
|
76
76
|
super(message);
|
|
77
77
|
this.code = code;
|
|
78
78
|
this.status = status;
|
|
79
|
+
this.params = params;
|
|
79
80
|
}
|
|
80
81
|
};
|
|
81
82
|
function publicFault(error) {
|
|
82
|
-
return error instanceof RuntimeFault ? error : new RuntimeFault("runtime_failed", "
|
|
83
|
+
return error instanceof RuntimeFault ? error : new RuntimeFault("runtime_failed", "The run failed. Check the model setup or the run records.", 500);
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
// src/server/shared/chat.ts
|
|
86
87
|
var ChatError = class extends Error {
|
|
87
|
-
constructor(status, message) {
|
|
88
|
+
constructor(status, message, code, params) {
|
|
88
89
|
super(message);
|
|
89
90
|
this.status = status;
|
|
91
|
+
this.code = code;
|
|
92
|
+
this.params = params;
|
|
90
93
|
}
|
|
91
94
|
};
|
|
92
95
|
|
|
93
96
|
// src/server/shared/errors.ts
|
|
94
97
|
var ValidationError = class extends Error {
|
|
98
|
+
constructor(message, code, params) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.code = code;
|
|
101
|
+
this.params = params;
|
|
102
|
+
}
|
|
95
103
|
};
|
|
96
104
|
var ConflictError = class extends Error {
|
|
105
|
+
constructor(message, code, params) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.code = code;
|
|
108
|
+
this.params = params;
|
|
109
|
+
}
|
|
97
110
|
};
|
|
98
111
|
|
|
99
112
|
// src/server/api/middleware/errors.ts
|
|
@@ -103,7 +116,9 @@ var handleError = (error, c) => {
|
|
|
103
116
|
else if (error instanceof ValidationError || error instanceof TypeError && "code" in error && error.code === "ERR_INVALID_URL") status = 400;
|
|
104
117
|
else if (error instanceof ConflictError) status = 409;
|
|
105
118
|
if (status === 500) console.error("[api] request failed", { path: c.req.path, code: error instanceof RuntimeFault ? error.code : "internal" });
|
|
106
|
-
return c.json({ error:
|
|
119
|
+
if (status === 500) return c.json({ error: "Internal server error" }, 500);
|
|
120
|
+
const coded = error instanceof RuntimeFault || error instanceof ChatError || error instanceof ValidationError || error instanceof ConflictError ? error : null;
|
|
121
|
+
return c.json({ error: error.message, ...coded?.code ? { code: coded.code } : {}, ...coded?.params ? { params: coded.params } : {} }, status);
|
|
107
122
|
};
|
|
108
123
|
|
|
109
124
|
// src/server/api/routes/account.ts
|
|
@@ -138,9 +153,9 @@ function textField(input, key, fallback) {
|
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
// src/server/api/routes/account.ts
|
|
141
|
-
function sessionCookie(c,
|
|
156
|
+
function sessionCookie(c, name2, value2, maxAge) {
|
|
142
157
|
const secure = !!c.env.incoming.socket.encrypted;
|
|
143
|
-
setCookie(c,
|
|
158
|
+
setCookie(c, name2, value2, { httpOnly: true, sameSite: "Strict", path: "/", maxAge, secure });
|
|
144
159
|
}
|
|
145
160
|
function installAuthentication(api, account) {
|
|
146
161
|
const COOKIE = `jarvy_session_${account.instance}`;
|
|
@@ -181,6 +196,7 @@ function botStatusView(status) {
|
|
|
181
196
|
runId: status.runId,
|
|
182
197
|
errorCode: status.error?.code ?? null,
|
|
183
198
|
error: status.error?.message ?? null,
|
|
199
|
+
...status.error?.params ? { errorParams: status.error.params } : {},
|
|
184
200
|
...status.context ? { context: { tokens: status.context.tokens, contextWindow: status.context.limit, percent: status.context.tokens === null ? null : status.context.tokens / status.context.limit * 100 } } : {}
|
|
185
201
|
};
|
|
186
202
|
}
|
|
@@ -189,6 +205,43 @@ function botStatusView(status) {
|
|
|
189
205
|
import { Hono as Hono2 } from "hono";
|
|
190
206
|
import { HTTPException as HTTPException5 } from "hono/http-exception";
|
|
191
207
|
|
|
208
|
+
// src/server/shared/quotes.ts
|
|
209
|
+
var MARK = /\[\[q(\d+)\]\]/g;
|
|
210
|
+
function quoteMarks(content2) {
|
|
211
|
+
return [...content2.matchAll(MARK)].map((match) => Number(match[1]));
|
|
212
|
+
}
|
|
213
|
+
function clipQuote(text3) {
|
|
214
|
+
const chars = [...text3];
|
|
215
|
+
return chars.length > 200 ? `${chars.slice(0, 120).join("")}\u2026\u2026${chars.slice(-60).join("")}` : text3;
|
|
216
|
+
}
|
|
217
|
+
function previewQuotes(content2, quotes) {
|
|
218
|
+
if (!quotes.length) return content2;
|
|
219
|
+
return content2.replace(MARK, (mark, n) => {
|
|
220
|
+
const quote = quotes[Number(n) - 1];
|
|
221
|
+
return quote ? `"${clipQuote(quote.text).replace(/\s+/g, " ").trim()}"` : mark;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
function clipMarked(content2, length) {
|
|
225
|
+
return content2.length <= length ? content2 : content2.slice(0, length).replace(/\[(?:\[(?:q\d*\]?)?)?$/, "");
|
|
226
|
+
}
|
|
227
|
+
function replyExcerpt(reply) {
|
|
228
|
+
return reply.content ? previewQuotes(reply.content, reply.quotes).slice(0, 200) : "(attachment)";
|
|
229
|
+
}
|
|
230
|
+
function expandQuotes(content2, quotes, name2) {
|
|
231
|
+
if (!quotes.length) return content2;
|
|
232
|
+
return content2.replace(/[ \t]*\[\[q(\d+)\]\][ \t]*/g, (mark, n, offset, whole) => {
|
|
233
|
+
const quote = quotes[Number(n) - 1];
|
|
234
|
+
if (!quote) return mark;
|
|
235
|
+
const head = `${name2(quote.senderId)}${quote.messageId ? ` (${quote.messageId})` : ""}: `;
|
|
236
|
+
const block = `${head}${clipQuote(quote.text).trim()}`.split("\n").map((line) => `> ${line}`).join("\n");
|
|
237
|
+
const end = offset + mark.length;
|
|
238
|
+
return `${offset > 0 && whole[offset - 1] !== "\n" ? "\n" : ""}${block}${end < whole.length && whole[end] !== "\n" ? "\n" : ""}`;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function quoteComparable(text3) {
|
|
242
|
+
return text3.replace(MARK, "").replace(/!\[[^\]]*\]\([^)]*\)/g, "").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/<(https?:\/\/[^>\s]+)>/g, "$1").replace(/^[ \t|:*_=-]*$/gm, (line) => /[-*_=]{3}/.test(line) ? "" : line).replace(/^[ \t]*(?:>[ \t>]*|#{1,6}[ \t]+|(?:[-*+]|\d+[.)])[ \t]+|```.*|~~~.*)/gm, "").replace(/\\([\\`*_{}[\]()#+\-.!|~>])/g, "$1").replace(/[*_~`|]/g, "").replace(/\s+/g, "");
|
|
243
|
+
}
|
|
244
|
+
|
|
192
245
|
// src/server/api/views/chat.ts
|
|
193
246
|
function chatView(chat, member) {
|
|
194
247
|
return {
|
|
@@ -224,7 +277,7 @@ function threadView(entry, member) {
|
|
|
224
277
|
...chatView(entry.chat, member),
|
|
225
278
|
replies: entry.info.replies,
|
|
226
279
|
lastReplyAt: entry.info.lastReplyAt,
|
|
227
|
-
root: entry.root ? { senderId: entry.root.senderId, content: entry.root.content
|
|
280
|
+
root: entry.root ? { senderId: entry.root.senderId, content: clipMarked(entry.root.content, 300), quotes: entry.root.quotes } : null
|
|
228
281
|
};
|
|
229
282
|
}
|
|
230
283
|
function messageView(message) {
|
|
@@ -243,7 +296,8 @@ function messageView(message) {
|
|
|
243
296
|
thread: message.thread,
|
|
244
297
|
echoOf: message.echoOf,
|
|
245
298
|
replyTo: message.replyTo,
|
|
246
|
-
reactions: message.reactions
|
|
299
|
+
reactions: message.reactions,
|
|
300
|
+
quotes: message.quotes
|
|
247
301
|
};
|
|
248
302
|
}
|
|
249
303
|
|
|
@@ -254,19 +308,19 @@ function cursor(raw) {
|
|
|
254
308
|
try {
|
|
255
309
|
value2 = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
256
310
|
} catch {
|
|
257
|
-
throw new HTTPException5(400, { message: "
|
|
311
|
+
throw new HTTPException5(400, { message: "Invalid page position" });
|
|
258
312
|
}
|
|
259
313
|
if (value2 && typeof value2 === "object" && "timestamp" in value2 && Number.isSafeInteger(value2.timestamp) && Number(value2.timestamp) >= 0 && "kind" in value2 && "id" in value2) {
|
|
260
314
|
if (value2.kind === "chat" && typeof value2.id === "string" && value2.id.length) return value2;
|
|
261
315
|
if (value2.kind === "history" && Number.isSafeInteger(value2.id) && Number(value2.id) > 0) return value2;
|
|
262
316
|
}
|
|
263
|
-
throw new HTTPException5(400, { message: "
|
|
317
|
+
throw new HTTPException5(400, { message: "Invalid page position" });
|
|
264
318
|
}
|
|
265
319
|
function activityRoutes(bots) {
|
|
266
320
|
const routes = new Hono2();
|
|
267
321
|
routes.get("/:id/activity", (c) => {
|
|
268
322
|
const limit = Number(c.req.query("limit") ?? 40);
|
|
269
|
-
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new HTTPException5(400, { message: "
|
|
323
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new HTTPException5(400, { message: "Invalid page size" });
|
|
270
324
|
const page = bots.activity(c.req.param("id"), cursor(c.req.query("before")), limit);
|
|
271
325
|
return c.json({ items: page.items, nextBefore: page.nextBefore ? Buffer.from(JSON.stringify(page.nextBefore)).toString("base64url") : null });
|
|
272
326
|
});
|
|
@@ -275,10 +329,10 @@ function activityRoutes(bots) {
|
|
|
275
329
|
return c.json({ chat: detail.chat, message: messageView(detail.message) });
|
|
276
330
|
});
|
|
277
331
|
routes.all("/:id/activity", () => {
|
|
278
|
-
throw new HTTPException5(405, { message: "
|
|
332
|
+
throw new HTTPException5(405, { message: "Activity is read-only" });
|
|
279
333
|
});
|
|
280
334
|
routes.all("/:id/activity/messages/:messageId", () => {
|
|
281
|
-
throw new HTTPException5(405, { message: "
|
|
335
|
+
throw new HTTPException5(405, { message: "Activity is read-only" });
|
|
282
336
|
});
|
|
283
337
|
return routes;
|
|
284
338
|
}
|
|
@@ -288,27 +342,33 @@ import { Hono as Hono3 } from "hono";
|
|
|
288
342
|
import { HTTPException as HTTPException6 } from "hono/http-exception";
|
|
289
343
|
function integer(raw, label) {
|
|
290
344
|
const n = Number(raw);
|
|
291
|
-
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(n) || n < 1) throw new HTTPException6(400, { message:
|
|
345
|
+
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(n) || n < 1) throw new HTTPException6(400, { message: `Invalid ${label}` });
|
|
292
346
|
return n;
|
|
293
347
|
}
|
|
294
348
|
function historyRoutes(bots) {
|
|
295
349
|
const routes = new Hono3();
|
|
296
350
|
routes.get("/:id/history", (c) => {
|
|
297
351
|
const memory = bots.memory(c.req.param("id"));
|
|
298
|
-
const before = c.req.query("before"), limit = integer(c.req.query("limit") ?? "50", "
|
|
299
|
-
if (limit > 100) throw new HTTPException6(400, { message: "
|
|
300
|
-
return c.json(memory.list(before === void 0 ? void 0 : integer(before, "
|
|
352
|
+
const before = c.req.query("before"), limit = integer(c.req.query("limit") ?? "50", "page size");
|
|
353
|
+
if (limit > 100) throw new HTTPException6(400, { message: "The page size can't be over 100" });
|
|
354
|
+
return c.json(memory.list(before === void 0 ? void 0 : integer(before, "page position"), limit));
|
|
301
355
|
});
|
|
302
356
|
routes.get("/:id/history/:recordId{[0-9]+}", (c) => {
|
|
303
|
-
const detail = bots.memory(c.req.param("id")).read(integer(c.req.param("recordId"), "
|
|
304
|
-
if (!detail) throw new
|
|
357
|
+
const detail = bots.memory(c.req.param("id")).read(integer(c.req.param("recordId"), "record ID"));
|
|
358
|
+
if (!detail) throw new RuntimeFault("history_missing", "History record not found", 404);
|
|
305
359
|
return c.json(detail);
|
|
306
360
|
});
|
|
361
|
+
routes.get("/:id/usage", (c) => {
|
|
362
|
+
const raw = c.req.query("since") ?? "";
|
|
363
|
+
const since = Number(raw);
|
|
364
|
+
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(since)) throw new HTTPException6(400, { message: "Invalid start time" });
|
|
365
|
+
return c.json({ since, models: bots.memory(c.req.param("id")).usage(since) });
|
|
366
|
+
});
|
|
307
367
|
routes.all("/:id/history", () => {
|
|
308
|
-
throw new HTTPException6(405, { message: "History
|
|
368
|
+
throw new HTTPException6(405, { message: "History is read-only" });
|
|
309
369
|
});
|
|
310
370
|
routes.all("/:id/history/:recordId{[0-9]+}", () => {
|
|
311
|
-
throw new HTTPException6(405, { message: "History
|
|
371
|
+
throw new HTTPException6(405, { message: "History is read-only" });
|
|
312
372
|
});
|
|
313
373
|
return routes;
|
|
314
374
|
}
|
|
@@ -319,7 +379,7 @@ function botRoutes(bots) {
|
|
|
319
379
|
routes.get("/", (c) => c.json({ bots: bots.list() }));
|
|
320
380
|
routes.post("/", jsonBody, async (c) => {
|
|
321
381
|
const input = await readJson(c);
|
|
322
|
-
return c.json(bots.create(
|
|
382
|
+
return c.json(bots.create(textField(input, "name")), 201);
|
|
323
383
|
});
|
|
324
384
|
routes.get("/:id", (c) => c.json(bots.describe(c.req.param("id"))));
|
|
325
385
|
routes.delete("/:id", async (c) => {
|
|
@@ -400,16 +460,16 @@ function chatRoutes({ chats, bots }) {
|
|
|
400
460
|
return c.json(messageView(message));
|
|
401
461
|
});
|
|
402
462
|
routes.post("/:id/attachments", attachmentBody, async (c) => {
|
|
403
|
-
let
|
|
463
|
+
let name2;
|
|
404
464
|
try {
|
|
405
|
-
|
|
465
|
+
name2 = decodeURIComponent(c.req.header("x-file-name") ?? "");
|
|
406
466
|
} catch {
|
|
407
467
|
throw new HTTPException7(400, { message: "Invalid filename" });
|
|
408
468
|
}
|
|
409
469
|
const data = Buffer.from(await c.req.arrayBuffer());
|
|
410
470
|
if (!data.length) throw new HTTPException7(400, { message: "Empty attachment" });
|
|
411
471
|
const mime = (c.req.header("content-type") ?? "application/octet-stream").slice(0, 100);
|
|
412
|
-
return c.json(chats.upload(c.get("actorId"), c.req.param("id"),
|
|
472
|
+
return c.json(chats.upload(c.get("actorId"), c.req.param("id"), name2, mime, data), 201);
|
|
413
473
|
});
|
|
414
474
|
routes.delete("/:id", async (c) => {
|
|
415
475
|
const id = c.req.param("id");
|
|
@@ -433,8 +493,8 @@ function attachmentRoutes(chats) {
|
|
|
433
493
|
routes.get("/:id/run", (c) => {
|
|
434
494
|
const attachment = chats.download(c.get("actorId"), c.req.param("id"));
|
|
435
495
|
if (!attachment) throw new HTTPException7(404, { message: "Attachment not found" });
|
|
436
|
-
const { name, mimeType } = attachment.file;
|
|
437
|
-
if (!/\.html?$/i.test(
|
|
496
|
+
const { name: name2, mimeType } = attachment.file;
|
|
497
|
+
if (!/\.html?$/i.test(name2) && mimeType !== "text/html") throw new HTTPException7(415, { message: "Only HTML attachments can run" });
|
|
438
498
|
c.set("sandboxedDocument", true);
|
|
439
499
|
return c.body(new Uint8Array(attachment.data), 200, { "Content-Type": "text/html; charset=utf-8", "Content-Length": String(attachment.data.length) });
|
|
440
500
|
});
|
|
@@ -559,6 +619,11 @@ function channelRoutes({ chats }) {
|
|
|
559
619
|
return chatView(found, (member) => chats.member(member));
|
|
560
620
|
};
|
|
561
621
|
routes.get("/members", (c) => c.json({ members: channels.everyone(c.get("actorId")) }));
|
|
622
|
+
routes.get("/search", (c) => {
|
|
623
|
+
const within = c.req.query("in") || void 0, limit = Number(c.req.query("limit") ?? 50), view = (found2) => chatView(found2, (member) => chats.member(member));
|
|
624
|
+
const found = chats.find(c.get("actorId"), c.req.query("q") ?? "", { within, limit });
|
|
625
|
+
return c.json({ chats: found.chats.map(view), messages: found.messages.map(({ message, chat: place }) => ({ message: messageView(message), chat: view(place) })) });
|
|
626
|
+
});
|
|
562
627
|
routes.put("/messages/:id/reaction", jsonBody, async (c) => {
|
|
563
628
|
const input = await readJson(c);
|
|
564
629
|
return c.json(messageView(chats.react(c.get("actorId"), c.req.param("id"), input.emoji, input.on ?? true)));
|
|
@@ -789,10 +854,10 @@ function migrateChannels(db) {
|
|
|
789
854
|
const rename2 = db.prepare("UPDATE bots SET name=? WHERE id=?");
|
|
790
855
|
for (const bot of db.prepare("SELECT id,name FROM bots ORDER BY created_at,id").all()) {
|
|
791
856
|
const base = bot.name.replace(/@/g, "").trim() || "Bot";
|
|
792
|
-
let
|
|
793
|
-
for (let suffix = 2; taken.has(
|
|
794
|
-
taken.add(
|
|
795
|
-
if (
|
|
857
|
+
let name2 = base;
|
|
858
|
+
for (let suffix = 2; taken.has(name2.toLowerCase()); suffix++) name2 = `${base} ${suffix}`;
|
|
859
|
+
taken.add(name2.toLowerCase());
|
|
860
|
+
if (name2 !== bot.name) rename2.run(name2, bot.id);
|
|
796
861
|
}
|
|
797
862
|
db.exec("PRAGMA user_version=12; COMMIT;");
|
|
798
863
|
} catch (error) {
|
|
@@ -898,6 +963,41 @@ function migrateInbox(db) {
|
|
|
898
963
|
}
|
|
899
964
|
}
|
|
900
965
|
|
|
966
|
+
// src/server/infra/storage/migrations/quote-schema.ts
|
|
967
|
+
function migrateQuotes(db) {
|
|
968
|
+
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
969
|
+
if (version >= 17) return;
|
|
970
|
+
db.exec("BEGIN IMMEDIATE");
|
|
971
|
+
try {
|
|
972
|
+
db.exec(`
|
|
973
|
+
CREATE TABLE message_quotes(message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
974
|
+
position INTEGER NOT NULL CHECK(position >= 1), source_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
|
|
975
|
+
source_sender_id TEXT NOT NULL REFERENCES members(id), text TEXT NOT NULL, PRIMARY KEY(message_id, position));
|
|
976
|
+
CREATE INDEX message_quotes_by_source ON message_quotes(source_id);
|
|
977
|
+
PRAGMA user_version=17;
|
|
978
|
+
`);
|
|
979
|
+
db.exec("COMMIT");
|
|
980
|
+
} catch (error) {
|
|
981
|
+
db.exec("ROLLBACK");
|
|
982
|
+
throw error;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/server/infra/storage/migrations/title-schema.ts
|
|
987
|
+
function migrateTitles(db) {
|
|
988
|
+
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
989
|
+
if (version >= 18) return;
|
|
990
|
+
db.exec("BEGIN IMMEDIATE");
|
|
991
|
+
try {
|
|
992
|
+
db.exec(`UPDATE chats SET title='' WHERE type='dm' AND title='\u65B0\u5BF9\u8BDD' AND NOT EXISTS(SELECT 1 FROM messages WHERE chat_id=chats.id);
|
|
993
|
+
PRAGMA user_version=18;`);
|
|
994
|
+
db.exec("COMMIT");
|
|
995
|
+
} catch (error) {
|
|
996
|
+
db.exec("ROLLBACK");
|
|
997
|
+
throw error;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
901
1001
|
// src/server/infra/storage/migrations/schema.ts
|
|
902
1002
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
903
1003
|
|
|
@@ -1021,7 +1121,7 @@ function migrateRuntime(db) {
|
|
|
1021
1121
|
// src/server/infra/storage/migrations/schema.ts
|
|
1022
1122
|
function migrateDatabase(db) {
|
|
1023
1123
|
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1024
|
-
if (version >
|
|
1124
|
+
if (version > 18) throw new Error(`Unsupported database version: ${version}`);
|
|
1025
1125
|
migratePrototype(db);
|
|
1026
1126
|
if (version >= 5) {
|
|
1027
1127
|
migrateRuntime(db);
|
|
@@ -1035,6 +1135,8 @@ function migrateDatabase(db) {
|
|
|
1035
1135
|
migrateSubscriptions(db);
|
|
1036
1136
|
migrateInteractions(db);
|
|
1037
1137
|
migrateInbox(db);
|
|
1138
|
+
migrateQuotes(db);
|
|
1139
|
+
migrateTitles(db);
|
|
1038
1140
|
return;
|
|
1039
1141
|
}
|
|
1040
1142
|
db.exec("PRAGMA foreign_keys=OFF; BEGIN IMMEDIATE");
|
|
@@ -1106,6 +1208,8 @@ function migrateDatabase(db) {
|
|
|
1106
1208
|
migrateSubscriptions(db);
|
|
1107
1209
|
migrateInteractions(db);
|
|
1108
1210
|
migrateInbox(db);
|
|
1211
|
+
migrateQuotes(db);
|
|
1212
|
+
migrateTitles(db);
|
|
1109
1213
|
}
|
|
1110
1214
|
|
|
1111
1215
|
// src/server/infra/storage/chat-repository.ts
|
|
@@ -1156,10 +1260,10 @@ var ChannelRepository = class {
|
|
|
1156
1260
|
section(ownerId, id) {
|
|
1157
1261
|
return this.db.prepare(`SELECT ${SECTION_COLUMNS} FROM sections WHERE id=? AND owner_id=?`).get(id, ownerId) ?? null;
|
|
1158
1262
|
}
|
|
1159
|
-
createSection(ownerId,
|
|
1263
|
+
createSection(ownerId, name2) {
|
|
1160
1264
|
const id = randomUUID3(), now = Date.now();
|
|
1161
1265
|
const position = this.db.prepare("SELECT COALESCE(MAX(position),0)+1 AS next FROM sections WHERE owner_id=?").get(ownerId).next;
|
|
1162
|
-
this.db.prepare("INSERT INTO sections(id,owner_id,name,position,created_at,updated_at) VALUES(?,?,?,?,?,?)").run(id, ownerId,
|
|
1266
|
+
this.db.prepare("INSERT INTO sections(id,owner_id,name,position,created_at,updated_at) VALUES(?,?,?,?,?,?)").run(id, ownerId, name2, position, now, now);
|
|
1163
1267
|
return this.section(ownerId, id);
|
|
1164
1268
|
}
|
|
1165
1269
|
updateSection(ownerId, id, change) {
|
|
@@ -1295,28 +1399,28 @@ var ChannelRepository = class {
|
|
|
1295
1399
|
|
|
1296
1400
|
// src/server/infra/storage/thread-repository.ts
|
|
1297
1401
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
1298
|
-
function threadTitle(content2, names) {
|
|
1402
|
+
function threadTitle(content2, names, file = "") {
|
|
1299
1403
|
const line = (content2.split("\n").map((item) => item.trim()).find(Boolean) ?? "").replace(/[*`~]/g, "").replace(/^[#>\s-]+/, "");
|
|
1300
1404
|
const sorted = [...names].sort((a, b) => b.length - a.length);
|
|
1301
1405
|
let rest = line;
|
|
1302
1406
|
for (; ; ) {
|
|
1303
1407
|
rest = rest.trimStart();
|
|
1304
|
-
const
|
|
1305
|
-
if (!
|
|
1306
|
-
rest = rest.slice(
|
|
1408
|
+
const name2 = rest.startsWith("@") ? sorted.find((item) => rest.toLowerCase().startsWith(`@${item.toLowerCase()}`)) : void 0;
|
|
1409
|
+
if (!name2) break;
|
|
1410
|
+
rest = rest.slice(name2.length + 1);
|
|
1307
1411
|
}
|
|
1308
|
-
return (rest.trim() || line ||
|
|
1412
|
+
return (rest.trim() || line || file).slice(0, 60);
|
|
1309
1413
|
}
|
|
1310
1414
|
var ThreadRepository = class {
|
|
1311
1415
|
constructor(db, chats) {
|
|
1312
1416
|
this.db = db;
|
|
1313
1417
|
this.chats = chats;
|
|
1314
1418
|
}
|
|
1315
|
-
/** The thread of a channel message, created on first use; whoever opens it
|
|
1419
|
+
/** The thread of a channel message, created on first use; whoever opens it and the root's author follow it. */
|
|
1316
1420
|
open(actorId, rootMessageId, names) {
|
|
1317
1421
|
this.db.exec("BEGIN IMMEDIATE");
|
|
1318
1422
|
try {
|
|
1319
|
-
const root = this.db.prepare(`SELECT m.chat_id AS chatId,m.content,c.type,c.owner_id AS ownerId FROM messages m JOIN chats c ON c.id=m.chat_id
|
|
1423
|
+
const root = this.db.prepare(`SELECT m.chat_id AS chatId,m.content,m.sender_id AS senderId,c.type,c.owner_id AS ownerId FROM messages m JOIN chats c ON c.id=m.chat_id
|
|
1320
1424
|
WHERE m.id=?`).get(rootMessageId);
|
|
1321
1425
|
if (!root || !this.chats.canAccess(actorId, root.chatId, true)) throw new ChatError(404, "Message not found");
|
|
1322
1426
|
if (root.type !== "channel") throw new ChatError(400, "Threads start from channel messages");
|
|
@@ -1324,9 +1428,11 @@ var ThreadRepository = class {
|
|
|
1324
1428
|
if (!id) {
|
|
1325
1429
|
id = randomUUID4();
|
|
1326
1430
|
const now = Date.now();
|
|
1431
|
+
const firstFile = this.db.prepare("SELECT name FROM attachments WHERE message_id=? ORDER BY created_at,rowid LIMIT 1").get(rootMessageId)?.name;
|
|
1327
1432
|
this.db.prepare(`INSERT INTO chats(id,type,title,owner_id,created_at,updated_at,parent_id,root_message_id,status)
|
|
1328
|
-
VALUES(?,'thread',?,?,?,?,?,?,'open')`).run(id, threadTitle(root.content, names), root.ownerId, now, now, root.chatId, rootMessageId);
|
|
1433
|
+
VALUES(?,'thread',?,?,?,?,?,?,'open')`).run(id, threadTitle(previewQuotes(root.content, this.chats.quotes.of(rootMessageId)), names, firstFile), root.ownerId, now, now, root.chatId, rootMessageId);
|
|
1329
1434
|
this.follow(actorId, id);
|
|
1435
|
+
if (root.senderId !== actorId && this.chats.canAccess(root.senderId, root.chatId)) this.follow(root.senderId, id);
|
|
1330
1436
|
}
|
|
1331
1437
|
this.db.exec("COMMIT");
|
|
1332
1438
|
return id;
|
|
@@ -1406,6 +1512,10 @@ var InteractionRepository = class {
|
|
|
1406
1512
|
}
|
|
1407
1513
|
/** Adds or removes the actor's mark (anyone who can write in the chat), writing the routed deliveries with a new
|
|
1408
1514
|
* mark (A41). `added` is false when the mark was already there. Removing a mark withdraws what was not told yet. */
|
|
1515
|
+
/** Whether this member already has this emoji on this message. */
|
|
1516
|
+
reactionOn(actorId, messageId, emoji) {
|
|
1517
|
+
return !!this.db.prepare("SELECT 1 FROM message_reactions WHERE message_id=? AND member_id=? AND emoji=?").get(messageId, actorId, emoji);
|
|
1518
|
+
}
|
|
1409
1519
|
react(actorId, messageId, emoji, on, deliveries) {
|
|
1410
1520
|
return inTransaction2(this.db, () => {
|
|
1411
1521
|
const row = this.db.prepare("SELECT chat_id AS chatId FROM messages WHERE id=?").get(messageId);
|
|
@@ -1440,9 +1550,9 @@ var InteractionRepository = class {
|
|
|
1440
1550
|
rowState(memberId, chatId) {
|
|
1441
1551
|
const counts = this.db.prepare(`SELECT count(*) AS n,COALESCE(SUM(reason IN ('mention','reply')),0) AS at FROM inbox
|
|
1442
1552
|
WHERE member_id=? AND chat_id=? AND kind='message' AND urgency='interrupt' AND state='pending'`).get(memberId, chatId);
|
|
1443
|
-
const row = this.db.prepare(`SELECT i.kind,c.type AS chatType,i.actor_id AS actorId,i.detail,i.created_at AS createdAt FROM inbox i JOIN chats c ON c.id=i.chat_id
|
|
1553
|
+
const row = this.db.prepare(`SELECT i.kind,i.chat_id AS chatId,c.type AS chatType,c.title,i.actor_id AS actorId,i.detail,i.created_at AS createdAt FROM inbox i JOIN chats c ON c.id=i.chat_id
|
|
1444
1554
|
WHERE i.member_id=? AND i.kind<>'message' AND i.urgency='queue' AND i.state='pending' AND (i.chat_id=? OR c.parent_id=?) ORDER BY i.id DESC LIMIT 1`).get(memberId, chatId, chatId);
|
|
1445
|
-
const hint = row ? { kind: row.kind, chatType: row.chatType, actor: row.actorId && this.chats.member(row.actorId)?.name || "", detail: row.detail ?? "", createdAt: row.createdAt } : null;
|
|
1555
|
+
const hint = row ? { kind: row.kind, chatId: row.chatId, chatType: row.chatType, title: row.title, actor: row.actorId && this.chats.member(row.actorId)?.name || "", detail: row.detail ?? "", createdAt: row.createdAt } : null;
|
|
1446
1556
|
return { alerts: counts.n, atMe: counts.at > 0, hint };
|
|
1447
1557
|
}
|
|
1448
1558
|
/** A person opened a chat: its queued deliveries (and its threads', for a channel) count as seen. */
|
|
@@ -1468,11 +1578,26 @@ var InteractionRepository = class {
|
|
|
1468
1578
|
}
|
|
1469
1579
|
};
|
|
1470
1580
|
|
|
1581
|
+
// src/server/infra/storage/quote-repository.ts
|
|
1582
|
+
var QuoteRepository = class {
|
|
1583
|
+
constructor(db) {
|
|
1584
|
+
this.db = db;
|
|
1585
|
+
}
|
|
1586
|
+
/** Inside the caller's transaction. */
|
|
1587
|
+
insert(messageId, quotes) {
|
|
1588
|
+
quotes.forEach((quote, index) => this.db.prepare("INSERT INTO message_quotes(message_id,position,source_id,source_sender_id,text) VALUES(?,?,?,?,?)").run(messageId, index + 1, quote.messageId, quote.senderId, quote.text));
|
|
1589
|
+
}
|
|
1590
|
+
of(messageId) {
|
|
1591
|
+
return this.db.prepare("SELECT source_id AS messageId,source_sender_id AS senderId,text FROM message_quotes WHERE message_id=? ORDER BY position").all(messageId);
|
|
1592
|
+
}
|
|
1593
|
+
};
|
|
1594
|
+
|
|
1471
1595
|
// src/server/infra/storage/chat-repository.ts
|
|
1472
1596
|
var MESSAGE_COLUMNS = "id,chat_id AS chatId,sequence,sender_id AS senderId,content,created_at AS createdAt,history_id AS historyId,tool_call_id AS toolCallId,question,echo_of AS echoOf,reply_to AS replyTo";
|
|
1473
1597
|
var CHAT_COLUMNS = `c.id,c.type,c.title,c.owner_id AS ownerId,c.created_at AS createdAt,c.updated_at AS updatedAt,
|
|
1474
1598
|
c.section_id AS sectionId,c.topic,c.parent_id AS parentId,c.root_message_id AS rootMessageId,c.status,
|
|
1475
|
-
(SELECT
|
|
1599
|
+
(SELECT content FROM messages WHERE chat_id=c.id ORDER BY sequence DESC LIMIT 1) AS lastMessage,
|
|
1600
|
+
(SELECT id FROM messages WHERE chat_id=c.id ORDER BY sequence DESC LIMIT 1) AS lastMessageId,
|
|
1476
1601
|
(SELECT COALESCE(a.username,b.name) FROM messages m LEFT JOIN account a ON a.member_id=m.sender_id LEFT JOIN bots b ON b.id=m.sender_id
|
|
1477
1602
|
WHERE m.chat_id=c.id ORDER BY m.sequence DESC LIMIT 1) AS lastSender`;
|
|
1478
1603
|
var UNREAD = `FROM messages m WHERE m.chat_id=:chat AND m.sender_id<>:member
|
|
@@ -1485,11 +1610,13 @@ var ChatRepository = class {
|
|
|
1485
1610
|
this.channels = new ChannelRepository(db, this);
|
|
1486
1611
|
this.threads = new ThreadRepository(db, this);
|
|
1487
1612
|
this.interactions = new InteractionRepository(db, this);
|
|
1613
|
+
this.quotes = new QuoteRepository(db);
|
|
1488
1614
|
}
|
|
1489
1615
|
/** Sections, channels, notes, read positions and threads share this connection. */
|
|
1490
1616
|
channels;
|
|
1491
1617
|
threads;
|
|
1492
1618
|
interactions;
|
|
1619
|
+
quotes;
|
|
1493
1620
|
member(id) {
|
|
1494
1621
|
const row = this.db.prepare(`SELECT m.id,m.kind,COALESCE(a.username,b.name,m.id) AS name,b.deleted_at IS NOT NULL AS deleted FROM members m
|
|
1495
1622
|
LEFT JOIN account a ON a.member_id=m.id LEFT JOIN bots b ON b.id=m.id WHERE m.id=?`).get(id);
|
|
@@ -1526,8 +1653,10 @@ var ChatRepository = class {
|
|
|
1526
1653
|
}
|
|
1527
1654
|
getChat(actorId, id) {
|
|
1528
1655
|
if (!this.canAccess(actorId, id)) return null;
|
|
1529
|
-
const
|
|
1530
|
-
if (!
|
|
1656
|
+
const found = this.db.prepare(`SELECT ${CHAT_COLUMNS} FROM chats c WHERE c.id=?`).get(id);
|
|
1657
|
+
if (!found) return null;
|
|
1658
|
+
const { lastMessageId, lastMessage: content2, ...rest } = found;
|
|
1659
|
+
const row = { ...rest, lastMessage: content2 !== null && lastMessageId ? { content: clipMarked(content2, 300), quotes: this.quotes.of(lastMessageId) } : null };
|
|
1531
1660
|
const members = this.db.prepare("SELECT member_id AS id FROM chat_members WHERE chat_id=? ORDER BY member_id").all(row.parentId ?? id);
|
|
1532
1661
|
const state = this.db.prepare("SELECT following,subscribed,pinned,muted FROM chat_states WHERE chat_id=? AND member_id=?").get(id, actorId);
|
|
1533
1662
|
return {
|
|
@@ -1618,13 +1747,22 @@ var ChatRepository = class {
|
|
|
1618
1747
|
const rows2 = this.db.prepare(`SELECT ${MESSAGE_COLUMNS.split(",").map((column) => `m.${column}`).join(",")} ${UNREAD} ORDER BY m.sequence LIMIT :limit`).all({ chat: chatId, member: memberId, limit });
|
|
1619
1748
|
return rows2.map((row) => this.messageFromRow(row));
|
|
1620
1749
|
}
|
|
1621
|
-
/** Search every chat the member can read: its DMs and channels and their threads (newest first).
|
|
1622
|
-
|
|
1750
|
+
/** Search every chat the member can read: its DMs and channels and their threads (newest first). `within` keeps
|
|
1751
|
+
* one chat, a channel with its threads. */
|
|
1752
|
+
searchAll(actorId, query, limit, within) {
|
|
1753
|
+
const scope = within ? "AND (c.id=? OR c.parent_id=?)" : "";
|
|
1623
1754
|
const rows2 = this.db.prepare(`SELECT ${MESSAGE_COLUMNS.split(",").map((column) => `m.${column}`).join(",")} FROM messages m JOIN chats c ON c.id=m.chat_id
|
|
1624
1755
|
JOIN chat_members cm ON cm.chat_id=COALESCE(c.parent_id,c.id) AND cm.member_id=? AND cm.can_read=1
|
|
1625
|
-
WHERE instr(lower(m.content),lower(?))>0 ORDER BY m.position DESC LIMIT ?`).all(actorId, query, limit);
|
|
1756
|
+
WHERE instr(lower(m.content),lower(?))>0 ${scope} ORDER BY m.position DESC LIMIT ?`).all(actorId, query, ...within ? [within, within] : [], limit);
|
|
1626
1757
|
return rows2.map((row) => this.messageFromRow(row));
|
|
1627
1758
|
}
|
|
1759
|
+
/** Channels and threads the member can read whose title, topic or notice / summary contains the query (newest first). */
|
|
1760
|
+
searchChatIds(actorId, query, limit) {
|
|
1761
|
+
return this.db.prepare(`SELECT c.id FROM chats c JOIN chat_members cm ON cm.chat_id=COALESCE(c.parent_id,c.id) AND cm.member_id=? AND cm.can_read=1
|
|
1762
|
+
LEFT JOIN chat_notes n ON n.chat_id=c.id WHERE c.type<>'dm'
|
|
1763
|
+
AND (instr(lower(c.title),lower(?))>0 OR instr(lower(c.topic),lower(?))>0 OR instr(lower(COALESCE(n.text,'')),lower(?))>0)
|
|
1764
|
+
ORDER BY c.updated_at DESC LIMIT ?`).all(actorId, query, query, query, limit).map((row) => row.id);
|
|
1765
|
+
}
|
|
1628
1766
|
messages(actorId, chatId, after = 0, limit = 1e3) {
|
|
1629
1767
|
this.assertAccess(actorId, chatId);
|
|
1630
1768
|
const rows2 = this.db.prepare(`SELECT ${MESSAGE_COLUMNS} FROM messages WHERE chat_id=? AND sequence>? ORDER BY sequence LIMIT ?`).all(chatId, after, limit);
|
|
@@ -1666,13 +1804,14 @@ var ChatRepository = class {
|
|
|
1666
1804
|
thread: this.threads.info(row.id),
|
|
1667
1805
|
echoOf: row.echoOf === null ? null : { id: row.echoOf, title: this.db.prepare("SELECT title FROM chats WHERE id=?").get(row.echoOf).title },
|
|
1668
1806
|
replyTo: row.replyTo === null ? null : this.quote(row.replyTo),
|
|
1669
|
-
reactions: this.interactions.reactions(row.id)
|
|
1807
|
+
reactions: this.interactions.reactions(row.id),
|
|
1808
|
+
quotes: this.quotes.of(row.id)
|
|
1670
1809
|
};
|
|
1671
1810
|
}
|
|
1672
|
-
/** What a reply shows of the message it answers: who wrote it and the start of its
|
|
1811
|
+
/** What a reply shows of the message it answers: who wrote it and the start of its content, with its quotes ("" is attachments only). */
|
|
1673
1812
|
quote(id) {
|
|
1674
1813
|
const row = this.db.prepare("SELECT id,sender_id AS senderId,content FROM messages WHERE id=?").get(id);
|
|
1675
|
-
return row ? { id: row.id, senderId: row.senderId, content: row.content
|
|
1814
|
+
return row ? { id: row.id, senderId: row.senderId, content: clipMarked(row.content, 200), quotes: this.quotes.of(row.id) } : null;
|
|
1676
1815
|
}
|
|
1677
1816
|
append(actorId, chatId, draft, requestHash, files = [], deliveries = { message: [], echo: [] }, mentions = []) {
|
|
1678
1817
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -1699,7 +1838,8 @@ var ChatRepository = class {
|
|
|
1699
1838
|
if (draft.replyTo && this.db.prepare("SELECT chat_id AS chatId FROM messages WHERE id=?").get(draft.replyTo)?.chatId !== chatId)
|
|
1700
1839
|
throw new ChatError(400, "Reply target is not in this chat");
|
|
1701
1840
|
this.db.prepare("INSERT INTO messages(position,id,chat_id,sequence,sender_id,content,created_at,client_key,request_hash,history_id,tool_call_id,question,reply_to) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)").run(position, id, chatId, sequence, actorId, draft.content, createdAt, draft.clientMessageId ?? null, requestHash, draft.source?.historyId ?? null, draft.source?.toolCallId ?? null, question, draft.replyTo ?? null);
|
|
1702
|
-
for (const
|
|
1841
|
+
for (const file2 of files) this.db.prepare("INSERT INTO attachments(id,chat_id,message_id,uploaded_by,name,mime_type,size,data,created_at) VALUES(?,?,?,?,?,?,?,?,?)").run(randomUUID5(), chatId, id, actorId, file2.name, file2.mimeType, file2.data.length, file2.data, createdAt);
|
|
1842
|
+
this.quotes.insert(id, draft.quotes ?? []);
|
|
1703
1843
|
for (const attachmentId of draft.attachmentIds) this.db.prepare("UPDATE attachments SET message_id=? WHERE id=?").run(id, attachmentId);
|
|
1704
1844
|
this.inbox(deliveries.message, chatId, id, actorId, createdAt);
|
|
1705
1845
|
for (const memberId of mentions) this.db.prepare("INSERT OR IGNORE INTO message_mentions(message_id,member_id) VALUES(?,?)").run(id, memberId);
|
|
@@ -1712,6 +1852,7 @@ var ChatRepository = class {
|
|
|
1712
1852
|
if (draft.content.trim()) {
|
|
1713
1853
|
const echoSequence = this.db.prepare("SELECT COALESCE(MAX(sequence),0)+1 AS next FROM messages WHERE chat_id=?").get(chat.parentId).next;
|
|
1714
1854
|
this.db.prepare("INSERT INTO messages(position,id,chat_id,sequence,sender_id,content,created_at,request_hash,history_id,tool_call_id,echo_of) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(position + 1, `m${position + 1}`, chat.parentId, echoSequence, actorId, draft.content, createdAt, requestHash, draft.source?.historyId ?? null, draft.source?.toolCallId ?? null, chatId);
|
|
1855
|
+
this.quotes.insert(`m${position + 1}`, draft.quotes ?? []);
|
|
1715
1856
|
this.markOwn(chat.parentId, actorId, echoSequence);
|
|
1716
1857
|
this.db.prepare("UPDATE chats SET updated_at=? WHERE id=?").run(createdAt, chat.parentId);
|
|
1717
1858
|
this.inbox(deliveries.echo, chat.parentId, `m${position + 1}`, actorId, createdAt);
|
|
@@ -1730,8 +1871,9 @@ var ChatRepository = class {
|
|
|
1730
1871
|
});
|
|
1731
1872
|
}
|
|
1732
1873
|
}
|
|
1733
|
-
const
|
|
1734
|
-
|
|
1874
|
+
const file = this.db.prepare("SELECT name FROM attachments WHERE message_id=? ORDER BY created_at,rowid LIMIT 1").get(id);
|
|
1875
|
+
const title = draft.content ? previewQuotes(draft.content, draft.quotes ?? []).slice(0, 46).replace(/\s+/g, " ") : file?.name.slice(0, 46) ?? "";
|
|
1876
|
+
this.db.prepare("UPDATE chats SET title=CASE WHEN title='' AND ?=1 THEN ? ELSE title END,updated_at=? WHERE id=?").run(sequence, title, createdAt, chatId);
|
|
1735
1877
|
const message = this.messageFromRow({ id, chatId, sequence, senderId: actorId, content: draft.content, createdAt, historyId: draft.source?.historyId ?? null, toolCallId: draft.source?.toolCallId ?? null, question, echoOf: null, replyTo: draft.replyTo ?? null });
|
|
1736
1878
|
this.db.exec("COMMIT");
|
|
1737
1879
|
return { message, duplicate: false, ...echo ? { echo } : {} };
|
|
@@ -1758,10 +1900,10 @@ var ChatRepository = class {
|
|
|
1758
1900
|
expirePendingQuestions() {
|
|
1759
1901
|
this.db.prepare(`UPDATE messages SET question=json_set(question,'$.state','ignored','$.settledAt',?) WHERE json_extract(question,'$.state')='pending'`).run(Date.now());
|
|
1760
1902
|
}
|
|
1761
|
-
createAttachment(actorId, chatId,
|
|
1903
|
+
createAttachment(actorId, chatId, name2, mimeType, data) {
|
|
1762
1904
|
this.assertAccess(actorId, chatId, true);
|
|
1763
|
-
const file = { id: randomUUID5(), name, mimeType, size: data.length };
|
|
1764
|
-
this.db.prepare("INSERT INTO attachments(id,chat_id,message_id,uploaded_by,name,mime_type,size,data,created_at) VALUES(?,?,NULL,?,?,?,?,?,?)").run(file.id, chatId, actorId,
|
|
1905
|
+
const file = { id: randomUUID5(), name: name2, mimeType, size: data.length };
|
|
1906
|
+
this.db.prepare("INSERT INTO attachments(id,chat_id,message_id,uploaded_by,name,mime_type,size,data,created_at) VALUES(?,?,NULL,?,?,?,?,?,?)").run(file.id, chatId, actorId, name2, mimeType, data.length, data, Date.now());
|
|
1765
1907
|
return file;
|
|
1766
1908
|
}
|
|
1767
1909
|
attachment(actorId, id) {
|
|
@@ -1831,7 +1973,7 @@ var RuntimeRepository = class {
|
|
|
1831
1973
|
}
|
|
1832
1974
|
submitted(runId) {
|
|
1833
1975
|
if (!this.db.prepare("UPDATE bot_runs SET state='submitted' WHERE id=? AND state='claimed'").run(runId).changes)
|
|
1834
|
-
throw new RuntimeFault("invalid_run", "
|
|
1976
|
+
throw new RuntimeFault("invalid_run", "The run state has changed.");
|
|
1835
1977
|
}
|
|
1836
1978
|
finish(runId, state, errorCode = null) {
|
|
1837
1979
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -1848,7 +1990,7 @@ var RuntimeRepository = class {
|
|
|
1848
1990
|
this.db.exec("BEGIN IMMEDIATE");
|
|
1849
1991
|
try {
|
|
1850
1992
|
if (!this.db.prepare("UPDATE bot_runs SET state='cancelled',ended_at=? WHERE id=? AND state='claimed'").run(Date.now(), runId).changes)
|
|
1851
|
-
throw new RuntimeFault("unsafe_replay", "
|
|
1993
|
+
throw new RuntimeFault("unsafe_replay", "A submitted run can't be replayed automatically.");
|
|
1852
1994
|
this.db.prepare("UPDATE inbox SET state='pending',run_id=NULL WHERE run_id=? AND state='claimed'").run(runId);
|
|
1853
1995
|
this.db.exec("COMMIT");
|
|
1854
1996
|
} catch (error) {
|
|
@@ -1871,6 +2013,11 @@ var RuntimeRepository = class {
|
|
|
1871
2013
|
runs(botId) {
|
|
1872
2014
|
return this.db.prepare(`SELECT ${RUN_COLUMNS} FROM bot_runs WHERE bot_id=? ORDER BY created_at DESC,rowid DESC LIMIT 50`).all(botId).map((row) => this.fromRow(row));
|
|
1873
2015
|
}
|
|
2016
|
+
/** The error code of the Bot's latest run; null when that run did not fail. */
|
|
2017
|
+
latestErrorCode(botId) {
|
|
2018
|
+
const row = this.db.prepare("SELECT error_code AS errorCode FROM bot_runs WHERE bot_id=? ORDER BY created_at DESC,rowid DESC LIMIT 1").get(botId);
|
|
2019
|
+
return row?.errorCode ?? null;
|
|
2020
|
+
}
|
|
1874
2021
|
fromRow({ selectionJson, ...row }) {
|
|
1875
2022
|
return { ...row, selection: JSON.parse(selectionJson) };
|
|
1876
2023
|
}
|
|
@@ -1910,8 +2057,8 @@ function providerCatalog() {
|
|
|
1910
2057
|
if (!models.length && provider.getAllModels?.().length) return [];
|
|
1911
2058
|
const common = { id: provider.id, api: models[0]?.api ?? "openai-completions", modelCount: models.length };
|
|
1912
2059
|
return [
|
|
1913
|
-
...provider.auth.oauth ? [{ ...common, name: provider.auth.oauth.name,
|
|
1914
|
-
...provider.auth.apiKey?.login ? [{ ...common, name: provider.name,
|
|
2060
|
+
...provider.auth.oauth ? [{ ...common, name: provider.auth.oauth.name, authType: "oauth" }] : [],
|
|
2061
|
+
...provider.auth.apiKey?.login ? [{ ...common, name: provider.name, authType: "api_key" }] : []
|
|
1915
2062
|
];
|
|
1916
2063
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
1917
2064
|
}
|
|
@@ -1919,15 +2066,15 @@ var modelApis = new Set(builtinProviders().flatMap((provider) => provider.getMod
|
|
|
1919
2066
|
|
|
1920
2067
|
// src/server/infra/storage/credential-repository.ts
|
|
1921
2068
|
function credentialValue(value2) {
|
|
1922
|
-
if (!value2 || typeof value2 !== "object") throw new RuntimeFault("invalid_credential", "
|
|
2069
|
+
if (!value2 || typeof value2 !== "object") throw new RuntimeFault("invalid_credential", "The credential format isn't valid.");
|
|
1923
2070
|
const v = value2;
|
|
1924
2071
|
if (v.type === "api_key" && (typeof v.key === "string" || v.env && typeof v.env === "object")) {
|
|
1925
|
-
if (v.env && (Array.isArray(v.env) || Object.values(v.env).some((item) => typeof item !== "string"))) throw new RuntimeFault("invalid_credential", "
|
|
2072
|
+
if (v.env && (Array.isArray(v.env) || Object.values(v.env).some((item) => typeof item !== "string"))) throw new RuntimeFault("invalid_credential", "The credential format isn't valid.");
|
|
1926
2073
|
return structuredClone(v);
|
|
1927
2074
|
}
|
|
1928
2075
|
if (v.type === "oauth" && typeof v.access === "string" && typeof v.refresh === "string" && typeof v.expires === "number" && Number.isFinite(v.expires))
|
|
1929
2076
|
return structuredClone(v);
|
|
1930
|
-
throw new RuntimeFault("invalid_credential", "
|
|
2077
|
+
throw new RuntimeFault("invalid_credential", "The credential format isn't valid.");
|
|
1931
2078
|
}
|
|
1932
2079
|
var CredentialRepository = class {
|
|
1933
2080
|
constructor(db) {
|
|
@@ -1943,9 +2090,9 @@ var CredentialRepository = class {
|
|
|
1943
2090
|
this.db.exec("BEGIN IMMEDIATE");
|
|
1944
2091
|
try {
|
|
1945
2092
|
if (!this.db.prepare("SELECT 1 FROM provider_connections WHERE id=? AND owner_id=?").get(connectionId, ownerId))
|
|
1946
|
-
throw new RuntimeFault("connection_missing", "
|
|
2093
|
+
throw new RuntimeFault("connection_missing", "Connection not found", 404);
|
|
1947
2094
|
const current = this.read(ownerId, connectionId);
|
|
1948
|
-
if ((current?.revision ?? null) !== prior) throw new RuntimeFault("credential_changed", "
|
|
2095
|
+
if ((current?.revision ?? null) !== prior) throw new RuntimeFault("credential_changed", "The credential changed. Please try again.");
|
|
1949
2096
|
this.db.prepare(`INSERT INTO provider_credentials(connection_id,type,credential_json,updated_at) VALUES(?,?,?,?)
|
|
1950
2097
|
ON CONFLICT(connection_id) DO UPDATE SET type=excluded.type,credential_json=excluded.credential_json,updated_at=excluded.updated_at`).run(connectionId, value2.type, next, Date.now());
|
|
1951
2098
|
this.db.exec("COMMIT");
|
|
@@ -1988,7 +2135,7 @@ function normalizeModel(value2) {
|
|
|
1988
2135
|
if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) throw new ValidationError("Invalid model");
|
|
1989
2136
|
const input = value2;
|
|
1990
2137
|
const id = requireText(input.id, "Model ID", 200);
|
|
1991
|
-
const
|
|
2138
|
+
const name2 = input.name === void 0 ? id : optionalText(input.name, "Model name", 200) || id;
|
|
1992
2139
|
if (input.reasoning !== void 0 && typeof input.reasoning !== "boolean") throw new ValidationError("Invalid reasoning capability");
|
|
1993
2140
|
const capabilities2 = input.input === void 0 ? ["text"] : input.input;
|
|
1994
2141
|
if (!Array.isArray(capabilities2) || !capabilities2.length || !capabilities2.includes("text") || capabilities2.some((item) => item !== "text" && item !== "image")) throw new ValidationError("Invalid input capabilities");
|
|
@@ -2009,7 +2156,7 @@ function normalizeModel(value2) {
|
|
|
2009
2156
|
const { thinkingFormat: _format, ...compat } = rawCompat;
|
|
2010
2157
|
return {
|
|
2011
2158
|
id,
|
|
2012
|
-
name,
|
|
2159
|
+
name: name2,
|
|
2013
2160
|
...input.api ? { api: input.api } : {},
|
|
2014
2161
|
headerNames: Object.keys(input.headers ?? {}),
|
|
2015
2162
|
reasoning: input.reasoning === true,
|
|
@@ -2067,14 +2214,14 @@ var Store = class {
|
|
|
2067
2214
|
return this.db.prepare("SELECT username FROM account WHERE id=1").get()?.username ?? null;
|
|
2068
2215
|
}
|
|
2069
2216
|
initialize(username, password) {
|
|
2070
|
-
const
|
|
2217
|
+
const name2 = requireText(username, "Username", 64);
|
|
2071
2218
|
if (typeof password !== "string" || password.length < 8 || password.length > 256) throw new ValidationError("Password must be 8-256 characters");
|
|
2072
2219
|
const salt = randomBytes(16).toString("hex");
|
|
2073
2220
|
const digest = passwordHash(password, salt);
|
|
2074
2221
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2075
2222
|
try {
|
|
2076
2223
|
if (this.isInitialized()) throw new ConflictError("Account is already initialized");
|
|
2077
|
-
this.db.prepare("INSERT INTO account(id,username,password_salt,password_hash,member_id) VALUES(1,?,?,?,?)").run(
|
|
2224
|
+
this.db.prepare("INSERT INTO account(id,username,password_salt,password_hash,member_id) VALUES(1,?,?,?,?)").run(name2, salt, digest, this.userMemberId());
|
|
2078
2225
|
this.db.exec("COMMIT");
|
|
2079
2226
|
} catch (error) {
|
|
2080
2227
|
this.db.exec("ROLLBACK");
|
|
@@ -2113,9 +2260,9 @@ var Store = class {
|
|
|
2113
2260
|
return row ? this.botFromRow(row) : null;
|
|
2114
2261
|
}
|
|
2115
2262
|
/** Member names are unique across people and Bots, ignoring case, so "@name" in a message is unambiguous (A37). */
|
|
2116
|
-
nameTaken(
|
|
2263
|
+
nameTaken(name2, exceptBotId) {
|
|
2117
2264
|
return !!this.db.prepare(`SELECT 1 FROM account WHERE lower(username)=lower(:name)
|
|
2118
|
-
UNION ALL SELECT 1 FROM bots WHERE lower(name)=lower(:name) AND id<>:except AND deleted_at IS NULL LIMIT 1`).get({ name, except: exceptBotId ?? "" });
|
|
2265
|
+
UNION ALL SELECT 1 FROM bots WHERE lower(name)=lower(:name) AND id<>:except AND deleted_at IS NULL LIMIT 1`).get({ name: name2, except: exceptBotId ?? "" });
|
|
2119
2266
|
}
|
|
2120
2267
|
/** Deletes a Bot: its DMs (with anyone), channel memberships and states, queued deliveries,
|
|
2121
2268
|
* notices and session row go; the member row stays, marked deleted, so messages it left in channels keep their
|
|
@@ -2136,15 +2283,14 @@ var Store = class {
|
|
|
2136
2283
|
return true;
|
|
2137
2284
|
}
|
|
2138
2285
|
botName(value2, exceptBotId) {
|
|
2139
|
-
const
|
|
2140
|
-
if (
|
|
2141
|
-
if (this.nameTaken(
|
|
2142
|
-
return
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
else clean = this.botName(name);
|
|
2286
|
+
const name2 = requireText(value2, "Bot name", 80);
|
|
2287
|
+
if (name2.includes("@") || /[\n\r]/.test(name2)) throw new ValidationError("Bot name cannot contain @ or line breaks");
|
|
2288
|
+
if (this.nameTaken(name2, exceptBotId)) throw new ConflictError("Another member already has this name", "name_taken");
|
|
2289
|
+
return name2;
|
|
2290
|
+
}
|
|
2291
|
+
/** The caller names the Bot (A45: the interface picks a free default in its own language). */
|
|
2292
|
+
createBot(name2) {
|
|
2293
|
+
const clean = this.botName(name2);
|
|
2148
2294
|
const id = randomUUID7(), createdAt = Date.now();
|
|
2149
2295
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2150
2296
|
try {
|
|
@@ -2159,10 +2305,10 @@ var Store = class {
|
|
|
2159
2305
|
}
|
|
2160
2306
|
updateBot(id, input) {
|
|
2161
2307
|
if (!this.getBot(id)) return null;
|
|
2162
|
-
const
|
|
2308
|
+
const name2 = this.botName(input.name, id);
|
|
2163
2309
|
const description = optionalText(input.description, "Description", 1e3);
|
|
2164
2310
|
const systemPrompt = optionalText(input.systemPrompt, "Persona", 16e3);
|
|
2165
|
-
if (!this.db.prepare("UPDATE bots SET name=?,description=?,system_prompt=? WHERE id=?").run(
|
|
2311
|
+
if (!this.db.prepare("UPDATE bots SET name=?,description=?,system_prompt=? WHERE id=?").run(name2, description, systemPrompt, id).changes) return null;
|
|
2166
2312
|
return this.getBot(id);
|
|
2167
2313
|
}
|
|
2168
2314
|
validateSelection(id, input) {
|
|
@@ -2229,8 +2375,8 @@ var Store = class {
|
|
|
2229
2375
|
}
|
|
2230
2376
|
return this.getModelConnection(id);
|
|
2231
2377
|
}
|
|
2232
|
-
checkCustomName(
|
|
2233
|
-
if (this.db.prepare("SELECT 1 FROM provider_connections WHERE owner_id=? AND kind='custom' AND provider_name=? AND id<>?").get(this.userMemberId(),
|
|
2378
|
+
checkCustomName(name2, except = "") {
|
|
2379
|
+
if (this.db.prepare("SELECT 1 FROM provider_connections WHERE owner_id=? AND kind='custom' AND provider_name=? AND id<>?").get(this.userMemberId(), name2, except)) throw new ValidationError("A provider with this name already exists", "provider_name_taken");
|
|
2234
2380
|
}
|
|
2235
2381
|
updateModelConnection(id, input) {
|
|
2236
2382
|
const current = this.getModelConnection(id);
|
|
@@ -2238,7 +2384,7 @@ var Store = class {
|
|
|
2238
2384
|
const providerName = current.kind === "custom" && input.providerName !== void 0 ? requireText(input.providerName, "Provider name", 80).toLowerCase() : current.providerName;
|
|
2239
2385
|
if (!/^[a-z0-9][a-z0-9._-]*$/.test(providerName)) throw new ValidationError("Invalid provider name");
|
|
2240
2386
|
if (current.kind === "custom") this.checkCustomName(providerName, id);
|
|
2241
|
-
const
|
|
2387
|
+
const name2 = requireText(input.displayName, "Connection name", 100);
|
|
2242
2388
|
const baseUrl = optionalText(input.baseUrl, "Base URL", 2048);
|
|
2243
2389
|
if (baseUrl) {
|
|
2244
2390
|
const url = new URL(baseUrl);
|
|
@@ -2246,7 +2392,7 @@ var Store = class {
|
|
|
2246
2392
|
}
|
|
2247
2393
|
const api = requireText(input.api, "API", 80);
|
|
2248
2394
|
if (!MODEL_APIS.has(api)) throw new ValidationError("Unsupported API protocol");
|
|
2249
|
-
this.db.prepare("UPDATE provider_connections SET display_name=?,base_url=?,api=?,updated_at=?,provider_name=? WHERE id=?").run(
|
|
2395
|
+
this.db.prepare("UPDATE provider_connections SET display_name=?,base_url=?,api=?,updated_at=?,provider_name=? WHERE id=?").run(name2, baseUrl, api, Date.now(), providerName, id);
|
|
2250
2396
|
return this.getModelConnection(id);
|
|
2251
2397
|
}
|
|
2252
2398
|
deleteModelConnection(id) {
|
|
@@ -2278,15 +2424,15 @@ var Store = class {
|
|
|
2278
2424
|
const updates = raw.headerUpdates;
|
|
2279
2425
|
if (updates !== void 0) {
|
|
2280
2426
|
if (!updates || typeof updates !== "object" || Array.isArray(updates)) throw new ValidationError("Invalid headers");
|
|
2281
|
-
for (const [
|
|
2282
|
-
if (value2 === null) headers2.delete(
|
|
2283
|
-
else if (typeof value2 === "string") headers2.set(
|
|
2427
|
+
for (const [name2, value2] of Object.entries(updates)) {
|
|
2428
|
+
if (value2 === null) headers2.delete(name2);
|
|
2429
|
+
else if (typeof value2 === "string") headers2.set(name2, value2);
|
|
2284
2430
|
else throw new ValidationError("Invalid header value");
|
|
2285
2431
|
}
|
|
2286
2432
|
}
|
|
2287
2433
|
const literalHeaders = {};
|
|
2288
|
-
headers2.forEach((value2,
|
|
2289
|
-
literalHeaders[
|
|
2434
|
+
headers2.forEach((value2, name2) => {
|
|
2435
|
+
literalHeaders[name2] = value2;
|
|
2290
2436
|
});
|
|
2291
2437
|
const stored = { ...model, headers: literalHeaders };
|
|
2292
2438
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -2359,8 +2505,8 @@ function installModel(runtime, connection, configured2, headers2 = {}) {
|
|
|
2359
2505
|
const contextWindow = configured2.contextWindow ?? builtin?.contextWindow;
|
|
2360
2506
|
const maxTokens = configured2.maxTokens ?? builtin?.maxTokens;
|
|
2361
2507
|
const baseUrl = connection.baseUrl || builtin?.baseUrl || runtime.getModels(connection.providerName)[0]?.baseUrl;
|
|
2362
|
-
if (!contextWindow || !maxTokens) throw new RuntimeFault("model_incomplete", "
|
|
2363
|
-
if (configured2.thinkingFormat && !FORMATS.has(configured2.thinkingFormat)) throw new RuntimeFault("
|
|
2508
|
+
if (!contextWindow || !maxTokens) throw new RuntimeFault("model_incomplete", "Fill in the model's context window and max output.", 422);
|
|
2509
|
+
if (configured2.thinkingFormat && !FORMATS.has(configured2.thinkingFormat)) throw new RuntimeFault("thinking_format", "This thinking format isn't supported.", 422);
|
|
2364
2510
|
const compat = { ...builtin?.compat, ...configured2.compat, ...configured2.thinkingFormat ? { thinkingFormat: configured2.thinkingFormat } : {} };
|
|
2365
2511
|
runtime.registerProvider(connection.providerName, {
|
|
2366
2512
|
baseUrl,
|
|
@@ -2382,7 +2528,7 @@ function installModel(runtime, connection, configured2, headers2 = {}) {
|
|
|
2382
2528
|
}]
|
|
2383
2529
|
});
|
|
2384
2530
|
const model = runtime.getModel(connection.providerName, configured2.id);
|
|
2385
|
-
if (!model) throw new RuntimeFault("model_missing", "
|
|
2531
|
+
if (!model) throw new RuntimeFault("model_missing", "Model not found", 404);
|
|
2386
2532
|
model.headers = { ...model.headers, ...headers2 };
|
|
2387
2533
|
return model;
|
|
2388
2534
|
}
|
|
@@ -2418,7 +2564,7 @@ var CredentialCoordinator = class {
|
|
|
2418
2564
|
tails = /* @__PURE__ */ new Map();
|
|
2419
2565
|
closing = false;
|
|
2420
2566
|
async exclusive(connectionId, action, signal) {
|
|
2421
|
-
if (this.closing) throw new RuntimeFault("stopping", "
|
|
2567
|
+
if (this.closing) throw new RuntimeFault("stopping", "Shutting down.");
|
|
2422
2568
|
signal?.throwIfAborted();
|
|
2423
2569
|
const prior = this.tails.get(connectionId) ?? Promise.resolve();
|
|
2424
2570
|
const operation = prior.then(() => {
|
|
@@ -2465,7 +2611,7 @@ var ConnectionCredentials = class {
|
|
|
2465
2611
|
options?.signal?.throwIfAborted();
|
|
2466
2612
|
if (providerId !== this.providerName) return void 0;
|
|
2467
2613
|
const row = this.repository.read(this.ownerId, this.connectionId);
|
|
2468
|
-
if (!row) throw new RuntimeFault("credentials_missing", "
|
|
2614
|
+
if (!row) throw new RuntimeFault("credentials_missing", "Set up this connection's credential first.", 422);
|
|
2469
2615
|
return row.value;
|
|
2470
2616
|
}
|
|
2471
2617
|
async list(options) {
|
|
@@ -2474,7 +2620,7 @@ var ConnectionCredentials = class {
|
|
|
2474
2620
|
return row ? [{ providerId: this.providerName, type: row.value.type }] : [];
|
|
2475
2621
|
}
|
|
2476
2622
|
async modify(providerId, fn, options) {
|
|
2477
|
-
if (providerId !== this.providerName) throw new RuntimeFault("credential_scope", "
|
|
2623
|
+
if (providerId !== this.providerName) throw new RuntimeFault("credential_scope", "The credential belongs to another connection.");
|
|
2478
2624
|
return this.coordinator.exclusive(this.connectionId, async () => {
|
|
2479
2625
|
const prior = this.repository.read(this.ownerId, this.connectionId);
|
|
2480
2626
|
const next = await fn(prior?.value);
|
|
@@ -2483,7 +2629,7 @@ var ConnectionCredentials = class {
|
|
|
2483
2629
|
}, options?.signal);
|
|
2484
2630
|
}
|
|
2485
2631
|
async delete(providerId, options) {
|
|
2486
|
-
if (providerId !== this.providerName) throw new RuntimeFault("credential_scope", "
|
|
2632
|
+
if (providerId !== this.providerName) throw new RuntimeFault("credential_scope", "The credential belongs to another connection.");
|
|
2487
2633
|
await this.coordinator.exclusive(this.connectionId, async () => {
|
|
2488
2634
|
this.repository.delete(this.ownerId, this.connectionId);
|
|
2489
2635
|
}, options?.signal);
|
|
@@ -2622,10 +2768,10 @@ function autoconfigure(modelId, api, choice) {
|
|
|
2622
2768
|
// src/server/infra/models/discovery.ts
|
|
2623
2769
|
async function discoverModels(input, signal) {
|
|
2624
2770
|
const api = String(input.api ?? "openai-completions");
|
|
2625
|
-
if (!["openai-completions", "openai-responses", "anthropic-messages", "google-generative-ai"].includes(api)) throw new RuntimeFault("discovery_protocol", "
|
|
2626
|
-
if (typeof input.baseUrl !== "string") throw new RuntimeFault("
|
|
2771
|
+
if (!["openai-completions", "openai-responses", "anthropic-messages", "google-generative-ai"].includes(api)) throw new RuntimeFault("discovery_protocol", "This protocol can't list models yet.", 400);
|
|
2772
|
+
if (typeof input.baseUrl !== "string") throw new RuntimeFault("discovery_url_missing", "Enter the API URL.", 400);
|
|
2627
2773
|
const url = new URL(input.baseUrl);
|
|
2628
|
-
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new RuntimeFault("discovery_url", "API URL
|
|
2774
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new RuntimeFault("discovery_url", "The API URL isn't valid.", 400);
|
|
2629
2775
|
const key = input.key;
|
|
2630
2776
|
let path = url.pathname.replace(/\/+$/, "");
|
|
2631
2777
|
if (!path.endsWith("/models")) {
|
|
@@ -2644,11 +2790,14 @@ async function discoverModels(input, signal) {
|
|
|
2644
2790
|
const models = /* @__PURE__ */ new Map();
|
|
2645
2791
|
let more = true;
|
|
2646
2792
|
while (more) {
|
|
2647
|
-
const response = await fetch(url, { headers: headers2, signal })
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
if (!
|
|
2793
|
+
const response = await fetch(url, { headers: headers2, signal }).catch((error) => {
|
|
2794
|
+
if (signal.aborted) throw error;
|
|
2795
|
+
throw new RuntimeFault("discovery_unreachable", `Couldn't reach ${url.host}.`, 502, { host: url.host });
|
|
2796
|
+
});
|
|
2797
|
+
if (!response.ok) throw new RuntimeFault("discovery_http", `Couldn't fetch the models: HTTP ${response.status}.`, 502, { status: response.status });
|
|
2798
|
+
const body = await response.json().catch(() => null);
|
|
2799
|
+
const items = api === "google-generative-ai" ? body?.models : body?.data;
|
|
2800
|
+
if (!body || !Array.isArray(items)) throw new RuntimeFault("discovery_format", "The response isn't a model list for this protocol.", 502);
|
|
2652
2801
|
for (const raw of items) {
|
|
2653
2802
|
if (!raw || typeof raw !== "object") continue;
|
|
2654
2803
|
const item = raw;
|
|
@@ -2733,26 +2882,26 @@ var ModelService = class {
|
|
|
2733
2882
|
return modelCatalog(credentials, connection.providerName);
|
|
2734
2883
|
}
|
|
2735
2884
|
discover(input, signal) {
|
|
2736
|
-
if (typeof input.baseUrl !== "string") throw new RuntimeFault("
|
|
2885
|
+
if (typeof input.baseUrl !== "string") throw new RuntimeFault("discovery_url_missing", "Enter the API URL.", 400);
|
|
2737
2886
|
let key = typeof input.apiKey === "string" ? input.apiKey.trim() : "";
|
|
2738
2887
|
if (!key && typeof input.connectionId === "string") {
|
|
2739
2888
|
this.required(this.store.getModelConnection(input.connectionId));
|
|
2740
2889
|
const credential = this.store.credentials.read(this.store.userMemberId(), input.connectionId)?.value;
|
|
2741
2890
|
if (credential?.type === "api_key") key = credential.key ?? "";
|
|
2742
2891
|
}
|
|
2743
|
-
if (!key) throw new RuntimeFault("discovery_key", "
|
|
2892
|
+
if (!key) throw new RuntimeFault("discovery_key", "Enter the API key.", 400);
|
|
2744
2893
|
return discoverModels({ api: String(input.api ?? "openai-completions"), baseUrl: input.baseUrl, key }, signal);
|
|
2745
2894
|
}
|
|
2746
2895
|
/** Suggests model fields from the local Pi catalog; the caller decides whether to save them. */
|
|
2747
2896
|
autoconfigure(input) {
|
|
2748
2897
|
const text3 = (value2) => typeof value2 === "string" && value2.trim() && value2.length <= 200 ? value2.trim() : null;
|
|
2749
2898
|
const modelId = text3(input.modelId), api = text3(input.api);
|
|
2750
|
-
if (!modelId || !api) throw new RuntimeFault("
|
|
2899
|
+
if (!modelId || !api) throw new RuntimeFault("autoconfig_model", "Enter the model ID.", 400);
|
|
2751
2900
|
let choice;
|
|
2752
2901
|
if (input.choice !== void 0) {
|
|
2753
2902
|
const raw = input.choice;
|
|
2754
2903
|
const provider = text3(raw?.provider), chosen = text3(raw?.modelId);
|
|
2755
|
-
if (!provider || !chosen) throw new RuntimeFault("
|
|
2904
|
+
if (!provider || !chosen) throw new RuntimeFault("autoconfig_source", "The setup source isn't valid.", 400);
|
|
2756
2905
|
choice = { provider, modelId: chosen };
|
|
2757
2906
|
}
|
|
2758
2907
|
return autoconfigure(modelId, api, choice);
|
|
@@ -2793,7 +2942,7 @@ var CredentialLogins = class {
|
|
|
2793
2942
|
flows = /* @__PURE__ */ new Map();
|
|
2794
2943
|
connection(id) {
|
|
2795
2944
|
const connection = this.store.getModelConnection(id);
|
|
2796
|
-
if (!connection) throw new RuntimeFault("connection_missing", "
|
|
2945
|
+
if (!connection) throw new RuntimeFault("connection_missing", "Connection not found", 404);
|
|
2797
2946
|
return connection;
|
|
2798
2947
|
}
|
|
2799
2948
|
status(id) {
|
|
@@ -2819,7 +2968,7 @@ var CredentialLogins = class {
|
|
|
2819
2968
|
flow.state = { phase: "completed" };
|
|
2820
2969
|
this.changed(id);
|
|
2821
2970
|
} catch (error) {
|
|
2822
|
-
flow.state = flow.controller.signal.aborted ? { phase: "cancelled" } : { phase: "error"
|
|
2971
|
+
flow.state = flow.controller.signal.aborted ? { phase: "cancelled" } : { phase: "error" };
|
|
2823
2972
|
if (!flow.controller.signal.aborted) console.error("[oauth] login failed", { connectionId: id, provider: connection.providerName, type: error instanceof Error ? error.constructor.name : "Error" });
|
|
2824
2973
|
} finally {
|
|
2825
2974
|
flow.answer = void 0;
|
|
@@ -2859,9 +3008,9 @@ var CredentialLogins = class {
|
|
|
2859
3008
|
input(id, body) {
|
|
2860
3009
|
this.connection(id);
|
|
2861
3010
|
const flow = this.flows.get(id), prompt = flow?.state.prompt;
|
|
2862
|
-
if (!flow?.answer || !prompt || body.promptId !== prompt.id) throw new RuntimeFault("login_step", "
|
|
2863
|
-
if (typeof body.value !== "string" || !body.value.trim()) throw new RuntimeFault("login_input", "
|
|
2864
|
-
if (prompt.options && !prompt.options.some((option2) => option2.id === body.value)) throw new RuntimeFault("
|
|
3011
|
+
if (!flow?.answer || !prompt || body.promptId !== prompt.id) throw new RuntimeFault("login_step", "The sign-in step has changed. Please start again.", 409);
|
|
3012
|
+
if (typeof body.value !== "string" || !body.value.trim()) throw new RuntimeFault("login_input", "Enter the sign-in details.", 400);
|
|
3013
|
+
if (prompt.options && !prompt.options.some((option2) => option2.id === body.value)) throw new RuntimeFault("login_option", "Pick a sign-in method.", 400);
|
|
2865
3014
|
flow.answer(body.value);
|
|
2866
3015
|
return this.status(id);
|
|
2867
3016
|
}
|
|
@@ -2928,10 +3077,10 @@ function findMentions(source2, candidates2) {
|
|
|
2928
3077
|
for (let at = lower.indexOf("@"); at >= 0; at = lower.indexOf("@", at + 1)) {
|
|
2929
3078
|
if (at > 0 && WORD.test(text3[at - 1])) continue;
|
|
2930
3079
|
const match = sorted.find((candidate) => {
|
|
2931
|
-
const
|
|
2932
|
-
if (!
|
|
2933
|
-
const next = text3[at + 1 +
|
|
2934
|
-
return !(next && WORD.test(next) && WORD.test(
|
|
3080
|
+
const name2 = candidate.name.toLowerCase();
|
|
3081
|
+
if (!name2 || !lower.startsWith(name2, at + 1)) return false;
|
|
3082
|
+
const next = text3[at + 1 + name2.length];
|
|
3083
|
+
return !(next && WORD.test(next) && WORD.test(name2.at(-1)));
|
|
2935
3084
|
});
|
|
2936
3085
|
if (match && !found.includes(match)) found.push(match);
|
|
2937
3086
|
}
|
|
@@ -3122,6 +3271,7 @@ var ChatMessages = class {
|
|
|
3122
3271
|
if (message.question) payload.push(message.question);
|
|
3123
3272
|
if (message.alsoToChannel) payload.push("alsoToChannel");
|
|
3124
3273
|
if (message.replyTo) payload.push({ replyTo: message.replyTo });
|
|
3274
|
+
if (message.quotes?.length) payload.push({ quotes: message.quotes });
|
|
3125
3275
|
const hash = createHash2("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
3126
3276
|
return this.repository.append(sender.id, this.id, message, hash, message.files, deliveries, mentions);
|
|
3127
3277
|
}
|
|
@@ -3188,7 +3338,7 @@ var ChatQuestions = class {
|
|
|
3188
3338
|
if (!question) return Promise.reject(new ChatError(409, "Not a question"));
|
|
3189
3339
|
if (question.state === "answered") return Promise.resolve(resultOf(question, question.answer));
|
|
3190
3340
|
if (question.state === "dismissed") return Promise.resolve({ status: "dismissed" });
|
|
3191
|
-
if (question.state === "ignored") return Promise.reject(new ChatError(409, "
|
|
3341
|
+
if (question.state === "ignored") return Promise.reject(new ChatError(409, "The question has expired", "question_expired"));
|
|
3192
3342
|
return new Promise((resolve6, reject) => {
|
|
3193
3343
|
const stop = () => {
|
|
3194
3344
|
this.waiting.delete(message.id);
|
|
@@ -3212,15 +3362,15 @@ var ChatQuestions = class {
|
|
|
3212
3362
|
this.repository.assertAccess(actorId, chatId, true);
|
|
3213
3363
|
if (message.sender.id === actorId) throw new ChatError(403, "A question is answered by the other members");
|
|
3214
3364
|
const question = message.question, waiter = this.waiting.get(messageId);
|
|
3215
|
-
if (question.state !== "pending") throw new ChatError(409, "
|
|
3365
|
+
if (question.state !== "pending") throw new ChatError(409, "The question is already settled", "question_closed");
|
|
3216
3366
|
if (!waiter) {
|
|
3217
3367
|
if (this.repository.settleQuestion(messageId, "ignored", null)) this.changed(chatId);
|
|
3218
|
-
throw new ChatError(409, "
|
|
3368
|
+
throw new ChatError(409, "The question has expired", "question_expired");
|
|
3219
3369
|
}
|
|
3220
3370
|
const dismissed = input.action === "dismiss";
|
|
3221
3371
|
if (!dismissed && input.action !== "answer") throw new ChatError(400, "Invalid question action");
|
|
3222
3372
|
const answer = dismissed ? null : answerOf(question, input);
|
|
3223
|
-
if (!this.repository.settleQuestion(messageId, dismissed ? "dismissed" : "answered", answer)) throw new ChatError(409, "
|
|
3373
|
+
if (!this.repository.settleQuestion(messageId, dismissed ? "dismissed" : "answered", answer)) throw new ChatError(409, "The question is already settled", "question_closed");
|
|
3224
3374
|
this.waiting.delete(messageId);
|
|
3225
3375
|
waiter(answer ? resultOf(question, answer) : { status: "dismissed" });
|
|
3226
3376
|
this.changed(chatId);
|
|
@@ -3236,9 +3386,9 @@ function text2(value2, label, max, required = false) {
|
|
|
3236
3386
|
}
|
|
3237
3387
|
var optional = (value2, label, max) => value2 === void 0 ? void 0 : text2(value2, label, max);
|
|
3238
3388
|
var nameOf = (value2, label) => {
|
|
3239
|
-
const
|
|
3240
|
-
if (/[\n\r]/.test(
|
|
3241
|
-
return
|
|
3389
|
+
const name2 = text2(value2, label, 80, true);
|
|
3390
|
+
if (/[\n\r]/.test(name2)) throw new ChatError(400, `Invalid ${label}`);
|
|
3391
|
+
return name2;
|
|
3242
3392
|
};
|
|
3243
3393
|
var ChatChannels = class {
|
|
3244
3394
|
constructor(repository, saved, sectionsChanged, delivered = () => {
|
|
@@ -3387,7 +3537,7 @@ var ChatThreads = class {
|
|
|
3387
3537
|
const chat = this.repository.getChat(actorId, threadId);
|
|
3388
3538
|
if (!chat || chat.type !== "thread") throw new ChatError(404, "Thread not found");
|
|
3389
3539
|
const root = chat.rootMessageId ? this.repository.message(actorId, chat.rootMessageId) : null;
|
|
3390
|
-
return { chat, info: this.store.info(chat.rootMessageId), root: root ? { senderId: root.sender.id, content: root.content } : null };
|
|
3540
|
+
return { chat, info: this.store.info(chat.rootMessageId), root: root ? { senderId: root.sender.id, content: root.content, quotes: root.quotes } : null };
|
|
3391
3541
|
}
|
|
3392
3542
|
list(actorId, channelId) {
|
|
3393
3543
|
return this.store.list(actorId, channelId).map((id) => this.entry(actorId, id));
|
|
@@ -3412,9 +3562,33 @@ var ChatThreads = class {
|
|
|
3412
3562
|
}
|
|
3413
3563
|
};
|
|
3414
3564
|
|
|
3565
|
+
// src/server/service/chat/quotes.ts
|
|
3566
|
+
var MAX_QUOTES = 10;
|
|
3567
|
+
function quoteDrafts(value2) {
|
|
3568
|
+
if (value2 === void 0 || value2 === null) return void 0;
|
|
3569
|
+
if (!Array.isArray(value2) || value2.length > MAX_QUOTES || !value2.every((item) => item && typeof item === "object" && !Array.isArray(item) && typeof item.messageId === "string" && /^m\d+$/.test(item.messageId) && typeof item.text === "string" && item.text.trim() && item.text.length <= 32e3 && Object.keys(item).every((key) => key === "messageId" || key === "text"))) throw new ChatError(400, "Invalid quotes");
|
|
3570
|
+
return value2.map((item) => ({ messageId: item.messageId, text: item.text.trim() }));
|
|
3571
|
+
}
|
|
3572
|
+
function resolveQuotes(repository, actorId, chatId, content2, drafts) {
|
|
3573
|
+
if (!drafts.length) return [];
|
|
3574
|
+
const marks = quoteMarks(content2);
|
|
3575
|
+
if (marks.length !== drafts.length || new Set(marks).size !== marks.length || !marks.every((n) => n >= 1 && n <= drafts.length))
|
|
3576
|
+
throw new ChatError(400, "Each quote needs exactly one [[qN]] mark");
|
|
3577
|
+
const chat = repository.getChat(actorId, chatId);
|
|
3578
|
+
if (!chat) throw new ChatError(404, "Chat not found");
|
|
3579
|
+
return drafts.map((draft) => {
|
|
3580
|
+
const source2 = repository.message(actorId, draft.messageId);
|
|
3581
|
+
if (!source2 || source2.chatId !== chatId && !(chat.type === "thread" && chat.rootMessageId === source2.id))
|
|
3582
|
+
throw new ChatError(400, "Quoted message is not in this chat");
|
|
3583
|
+
const passage = quoteComparable(draft.text);
|
|
3584
|
+
if (!passage || !quoteComparable(source2.content).includes(passage)) throw new ChatError(400, "Quoted text is not part of the message");
|
|
3585
|
+
return { messageId: source2.id, senderId: source2.sender.id, text: draft.text };
|
|
3586
|
+
});
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3415
3589
|
// src/server/service/chat/service.ts
|
|
3416
3590
|
function titleValue(value2) {
|
|
3417
|
-
if (value2 === void 0) return "
|
|
3591
|
+
if (value2 === void 0) return "";
|
|
3418
3592
|
if (typeof value2 !== "string" || !value2.trim() || value2.length > 100 || value2.includes("\0")) throw new ChatError(400, "Invalid Chat title");
|
|
3419
3593
|
return value2.trim();
|
|
3420
3594
|
}
|
|
@@ -3478,7 +3652,7 @@ var ChatService = class {
|
|
|
3478
3652
|
const owner = this.repository.channels.ownerOf(actorId), directory = this.repository.channels.directory(owner);
|
|
3479
3653
|
const actor = directory.find((member) => member.id === actorId), target = directory.find((member) => member.id === targetId);
|
|
3480
3654
|
if (!actor || !target) throw new ChatError(404, "Member not found");
|
|
3481
|
-
const chat = this.repository.openDirect(actorId, targetId, owner, `${actor.name}\
|
|
3655
|
+
const chat = this.repository.openDirect(actorId, targetId, owner, `${actor.name} \xB7 ${target.name}`);
|
|
3482
3656
|
this.notifySaved(chat.id);
|
|
3483
3657
|
return chat;
|
|
3484
3658
|
}
|
|
@@ -3506,14 +3680,21 @@ var ChatService = class {
|
|
|
3506
3680
|
this.channels.tellNotices(botId, ids);
|
|
3507
3681
|
}
|
|
3508
3682
|
/** Marks a message with an emoji, or removes the actor's mark. Its author is told (A39), as
|
|
3509
|
-
* routed and graded by `service/delivery` (A41). Returns the message as it is now.
|
|
3683
|
+
* routed and graded by `service/delivery` (A41). Returns the message as it is now.
|
|
3684
|
+
* Called with `on` for a mark the actor already has, the mark is taken off instead (the caller says so in the result). */
|
|
3685
|
+
/** Whether this member already has this emoji on this message (used by `messages.react` to report a toggle). */
|
|
3686
|
+
reactionOn(actorId, messageId, emoji) {
|
|
3687
|
+
return this.repository.interactions.reactionOn(actorId, messageId, emojiValue(emoji));
|
|
3688
|
+
}
|
|
3510
3689
|
react(actorId, messageId, emoji, on) {
|
|
3511
3690
|
if (typeof on !== "boolean") throw new ChatError(400, "Invalid reaction state");
|
|
3512
3691
|
const marked = this.repository.message(actorId, messageId);
|
|
3513
3692
|
if (!marked) throw new ChatError(404, "Message not found");
|
|
3693
|
+
const value2 = emojiValue(emoji), toggled = this.repository.interactions.reactionOn(actorId, messageId, value2);
|
|
3694
|
+
const apply = on === toggled ? !on : on;
|
|
3514
3695
|
const author = this.repository.canAccess(marked.sender.id, marked.chatId) ? marked.sender : void 0;
|
|
3515
|
-
const deliveries =
|
|
3516
|
-
const { chatId, added } = this.repository.interactions.react(actorId, messageId,
|
|
3696
|
+
const deliveries = apply ? plan("reaction", reactionTargets({ actorId, author })) : [];
|
|
3697
|
+
const { chatId, added } = this.repository.interactions.react(actorId, messageId, value2, apply, deliveries);
|
|
3517
3698
|
this.notifySaved(chatId);
|
|
3518
3699
|
if (added) this.dispatcher.dispatch(deliveries);
|
|
3519
3700
|
return this.repository.message(actorId, messageId);
|
|
@@ -3542,9 +3723,25 @@ var ChatService = class {
|
|
|
3542
3723
|
unreadMessages(actorId, chatId, limit) {
|
|
3543
3724
|
return this.repository.unreadMessages(actorId, chatId, limit);
|
|
3544
3725
|
}
|
|
3545
|
-
searchAll(actorId, query, limit = 30) {
|
|
3726
|
+
searchAll(actorId, query, limit = 30, within) {
|
|
3546
3727
|
if (!query.trim() || query.length > 500 || !Number.isInteger(limit) || limit < 1 || limit > 100) throw new ChatError(400, "Invalid search query or limit");
|
|
3547
|
-
|
|
3728
|
+
if (within) this.repository.assertAccess(actorId, within);
|
|
3729
|
+
return this.repository.searchAll(actorId, query.trim(), limit, within);
|
|
3730
|
+
}
|
|
3731
|
+
/** The search box: channels and threads by title, topic or notice / summary, and messages by text, with the chat
|
|
3732
|
+
* each message is in. `within` searches one chat (a channel with its threads) and returns no chats. */
|
|
3733
|
+
find(actorId, query, { within, limit = 50 } = {}) {
|
|
3734
|
+
const messages = this.searchAll(actorId, query, limit, within);
|
|
3735
|
+
const chats = within ? [] : this.repository.searchChatIds(actorId, query.trim(), 30).map((id) => this.repository.getChat(actorId, id)).filter((chat) => chat !== null);
|
|
3736
|
+
const where = /* @__PURE__ */ new Map();
|
|
3737
|
+
const chatOf = (id) => {
|
|
3738
|
+
if (!where.has(id)) where.set(id, this.repository.getChat(actorId, id));
|
|
3739
|
+
return where.get(id);
|
|
3740
|
+
};
|
|
3741
|
+
return { chats, messages: messages.flatMap((message) => {
|
|
3742
|
+
const chat = chatOf(message.chatId);
|
|
3743
|
+
return chat ? [{ message, chat }] : [];
|
|
3744
|
+
}) };
|
|
3548
3745
|
}
|
|
3549
3746
|
/** The newest `limit` messages before a sequence (null: the newest of all), oldest first, for scrolling back. */
|
|
3550
3747
|
readBefore(actorId, chatId, before, limit) {
|
|
@@ -3576,7 +3773,8 @@ var ChatService = class {
|
|
|
3576
3773
|
}
|
|
3577
3774
|
async send(actorId, chatId, input) {
|
|
3578
3775
|
if (input.alsoToChannel !== void 0 && typeof input.alsoToChannel !== "boolean") throw new ChatError(400, "Invalid alsoToChannel");
|
|
3579
|
-
|
|
3776
|
+
const draft = messageDraft(input), quotes = resolveQuotes(this.repository, actorId, chatId, draft.content, quoteDrafts(input.quotes) ?? []);
|
|
3777
|
+
return this.deliver(actorId, chatId, { ...draft, alsoToChannel: input.alsoToChannel === true, ...quotes.length ? { quotes } : {} });
|
|
3580
3778
|
}
|
|
3581
3779
|
async deliver(actorId, chatId, message) {
|
|
3582
3780
|
const files = message.files ?? [];
|
|
@@ -3592,7 +3790,8 @@ var ChatService = class {
|
|
|
3592
3790
|
files: message.files,
|
|
3593
3791
|
source: message.source,
|
|
3594
3792
|
...question ? { question } : {},
|
|
3595
|
-
...message.alsoToChannel ? { alsoToChannel: true } : {}
|
|
3793
|
+
...message.alsoToChannel ? { alsoToChannel: true } : {},
|
|
3794
|
+
...message.quotes?.length ? { quotes: message.quotes } : {}
|
|
3596
3795
|
});
|
|
3597
3796
|
}
|
|
3598
3797
|
message(actorId, id) {
|
|
@@ -3714,7 +3913,7 @@ var ChatFiles = class {
|
|
|
3714
3913
|
...chat.status ? { status: chat.status } : {}
|
|
3715
3914
|
},
|
|
3716
3915
|
sender: message.sender,
|
|
3717
|
-
content: message.content,
|
|
3916
|
+
content: expandQuotes(message.content, message.quotes, (memberId) => this.chats.member(memberId)?.name ?? memberId),
|
|
3718
3917
|
createdAt: message.createdAt,
|
|
3719
3918
|
attachments: await this.paths(botId, message, signal),
|
|
3720
3919
|
...this.reply(message)
|
|
@@ -3725,7 +3924,7 @@ var ChatFiles = class {
|
|
|
3725
3924
|
/** The quoted message as the envelope shows it: who wrote it and its first 200 characters. */
|
|
3726
3925
|
reply(message) {
|
|
3727
3926
|
const sender = message.replyTo ? this.chats.member(message.replyTo.senderId) : null;
|
|
3728
|
-
return message.replyTo && sender ? { replyTo: { id: message.replyTo.id, sender, text: message.replyTo
|
|
3927
|
+
return message.replyTo && sender ? { replyTo: { id: message.replyTo.id, sender, text: replyExcerpt(message.replyTo) } } : {};
|
|
3729
3928
|
}
|
|
3730
3929
|
async paths(actorId, message, signal) {
|
|
3731
3930
|
const paths = [];
|
|
@@ -3844,8 +4043,8 @@ var PiHistoryAdapter = class {
|
|
|
3844
4043
|
if (m.role === "system") {
|
|
3845
4044
|
if (m.sections) {
|
|
3846
4045
|
const sections = {};
|
|
3847
|
-
for (const [
|
|
3848
|
-
sections[
|
|
4046
|
+
for (const [name2, text3] of Object.entries(m.sections)) {
|
|
4047
|
+
sections[name2] = text3 === null ? null : message.content.length;
|
|
3849
4048
|
if (text3 !== null) message.content.push({ type: "text", text: text3 });
|
|
3850
4049
|
}
|
|
3851
4050
|
pi.sections = sections;
|
|
@@ -3853,9 +4052,9 @@ var PiHistoryAdapter = class {
|
|
|
3853
4052
|
if (m.toolsAdded) {
|
|
3854
4053
|
const toolProtocol = {};
|
|
3855
4054
|
message.tools = m.toolsAdded.map((tool, index) => {
|
|
3856
|
-
const { name, description, parameters, ...specific } = tool;
|
|
4055
|
+
const { name: name2, description, parameters, ...specific } = tool;
|
|
3857
4056
|
if (Object.keys(specific).length) toolProtocol[index] = value(specific);
|
|
3858
|
-
return { name, description, parameters: value(parameters) };
|
|
4057
|
+
return { name: name2, description, parameters: value(parameters) };
|
|
3859
4058
|
});
|
|
3860
4059
|
if (Object.keys(toolProtocol).length) pi.tools = toolProtocol;
|
|
3861
4060
|
}
|
|
@@ -3960,28 +4159,28 @@ var PiSession = class _PiSession {
|
|
|
3960
4159
|
await mkdir2(sessionsDir, { recursive: true, mode: 448 });
|
|
3961
4160
|
const expected = resolve3(sessionsDir) + sep;
|
|
3962
4161
|
if (!resolve3(reference.file).startsWith(expected) || dirname2(resolve3(reference.file)) !== resolve3(sessionsDir))
|
|
3963
|
-
throw new RuntimeFault("session_path", "
|
|
4162
|
+
throw new RuntimeFault("session_path", "The session file path isn't valid.");
|
|
3964
4163
|
const exists = existsSync(reference.file);
|
|
3965
4164
|
if (!exists && (reference.persisted || store.runtime.runs(bot.id).some((run) => run.state === "unknown")))
|
|
3966
|
-
throw new RuntimeFault("session_missing", "
|
|
4165
|
+
throw new RuntimeFault("session_missing", "The session file is missing; check the run records before recovering.");
|
|
3967
4166
|
if (exists) {
|
|
3968
4167
|
let header;
|
|
3969
4168
|
try {
|
|
3970
4169
|
header = JSON.parse(readFileSync(reference.file, "utf8").split("\n")[0]);
|
|
3971
4170
|
} catch {
|
|
3972
|
-
throw new RuntimeFault("session_corrupt", "
|
|
4171
|
+
throw new RuntimeFault("session_corrupt", "The session file can't be read.");
|
|
3973
4172
|
}
|
|
3974
4173
|
if (header.type !== "session" || !header.id || reference.sessionId && header.id !== reference.sessionId)
|
|
3975
|
-
throw new RuntimeFault("session_mismatch", "
|
|
4174
|
+
throw new RuntimeFault("session_mismatch", "The session file doesn't match this Bot.");
|
|
3976
4175
|
}
|
|
3977
4176
|
const connection = store.getModelConnection(selection.connectionId);
|
|
3978
|
-
if (!connection) throw new RuntimeFault("connection_missing", "
|
|
4177
|
+
if (!connection) throw new RuntimeFault("connection_missing", "Connection not found", 404);
|
|
3979
4178
|
const scoped = new ConnectionCredentials(store.credentials, options.credentials, store.userMemberId(), connection.id, connection.providerName);
|
|
3980
4179
|
await scoped.read(connection.providerName);
|
|
3981
4180
|
const credentials = new CurrentCredentials(scoped);
|
|
3982
4181
|
const models = await ModelRuntime3.create({ credentials, modelsPath: null, allowModelNetwork: false, refreshOnCreate: false });
|
|
3983
4182
|
const configured2 = connection.models.find((model2) => model2.id === selection.modelId);
|
|
3984
|
-
if (!configured2) throw new RuntimeFault("model_missing", "
|
|
4183
|
+
if (!configured2) throw new RuntimeFault("model_missing", "Model not found", 404);
|
|
3985
4184
|
const model = installModel(models, connection, configured2, store.modelHeaders(connection.id, configured2.id));
|
|
3986
4185
|
const sessionManager = SessionManager.open(reference.file, sessionsDir, bot.workspacePath);
|
|
3987
4186
|
const adapter = new _PiSession(options, models, credentials);
|
|
@@ -4026,9 +4225,9 @@ var PiSession = class _PiSession {
|
|
|
4026
4225
|
tools
|
|
4027
4226
|
});
|
|
4028
4227
|
adapter.session = result.session;
|
|
4029
|
-
if (result.extensionsResult.errors.length || !tools.every((
|
|
4228
|
+
if (result.extensionsResult.errors.length || !tools.every((name2) => adapter.session.getActiveToolNames().includes(name2))) {
|
|
4030
4229
|
adapter.session.dispose();
|
|
4031
|
-
throw new RuntimeFault("tools_missing", "
|
|
4230
|
+
throw new RuntimeFault("tools_missing", "Couldn't load the run's tools.");
|
|
4032
4231
|
}
|
|
4033
4232
|
adapter.unsubscribe = adapter.session.subscribe((event) => adapter.onEvent(event));
|
|
4034
4233
|
if (options.onHistory) {
|
|
@@ -4090,7 +4289,7 @@ var PiSession = class _PiSession {
|
|
|
4090
4289
|
* request that follows a tool cancelled mid-run. Apply the same signal rule to those. */
|
|
4091
4290
|
classify(message) {
|
|
4092
4291
|
if (message.stopReason === "aborted" || message.stopReason === "error" && this.session.agent.signal?.aborted) return { outcome: "interrupted" };
|
|
4093
|
-
if (message.stopReason === "error") return { outcome: "failed", error: message.errorMessage?.trim() || "
|
|
4292
|
+
if (message.stopReason === "error") return { outcome: "failed", error: message.errorMessage?.trim() || "Unknown error" };
|
|
4094
4293
|
return { outcome: "completed" };
|
|
4095
4294
|
}
|
|
4096
4295
|
maybeAbort() {
|
|
@@ -4139,7 +4338,7 @@ var PiSession = class _PiSession {
|
|
|
4139
4338
|
}
|
|
4140
4339
|
async configure(selection) {
|
|
4141
4340
|
const connection = this.options.store.getModelConnection(selection.connectionId), configured2 = connection?.models.find((item) => item.id === selection.modelId);
|
|
4142
|
-
if (!connection || !configured2) throw new RuntimeFault("model_missing", "
|
|
4341
|
+
if (!connection || !configured2) throw new RuntimeFault("model_missing", "Model not found", 404);
|
|
4143
4342
|
const signature = JSON.stringify([{ connectionId: selection.connectionId, modelId: selection.modelId }, connection.providerName, connection.api, connection.baseUrl, configured2, this.options.store.modelHeaders(connection.id, configured2.id)]);
|
|
4144
4343
|
if (signature === this.signature) {
|
|
4145
4344
|
this.session.setThinkingLevel(resolveThinking(configured2, selection.thinkingLevel), { persist: false });
|
|
@@ -4157,7 +4356,7 @@ var PiSession = class _PiSession {
|
|
|
4157
4356
|
return this.session.thinkingLevel;
|
|
4158
4357
|
} catch {
|
|
4159
4358
|
this.credentials.current = previous;
|
|
4160
|
-
throw new RuntimeFault("model_switch_uncertain", "
|
|
4359
|
+
throw new RuntimeFault("model_switch_uncertain", "The model switch didn't finish. Pick the model again.", 409);
|
|
4161
4360
|
}
|
|
4162
4361
|
}
|
|
4163
4362
|
async dispose() {
|
|
@@ -4176,14 +4375,12 @@ import { chmodSync as chmodSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
|
4176
4375
|
import { dirname as dirname3 } from "node:path";
|
|
4177
4376
|
var json = (value2) => value2 == null ? null : JSON.stringify(value2);
|
|
4178
4377
|
var CALL = "EXISTS (SELECT 1 FROM json_each(history.message, '$.content') b WHERE json_extract(b.value, '$.type')=? AND json_extract(b.value, '$.tool_call_id')=?)";
|
|
4179
|
-
var
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
WHEN 'image' THEN '\u56FE\u7247' END, ' '),1,160)
|
|
4186
|
-
FROM json_each(history.message,'$.content') b), '') AS preview FROM history`;
|
|
4378
|
+
var BLOCKS = "FROM json_each(history.message,'$.content') b WHERE json_extract(b.value,'$.type')";
|
|
4379
|
+
var SUMMARY = `SELECT id,timestamp,role,model,
|
|
4380
|
+
COALESCE((SELECT substr(group_concat(substr(json_extract(b.value,'$.text'),1,160),' '),1,160) ${BLOCKS} IN ('text','reasoning')),'') AS preview,
|
|
4381
|
+
(SELECT json_group_array(json_object('kind',CASE json_extract(b.value,'$.type') WHEN 'tool_call' THEN 'call' ELSE 'result' END,
|
|
4382
|
+
'name',json_extract(b.value,'$.name'),'error',json_extract(b.value,'$.is_error'))) ${BLOCKS} IN ('tool_call','tool_result')) AS tools,
|
|
4383
|
+
(SELECT count(*) ${BLOCKS}='image') AS images FROM history`;
|
|
4187
4384
|
var HistoryRepository = class {
|
|
4188
4385
|
db;
|
|
4189
4386
|
constructor(path) {
|
|
@@ -4225,8 +4422,19 @@ var HistoryRepository = class {
|
|
|
4225
4422
|
return row ? { ...row, model: row.model === null ? null : JSON.parse(row.model), message: JSON.parse(row.message) } : null;
|
|
4226
4423
|
});
|
|
4227
4424
|
}
|
|
4425
|
+
/** Model calls since a time, per provider and model: every assistant record with usage is one call. */
|
|
4426
|
+
usage(since) {
|
|
4427
|
+
const rows2 = this.db.prepare(`SELECT json_extract(model,'$.provider') AS provider,json_extract(model,'$.id') AS model,count(*) AS calls,
|
|
4428
|
+
COALESCE(SUM(json_extract(usage,'$.input_tokens')),0) AS input,COALESCE(SUM(json_extract(usage,'$.output_tokens')),0) AS output,
|
|
4429
|
+
COALESCE(SUM(json_extract(usage,'$.cache_read_tokens')),0) AS cacheRead,COALESCE(SUM(json_extract(usage,'$.cache_write_tokens')),0) AS cacheWrite,
|
|
4430
|
+
COALESCE(SUM(json_extract(usage,'$.cost.total')),0) AS cost,COALESCE(MAX(json_extract(usage,'$.cost.total')),0)>0 AS priced
|
|
4431
|
+
FROM history WHERE role='assistant' AND usage IS NOT NULL AND model IS NOT NULL AND timestamp>=?
|
|
4432
|
+
GROUP BY 1,2 ORDER BY cost DESC,calls DESC`).all(since);
|
|
4433
|
+
return rows2.map(({ priced, cost, ...row }) => ({ ...row, cost: priced ? cost : null }));
|
|
4434
|
+
}
|
|
4228
4435
|
decodeSummary(row) {
|
|
4229
|
-
|
|
4436
|
+
const tools = JSON.parse(row.tools).map(({ kind, name: name2, error }) => error ? { kind, name: name2, error: true } : { kind, name: name2 });
|
|
4437
|
+
return { ...row, model: row.model === null ? null : JSON.parse(row.model), preview: row.preview.replace(/\s+/g, " ").trim(), tools };
|
|
4230
4438
|
}
|
|
4231
4439
|
read(id) {
|
|
4232
4440
|
const row = this.db.prepare("SELECT * FROM history WHERE id=?").get(id);
|
|
@@ -4257,6 +4465,11 @@ var HistoryRepository = class {
|
|
|
4257
4465
|
};
|
|
4258
4466
|
|
|
4259
4467
|
// src/server/service/bot/memory.ts
|
|
4468
|
+
function summaryLine({ tools, images, ...summary }) {
|
|
4469
|
+
const steps = tools.map((tool) => tool.kind === "call" ? `Called ${tool.name}` : `${tool.name} \xB7 ${tool.error ? "failed" : "result"}`);
|
|
4470
|
+
const line = [summary.preview, ...steps, ...Array.from({ length: images }, () => "image")].filter(Boolean).join(" ");
|
|
4471
|
+
return { ...summary, preview: line.slice(0, 160).replace(/\s+/g, " ").trim() };
|
|
4472
|
+
}
|
|
4260
4473
|
var Memory = class {
|
|
4261
4474
|
constructor(historyPath) {
|
|
4262
4475
|
this.historyPath = historyPath;
|
|
@@ -4277,12 +4490,15 @@ var Memory = class {
|
|
|
4277
4490
|
timeline(before, limit) {
|
|
4278
4491
|
return this.history().timeline(before, limit);
|
|
4279
4492
|
}
|
|
4493
|
+
usage(since) {
|
|
4494
|
+
return this.history().usage(since);
|
|
4495
|
+
}
|
|
4280
4496
|
read(id) {
|
|
4281
4497
|
const record = this.history().read(id);
|
|
4282
4498
|
return record ? { record, links: this.history().links(record) } : null;
|
|
4283
4499
|
}
|
|
4284
4500
|
readMany(ids, mode) {
|
|
4285
|
-
const found = mode === "summary" ? this.history().summaries(ids) : this.history().details(ids);
|
|
4501
|
+
const found = mode === "summary" ? this.history().summaries(ids).map((item) => item && summaryLine(item)) : this.history().details(ids);
|
|
4286
4502
|
const result = { mode, records: [], missingIds: [] };
|
|
4287
4503
|
found.forEach((record, index) => {
|
|
4288
4504
|
if (record) result.records.push(record);
|
|
@@ -4461,7 +4677,7 @@ var Model = class {
|
|
|
4461
4677
|
}
|
|
4462
4678
|
current() {
|
|
4463
4679
|
const bot = this.store.getBot(this.botId);
|
|
4464
|
-
if (!bot.connectionId || !bot.modelId) throw new RuntimeFault("model_required", "
|
|
4680
|
+
if (!bot.connectionId || !bot.modelId) throw new RuntimeFault("model_required", "Pick a model for the Bot first.", 422);
|
|
4465
4681
|
return { selection: { connectionId: bot.connectionId, modelId: bot.modelId, thinkingLevel: bot.thinkingLevel }, credentialRef: bot.connectionId };
|
|
4466
4682
|
}
|
|
4467
4683
|
async prepare(input) {
|
|
@@ -4491,9 +4707,9 @@ var LocalWorkspace = class {
|
|
|
4491
4707
|
async prepare() {
|
|
4492
4708
|
await mkdir3(this.tempDir, { recursive: true, mode: 448 });
|
|
4493
4709
|
}
|
|
4494
|
-
async writeTempFile(
|
|
4710
|
+
async writeTempFile(name2, content2, signal) {
|
|
4495
4711
|
await this.prepare();
|
|
4496
|
-
const path = join4(this.tempDir,
|
|
4712
|
+
const path = join4(this.tempDir, name2);
|
|
4497
4713
|
await writeFile(path, content2, { encoding: "utf8", flag: "wx", mode: 384, signal });
|
|
4498
4714
|
return path;
|
|
4499
4715
|
}
|
|
@@ -4508,8 +4724,8 @@ var Workspace = class {
|
|
|
4508
4724
|
prepare() {
|
|
4509
4725
|
return this.local.prepare();
|
|
4510
4726
|
}
|
|
4511
|
-
writeTempFile(
|
|
4512
|
-
return this.local.writeTempFile(
|
|
4727
|
+
writeTempFile(name2, content2, signal) {
|
|
4728
|
+
return this.local.writeTempFile(name2, content2, signal);
|
|
4513
4729
|
}
|
|
4514
4730
|
location() {
|
|
4515
4731
|
return { type: "local", cwd: this.local.cwd, tempDir: this.local.tempDir };
|
|
@@ -4605,10 +4821,10 @@ ${result.missingIds.join(", ")}`);
|
|
|
4605
4821
|
|
|
4606
4822
|
// src/server/service/bot/history-read.ts
|
|
4607
4823
|
var historyReadSchema = Type2.Object({
|
|
4608
|
-
ids: Type2.Array(Type2.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }), { description: "
|
|
4609
|
-
mode: Type2.Optional(Type2.Union([Type2.Literal("summary"), Type2.Literal("detail")], { default: "summary", description: "summary
|
|
4824
|
+
ids: Type2.Array(Type2.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }), { description: "IDs in your own History, read in the order given." }),
|
|
4825
|
+
mode: Type2.Optional(Type2.Union([Type2.Literal("summary"), Type2.Literal("detail")], { default: "summary", description: "summary returns an excerpt of each record; detail returns the full message content, without usage or adapter extras." })),
|
|
4610
4826
|
format: Type2.Optional(Type2.Union([Type2.Literal("json"), Type2.Literal("markdown")], { default: "json" })),
|
|
4611
|
-
to_file: Type2.Optional(Type2.Boolean({ default: false, description: "
|
|
4827
|
+
to_file: Type2.Optional(Type2.Boolean({ default: false, description: "Write the result to your temporary directory and return only its path; false returns the content directly." }))
|
|
4612
4828
|
}, { additionalProperties: false });
|
|
4613
4829
|
async function readHistory(memory, workspace, input, signal) {
|
|
4614
4830
|
signal?.throwIfAborted();
|
|
@@ -4617,12 +4833,13 @@ async function readHistory(memory, workspace, input, signal) {
|
|
|
4617
4833
|
const rendered = formatHistory(result, format);
|
|
4618
4834
|
signal?.throwIfAborted();
|
|
4619
4835
|
if (!input.to_file) return rendered;
|
|
4620
|
-
const
|
|
4621
|
-
return workspace.writeTempFile(
|
|
4836
|
+
const name2 = `history-${randomUUID10()}.${format === "json" ? "json" : "md"}`;
|
|
4837
|
+
return workspace.writeTempFile(name2, rendered, signal);
|
|
4622
4838
|
}
|
|
4623
4839
|
|
|
4624
4840
|
// src/server/service/bot/chat-views.ts
|
|
4625
4841
|
var iso = (time) => new Date(time).toISOString();
|
|
4842
|
+
var name = (chats) => (memberId) => chats.member(memberId)?.name ?? memberId;
|
|
4626
4843
|
function chatRows(chats, botId) {
|
|
4627
4844
|
const sections = new Map(chats.channels.sections(botId).map((section) => [section.id, section.name]));
|
|
4628
4845
|
const row = (chat) => ({
|
|
@@ -4656,7 +4873,7 @@ function chatDetail(chats, botId, chat) {
|
|
|
4656
4873
|
parent: chat.parentId,
|
|
4657
4874
|
status: chat.status,
|
|
4658
4875
|
following: chat.following,
|
|
4659
|
-
root: root ? { id: root.id, from: root.sender.name, content: root.content, at: iso(root.createdAt) } : null
|
|
4876
|
+
root: root ? { id: root.id, from: root.sender.name, content: expandQuotes(root.content, root.quotes, name(chats)), at: iso(root.createdAt) } : null
|
|
4660
4877
|
} : {},
|
|
4661
4878
|
members,
|
|
4662
4879
|
notice: chat.note ? { text: chat.note.text, version: chat.note.version, by: chat.note.updatedBy.name, at: iso(chat.note.updatedAt) } : null,
|
|
@@ -4672,7 +4889,7 @@ function threadRow(chats, entry) {
|
|
|
4672
4889
|
status: entry.chat.status,
|
|
4673
4890
|
replies: entry.info.replies,
|
|
4674
4891
|
...entry.info.lastReplyAt ? { lastReplyAt: iso(entry.info.lastReplyAt) } : {},
|
|
4675
|
-
root: entry.root ? { id: entry.chat.rootMessageId, from: chats.member(entry.root.senderId)?.name ?? entry.root.senderId, preview: entry.root.content.slice(0, 200) } : null,
|
|
4892
|
+
root: entry.root ? { id: entry.chat.rootMessageId, from: chats.member(entry.root.senderId)?.name ?? entry.root.senderId, preview: previewQuotes(entry.root.content, entry.root.quotes).slice(0, 200) } : null,
|
|
4676
4893
|
following: entry.chat.following,
|
|
4677
4894
|
subscribed: entry.chat.subscribed,
|
|
4678
4895
|
unread: entry.chat.unread
|
|
@@ -4686,12 +4903,12 @@ function messageRow(chats, botId, message, attachments, chat) {
|
|
|
4686
4903
|
...chat ? { chat: { id: chat.id, type: chat.type, title: chat.title } } : {},
|
|
4687
4904
|
from: message.sender.name,
|
|
4688
4905
|
at: iso(message.createdAt),
|
|
4689
|
-
text: message.content,
|
|
4906
|
+
text: expandQuotes(message.content, message.quotes, name(chats)),
|
|
4690
4907
|
...attachments.length ? { attachments } : {},
|
|
4691
4908
|
...message.mentions.length ? { mentions: message.mentions.map((id) => chats.member(id)?.name ?? id) } : {},
|
|
4692
4909
|
...message.thread ? { thread: { id: message.thread.id, title: message.thread.title, status: message.thread.status, replies: message.thread.replies } } : {},
|
|
4693
4910
|
...message.echoOf ? { fromThread: message.echoOf } : {},
|
|
4694
|
-
...message.replyTo ? { replyTo: { id: message.replyTo.id, from: chats.member(message.replyTo.senderId)?.name ?? message.replyTo.senderId, text: message.replyTo
|
|
4911
|
+
...message.replyTo ? { replyTo: { id: message.replyTo.id, from: chats.member(message.replyTo.senderId)?.name ?? message.replyTo.senderId, text: replyExcerpt(message.replyTo) } } : {},
|
|
4695
4912
|
...message.reactions.length ? { reactions: message.reactions.map((group) => ({
|
|
4696
4913
|
emoji: group.emoji,
|
|
4697
4914
|
by: group.memberIds.map((id) => chats.member(id)?.name ?? id)
|
|
@@ -4725,7 +4942,7 @@ var ChatTools = class {
|
|
|
4725
4942
|
/** Members by name (case-insensitive, a leading @ allowed); unknown names are an error that lists them. */
|
|
4726
4943
|
members(names) {
|
|
4727
4944
|
const directory = this.chats.channels.directory(this.bot);
|
|
4728
|
-
const found = names.map((
|
|
4945
|
+
const found = names.map((name2) => ({ name: name2, member: directory.find((member) => same(name2, member.name)) }));
|
|
4729
4946
|
const missing = found.filter((item) => !item.member).map((item) => item.name);
|
|
4730
4947
|
if (missing.length) throw new ChatError(404, `No member named: ${missing.join(", ")} (see members.list)`);
|
|
4731
4948
|
return found.map((item) => item.member);
|
|
@@ -4869,33 +5086,34 @@ var None = Type3.Object({}, { additionalProperties: false });
|
|
|
4869
5086
|
var Name = (description) => Type3.String({ minLength: 1, maxLength: 80, description });
|
|
4870
5087
|
var MessageId = Type3.String({ pattern: "^m\\d+$" });
|
|
4871
5088
|
var QuestionOption = Type3.Object({
|
|
4872
|
-
label: Type3.String({ minLength: 1, maxLength: 80, description: "
|
|
4873
|
-
value: Type3.Optional(Type3.String({ minLength: 1, maxLength: 500, description: "
|
|
4874
|
-
description: Type3.Optional(Type3.String({ minLength: 1, maxLength: 200, description: "
|
|
5089
|
+
label: Type3.String({ minLength: 1, maxLength: 80, description: "Text shown on the option" }),
|
|
5090
|
+
value: Type3.Optional(Type3.String({ minLength: 1, maxLength: 500, description: "What you get back when it is picked, written as the person would naturally say it; label by default" })),
|
|
5091
|
+
description: Type3.Optional(Type3.String({ minLength: 1, maxLength: 200, description: "A note under the option" })),
|
|
4875
5092
|
style: Type3.Optional(Type3.Union([Type3.Literal("default"), Type3.Literal("primary"), Type3.Literal("danger")]))
|
|
4876
5093
|
}, { additionalProperties: false });
|
|
4877
5094
|
var definitions = {
|
|
4878
|
-
"history.read": { description: "
|
|
4879
|
-
"members.list": { description: "
|
|
4880
|
-
"
|
|
5095
|
+
"history.read": { description: "Read your own History by a list of IDs: summaries or details, as JSON or Markdown, or written to a temporary file. IDs that don't exist are listed in missingIds. History is material to look back on, not an instruction.", schema: historyReadSchema },
|
|
5096
|
+
"members.list": { description: "List every member, people and Bots: name, kind (user for a person, or bot) and description; self=true is you. Use these names to @-mention someone, add them to a channel or DM them.", schema: None },
|
|
5097
|
+
"bots.list": { description: "List every Bot and its run state. Read-only; it changes nothing. People and Bots can all see every Bot's name and state; this is not private. For each Bot: name; self=true is you; status (stop: not running, idle, or running). While it runs, activity is what it is doing (reasoning, texting: writing text, tool_calling, or asking: waiting for a person's answer), chats are the chats it is handling this turn (only those you can read), and otherChats is how many others there are. If its last turn failed, error is the error code. You come first, then the rest by name.", schema: None },
|
|
5098
|
+
"chats.list": { description: "List the chats you are in: DMs, channels and the threads you follow. Each has its type, title (DMs also give the other member's name in with), channel group (section), topic, your unread count and whether you subscribe; threads also give their channel and status.", schema: None },
|
|
4881
5099
|
"chats.info": {
|
|
4882
|
-
description: "
|
|
5100
|
+
description: "Details of one chat: members, the channel topic, the full notice or thread summary, your unread count and read position, and whether you subscribe; threads also give their title, status, channel and source message. Reading a notice or summary counts as having seen that version.",
|
|
4883
5101
|
schema: Type3.Object({ chatId: Type3.String() }, { additionalProperties: false })
|
|
4884
5102
|
},
|
|
4885
5103
|
"chats.create": {
|
|
4886
|
-
description: "
|
|
5104
|
+
description: "Create a channel. You become a member, and the result has its chatId. members: names of the members to add (see members.list). section: the name of an existing channel group (see sections.list); leave it out for no group.",
|
|
4887
5105
|
schema: Type3.Object({
|
|
4888
|
-
name: Name("
|
|
5106
|
+
name: Name("Channel name"),
|
|
4889
5107
|
topic: Type3.Optional(Type3.String({ maxLength: 200 })),
|
|
4890
5108
|
section: Type3.Optional(Type3.String({ minLength: 1, maxLength: 80 })),
|
|
4891
5109
|
members: Type3.Optional(Type3.Array(Type3.String({ minLength: 1, maxLength: 80 }), { maxItems: 50 }))
|
|
4892
5110
|
}, { additionalProperties: false })
|
|
4893
5111
|
},
|
|
4894
5112
|
"chats.update": {
|
|
4895
|
-
description: "
|
|
5113
|
+
description: "Change a chat you are in. Channels: name, topic, section (a channel group's name; an empty string takes it out of its group) and notice. Threads: title, status (open or done) and notice (the summary). notice is Markdown and replaces the whole text.",
|
|
4896
5114
|
schema: Type3.Object({
|
|
4897
5115
|
chatId: Type3.String(),
|
|
4898
|
-
name: Type3.Optional(Name("
|
|
5116
|
+
name: Type3.Optional(Name("Channel name")),
|
|
4899
5117
|
topic: Type3.Optional(Type3.String({ maxLength: 200 })),
|
|
4900
5118
|
section: Type3.Optional(Type3.String({ maxLength: 80 })),
|
|
4901
5119
|
notice: Type3.Optional(Type3.String({ minLength: 1, maxLength: 2e4 })),
|
|
@@ -4904,29 +5122,29 @@ var definitions = {
|
|
|
4904
5122
|
}, { additionalProperties: false })
|
|
4905
5123
|
},
|
|
4906
5124
|
"chats.addMembers": {
|
|
4907
|
-
description: "
|
|
5125
|
+
description: "Add members to a channel you are in, by name (see members.list). Only people can remove members from a channel or delete channels and channel groups.",
|
|
4908
5126
|
schema: Type3.Object({ chatId: Type3.String(), members: Type3.Array(Type3.String({ minLength: 1, maxLength: 80 }), { minItems: 1, maxItems: 50 }) }, { additionalProperties: false })
|
|
4909
5127
|
},
|
|
4910
5128
|
"chats.subscribe": {
|
|
4911
|
-
description: "
|
|
5129
|
+
description: "Subscribe to a channel or thread, or stop. Normally you only get new messages in channels and threads when you are @-mentioned. While you subscribe, every new message there (with or without an @, whoever it mentions, from a person or a Bot) is sent to you at once and interrupts what you are doing. Subscribe only while you need to follow something live, and stop when you are done; you don't have to reply to every message you get this way. Subscribing to a channel doesn't include its threads; subscribing to a thread also follows it.",
|
|
4912
5130
|
schema: Type3.Object({ chatId: Type3.String(), subscribed: Type3.Boolean() }, { additionalProperties: false })
|
|
4913
5131
|
},
|
|
4914
5132
|
"threads.list": {
|
|
4915
|
-
description: "
|
|
4916
|
-
schema: Type3.Object({ chatId: Type3.String({ description: "
|
|
5133
|
+
description: "List a channel's threads: title, status, reply count, time of the last reply, source message, and whether you follow or subscribe. status is open by default; done and all also work.",
|
|
5134
|
+
schema: Type3.Object({ chatId: Type3.String({ description: "Channel ID" }), status: Type3.Optional(Type3.Union([Type3.Literal("open"), Type3.Literal("done"), Type3.Literal("all")])) }, { additionalProperties: false })
|
|
4917
5135
|
},
|
|
4918
5136
|
"threads.follow": {
|
|
4919
|
-
description: "
|
|
4920
|
-
schema: Type3.Object({ chatId: Type3.String({ description: "
|
|
5137
|
+
description: "Follow or unfollow a thread. Followed threads appear in chats.list, and their new replies count as unread for you without interrupting you (use chats.subscribe to get them live). Posting in a thread or being @-mentioned in it follows it for you.",
|
|
5138
|
+
schema: Type3.Object({ chatId: Type3.String({ description: "Thread ID" }), following: Type3.Boolean() }, { additionalProperties: false })
|
|
4921
5139
|
},
|
|
4922
|
-
"sections.list": { description: "
|
|
4923
|
-
"sections.create": { description: "
|
|
5140
|
+
"sections.list": { description: "List the channel groups (sections) with their IDs and names.", schema: None },
|
|
5141
|
+
"sections.create": { description: "Create a channel group.", schema: Type3.Object({ name: Name("Group name") }, { additionalProperties: false }) },
|
|
4924
5142
|
"sections.update": {
|
|
4925
|
-
description: "
|
|
4926
|
-
schema: Type3.Object({ section: Type3.String({ minLength: 1, maxLength: 80 }), name: Name("
|
|
5143
|
+
description: "Rename a channel group. section: its current name or ID.",
|
|
5144
|
+
schema: Type3.Object({ section: Type3.String({ minLength: 1, maxLength: 80 }), name: Name("New name") }, { additionalProperties: false })
|
|
4927
5145
|
},
|
|
4928
5146
|
"messages.read": {
|
|
4929
|
-
description: "\
|
|
5147
|
+
description: "Read messages and the paths of the attachments you can read. Three ways: \u2460 chatId + after: read on from that sequence number (from the start by default); \u2461 around with a message ID: that message and context messages on each side (5 by default), to see the context; this doesn't move your read position; \u2462 unread=true: your unread messages, in one chat with chatId, or in every chat (grouped by chat) without it. \u2460 and \u2462 move your read position to the last message read; hasMore=true means there is more, so call again.",
|
|
4930
5148
|
schema: Type3.Object({
|
|
4931
5149
|
chatId: Type3.Optional(Type3.String()),
|
|
4932
5150
|
after: Type3.Optional(Type3.Integer({ minimum: 0 })),
|
|
@@ -4937,15 +5155,15 @@ var definitions = {
|
|
|
4937
5155
|
}, { additionalProperties: false })
|
|
4938
5156
|
},
|
|
4939
5157
|
"messages.search": {
|
|
4940
|
-
description: "
|
|
5158
|
+
description: "Search message text. With chatId, only that chat; without it, every chat you can see, and each result says which chat it is in. Use messages.read to read attachments.",
|
|
4941
5159
|
schema: Type3.Object({ chatId: Type3.Optional(Type3.String()), query: Type3.String({ minLength: 1, maxLength: 500 }), limit: Type3.Optional(Type3.Integer({ minimum: 1, maximum: 100 })) }, { additionalProperties: false })
|
|
4942
5160
|
},
|
|
4943
5161
|
"messages.react": {
|
|
4944
|
-
description:
|
|
4945
|
-
schema: Type3.Object({ messageId: MessageId, emoji: Type3.String({ minLength: 1, maxLength: 32, description: "
|
|
5162
|
+
description: `Add or remove an emoji reaction on a message (people and Bots can both react). on=false removes it. Giving an emoji the message already has from you removes it instead (and the other way round); marked or unmarked and note in the result say which happened. Use this toggle to signal states such as "got it", "on it" and "done". Only the message's author is told: if they are running, it joins their current turn without interrupting; if they are idle, it starts a turn.`,
|
|
5163
|
+
schema: Type3.Object({ messageId: MessageId, emoji: Type3.String({ minLength: 1, maxLength: 32, description: "A single emoji, such as \u{1F44D}" }), on: Type3.Optional(Type3.Boolean()) }, { additionalProperties: false })
|
|
4946
5164
|
},
|
|
4947
5165
|
"messages.ask": {
|
|
4948
|
-
description: '
|
|
5166
|
+
description: `Post a multiple-choice card in a chat you are in and wait for the person's answer. It returns {status:"answered", selected:[{label,value}], custom}, or {status:"dismissed"} if they closed the question without answering. Use it when someone needs to choose or confirm (a plan, a preference, whether to go on with something risky); ask open questions with messages.send. options: 1\u20136; multiple allows several choices; allowCustom lets them write their own answer (custom). If, while you wait, the person sends a new message, cancels this turn or stops the Bot, the call is interrupted and the new message arrives in the next batch. Only people can answer, so it can't be used in chats without one (such as a DM between Bots). Never use it to ask for passwords or keys.`,
|
|
4949
5167
|
schema: Type3.Object({
|
|
4950
5168
|
chatId: Type3.String(),
|
|
4951
5169
|
question: Type3.String({ minLength: 1, maxLength: 500 }),
|
|
@@ -4956,7 +5174,7 @@ var definitions = {
|
|
|
4956
5174
|
}, { additionalProperties: false })
|
|
4957
5175
|
},
|
|
4958
5176
|
"messages.send": {
|
|
4959
|
-
description: "
|
|
5177
|
+
description: "Send a message, with files from the tool host if you like (path and name). Give chatId or to, not both: to is a member's name and sends them a DM (a person or a Bot; the first time creates the DM). Only messages sent successfully are visible in the chat. In channels and threads, only the members whose @name is in the text and the members subscribed there receive it; DMs need no @. The result lists who was @-mentioned (notified), who got it through a subscription (subscribers), and the names that aren't in the channel and weren't told. thread: the ID of a message in the channel, to reply in that message's thread (created if it has none; chatId is the channel). replyTo: the ID of a message in the same chat, to reply to it as a quote; this tells its author, like an @. When replying in a thread, alsoToChannel=true also posts a copy to the channel.",
|
|
4960
5178
|
schema: Type3.Object({
|
|
4961
5179
|
chatId: Type3.Optional(Type3.String()),
|
|
4962
5180
|
to: Type3.Optional(Type3.String({ minLength: 1, maxLength: 80 })),
|
|
@@ -4969,6 +5187,31 @@ var definitions = {
|
|
|
4969
5187
|
}
|
|
4970
5188
|
};
|
|
4971
5189
|
|
|
5190
|
+
// src/server/service/bot/roster.ts
|
|
5191
|
+
function botRows(chats, viewerId, entries) {
|
|
5192
|
+
const rows2 = entries.map((entry) => {
|
|
5193
|
+
const running = entry.status.status === "running";
|
|
5194
|
+
const visible = [];
|
|
5195
|
+
let hidden = 0;
|
|
5196
|
+
if (running) for (const chatId of new Set(entry.runChatIds)) {
|
|
5197
|
+
const chat = chats.get(viewerId, chatId);
|
|
5198
|
+
if (chat) visible.push({ id: chat.id, type: chat.type, title: chat.title });
|
|
5199
|
+
else hidden++;
|
|
5200
|
+
}
|
|
5201
|
+
return { self: entry.id === viewerId, row: {
|
|
5202
|
+
name: entry.name,
|
|
5203
|
+
...entry.id === viewerId ? { self: true } : {},
|
|
5204
|
+
status: entry.status.status,
|
|
5205
|
+
...running && entry.status.activity ? { activity: entry.status.activity } : {},
|
|
5206
|
+
...visible.length ? { chats: visible } : {},
|
|
5207
|
+
...hidden ? { otherChats: hidden } : {},
|
|
5208
|
+
...entry.errorCode ? { error: entry.errorCode } : {}
|
|
5209
|
+
} };
|
|
5210
|
+
});
|
|
5211
|
+
rows2.sort((a, b) => Number(b.self) - Number(a.self) || a.row.name.localeCompare(b.row.name));
|
|
5212
|
+
return rows2.map((item) => item.row);
|
|
5213
|
+
}
|
|
5214
|
+
|
|
4972
5215
|
// src/server/service/bot/tools.ts
|
|
4973
5216
|
var Gateway = Type4.Object({
|
|
4974
5217
|
action: Type4.Union([Type4.Literal("list"), Type4.Literal("describe"), Type4.Literal("call")]),
|
|
@@ -4988,24 +5231,24 @@ var Tools = class {
|
|
|
4988
5231
|
definitions() {
|
|
4989
5232
|
return [{
|
|
4990
5233
|
name: "chat",
|
|
4991
|
-
description: "
|
|
5234
|
+
description: "Gateway to the chat and to your own History. Use list to see the tools, describe to check a tool's parameters, then call to run it. Only what messages.send or messages.ask sends successfully is visible in the chat.",
|
|
4992
5235
|
parameters: Gateway,
|
|
4993
5236
|
execute: async (raw, { toolCallId, signal }) => {
|
|
4994
5237
|
if (!Value.Check(Gateway, raw)) throw new ChatError(400, "Invalid chat tool request");
|
|
4995
5238
|
signal?.throwIfAborted();
|
|
4996
5239
|
if (raw.action === "list") {
|
|
4997
5240
|
if (raw.tool !== void 0 || raw.args !== void 0) throw new ChatError(400, "list does not accept tool or args");
|
|
4998
|
-
return Object.entries(definitions).map(([
|
|
5241
|
+
return Object.entries(definitions).map(([name3, tool]) => ({ name: name3, description: tool.description }));
|
|
4999
5242
|
}
|
|
5000
5243
|
if (!raw.tool || !Object.hasOwn(definitions, raw.tool)) throw new ChatError(400, "Unknown chat tool; use list first");
|
|
5001
|
-
const
|
|
5244
|
+
const name2 = raw.tool, definition = definitions[name2];
|
|
5002
5245
|
if (raw.action === "describe") {
|
|
5003
5246
|
if (raw.args !== void 0) throw new ChatError(400, "describe does not accept args");
|
|
5004
|
-
return { name, description: definition.description, parameters: definition.schema };
|
|
5247
|
+
return { name: name2, description: definition.description, parameters: definition.schema };
|
|
5005
5248
|
}
|
|
5006
5249
|
const args = raw.args;
|
|
5007
5250
|
if (!args || !Value.Check(definition.schema, args)) throw new ChatError(400, "Invalid tool arguments; use describe for the parameter schema");
|
|
5008
|
-
switch (
|
|
5251
|
+
switch (name2) {
|
|
5009
5252
|
case "history.read":
|
|
5010
5253
|
return readHistory(this.scope.memory, this.scope.workspace, args, signal);
|
|
5011
5254
|
case "chats.list":
|
|
@@ -5014,6 +5257,8 @@ var Tools = class {
|
|
|
5014
5257
|
return chatDetail(this.scope.chats, this.scope.botId, this.tools.chat(args.chatId));
|
|
5015
5258
|
case "members.list":
|
|
5016
5259
|
return this.tools.membersList();
|
|
5260
|
+
case "bots.list":
|
|
5261
|
+
return botRows(this.scope.chats, this.scope.botId, this.scope.roster());
|
|
5017
5262
|
case "chats.create":
|
|
5018
5263
|
return this.tools.create(args);
|
|
5019
5264
|
case "chats.update":
|
|
@@ -5034,8 +5279,16 @@ var Tools = class {
|
|
|
5034
5279
|
return this.tools.updateSection(args);
|
|
5035
5280
|
case "messages.react": {
|
|
5036
5281
|
const parsed = args;
|
|
5037
|
-
const
|
|
5038
|
-
|
|
5282
|
+
const wanted = parsed.on !== false, had = this.scope.chats.reactionOn(this.scope.botId, parsed.messageId, parsed.emoji);
|
|
5283
|
+
const message = this.scope.chats.react(this.scope.botId, parsed.messageId, parsed.emoji, wanted);
|
|
5284
|
+
const applied = wanted === had ? !wanted : wanted;
|
|
5285
|
+
return {
|
|
5286
|
+
messageId: message.id,
|
|
5287
|
+
emoji: parsed.emoji,
|
|
5288
|
+
[applied ? "marked" : "unmarked"]: true,
|
|
5289
|
+
note: applied ? "Reaction added" : "You already had this reaction, so this removed it",
|
|
5290
|
+
reactions: message.reactions.map((group) => ({ emoji: group.emoji, count: group.memberIds.length }))
|
|
5291
|
+
};
|
|
5039
5292
|
}
|
|
5040
5293
|
case "messages.read":
|
|
5041
5294
|
return this.tools.read(args, signal);
|
|
@@ -5063,8 +5316,8 @@ var Tools = class {
|
|
|
5063
5316
|
const chat = this.scope.chats.get(this.scope.botId, chatId);
|
|
5064
5317
|
if (!chat || chat.type === "dm") return { messageId: sent.id, chatId: sent.chatId, sequence: sent.sequence };
|
|
5065
5318
|
const report = this.scope.chats.channels.mentionReport(this.scope.botId, chatId, parsed.message);
|
|
5066
|
-
const notified = sent.mentions.map((id) => this.scope.chats.member(id)?.name).filter((
|
|
5067
|
-
const subscribers = this.scope.chats.channels.subscribers(this.scope.botId, chatId).map((member) => member.name).filter((
|
|
5319
|
+
const notified = sent.mentions.map((id) => this.scope.chats.member(id)?.name).filter((name3) => !!name3);
|
|
5320
|
+
const subscribers = this.scope.chats.channels.subscribers(this.scope.botId, chatId).map((member) => member.name).filter((name3) => name3 !== this.scope.chats.member(this.scope.botId)?.name && !notified.includes(name3));
|
|
5068
5321
|
return {
|
|
5069
5322
|
messageId: sent.id,
|
|
5070
5323
|
chatId: sent.chatId,
|
|
@@ -5108,9 +5361,9 @@ function escape(value2) {
|
|
|
5108
5361
|
if (INVALID_XML.test(value2)) throw new Error("Invalid XML character");
|
|
5109
5362
|
return value2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, " ");
|
|
5110
5363
|
}
|
|
5111
|
-
function attribute(
|
|
5112
|
-
if (!value2.trim()) throw new Error(`Missing ${
|
|
5113
|
-
return ` ${
|
|
5364
|
+
function attribute(name2, value2) {
|
|
5365
|
+
if (!value2.trim()) throw new Error(`Missing ${name2}`);
|
|
5366
|
+
return ` ${name2}="${escape(value2).replace(/"/g, """).replace(/\n/g, " ").replace(/\t/g, "	")}"`;
|
|
5114
5367
|
}
|
|
5115
5368
|
function remember(map, id, definition) {
|
|
5116
5369
|
const old = map.get(id);
|
|
@@ -5133,7 +5386,7 @@ function noticeTail(notices, chats, members) {
|
|
|
5133
5386
|
for (const notice of notices) if (notice.kind === "note" && (latest.get(notice.chat.id)?.version ?? 0) < notice.version) latest.set(notice.chat.id, notice);
|
|
5134
5387
|
const updates = [...latest.values()].map((note) => `<${note.chat.type === "thread" ? "summary" : "notice"}${attribute("chat", note.chat.id)}${attribute("version", String(note.version))}${attribute("by", note.actor.id)}${attribute("at", iso2(note.createdAt))}/>`);
|
|
5135
5388
|
const marks = notices.flatMap((notice) => notice.kind !== "reaction" ? [] : [`<reaction${attribute("message", notice.message.id)}${attribute("chat", notice.chat.id)}${attribute("by", notice.actor.id)}${attribute("emoji", notice.emoji)}${attribute("at", iso2(notice.createdAt))}>${escape(notice.message.text)}</reaction>`]);
|
|
5136
|
-
return (updates.length ? `<updates>${updates.join("")}</updates>` : "") + (marks.length ? `<notices>${marks.join("")}<note
|
|
5389
|
+
return (updates.length ? `<updates>${updates.join("")}</updates>` : "") + (marks.length ? `<notices>${marks.join("")}<note>These are reactions to your messages. Act on them if you need to; no reply is required.</note></notices>` : "");
|
|
5137
5390
|
}
|
|
5138
5391
|
function encodeChatBatch(botId, messages, digest, notices) {
|
|
5139
5392
|
if (!messages.length) throw new Error("Empty chat batch");
|
|
@@ -5180,7 +5433,7 @@ var BotInbox = class {
|
|
|
5180
5433
|
return this.chats.notices(this.botId).length > 0;
|
|
5181
5434
|
}
|
|
5182
5435
|
reference(workspace) {
|
|
5183
|
-
if (this.repository.hasUncertainModel(this.botId)) throw new RuntimeFault("model_switch_uncertain", "
|
|
5436
|
+
if (this.repository.hasUncertainModel(this.botId)) throw new RuntimeFault("model_switch_uncertain", "The model switch didn't finish. Pick the model again.");
|
|
5184
5437
|
return this.repository.ensureSession(this.botId, join5(dirname4(workspace), "agent", "pi", "sessions", `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.jsonl`));
|
|
5185
5438
|
}
|
|
5186
5439
|
/** XML for the claimed messages plus the tail (other deliveries not told yet, unread); `notices` are told once
|
|
@@ -5191,7 +5444,7 @@ var BotInbox = class {
|
|
|
5191
5444
|
return pending2 && { xml: pending2.xml, notices: pending2.ids };
|
|
5192
5445
|
}
|
|
5193
5446
|
const inputs = await this.files.envelope(this.botId, batch.inputs.map((input) => input.messageId));
|
|
5194
|
-
for (const input of batch.inputs) if (!this.chats.get(this.botId, input.chatId)) throw new RuntimeFault("chat_access_changed", "
|
|
5447
|
+
for (const input of batch.inputs) if (!this.chats.get(this.botId, input.chatId)) throw new RuntimeFault("chat_access_changed", "Chat access has changed.");
|
|
5195
5448
|
const digest = this.chats.channels.digest(this.botId);
|
|
5196
5449
|
const pending = this.envelope(this.chats.notices(this.botId));
|
|
5197
5450
|
return { xml: encodeChatBatch(this.botId, inputs, digest, pending.length ? pending : void 0), notices: pending.map((notice) => notice.id) };
|
|
@@ -5201,7 +5454,8 @@ var BotInbox = class {
|
|
|
5201
5454
|
return notices.map((notice) => {
|
|
5202
5455
|
const chat = { id: notice.chatId, type: notice.chatType, title: notice.chatTitle };
|
|
5203
5456
|
if (notice.kind === "note") return { kind: "note", id: notice.id, chat, actor: notice.actor, version: Number(notice.detail), createdAt: notice.createdAt };
|
|
5204
|
-
const
|
|
5457
|
+
const marked = notice.messageId ? this.chats.message(this.botId, notice.messageId) : null;
|
|
5458
|
+
const text3 = marked ? previewQuotes(marked.content, marked.quotes).slice(0, 120) : "";
|
|
5205
5459
|
return { kind: "reaction", id: notice.id, chat, message: { id: notice.messageId ?? "", text: text3 }, actor: notice.actor, emoji: notice.detail, createdAt: notice.createdAt };
|
|
5206
5460
|
});
|
|
5207
5461
|
}
|
|
@@ -5259,7 +5513,7 @@ var AgentRuntime = class {
|
|
|
5259
5513
|
}
|
|
5260
5514
|
fail(error) {
|
|
5261
5515
|
const fault = publicFault(error);
|
|
5262
|
-
this.update({ status: this.session ? "idle" : "stop", activity: null, error: { code: fault.code, message: fault.message } });
|
|
5516
|
+
this.update({ status: this.session ? "idle" : "stop", activity: null, error: { code: fault.code, message: fault.message, ...fault.params ? { params: fault.params } : {} } });
|
|
5263
5517
|
console.error("[bot] execution failed", { botId: this.inbox.botId, runId: this.runId, code: fault.code, ...error instanceof RuntimeFault ? {} : { error } });
|
|
5264
5518
|
}
|
|
5265
5519
|
async start(setup) {
|
|
@@ -5360,7 +5614,10 @@ var AgentRuntime = class {
|
|
|
5360
5614
|
this.promptTask = session.prompt(xml);
|
|
5361
5615
|
const result = await this.promptTask;
|
|
5362
5616
|
repository.finish(batch.run.id, result.outcome, result.outcome === "failed" ? "model_request_failed" : null);
|
|
5363
|
-
if (result.outcome === "failed")
|
|
5617
|
+
if (result.outcome === "failed") {
|
|
5618
|
+
const detail = result.error.replace(/\s+/g, " ").slice(0, 300);
|
|
5619
|
+
throw new RuntimeFault("model_request_failed", `Model request failed: ${detail}`, 502, { detail });
|
|
5620
|
+
}
|
|
5364
5621
|
} catch (error) {
|
|
5365
5622
|
const run = repository.run(batch.run.id);
|
|
5366
5623
|
if (run?.state === "claimed" || run?.state === "submitted") repository.finish(batch.run.id, submitted ? "unknown" : "failed", publicFault(error).code);
|
|
@@ -5386,7 +5643,7 @@ var AgentRuntime = class {
|
|
|
5386
5643
|
reconfigure(operation) {
|
|
5387
5644
|
const task = this.operations.catch(() => {
|
|
5388
5645
|
}).then(async () => {
|
|
5389
|
-
if (this.stopping) throw new RuntimeFault("stopping", "
|
|
5646
|
+
if (this.stopping) throw new RuntimeFault("stopping", "Shutting down.");
|
|
5390
5647
|
await operation();
|
|
5391
5648
|
});
|
|
5392
5649
|
this.operations = task;
|
|
@@ -5397,7 +5654,7 @@ var AgentRuntime = class {
|
|
|
5397
5654
|
this.pauses++;
|
|
5398
5655
|
const task = this.operations.catch(() => {
|
|
5399
5656
|
}).then(async () => {
|
|
5400
|
-
if (this.stopping) throw new RuntimeFault("stopping", "
|
|
5657
|
+
if (this.stopping) throw new RuntimeFault("stopping", "Shutting down.");
|
|
5401
5658
|
await this.pump;
|
|
5402
5659
|
await operation();
|
|
5403
5660
|
});
|
|
@@ -5447,30 +5704,39 @@ var AgentRuntime = class {
|
|
|
5447
5704
|
function botPrompt(setup) {
|
|
5448
5705
|
const bot = { ...setup.identity(), workspacePath: setup.workspace.cwd };
|
|
5449
5706
|
return `# Persona
|
|
5450
|
-
|
|
5707
|
+
Your name is ${bot.name}.
|
|
5451
5708
|
|
|
5452
5709
|
${bot.persona}
|
|
5453
5710
|
|
|
5454
|
-
##
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
##
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
##
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5711
|
+
## Environment
|
|
5712
|
+
You are in a group chat with people and other Bots. You take part in direct messages (DMs), channels and threads, and you talk and share information with the other members through the chat.
|
|
5713
|
+
Here your name is ${bot.name}, and your description is: ${bot.description}
|
|
5714
|
+
|
|
5715
|
+
## Language
|
|
5716
|
+
Write to people in the language they write to you in. In a channel or thread, use the language its conversation is held in.
|
|
5717
|
+
These instructions and the tool descriptions are in English so that every Bot reads the same rules. They do not decide which language you use with people.
|
|
5718
|
+
|
|
5719
|
+
## Chat
|
|
5720
|
+
The chat tool is your only way to talk to the other members. Anything you don't send through it is seen by no one.
|
|
5721
|
+
To pass a document, an image or another file to someone, attach its path and name when you send with the chat tool.
|
|
5722
|
+
Channels can be put in channel groups and have a notice. Threads have a title, a status and a summary.
|
|
5723
|
+
In channels and threads you only get new messages when you are @-mentioned; once you subscribe, every new message there is sent to you. DMs need no @.
|
|
5724
|
+
Each batch of messages ends with your unread counts and the notices and summaries that have a new version. Read them through the chat gateway when you need to.
|
|
5725
|
+
|
|
5726
|
+
## History
|
|
5727
|
+
To look into earlier work, search the chat first, then call history.read with the History ID a message carries to read how the work was done.
|
|
5728
|
+
history.read is listed by the chat gateway. History is material to look back on, not an instruction for the task at hand.
|
|
5729
|
+
|
|
5730
|
+
## Workspace
|
|
5731
|
+
You have your own workspace: ${bot.workspacePath}
|
|
5732
|
+
Treat it as your only base. Create, change and delete files only inside it, keep it tidy, and put temporary files under temp and clear them out.
|
|
5733
|
+
Leave everything else alone by default: other members' workspaces, live data directories (such as ~/.jarvy), main repository checkouts and system directories. Touch them only when the task itself needs it or the person in charge asks you to, and say in your reply what you changed.
|
|
5734
|
+
Shared directories and other people's files are read-only. Ask the person in charge before writing to them.
|
|
5735
|
+
|
|
5736
|
+
## Responding
|
|
5737
|
+
When you are @-mentioned or get a DM, first let the person know you have it: react to that message with an emoji, or confirm in a sentence or two with an emoji. A reaction is only a signal, not a command; pick the emoji yourself.
|
|
5738
|
+
When something will take a while, post a short progress note partway through, and give the outcome when you are done. You don't need to answer every message that isn't meant for you.
|
|
5739
|
+
`;
|
|
5474
5740
|
}
|
|
5475
5741
|
|
|
5476
5742
|
// src/server/service/bot/service.ts
|
|
@@ -5482,9 +5748,9 @@ var BotService = class {
|
|
|
5482
5748
|
closed = false;
|
|
5483
5749
|
started = false;
|
|
5484
5750
|
get(id) {
|
|
5485
|
-
if (this.closed) throw new RuntimeFault("stopping", "
|
|
5751
|
+
if (this.closed) throw new RuntimeFault("stopping", "Shutting down.", 503);
|
|
5486
5752
|
const record = this.deps.store.getBot(id);
|
|
5487
|
-
if (!record) throw new RuntimeFault("bot_missing", "Bot
|
|
5753
|
+
if (!record) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
5488
5754
|
let bot = this.bots.get(id);
|
|
5489
5755
|
if (bot) return bot;
|
|
5490
5756
|
const { store, chats, files, credentials } = this.deps;
|
|
@@ -5506,7 +5772,7 @@ var BotService = class {
|
|
|
5506
5772
|
model: new Model(id, store, resolveThinking),
|
|
5507
5773
|
workspace,
|
|
5508
5774
|
memory,
|
|
5509
|
-
tools: new Tools({ botId: id, chats, files, memory, workspace, asking: (on) => runtime.waitingForUser(on) }),
|
|
5775
|
+
tools: new Tools({ botId: id, chats, files, memory, workspace, asking: (on) => runtime.waitingForUser(on), roster: () => this.roster() }),
|
|
5510
5776
|
agentRuntime: runtime,
|
|
5511
5777
|
state: new BotState(() => this.deps.changed(id))
|
|
5512
5778
|
}, { deliver: async (message) => {
|
|
@@ -5536,8 +5802,8 @@ var BotService = class {
|
|
|
5536
5802
|
list() {
|
|
5537
5803
|
return this.deps.store.listBots().map((record) => this.details(record));
|
|
5538
5804
|
}
|
|
5539
|
-
create(
|
|
5540
|
-
return this.details(this.deps.store.createBot(
|
|
5805
|
+
create(name2) {
|
|
5806
|
+
return this.details(this.deps.store.createBot(name2));
|
|
5541
5807
|
}
|
|
5542
5808
|
describe(id) {
|
|
5543
5809
|
const record = this.deps.store.getBot(id);
|
|
@@ -5557,7 +5823,7 @@ var BotService = class {
|
|
|
5557
5823
|
this.describe(id);
|
|
5558
5824
|
const actor = this.deps.store.userMemberId();
|
|
5559
5825
|
const message = this.deps.chats.message(actor, messageId);
|
|
5560
|
-
if (!message || message.sender.id !== id) throw new RuntimeFault("message_missing", "
|
|
5826
|
+
if (!message || message.sender.id !== id) throw new RuntimeFault("message_missing", "Message not found", 404);
|
|
5561
5827
|
const chat = this.deps.chats.get(actor, message.chatId);
|
|
5562
5828
|
return { message, chat: { id: chat.id, title: chat.title } };
|
|
5563
5829
|
}
|
|
@@ -5570,7 +5836,7 @@ var BotService = class {
|
|
|
5570
5836
|
const bot = this.get(id);
|
|
5571
5837
|
void bot.deliver(urgency).catch((error) => {
|
|
5572
5838
|
const fault = publicFault(error);
|
|
5573
|
-
bot.state.update({ ...bot.status(), error: { code: fault.code, message: fault.message } });
|
|
5839
|
+
bot.state.update({ ...bot.status(), error: { code: fault.code, message: fault.message, ...fault.params ? { params: fault.params } : {} } });
|
|
5574
5840
|
console.error("[bot] delivery failed", { botId: id, code: fault.code });
|
|
5575
5841
|
});
|
|
5576
5842
|
}
|
|
@@ -5587,14 +5853,29 @@ var BotService = class {
|
|
|
5587
5853
|
this.started = true;
|
|
5588
5854
|
for (const { botId, urgency } of this.deps.store.runtime.waitingBots(this.deps.store.userMemberId())) this.deliver(botId, urgency);
|
|
5589
5855
|
}
|
|
5856
|
+
/** Every Bot's run state, for `bots.list` (A43). A Bot not loaded in this process is stopped. */
|
|
5857
|
+
roster() {
|
|
5858
|
+
const { store } = this.deps;
|
|
5859
|
+
return store.listBots().map((record) => {
|
|
5860
|
+
const bot = this.bots.get(record.id), runId = bot?.agentRuntime.activeRunId ?? null;
|
|
5861
|
+
const status = bot?.status() ?? { status: "stop", activity: null, context: null, error: null };
|
|
5862
|
+
return {
|
|
5863
|
+
id: record.id,
|
|
5864
|
+
name: record.name,
|
|
5865
|
+
status,
|
|
5866
|
+
runChatIds: status.status === "running" && runId ? store.runtime.inputs(runId).map((input) => input.chatId) : [],
|
|
5867
|
+
errorCode: store.runtime.latestErrorCode(record.id)
|
|
5868
|
+
};
|
|
5869
|
+
});
|
|
5870
|
+
}
|
|
5590
5871
|
status(id) {
|
|
5591
|
-
if (!this.deps.store.getBot(id)) throw new RuntimeFault("bot_missing", "Bot
|
|
5872
|
+
if (!this.deps.store.getBot(id)) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
5592
5873
|
const bot = this.bots.get(id);
|
|
5593
5874
|
return { ...bot?.status() ?? { status: "stop", activity: null, context: null, error: null }, botId: id, runId: bot?.agentRuntime.activeRunId ?? null };
|
|
5594
5875
|
}
|
|
5595
5876
|
async selectModel(id, input) {
|
|
5596
5877
|
const selection = this.deps.store.validateSelection(id, input);
|
|
5597
|
-
if (!selection) throw new RuntimeFault("bot_missing", "Bot
|
|
5878
|
+
if (!selection) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
5598
5879
|
await this.get(id).changeModel(selection);
|
|
5599
5880
|
this.resume(id);
|
|
5600
5881
|
return this.deps.store.getBot(id);
|
|
@@ -5614,7 +5895,7 @@ var BotService = class {
|
|
|
5614
5895
|
const bot = this.get(id), workspacePath = bot.workspace.location().cwd;
|
|
5615
5896
|
await bot.stop();
|
|
5616
5897
|
this.bots.delete(id);
|
|
5617
|
-
if (!this.deps.store.deleteBot(id)) throw new RuntimeFault("bot_missing", "Bot
|
|
5898
|
+
if (!this.deps.store.deleteBot(id)) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
5618
5899
|
if (!keepFiles) await rm2(dirname5(workspacePath), { recursive: true, force: true });
|
|
5619
5900
|
}
|
|
5620
5901
|
async close() {
|
|
@@ -5686,7 +5967,7 @@ var Application = class _Application {
|
|
|
5686
5967
|
}
|
|
5687
5968
|
});
|
|
5688
5969
|
} catch {
|
|
5689
|
-
throw new RuntimeFault("data_locked", "
|
|
5970
|
+
throw new RuntimeFault("data_locked", "The data directory is in use, or its run lock can't be taken.", 409);
|
|
5690
5971
|
}
|
|
5691
5972
|
let store;
|
|
5692
5973
|
try {
|