@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.
- package/dist/orchestration/deterministicOrchestrator.d.ts +18 -0
- package/dist/orchestration/deterministicOrchestrator.js +175 -0
- package/dist/orchestration/deterministicOrchestrator.js.map +1 -0
- package/dist/orchestration/runtime.d.ts +2 -0
- package/dist/orchestration/runtime.js +80 -25
- package/dist/orchestration/runtime.js.map +1 -1
- package/dist-frontend/assets/{index-omaWjYXM.js → index-CgNbleJE.js} +2 -2
- package/dist-frontend/assets/{index-omaWjYXM.js.map → index-CgNbleJE.js.map} +1 -1
- package/dist-frontend/index.html +1 -1
- package/docs/assistant-kb/orchestrator-and-direction.md +10 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
854
|
-
|
|
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
|
-
|
|
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 =
|
|
888
|
-
|
|
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
|
|
923
|
-
|
|
924
|
-
messages,
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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}`);
|