@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.
@@ -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 Hono9 } from "hono";
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 after = Number(c.req.query("after") ?? 0);
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"), after, limit);
452
- return c.json({ messages: messages.map(messageView), nextSequence: messages.at(-1)?.sequence ?? after, hasMore: messages.length === limit });
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
- return c.body(new Uint8Array(attachment.data), 200, {
488
- "Content-Type": "application/octet-stream",
489
- "Content-Length": String(attachment.data.length),
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 Hono9();
775
+ const app = new Hono10();
691
776
  app.use("*", headers);
692
777
  app.onError(handleError);
693
- const api = new Hono9();
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 randomInt2, randomUUID as randomUUID7, scryptSync, timingSafeEqual } from "node:crypto";
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 '${DEFAULT_BOT_ID}',name,'',system_prompt,'',0 FROM bot_settings WHERE id=1;
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='${DEFAULT_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 > 18) throw new Error(`Unsupported database version: ${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, after = 0, limit = 1e3) {
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, after, limit);
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[randomInt2(BOT_COLORS.length)]);
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 same2 = all.filter((row) => row.model.id.toLowerCase() === q || strip(row.model.id) === strip(q));
2703
- return same2.length ? same2 : all.filter((row) => row.model.id.toLowerCase().includes(strip(q)));
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((target) => target.member.id === member.id)) targets.push({ member, reason });
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((target) => target.member.id === member.id)).map((member) => ({ member, reason: "subscription" }));
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((target) => {
3130
- const urgency = urgencyOf(kind, target.reason);
3131
- return urgency ? [{ ...target, kind, urgency: muted.includes(target.member.id) ? mutedUrgency(target.reason, urgency) : 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, after, limit);
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), target = directory.find((member) => member.id === targetId);
3654
- if (!actor || !target) throw new ChatError(404, "Member not found");
3655
- const chat = this.repository.openDirect(actorId, targetId, owner, `${actor.name} \xB7 ${target.name}`);
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, after = 0, limit = 1e3) {
3754
- if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 1e3)
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, after, limit);
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((record) => ({ kind: "chat", record })),
3968
- ...history.map((record) => ({ kind: "history", record }))
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({ httpIdleTimeoutMs: HTTP_IDLE_TIMEOUT_MS });
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 record = history.convert(entry);
4242
- if (record) await options.onHistory(record);
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
- await this.session.prompt(xml, { expandPromptTemplates: false });
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
- await this.session.waitForIdle();
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(record) {
4699
+ links(record2) {
4444
4700
  const links = [];
4445
- for (const block of record.message.content) {
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(record.id, "tool_call", callId);
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(record.id, "tool_call", callId);
4453
- const results = this.db.prepare(`SELECT id,role FROM history WHERE id>? AND id<? AND ${CALL} ORDER BY id`).all(record.id, next?.id ?? Number.MAX_SAFE_INTEGER, "tool_result", callId);
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 record = this.history().read(id);
4498
- return record ? { record, links: this.history().links(record) } : null;
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((record, index) => {
4504
- if (record) result.records.push(record);
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: (record) => this.recordHistory(record)
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(record) {
4639
- const historyId = await this.memory.append(record);
4640
- if (record.role !== "assistant") return;
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 record.message.content) {
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 Type4 } from "typebox";
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 record of result.records) {
4802
- sections.push(`## #${record.id} \xB7 ${record.role}`, `Time: ${new Date(record.timestamp).toISOString()}`);
4803
- if (record.model) sections.push(`Model:
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(record.model)}`);
4806
- if ("preview" in record) {
4807
- sections.push(fence(record.preview));
5061
+ ${jsonBlock(record2.model)}`);
5062
+ if ("preview" in record2) {
5063
+ sections.push(fence(record2.preview));
4808
5064
  continue;
4809
5065
  }
4810
- sections.push(content(record.message.content));
4811
- if (record.message.tools !== void 0) sections.push("### Tool definitions", jsonBlock(record.message.tools));
4812
- if (record.message.tools_removed !== void 0) sections.push("### Tools removed", jsonBlock(record.message.tools_removed));
4813
- if (record.message.stop_reason !== void 0) sections.push("### Stop reason", fence(record.message.stop_reason));
4814
- if (record.message.error !== void 0) sections.push("### Error", fence(record.message.error));
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 = Type4.Object({
5217
- action: Type4.Union([Type4.Literal("list"), Type4.Literal("describe"), Type4.Literal("call")]),
5218
- tool: Type4.Optional(Type4.String()),
5219
- args: Type4.Optional(Type4.Record(Type4.String(), Type4.Unknown()))
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(definitions).map(([name3, tool]) => ({ name: name3, description: tool.description }));
5657
+ return Object.entries(this.catalog).map(([name3, tool]) => ({ name: name3, description: tool.description }));
5242
5658
  }
5243
- if (!raw.tool || !Object.hasOwn(definitions, raw.tool)) throw new ChatError(400, "Unknown chat tool; use list first");
5244
- const name2 = raw.tool, definition = definitions[name2];
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
- const sent = await this.send(parsed.chatId, { content: parsed.question, question, toolCallId, signal });
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
- 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>` : "");
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).length > 0;
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 record = this.deps.store.getBot(id);
5753
- if (!record) throw new RuntimeFault("bot_missing", "Bot not found", 404);
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: record,
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(record.workspacePath);
5769
- const memory = new Memory(join6(dirname5(record.workspacePath), "history.db"));
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({ botId: id, chats, files, memory, workspace, asking: (on) => runtime.waitingForUser(on), roster: () => this.roster() }),
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(record.workspacePath, message.attachments ?? [], message.signal);
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(record) {
5796
- const model = record.connectionId ? this.deps.store.getModelConnection(record.connectionId)?.models.find((model2) => model2.id === record.modelId) : void 0;
5797
- return { ...record, effectiveThinkingLevel: model ? resolveThinking(model, record.thinkingLevel) : null };
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((record) => this.details(record));
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 record = this.deps.store.getBot(id);
5810
- if (!record) throw new RuntimeFault("bot_missing", "Bot not found", 404);
5811
- return this.details(record);
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((record) => {
5860
- const bot = this.bots.get(record.id), runId = bot?.agentRuntime.activeRunId ?? null;
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: record.id,
5864
- name: record.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(record.id)
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
- async chatDeleted(chatId) {
5887
- for (const bot of this.bots.values()) {
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 record = { pid: process.pid, startToken: startToken(process.pid), url, startedAt: Date.now() };
6007
- writeFileSync(pidPath(dir), JSON.stringify(record), { mode: 384 });
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 record = readRecord(dir);
6036
- if (!record) return null;
6037
- const token = alive(record.pid) ? startToken(record.pid) : null;
6038
- if (token === null || token && record.startToken && token !== record.startToken) {
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 record;
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 record = runningServer(dir);
6076
- if (!record) return "not_running";
6077
- process.kill(record.pid, "SIGTERM");
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(record.pid)) {
6923
+ if (!alive(record2.pid)) {
6080
6924
  rmSync(pidPath(dir), { force: true });
6081
6925
  return "stopped";
6082
6926
  }