Bug 1 — loreResolver no longer uses lore-index chunks as randomizable values. A disposition placeholder resolved to the 'All NPCs' index (Overview + [[wiki- links]]), corrupting the opening narrative. Filter out index-like chunks (Overview/Entries/Index header OR >=2 [[links]]) from candidates; fall back if only index chunks remain. TDD: tests/unit/loreResolver.test.ts (4 tests). (Follow-up: short-value randomizables like dispositions should use source:vocabulary — the engine guard is the safety net.) Bug 2 — begin announcement now precedes the opening narrative. handleStartBtn uses interaction.update (edits the lobby embed to the announcement) BEFORE beginEncounter posts the opening, so the thread reads announcement -> opening. Bug 3 — lobby close updates the UI + renames the thread. On Begin: the lobby embed is closed in place (Join/Begin buttons removed, replaced by the announcement) + the thread is renamed '... — underway'. On Cancel: the thread is renamed '... — cancelled' before archive. Live test AC-8 asserts the embed is closed (no buttons + announcement) + the thread rename. Verified: tsc clean, 543 unit tests pass (+4), live multiplayer E2E 3/3. Co-Authored-By: Claude <noreply@anthropic.com>
67 lines
2.6 KiB
TypeScript
67 lines
2.6 KiB
TypeScript
import { semanticSearch } from './client.js';
|
|
import { sampleFromVocabulary } from './vocabularyResolver.js';
|
|
import { config } from '../config.js';
|
|
import type { RandomizableItem } from '../types/index.js';
|
|
|
|
/**
|
|
* Resolves all randomizable spec items against the GraphMCP knowledge graph.
|
|
*
|
|
* For each item: queries semanticSearch, filters results above the score
|
|
* threshold, picks one at random, and trims it to a usable length.
|
|
* Falls back to item.fallback if GraphMCP returns nothing useful.
|
|
*
|
|
* Returns a flat Record<key, resolvedValue> ready to store in session state
|
|
* and inject into the system prompt.
|
|
*/
|
|
|
|
// Lore-index chunks (the "All NPCs" overview, category lists, etc.) are
|
|
// encyclopedia navigation, not usable VALUES for a prose placeholder. They leak
|
|
// "[[wiki-link]]" lists + "Overview/Entries" headers into interpolated text
|
|
// (Bug 1: a disposition placeholder resolved to the NPC index). Skip them and
|
|
// prefer a real content chunk, or fall back.
|
|
function isIndexLike(content: string): boolean {
|
|
const head = content.trimStart().slice(0, 64).toLowerCase();
|
|
if (/^(overview|entries|index|contents|categories)\b/.test(head)) return true;
|
|
const linkCount = (content.match(/\[\[[^\]]+\]\]/g) ?? []).length;
|
|
return linkCount >= 2; // a value chunk rarely has 2+ wiki-links; an index does
|
|
}
|
|
|
|
export async function resolveRandomizables(
|
|
items: RandomizableItem[],
|
|
): Promise<Record<string, string>> {
|
|
if (items.length === 0) return {};
|
|
|
|
const resolved: Record<string, string> = {};
|
|
|
|
await Promise.all(
|
|
items.map(async item => {
|
|
if (item.source === 'vocabulary') {
|
|
resolved[item.key] = sampleFromVocabulary(item.category ?? '', item.fallback);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await semanticSearch(item.query, config.GRAPHMCP_NPC_MEMORY_LIMIT);
|
|
const candidates = (result.chunks ?? [])
|
|
.filter(c => c.score > config.GRAPHMCP_SCORE_THRESHOLD)
|
|
.filter(c => !isIndexLike(c.content));
|
|
|
|
if (candidates.length > 0) {
|
|
// Pick randomly so repeated starts of the same spec feel different
|
|
const pick = candidates[Math.floor(Math.random() * candidates.length)];
|
|
// Trim to something the LLM can use inline without blowing context
|
|
const text = pick.content.trim().slice(0, 200);
|
|
resolved[item.key] = text || item.fallback;
|
|
} else {
|
|
resolved[item.key] = item.fallback;
|
|
}
|
|
} catch (err) {
|
|
console.warn(`[loreResolver] failed to resolve "${item.key}":`, err);
|
|
resolved[item.key] = item.fallback;
|
|
}
|
|
}),
|
|
);
|
|
|
|
return resolved;
|
|
}
|