diff --git a/lib/engine/director.ts b/lib/engine/director.ts index 8a2b029..aeb5e6c 100644 --- a/lib/engine/director.ts +++ b/lib/engine/director.ts @@ -13,6 +13,7 @@ import type { Session, StoryState, StoryStatePatch, + StreamRouterResult, WriterScenePlan, } from "@infiplot/types"; import type { CharacterCard } from "./agents/characterDesigner"; @@ -259,6 +260,25 @@ export async function directScene( resolvePlan(extracted); } return result; + }).catch((err): StreamRouterResult => { + // routeTaggedStream rejected (stream read / network failure) BEFORE onPlan + // fired. Without this, planPromise would never settle and `await + // planPromise` below would hang the whole request FOREVER. Settle the plan + // with a minimal fallback and resolve routing to a degraded result so the + // pipeline produces a playable fallback scene (graceful degradation) rather + // than hanging or hard-crashing. + console.warn("[directScene] routeTaggedStream rejected, degrading:", err); + if (!planSettled) { + planSettled = true; + resolvePlan(minimalFallbackPlan()); + } + return { + plan: undefined, + beats: [], + choices: undefined, + rawStorySegment: undefined, + degraded: true, + }; }); // ── Step 2 — await plan (settles at close — EARLY) ──────── diff --git a/lib/engine/prompts/registry.ts b/lib/engine/prompts/registry.ts index 2063270..e62983e 100644 --- a/lib/engine/prompts/registry.ts +++ b/lib/engine/prompts/registry.ts @@ -27,7 +27,11 @@ export const WRITER_SEGMENTS: PromptSegment[] = [ WRITER_FORMAT, ]; -if (process.env.NODE_ENV === "development") { +// Validate unique segment IDs in ALL environments (not just development). +// A duplicate ID — e.g. introduced by a bad merge — would otherwise silently +// shadow a segment in production. This runs once at module load; the cost is +// negligible. Throwing fast surfaces the misconfiguration at startup. +{ const ids = WRITER_SEGMENTS.map((s) => s.id); const seen = new Set(); for (const id of ids) {