Files
zalbot/src/graphmcp/loreResolver.ts
Kaysser Kayyali fdf1d705d1 fix: lobby close UI + loreResolver skips index chunks (3 live-run bugs)
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>
2026-06-22 22:09:42 +00:00

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;
}