From e31bd16b15bf90bf591f6c442b0dca8100508d29 Mon Sep 17 00:00:00 2001 From: Kai ki <155355644+zbf1009@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:06:19 +0800 Subject: [PATCH] fix(engine): prevent directScene hang + enforce segment ID uniqueness in prod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two defensive fixes surfaced by the PR #95 review (PR-Agent), applied on top of the staging sync: 1. directScene: routeTaggedStream rejecting BEFORE onPlan fires would leave planPromise unsettled, hanging `await planPromise` — and thus the whole /api/start and /api/scene request — forever. Add a .catch that settles the plan with a minimal fallback and resolves routing to a degraded result, so the pipeline produces a playable fallback scene (graceful degradation) instead of hanging. 2. prompts/registry: the duplicate-segment-ID guard only ran under NODE_ENV=development, so a bad merge introducing a duplicate ID would silently shadow a segment in production. Run the check in all environments (once at module load; negligible cost). --- lib/engine/director.ts | 20 ++++++++++++++++++++ lib/engine/prompts/registry.ts | 6 +++++- 2 files changed, 25 insertions(+), 1 deletion(-) 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) {