@waifucave/discord-waifus 1.5.192 → 1.5.193

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.
@@ -0,0 +1,18 @@
1
+ import type { ContextMessage } from "./context.js";
2
+ import type { OrchestratorDecision } from "./decisions.js";
3
+ import type { WaifuAvailability } from "../shared/schemas/domain.js";
4
+ export type DeterministicCastMember = {
5
+ waifuId: string;
6
+ /** Every handle she answers to: name, display name, server nickname. */
7
+ names: string[];
8
+ /** Discord author ids that attribute context messages to her (bot entry id + application id). */
9
+ authorIds: string[];
10
+ availability: WaifuAvailability;
11
+ };
12
+ export type DeterministicDecisionInput = {
13
+ /** Chronological, oldest first — same order the model orchestrator receives. */
14
+ messages: ContextMessage[];
15
+ cast: DeterministicCastMember[];
16
+ now: Date;
17
+ };
18
+ export declare function decideDeterministically(input: DeterministicDecisionInput): OrchestratorDecision;
@@ -0,0 +1,175 @@
1
+ // Deterministic (model-free) orchestrator decisions.
2
+ //
3
+ // Two jobs: a zero-credit orchestration mode, and the fallback when the model call fails or a
4
+ // provider blocks the prompt (live incident 2026-07-06: Gemini PROHIBITED_CONTENT blocked an
5
+ // active room repeatedly). It picks the next speaker from structural signals only — reply
6
+ // targets, name addressing, references, rotation, availability — never content judgment.
7
+ //
8
+ // Pure module: no side effects, no imports from runtime. Interval helpers are copied verbatim
9
+ // from runtime.ts (modules stay decoupled per W1 precedent). Schedules are server-local time,
10
+ // same convention as currentlyDoingForWaifu.
11
+ const ROTATION_WINDOW = 4; // recent waifu messages examined for dominance penalties
12
+ const SCORE_REPLY_TARGET = 100;
13
+ const SCORE_ADDRESSED = 90;
14
+ const SCORE_REFERENCED = 30;
15
+ const SCORE_WAIFU_REFERENCED = 15;
16
+ const SCORE_PARTICIPANT = 20; // must stay below PENALTY_PER_RECENT_MESSAGE so rotation wins on generic messages
17
+ const PENALTY_LAST_SPEAKER = -60;
18
+ const PENALTY_PER_RECENT_MESSAGE = -25;
19
+ const PARTICIPATION_WINDOW = 10; // messages scanned for thread participation
20
+ export function decideDeterministically(input) {
21
+ const { messages, now } = input;
22
+ const latest = messages[messages.length - 1];
23
+ const newestHumanTs = [...messages].reverse().find((m) => m.authorKind === "user")?.timestamp;
24
+ const humanAgeSeconds = newestHumanTs !== undefined ? Math.max(0, (now.getTime() - Date.parse(newestHumanTs)) / 1000) : Infinity;
25
+ const candidates = input.cast.filter((member) => isAvailable(member.availability, now));
26
+ const quiet = (reason) => ({
27
+ action: "no_reply",
28
+ respondingWaifus: [],
29
+ retriggerAfterSeconds: retriggerForHumanAge(humanAgeSeconds),
30
+ reasoning: `deterministic: ${reason}`
31
+ });
32
+ if (candidates.length === 0)
33
+ return quiet("no enabled, awake, unbusy waifu is available");
34
+ if (!latest || latest.authorKind !== "user") {
35
+ return quiet("latest message is not from a human; cast-only pacing");
36
+ }
37
+ const authorOf = (message) => input.cast.find((member) => member.authorIds.includes(message.authorId) || matchesName(member, message.displayName, "exact"));
38
+ const recentWaifuMessages = messages.filter((m) => m.authorKind === "waifu").slice(-ROTATION_WINDOW);
39
+ const lastWaifuMessage = recentWaifuMessages[recentWaifuMessages.length - 1];
40
+ const participationSlice = messages.slice(-PARTICIPATION_WINDOW);
41
+ const scored = candidates.map((member) => {
42
+ let score = 0;
43
+ const reasons = [];
44
+ if (latest.replyTo?.authorName && matchesName(member, latest.replyTo.authorName, "exact")) {
45
+ score += SCORE_REPLY_TARGET;
46
+ reasons.push("reply target");
47
+ }
48
+ if (isAddressed(member, latest.content)) {
49
+ score += SCORE_ADDRESSED;
50
+ reasons.push("addressed by name");
51
+ }
52
+ else if (isReferenced(member, latest.content)) {
53
+ score += SCORE_REFERENCED;
54
+ reasons.push("referenced");
55
+ }
56
+ if (recentWaifuMessages.some((m) => authorOf(m)?.waifuId !== member.waifuId && isReferenced(member, m.content))) {
57
+ score += SCORE_WAIFU_REFERENCED;
58
+ reasons.push("talked about by the cast");
59
+ }
60
+ if (participationSlice.some((m) => m.authorKind === "waifu" && authorOf(m)?.waifuId === member.waifuId)) {
61
+ score += SCORE_PARTICIPANT;
62
+ reasons.push("in the live thread");
63
+ }
64
+ if (lastWaifuMessage && authorOf(lastWaifuMessage)?.waifuId === member.waifuId) {
65
+ score += PENALTY_LAST_SPEAKER;
66
+ reasons.push("spoke last");
67
+ }
68
+ const recentCount = recentWaifuMessages.filter((m) => authorOf(m)?.waifuId === member.waifuId).length;
69
+ score += recentCount * PENALTY_PER_RECENT_MESSAGE;
70
+ // rotation tiebreak: the longer since she spoke, the better
71
+ const lastSpokeIndex = findLastIndex(messages, (m) => m.authorKind === "waifu" && authorOf(m)?.waifuId === member.waifuId);
72
+ const staleness = lastSpokeIndex === -1 ? messages.length : messages.length - 1 - lastSpokeIndex;
73
+ return { member, score, staleness, reasons };
74
+ });
75
+ scored.sort((a, b) => {
76
+ if (b.score !== a.score)
77
+ return b.score - a.score;
78
+ if (b.staleness !== a.staleness)
79
+ return b.staleness - a.staleness;
80
+ // stable variety tiebreak: hash of (latest message id, waifu id)
81
+ return stableHash(latest.id + b.member.waifuId) - stableHash(latest.id + a.member.waifuId);
82
+ });
83
+ const winner = scored[0];
84
+ const delaySeconds = 2 + (stableHash(latest.id + winner.member.waifuId) % 4);
85
+ return {
86
+ action: "reply",
87
+ respondingWaifus: [{ waifuId: winner.member.waifuId, delaySeconds }],
88
+ reasoning: `deterministic: ${winner.member.waifuId} picked (${winner.reasons.join(", ") || "rotation"}; score ${winner.score})`
89
+ };
90
+ }
91
+ /** Is she awake and not busy right now? Server-local time, like the schedule prompt blocks. */
92
+ function isAvailable(availability, now) {
93
+ const minutes = now.getHours() * 60 + now.getMinutes();
94
+ if (availability.sleep.enabled && dailyIntervalContains(minutes, availability.sleep))
95
+ return false;
96
+ for (const interval of availability.busy) {
97
+ if (dailyIntervalContains(minutes, interval))
98
+ return false;
99
+ }
100
+ return true;
101
+ }
102
+ /** Addressed = her name opens/closes the message, or appears alongside second-person "you". */
103
+ function isAddressed(member, content) {
104
+ const text = content.trim();
105
+ const lower = text.toLowerCase();
106
+ for (const name of member.names) {
107
+ const n = name.trim();
108
+ if (!n)
109
+ continue;
110
+ const ln = n.toLowerCase();
111
+ if (lower.startsWith(ln))
112
+ return true;
113
+ if (lower.endsWith(ln) || lower.endsWith(`${ln}?`) || lower.endsWith(`${ln}!`))
114
+ return true;
115
+ if (containsName(lower, ln) && /\byou\b|\bu\b/.test(lower))
116
+ return true;
117
+ }
118
+ return false;
119
+ }
120
+ function isReferenced(member, content) {
121
+ const lower = content.toLowerCase();
122
+ return member.names.some((name) => name.trim() && containsName(lower, name.trim().toLowerCase()));
123
+ }
124
+ /** Word-boundary match for ASCII names, substring for CJK/emoji handles where \b is useless. */
125
+ function containsName(haystackLower, nameLower) {
126
+ if (/^[\x20-\x7e]+$/.test(nameLower)) {
127
+ return new RegExp(`(^|[^a-z0-9])${escapeRegExp(nameLower)}($|[^a-z0-9])`).test(haystackLower);
128
+ }
129
+ return haystackLower.includes(nameLower);
130
+ }
131
+ function matchesName(member, value, _mode) {
132
+ if (!value)
133
+ return false;
134
+ const v = value.trim().toLowerCase();
135
+ return member.names.some((name) => name.trim().toLowerCase() === v);
136
+ }
137
+ function retriggerForHumanAge(ageSeconds) {
138
+ if (ageSeconds < 120)
139
+ return 45;
140
+ if (ageSeconds < 900)
141
+ return 240;
142
+ if (ageSeconds < 7200)
143
+ return 900;
144
+ return 3600;
145
+ }
146
+ function escapeRegExp(value) {
147
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
148
+ }
149
+ function stableHash(value) {
150
+ let hash = 0;
151
+ for (let i = 0; i < value.length; i++)
152
+ hash = (hash * 31 + value.charCodeAt(i)) | 0;
153
+ return Math.abs(hash);
154
+ }
155
+ function findLastIndex(items, predicate) {
156
+ for (let i = items.length - 1; i >= 0; i--) {
157
+ if (predicate(items[i]))
158
+ return i;
159
+ }
160
+ return -1;
161
+ }
162
+ function dailyIntervalContains(currentMinutes, interval) {
163
+ const start = timeOfDayMinutes(interval.start);
164
+ const end = timeOfDayMinutes(interval.end);
165
+ if (start === end)
166
+ return false;
167
+ if (start < end)
168
+ return currentMinutes >= start && currentMinutes < end;
169
+ return currentMinutes >= start || currentMinutes < end;
170
+ }
171
+ function timeOfDayMinutes(value) {
172
+ const [hours = "0", minutes = "0"] = value.split(":");
173
+ return Number(hours) * 60 + Number(minutes);
174
+ }
175
+ //# sourceMappingURL=deterministicOrchestrator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deterministicOrchestrator.js","sourceRoot":"","sources":["../../src/orchestration/deterministicOrchestrator.ts"],"names":[],"mappings":"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"}
@@ -156,6 +156,8 @@ export declare class RuntimeOrchestrator {
156
156
  private readActiveChatParticipants;
157
157
  private recordWaifuNotes;
158
158
  private buildWaifuPromptParts;
159
+ /** Cast members + guild-nickname map for the deterministic decider and casting cards. */
160
+ private buildDeterministicCast;
159
161
  private buildOrchestratorSystemPrompt;
160
162
  private buildOrchestratorTrailingPrompt;
161
163
  private ensureServer;
@@ -8,6 +8,7 @@ import { SOFT_VALIDATOR_CHECKS, correctiveRetryMessage, validateWaifuOutput } fr
8
8
  import { extractReplyQuote } from "./replyQuote.js";
9
9
  import { createGatewayModelPipeline } from "./pipeline/gatewayPipeline.js";
10
10
  import { isPermissionError, PermissionWarningTracker } from "./permissionWarnings.js";
11
+ import { decideDeterministically } from "./deterministicOrchestrator.js";
11
12
  import { GatewayPipelineError } from "./pipeline/params.js";
12
13
  import { resolveModelTarget, sharedRegistry } from "./pipeline/resolveTarget.js";
13
14
  import { ActiveChatParticipantsFileSchema, AgentConfigSchema, DiscordBotsFileSchema, GuildEmojisFileSchema, GuildMembersFileSchema, MemoryStoreSchema, OrchestratorDebugConfigFileSchema, OrchestratorHistoryFileSchema, PendingObservationsFileSchema, ReviewerHistoryFileSchema, ServerConfigSchema, StageManagerHistoryFileSchema, WaifuConfigSchema, createEmptyRevisionedFile } from "../shared/schemas/domain.js";
@@ -850,10 +851,8 @@ export class RuntimeOrchestrator {
850
851
  return;
851
852
  }
852
853
  const orchestrator = await this.readAgentConfig("orchestrator", 40);
853
- if (!orchestrator.modelId) {
854
- this.options.logger.warn("Orchestrator model is not configured; channel loop stopped", { guildId, channelId });
855
- return;
856
- }
854
+ // No model + enabled = free deterministic mode: structural speaker selection, no API spend.
855
+ const deterministicMode = !orchestrator.modelId;
857
856
  let messages = await this.options.discord.fetchFreshContext({
858
857
  guildId,
859
858
  channelId,
@@ -861,7 +860,9 @@ export class RuntimeOrchestrator {
861
860
  signal
862
861
  });
863
862
  await this.noteActiveChatParticipantsFromContext(guildId, channelId, messages);
864
- messages = await this.messagesForModel(messages, { providerId: orchestrator.providerId, modelId: orchestrator.modelId }, signal);
863
+ if (orchestrator.modelId) {
864
+ messages = await this.messagesForModel(messages, { providerId: orchestrator.providerId, modelId: orchestrator.modelId }, signal);
865
+ }
865
866
  this.options.logger.info("Fetched Discord context for orchestrator", {
866
867
  guildId,
867
868
  channelId,
@@ -884,11 +885,14 @@ export class RuntimeOrchestrator {
884
885
  return;
885
886
  }
886
887
  }
887
- const pipeline = this.pipelineFor({ providerId: orchestrator.providerId, modelId: orchestrator.modelId }, "orchestrator");
888
- if (!pipeline.decideOrchestrator) {
888
+ const pipeline = deterministicMode
889
+ ? undefined
890
+ : this.pipelineFor({ providerId: orchestrator.providerId, modelId: orchestrator.modelId }, "orchestrator");
891
+ if (pipeline && !pipeline.decideOrchestrator) {
889
892
  throw new Error(`Model ${orchestrator.modelId} does not implement orchestrator decisions.`);
890
893
  }
891
894
  const availableWaifus = await this.listAvailableWaifusForChannel(channel);
895
+ const { cast: deterministicCast, nicknames: guildNicknames } = await this.buildDeterministicCast(guildId, availableWaifus);
892
896
  const pastDecisions = await this.readCompletedOrchestratorDecisionsForChannel(guildId, channelId);
893
897
  const requireReply = Boolean(options.firstOrchestratorMustReply && turns === 1);
894
898
  const loop = assessLoop(messages);
@@ -916,22 +920,49 @@ export class RuntimeOrchestrator {
916
920
  wakePlan: lastNoReply.wakePlan
917
921
  }];
918
922
  }
919
- const trailingPrompt = this.buildOrchestratorTrailingPrompt(orchestrator, availableWaifus, requireReply, runtimeNotice);
923
+ const trailingPrompt = this.buildOrchestratorTrailingPrompt(orchestrator, availableWaifus, requireReply, runtimeNotice, guildNicknames);
920
924
  // No typing scope for orchestrator decisions: the orchestrator bot showing "typing…" while it
921
925
  // deliberates leaks the machinery's presence into the room. Only the waifu send path types.
922
- let decision = await pipeline.decideOrchestrator({
923
- modelId: orchestrator.modelId,
924
- messages,
925
- pastDecisions,
926
- decisionMarkers,
927
- directiveBudgetOpen,
928
- trailingPrompt,
929
- systemPrompt: this.buildOrchestratorSystemPrompt(orchestrator, server, requireReply),
930
- availableWaifuIds: availableWaifus.map((waifu) => waifu.id),
931
- replyRequired: requireReply,
932
- params: orchestrator.params,
933
- signal
934
- });
926
+ let decision;
927
+ if (!pipeline || !pipeline.decideOrchestrator) {
928
+ decision = decideDeterministically({ messages, cast: deterministicCast, now: new Date() });
929
+ this.options.logger.info("Deterministic orchestrator decision (no model configured)", {
930
+ guildId,
931
+ channelId,
932
+ action: decision.action,
933
+ reasoning: decision.reasoning
934
+ });
935
+ }
936
+ else {
937
+ try {
938
+ decision = await pipeline.decideOrchestrator({
939
+ modelId: orchestrator.modelId,
940
+ messages,
941
+ pastDecisions,
942
+ decisionMarkers,
943
+ directiveBudgetOpen,
944
+ trailingPrompt,
945
+ systemPrompt: this.buildOrchestratorSystemPrompt(orchestrator, server, requireReply),
946
+ availableWaifuIds: availableWaifus.map((waifu) => waifu.id),
947
+ replyRequired: requireReply,
948
+ params: orchestrator.params,
949
+ signal
950
+ });
951
+ }
952
+ catch (error) {
953
+ // Manual /run promised the user a reply from the model — surface its failure instead.
954
+ if (requireReply || signal.aborted)
955
+ throw error;
956
+ decision = decideDeterministically({ messages, cast: deterministicCast, now: new Date() });
957
+ this.options.logger.warn("Model orchestrator failed; deterministic fallback decision used", {
958
+ guildId,
959
+ channelId,
960
+ message: error instanceof Error ? error.message : String(error),
961
+ action: decision.action,
962
+ reasoning: decision.reasoning
963
+ });
964
+ }
965
+ }
935
966
  decision = capDecisionDelays(decision);
936
967
  if (requireReply) {
937
968
  decision = applyFirstResponderDirectiveOverride(decision, options.firstResponderSceneDirectionOverride);
@@ -2631,6 +2662,29 @@ export class RuntimeOrchestrator {
2631
2662
  selfGuildNickname: serverNickname !== waifu.displayName ? serverNickname : undefined
2632
2663
  };
2633
2664
  }
2665
+ /** Cast members + guild-nickname map for the deterministic decider and casting cards. */
2666
+ async buildDeterministicCast(guildId, waifus) {
2667
+ const [bots, members] = await Promise.all([
2668
+ this.readDiscordBotsFile(),
2669
+ this.options.storage.readJson(path.join("user", "servers", guildId, "members.json"), GuildMembersFileSchema, GuildMembersFileSchema.parse(createEmptyRevisionedFile({ guildId, members: [] })))
2670
+ ]);
2671
+ const guildNameByUserId = new Map(members.members
2672
+ .filter((member) => member.guildDisplayName)
2673
+ .map((member) => [member.userId, member.guildDisplayName]));
2674
+ const nicknames = new Map();
2675
+ const cast = [];
2676
+ for (const waifu of waifus) {
2677
+ const authorIds = resolveBotAuthorIds(waifu.botId, bots);
2678
+ const nickname = authorIds.map((id) => guildNameByUserId.get(id)).find(Boolean);
2679
+ if (nickname && nickname !== waifu.displayName)
2680
+ nicknames.set(waifu.id, nickname);
2681
+ if (!waifu.modelId)
2682
+ continue; // she cannot generate a reply anyway
2683
+ const names = [...new Set([waifu.name, waifu.displayName, nickname].filter((v) => Boolean(v?.trim())))];
2684
+ cast.push({ waifuId: waifu.id, names, authorIds, availability: waifu.availability });
2685
+ }
2686
+ return { cast, nicknames };
2687
+ }
2634
2688
  buildOrchestratorSystemPrompt(orchestrator, server, replyRequired = false) {
2635
2689
  const identity = replyRequired
2636
2690
  ? "You are the director of a multi-character Discord bot. This manual /run requires you to choose at least one waifu to reply now."
@@ -2654,10 +2708,10 @@ export class RuntimeOrchestrator {
2654
2708
  `<discord_server_information>\n${server.name ?? server.guildId}\n</discord_server_information>`
2655
2709
  ].filter(Boolean).join("\n");
2656
2710
  }
2657
- buildOrchestratorTrailingPrompt(orchestrator, availableWaifus, replyRequired = false, loopNotice) {
2711
+ buildOrchestratorTrailingPrompt(orchestrator, availableWaifus, replyRequired = false, loopNotice, nicknames) {
2658
2712
  const now = new Date();
2659
2713
  const activeWaifusContent = availableWaifus.length
2660
- ? availableWaifus.map((waifu) => castingCard(waifu, now, waifuSeesImages(waifu))).join("\n")
2714
+ ? availableWaifus.map((waifu) => castingCard(waifu, now, waifuSeesImages(waifu), nicknames?.get(waifu.id))).join("\n")
2661
2715
  : "No waifus are currently enabled for this channel.";
2662
2716
  const pausePlanning = "When you choose no_reply, retriggerAfterSeconds is a planned pause before YOU re-check the room — any new human message wakes you regardless, so long pauses cost nothing. wakePlan is one sentence on what you intend at wake; the runtime shows it back to you when the timer fires. A pivot plan should name the new topic, and when the wake comes you execute it with a change_topic directive — a plan without a directive usually dissolves into the old topic. Picking the pause: remember an ACTIVE room should rarely get here at all — if humans are engaged, the right move was a reply, not a pause. When the room genuinely cooled: 10–60s only to give a mid-flow room a short breath, 100–180s right after a beat lands (a quick check whether it caught), 240–600s for a cooling thread, 900–1800s for a planned revival of a quiet room, 3600s+ when you are mostly waiting for humans to return. Do NOT default to a round 300 — pick an exact number that matches the room's energy (e.g. 110, 140, 190, 420), and vary it so your pacing never becomes a predictable cycle. The same is true of your wake rhythm: if your last few wakes each produced the same shape (wake, two replies, pause), break the pattern — one voice instead of two, a directive instead of a reply, or a genuinely longer sleep. Repeated quiet checks must back off to longer pauses.";
2663
2717
  const task = replyRequired
@@ -3701,12 +3755,13 @@ function waifuSeesImages(waifu) {
3701
3755
  return false;
3702
3756
  }
3703
3757
  }
3704
- function castingCard(waifu, now, seesImages = false) {
3758
+ function castingCard(waifu, now, seesImages = false, serverNickname) {
3705
3759
  const tagName = promptTagName(waifu.name || waifu.id);
3706
3760
  const displayName = waifu.displayName || waifu.name;
3761
+ const nicknamePart = serverNickname && serverNickname !== displayName ? ` · appears in this server as "${serverNickname}"` : "";
3707
3762
  const lines = [
3708
3763
  `<${tagName}>`,
3709
- `ID: ${waifu.id} · ${displayName}${seesImages ? " · sees images natively" : ""}`
3764
+ `ID: ${waifu.id} · ${displayName}${nicknamePart}${seesImages ? " · sees images natively" : ""}`
3710
3765
  ];
3711
3766
  if (waifu.personaDigest) {
3712
3767
  lines.push(`Voice: ${waifu.personaDigest.voice}`);