@kevin5251984/guild 0.2.18 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/handlers.ts CHANGED
@@ -18,14 +18,25 @@ import {
18
18
  } from "./generate.ts";
19
19
  import {
20
20
  liveTrajectoryEvents,
21
+ promoteSpawnEvent,
21
22
  synthesizeTrajectory,
22
23
  turnTrajectoryEvents,
23
24
  userTrajectoryEvent,
24
25
  } from "./trajectory.ts";
25
26
  import { importFromGithub, importFromUrl } from "./skill-import.ts";
26
- import { harvestBotMemory, harvestChannelMemory } from "./memory.ts";
27
+ import {
28
+ harvestBotMemory,
29
+ harvestChannelMemory,
30
+ mergeQuestMemory,
31
+ } from "./memory.ts";
27
32
  import { listHostSkills, type HostSkill } from "./host-skills.ts";
28
- import { GuildStore, StoreError, type LiveStep, type LiveTurn } from "./store.ts";
33
+ import {
34
+ CHANNEL_ROSTER_CAP,
35
+ GuildStore,
36
+ StoreError,
37
+ type LiveStep,
38
+ type LiveTurn,
39
+ } from "./store.ts";
29
40
  import { listSpawnRefs } from "./subagent.ts";
30
41
  import {
31
42
  importHostMcp,
@@ -38,7 +49,9 @@ import {
38
49
  import {
39
50
  assignmentFor,
40
51
  isBroadcastMention,
41
- summonedHandles,
52
+ messageMentionIds,
53
+ parseMentionIds,
54
+ sanitizeMentionIds,
42
55
  } from "./mention.ts";
43
56
  import { slashNames } from "./slash.ts";
44
57
  import { toHistoryItem, type HistoryItem } from "./compact.ts";
@@ -59,6 +72,8 @@ export type HandlerExtras = {
59
72
  mcpTools?: McpToolRef[] | Promise<McpToolRef[]>;
60
73
  onTurnComplete?: (turn: TurnComplete) => void;
61
74
  turn?: (input: Parameters<typeof chatReply>[0]) => Promise<ChatReply>;
75
+ /** Bot ids the client already resolved from @mentions. */
76
+ mentions?: string[];
62
77
  };
63
78
 
64
79
  export function healthPayload(): HealthResponse {
@@ -242,6 +257,7 @@ export function updateBot(
242
257
  name?: string;
243
258
  handle?: string;
244
259
  oneLiner?: string;
260
+ portrait?: string | null;
245
261
  skillIds?: string[];
246
262
  soul?: MarkdownDraft;
247
263
  agent?: MarkdownDraft;
@@ -256,6 +272,162 @@ export function updateBot(
256
272
  return store.updateBot(id, { ...input, skillIds });
257
273
  }
258
274
 
275
+ const LOOK_HAIR = [
276
+ "jet-black short crop",
277
+ "jet-black long straight hair",
278
+ "copper-red bob with blunt bangs",
279
+ "copper-red messy spikes",
280
+ "indigo long waves",
281
+ "indigo pixie cut",
282
+ "honey-blonde high ponytail",
283
+ "honey-blonde bowl cut",
284
+ "hot-pink messy spikes",
285
+ "hot-pink bob",
286
+ "ash-white curly volume",
287
+ "ash-white long hair",
288
+ "teal-tinted undercut",
289
+ "teal high ponytail",
290
+ "deep-burgundy twin braids",
291
+ "deep-burgundy shag",
292
+ ];
293
+ const LOOK_CLOTH = [
294
+ "sunflower-yellow collared shirt",
295
+ "cobalt hooded jacket",
296
+ "rose cardigan over a cream tee",
297
+ "forest-green knit turtleneck",
298
+ "ivory blouse with a coral scarf",
299
+ "charcoal work vest over a rust tee",
300
+ "lilac haori",
301
+ "orange windbreaker",
302
+ "white lab coat over a black tee",
303
+ "crimson bomber jacket",
304
+ "mint sailor collar",
305
+ "navy peacoat",
306
+ "gold-trimmed teal capelet",
307
+ "checkered red-and-black shirt",
308
+ "pale-blue denim jacket",
309
+ "magenta track jacket",
310
+ ];
311
+ const LOOK_EXTRA = [
312
+ "round wire glasses",
313
+ "small gold hoop earrings",
314
+ "over-ear headphones around the neck",
315
+ "a paintbrush tucked behind one ear",
316
+ "a red hair clip",
317
+ "a thin black choker",
318
+ "a knitted ear warmer",
319
+ "no extra accessories",
320
+ ];
321
+ const LOOK_SKIN = [
322
+ "fair peach human skin",
323
+ "warm tan human skin",
324
+ "light brown human skin",
325
+ "deep brown human skin",
326
+ "golden beige human skin",
327
+ ];
328
+ const LOOK_FACE = [
329
+ "round cheerful face with wide-set eyes",
330
+ "sharp jaw and narrow eyes",
331
+ "soft oval face with thick brows",
332
+ "heart-shaped face and a small nose",
333
+ "square face with a bright closed-mouth smile",
334
+ ];
335
+ const LOOK_RACE = [
336
+ {
337
+ id: "human" as const,
338
+ label: "human",
339
+ prompt:
340
+ "human. Ordinary rounded human ears, fully human anatomy, no fantasy ears.",
341
+ },
342
+ {
343
+ id: "dwarf" as const,
344
+ label: "dwarf",
345
+ prompt:
346
+ "young dwarf. Stout neck, broader cheekbones, a slightly larger nose, thick brows, rounded ears, youthful (not elderly, not bald).",
347
+ },
348
+ {
349
+ id: "elf" as const,
350
+ label: "elf",
351
+ prompt:
352
+ "young elf. Long pointed ears clearly visible, fine features, almond eyes, still youthful.",
353
+ },
354
+ {
355
+ id: "demihuman" as const,
356
+ label: "demihuman",
357
+ prompt:
358
+ "demihuman who is 90% human: a human face and bust with only one subtle tell (tiny pointed ear tips, faint whisker marks, or slightly elongated canines). Not a full animal-person, not a mascot, not extra limbs.",
359
+ },
360
+ ];
361
+
362
+ function lookSeed(key: string): number {
363
+ let n = 2166136261;
364
+ for (const ch of key) {
365
+ n ^= ch.charCodeAt(0);
366
+ n = Math.imul(n, 16777619);
367
+ }
368
+ return n >>> 0;
369
+ }
370
+
371
+ export function lookTraits(bot: { name: string; handle: string }): {
372
+ hair: string;
373
+ cloth: string;
374
+ extra: string;
375
+ skin: string;
376
+ face: string;
377
+ race: (typeof LOOK_RACE)[number];
378
+ } {
379
+ const n = lookSeed(bot.handle || bot.name || "bot");
380
+ return {
381
+ hair: LOOK_HAIR[n % LOOK_HAIR.length],
382
+ cloth: LOOK_CLOTH[(n >>> 4) % LOOK_CLOTH.length],
383
+ extra: LOOK_EXTRA[(n >>> 8) % LOOK_EXTRA.length],
384
+ skin: LOOK_SKIN[(n >>> 12) % LOOK_SKIN.length],
385
+ face: LOOK_FACE[(n >>> 16) % LOOK_FACE.length],
386
+ race: LOOK_RACE[(n >>> 20) % LOOK_RACE.length],
387
+ };
388
+ }
389
+
390
+ export function lookPrompt(bot: {
391
+ name: string;
392
+ handle: string;
393
+ oneLiner?: string;
394
+ }): string {
395
+ const role = bot.oneLiner?.trim() || "keeps a seat in the guild tavern";
396
+ const look = lookTraits(bot);
397
+ return [
398
+ `SNES 16-bit pixel-art bust portrait of ${bot.name} (@${bot.handle}), a unique ${look.race.label} guild adventurer.`,
399
+ `Race (mandatory): ${look.race.prompt}`,
400
+ `Mandatory look: ${look.skin}, ${look.face}, ${look.hair}, wearing a ${look.cloth}, ${look.extra}.`,
401
+ `They ${role}.`,
402
+ "Hair color, outfit, and race are locked; do not default to brown hair or a beige coat.",
403
+ "Natural skin tones only, never green, gray, or monster skin.",
404
+ "Head and shoulders only, facing the camera, chunky 16-bit pixels, limited tavern palette, cream pixel outline.",
405
+ "Animal Crossing crossed with Earthbound, youthful SNES NPC.",
406
+ "Opaque cream or tavern-wood background, no black void, no white photo studio, no checkerboard, no transparency.",
407
+ "Close-up character headshot, no full body, no legs, no floor, no scenery, no photorealism, no 3D render, no text, no watermark.",
408
+ ].join(" ");
409
+ }
410
+
411
+ export async function generateBotLook(
412
+ store: GuildStore,
413
+ id: string,
414
+ env: NodeJS.ProcessEnv = process.env,
415
+ ) {
416
+ const bot = store.getBot(id);
417
+ if (!bot) throw new StoreError(404, "bot not found");
418
+ const { generateImage } = await import("./image-gen.ts");
419
+ const result = await generateImage({
420
+ prompt: lookPrompt(bot),
421
+ aspectRatio: "1:1",
422
+ dataDir: store.dataDir,
423
+ env,
424
+ });
425
+ if (result.isError || !result.publicPath) {
426
+ throw new StoreError(502, result.text);
427
+ }
428
+ return store.updateBot(id, { portrait: result.publicPath });
429
+ }
430
+
259
431
  export async function importSkills(
260
432
  store: GuildStore,
261
433
  input: { source: string; url?: string; repo?: string },
@@ -356,10 +528,53 @@ export function createChannel(store: GuildStore, name: string) {
356
528
  return store.createChannel(name);
357
529
  }
358
530
 
531
+ export function createBranch(
532
+ store: GuildStore,
533
+ parentId: string,
534
+ messageId: string,
535
+ name?: string,
536
+ ) {
537
+ return store.createBranch(parentId, messageId, name);
538
+ }
539
+
359
540
  export function deleteChannel(store: GuildStore, id: string) {
541
+ const room = store.getRoom(id);
542
+ if (room?.parentId) {
543
+ throw new StoreError(400, "close the branch instead");
544
+ }
360
545
  return store.deleteChannel(id);
361
546
  }
362
547
 
548
+ export async function closeBranch(
549
+ store: GuildStore,
550
+ id: string,
551
+ merge = false,
552
+ env: NodeJS.ProcessEnv = process.env,
553
+ ) {
554
+ const room = store.getRoom(id);
555
+ if (!room) throw new StoreError(404, "channel not found");
556
+ if (room.kind !== "channel") throw new StoreError(400, "not a channel");
557
+ if (!room.parentId) throw new StoreError(400, "not a branch");
558
+ const parent = store.getRoom(room.parentId);
559
+ if (!parent) throw new StoreError(404, "parent not found");
560
+ if (merge) {
561
+ for (const child of store
562
+ .listChannels()
563
+ .filter((item) => item.parentId === id)) {
564
+ await closeBranch(store, child.id, true, env);
565
+ }
566
+ await mergeQuestMemory({
567
+ store,
568
+ parentId: parent.id,
569
+ childId: id,
570
+ questName: room.name,
571
+ env,
572
+ });
573
+ }
574
+ store.deleteChannel(id);
575
+ return { ok: true as const, id, parentId: parent.id, merged: Boolean(merge) };
576
+ }
577
+
363
578
  export function renameChannel(store: GuildStore, id: string, name: string) {
364
579
  return store.renameChannel(id, name);
365
580
  }
@@ -401,16 +616,23 @@ export function openDm(store: GuildStore, botId: string) {
401
616
  return store.openDm(botId);
402
617
  }
403
618
 
619
+ /** A seat may speak twice in one turn so report-back can resume the assigner. */
620
+ const HANDOFF_SEAT_CAP = 2;
621
+
404
622
  function handoffTargets(
405
623
  store: GuildStore,
406
624
  memberIds: string[],
407
625
  replies: ChatMessage[],
408
626
  asked: string,
409
627
  history: HistoryItem[],
628
+ fresh: ChatMessage[],
410
629
  ): { botId: string; fromHandle: string; asked: string; history: HistoryItem[] }[] {
411
630
  const bots = store.listBots();
412
631
  const handles = bots.map((bot) => bot.handle);
413
- const spoke = new Set(replies.map((row) => row.author));
632
+ const spoke = new Map<string, number>();
633
+ for (const row of replies) {
634
+ spoke.set(row.author, (spoke.get(row.author) || 0) + 1);
635
+ }
414
636
  const hops: {
415
637
  botId: string;
416
638
  fromHandle: string;
@@ -418,10 +640,10 @@ function handoffTargets(
418
640
  history: HistoryItem[];
419
641
  }[] = [];
420
642
  const queued = new Set<string>();
421
- for (const reply of replies) {
643
+ for (const reply of fresh) {
422
644
  if (isBroadcastMention(reply.body)) continue;
423
- const names = summonedHandles(reply.body, handles);
424
- if (!names.length) continue;
645
+ const ids = messageMentionIds(reply, bots);
646
+ if (!ids.length) continue;
425
647
  const from = bots.find((bot) => bot.id === reply.author);
426
648
  const fromHandle = from?.handle || reply.author;
427
649
  const hopHistory = history.concat(
@@ -429,10 +651,10 @@ function handoffTargets(
429
651
  { author: reply.author, body: reply.body },
430
652
  );
431
653
  for (const bot of bots) {
432
- if (!names.includes(bot.handle.toLowerCase())) continue;
654
+ if (!ids.includes(bot.id)) continue;
433
655
  if (!memberIds.includes(bot.id)) continue;
434
656
  if (bot.id === reply.author) continue;
435
- if (spoke.has(bot.id) || queued.has(bot.id)) continue;
657
+ if ((spoke.get(bot.id) || 0) >= HANDOFF_SEAT_CAP || queued.has(bot.id)) continue;
436
658
  queued.add(bot.id);
437
659
  const spec = assignmentFor(reply.body, bot.handle, handles);
438
660
  hops.push({
@@ -449,19 +671,16 @@ function handoffTargets(
449
671
  function replyBots(
450
672
  store: GuildStore,
451
673
  memberIds: string[],
452
- userText: string,
674
+ userMessage: { author?: string; body: string; mentions?: string[] },
453
675
  extraBotId?: string,
454
676
  ): string[] {
455
- if (isBroadcastMention(userText)) return memberIds;
456
- const bots = store.listBots();
457
- const names = summonedHandles(
458
- userText,
459
- bots.map((bot) => bot.handle),
677
+ if (isBroadcastMention(userMessage.body)) return memberIds;
678
+ const mentioned = new Set(
679
+ messageMentionIds(
680
+ { author: userMessage.author || "you", body: userMessage.body, mentions: userMessage.mentions },
681
+ store.listBots(),
682
+ ),
460
683
  );
461
- const mentioned = new Set<string>();
462
- for (const bot of bots) {
463
- if (names.includes(bot.handle.toLowerCase())) mentioned.add(bot.id);
464
- }
465
684
  if (mentioned.size > 0) {
466
685
  return memberIds.filter((id) => mentioned.has(id));
467
686
  }
@@ -480,7 +699,11 @@ function turnUserMessage(
480
699
  store.listBots().find((bot) => bot.id === parent.author)?.handle ||
481
700
  parent.author;
482
701
  const preview = parent.body.replace(/\s+/g, " ").trim().slice(0, 240);
483
- return `(回覆 @${handle}:${preview})\n${body}`;
702
+ return [
703
+ `(回覆 @${handle}。只做下一句;引言裡的交棒已經發生過,不要再叫別人。)`,
704
+ `> ${preview}`,
705
+ body,
706
+ ].join("\n");
484
707
  }
485
708
 
486
709
  const ATTACH_TOKEN = /^\[[A-Za-z]+ #\d+\]$/;
@@ -534,22 +757,26 @@ function askedText(
534
757
  export function inviteMentionedBots(
535
758
  store: GuildStore,
536
759
  roomId: string,
537
- userText: string,
760
+ userMessage: { author?: string; body: string; mentions?: string[] },
538
761
  ): string[] {
539
762
  const room = store.getRoom(roomId);
540
763
  if (!room) return [];
541
764
  if (room.kind !== "channel") return room.memberIds;
542
- if (isBroadcastMention(userText)) return room.memberIds;
543
- const names = summonedHandles(
544
- userText,
545
- store.listBots().map((bot) => bot.handle),
765
+ if (isBroadcastMention(userMessage.body)) return room.memberIds;
766
+ const ids = messageMentionIds(
767
+ {
768
+ author: userMessage.author || "you",
769
+ body: userMessage.body,
770
+ mentions: userMessage.mentions,
771
+ },
772
+ store.listBots(),
546
773
  );
547
774
  let memberIds = room.memberIds;
548
- for (const bot of store.listBots()) {
549
- if (!names.includes(bot.handle.toLowerCase())) continue;
550
- if (memberIds.includes(bot.id)) continue;
551
- store.addMember(roomId, bot.id);
552
- memberIds = [...memberIds, bot.id];
775
+ for (const id of ids) {
776
+ if (memberIds.includes(id)) continue;
777
+ if (!store.getBot(id)) continue;
778
+ store.addMember(roomId, id);
779
+ memberIds = [...memberIds, id];
553
780
  }
554
781
  return memberIds;
555
782
  }
@@ -734,7 +961,18 @@ function liveDetail(trace: ToolTrace): string {
734
961
  if (trace.name === "skill") return String(args.name || "");
735
962
  if (trace.name === "image_gen") return String(args.prompt || "");
736
963
  if (trace.name === "spawn") {
737
- return String(args.description || args.name || args.prompt || "");
964
+ return String(
965
+ args.title ||
966
+ args.description ||
967
+ args.profile ||
968
+ args.name ||
969
+ args.task ||
970
+ args.prompt ||
971
+ "",
972
+ );
973
+ }
974
+ if (trace.name === "read_spawn") {
975
+ return String(args.agent_id || args.id || "");
738
976
  }
739
977
  if (trace.name.startsWith("mcp__")) {
740
978
  return JSON.stringify(args).slice(0, 120);
@@ -918,20 +1156,20 @@ async function generateReplies(
918
1156
  store: GuildStore,
919
1157
  roomId: string,
920
1158
  memberIds: string[],
921
- userMessage: { body: string; attachments?: ChatAttachment[] },
1159
+ userMessage: { author?: string; body: string; attachments?: ChatAttachment[]; mentions?: string[] },
922
1160
  history: HistoryItem[],
923
1161
  onlyBotId?: string,
924
1162
  env: NodeJS.ProcessEnv = process.env,
925
1163
  parent?: ChatMessage,
926
1164
  extras: HandlerExtras = {},
927
1165
  ) {
928
- const extraBotId = hasExplicitSummon(store, userMessage.body)
1166
+ const extraBotId = hasExplicitSummon(store, userMessage)
929
1167
  ? undefined
930
1168
  : followBotId(store, history, parent);
931
1169
  const asked = askedText(store, parent, userMessage);
932
1170
  const targets = onlyBotId
933
1171
  ? [onlyBotId]
934
- : replyBots(store, memberIds, userMessage.body, extraBotId);
1172
+ : replyBots(store, memberIds, userMessage, extraBotId);
935
1173
  const replies: ChatMessage[] = [];
936
1174
  const harvested: { handle: string; author: string; body: string }[] = [];
937
1175
  const signal = store.beginTurn(roomId, targets);
@@ -996,6 +1234,11 @@ async function generateReplies(
996
1234
  throw err;
997
1235
  }
998
1236
  const usage = { ...(generated.usage || {}), startedAt };
1237
+ const hopMentions = parseMentionIds(
1238
+ generated.body,
1239
+ store.listBots(),
1240
+ "bot",
1241
+ ).filter((id) => id !== botId);
999
1242
  const reply = store.appendMessage(
1000
1243
  roomId,
1001
1244
  botId,
@@ -1004,6 +1247,9 @@ async function generateReplies(
1004
1247
  undefined,
1005
1248
  undefined,
1006
1249
  usage,
1250
+ undefined,
1251
+ undefined,
1252
+ hopMentions,
1007
1253
  );
1008
1254
  store.dropLiveBotTurn(roomId, botId);
1009
1255
  recordTurn(store, roomId, botId, generated, reply);
@@ -1045,8 +1291,12 @@ async function generateReplies(
1045
1291
  return speak(botId, turnAsked, history);
1046
1292
  }),
1047
1293
  );
1048
- const hops = handoffTargets(store, memberIds, replies, asked, history);
1049
- if (hops.length && !signal.aborted) {
1294
+ let seen = 0;
1295
+ for (let wave = 0; wave < CHANNEL_ROSTER_CAP && !signal.aborted; wave++) {
1296
+ const fresh = replies.slice(seen);
1297
+ seen = replies.length;
1298
+ const hops = handoffTargets(store, memberIds, replies, asked, history, fresh);
1299
+ if (!hops.length) break;
1050
1300
  const hopAt = new Date().toISOString();
1051
1301
  for (const hop of hops) {
1052
1302
  store.adoptTurn(roomId, hop.botId, signal);
@@ -1133,7 +1383,7 @@ export function listRoomTrajectory(store: GuildStore, roomId: string) {
1133
1383
  });
1134
1384
  return {
1135
1385
  ...base,
1136
- events: base.events.concat(extra),
1386
+ events: base.events.map(promoteSpawnEvent).concat(extra),
1137
1387
  live: extra.length > 0 || open,
1138
1388
  };
1139
1389
  }
@@ -1200,12 +1450,33 @@ function followBotId(
1200
1450
  return lastBotSpeaker(store, messages);
1201
1451
  }
1202
1452
 
1203
- function hasExplicitSummon(store: GuildStore, userText: string): boolean {
1204
- if (isBroadcastMention(userText)) return true;
1453
+ /** Reply-to-a-bot locks that seat. Client assigneeId cannot steal it unless the new text @mentions someone. */
1454
+ function exclusiveReplyBot(
1455
+ store: GuildStore,
1456
+ userMessage: { author?: string; body: string; mentions?: string[] },
1457
+ parent: ChatMessage | undefined,
1458
+ assignee?: string,
1459
+ ): string | undefined {
1460
+ if (hasExplicitSummon(store, userMessage)) return assignee || undefined;
1461
+ if (parent && parent.author !== "you" && store.getBot(parent.author)) {
1462
+ return parent.author;
1463
+ }
1464
+ return assignee || undefined;
1465
+ }
1466
+
1467
+ function hasExplicitSummon(
1468
+ store: GuildStore,
1469
+ userMessage: { author?: string; body: string; mentions?: string[] },
1470
+ ): boolean {
1471
+ if (isBroadcastMention(userMessage.body)) return true;
1205
1472
  return (
1206
- summonedHandles(
1207
- userText,
1208
- store.listBots().map((bot) => bot.handle),
1473
+ messageMentionIds(
1474
+ {
1475
+ author: userMessage.author || "you",
1476
+ body: userMessage.body,
1477
+ mentions: userMessage.mentions,
1478
+ },
1479
+ store.listBots(),
1209
1480
  ).length > 0
1210
1481
  );
1211
1482
  }
@@ -1258,6 +1529,11 @@ export async function postUserMessage(
1258
1529
  const packed = parseAttachments(attachments);
1259
1530
  const tokens = packed?.map((att) => att.token).join(" ") || "";
1260
1531
  const text = body.trim() || tokens;
1532
+ const bots = store.listBots();
1533
+ const mentions =
1534
+ extras.mentions !== undefined
1535
+ ? sanitizeMentionIds(extras.mentions, bots)
1536
+ : parseMentionIds(text, bots, "user");
1261
1537
  const message = store.appendMessage(
1262
1538
  roomId,
1263
1539
  "you",
@@ -1265,6 +1541,10 @@ export async function postUserMessage(
1265
1541
  undefined,
1266
1542
  parent ? parent.id : undefined,
1267
1543
  packed,
1544
+ undefined,
1545
+ undefined,
1546
+ undefined,
1547
+ mentions,
1268
1548
  );
1269
1549
  try {
1270
1550
  store.appendTrajectory(roomId, [
@@ -1275,10 +1555,11 @@ export async function postUserMessage(
1275
1555
  }
1276
1556
  const history = previous.map(toHistoryItem);
1277
1557
  const assignee = assigneeId?.trim();
1278
- let memberIds = assignee
1279
- ? inviteAssignee(store, roomId, assignee)
1280
- : inviteMentionedBots(store, roomId, message.body);
1281
- if (!assignee && !hasExplicitSummon(store, message.body)) {
1558
+ const only = exclusiveReplyBot(store, message, parent, assignee);
1559
+ let memberIds = only
1560
+ ? inviteAssignee(store, roomId, only)
1561
+ : inviteMentionedBots(store, roomId, message);
1562
+ if (!only && !hasExplicitSummon(store, message)) {
1282
1563
  memberIds = includeFollowBot(
1283
1564
  store,
1284
1565
  roomId,
@@ -1292,7 +1573,7 @@ export async function postUserMessage(
1292
1573
  memberIds,
1293
1574
  message,
1294
1575
  history,
1295
- assignee || undefined,
1576
+ only || undefined,
1296
1577
  env,
1297
1578
  parent,
1298
1579
  extras,
@@ -1317,19 +1598,25 @@ export async function retryMessage(
1317
1598
  const current = messages[index];
1318
1599
 
1319
1600
  if (current.author === "you") {
1601
+ const bots = store.listBots();
1602
+ const mentions =
1603
+ extras.mentions !== undefined
1604
+ ? sanitizeMentionIds(extras.mentions, bots)
1605
+ : undefined;
1320
1606
  const message =
1321
1607
  typeof body === "string" && body.trim()
1322
- ? store.updateMessage(roomId, messageId, body)
1608
+ ? store.updateMessage(roomId, messageId, body, mentions)
1323
1609
  : current;
1324
1610
  store.truncateAfter(roomId, messageId);
1325
1611
  const kept = store.listMessages(roomId);
1326
1612
  const history = kept.slice(0, -1).map(toHistoryItem);
1327
1613
  const parent = parentMessage(kept.slice(0, -1), message.replyTo);
1328
1614
  const assignee = assigneeId?.trim();
1329
- let memberIds = assignee
1330
- ? inviteAssignee(store, roomId, assignee)
1331
- : inviteMentionedBots(store, roomId, message.body);
1332
- if (!assignee && !hasExplicitSummon(store, message.body)) {
1615
+ const only = exclusiveReplyBot(store, message, parent, assignee);
1616
+ let memberIds = only
1617
+ ? inviteAssignee(store, roomId, only)
1618
+ : inviteMentionedBots(store, roomId, message);
1619
+ if (!only && !hasExplicitSummon(store, message)) {
1333
1620
  memberIds = includeFollowBot(
1334
1621
  store,
1335
1622
  roomId,
@@ -1343,7 +1630,7 @@ export async function retryMessage(
1343
1630
  memberIds,
1344
1631
  message,
1345
1632
  history,
1346
- assignee || undefined,
1633
+ only || undefined,
1347
1634
  env,
1348
1635
  parent,
1349
1636
  extras,
@@ -1443,4 +1730,4 @@ export async function retryMessage(
1443
1730
 
1444
1731
  export { StoreError, localGenerate };
1445
1732
 
1446
- export { publicModels, mergeModelsFile } from "./llm.ts";
1733
+ export { publicModels, mergeModelsFile, refreshOpenCodeFreeCatalog } from "./llm.ts";
package/src/harness.ts CHANGED
@@ -156,7 +156,15 @@ export function gateTool(
156
156
  if (sandbox === "full_access") return null;
157
157
 
158
158
  if (sandbox === "read_only") {
159
- if (name === "read" || name === "list" || name === "skill") return null;
159
+ if (
160
+ name === "read" ||
161
+ name === "list" ||
162
+ name === "skill" ||
163
+ name === "spawn" ||
164
+ name === "read_spawn"
165
+ ) {
166
+ return null;
167
+ }
160
168
  return {
161
169
  text: `sandbox=read_only refused ${name}`,
162
170
  isError: true,
@@ -174,7 +182,13 @@ export function gateTool(
174
182
  };
175
183
  }
176
184
 
177
- if (name === "read" || name === "list" || name === "skill" || name === "spawn") {
185
+ if (
186
+ name === "read" ||
187
+ name === "list" ||
188
+ name === "skill" ||
189
+ name === "spawn" ||
190
+ name === "read_spawn"
191
+ ) {
178
192
  return null;
179
193
  }
180
194
 
@@ -296,18 +310,17 @@ export async function runAgentLoop(input: {
296
310
  if (traces.length) return { text: emptyAfterTools, traces, thinking };
297
311
  return input.nullIfNoTraces ? null : { text: emptyAfterTools, traces, thinking };
298
312
  }
299
- const outcomes: ToolOutcome[] = [];
300
- for (const call of asked.calls) {
301
- outcomes.push(
302
- await executeToolTraced(
313
+ const outcomes = await Promise.all(
314
+ asked.calls.map((call) =>
315
+ executeToolTraced(
303
316
  call.name,
304
317
  call.args,
305
318
  input.toolCtx,
306
319
  traces,
307
320
  thinking,
308
321
  ),
309
- );
310
- }
322
+ ),
323
+ );
311
324
  input.onTools?.(asked.calls, outcomes);
312
325
  }
313
326
  }
package/src/image-gen.ts CHANGED
@@ -218,7 +218,7 @@ export async function generateImage(input: {
218
218
  aspectRatio?: string;
219
219
  dataDir?: string;
220
220
  env?: NodeJS.ProcessEnv;
221
- }): Promise<{ text: string; isError: boolean }> {
221
+ }): Promise<{ text: string; isError: boolean; publicPath?: string }> {
222
222
  const prompt = String(input.prompt || "").trim();
223
223
  if (!prompt) return { text: "prompt is required", isError: true };
224
224
  const aspect = String(input.aspectRatio || "").trim();
@@ -252,6 +252,7 @@ export async function generateImage(input: {
252
252
  `markdown: ![${prompt.slice(0, 80)}](${saved.publicPath})`,
253
253
  ].join("\n"),
254
254
  isError: false,
255
+ publicPath: saved.publicPath,
255
256
  };
256
257
  }
257
258
  errors.push(`${route.model} @ ${route.url}: ${hit.error}`);