@starlight-ai/discord-waifus 1.3.8 → 1.3.9

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.
@@ -1,3 +1,3 @@
1
1
  import { WaifuConfig } from "../shared/schemas/domain.js";
2
- export type PrebuiltWaifu = Pick<WaifuConfig, "id" | "name" | "displayName" | "enabled" | "persona" | "contextWindow" | "generation">;
2
+ export type PrebuiltWaifu = Pick<WaifuConfig, "id" | "name" | "displayName" | "enabled" | "persona" | "contextWindow" | "generation" | "availability" | "tools">;
3
3
  export declare const PREBUILT_WAIFUS: PrebuiltWaifu[];
@@ -9,6 +9,17 @@ export const PREBUILT_WAIFUS = [
9
9
  temperature: 0.8,
10
10
  topP: 0.95
11
11
  },
12
+ availability: {
13
+ sleep: { enabled: true, start: "00:30", end: "08:30" },
14
+ busy: [
15
+ { start: "13:00", end: "14:00", reason: "offline for lunch and errands" },
16
+ { start: "19:00", end: "20:30", reason: "winding down away from chat" }
17
+ ]
18
+ },
19
+ tools: {
20
+ toolUse: true,
21
+ pickNextWaifu: true
22
+ },
12
23
  persona: [
13
24
  "Lumi is warm, bright, and emotionally attentive. She notices small mood shifts in chat and responds with gentle curiosity instead of forcing positivity.",
14
25
  "She speaks in short, natural Discord messages, uses soft humor, and is good at making quieter users feel included.",
@@ -25,6 +36,16 @@ export const PREBUILT_WAIFUS = [
25
36
  temperature: 0.9,
26
37
  topP: 0.9
27
38
  },
39
+ availability: {
40
+ sleep: { enabled: true, start: "03:00", end: "11:00" },
41
+ busy: [
42
+ { start: "16:30", end: "17:30", reason: "pretending to be productive" }
43
+ ]
44
+ },
45
+ tools: {
46
+ toolUse: true,
47
+ pickNextWaifu: true
48
+ },
28
49
  persona: [
29
50
  "Nox is dry, witty, and a little mischievous. She likes deadpan one-liners, clever callbacks, and playful skepticism.",
30
51
  "She is never cruel: the teasing should feel like a friend poking fun, not an insult. She backs off when the conversation gets serious.",
@@ -41,6 +62,17 @@ export const PREBUILT_WAIFUS = [
41
62
  temperature: 0.75,
42
63
  topP: 0.9
43
64
  },
65
+ availability: {
66
+ sleep: { enabled: true, start: "23:00", end: "06:30" },
67
+ busy: [
68
+ { start: "09:00", end: "11:00", reason: "focused planning block" },
69
+ { start: "15:00", end: "16:00", reason: "quiet work session" }
70
+ ]
71
+ },
72
+ tools: {
73
+ toolUse: true,
74
+ pickNextWaifu: true
75
+ },
44
76
  persona: [
45
77
  "Mira is calm, precise, and quietly competent. She enjoys organizing messy conversations, asking useful questions, and helping people decide what to do next.",
46
78
  "She should sound like a composed friend, not a corporate assistant. She can be practical without becoming stiff.",
@@ -57,6 +89,17 @@ export const PREBUILT_WAIFUS = [
57
89
  temperature: 1,
58
90
  topP: 0.95
59
91
  },
92
+ availability: {
93
+ sleep: { enabled: true, start: "02:00", end: "09:30" },
94
+ busy: [
95
+ { start: "12:00", end: "12:45", reason: "running around between plans" },
96
+ { start: "21:30", end: "22:15", reason: "dramatic snack break" }
97
+ ]
98
+ },
99
+ tools: {
100
+ toolUse: true,
101
+ pickNextWaifu: true
102
+ },
60
103
  persona: [
61
104
  "Riko is energetic, impulsive, and dramatic in a fun way. She likes bits, sudden enthusiasm, mock-serious declarations, and turning ordinary moments into tiny events.",
62
105
  "She should not dominate the chat. Her best messages are brief sparks that make others want to reply.",
@@ -1 +1 @@
1
- {"version":3,"file":"prebuiltWaifus.js","sourceRoot":"","sources":["../../src/config/prebuiltWaifus.ts"],"names":[],"mappings":"AAOA,MAAM,CAAC,MAAM,eAAe,GAAoB;IAC9C;QACE,EAAE,EAAE,MAAM;QACV,IAAI,EAAE,MAAM;QACZ,WAAW,EAAE,MAAM;QACnB,OAAO,EAAE,IAAI;QACb,aAAa,EAAE,EAAE;QACjB,UAAU,EAAE;YACV,WAAW,EAAE,GAAG;YAChB,IAAI,EAAE,IAAI;SACX;QACD,OAAO,EAAE;YACP,0JAA0J;YAC1J,oHAAoH;YACpH,0IAA0I;SAC3I,CAAC,IAAI,CAAC,MAAM,CAAC;KACf;IACD;QACE,EAAE,EAAE,KAAK;QACT,IAAI,EAAE,KAAK;QACX,WAAW,EAAE,KAAK;QAClB,OAAO,EAAE,IAAI;QACb,aAAa,EAAE,EAAE;QACjB,UAAU,EAAE;YACV,WAAW,EAAE,GAAG;YAChB,IAAI,EAAE,GAAG;SACV;QACD,OAAO,EAAE;YACP,sHAAsH;YACtH,wIAAwI;YACxI,gHAAgH;SACjH,CAAC,IAAI,CAAC,MAAM,CAAC;KACf;IACD;QACE,EAAE,EAAE,MAAM;QACV,IAAI,EAAE,MAAM;QACZ,WAAW,EAAE,MAAM;QACnB,OAAO,EAAE,IAAI;QACb,aAAa,EAAE,EAAE;QACjB,UAAU,EAAE;YACV,WAAW,EAAE,IAAI;YACjB,IAAI,EAAE,GAAG;SACV;QACD,OAAO,EAAE;YACP,8JAA8J;YAC9J,kHAAkH;YAClH,+HAA+H;SAChI,CAAC,IAAI,CAAC,MAAM,CAAC;KACf;IACD;QACE,EAAE,EAAE,MAAM;QACV,IAAI,EAAE,MAAM;QACZ,WAAW,EAAE,MAAM;QACnB,OAAO,EAAE,IAAI;QACb,aAAa,EAAE,EAAE;QACjB,UAAU,EAAE;YACV,WAAW,EAAE,CAAC;YACd,IAAI,EAAE,IAAI;SACX;QACD,OAAO,EAAE;YACP,uKAAuK;YACvK,sGAAsG;YACtG,kHAAkH;SACnH,CAAC,IAAI,CAAC,MAAM,CAAC;KACf;CACF,CAAC"}
1
+ {"version":3,"file":"prebuiltWaifus.js","sourceRoot":"","sources":["../../src/config/prebuiltWaifus.ts"],"names":[],"mappings":"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"}
@@ -15,6 +15,7 @@ export declare const OrchestratorActionSchema: z.ZodEnum<{
15
15
  }>;
16
16
  export declare const RETRIGGER_MIN_SECONDS = 100;
17
17
  export declare const RETRIGGER_MAX_SECONDS = 7200;
18
+ export declare const MAX_WAIFU_DELAY_SECONDS = 30;
18
19
  export declare const RespondingWaifuSchema: z.ZodObject<{
19
20
  waifuId: z.ZodString;
20
21
  delaySeconds: z.ZodNumber;
@@ -5,6 +5,7 @@ export const ORCHESTRATOR_ACTION_VALUES = ["reply", "no_reply"];
5
5
  export const OrchestratorActionSchema = z.enum(ORCHESTRATOR_ACTION_VALUES);
6
6
  export const RETRIGGER_MIN_SECONDS = 100;
7
7
  export const RETRIGGER_MAX_SECONDS = 7200;
8
+ export const MAX_WAIFU_DELAY_SECONDS = 30;
8
9
  export const RespondingWaifuSchema = z.object({
9
10
  waifuId: z.string().min(1),
10
11
  delaySeconds: z.number().min(0),
@@ -1 +1 @@
1
- {"version":3,"file":"decisions.js","sourceRoot":"","sources":["../../src/orchestration/decisions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"decisions.js","sourceRoot":"","sources":["../../src/orchestration/decisions.ts"],"names":[],"mappings":"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"}
@@ -9,7 +9,7 @@ import { AgentConfigSchema, DiscordBotsFileSchema, GuildEmojisFileSchema, Memory
9
9
  import { createRevisionedBase, nowIso } from "../shared/schemas/common.js";
10
10
  import { ChannelSessionStateSchema, createEmptyChannelSessionState } from "./session.js";
11
11
  import { formatTimestamp } from "./context.js";
12
- import { RETRIGGER_MAX_SECONDS, RETRIGGER_MIN_SECONDS } from "./decisions.js";
12
+ import { MAX_WAIFU_DELAY_SECONDS, RETRIGGER_MAX_SECONDS, RETRIGGER_MIN_SECONDS } from "./decisions.js";
13
13
  export class RuntimeOrchestrator {
14
14
  options;
15
15
  activeRuns = new Map();
@@ -321,6 +321,7 @@ export class RuntimeOrchestrator {
321
321
  finally {
322
322
  orchestratorTyping.stop();
323
323
  }
324
+ decision = capDecisionDelays(decision);
324
325
  await this.appendOrchestratorHistory({
325
326
  id: randomUUID(),
326
327
  guildId,
@@ -345,9 +346,14 @@ export class RuntimeOrchestrator {
345
346
  return;
346
347
  }
347
348
  let executedCount = 0;
349
+ let directHandoffCount = 0;
348
350
  const allowedWaifus = new Set(channel.enabledWaifuIds ?? []);
349
- for (const responder of decision.respondingWaifus) {
351
+ const responderQueue = [...decision.respondingWaifus];
352
+ while (responderQueue.length > 0) {
350
353
  throwIfAborted(signal);
354
+ const responder = responderQueue.shift();
355
+ if (!responder)
356
+ continue;
351
357
  if (!allowedWaifus.has(responder.waifuId)) {
352
358
  this.options.logger.warn("Orchestrator selected a waifu that is not enabled for channel", {
353
359
  guildId,
@@ -382,12 +388,13 @@ export class RuntimeOrchestrator {
382
388
  continue;
383
389
  }
384
390
  if (responder.delaySeconds > 0) {
385
- const cappedDelayMs = Math.min(responder.delaySeconds, 600) * 1000;
391
+ const cappedDelaySeconds = Math.min(responder.delaySeconds, MAX_WAIFU_DELAY_SECONDS);
392
+ const cappedDelayMs = cappedDelaySeconds * 1000;
386
393
  this.options.logger.info("Waiting before waifu reply", {
387
394
  guildId,
388
395
  channelId,
389
396
  waifuId: waifu.id,
390
- delaySeconds: Math.min(responder.delaySeconds, 600)
397
+ delaySeconds: cappedDelaySeconds
391
398
  });
392
399
  await this.sleep(cappedDelayMs, signal);
393
400
  throwIfAborted(signal);
@@ -416,6 +423,9 @@ export class RuntimeOrchestrator {
416
423
  });
417
424
  try {
418
425
  const currentWaifuAuthorIds = await this.waifuAuthorIdsFor(waifu.botId);
426
+ const nextWaifuIds = availableWaifus
427
+ .filter((candidate) => candidate.id !== waifu.id && candidate.botId && candidate.modelId)
428
+ .map((candidate) => candidate.id);
419
429
  const waifuQueryKey = runKey(guildId);
420
430
  incrementActive(this.activeWaifuQueries, waifuQueryKey);
421
431
  const result = await (async () => {
@@ -423,9 +433,11 @@ export class RuntimeOrchestrator {
423
433
  return await waifuPipeline.generateWaifu({
424
434
  modelId: waifuModelId,
425
435
  messages: waifuMessages,
426
- systemPrompt: await this.buildWaifuSystemPrompt(guildId, waifu),
436
+ systemPrompt: await this.buildWaifuSystemPrompt(guildId, waifu, availableWaifus),
427
437
  sceneDirection: responder.sceneDirection,
428
438
  replyStyle: responder.replyStyle,
439
+ availableWaifuIds: nextWaifuIds,
440
+ pickNextWaifuToolEnabled: waifu.tools.pickNextWaifu,
429
441
  temperature: waifu.generation.temperature,
430
442
  topP: waifu.generation.topP,
431
443
  maxOutputTokens: waifu.generation.maxOutputTokens,
@@ -478,6 +490,29 @@ export class RuntimeOrchestrator {
478
490
  allowedUserMentionIds: activeAuthorIds,
479
491
  signal
480
492
  });
493
+ if (result.pickedNextWaifuId && directHandoffCount < this.maxAutomaticTurns) {
494
+ directHandoffCount += 1;
495
+ this.options.logger.info("Waifu picked next waifu; skipping orchestrator for direct handoff", {
496
+ guildId,
497
+ channelId,
498
+ waifuId: waifu.id,
499
+ pickedNextWaifuId: result.pickedNextWaifuId
500
+ });
501
+ responderQueue.splice(0, responderQueue.length, {
502
+ waifuId: result.pickedNextWaifuId,
503
+ delaySeconds: 0,
504
+ replyStyle: "normal"
505
+ });
506
+ }
507
+ else if (result.pickedNextWaifuId) {
508
+ this.options.logger.warn("Ignoring PickNextWaifu handoff because the automatic handoff limit was reached", {
509
+ guildId,
510
+ channelId,
511
+ waifuId: waifu.id,
512
+ pickedNextWaifuId: result.pickedNextWaifuId,
513
+ maxAutomaticTurns: this.maxAutomaticTurns
514
+ });
515
+ }
481
516
  }
482
517
  finally {
483
518
  waifuTyping.stop();
@@ -943,7 +978,7 @@ export class RuntimeOrchestrator {
943
978
  async readMemoryStore() {
944
979
  return this.options.storage.readJson("user/memories.json", MemoryStoreSchema, emptyMemoryStore());
945
980
  }
946
- async buildWaifuSystemPrompt(guildId, waifu) {
981
+ async buildWaifuSystemPrompt(guildId, waifu, availableWaifus) {
947
982
  const [store, emojis] = await Promise.all([
948
983
  this.readMemoryStore(),
949
984
  this.options.storage.readJson(path.join("user", "servers", guildId, "emojis.json"), GuildEmojisFileSchema, GuildEmojisFileSchema.parse(createEmptyRevisionedFile({ guildId, emojis: [] })))
@@ -988,6 +1023,10 @@ export class RuntimeOrchestrator {
988
1023
  if (memories) {
989
1024
  behaviorSections.push(`<memories>\n${memories}\n</memories>`);
990
1025
  }
1026
+ const toolUse = buildWaifuToolUseInstructions(waifu, availableWaifus);
1027
+ if (toolUse) {
1028
+ behaviorSections.push(`<tool_use>\n${toolUse}\n</tool_use>`);
1029
+ }
991
1030
  const behaviorBlock = `<${behaviorTag}>\n${behaviorSections.join("\n")}\n</${behaviorTag}>`;
992
1031
  const currentTimeBlock = `<current_time>\n${formatTimestamp(new Date())} (UTC)\n</current_time>`;
993
1032
  const emojiBlock = `<available_server_emojis>\n${emojiList || "(none cached)"}\n</available_server_emojis>`;
@@ -997,6 +1036,7 @@ export class RuntimeOrchestrator {
997
1036
  if (orchestrator.useLegacyPrompt) {
998
1037
  return buildLegacyOrchestratorPrompt(server, availableWaifus);
999
1038
  }
1039
+ const scheduleNow = new Date();
1000
1040
  const activeWaifusContent = availableWaifus.length
1001
1041
  ? availableWaifus
1002
1042
  .map((waifu) => {
@@ -1004,7 +1044,8 @@ export class RuntimeOrchestrator {
1004
1044
  const displayName = waifu.displayName || waifu.name;
1005
1045
  const persona = waifu.persona.trim();
1006
1046
  const personaBlock = persona || "(no persona configured)";
1007
- return `<${tagName}>\nID: ${waifu.id}\nDisplay name: ${displayName}\nPersona:\n${personaBlock}\n</${tagName}>`;
1047
+ const availability = formatWaifuAvailabilityForPrompt(waifu, scheduleNow);
1048
+ return `<${tagName}>\nID: ${waifu.id}\nDisplay name: ${displayName}\nPersona:\n${personaBlock}\nAvailability:\n${availability}\n</${tagName}>`;
1008
1049
  })
1009
1050
  .join("\n\n")
1010
1051
  : "No waifus are currently enabled for this channel.";
@@ -1012,11 +1053,13 @@ export class RuntimeOrchestrator {
1012
1053
  const hardRules = [
1013
1054
  "- Every respondingWaifus[].waifuId must be copied verbatim from one of the IDs listed in <active_waifus>.",
1014
1055
  "- action=\"reply\" requires a non-empty respondingWaifus array and a null retriggerAfterSeconds. action=\"no_reply\" requires respondingWaifus=[] and a retriggerAfterSeconds in [100, 7200].",
1015
- "- delaySeconds is a realistic reading/typing delay before that waifu starts replying, in seconds. Use 0 if she should start immediately.",
1056
+ `- delaySeconds is a realistic reading/typing delay before that waifu starts replying, in seconds, from 0 to ${MAX_WAIFU_DELAY_SECONDS}. Use 0 if she should start immediately.`,
1016
1057
  "- replyStyle is a soft hint for length/tone: \"normal\" by default; \"short\" for one terse line; \"long\" for a slightly fuller reply; \"sleepy\" for tired/low-energy voice.",
1058
+ "- Sleep and busy availability in <active_waifus> is soft context, not a hard rule. A sleeping or busy waifu can still answer if recent momentum suggests she is awake, if she just spoke, if she was directly pulled in, or if waking her improves the room.",
1017
1059
  "- repleyToMessageIndex should usually be null. Set it only when a waifu is reviving or anchoring to a specific older message that is no longer the latest visible message; never to the immediately previous message.",
1018
1060
  "- sceneDirection is the only private channel between you and that waifu. If you want her to do or say something specific, you MUST put it there. She does not see your reasoning. Use null when no special steering is needed.",
1019
- "- Consecutive waifu replies in a single decision are allowed but soft-discouraged. More consecutive waifus require a stronger reason: a fresh beat, escalation, interruption, joke, reaction, or emotional shift."
1061
+ "- After a single waifu message, do not default to no_reply just because a waifu already spoke. Actively consider whether another waifu should react, interrupt, tease, disagree, answer a missed user, or carry the beat one step further.",
1062
+ "- Consecutive waifu replies in a single decision are allowed when they add a fresh beat: escalation, interruption, joke, reaction, disagreement, emotional shift, or a new topic. Avoid only empty echoing or repetitive back-and-forth."
1020
1063
  ].join("\n");
1021
1064
  const taskInstructions = DEFAULT_ORCHESTRATOR_PROMPT;
1022
1065
  const loopBreaking = [
@@ -1047,7 +1090,7 @@ export class RuntimeOrchestrator {
1047
1090
  " \"respondingWaifus\": [",
1048
1091
  " {",
1049
1092
  " \"waifuId\": string,",
1050
- " \"delaySeconds\": number (>= 0),",
1093
+ ` \"delaySeconds\": number (0..${MAX_WAIFU_DELAY_SECONDS}),`,
1051
1094
  " \"replyStyle\": \"normal\" | \"short\" | \"long\" | \"sleepy\",",
1052
1095
  " \"repleyToMessageIndex\": number | null,",
1053
1096
  " \"sceneDirection\": string | null",
@@ -1060,7 +1103,7 @@ export class RuntimeOrchestrator {
1060
1103
  "Rules:",
1061
1104
  "- action=\"reply\" => respondingWaifus is non-empty; retriggerAfterSeconds is null.",
1062
1105
  "- action=\"no_reply\" => respondingWaifus is empty; retriggerAfterSeconds is a number in [100, 7200].",
1063
- "- Order matters in respondingWaifus: the first waifu speaks first, then the next, and so on. Any new chat message interrupts the rest of the chain.",
1106
+ "- Order matters in respondingWaifus: the first waifu speaks first, then the next, and so on. Each waifu's delay starts only after the previous waifu has finished. Any new chat message interrupts the rest of the chain.",
1064
1107
  "- All five fields on each respondingWaifus entry are required; set repleyToMessageIndex and sceneDirection to null when not needed."
1065
1108
  ].join("\n");
1066
1109
  const sections = orchestrator.promptSections;
@@ -1225,6 +1268,71 @@ export class RuntimeOrchestrator {
1225
1268
  function emptyMemoryStore() {
1226
1269
  return MemoryStoreSchema.parse(createEmptyRevisionedFile({ memories: [] }));
1227
1270
  }
1271
+ function buildWaifuToolUseInstructions(waifu, availableWaifus) {
1272
+ if (!waifu.tools.toolUse || !waifu.tools.pickNextWaifu)
1273
+ return undefined;
1274
+ const model = waifu.modelId ? getModel(waifu.modelId) : undefined;
1275
+ if (!model?.supportsTools)
1276
+ return undefined;
1277
+ const candidates = availableWaifus
1278
+ .filter((candidate) => candidate.id !== waifu.id && candidate.botId && candidate.modelId)
1279
+ .map((candidate) => `${candidate.id} (${candidate.displayName || candidate.name})`);
1280
+ if (candidates.length === 0)
1281
+ return undefined;
1282
+ return [
1283
+ "You have one optional tool: PickNextWaifu.",
1284
+ "Use it only after writing your Discord reply when another waifu should immediately speak next and the orchestrator should be skipped for that handoff.",
1285
+ "Do not call it if your message should be the end of this beat.",
1286
+ "Arguments: { \"waifuId\": string }.",
1287
+ "Available waifus:",
1288
+ ...candidates.map((candidate) => `- ${candidate}`)
1289
+ ].join("\n");
1290
+ }
1291
+ function formatWaifuAvailabilityForPrompt(waifu, now) {
1292
+ const availability = waifu.availability;
1293
+ const currentMinutes = localTimeOfDayMinutes(now);
1294
+ const lines = [`- Current local schedule time: ${formatLocalTimeOfDay(now)}.`];
1295
+ if (availability.sleep.enabled) {
1296
+ const sleepingNow = dailyIntervalContains(currentMinutes, availability.sleep);
1297
+ lines.push(`- Sleep: ${availability.sleep.start}-${availability.sleep.end} daily (${sleepingNow ? "currently inside sleep time" : "not currently inside sleep time"}). Treat this as lower likelihood, not a rule; she may still be awake if she spoke recently, was directly pulled in, or waking her improves the room.`);
1298
+ }
1299
+ else {
1300
+ lines.push("- Sleep: none configured.");
1301
+ }
1302
+ if (availability.busy.length > 0) {
1303
+ lines.push("- Busy:");
1304
+ for (const interval of availability.busy) {
1305
+ const busyNow = dailyIntervalContains(currentMinutes, interval);
1306
+ lines.push(` - ${interval.start}-${interval.end}: ${interval.reason}${busyNow ? " (currently busy)" : ""}`);
1307
+ }
1308
+ }
1309
+ else {
1310
+ lines.push("- Busy: none configured.");
1311
+ }
1312
+ return lines.join("\n");
1313
+ }
1314
+ function localTimeOfDayMinutes(date) {
1315
+ return date.getHours() * 60 + date.getMinutes();
1316
+ }
1317
+ function formatLocalTimeOfDay(date) {
1318
+ return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
1319
+ }
1320
+ function dailyIntervalContains(currentMinutes, interval) {
1321
+ const start = timeOfDayMinutes(interval.start);
1322
+ const end = timeOfDayMinutes(interval.end);
1323
+ if (start === end)
1324
+ return false;
1325
+ if (start < end)
1326
+ return currentMinutes >= start && currentMinutes < end;
1327
+ return currentMinutes >= start || currentMinutes < end;
1328
+ }
1329
+ function timeOfDayMinutes(value) {
1330
+ const [hours = "0", minutes = "0"] = value.split(":");
1331
+ return Number(hours) * 60 + Number(minutes);
1332
+ }
1333
+ function indentLines(value, indent) {
1334
+ return value.split("\n").map((line) => `${indent}${line}`).join("\n");
1335
+ }
1228
1336
  function sanitizeTagName(value) {
1229
1337
  return value.trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "waifu";
1230
1338
  }
@@ -1243,6 +1351,15 @@ function replyTargetForFreshContext(replyToMessageId, messages) {
1243
1351
  const latestMessage = messages.at(-1);
1244
1352
  return latestMessage?.id === replyToMessageId ? undefined : replyToMessageId;
1245
1353
  }
1354
+ function capDecisionDelays(decision) {
1355
+ return {
1356
+ ...decision,
1357
+ respondingWaifus: decision.respondingWaifus.map((responder) => ({
1358
+ ...responder,
1359
+ delaySeconds: Math.min(responder.delaySeconds, MAX_WAIFU_DELAY_SECONDS)
1360
+ }))
1361
+ };
1362
+ }
1246
1363
  function throwIfAborted(signal) {
1247
1364
  if (signal?.aborted) {
1248
1365
  throw signal.reason instanceof Error ? signal.reason : new Error("Request aborted.");
@@ -1318,11 +1435,13 @@ const DEFAULT_ORCHESTRATOR_PROMPT = [
1318
1435
  "",
1319
1436
  "When action=\"no_reply\", set retriggerAfterSeconds to the number of seconds you want to wait before re-evaluating the room. The orchestrator also wakes up automatically on any new chat message, so retriggerAfterSeconds is a planned pause, not a polling tick. A chain of one no_reply means \"don't speak now; re-check after the pause.\"",
1320
1437
  "",
1438
+ "After a single waifu message, do not treat no_reply as the automatic cleanup move. Ask whether the room would feel more alive if another waifu reacts, cuts in, disagrees, lightly teases, answers a missed user, or follows the beat one step further. Choose no_reply when the beat has genuinely landed, the next bot message would feel repetitive, or silence creates better pacing.",
1439
+ "",
1321
1440
  "If a recent chat participant message or direct ping was missed while the room moved on, prefer steering a waifu to acknowledge it so the chat stays socially inclusive — unless silence is clearly the more natural choice.",
1322
1441
  "",
1323
1442
  "Reach for sceneDirection when the next reply needs steering that personality alone won't provide: redirecting topic, closing a beat, creating an interruption, shifting momentum, or deliberately starting something new even when it cuts against the current flow. Prefer a natural bridge when pivoting, but a jarring shift is fine if the scene needs it. Keep sceneDirection short, concrete, and immediately actionable — one sentence is usually enough. When you refer to a specific person, use their actual display name from the chat history, never generic phrases like \"the user\". Name intended participants explicitly when more than one person is involved; avoid ambiguous group references like \"us\", \"them\", or \"everyone\". If multiple waifus respond in the same turn, each may receive a different sceneDirection.",
1324
1443
  "",
1325
- "delaySeconds is a realistic reading/typing delay before that waifu starts replying. Use 0 to start immediately; small values (a few seconds) feel natural for short replies; larger values fit longer or more thoughtful replies. Keep it grounded in the chat's pace.",
1444
+ `delaySeconds is a realistic reading/typing delay before that waifu starts replying, capped at ${MAX_WAIFU_DELAY_SECONDS} seconds. Use 0 to start immediately; small values feel natural for short replies; larger values fit longer or more thoughtful replies. Keep it grounded in the chat's pace.`,
1326
1445
  "",
1327
1446
  "replyStyle is a soft hint for that one reply: \"normal\" by default, \"short\" for a one-line beat, \"long\" for a slightly fuller reply, \"sleepy\" for a low-energy tone. The waifu's persona still does most of the work.",
1328
1447
  "",
@@ -1330,15 +1449,17 @@ const DEFAULT_ORCHESTRATOR_PROMPT = [
1330
1449
  "",
1331
1450
  "Pay special attention to the latest 10 messages and the recent speaker pattern. If the same waifu has been carrying the scene for multiple beats, strongly consider switching to another waifu, using no_reply, or using sceneDirection to create a fresh beat.",
1332
1451
  "",
1333
- "Continue a waifu-to-waifu chain only when the next message adds something new: escalation, interruption, joke, emotional shift, contradiction, surprise, or a new topic. Do not continue just to restate the same mood."
1452
+ "Continue a waifu-to-waifu chain when the next message adds something new: escalation, interruption, joke, emotional shift, contradiction, surprise, a missed-user acknowledgment, or a new topic. Do not continue just to restate the same mood."
1334
1453
  ].join("\n");
1335
1454
  function buildLegacyOrchestratorPrompt(server, availableWaifus) {
1455
+ const scheduleNow = new Date();
1336
1456
  const waifuBlock = availableWaifus.length
1337
1457
  ? availableWaifus
1338
1458
  .map((waifu) => {
1339
1459
  const displayName = waifu.displayName || waifu.name || waifu.id;
1340
1460
  const persona = waifu.persona.trim() || "(no persona configured)";
1341
- return `### ${displayName} (ID: ${waifu.id})\n- Personality: ${persona}`;
1461
+ const availability = formatWaifuAvailabilityForPrompt(waifu, scheduleNow);
1462
+ return `### ${displayName} (ID: ${waifu.id})\n- Personality: ${persona}\n- Availability:\n${indentLines(availability, " ")}`;
1342
1463
  })
1343
1464
  .join("\n\n")
1344
1465
  : "No waifus are currently enabled for this channel.";
@@ -1364,17 +1485,18 @@ function buildLegacyOrchestratorPrompt(server, availableWaifus) {
1364
1485
  "4. Always pay special attention to the latest 10 messages. They are the strongest signal for what the room is currently doing, who may have been overlooked, and whether a loop is starting to form.",
1365
1486
  "5. Sleep time, busy time, and consecutive-message heuristics are soft preferences. Break them whenever doing so would clearly improve conversational flow, realism, or enjoyment.",
1366
1487
  "6. The same waifu may speak again, a different waifu may jump in, or multiple waifus may chain if it feels right.",
1367
- "7. Avoid repetitive follow-ups that merely restate the same beat. Continue only when the next message adds something new.",
1368
- "8. If a recent user message or direct ping went unnoticed while the room moved on, prefer steering someone to acknowledge it so the chat stays socially inclusive unless silence is clearly more natural.",
1369
- "9. \"no_reply\" is valid. If you choose it, set retriggerAfterSeconds to a natural delay between 100 and 7200 seconds. respondingWaifus must be empty.",
1370
- "10. Use timestamps and pacing. Slow gaps matter.",
1371
- "11. delaySeconds should reflect realistic reading and typing time. 0 means start immediately.",
1372
- "12. replyStyle is a soft hint: \"normal\" by default; \"short\" for one terse line; \"long\" for a slightly fuller reply; \"sleepy\" for a low-energy voice. Use \"normal\" when in doubt.",
1373
- "13. repleyToMessageIndex is optional. Leave it null by default.",
1374
- "14. Most waifu messages should be normal messages, not Discord replies.",
1375
- "15. Do not set repleyToMessageIndex to the immediately previous message. If a waifu is simply responding to the latest beat, send a normal message instead.",
1376
- "16. If you are reviving, acknowledging, or directly answering an older user message or direct ping that went overlooked, you should usually set repleyToMessageIndex to that message's #N context index so the response stays anchored to the right person and beat.",
1377
- "17. Use repleyToMessageIndex only when targeting a specific older message materially improves clarity, isolates a side thread, answers an earlier question, or creates a specific social effect. Copy the #N index from the chat history.",
1488
+ "7. After a single waifu message, do not default to no_reply. Consider whether another waifu should react, interrupt, disagree, tease, answer a missed user, or carry the beat one step further.",
1489
+ "8. Avoid repetitive follow-ups that merely restate the same beat. Continue when the next message adds something new.",
1490
+ "9. If a recent user message or direct ping went unnoticed while the room moved on, prefer steering someone to acknowledge it so the chat stays socially inclusive unless silence is clearly more natural.",
1491
+ "10. \"no_reply\" is valid. If you choose it, set retriggerAfterSeconds to a natural delay between 100 and 7200 seconds. respondingWaifus must be empty.",
1492
+ "11. Use timestamps and pacing. Slow gaps matter.",
1493
+ `12. delaySeconds should reflect realistic reading and typing time from 0 to ${MAX_WAIFU_DELAY_SECONDS}. 0 means start immediately.`,
1494
+ "13. replyStyle is a soft hint: \"normal\" by default; \"short\" for one terse line; \"long\" for a slightly fuller reply; \"sleepy\" for a low-energy voice. Use \"normal\" when in doubt.",
1495
+ "14. repleyToMessageIndex is optional. Leave it null by default.",
1496
+ "15. Most waifu messages should be normal messages, not Discord replies.",
1497
+ "16. Do not set repleyToMessageIndex to the immediately previous message. If a waifu is simply responding to the latest beat, send a normal message instead.",
1498
+ "17. If you are reviving, acknowledging, or directly answering an older user message or direct ping that went overlooked, you should usually set repleyToMessageIndex to that message's #N context index so the response stays anchored to the right person and beat.",
1499
+ "18. Use repleyToMessageIndex only when targeting a specific older message materially improves clarity, isolates a side thread, answers an earlier question, or creates a specific social effect. Copy the #N index from the chat history.",
1378
1500
  "",
1379
1501
  "## sceneDirection",
1380
1502
  "sceneDirection is an invisible director note for that waifu's next message only.",
@@ -1397,7 +1519,7 @@ function buildLegacyOrchestratorPrompt(server, availableWaifus) {
1397
1519
  "{",
1398
1520
  " \"action\": \"reply\" | \"no_reply\",",
1399
1521
  " \"respondingWaifus\": [",
1400
- " { \"waifuId\": string, \"delaySeconds\": number, \"replyStyle\": \"normal\"|\"short\"|\"long\"|\"sleepy\", \"repleyToMessageIndex\": number|null, \"sceneDirection\": string|null }",
1522
+ ` { \"waifuId\": string, \"delaySeconds\": number (0..${MAX_WAIFU_DELAY_SECONDS}), \"replyStyle\": \"normal\"|\"short\"|\"long\"|\"sleepy\", \"repleyToMessageIndex\": number|null, \"sceneDirection\": string|null }`,
1401
1523
  " ],",
1402
1524
  " \"retriggerAfterSeconds\": number (100..7200) | null,",
1403
1525
  " \"reasoning\": string",