fix(engine): prevent directScene hang + enforce segment ID uniqueness in prod

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).
This commit is contained in:
Kai ki
2026-06-23 19:06:19 +08:00
parent ae4d9f8873
commit e31bd16b15
2 changed files with 25 additions and 1 deletions
+20
View File
@@ -13,6 +13,7 @@ import type {
Session, Session,
StoryState, StoryState,
StoryStatePatch, StoryStatePatch,
StreamRouterResult,
WriterScenePlan, WriterScenePlan,
} from "@infiplot/types"; } from "@infiplot/types";
import type { CharacterCard } from "./agents/characterDesigner"; import type { CharacterCard } from "./agents/characterDesigner";
@@ -259,6 +260,25 @@ export async function directScene(
resolvePlan(extracted); resolvePlan(extracted);
} }
return result; 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 </plan> close — EARLY) ──────── // ── Step 2 — await plan (settles at </plan> close — EARLY) ────────
+5 -1
View File
@@ -27,7 +27,11 @@ export const WRITER_SEGMENTS: PromptSegment[] = [
WRITER_FORMAT, 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 ids = WRITER_SEGMENTS.map((s) => s.id);
const seen = new Set<string>(); const seen = new Set<string>();
for (const id of ids) { for (const id of ids) {