@goodfish/jarvy 0.3.0 → 0.5.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 +36 -0
- package/README.md +7 -3
- package/README.zh-CN.md +7 -3
- package/dist/server/cli.js +974 -130
- package/dist/web/assets/figure-DQGUDG-4.js +1 -0
- package/dist/web/assets/index-DoVuF8GU.js +534 -0
- package/dist/web/assets/index-Dsjq2IH9.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/figure-DBBkOPNl.js +0 -1
- package/dist/web/assets/index-C1Aq7FIn.js +0 -505
- package/dist/web/assets/index-CDOMm3pH.css +0 -1
package/dist/server/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import { createServer } from "node:http";
|
|
|
11
11
|
import { getRequestListener } from "@hono/node-server";
|
|
12
12
|
|
|
13
13
|
// src/server/api/app.ts
|
|
14
|
-
import { Hono as
|
|
14
|
+
import { Hono as Hono10 } from "hono";
|
|
15
15
|
import { HTTPException as HTTPException9 } from "hono/http-exception";
|
|
16
16
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
17
17
|
|
|
@@ -432,6 +432,26 @@ function botRoutes(bots) {
|
|
|
432
432
|
// src/server/api/routes/chats.ts
|
|
433
433
|
import { Hono as Hono5 } from "hono";
|
|
434
434
|
import { HTTPException as HTTPException7 } from "hono/http-exception";
|
|
435
|
+
|
|
436
|
+
// src/server/api/views/download.ts
|
|
437
|
+
var MEDIA_TYPES = /* @__PURE__ */ new Set(["video/webm", "video/mp4", "video/quicktime", "audio/mpeg", "audio/wav", "audio/x-wav", "audio/ogg", "audio/webm", "audio/mp4"]);
|
|
438
|
+
function downloadType(mimeType) {
|
|
439
|
+
const type = mimeType.split(";")[0].trim().toLowerCase();
|
|
440
|
+
return MEDIA_TYPES.has(type) ? type : "application/octet-stream";
|
|
441
|
+
}
|
|
442
|
+
function byteRange(header, size) {
|
|
443
|
+
const match = header?.trim().match(/^bytes=(\d*)-(\d*)$/);
|
|
444
|
+
if (!match || !match[1] && !match[2]) return null;
|
|
445
|
+
if (!match[1]) {
|
|
446
|
+
const suffix = Number(match[2]);
|
|
447
|
+
return suffix === 0 ? "unsatisfiable" : { start: Math.max(0, size - suffix), end: size - 1 };
|
|
448
|
+
}
|
|
449
|
+
const start = Number(match[1]), end = match[2] ? Number(match[2]) : Infinity;
|
|
450
|
+
if (end < start) return null;
|
|
451
|
+
return start >= size ? "unsatisfiable" : { start, end: Math.min(end, size - 1) };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/server/api/routes/chats.ts
|
|
435
455
|
function chatRoutes({ chats, bots }) {
|
|
436
456
|
const routes = new Hono5();
|
|
437
457
|
const view = (chat) => chatView(chat, (id) => chats.member(id));
|
|
@@ -446,10 +466,10 @@ function chatRoutes({ chats, bots }) {
|
|
|
446
466
|
const page = chats.readBefore(c.get("actorId"), c.req.param("id"), before === void 0 ? null : Number(before), Number(tail ?? c.req.query("limit") ?? 50));
|
|
447
467
|
return c.json({ messages: page.messages.map(messageView), hasOlder: page.hasOlder });
|
|
448
468
|
}
|
|
449
|
-
const
|
|
469
|
+
const after2 = Number(c.req.query("after") ?? 0);
|
|
450
470
|
const limit = Number(c.req.query("limit") ?? 1e3);
|
|
451
|
-
const messages = chats.read(c.get("actorId"), c.req.param("id"),
|
|
452
|
-
return c.json({ messages: messages.map(messageView), nextSequence: messages.at(-1)?.sequence ??
|
|
471
|
+
const messages = chats.read(c.get("actorId"), c.req.param("id"), after2, limit);
|
|
472
|
+
return c.json({ messages: messages.map(messageView), nextSequence: messages.at(-1)?.sequence ?? after2, hasMore: messages.length === limit });
|
|
453
473
|
});
|
|
454
474
|
routes.post("/:id/messages", jsonBody, async (c) => {
|
|
455
475
|
const message = await chats.send(c.get("actorId"), c.req.param("id"), await readJson(c));
|
|
@@ -484,11 +504,19 @@ function attachmentRoutes(chats) {
|
|
|
484
504
|
routes.get("/:id", (c) => {
|
|
485
505
|
const attachment = chats.download(c.get("actorId"), c.req.param("id"));
|
|
486
506
|
if (!attachment) throw new HTTPException7(404, { message: "Attachment not found" });
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
"Content-
|
|
507
|
+
const size = attachment.data.length, range = byteRange(c.req.header("Range"), size);
|
|
508
|
+
const headers2 = {
|
|
509
|
+
"Content-Type": downloadType(attachment.file.mimeType),
|
|
510
|
+
"Accept-Ranges": "bytes",
|
|
490
511
|
"Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(attachment.file.name)}`
|
|
491
|
-
}
|
|
512
|
+
};
|
|
513
|
+
if (range === "unsatisfiable") return c.body(null, 416, { ...headers2, "Content-Range": `bytes */${size}` });
|
|
514
|
+
if (!range) return c.body(new Uint8Array(attachment.data), 200, { ...headers2, "Content-Length": String(size) });
|
|
515
|
+
return c.body(
|
|
516
|
+
new Uint8Array(attachment.data.subarray(range.start, range.end + 1)),
|
|
517
|
+
206,
|
|
518
|
+
{ ...headers2, "Content-Length": String(range.end - range.start + 1), "Content-Range": `bytes ${range.start}-${range.end}/${size}` }
|
|
519
|
+
);
|
|
492
520
|
});
|
|
493
521
|
routes.get("/:id/run", (c) => {
|
|
494
522
|
const attachment = chats.download(c.get("actorId"), c.req.param("id"));
|
|
@@ -579,6 +607,7 @@ data: ${JSON.stringify(data)}
|
|
|
579
607
|
if (change.kind === "sections" && change.ownerId === actorId) send("sections", {});
|
|
580
608
|
if (change.kind === "inbox" && change.memberId === actorId) send("inbox", {});
|
|
581
609
|
if (change.kind === "bot" && bots.owns(change.id)) send("bot", { botId: change.id, status: botStatusView(change.status) });
|
|
610
|
+
if (change.kind === "bots") send("bots", {});
|
|
582
611
|
}, () => disconnect());
|
|
583
612
|
const heartbeat = setInterval(() => {
|
|
584
613
|
if (!account.authenticated(token)) disconnect();
|
|
@@ -685,12 +714,68 @@ function channelRoutes({ chats }) {
|
|
|
685
714
|
return routes;
|
|
686
715
|
}
|
|
687
716
|
|
|
717
|
+
// src/server/api/routes/live.ts
|
|
718
|
+
import { Hono as Hono9 } from "hono";
|
|
719
|
+
function liveRoutes({ account, bots }) {
|
|
720
|
+
const routes = new Hono9();
|
|
721
|
+
routes.get("/bots/:id/live", (c) => {
|
|
722
|
+
const token = c.get("sessionToken"), signal = c.req.raw.signal, live = bots.live(c.req.param("id"));
|
|
723
|
+
let disconnect;
|
|
724
|
+
const body = new ReadableStream({
|
|
725
|
+
start(controller) {
|
|
726
|
+
let ended = false;
|
|
727
|
+
const encoder = new TextEncoder();
|
|
728
|
+
const send = (event, data) => {
|
|
729
|
+
if (ended) return;
|
|
730
|
+
if ((controller.desiredSize ?? 0) <= 0) {
|
|
731
|
+
disconnect();
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
controller.enqueue(encoder.encode(`event: ${event}
|
|
735
|
+
data: ${JSON.stringify(data)}
|
|
736
|
+
|
|
737
|
+
`));
|
|
738
|
+
};
|
|
739
|
+
const { turn, unsubscribe } = live.subscribe((update) => {
|
|
740
|
+
if (!account.authenticated(token)) {
|
|
741
|
+
disconnect();
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
send(update.type, update);
|
|
745
|
+
}, () => disconnect());
|
|
746
|
+
const heartbeat = setInterval(() => {
|
|
747
|
+
if (!account.authenticated(token)) disconnect();
|
|
748
|
+
else send("heartbeat", {});
|
|
749
|
+
}, 15e3);
|
|
750
|
+
heartbeat.unref();
|
|
751
|
+
const aborted = () => disconnect();
|
|
752
|
+
disconnect = (close = true) => {
|
|
753
|
+
if (ended) return;
|
|
754
|
+
ended = true;
|
|
755
|
+
clearInterval(heartbeat);
|
|
756
|
+
unsubscribe();
|
|
757
|
+
signal.removeEventListener("abort", aborted);
|
|
758
|
+
if (close) controller.close();
|
|
759
|
+
};
|
|
760
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
761
|
+
if (signal.aborted) disconnect();
|
|
762
|
+
else send("snapshot", { turn, now: Date.now() });
|
|
763
|
+
},
|
|
764
|
+
cancel() {
|
|
765
|
+
disconnect(false);
|
|
766
|
+
}
|
|
767
|
+
}, new ByteLengthQueuingStrategy({ highWaterMark: c.env.outgoing.writableHighWaterMark }));
|
|
768
|
+
return c.body(body, 200, { "Content-Type": "text/event-stream", Connection: "keep-alive", "X-Accel-Buffering": "no" });
|
|
769
|
+
});
|
|
770
|
+
return routes;
|
|
771
|
+
}
|
|
772
|
+
|
|
688
773
|
// src/server/api/app.ts
|
|
689
774
|
function createHttpApp(services, webRoot) {
|
|
690
|
-
const app = new
|
|
775
|
+
const app = new Hono10();
|
|
691
776
|
app.use("*", headers);
|
|
692
777
|
app.onError(handleError);
|
|
693
|
-
const api = new
|
|
778
|
+
const api = new Hono10();
|
|
694
779
|
api.use("*", apiSecurity);
|
|
695
780
|
installAuthentication(api, services.account);
|
|
696
781
|
api.route("/bots", botRoutes(services.bots));
|
|
@@ -699,6 +784,7 @@ function createHttpApp(services, webRoot) {
|
|
|
699
784
|
api.route("/attachments", attachmentRoutes(services.chats));
|
|
700
785
|
api.route("/", modelRoutes(services.models));
|
|
701
786
|
api.route("/", eventRoutes(services));
|
|
787
|
+
api.route("/", liveRoutes(services));
|
|
702
788
|
api.all("*", () => {
|
|
703
789
|
throw new HTTPException9(404, { message: "API route not found" });
|
|
704
790
|
});
|
|
@@ -724,9 +810,10 @@ function createJarvyServer(services, webRoot) {
|
|
|
724
810
|
|
|
725
811
|
// src/server/shared/models.ts
|
|
726
812
|
var BOT_COLORS = ["gray", "blue", "green", "amber", "rose", "violet"];
|
|
813
|
+
var AGENT_BOT_ID = "00000000-0000-4000-8000-000000000001";
|
|
727
814
|
|
|
728
815
|
// src/server/infra/storage/store.ts
|
|
729
|
-
import { createHash, randomBytes, randomInt as
|
|
816
|
+
import { createHash, randomBytes, randomInt as randomInt3, randomUUID as randomUUID7, scryptSync, timingSafeEqual } from "node:crypto";
|
|
730
817
|
import { chmodSync, mkdirSync } from "node:fs";
|
|
731
818
|
import { homedir } from "node:os";
|
|
732
819
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -998,12 +1085,49 @@ function migrateTitles(db) {
|
|
|
998
1085
|
}
|
|
999
1086
|
}
|
|
1000
1087
|
|
|
1088
|
+
// src/server/infra/storage/migrations/agent-schema.ts
|
|
1089
|
+
function migrateAgent(db) {
|
|
1090
|
+
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1091
|
+
if (version >= 19) return;
|
|
1092
|
+
db.exec("PRAGMA foreign_keys=OFF; BEGIN IMMEDIATE");
|
|
1093
|
+
try {
|
|
1094
|
+
const sequence = db.prepare("SELECT seq FROM sqlite_sequence WHERE name='inbox'").get()?.seq ?? 0;
|
|
1095
|
+
db.exec(`
|
|
1096
|
+
CREATE TABLE settings(key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
1097
|
+
CREATE TABLE inbox_next(id INTEGER PRIMARY KEY AUTOINCREMENT, member_id TEXT NOT NULL REFERENCES members(id),
|
|
1098
|
+
kind TEXT NOT NULL CHECK(kind IN ('message','reaction','note','onboard','change')), reason TEXT NOT NULL,
|
|
1099
|
+
urgency TEXT NOT NULL CHECK(urgency IN ('interrupt','queue','digest')),
|
|
1100
|
+
chat_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE, message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,
|
|
1101
|
+
actor_id TEXT REFERENCES members(id), detail TEXT,
|
|
1102
|
+
state TEXT NOT NULL DEFAULT 'pending' CHECK(state IN ('pending','claimed','told','done','failed','interrupted','unknown','cancelled')),
|
|
1103
|
+
run_id TEXT REFERENCES bot_runs(id), created_at INTEGER NOT NULL, told_at INTEGER);
|
|
1104
|
+
INSERT INTO inbox_next(id,member_id,kind,reason,urgency,chat_id,message_id,actor_id,detail,state,run_id,created_at,told_at)
|
|
1105
|
+
SELECT id,member_id,kind,reason,urgency,chat_id,message_id,actor_id,detail,state,run_id,created_at,told_at FROM inbox ORDER BY id;
|
|
1106
|
+
DROP TABLE inbox;
|
|
1107
|
+
ALTER TABLE inbox_next RENAME TO inbox;
|
|
1108
|
+
CREATE UNIQUE INDEX inbox_one_message ON inbox(member_id, message_id) WHERE kind='message';
|
|
1109
|
+
CREATE INDEX inbox_open ON inbox(member_id, state, id);
|
|
1110
|
+
CREATE INDEX inbox_run ON inbox(run_id) WHERE run_id IS NOT NULL;
|
|
1111
|
+
`);
|
|
1112
|
+
db.prepare("UPDATE sqlite_sequence SET seq=MAX(seq,?) WHERE name='inbox'").run(sequence);
|
|
1113
|
+
db.prepare(`INSERT INTO settings(key,value) SELECT 'agent_onboarded',? FROM bots
|
|
1114
|
+
WHERE id=? AND deleted_at IS NULL AND connection_id IS NOT NULL`).run(String(Date.now()), AGENT_BOT_ID);
|
|
1115
|
+
db.exec("PRAGMA user_version=19");
|
|
1116
|
+
if (db.prepare("PRAGMA foreign_key_check").all().length) throw new Error("Migration has invalid references");
|
|
1117
|
+
db.exec("COMMIT");
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
db.exec("ROLLBACK");
|
|
1120
|
+
throw error;
|
|
1121
|
+
} finally {
|
|
1122
|
+
db.exec("PRAGMA foreign_keys=ON");
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1001
1126
|
// src/server/infra/storage/migrations/schema.ts
|
|
1002
1127
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1003
1128
|
|
|
1004
1129
|
// src/server/infra/storage/migrations/prototype-schema.ts
|
|
1005
1130
|
import { randomUUID } from "node:crypto";
|
|
1006
|
-
var DEFAULT_BOT_ID = "00000000-0000-4000-8000-000000000001";
|
|
1007
1131
|
function migratePrototype(db) {
|
|
1008
1132
|
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1009
1133
|
if (version >= 4) return;
|
|
@@ -1036,9 +1160,9 @@ function migratePrototype(db) {
|
|
|
1036
1160
|
system_prompt TEXT NOT NULL, model_id TEXT NOT NULL, avatar_mime TEXT, avatar_data BLOB,
|
|
1037
1161
|
avatar_updated_at INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL);
|
|
1038
1162
|
INSERT INTO bots(id,name,description,system_prompt,model_id,created_at)
|
|
1039
|
-
SELECT '${
|
|
1163
|
+
SELECT '${AGENT_BOT_ID}',name,'',system_prompt,'',0 FROM bot_settings WHERE id=1;
|
|
1040
1164
|
ALTER TABLE chats ADD COLUMN bot_id TEXT REFERENCES bots(id);
|
|
1041
|
-
UPDATE chats SET bot_id='${
|
|
1165
|
+
UPDATE chats SET bot_id='${AGENT_BOT_ID}';
|
|
1042
1166
|
CREATE INDEX chat_by_bot ON chats(bot_id,updated_at DESC);
|
|
1043
1167
|
CREATE TABLE attachments (id TEXT PRIMARY KEY, chat_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
|
|
1044
1168
|
message_id TEXT REFERENCES messages(id) ON DELETE CASCADE, name TEXT NOT NULL,
|
|
@@ -1121,7 +1245,7 @@ function migrateRuntime(db) {
|
|
|
1121
1245
|
// src/server/infra/storage/migrations/schema.ts
|
|
1122
1246
|
function migrateDatabase(db) {
|
|
1123
1247
|
const version = db.prepare("PRAGMA user_version").get().user_version;
|
|
1124
|
-
if (version >
|
|
1248
|
+
if (version > 19) throw new Error(`Unsupported database version: ${version}`);
|
|
1125
1249
|
migratePrototype(db);
|
|
1126
1250
|
if (version >= 5) {
|
|
1127
1251
|
migrateRuntime(db);
|
|
@@ -1137,6 +1261,7 @@ function migrateDatabase(db) {
|
|
|
1137
1261
|
migrateInbox(db);
|
|
1138
1262
|
migrateQuotes(db);
|
|
1139
1263
|
migrateTitles(db);
|
|
1264
|
+
migrateAgent(db);
|
|
1140
1265
|
return;
|
|
1141
1266
|
}
|
|
1142
1267
|
db.exec("PRAGMA foreign_keys=OFF; BEGIN IMMEDIATE");
|
|
@@ -1210,6 +1335,7 @@ function migrateDatabase(db) {
|
|
|
1210
1335
|
migrateInbox(db);
|
|
1211
1336
|
migrateQuotes(db);
|
|
1212
1337
|
migrateTitles(db);
|
|
1338
|
+
migrateAgent(db);
|
|
1213
1339
|
}
|
|
1214
1340
|
|
|
1215
1341
|
// src/server/infra/storage/chat-repository.ts
|
|
@@ -1533,7 +1659,7 @@ var InteractionRepository = class {
|
|
|
1533
1659
|
}
|
|
1534
1660
|
/** Notices the Bot has not been told, oldest first, in chats it can still read. */
|
|
1535
1661
|
notices(botId) {
|
|
1536
|
-
const rows2 = this.db.prepare(`SELECT n.id,n.kind,n.chat_id AS chatId,c.title AS chatTitle,c.type AS chatType,n.message_id AS messageId,n.actor_id AS actorId,n.detail,n.created_at AS createdAt
|
|
1662
|
+
const rows2 = this.db.prepare(`SELECT n.id,n.kind,n.urgency,n.chat_id AS chatId,c.title AS chatTitle,c.type AS chatType,n.message_id AS messageId,n.actor_id AS actorId,n.detail,n.created_at AS createdAt
|
|
1537
1663
|
FROM inbox n JOIN chats c ON c.id=n.chat_id WHERE n.member_id=? AND n.kind<>'message' AND n.state='pending' ORDER BY n.id`).all(botId);
|
|
1538
1664
|
return rows2.filter((row) => this.chats.canAccess(botId, row.chatId)).map(({ actorId, ...row }) => ({ ...row, actor: this.chats.member(actorId) }));
|
|
1539
1665
|
}
|
|
@@ -1763,9 +1889,9 @@ var ChatRepository = class {
|
|
|
1763
1889
|
AND (instr(lower(c.title),lower(?))>0 OR instr(lower(c.topic),lower(?))>0 OR instr(lower(COALESCE(n.text,'')),lower(?))>0)
|
|
1764
1890
|
ORDER BY c.updated_at DESC LIMIT ?`).all(actorId, query, query, query, limit).map((row) => row.id);
|
|
1765
1891
|
}
|
|
1766
|
-
messages(actorId, chatId,
|
|
1892
|
+
messages(actorId, chatId, after2 = 0, limit = 1e3) {
|
|
1767
1893
|
this.assertAccess(actorId, chatId);
|
|
1768
|
-
const rows2 = this.db.prepare(`SELECT ${MESSAGE_COLUMNS} FROM messages WHERE chat_id=? AND sequence>? ORDER BY sequence LIMIT ?`).all(chatId,
|
|
1894
|
+
const rows2 = this.db.prepare(`SELECT ${MESSAGE_COLUMNS} FROM messages WHERE chat_id=? AND sequence>? ORDER BY sequence LIMIT ?`).all(chatId, after2, limit);
|
|
1769
1895
|
return rows2.map((row) => this.messageFromRow(row));
|
|
1770
1896
|
}
|
|
1771
1897
|
message(actorId, id) {
|
|
@@ -2106,6 +2232,47 @@ var CredentialRepository = class {
|
|
|
2106
2232
|
}
|
|
2107
2233
|
};
|
|
2108
2234
|
|
|
2235
|
+
// src/server/infra/storage/agent-repository.ts
|
|
2236
|
+
import { randomInt as randomInt2 } from "node:crypto";
|
|
2237
|
+
var AgentRepository = class {
|
|
2238
|
+
constructor(db, ownerId) {
|
|
2239
|
+
this.db = db;
|
|
2240
|
+
this.ownerId = ownerId;
|
|
2241
|
+
}
|
|
2242
|
+
/** Brings the agent Bot back when it is missing (deleted before it became undeletable): the same id, the first free
|
|
2243
|
+
* name of Jarvy, Jarvy 2, …, no persona and no model, so the owner picks one and is welcomed again. */
|
|
2244
|
+
ensure() {
|
|
2245
|
+
const row = this.db.prepare("SELECT deleted_at AS deletedAt FROM bots WHERE id=?").get(AGENT_BOT_ID);
|
|
2246
|
+
if (row && row.deletedAt === null) return;
|
|
2247
|
+
const taken = this.db.prepare(`SELECT 1 FROM account WHERE lower(username)=lower(:name)
|
|
2248
|
+
UNION ALL SELECT 1 FROM bots WHERE lower(name)=lower(:name) AND deleted_at IS NULL LIMIT 1`);
|
|
2249
|
+
let name2 = "Jarvy";
|
|
2250
|
+
for (let n = 2; taken.get({ name: name2 }); n++) name2 = `Jarvy ${n}`;
|
|
2251
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
2252
|
+
try {
|
|
2253
|
+
if (row) this.db.prepare(`UPDATE bots SET deleted_at=NULL,name=?,description='',system_prompt='',connection_id=NULL,model_id='',thinking_level='auto'
|
|
2254
|
+
WHERE id=?`).run(name2, AGENT_BOT_ID);
|
|
2255
|
+
else {
|
|
2256
|
+
this.db.prepare("INSERT OR IGNORE INTO members VALUES(?,'bot')").run(AGENT_BOT_ID);
|
|
2257
|
+
this.db.prepare("INSERT INTO bots(id,name,description,system_prompt,model_id,created_at,owner_id,thinking_level,color) VALUES(?,?,'','','',?,?,'auto',?)").run(AGENT_BOT_ID, name2, Date.now(), this.ownerId(), BOT_COLORS[randomInt2(BOT_COLORS.length)]);
|
|
2258
|
+
}
|
|
2259
|
+
this.db.prepare("DELETE FROM settings WHERE key='agent_onboarded'").run();
|
|
2260
|
+
this.db.exec("COMMIT");
|
|
2261
|
+
} catch (error) {
|
|
2262
|
+
this.db.exec("ROLLBACK");
|
|
2263
|
+
throw error;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
/** Records that the owner is being welcomed; false when that already happened, so only one caller asks. */
|
|
2267
|
+
markOnboarded() {
|
|
2268
|
+
return Number(this.db.prepare("INSERT OR IGNORE INTO settings(key,value) VALUES('agent_onboarded',?)").run(String(Date.now())).changes) > 0;
|
|
2269
|
+
}
|
|
2270
|
+
/** One delivery about no message, for one Bot; the caller wakes it. */
|
|
2271
|
+
notify(input) {
|
|
2272
|
+
this.db.prepare("INSERT INTO inbox(member_id,kind,reason,urgency,chat_id,actor_id,detail,created_at) VALUES(?,?,?,?,?,?,?,?)").run(input.botId, input.kind, input.reason, input.urgency, input.chatId, input.actorId, input.detail, Date.now());
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
2275
|
+
|
|
2109
2276
|
// src/server/infra/storage/store.ts
|
|
2110
2277
|
var defaultDataDir = () => process.env.JARVY_DATA_DIR || join(homedir(), ".jarvy");
|
|
2111
2278
|
var DATABASE_FILENAME = "jarvy.db";
|
|
@@ -2175,6 +2342,7 @@ var Store = class {
|
|
|
2175
2342
|
chats;
|
|
2176
2343
|
runtime;
|
|
2177
2344
|
credentials;
|
|
2345
|
+
agent;
|
|
2178
2346
|
constructor(dataDir = defaultDataDir()) {
|
|
2179
2347
|
const root = resolve(dataDir);
|
|
2180
2348
|
mkdirSync(root, { recursive: true, mode: 448 });
|
|
@@ -2191,6 +2359,8 @@ var Store = class {
|
|
|
2191
2359
|
this.chats = new ChatRepository(this.db);
|
|
2192
2360
|
this.runtime = new RuntimeRepository(this.db);
|
|
2193
2361
|
this.credentials = new CredentialRepository(this.db);
|
|
2362
|
+
this.agent = new AgentRepository(this.db, () => this.userMemberId());
|
|
2363
|
+
this.agent.ensure();
|
|
2194
2364
|
}
|
|
2195
2365
|
close() {
|
|
2196
2366
|
this.db.close();
|
|
@@ -2295,7 +2465,7 @@ var Store = class {
|
|
|
2295
2465
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2296
2466
|
try {
|
|
2297
2467
|
this.db.prepare("INSERT INTO members VALUES(?,'bot')").run(id);
|
|
2298
|
-
this.db.prepare("INSERT INTO bots(id,name,description,system_prompt,model_id,created_at,owner_id,thinking_level,color) VALUES(?,?,?,?,?,?,?,'auto',?)").run(id, clean, "", "", "", createdAt, this.userMemberId(), BOT_COLORS[
|
|
2468
|
+
this.db.prepare("INSERT INTO bots(id,name,description,system_prompt,model_id,created_at,owner_id,thinking_level,color) VALUES(?,?,?,?,?,?,?,'auto',?)").run(id, clean, "", "", "", createdAt, this.userMemberId(), BOT_COLORS[randomInt3(BOT_COLORS.length)]);
|
|
2299
2469
|
this.db.exec("COMMIT");
|
|
2300
2470
|
} catch (error) {
|
|
2301
2471
|
this.db.exec("ROLLBACK");
|
|
@@ -2699,8 +2869,8 @@ function candidates(query) {
|
|
|
2699
2869
|
const q = query.trim().toLowerCase();
|
|
2700
2870
|
if (!q) return [];
|
|
2701
2871
|
const all = catalog();
|
|
2702
|
-
const
|
|
2703
|
-
return
|
|
2872
|
+
const same3 = all.filter((row) => row.model.id.toLowerCase() === q || strip(row.model.id) === strip(q));
|
|
2873
|
+
return same3.length ? same3 : all.filter((row) => row.model.id.toLowerCase().includes(strip(q)));
|
|
2704
2874
|
}
|
|
2705
2875
|
function similar(query) {
|
|
2706
2876
|
const q = strip(query);
|
|
@@ -3092,14 +3262,14 @@ function messageTargets({ dm, sender, members, mentioned, replied, subscribers }
|
|
|
3092
3262
|
if (dm) return members.filter((member) => member.id !== sender.id).map((member) => ({ member, reason: "dm" }));
|
|
3093
3263
|
const targets = mentioned.filter((member) => member.id !== sender.id).map((member) => ({ member, reason: "mention" }));
|
|
3094
3264
|
const add = (member, reason) => {
|
|
3095
|
-
if (member.id !== sender.id && !targets.some((
|
|
3265
|
+
if (member.id !== sender.id && !targets.some((target2) => target2.member.id === member.id)) targets.push({ member, reason });
|
|
3096
3266
|
};
|
|
3097
3267
|
if (replied && members.some((member) => member.id === replied.id)) add(replied, "reply");
|
|
3098
3268
|
for (const member of members) if (subscribers.includes(member.id)) add(member, "subscription");
|
|
3099
3269
|
return targets;
|
|
3100
3270
|
}
|
|
3101
3271
|
function echoTargets({ sender, members, subscribers, reached }) {
|
|
3102
|
-
return members.filter((member) => member.id !== sender.id && subscribers.includes(member.id) && !reached.some((
|
|
3272
|
+
return members.filter((member) => member.id !== sender.id && subscribers.includes(member.id) && !reached.some((target2) => target2.member.id === member.id)).map((member) => ({ member, reason: "subscription" }));
|
|
3103
3273
|
}
|
|
3104
3274
|
function reactionTargets({ actorId, author }) {
|
|
3105
3275
|
return author && author.id !== actorId ? [{ member: author, reason: "author" }] : [];
|
|
@@ -3115,7 +3285,11 @@ var POLICY = {
|
|
|
3115
3285
|
// Marks on your messages are queued: they wake you but never interrupt (A39).
|
|
3116
3286
|
reaction: { author: "queue" },
|
|
3117
3287
|
// A new version of a notice or summary is queued like a mark.
|
|
3118
|
-
note: { member: "queue", follower: "queue" }
|
|
3288
|
+
note: { member: "queue", follower: "queue" },
|
|
3289
|
+
// The owner just gave the agent Bot its first model: it should welcome them now (A48).
|
|
3290
|
+
onboard: { setup: "interrupt" },
|
|
3291
|
+
// What the agent Bot changed about a Bot rides along with that Bot's next batch, without waking it.
|
|
3292
|
+
change: { agent: "digest" }
|
|
3119
3293
|
};
|
|
3120
3294
|
function urgencyOf(kind, reason) {
|
|
3121
3295
|
return POLICY[kind][reason];
|
|
@@ -3126,9 +3300,9 @@ function mutedUrgency(reason, urgency) {
|
|
|
3126
3300
|
|
|
3127
3301
|
// src/server/service/delivery/delivery.ts
|
|
3128
3302
|
function plan(kind, targets, muted = []) {
|
|
3129
|
-
return targets.flatMap((
|
|
3130
|
-
const urgency = urgencyOf(kind,
|
|
3131
|
-
return urgency ? [{ ...
|
|
3303
|
+
return targets.flatMap((target2) => {
|
|
3304
|
+
const urgency = urgencyOf(kind, target2.reason);
|
|
3305
|
+
return urgency ? [{ ...target2, kind, urgency: muted.includes(target2.member.id) ? mutedUrgency(target2.reason, urgency) : urgency }] : [];
|
|
3132
3306
|
});
|
|
3133
3307
|
}
|
|
3134
3308
|
var Dispatcher = class {
|
|
@@ -3279,8 +3453,8 @@ var ChatMessages = class {
|
|
|
3279
3453
|
const message = this.repository.message(this.actor, id);
|
|
3280
3454
|
return message?.chatId === this.id ? message : null;
|
|
3281
3455
|
}
|
|
3282
|
-
async list({ after = 0, limit }) {
|
|
3283
|
-
return this.repository.messages(this.actor, this.id,
|
|
3456
|
+
async list({ after: after2 = 0, limit }) {
|
|
3457
|
+
return this.repository.messages(this.actor, this.id, after2, limit);
|
|
3284
3458
|
}
|
|
3285
3459
|
async search(query, { limit }) {
|
|
3286
3460
|
return this.repository.search(this.actor, this.id, query, limit);
|
|
@@ -3650,9 +3824,9 @@ var ChatService = class {
|
|
|
3650
3824
|
/** A DM with another member of the actor's space (a Bot DMs its owner or another Bot); one per pair. */
|
|
3651
3825
|
openDirect(actorId, targetId) {
|
|
3652
3826
|
const owner = this.repository.channels.ownerOf(actorId), directory = this.repository.channels.directory(owner);
|
|
3653
|
-
const actor = directory.find((member) => member.id === actorId),
|
|
3654
|
-
if (!actor || !
|
|
3655
|
-
const chat = this.repository.openDirect(actorId, targetId, owner, `${actor.name} \xB7 ${
|
|
3827
|
+
const actor = directory.find((member) => member.id === actorId), target2 = directory.find((member) => member.id === targetId);
|
|
3828
|
+
if (!actor || !target2) throw new ChatError(404, "Member not found");
|
|
3829
|
+
const chat = this.repository.openDirect(actorId, targetId, owner, `${actor.name} \xB7 ${target2.name}`);
|
|
3656
3830
|
this.notifySaved(chat.id);
|
|
3657
3831
|
return chat;
|
|
3658
3832
|
}
|
|
@@ -3750,10 +3924,10 @@ var ChatService = class {
|
|
|
3750
3924
|
const messages = this.repository.messagesBefore(actorId, chatId, before ?? Number.MAX_SAFE_INTEGER, limit + 1);
|
|
3751
3925
|
return { messages: messages.slice(-limit), hasOlder: messages.length > limit };
|
|
3752
3926
|
}
|
|
3753
|
-
read(actorId, chatId,
|
|
3754
|
-
if (!Number.isSafeInteger(
|
|
3927
|
+
read(actorId, chatId, after2 = 0, limit = 1e3) {
|
|
3928
|
+
if (!Number.isSafeInteger(after2) || after2 < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 1e3)
|
|
3755
3929
|
throw new ChatError(400, "Invalid message cursor or limit");
|
|
3756
|
-
return this.repository.messages(actorId, chatId,
|
|
3930
|
+
return this.repository.messages(actorId, chatId, after2, limit);
|
|
3757
3931
|
}
|
|
3758
3932
|
instance(actorId, chatId) {
|
|
3759
3933
|
this.repository.assertAccess(actorId, chatId);
|
|
@@ -3964,8 +4138,8 @@ var Changes = class {
|
|
|
3964
4138
|
// src/server/service/bot/activity.ts
|
|
3965
4139
|
function activityPage(messages, history, limit) {
|
|
3966
4140
|
const candidates2 = [
|
|
3967
|
-
...messages.map((
|
|
3968
|
-
...history.map((
|
|
4141
|
+
...messages.map((record2) => ({ kind: "chat", record: record2 })),
|
|
4142
|
+
...history.map((record2) => ({ kind: "history", record: record2 }))
|
|
3969
4143
|
];
|
|
3970
4144
|
candidates2.sort((a, b) => {
|
|
3971
4145
|
if (a.record.timestamp !== b.record.timestamp) return b.record.timestamp - a.record.timestamp;
|
|
@@ -4121,7 +4295,74 @@ function configureHttp(timeoutMs) {
|
|
|
4121
4295
|
|
|
4122
4296
|
// src/server/infra/pi/session.ts
|
|
4123
4297
|
import { Type } from "typebox";
|
|
4298
|
+
|
|
4299
|
+
// src/server/infra/pi/live.ts
|
|
4300
|
+
var LiveTranslator = class {
|
|
4301
|
+
message = 0;
|
|
4302
|
+
open = /* @__PURE__ */ new Set();
|
|
4303
|
+
translate(event) {
|
|
4304
|
+
switch (event.type) {
|
|
4305
|
+
case "message_start":
|
|
4306
|
+
if (event.message.role === "assistant") this.message++;
|
|
4307
|
+
return [];
|
|
4308
|
+
case "message_update": {
|
|
4309
|
+
const update = event.assistantMessageEvent;
|
|
4310
|
+
if (update.type !== "thinking_start" && update.type !== "thinking_delta" && update.type !== "thinking_end" && update.type !== "text_start" && update.type !== "text_delta" && update.type !== "text_end") return [];
|
|
4311
|
+
const key = `${this.message}:${update.contentIndex}`, kind = update.type.startsWith("thinking") ? "thinking" : "text";
|
|
4312
|
+
const events = [];
|
|
4313
|
+
if (!this.open.has(key) && !update.type.endsWith("_end")) {
|
|
4314
|
+
this.open.add(key);
|
|
4315
|
+
events.push({ type: "block_start", key, kind });
|
|
4316
|
+
}
|
|
4317
|
+
if (update.type.endsWith("_delta") && "delta" in update && update.delta) events.push({ type: "block_delta", key, kind, delta: update.delta });
|
|
4318
|
+
if (update.type.endsWith("_end") && this.open.delete(key)) events.push({ type: "block_end", key });
|
|
4319
|
+
return events;
|
|
4320
|
+
}
|
|
4321
|
+
case "message_end": {
|
|
4322
|
+
if (event.message.role !== "assistant") return [];
|
|
4323
|
+
const prefix = `${this.message}:`, closing = [...this.open].filter((key) => key.startsWith(prefix));
|
|
4324
|
+
for (const key of closing) this.open.delete(key);
|
|
4325
|
+
return closing.map((key) => ({ type: "block_end", key }));
|
|
4326
|
+
}
|
|
4327
|
+
case "tool_execution_start":
|
|
4328
|
+
return [{ type: "tool_start", id: event.toolCallId, parentId: event.parentToolCallId ?? null, name: event.toolName, args: event.args }];
|
|
4329
|
+
case "tool_execution_end":
|
|
4330
|
+
return [{ type: "tool_end", id: event.toolCallId, output: resultText(event.result), failed: event.isError }];
|
|
4331
|
+
case "compaction_start":
|
|
4332
|
+
return [{ type: "compaction_start" }];
|
|
4333
|
+
case "compaction_end":
|
|
4334
|
+
return [{
|
|
4335
|
+
type: "compaction_end",
|
|
4336
|
+
failed: !!event.errorMessage || event.aborted,
|
|
4337
|
+
error: event.errorMessage ?? null,
|
|
4338
|
+
tokensBefore: event.result?.tokensBefore ?? null,
|
|
4339
|
+
tokensAfter: event.result?.estimatedTokensAfter ?? null
|
|
4340
|
+
}];
|
|
4341
|
+
case "auto_retry_start":
|
|
4342
|
+
return [{ type: "retry_start", attempt: event.attempt, maxAttempts: event.maxAttempts, error: event.errorMessage }];
|
|
4343
|
+
case "auto_retry_end":
|
|
4344
|
+
return [{ type: "retry_end", failed: !event.success, error: event.finalError ?? null }];
|
|
4345
|
+
default:
|
|
4346
|
+
return [];
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
};
|
|
4350
|
+
function resultText(result) {
|
|
4351
|
+
const content2 = result?.content;
|
|
4352
|
+
if (Array.isArray(content2)) return content2.map((part) => part?.type === "text" ? String(part.text ?? "") : part?.type === "image" ? "[image]" : "").filter(Boolean).join("\n");
|
|
4353
|
+
if (typeof result === "string") return result;
|
|
4354
|
+
try {
|
|
4355
|
+
return JSON.stringify(result) ?? "";
|
|
4356
|
+
} catch {
|
|
4357
|
+
return String(result);
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
|
|
4361
|
+
// src/server/infra/pi/session.ts
|
|
4124
4362
|
var HTTP_IDLE_TIMEOUT_MS = 9e5;
|
|
4363
|
+
function compactionReserve(contextWindow) {
|
|
4364
|
+
return Math.min(65536, Math.max(16384, Math.min(32768, Math.floor(contextWindow / 4)), Math.floor(contextWindow * 0.13)));
|
|
4365
|
+
}
|
|
4125
4366
|
var PiSession = class _PiSession {
|
|
4126
4367
|
constructor(options, models, credentials) {
|
|
4127
4368
|
this.options = options;
|
|
@@ -4129,6 +4370,7 @@ var PiSession = class _PiSession {
|
|
|
4129
4370
|
this.credentials = credentials;
|
|
4130
4371
|
}
|
|
4131
4372
|
session;
|
|
4373
|
+
settings;
|
|
4132
4374
|
unsubscribe;
|
|
4133
4375
|
unsubscribeHistory;
|
|
4134
4376
|
promptActive = false;
|
|
@@ -4140,6 +4382,7 @@ var PiSession = class _PiSession {
|
|
|
4140
4382
|
abortFailure;
|
|
4141
4383
|
result = { outcome: "completed" };
|
|
4142
4384
|
signature = "";
|
|
4385
|
+
translator = new LiveTranslator();
|
|
4143
4386
|
get context() {
|
|
4144
4387
|
const usage = this.session.getContextUsage();
|
|
4145
4388
|
return usage ? { tokens: usage.tokens, contextWindow: usage.contextWindow, percent: usage.percent } : void 0;
|
|
@@ -4185,7 +4428,10 @@ var PiSession = class _PiSession {
|
|
|
4185
4428
|
const sessionManager = SessionManager.open(reference.file, sessionsDir, bot.workspacePath);
|
|
4186
4429
|
const adapter = new _PiSession(options, models, credentials);
|
|
4187
4430
|
configureHttp(HTTP_IDLE_TIMEOUT_MS);
|
|
4188
|
-
const settingsManager = SettingsManager.inMemory({
|
|
4431
|
+
const settingsManager = SettingsManager.inMemory({
|
|
4432
|
+
httpIdleTimeoutMs: HTTP_IDLE_TIMEOUT_MS,
|
|
4433
|
+
compaction: { reserveTokens: compactionReserve(model.contextWindow) }
|
|
4434
|
+
});
|
|
4189
4435
|
const tools = ["bash", "read", "edit", "write", "codemode", ...options.tools.map((tool) => tool.name)];
|
|
4190
4436
|
const resourceLoader = new DefaultResourceLoader({
|
|
4191
4437
|
cwd: bot.workspacePath,
|
|
@@ -4225,6 +4471,7 @@ var PiSession = class _PiSession {
|
|
|
4225
4471
|
tools
|
|
4226
4472
|
});
|
|
4227
4473
|
adapter.session = result.session;
|
|
4474
|
+
adapter.settings = settingsManager;
|
|
4228
4475
|
if (result.extensionsResult.errors.length || !tools.every((name2) => adapter.session.getActiveToolNames().includes(name2))) {
|
|
4229
4476
|
adapter.session.dispose();
|
|
4230
4477
|
throw new RuntimeFault("tools_missing", "Couldn't load the run's tools.");
|
|
@@ -4238,8 +4485,8 @@ var PiSession = class _PiSession {
|
|
|
4238
4485
|
for (let index = entries.length - 1; index >= 0; index--) {
|
|
4239
4486
|
const entry = entries[index];
|
|
4240
4487
|
if (entry.type !== "message" || entry.message !== event.message) continue;
|
|
4241
|
-
const
|
|
4242
|
-
if (
|
|
4488
|
+
const record2 = history.convert(entry);
|
|
4489
|
+
if (record2) await options.onHistory(record2);
|
|
4243
4490
|
break;
|
|
4244
4491
|
}
|
|
4245
4492
|
});
|
|
@@ -4281,6 +4528,7 @@ var PiSession = class _PiSession {
|
|
|
4281
4528
|
}
|
|
4282
4529
|
);
|
|
4283
4530
|
if (event.type === "message_end" && event.message.role === "assistant") this.result = this.classify(event.message);
|
|
4531
|
+
if (this.options.live) for (const live of this.translator.translate(event)) this.options.live(live);
|
|
4284
4532
|
this.maybeAbort();
|
|
4285
4533
|
}
|
|
4286
4534
|
/** The last assistant message decides the prompt (a later retry success replaces an earlier error).
|
|
@@ -4324,17 +4572,24 @@ var PiSession = class _PiSession {
|
|
|
4324
4572
|
const settled = new Promise((resolve6) => {
|
|
4325
4573
|
this.settledResolve = resolve6;
|
|
4326
4574
|
});
|
|
4575
|
+
this.options.live?.({ type: "start" });
|
|
4576
|
+
let outcome = "failed";
|
|
4327
4577
|
try {
|
|
4328
|
-
|
|
4578
|
+
try {
|
|
4579
|
+
await this.session.prompt(xml, { expandPromptTemplates: false });
|
|
4580
|
+
} finally {
|
|
4581
|
+
await this.session.waitForIdle();
|
|
4582
|
+
if (this.started && !this.settled) await settled;
|
|
4583
|
+
this.promptActive = false;
|
|
4584
|
+
await this.abortTask;
|
|
4585
|
+
this.options.store.runtime.recordSession(this.options.bot.id, this.sessionId, existsSync(this.sessionFile));
|
|
4586
|
+
}
|
|
4587
|
+
if (this.abortFailure) throw this.abortFailure;
|
|
4588
|
+
outcome = this.result.outcome;
|
|
4589
|
+
return this.result;
|
|
4329
4590
|
} finally {
|
|
4330
|
-
|
|
4331
|
-
if (this.started && !this.settled) await settled;
|
|
4332
|
-
this.promptActive = false;
|
|
4333
|
-
await this.abortTask;
|
|
4334
|
-
this.options.store.runtime.recordSession(this.options.bot.id, this.sessionId, existsSync(this.sessionFile));
|
|
4591
|
+
this.options.live?.({ type: "end", outcome });
|
|
4335
4592
|
}
|
|
4336
|
-
if (this.abortFailure) throw this.abortFailure;
|
|
4337
|
-
return this.result;
|
|
4338
4593
|
}
|
|
4339
4594
|
async configure(selection) {
|
|
4340
4595
|
const connection = this.options.store.getModelConnection(selection.connectionId), configured2 = connection?.models.find((item) => item.id === selection.modelId);
|
|
@@ -4351,6 +4606,7 @@ var PiSession = class _PiSession {
|
|
|
4351
4606
|
this.credentials.current = next;
|
|
4352
4607
|
try {
|
|
4353
4608
|
await this.session.setModel(model, { persist: false });
|
|
4609
|
+
this.settings.applyOverrides({ compaction: { reserveTokens: compactionReserve(model.contextWindow) } });
|
|
4354
4610
|
this.session.setThinkingLevel(resolveThinking(model, selection.thinkingLevel), { persist: false });
|
|
4355
4611
|
this.signature = signature;
|
|
4356
4612
|
return this.session.thinkingLevel;
|
|
@@ -4440,17 +4696,17 @@ var HistoryRepository = class {
|
|
|
4440
4696
|
const row = this.db.prepare("SELECT * FROM history WHERE id=?").get(id);
|
|
4441
4697
|
return row ? this.decode(row) : null;
|
|
4442
4698
|
}
|
|
4443
|
-
links(
|
|
4699
|
+
links(record2) {
|
|
4444
4700
|
const links = [];
|
|
4445
|
-
for (const block of
|
|
4701
|
+
for (const block of record2.message.content) {
|
|
4446
4702
|
if (block.type !== "tool_call" && block.type !== "tool_result") continue;
|
|
4447
4703
|
const callId = block.tool_call_id;
|
|
4448
4704
|
if (block.type === "tool_result") {
|
|
4449
|
-
const call = this.db.prepare(`SELECT id,role FROM history WHERE id<? AND ${CALL} ORDER BY id DESC LIMIT 1`).get(
|
|
4705
|
+
const call = this.db.prepare(`SELECT id,role FROM history WHERE id<? AND ${CALL} ORDER BY id DESC LIMIT 1`).get(record2.id, "tool_call", callId);
|
|
4450
4706
|
if (call) links.push({ ...call, tool_call_id: callId });
|
|
4451
4707
|
} else {
|
|
4452
|
-
const next = this.db.prepare(`SELECT id FROM history WHERE id>? AND ${CALL} ORDER BY id LIMIT 1`).get(
|
|
4453
|
-
const results = this.db.prepare(`SELECT id,role FROM history WHERE id>? AND id<? AND ${CALL} ORDER BY id`).all(
|
|
4708
|
+
const next = this.db.prepare(`SELECT id FROM history WHERE id>? AND ${CALL} ORDER BY id LIMIT 1`).get(record2.id, "tool_call", callId);
|
|
4709
|
+
const results = this.db.prepare(`SELECT id,role FROM history WHERE id>? AND id<? AND ${CALL} ORDER BY id`).all(record2.id, next?.id ?? Number.MAX_SAFE_INTEGER, "tool_result", callId);
|
|
4454
4710
|
links.push(...results.map((result) => ({ ...result, tool_call_id: callId })));
|
|
4455
4711
|
}
|
|
4456
4712
|
}
|
|
@@ -4494,14 +4750,14 @@ var Memory = class {
|
|
|
4494
4750
|
return this.history().usage(since);
|
|
4495
4751
|
}
|
|
4496
4752
|
read(id) {
|
|
4497
|
-
const
|
|
4498
|
-
return
|
|
4753
|
+
const record2 = this.history().read(id);
|
|
4754
|
+
return record2 ? { record: record2, links: this.history().links(record2) } : null;
|
|
4499
4755
|
}
|
|
4500
4756
|
readMany(ids, mode) {
|
|
4501
4757
|
const found = mode === "summary" ? this.history().summaries(ids).map((item) => item && summaryLine(item)) : this.history().details(ids);
|
|
4502
4758
|
const result = { mode, records: [], missingIds: [] };
|
|
4503
|
-
found.forEach((
|
|
4504
|
-
if (
|
|
4759
|
+
found.forEach((record2, index) => {
|
|
4760
|
+
if (record2) result.records.push(record2);
|
|
4505
4761
|
else result.missingIds.push(ids[index]);
|
|
4506
4762
|
});
|
|
4507
4763
|
return result;
|
|
@@ -4629,17 +4885,17 @@ var Bot = class extends Member {
|
|
|
4629
4885
|
workspace: this.workspace.location(),
|
|
4630
4886
|
tools: this.tools.definitions(),
|
|
4631
4887
|
onStatus: (status) => this.updateState(status),
|
|
4632
|
-
onHistory: (
|
|
4888
|
+
onHistory: (record2) => this.recordHistory(record2)
|
|
4633
4889
|
};
|
|
4634
4890
|
}
|
|
4635
4891
|
updateState(status) {
|
|
4636
4892
|
this.state.update(status);
|
|
4637
4893
|
}
|
|
4638
|
-
async recordHistory(
|
|
4639
|
-
const historyId = await this.memory.append(
|
|
4640
|
-
if (
|
|
4894
|
+
async recordHistory(record2) {
|
|
4895
|
+
const historyId = await this.memory.append(record2);
|
|
4896
|
+
if (record2.role !== "assistant") return;
|
|
4641
4897
|
this.callHistory.clear();
|
|
4642
|
-
for (const block of
|
|
4898
|
+
for (const block of record2.message.content) {
|
|
4643
4899
|
if (block.type === "tool_call") this.callHistory.set(block.tool_call_id, historyId);
|
|
4644
4900
|
}
|
|
4645
4901
|
}
|
|
@@ -4664,7 +4920,7 @@ var Identity = class {
|
|
|
4664
4920
|
}
|
|
4665
4921
|
snapshot() {
|
|
4666
4922
|
const bot = this.store.getBot(this.botId);
|
|
4667
|
-
return { name: bot.name, description: bot.description, persona: bot.systemPrompt };
|
|
4923
|
+
return { name: bot.name, description: bot.description, persona: bot.systemPrompt, agent: this.botId === AGENT_BOT_ID };
|
|
4668
4924
|
}
|
|
4669
4925
|
};
|
|
4670
4926
|
|
|
@@ -4749,7 +5005,7 @@ var BotState = class {
|
|
|
4749
5005
|
};
|
|
4750
5006
|
|
|
4751
5007
|
// src/server/service/bot/tools.ts
|
|
4752
|
-
import { Type as
|
|
5008
|
+
import { Type as Type5 } from "typebox";
|
|
4753
5009
|
import { Value } from "typebox/value";
|
|
4754
5010
|
|
|
4755
5011
|
// src/server/service/bot/history-read.ts
|
|
@@ -4798,20 +5054,20 @@ ${content(block.result.content, heading + "#")}`;
|
|
|
4798
5054
|
function formatHistory(result, format) {
|
|
4799
5055
|
if (format === "json") return JSON.stringify(result, null, 2);
|
|
4800
5056
|
const sections = [`# History (${result.mode})`];
|
|
4801
|
-
for (const
|
|
4802
|
-
sections.push(`## #${
|
|
4803
|
-
if (
|
|
5057
|
+
for (const record2 of result.records) {
|
|
5058
|
+
sections.push(`## #${record2.id} \xB7 ${record2.role}`, `Time: ${new Date(record2.timestamp).toISOString()}`);
|
|
5059
|
+
if (record2.model) sections.push(`Model:
|
|
4804
5060
|
|
|
4805
|
-
${jsonBlock(
|
|
4806
|
-
if ("preview" in
|
|
4807
|
-
sections.push(fence(
|
|
5061
|
+
${jsonBlock(record2.model)}`);
|
|
5062
|
+
if ("preview" in record2) {
|
|
5063
|
+
sections.push(fence(record2.preview));
|
|
4808
5064
|
continue;
|
|
4809
5065
|
}
|
|
4810
|
-
sections.push(content(
|
|
4811
|
-
if (
|
|
4812
|
-
if (
|
|
4813
|
-
if (
|
|
4814
|
-
if (
|
|
5066
|
+
sections.push(content(record2.message.content));
|
|
5067
|
+
if (record2.message.tools !== void 0) sections.push("### Tool definitions", jsonBlock(record2.message.tools));
|
|
5068
|
+
if (record2.message.tools_removed !== void 0) sections.push("### Tools removed", jsonBlock(record2.message.tools_removed));
|
|
5069
|
+
if (record2.message.stop_reason !== void 0) sections.push("### Stop reason", fence(record2.message.stop_reason));
|
|
5070
|
+
if (record2.message.error !== void 0) sections.push("### Error", fence(record2.message.error));
|
|
4815
5071
|
}
|
|
4816
5072
|
if (result.missingIds.length) sections.push(`## Missing IDs
|
|
4817
5073
|
|
|
@@ -4947,6 +5203,7 @@ var ChatTools = class {
|
|
|
4947
5203
|
if (missing.length) throw new ChatError(404, `No member named: ${missing.join(", ")} (see members.list)`);
|
|
4948
5204
|
return found.map((item) => item.member);
|
|
4949
5205
|
}
|
|
5206
|
+
/** A channel group by ID or name. */
|
|
4950
5207
|
section(value2) {
|
|
4951
5208
|
const sections = this.chats.channels.sections(this.bot);
|
|
4952
5209
|
const matches = sections.filter((section) => section.id === value2 || same(value2, section.name));
|
|
@@ -5094,7 +5351,7 @@ var QuestionOption = Type3.Object({
|
|
|
5094
5351
|
var definitions = {
|
|
5095
5352
|
"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
5353
|
"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 },
|
|
5354
|
+
"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; agent=true is the agent Bot, the owner's helper for running Jarvy: ask it to create or change a Bot, give a Bot a model, see a Bot's usage, or delete a Bot, channel or channel group; 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
5355
|
"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 },
|
|
5099
5356
|
"chats.info": {
|
|
5100
5357
|
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.",
|
|
@@ -5122,7 +5379,7 @@ var definitions = {
|
|
|
5122
5379
|
}, { additionalProperties: false })
|
|
5123
5380
|
},
|
|
5124
5381
|
"chats.addMembers": {
|
|
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.",
|
|
5382
|
+
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, and the agent Bot (see bots.list) once the owner confirms.",
|
|
5126
5383
|
schema: Type3.Object({ chatId: Type3.String(), members: Type3.Array(Type3.String({ minLength: 1, maxLength: 80 }), { minItems: 1, maxItems: 50 }) }, { additionalProperties: false })
|
|
5127
5384
|
},
|
|
5128
5385
|
"chats.subscribe": {
|
|
@@ -5201,6 +5458,7 @@ function botRows(chats, viewerId, entries) {
|
|
|
5201
5458
|
return { self: entry.id === viewerId, row: {
|
|
5202
5459
|
name: entry.name,
|
|
5203
5460
|
...entry.id === viewerId ? { self: true } : {},
|
|
5461
|
+
...entry.agent ? { agent: true } : {},
|
|
5204
5462
|
status: entry.status.status,
|
|
5205
5463
|
...running && entry.status.activity ? { activity: entry.status.activity } : {},
|
|
5206
5464
|
...visible.length ? { chats: visible } : {},
|
|
@@ -5212,19 +5470,177 @@ function botRows(chats, viewerId, entries) {
|
|
|
5212
5470
|
return rows2.map((item) => item.row);
|
|
5213
5471
|
}
|
|
5214
5472
|
|
|
5473
|
+
// src/server/service/bot/agent-tools.ts
|
|
5474
|
+
var AgentTools = class {
|
|
5475
|
+
constructor(scope) {
|
|
5476
|
+
this.scope = scope;
|
|
5477
|
+
}
|
|
5478
|
+
get ops() {
|
|
5479
|
+
return this.scope.ops;
|
|
5480
|
+
}
|
|
5481
|
+
async confirmed(input, ask) {
|
|
5482
|
+
const chat = this.scope.tools.chat(input.askIn);
|
|
5483
|
+
if (!chat.memberIds.includes(this.ops.owner)) throw new ChatError(400, "The owner is not in askIn; use a chat they are in, such as your DM with them");
|
|
5484
|
+
const answer = await ask(chat.id, {
|
|
5485
|
+
question: input.question,
|
|
5486
|
+
multiple: false,
|
|
5487
|
+
allowCustom: false,
|
|
5488
|
+
options: [{ label: input.confirm, value: "confirm", style: "danger" }, { label: input.cancel, value: "cancel" }]
|
|
5489
|
+
});
|
|
5490
|
+
if (answer.status === "dismissed") return { deleted: false, answer: "dismissed" };
|
|
5491
|
+
return answer.selected[0]?.value === "confirm" ? null : { deleted: false, answer: "cancelled" };
|
|
5492
|
+
}
|
|
5493
|
+
channel(chatId) {
|
|
5494
|
+
const chat = this.scope.tools.chat(chatId);
|
|
5495
|
+
if (chat.type !== "channel") throw new ChatError(400, "Not a channel");
|
|
5496
|
+
return chat;
|
|
5497
|
+
}
|
|
5498
|
+
async call(name2, args, ask) {
|
|
5499
|
+
switch (name2) {
|
|
5500
|
+
case "models.list":
|
|
5501
|
+
return this.ops.models();
|
|
5502
|
+
case "bots.create": {
|
|
5503
|
+
const bot = await this.ops.create(args);
|
|
5504
|
+
return { name: bot.name, model: bot.connectionId ? this.ops.modelLabel(bot) : null, thinking: bot.thinkingLevel };
|
|
5505
|
+
}
|
|
5506
|
+
case "bots.update": {
|
|
5507
|
+
const { bot, ...settings } = args;
|
|
5508
|
+
const result = await this.ops.update(this.ops.bot(bot), settings);
|
|
5509
|
+
return { name: result.bot.name, changed: result.changed };
|
|
5510
|
+
}
|
|
5511
|
+
case "bots.start":
|
|
5512
|
+
case "bots.stop": {
|
|
5513
|
+
const bot = this.ops.bot(args.bot);
|
|
5514
|
+
return { name: bot.name, status: await this.ops.run(bot, name2 === "bots.start" ? "start" : "stop") };
|
|
5515
|
+
}
|
|
5516
|
+
case "bots.usage": {
|
|
5517
|
+
const input = args, bot = this.ops.bot(input.bot);
|
|
5518
|
+
return { name: bot.name, ...this.ops.usage(bot, input.days ?? 7) };
|
|
5519
|
+
}
|
|
5520
|
+
case "bots.delete": {
|
|
5521
|
+
const input = args, bot = this.ops.bot(input.bot);
|
|
5522
|
+
if (bot.id === this.ops.agentId) throw new ChatError(400, "You can't delete yourself");
|
|
5523
|
+
const declined = await this.confirmed(input, ask);
|
|
5524
|
+
if (declined) return declined;
|
|
5525
|
+
await this.ops.deleteBot(this.ops.bot(input.bot));
|
|
5526
|
+
return { deleted: true, name: bot.name };
|
|
5527
|
+
}
|
|
5528
|
+
case "chats.delete": {
|
|
5529
|
+
const input = args, chat = this.channel(input.chatId);
|
|
5530
|
+
const declined = await this.confirmed(input, ask);
|
|
5531
|
+
if (declined) return declined;
|
|
5532
|
+
await this.ops.deleteChannel(chat.id);
|
|
5533
|
+
return { deleted: true, name: chat.title };
|
|
5534
|
+
}
|
|
5535
|
+
case "chats.removeMembers": {
|
|
5536
|
+
const input = args, chat = this.channel(input.chatId);
|
|
5537
|
+
const members = this.scope.tools.members(input.members).filter((member) => chat.memberIds.includes(member.id));
|
|
5538
|
+
if (!members.length) throw new ChatError(400, "None of them is in this channel");
|
|
5539
|
+
const declined = await this.confirmed(input, ask);
|
|
5540
|
+
if (declined) return { removed: [], answer: declined.answer };
|
|
5541
|
+
for (const member of members) this.ops.removeMember(chat.id, member.id);
|
|
5542
|
+
return { removed: members.map((member) => member.name) };
|
|
5543
|
+
}
|
|
5544
|
+
case "sections.delete": {
|
|
5545
|
+
const input = args, section = this.scope.tools.section(input.section);
|
|
5546
|
+
const declined = await this.confirmed(input, ask);
|
|
5547
|
+
if (declined) return declined;
|
|
5548
|
+
this.ops.deleteSection(section);
|
|
5549
|
+
return { deleted: true };
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
};
|
|
5554
|
+
|
|
5555
|
+
// src/server/service/bot/agent-schemas.ts
|
|
5556
|
+
import { Type as Type4 } from "typebox";
|
|
5557
|
+
var None2 = Type4.Object({}, { additionalProperties: false });
|
|
5558
|
+
var Name2 = Type4.String({ minLength: 1, maxLength: 80, description: "Unique among all members, people and Bots" });
|
|
5559
|
+
var BotName = Type4.String({ minLength: 1, maxLength: 80, description: "The Bot's name, as bots.list shows it" });
|
|
5560
|
+
var Model2 = Type4.Object({
|
|
5561
|
+
connection: Type4.String({ minLength: 1, maxLength: 200, description: "A connection's name or ID from models.list" }),
|
|
5562
|
+
model: Type4.String({ minLength: 1, maxLength: 200, description: "A model's ID or name in that connection" })
|
|
5563
|
+
}, { additionalProperties: false });
|
|
5564
|
+
var Thinking = Type4.Union(
|
|
5565
|
+
["auto", "off", "minimal", "low", "medium", "high", "xhigh", "max"].map((level) => Type4.Literal(level)),
|
|
5566
|
+
{ description: "How much the model thinks; auto lets Jarvy choose. models.list gives the levels each model has" }
|
|
5567
|
+
);
|
|
5568
|
+
var Settings = {
|
|
5569
|
+
description: Type4.Optional(Type4.String({ maxLength: 1e3, description: "One line that other members see in members.list" })),
|
|
5570
|
+
persona: Type4.Optional(Type4.String({ maxLength: 16e3, description: "Its standing instructions: who it is and how it works (the Persona section of its prompt)" })),
|
|
5571
|
+
model: Type4.Optional(Model2),
|
|
5572
|
+
thinking: Type4.Optional(Thinking)
|
|
5573
|
+
};
|
|
5574
|
+
var Confirm = {
|
|
5575
|
+
askIn: Type4.String({ description: "ID of a chat the owner is in, where the confirmation card goes; usually the chat you were asked in" }),
|
|
5576
|
+
question: Type4.String({ minLength: 1, maxLength: 500, description: "The card's question, in the owner's language: what will be deleted and what goes with it" }),
|
|
5577
|
+
confirm: Type4.String({ minLength: 1, maxLength: 40, description: "Label of the button that deletes, in the owner's language" }),
|
|
5578
|
+
cancel: Type4.String({ minLength: 1, maxLength: 40, description: "Label of the button that keeps everything, in the owner's language" })
|
|
5579
|
+
};
|
|
5580
|
+
var after = "The owner first gets a card in askIn, and nothing is deleted unless they confirm; the result says what they chose.";
|
|
5581
|
+
var agentDefinitions = {
|
|
5582
|
+
"models.list": { description: "List the models you can give a Bot: each connection with its models and their thinking levels. Only the owner adds connections, keys and logins, in Settings.", schema: None2 },
|
|
5583
|
+
"bots.create": {
|
|
5584
|
+
description: "Create a Bot. Give it a model so it can work; without one it waits until it gets one. It starts when a message first arrives for it. bots.update changes everything later.",
|
|
5585
|
+
schema: Type4.Object({ name: Name2, ...Settings }, { additionalProperties: false })
|
|
5586
|
+
},
|
|
5587
|
+
"bots.update": {
|
|
5588
|
+
description: "Change a Bot's name, description, persona, model or thinking; give only what changes. Works for any Bot, yourself included. The Bot is told what changed with its next batch, in its DM with you.",
|
|
5589
|
+
schema: Type4.Object({ bot: BotName, name: Type4.Optional(Name2), ...Settings }, { additionalProperties: false })
|
|
5590
|
+
},
|
|
5591
|
+
"bots.start": { description: "Start a stopped Bot. A stopped Bot also starts by itself when a message arrives for it.", schema: Type4.Object({ bot: BotName }, { additionalProperties: false }) },
|
|
5592
|
+
"bots.stop": { description: "Stop a Bot (not yourself): its current turn ends, and it stays stopped until it is started or a message arrives for it.", schema: Type4.Object({ bot: BotName }, { additionalProperties: false }) },
|
|
5593
|
+
"bots.usage": {
|
|
5594
|
+
description: "A Bot's token use and cost over the last days (7 by default), per model.",
|
|
5595
|
+
schema: Type4.Object({ bot: BotName, days: Type4.Optional(Type4.Integer({ minimum: 1, maximum: 365 })) }, { additionalProperties: false })
|
|
5596
|
+
},
|
|
5597
|
+
"bots.delete": {
|
|
5598
|
+
description: `Delete a Bot (not yourself). ${after} Its DMs and channel memberships go; its messages in channels and its workspace stay.`,
|
|
5599
|
+
schema: Type4.Object({ bot: BotName, ...Confirm }, { additionalProperties: false })
|
|
5600
|
+
},
|
|
5601
|
+
"chats.delete": {
|
|
5602
|
+
description: `Delete a channel you are in, with its threads, messages and files. ${after}`,
|
|
5603
|
+
schema: Type4.Object({ chatId: Type4.String({ description: "Channel ID" }), ...Confirm }, { additionalProperties: false })
|
|
5604
|
+
},
|
|
5605
|
+
"chats.removeMembers": {
|
|
5606
|
+
description: `Remove members from a channel you are in, by name (see members.list). ${after}`,
|
|
5607
|
+
schema: Type4.Object({ chatId: Type4.String({ description: "Channel ID" }), members: Type4.Array(Type4.String({ minLength: 1, maxLength: 80 }), { minItems: 1, maxItems: 50 }), ...Confirm }, { additionalProperties: false })
|
|
5608
|
+
},
|
|
5609
|
+
"sections.delete": {
|
|
5610
|
+
description: `Delete a channel group; its channels stay, outside any group. ${after}`,
|
|
5611
|
+
schema: Type4.Object({ section: Type4.String({ minLength: 1, maxLength: 80, description: "The group's name or ID (see sections.list)" }), ...Confirm }, { additionalProperties: false })
|
|
5612
|
+
}
|
|
5613
|
+
};
|
|
5614
|
+
|
|
5215
5615
|
// src/server/service/bot/tools.ts
|
|
5216
|
-
var Gateway =
|
|
5217
|
-
action:
|
|
5218
|
-
tool:
|
|
5219
|
-
args:
|
|
5616
|
+
var Gateway = Type5.Object({
|
|
5617
|
+
action: Type5.Union([Type5.Literal("list"), Type5.Literal("describe"), Type5.Literal("call")]),
|
|
5618
|
+
tool: Type5.Optional(Type5.String()),
|
|
5619
|
+
args: Type5.Optional(Type5.Record(Type5.String(), Type5.Unknown()))
|
|
5220
5620
|
}, { additionalProperties: false });
|
|
5221
5621
|
var Tools = class {
|
|
5222
5622
|
constructor(scope) {
|
|
5223
5623
|
this.scope = scope;
|
|
5224
5624
|
this.tools = new ChatTools(scope);
|
|
5625
|
+
if (scope.agent) this.agent = new AgentTools({ ops: scope.agent, tools: this.tools });
|
|
5225
5626
|
}
|
|
5226
5627
|
send;
|
|
5227
5628
|
tools;
|
|
5629
|
+
/** The agent Bot's own tools (A48); other Bots have none. */
|
|
5630
|
+
agent;
|
|
5631
|
+
get catalog() {
|
|
5632
|
+
return this.agent ? { ...definitions, ...agentDefinitions } : definitions;
|
|
5633
|
+
}
|
|
5634
|
+
/** Posts a card and waits for the person's answer (messages.ask, and the agent's confirmations). */
|
|
5635
|
+
async ask(chatId, question, toolCallId, signal) {
|
|
5636
|
+
const sent = await this.send(chatId, { content: question.question, question, toolCallId, signal });
|
|
5637
|
+
this.scope.asking?.(true);
|
|
5638
|
+
try {
|
|
5639
|
+
return { messageId: sent.id, ...await this.scope.chats.questions.wait(this.scope.chats.message(this.scope.botId, sent.id), signal) };
|
|
5640
|
+
} finally {
|
|
5641
|
+
if (!signal?.aborted) this.scope.asking?.(false);
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5228
5644
|
bindSend(send) {
|
|
5229
5645
|
this.send = send;
|
|
5230
5646
|
}
|
|
@@ -5238,16 +5654,18 @@ var Tools = class {
|
|
|
5238
5654
|
signal?.throwIfAborted();
|
|
5239
5655
|
if (raw.action === "list") {
|
|
5240
5656
|
if (raw.tool !== void 0 || raw.args !== void 0) throw new ChatError(400, "list does not accept tool or args");
|
|
5241
|
-
return Object.entries(
|
|
5657
|
+
return Object.entries(this.catalog).map(([name3, tool]) => ({ name: name3, description: tool.description }));
|
|
5242
5658
|
}
|
|
5243
|
-
if (!raw.tool || !Object.hasOwn(
|
|
5244
|
-
const name2 = raw.tool, definition =
|
|
5659
|
+
if (!raw.tool || !Object.hasOwn(this.catalog, raw.tool)) throw new ChatError(400, "Unknown chat tool; use list first");
|
|
5660
|
+
const name2 = raw.tool, definition = this.catalog[name2];
|
|
5245
5661
|
if (raw.action === "describe") {
|
|
5246
5662
|
if (raw.args !== void 0) throw new ChatError(400, "describe does not accept args");
|
|
5247
5663
|
return { name: name2, description: definition.description, parameters: definition.schema };
|
|
5248
5664
|
}
|
|
5249
5665
|
const args = raw.args;
|
|
5250
5666
|
if (!args || !Value.Check(definition.schema, args)) throw new ChatError(400, "Invalid tool arguments; use describe for the parameter schema");
|
|
5667
|
+
if (this.agent && Object.hasOwn(agentDefinitions, name2))
|
|
5668
|
+
return this.agent.call(name2, args, (chatId, question) => this.ask(chatId, question, toolCallId, signal));
|
|
5251
5669
|
switch (name2) {
|
|
5252
5670
|
case "history.read":
|
|
5253
5671
|
return readHistory(this.scope.memory, this.scope.workspace, args, signal);
|
|
@@ -5337,13 +5755,7 @@ var Tools = class {
|
|
|
5337
5755
|
multiple: parsed.multiple === true,
|
|
5338
5756
|
allowCustom: parsed.allowCustom === true
|
|
5339
5757
|
};
|
|
5340
|
-
|
|
5341
|
-
this.scope.asking?.(true);
|
|
5342
|
-
try {
|
|
5343
|
-
return { messageId: sent.id, ...await this.scope.chats.questions.wait(this.scope.chats.message(this.scope.botId, sent.id), signal) };
|
|
5344
|
-
} finally {
|
|
5345
|
-
if (!signal?.aborted) this.scope.asking?.(false);
|
|
5346
|
-
}
|
|
5758
|
+
return this.ask(parsed.chatId, question, toolCallId, signal);
|
|
5347
5759
|
}
|
|
5348
5760
|
}
|
|
5349
5761
|
}
|
|
@@ -5356,6 +5768,12 @@ import { join as join5, dirname as dirname4 } from "node:path";
|
|
|
5356
5768
|
|
|
5357
5769
|
// src/server/service/chat/envelope.ts
|
|
5358
5770
|
import { isAbsolute } from "node:path";
|
|
5771
|
+
|
|
5772
|
+
// src/server/shared/agent.ts
|
|
5773
|
+
var WELCOME_LANGUAGES = { en: "English", zh: "Chinese" };
|
|
5774
|
+
var welcomeLanguage = (value2) => typeof value2 === "string" && Object.hasOwn(WELCOME_LANGUAGES, value2) ? value2 : "en";
|
|
5775
|
+
|
|
5776
|
+
// src/server/service/chat/envelope.ts
|
|
5359
5777
|
var INVALID_XML = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/u;
|
|
5360
5778
|
function escape(value2) {
|
|
5361
5779
|
if (INVALID_XML.test(value2)) throw new Error("Invalid XML character");
|
|
@@ -5371,6 +5789,7 @@ function remember(map, id, definition) {
|
|
|
5371
5789
|
map.set(id, definition);
|
|
5372
5790
|
}
|
|
5373
5791
|
var iso2 = (time) => new Date(time).toISOString().replace(".000Z", "Z");
|
|
5792
|
+
var welcome = (owner, language) => `${owner} has just set up Jarvy and picked your model. You are their agent Bot. In this DM, welcome them in ${WELCOME_LANGUAGES[language]}, the language of their interface: say in a few sentences what Jarvy is and how you can help, such as building a team of Bots for their work and running Jarvy for them. Then ask what they want to get done: a software project, writing, research or something else.`;
|
|
5374
5793
|
function encodeNoticeBatch(botId, notices) {
|
|
5375
5794
|
if (!notices.length) throw new Error("Empty notice batch");
|
|
5376
5795
|
const chats = /* @__PURE__ */ new Map(), members = /* @__PURE__ */ new Map();
|
|
@@ -5379,14 +5798,16 @@ function encodeNoticeBatch(botId, notices) {
|
|
|
5379
5798
|
}
|
|
5380
5799
|
function noticeTail(notices, chats, members) {
|
|
5381
5800
|
for (const notice of notices) {
|
|
5382
|
-
if (!chats.has(notice.chat.id)) chats.set(notice.chat.id, `<chat${attribute("id", notice.chat.id)}${attribute("type", notice.chat.type)}${attribute("title", notice.chat.title)}/>`);
|
|
5801
|
+
if (!chats.has(notice.chat.id)) chats.set(notice.chat.id, `<chat${attribute("id", notice.chat.id)}${attribute("type", notice.chat.type)}${notice.chat.title ? attribute("title", notice.chat.title) : ""}/>`);
|
|
5383
5802
|
remember(members, notice.actor.id, `<member${attribute("id", notice.actor.id)}${attribute("name", notice.actor.name)}${attribute("kind", notice.actor.kind)}/>`);
|
|
5384
5803
|
}
|
|
5385
5804
|
const latest = /* @__PURE__ */ new Map();
|
|
5386
5805
|
for (const notice of notices) if (notice.kind === "note" && (latest.get(notice.chat.id)?.version ?? 0) < notice.version) latest.set(notice.chat.id, notice);
|
|
5387
5806
|
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))}/>`);
|
|
5388
5807
|
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>`]);
|
|
5389
|
-
|
|
5808
|
+
const changes = notices.flatMap((notice) => notice.kind !== "change" ? [] : [`<change${attribute("chat", notice.chat.id)}${attribute("by", notice.actor.id)}${attribute("what", notice.what)}${attribute("at", iso2(notice.createdAt))}${notice.value === null ? "/>" : `>${escape(notice.value)}</change>`}`]);
|
|
5809
|
+
const onboard = notices.flatMap((notice) => notice.kind !== "onboard" ? [] : [`<onboard${attribute("chat", notice.chat.id)}${attribute("by", notice.actor.id)}${attribute("at", iso2(notice.createdAt))}><note>${escape(welcome(notice.actor.name, notice.language))}</note></onboard>`]).slice(-1);
|
|
5810
|
+
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>` : "") + (changes.length ? `<changes>${changes.join("")}<note>The agent Bot changed your settings, and the changes already apply. A persona change is already in your instructions. If something is unclear, ask it in that DM.</note></changes>` : "") + onboard.join("");
|
|
5390
5811
|
}
|
|
5391
5812
|
function encodeChatBatch(botId, messages, digest, notices) {
|
|
5392
5813
|
if (!messages.length) throw new Error("Empty chat batch");
|
|
@@ -5422,7 +5843,8 @@ var BotInbox = class {
|
|
|
5422
5843
|
this.chats = chats;
|
|
5423
5844
|
this.files = files;
|
|
5424
5845
|
}
|
|
5425
|
-
/** Messages and other deliveries wait in the inbox (A41); either kind is work for the next run.
|
|
5846
|
+
/** Messages and other deliveries wait in the inbox (A41); either kind is work for the next run. A digest delivery
|
|
5847
|
+
* (a change the agent Bot made, A48) is not work of its own: it rides along with the next batch. */
|
|
5426
5848
|
pending() {
|
|
5427
5849
|
return this.repository.hasPending(this.botId) || this.hasNotices();
|
|
5428
5850
|
}
|
|
@@ -5430,7 +5852,7 @@ var BotInbox = class {
|
|
|
5430
5852
|
return this.repository.claim(this.botId, selection, this.hasNotices());
|
|
5431
5853
|
}
|
|
5432
5854
|
hasNotices() {
|
|
5433
|
-
return this.chats.notices(this.botId).
|
|
5855
|
+
return this.chats.notices(this.botId).some((notice) => notice.urgency !== "digest");
|
|
5434
5856
|
}
|
|
5435
5857
|
reference(workspace) {
|
|
5436
5858
|
if (this.repository.hasUncertainModel(this.botId)) throw new RuntimeFault("model_switch_uncertain", "The model switch didn't finish. Pick the model again.");
|
|
@@ -5454,6 +5876,11 @@ var BotInbox = class {
|
|
|
5454
5876
|
return notices.map((notice) => {
|
|
5455
5877
|
const chat = { id: notice.chatId, type: notice.chatType, title: notice.chatTitle };
|
|
5456
5878
|
if (notice.kind === "note") return { kind: "note", id: notice.id, chat, actor: notice.actor, version: Number(notice.detail), createdAt: notice.createdAt };
|
|
5879
|
+
if (notice.kind === "onboard") return { kind: "onboard", id: notice.id, chat, actor: notice.actor, language: welcomeLanguage(notice.detail), createdAt: notice.createdAt };
|
|
5880
|
+
if (notice.kind === "change") {
|
|
5881
|
+
const change = JSON.parse(notice.detail);
|
|
5882
|
+
return { kind: "change", id: notice.id, chat, actor: notice.actor, what: change.what, value: change.value, createdAt: notice.createdAt };
|
|
5883
|
+
}
|
|
5457
5884
|
const marked = notice.messageId ? this.chats.message(this.botId, notice.messageId) : null;
|
|
5458
5885
|
const text3 = marked ? previewQuotes(marked.content, marked.quotes).slice(0, 120) : "";
|
|
5459
5886
|
return { kind: "reaction", id: notice.id, chat, message: { id: notice.messageId ?? "", text: text3 }, actor: notice.actor, emoji: notice.detail, createdAt: notice.createdAt };
|
|
@@ -5701,6 +6128,14 @@ var AgentRuntime = class {
|
|
|
5701
6128
|
};
|
|
5702
6129
|
|
|
5703
6130
|
// src/server/service/bot/prompt.ts
|
|
6131
|
+
var AGENT_ROLE = `
|
|
6132
|
+
## Your role
|
|
6133
|
+
You are the owner's agent Bot. Besides working like any Bot, you help them set up and run Jarvy itself.
|
|
6134
|
+
Help a new owner get started: find out what they want to get done, suggest a team of Bots for it, then create and set up those Bots.
|
|
6135
|
+
The chat gateway lists tools only you have: models.list, bots.create, bots.update, bots.start, bots.stop and bots.usage. bots.delete, chats.delete, chats.removeMembers and sections.delete first ask the owner on a card, and delete nothing unless they confirm.
|
|
6136
|
+
Other Bots may ask you to do these things for them. Do the ordinary ones; deleting always waits for the owner's confirmation.
|
|
6137
|
+
You can't add model connections, keys or logins. The owner does that in Settings, so point them there when a model is missing.
|
|
6138
|
+
`;
|
|
5704
6139
|
function botPrompt(setup) {
|
|
5705
6140
|
const bot = { ...setup.identity(), workspacePath: setup.workspace.cwd };
|
|
5706
6141
|
return `# Persona
|
|
@@ -5711,7 +6146,7 @@ ${bot.persona}
|
|
|
5711
6146
|
## Environment
|
|
5712
6147
|
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
6148
|
Here your name is ${bot.name}, and your description is: ${bot.description}
|
|
5714
|
-
|
|
6149
|
+
${bot.agent ? AGENT_ROLE : ""}
|
|
5715
6150
|
## Language
|
|
5716
6151
|
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
6152
|
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.
|
|
@@ -5739,44 +6174,426 @@ When something will take a while, post a short progress note partway through, an
|
|
|
5739
6174
|
`;
|
|
5740
6175
|
}
|
|
5741
6176
|
|
|
6177
|
+
// src/server/shared/live.ts
|
|
6178
|
+
var LIVE_STEPS = 300;
|
|
6179
|
+
var LIVE_TEXT = 4e3;
|
|
6180
|
+
var LIVE_OUTPUT_END = 1200;
|
|
6181
|
+
var LIVE_INPUT = 300;
|
|
6182
|
+
var LIVE_MESSAGE = 2e3;
|
|
6183
|
+
|
|
6184
|
+
// src/server/service/bot/live-input.ts
|
|
6185
|
+
function clip(text3, length) {
|
|
6186
|
+
return text3.length > length ? `${text3.slice(0, length - 1)}\u2026` : text3;
|
|
6187
|
+
}
|
|
6188
|
+
var field = (value2, length = LIVE_INPUT) => typeof value2 === "string" ? clip(value2, length) : void 0;
|
|
6189
|
+
var record = (value2) => value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : {};
|
|
6190
|
+
function liveInput(tool, args, chat) {
|
|
6191
|
+
const values = record(args);
|
|
6192
|
+
switch (tool) {
|
|
6193
|
+
case "bash":
|
|
6194
|
+
return { command: field(values.command) };
|
|
6195
|
+
case "read":
|
|
6196
|
+
case "edit":
|
|
6197
|
+
case "write":
|
|
6198
|
+
return { path: field(values.path) };
|
|
6199
|
+
case "codemode":
|
|
6200
|
+
return { command: field(values.code) };
|
|
6201
|
+
case "chat":
|
|
6202
|
+
return gateway(values, chat);
|
|
6203
|
+
default: {
|
|
6204
|
+
let json2 = "";
|
|
6205
|
+
try {
|
|
6206
|
+
json2 = JSON.stringify(args) ?? "";
|
|
6207
|
+
} catch {
|
|
6208
|
+
}
|
|
6209
|
+
return json2 ? { args: clip(json2, LIVE_INPUT) } : {};
|
|
6210
|
+
}
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
function gateway(values, chat) {
|
|
6214
|
+
const action = values.action === "call" && typeof values.tool === "string" ? values.tool : typeof values.action === "string" ? values.action : "";
|
|
6215
|
+
const inner = record(values.args), where = target(inner, chat);
|
|
6216
|
+
if (action === "messages.send") return { action, chat: where, text: field(inner.message, LIVE_MESSAGE) };
|
|
6217
|
+
if (action === "messages.ask") return {
|
|
6218
|
+
action,
|
|
6219
|
+
chat: where,
|
|
6220
|
+
text: field(inner.question, LIVE_MESSAGE),
|
|
6221
|
+
options: Array.isArray(inner.options) ? inner.options.map((option2) => field(record(option2).label) ?? "").filter(Boolean) : void 0
|
|
6222
|
+
};
|
|
6223
|
+
return where ? { action, chat: where } : { action };
|
|
6224
|
+
}
|
|
6225
|
+
function target(inner, chat) {
|
|
6226
|
+
if (typeof inner.to === "string") return { id: null, type: "dm", title: clip(inner.to, 80) };
|
|
6227
|
+
return typeof inner.chatId === "string" ? chat(inner.chatId) ?? void 0 : void 0;
|
|
6228
|
+
}
|
|
6229
|
+
function sentChat(output, chat) {
|
|
6230
|
+
try {
|
|
6231
|
+
const chatId = JSON.parse(output).chatId;
|
|
6232
|
+
return typeof chatId === "string" ? chat(chatId) ?? void 0 : void 0;
|
|
6233
|
+
} catch {
|
|
6234
|
+
return void 0;
|
|
6235
|
+
}
|
|
6236
|
+
}
|
|
6237
|
+
|
|
6238
|
+
// src/server/service/bot/live.ts
|
|
6239
|
+
var FLUSH_MS = 100;
|
|
6240
|
+
function liveOutput(text3) {
|
|
6241
|
+
if (!text3) return null;
|
|
6242
|
+
if (text3.length <= 2 * LIVE_OUTPUT_END) return { head: text3, tail: "", omitted: 0 };
|
|
6243
|
+
return { head: text3.slice(0, LIVE_OUTPUT_END), tail: text3.slice(-LIVE_OUTPUT_END), omitted: text3.length - 2 * LIVE_OUTPUT_END };
|
|
6244
|
+
}
|
|
6245
|
+
var LiveTurns = class {
|
|
6246
|
+
constructor(chat) {
|
|
6247
|
+
this.chat = chat;
|
|
6248
|
+
}
|
|
6249
|
+
turn = null;
|
|
6250
|
+
steps = /* @__PURE__ */ new Map();
|
|
6251
|
+
blocks = /* @__PURE__ */ new Map();
|
|
6252
|
+
compaction = null;
|
|
6253
|
+
retry = null;
|
|
6254
|
+
counter = 0;
|
|
6255
|
+
pending = [];
|
|
6256
|
+
timer = null;
|
|
6257
|
+
listeners = /* @__PURE__ */ new Map();
|
|
6258
|
+
/** Listens from now on and returns the turn so far (null when the Bot is not running one). Pending changes go to
|
|
6259
|
+
* the earlier listeners first, so the snapshot and the next patch never overlap. */
|
|
6260
|
+
subscribe(listener, close) {
|
|
6261
|
+
this.flush();
|
|
6262
|
+
this.listeners.set(listener, close);
|
|
6263
|
+
return { turn: this.turn ? structuredClone(this.turn) : null, unsubscribe: () => {
|
|
6264
|
+
this.listeners.delete(listener);
|
|
6265
|
+
} };
|
|
6266
|
+
}
|
|
6267
|
+
/** Ends every listener's stream (the Bot was deleted, or the server is stopping). */
|
|
6268
|
+
close() {
|
|
6269
|
+
for (const close of [...this.listeners.values()]) close();
|
|
6270
|
+
this.listeners.clear();
|
|
6271
|
+
}
|
|
6272
|
+
event(event) {
|
|
6273
|
+
if (event.type === "start") {
|
|
6274
|
+
this.flush();
|
|
6275
|
+
this.reset();
|
|
6276
|
+
this.turn = { startedAt: Date.now(), steps: [], dropped: 0 };
|
|
6277
|
+
this.emit({ type: "start", turn: structuredClone(this.turn) });
|
|
6278
|
+
return;
|
|
6279
|
+
}
|
|
6280
|
+
if (!this.turn) return;
|
|
6281
|
+
switch (event.type) {
|
|
6282
|
+
case "end": {
|
|
6283
|
+
for (const step of this.turn.steps) if (step.state === "running") this.finish(step, event.outcome !== "completed");
|
|
6284
|
+
this.flush();
|
|
6285
|
+
this.emit({ type: "end", outcome: event.outcome, endedAt: Date.now() });
|
|
6286
|
+
this.reset();
|
|
6287
|
+
return;
|
|
6288
|
+
}
|
|
6289
|
+
case "block_start":
|
|
6290
|
+
this.blocks.set(event.key, this.add({ kind: event.kind, text: "", omitted: 0 }).id);
|
|
6291
|
+
return;
|
|
6292
|
+
case "block_delta": {
|
|
6293
|
+
let id = this.blocks.get(event.key);
|
|
6294
|
+
if (!id) {
|
|
6295
|
+
id = this.add({ kind: event.kind, text: "", omitted: 0 }).id;
|
|
6296
|
+
this.blocks.set(event.key, id);
|
|
6297
|
+
}
|
|
6298
|
+
const step = this.steps.get(id);
|
|
6299
|
+
if (!step) return;
|
|
6300
|
+
const text3 = (step.text ?? "") + event.delta, over = Math.max(0, text3.length - LIVE_TEXT);
|
|
6301
|
+
step.text = text3.slice(over);
|
|
6302
|
+
step.omitted = (step.omitted ?? 0) + over;
|
|
6303
|
+
this.queue({ op: "text", id, append: event.delta });
|
|
6304
|
+
return;
|
|
6305
|
+
}
|
|
6306
|
+
case "block_end": {
|
|
6307
|
+
const step = this.steps.get(this.blocks.get(event.key) ?? "");
|
|
6308
|
+
if (step) this.finish(step, false);
|
|
6309
|
+
this.blocks.delete(event.key);
|
|
6310
|
+
return;
|
|
6311
|
+
}
|
|
6312
|
+
case "tool_start":
|
|
6313
|
+
this.add({ id: event.id, kind: "tool", parentId: event.parentId, tool: event.name, input: liveInput(event.name, event.args, this.chat) });
|
|
6314
|
+
return;
|
|
6315
|
+
case "tool_end": {
|
|
6316
|
+
const step = this.steps.get(event.id);
|
|
6317
|
+
if (!step) return;
|
|
6318
|
+
step.output = liveOutput(event.output);
|
|
6319
|
+
if (step.input?.action === "messages.send" && !event.failed) step.input.chat = sentChat(event.output, this.chat) ?? step.input.chat;
|
|
6320
|
+
this.finish(step, event.failed);
|
|
6321
|
+
return;
|
|
6322
|
+
}
|
|
6323
|
+
case "compaction_start":
|
|
6324
|
+
this.compaction = this.add({ kind: "compaction" }).id;
|
|
6325
|
+
return;
|
|
6326
|
+
case "compaction_end": {
|
|
6327
|
+
const step = this.steps.get(this.compaction ?? "");
|
|
6328
|
+
this.compaction = null;
|
|
6329
|
+
if (!step) return;
|
|
6330
|
+
step.tokensBefore = event.tokensBefore;
|
|
6331
|
+
step.tokensAfter = event.tokensAfter;
|
|
6332
|
+
step.output = event.error ? liveOutput(event.error) : null;
|
|
6333
|
+
this.finish(step, event.failed);
|
|
6334
|
+
return;
|
|
6335
|
+
}
|
|
6336
|
+
case "retry_start": {
|
|
6337
|
+
const previous = this.steps.get(this.retry ?? "");
|
|
6338
|
+
if (previous?.state === "running") this.finish(previous, true);
|
|
6339
|
+
this.retry = this.add({ kind: "retry", attempt: event.attempt, maxAttempts: event.maxAttempts, output: liveOutput(event.error) }).id;
|
|
6340
|
+
return;
|
|
6341
|
+
}
|
|
6342
|
+
case "retry_end": {
|
|
6343
|
+
const step = this.steps.get(this.retry ?? "");
|
|
6344
|
+
this.retry = null;
|
|
6345
|
+
if (step) {
|
|
6346
|
+
if (event.error) step.output = liveOutput(event.error);
|
|
6347
|
+
this.finish(step, event.failed);
|
|
6348
|
+
}
|
|
6349
|
+
return;
|
|
6350
|
+
}
|
|
6351
|
+
}
|
|
6352
|
+
}
|
|
6353
|
+
add(fields) {
|
|
6354
|
+
const turn = this.turn;
|
|
6355
|
+
if (turn.steps.length >= LIVE_STEPS) {
|
|
6356
|
+
const index = Math.max(0, turn.steps.findIndex((step2) => step2.state !== "running")), [gone] = turn.steps.splice(index, 1);
|
|
6357
|
+
this.steps.delete(gone.id);
|
|
6358
|
+
turn.dropped++;
|
|
6359
|
+
this.queue({ op: "drop", id: gone.id });
|
|
6360
|
+
}
|
|
6361
|
+
const step = { id: `s${++this.counter}`, state: "running", startedAt: Date.now(), endedAt: null, parentId: null, ...fields };
|
|
6362
|
+
turn.steps.push(step);
|
|
6363
|
+
this.steps.set(step.id, step);
|
|
6364
|
+
this.queue({ op: "step", step: structuredClone(step) });
|
|
6365
|
+
return step;
|
|
6366
|
+
}
|
|
6367
|
+
finish(step, failed) {
|
|
6368
|
+
step.state = failed ? "failed" : "ok";
|
|
6369
|
+
step.endedAt = Date.now();
|
|
6370
|
+
this.queue({ op: "step", step: structuredClone(step) });
|
|
6371
|
+
}
|
|
6372
|
+
queue(patch) {
|
|
6373
|
+
this.pending.push(patch);
|
|
6374
|
+
this.timer ??= setTimeout(() => this.flush(), FLUSH_MS);
|
|
6375
|
+
}
|
|
6376
|
+
/** Sends the pending changes as one patch: text streamed into the same step is joined, and a step sent whole
|
|
6377
|
+
* replaces what came before it for that step. A step is copied when it is queued, so text streamed after that
|
|
6378
|
+
* follows it as its own patch. */
|
|
6379
|
+
flush() {
|
|
6380
|
+
if (this.timer) {
|
|
6381
|
+
clearTimeout(this.timer);
|
|
6382
|
+
this.timer = null;
|
|
6383
|
+
}
|
|
6384
|
+
if (!this.pending.length) return;
|
|
6385
|
+
const patches = [];
|
|
6386
|
+
for (const patch of this.pending) {
|
|
6387
|
+
const last = patches.at(-1);
|
|
6388
|
+
if (patch.op === "text" && last?.op === "text" && last.id === patch.id) {
|
|
6389
|
+
last.append += patch.append;
|
|
6390
|
+
continue;
|
|
6391
|
+
}
|
|
6392
|
+
if (patch.op === "step") {
|
|
6393
|
+
for (let index = patches.length - 1; index >= 0; index--) {
|
|
6394
|
+
const item = patches[index];
|
|
6395
|
+
if ((item.op === "step" ? item.step.id : item.id) === patch.step.id) patches.splice(index, 1);
|
|
6396
|
+
}
|
|
6397
|
+
patches.push(patch);
|
|
6398
|
+
continue;
|
|
6399
|
+
}
|
|
6400
|
+
patches.push({ ...patch });
|
|
6401
|
+
}
|
|
6402
|
+
this.pending = [];
|
|
6403
|
+
this.emit({ type: "patch", patches });
|
|
6404
|
+
}
|
|
6405
|
+
emit(update) {
|
|
6406
|
+
for (const listener of [...this.listeners.keys()]) {
|
|
6407
|
+
try {
|
|
6408
|
+
listener(update);
|
|
6409
|
+
} catch (error) {
|
|
6410
|
+
console.error("[live] listener failed", { error });
|
|
6411
|
+
}
|
|
6412
|
+
}
|
|
6413
|
+
}
|
|
6414
|
+
reset() {
|
|
6415
|
+
this.turn = null;
|
|
6416
|
+
this.steps.clear();
|
|
6417
|
+
this.blocks.clear();
|
|
6418
|
+
this.compaction = null;
|
|
6419
|
+
this.retry = null;
|
|
6420
|
+
this.counter = 0;
|
|
6421
|
+
this.pending = [];
|
|
6422
|
+
if (this.timer) {
|
|
6423
|
+
clearTimeout(this.timer);
|
|
6424
|
+
this.timer = null;
|
|
6425
|
+
}
|
|
6426
|
+
}
|
|
6427
|
+
};
|
|
6428
|
+
|
|
6429
|
+
// src/server/service/bot/agent.ts
|
|
6430
|
+
var same2 = (a, b) => a.trim().replace(/^@/, "").toLocaleLowerCase() === b.trim().toLocaleLowerCase();
|
|
6431
|
+
var AgentOps = class {
|
|
6432
|
+
constructor(bots, store, chats) {
|
|
6433
|
+
this.bots = bots;
|
|
6434
|
+
this.store = store;
|
|
6435
|
+
this.chats = chats;
|
|
6436
|
+
}
|
|
6437
|
+
get owner() {
|
|
6438
|
+
return this.store.userMemberId();
|
|
6439
|
+
}
|
|
6440
|
+
agentId = AGENT_BOT_ID;
|
|
6441
|
+
/** A Bot by name, ignoring case and a leading @. */
|
|
6442
|
+
bot(name2) {
|
|
6443
|
+
const bot = this.store.listBots().find((record2) => same2(name2, record2.name));
|
|
6444
|
+
if (!bot) throw new ChatError(404, `No Bot named ${name2} (see bots.list)`);
|
|
6445
|
+
return bot;
|
|
6446
|
+
}
|
|
6447
|
+
/** Connected models and their thinking levels; never credentials. */
|
|
6448
|
+
models() {
|
|
6449
|
+
return this.store.listModelConnections().filter((connection) => connection.models.length).map((connection) => ({
|
|
6450
|
+
connection: connection.displayName,
|
|
6451
|
+
connectionId: connection.id,
|
|
6452
|
+
models: connection.models.map((model) => ({ id: model.id, name: model.name, thinking: ["auto", ...thinkingOptions(model).levels] }))
|
|
6453
|
+
}));
|
|
6454
|
+
}
|
|
6455
|
+
selection(bot, input) {
|
|
6456
|
+
if (!input.model && input.thinking === void 0) return null;
|
|
6457
|
+
let connectionId = bot?.connectionId ?? null, modelId = bot?.modelId ?? "";
|
|
6458
|
+
if (input.model) {
|
|
6459
|
+
const { connection, model } = input.model;
|
|
6460
|
+
const found = this.store.listModelConnections().find((item) => item.id === connection || same2(connection, item.displayName));
|
|
6461
|
+
if (!found) throw new ChatError(404, `No connection named ${connection} (see models.list)`);
|
|
6462
|
+
const picked = found.models.find((item) => item.id === model || same2(model, item.name));
|
|
6463
|
+
if (!picked) throw new ChatError(404, `No model ${model} in ${found.displayName} (see models.list)`);
|
|
6464
|
+
connectionId = found.id;
|
|
6465
|
+
modelId = picked.id;
|
|
6466
|
+
}
|
|
6467
|
+
if (!connectionId) throw new ChatError(400, "This Bot has no model yet; give model too");
|
|
6468
|
+
return { connectionId, modelId, ...input.thinking === void 0 ? {} : { thinkingLevel: input.thinking } };
|
|
6469
|
+
}
|
|
6470
|
+
/** "model (connection)", as the agent and the changed Bot read it. */
|
|
6471
|
+
modelLabel(bot) {
|
|
6472
|
+
const connection = bot.connectionId ? this.store.getModelConnection(bot.connectionId) : null;
|
|
6473
|
+
const model = connection?.models.find((item) => item.id === bot.modelId);
|
|
6474
|
+
return model ? `${model.name} (${connection.displayName})` : bot.modelId;
|
|
6475
|
+
}
|
|
6476
|
+
async create(input) {
|
|
6477
|
+
const selection = this.selection(null, input);
|
|
6478
|
+
const created = this.bots.create(input.name);
|
|
6479
|
+
if (input.description !== void 0 || input.persona !== void 0)
|
|
6480
|
+
this.bots.get(created.id).identity.update({ name: created.name, description: input.description ?? "", persona: input.persona ?? "" });
|
|
6481
|
+
if (selection) await this.bots.selectModel(created.id, selection);
|
|
6482
|
+
this.bots.listChanged();
|
|
6483
|
+
return this.store.getBot(created.id);
|
|
6484
|
+
}
|
|
6485
|
+
async update(bot, input) {
|
|
6486
|
+
const selection = this.selection(bot, input);
|
|
6487
|
+
if (input.name !== void 0 || input.description !== void 0 || input.persona !== void 0)
|
|
6488
|
+
this.bots.get(bot.id).identity.update({ name: input.name ?? bot.name, description: input.description ?? bot.description, persona: input.persona ?? bot.systemPrompt });
|
|
6489
|
+
if (selection) await this.bots.selectModel(bot.id, selection);
|
|
6490
|
+
const after2 = this.store.getBot(bot.id), changes = [];
|
|
6491
|
+
if (after2.name !== bot.name) changes.push({ what: "name", value: after2.name });
|
|
6492
|
+
if (after2.description !== bot.description) changes.push({ what: "description", value: after2.description });
|
|
6493
|
+
if (after2.systemPrompt !== bot.systemPrompt) changes.push({ what: "persona", value: null });
|
|
6494
|
+
if (after2.connectionId !== bot.connectionId || after2.modelId !== bot.modelId) changes.push({ what: "model", value: this.modelLabel(after2) });
|
|
6495
|
+
if (after2.thinkingLevel !== bot.thinkingLevel) changes.push({ what: "thinking", value: after2.thinkingLevel });
|
|
6496
|
+
this.tell(bot.id, changes);
|
|
6497
|
+
this.bots.listChanged();
|
|
6498
|
+
return { bot: after2, changed: changes.map((change) => change.what) };
|
|
6499
|
+
}
|
|
6500
|
+
async run(bot, action) {
|
|
6501
|
+
if (action === "stop" && bot.id === AGENT_BOT_ID) throw new ChatError(400, "You can't stop yourself");
|
|
6502
|
+
const runtime = this.bots.get(bot.id);
|
|
6503
|
+
await (action === "start" ? runtime.start() : runtime.stop());
|
|
6504
|
+
this.tell(bot.id, [{ what: "run", value: action }]);
|
|
6505
|
+
return this.bots.status(bot.id).status;
|
|
6506
|
+
}
|
|
6507
|
+
usage(bot, days) {
|
|
6508
|
+
const since = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
6509
|
+
return { since: new Date(since).toISOString(), models: this.bots.memory(bot.id).usage(since) };
|
|
6510
|
+
}
|
|
6511
|
+
/** Deletions run as the owner, who has just confirmed them on a card. */
|
|
6512
|
+
async deleteBot(bot) {
|
|
6513
|
+
if (bot.id === AGENT_BOT_ID) throw new ChatError(400, "You can't delete yourself");
|
|
6514
|
+
await this.bots.delete(bot.id, true);
|
|
6515
|
+
this.bots.listChanged();
|
|
6516
|
+
}
|
|
6517
|
+
async deleteChannel(chatId) {
|
|
6518
|
+
if (!await this.chats.delete(this.owner, chatId)) throw new ChatError(404, "Channel not found");
|
|
6519
|
+
await this.bots.chatDeleted(chatId, AGENT_BOT_ID);
|
|
6520
|
+
}
|
|
6521
|
+
removeMember(chatId, memberId) {
|
|
6522
|
+
this.chats.channels.removeMember(this.owner, chatId, memberId);
|
|
6523
|
+
}
|
|
6524
|
+
deleteSection(sectionId) {
|
|
6525
|
+
if (!this.chats.channels.deleteSection(this.owner, sectionId)) throw new ChatError(404, "Section not found");
|
|
6526
|
+
}
|
|
6527
|
+
/** The changed Bot is told with its next batch (digest: it is not woken for it), in its DM with the agent. */
|
|
6528
|
+
tell(botId, changes) {
|
|
6529
|
+
if (botId === AGENT_BOT_ID || !changes.length) return;
|
|
6530
|
+
const chatId = this.chats.openDirect(AGENT_BOT_ID, botId).id;
|
|
6531
|
+
for (const change of changes) this.store.agent.notify({ botId, kind: "change", reason: "agent", urgency: "digest", chatId, actorId: AGENT_BOT_ID, detail: JSON.stringify(change) });
|
|
6532
|
+
}
|
|
6533
|
+
/** The agent's first model: ask it, once, to welcome the owner in their DM, in their interface language. */
|
|
6534
|
+
onboard(language) {
|
|
6535
|
+
if (!this.store.agent.markOnboarded()) return;
|
|
6536
|
+
const chatId = this.chats.openDirect(this.owner, AGENT_BOT_ID).id;
|
|
6537
|
+
this.store.agent.notify({ botId: AGENT_BOT_ID, kind: "onboard", reason: "setup", urgency: "interrupt", chatId, actorId: this.owner, detail: language });
|
|
6538
|
+
this.bots.deliver(AGENT_BOT_ID, "interrupt");
|
|
6539
|
+
}
|
|
6540
|
+
};
|
|
6541
|
+
|
|
5742
6542
|
// src/server/service/bot/service.ts
|
|
5743
6543
|
var BotService = class {
|
|
5744
6544
|
constructor(deps) {
|
|
5745
6545
|
this.deps = deps;
|
|
6546
|
+
this.agent = new AgentOps(this, deps.store, deps.chats);
|
|
5746
6547
|
}
|
|
5747
6548
|
bots = /* @__PURE__ */ new Map();
|
|
6549
|
+
lives = /* @__PURE__ */ new Map();
|
|
5748
6550
|
closed = false;
|
|
5749
6551
|
started = false;
|
|
6552
|
+
/** What only the agent Bot does (A48). */
|
|
6553
|
+
agent;
|
|
6554
|
+
listChanged() {
|
|
6555
|
+
this.deps.listChanged?.();
|
|
6556
|
+
}
|
|
5750
6557
|
get(id) {
|
|
5751
6558
|
if (this.closed) throw new RuntimeFault("stopping", "Shutting down.", 503);
|
|
5752
|
-
const
|
|
5753
|
-
if (!
|
|
6559
|
+
const record2 = this.deps.store.getBot(id);
|
|
6560
|
+
if (!record2) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
5754
6561
|
let bot = this.bots.get(id);
|
|
5755
6562
|
if (bot) return bot;
|
|
5756
|
-
const { store, chats, files, credentials } = this.deps;
|
|
6563
|
+
const { store, chats, files, credentials } = this.deps, live = this.live(id);
|
|
5757
6564
|
const runtime = new AgentRuntime(new BotInbox(id, store.runtime, chats, files), (setup, model, reference, activity) => (this.deps.factory ?? PiSession.create)({
|
|
5758
6565
|
store,
|
|
5759
|
-
bot:
|
|
6566
|
+
bot: record2,
|
|
5760
6567
|
selection: model.selection,
|
|
5761
6568
|
reference,
|
|
5762
6569
|
credentials,
|
|
5763
6570
|
tools: setup.tools,
|
|
5764
6571
|
systemPrompt: () => botPrompt(setup),
|
|
5765
6572
|
onHistory: setup.onHistory,
|
|
5766
|
-
activity
|
|
6573
|
+
activity,
|
|
6574
|
+
live: (event) => live.event(event)
|
|
5767
6575
|
}));
|
|
5768
|
-
const workspace = new Workspace(
|
|
5769
|
-
const memory = new Memory(join6(dirname5(
|
|
6576
|
+
const workspace = new Workspace(record2.workspacePath);
|
|
6577
|
+
const memory = new Memory(join6(dirname5(record2.workspacePath), "history.db"));
|
|
5770
6578
|
bot = new Bot(id, {
|
|
5771
6579
|
identity: new Identity(id, store),
|
|
5772
6580
|
model: new Model(id, store, resolveThinking),
|
|
5773
6581
|
workspace,
|
|
5774
6582
|
memory,
|
|
5775
|
-
tools: new Tools({
|
|
6583
|
+
tools: new Tools({
|
|
6584
|
+
botId: id,
|
|
6585
|
+
chats,
|
|
6586
|
+
files,
|
|
6587
|
+
memory,
|
|
6588
|
+
workspace,
|
|
6589
|
+
asking: (on) => runtime.waitingForUser(on),
|
|
6590
|
+
roster: () => this.roster(),
|
|
6591
|
+
...id === AGENT_BOT_ID ? { agent: this.agent } : {}
|
|
6592
|
+
}),
|
|
5776
6593
|
agentRuntime: runtime,
|
|
5777
6594
|
state: new BotState(() => this.deps.changed(id))
|
|
5778
6595
|
}, { deliver: async (message) => {
|
|
5779
|
-
const outgoing = await files.readOutgoing(
|
|
6596
|
+
const outgoing = await files.readOutgoing(record2.workspacePath, message.attachments ?? [], message.signal);
|
|
5780
6597
|
message.signal?.throwIfAborted();
|
|
5781
6598
|
return chats.deliver(id, message.chatId, {
|
|
5782
6599
|
content: message.content,
|
|
@@ -5792,23 +6609,40 @@ var BotService = class {
|
|
|
5792
6609
|
this.bots.set(id, bot);
|
|
5793
6610
|
return bot;
|
|
5794
6611
|
}
|
|
5795
|
-
details(
|
|
5796
|
-
const model =
|
|
5797
|
-
return { ...
|
|
6612
|
+
details(record2) {
|
|
6613
|
+
const model = record2.connectionId ? this.deps.store.getModelConnection(record2.connectionId)?.models.find((model2) => model2.id === record2.modelId) : void 0;
|
|
6614
|
+
return { ...record2, effectiveThinkingLevel: model ? resolveThinking(model, record2.thinkingLevel) : null, agent: record2.id === AGENT_BOT_ID };
|
|
5798
6615
|
}
|
|
5799
6616
|
owns(id) {
|
|
5800
6617
|
return this.deps.store.getBot(id) !== null;
|
|
5801
6618
|
}
|
|
6619
|
+
/** The turn a Bot is running, live (A47); kept for a Bot whether or not it has started. */
|
|
6620
|
+
live(id) {
|
|
6621
|
+
if (!this.deps.store.getBot(id)) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
6622
|
+
let live = this.lives.get(id);
|
|
6623
|
+
if (!live) {
|
|
6624
|
+
live = new LiveTurns((chatId) => this.liveChat(id, chatId));
|
|
6625
|
+
this.lives.set(id, live);
|
|
6626
|
+
}
|
|
6627
|
+
return live;
|
|
6628
|
+
}
|
|
6629
|
+
/** A chat as the Bot sees it, for the live turn: a DM is named after the other member. */
|
|
6630
|
+
liveChat(botId, chatId) {
|
|
6631
|
+
const chat = this.deps.chats.get(botId, chatId);
|
|
6632
|
+
if (!chat) return null;
|
|
6633
|
+
const other = chat.type === "dm" ? chat.memberIds.find((member) => member !== botId) : void 0;
|
|
6634
|
+
return { id: chat.id, type: chat.type, title: other ? this.deps.chats.member(other)?.name ?? chat.title : chat.title };
|
|
6635
|
+
}
|
|
5802
6636
|
list() {
|
|
5803
|
-
return this.deps.store.listBots().map((
|
|
6637
|
+
return this.deps.store.listBots().map((record2) => this.details(record2));
|
|
5804
6638
|
}
|
|
5805
6639
|
create(name2) {
|
|
5806
6640
|
return this.details(this.deps.store.createBot(name2));
|
|
5807
6641
|
}
|
|
5808
6642
|
describe(id) {
|
|
5809
|
-
const
|
|
5810
|
-
if (!
|
|
5811
|
-
return this.details(
|
|
6643
|
+
const record2 = this.deps.store.getBot(id);
|
|
6644
|
+
if (!record2) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
6645
|
+
return this.details(record2);
|
|
5812
6646
|
}
|
|
5813
6647
|
runs(id) {
|
|
5814
6648
|
this.describe(id);
|
|
@@ -5856,15 +6690,16 @@ var BotService = class {
|
|
|
5856
6690
|
/** Every Bot's run state, for `bots.list` (A43). A Bot not loaded in this process is stopped. */
|
|
5857
6691
|
roster() {
|
|
5858
6692
|
const { store } = this.deps;
|
|
5859
|
-
return store.listBots().map((
|
|
5860
|
-
const bot = this.bots.get(
|
|
6693
|
+
return store.listBots().map((record2) => {
|
|
6694
|
+
const bot = this.bots.get(record2.id), runId = bot?.agentRuntime.activeRunId ?? null;
|
|
5861
6695
|
const status = bot?.status() ?? { status: "stop", activity: null, context: null, error: null };
|
|
5862
6696
|
return {
|
|
5863
|
-
id:
|
|
5864
|
-
name:
|
|
6697
|
+
id: record2.id,
|
|
6698
|
+
name: record2.name,
|
|
6699
|
+
agent: record2.id === AGENT_BOT_ID,
|
|
5865
6700
|
status,
|
|
5866
6701
|
runChatIds: status.status === "running" && runId ? store.runtime.inputs(runId).map((input) => input.chatId) : [],
|
|
5867
|
-
errorCode: store.runtime.latestErrorCode(
|
|
6702
|
+
errorCode: store.runtime.latestErrorCode(record2.id)
|
|
5868
6703
|
};
|
|
5869
6704
|
});
|
|
5870
6705
|
}
|
|
@@ -5873,18 +6708,22 @@ var BotService = class {
|
|
|
5873
6708
|
const bot = this.bots.get(id);
|
|
5874
6709
|
return { ...bot?.status() ?? { status: "stop", activity: null, context: null, error: null }, botId: id, runId: bot?.agentRuntime.activeRunId ?? null };
|
|
5875
6710
|
}
|
|
6711
|
+
/** `language` is the browser's interface language: the agent Bot's first model asks it to welcome the owner in it (A48). */
|
|
5876
6712
|
async selectModel(id, input) {
|
|
5877
6713
|
const selection = this.deps.store.validateSelection(id, input);
|
|
5878
6714
|
if (!selection) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
5879
6715
|
await this.get(id).changeModel(selection);
|
|
5880
6716
|
this.resume(id);
|
|
6717
|
+
if (id === AGENT_BOT_ID) this.agent.onboard(welcomeLanguage(input.language));
|
|
5881
6718
|
return this.deps.store.getBot(id);
|
|
5882
6719
|
}
|
|
5883
6720
|
connectionChanged(connectionId) {
|
|
5884
6721
|
for (const bot of this.deps.store.listBots()) if (bot.connectionId === connectionId) this.resume(bot.id);
|
|
5885
6722
|
}
|
|
5886
|
-
|
|
5887
|
-
|
|
6723
|
+
/** A deleted chat ends the turns handling it; `except` is the agent Bot that deleted it from inside its turn. */
|
|
6724
|
+
async chatDeleted(chatId, except) {
|
|
6725
|
+
for (const [id, bot] of this.bots) {
|
|
6726
|
+
if (id === except) continue;
|
|
5888
6727
|
const runId = bot.agentRuntime.activeRunId;
|
|
5889
6728
|
if (runId && this.deps.store.runtime.inputs(runId).some((input) => input.chatId === chatId)) await bot.abort();
|
|
5890
6729
|
}
|
|
@@ -5892,9 +6731,12 @@ var BotService = class {
|
|
|
5892
6731
|
/** Deletes a Bot: stop it, drop its data, keep its messages. `keepFiles` leaves the workspace (and
|
|
5893
6732
|
* the History database) on disk, since it may hold work worth keeping. */
|
|
5894
6733
|
async delete(id, keepFiles) {
|
|
6734
|
+
if (id === AGENT_BOT_ID) throw new RuntimeFault("agent_undeletable", "The agent Bot can't be deleted", 409);
|
|
5895
6735
|
const bot = this.get(id), workspacePath = bot.workspace.location().cwd;
|
|
5896
6736
|
await bot.stop();
|
|
5897
6737
|
this.bots.delete(id);
|
|
6738
|
+
this.lives.get(id)?.close();
|
|
6739
|
+
this.lives.delete(id);
|
|
5898
6740
|
if (!this.deps.store.deleteBot(id)) throw new RuntimeFault("bot_missing", "Bot not found", 404);
|
|
5899
6741
|
if (!keepFiles) await rm2(dirname5(workspacePath), { recursive: true, force: true });
|
|
5900
6742
|
}
|
|
@@ -5902,6 +6744,7 @@ var BotService = class {
|
|
|
5902
6744
|
if (this.closed) return;
|
|
5903
6745
|
this.closed = true;
|
|
5904
6746
|
await Promise.all([...this.bots.values()].map((bot) => bot.stop()));
|
|
6747
|
+
for (const live of this.lives.values()) live.close();
|
|
5905
6748
|
}
|
|
5906
6749
|
};
|
|
5907
6750
|
|
|
@@ -5933,6 +6776,7 @@ var Application = class _Application {
|
|
|
5933
6776
|
files: this.files,
|
|
5934
6777
|
credentials: this.credentials,
|
|
5935
6778
|
changed: (id) => this.events.publish({ kind: "bot", id, status: this.bots.status(id) }),
|
|
6779
|
+
listChanged: () => this.events.publish({ kind: "bots" }),
|
|
5936
6780
|
factory
|
|
5937
6781
|
});
|
|
5938
6782
|
this.auth = new CredentialLogins(store, this.credentials, (id) => this.bots.connectionChanged(id));
|
|
@@ -6003,8 +6847,8 @@ import { join as join8, resolve as resolve5 } from "node:path";
|
|
|
6003
6847
|
var pidPath = (dir) => join8(resolve5(dir), "jarvy.pid");
|
|
6004
6848
|
var logPath = (dir) => join8(resolve5(dir), "jarvy.log");
|
|
6005
6849
|
function writePidFile(dir, url) {
|
|
6006
|
-
const
|
|
6007
|
-
writeFileSync(pidPath(dir), JSON.stringify(
|
|
6850
|
+
const record2 = { pid: process.pid, startToken: startToken(process.pid), url, startedAt: Date.now() };
|
|
6851
|
+
writeFileSync(pidPath(dir), JSON.stringify(record2), { mode: 384 });
|
|
6008
6852
|
}
|
|
6009
6853
|
function removePidFile(dir) {
|
|
6010
6854
|
if (readRecord(dir)?.pid === process.pid) rmSync(pidPath(dir), { force: true });
|
|
@@ -6032,14 +6876,14 @@ function alive(pid) {
|
|
|
6032
6876
|
}
|
|
6033
6877
|
}
|
|
6034
6878
|
function runningServer(dir) {
|
|
6035
|
-
const
|
|
6036
|
-
if (!
|
|
6037
|
-
const token = alive(
|
|
6038
|
-
if (token === null || token &&
|
|
6879
|
+
const record2 = readRecord(dir);
|
|
6880
|
+
if (!record2) return null;
|
|
6881
|
+
const token = alive(record2.pid) ? startToken(record2.pid) : null;
|
|
6882
|
+
if (token === null || token && record2.startToken && token !== record2.startToken) {
|
|
6039
6883
|
rmSync(pidPath(dir), { force: true });
|
|
6040
6884
|
return null;
|
|
6041
6885
|
}
|
|
6042
|
-
return
|
|
6886
|
+
return record2;
|
|
6043
6887
|
}
|
|
6044
6888
|
function startBackground(script, runArgs, dir) {
|
|
6045
6889
|
mkdirSync3(resolve5(dir), { recursive: true, mode: 448 });
|
|
@@ -6072,11 +6916,11 @@ function signalReady(url) {
|
|
|
6072
6916
|
process.disconnect();
|
|
6073
6917
|
}
|
|
6074
6918
|
async function stopServer(dir, waitMs = 3e4) {
|
|
6075
|
-
const
|
|
6076
|
-
if (!
|
|
6077
|
-
process.kill(
|
|
6919
|
+
const record2 = runningServer(dir);
|
|
6920
|
+
if (!record2) return "not_running";
|
|
6921
|
+
process.kill(record2.pid, "SIGTERM");
|
|
6078
6922
|
for (const deadline = Date.now() + waitMs; Date.now() < deadline; ) {
|
|
6079
|
-
if (!alive(
|
|
6923
|
+
if (!alive(record2.pid)) {
|
|
6080
6924
|
rmSync(pidPath(dir), { force: true });
|
|
6081
6925
|
return "stopped";
|
|
6082
6926
|
}
|