@mulmoclaude/mulmoscript-plugin 4.5.0 → 4.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/ops.d.ts.map +1 -1
- package/dist/server/types.d.ts +11 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server.cjs +56 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +56 -2
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":[],"sources":["../src/server/support.ts","../src/server/mulmoErrorCapture.ts","../src/server/ops.ts","../src/server/dispatch.ts"],"sourcesContent":["// Small server-side utilities. The realpath-based traversal check the ops\n// depend on used to live here as a faithful copy of the host's — it is now\n// imported from `@mulmoclaude/core/files` (#2461) so the security-critical\n// primitive cannot drift per host.\n\nimport { readFile } from \"node:fs/promises\";\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function stripDataUri(dataUri: string): string {\n return dataUri.replace(/^data:image\\/[^;]+;base64,/, \"\");\n}\n\n// Async so reading a large generated image/audio file doesn't stall the\n// host's event loop (CodeRabbit on #2137).\nexport async function fileToDataUri(filePath: string, mimeType: string): Promise<string> {\n const data = await readFile(filePath);\n return `data:${mimeType};base64,${data.toString(\"base64\")}`;\n}\n","// Surfaces the underlying provider error that mulmocast swallows when a\n// generation fails. mulmocast catches the real error (missing API key,\n// quota, moderation, …), logs it via GraphAILogger.error, and rethrows a\n// generic wrapper like \"generateReferenceImage: generate error: key=x\" —\n// and `setGraphAILogger(false)` (called per request in buildContext to\n// silence GraphAI's chatty info/debug output) turns off even the error\n// level, so the true cause used to vanish entirely.\n//\n// Moved verbatim from MulmoClaude's server/utils/mulmoErrorCapture.ts in\n// phase 3 (only mulmoScript code ever used it). Hosts must resolve ONE\n// hoisted `graphai` copy shared with their `mulmocast` — GraphAILogger\n// state is module-local, and a second copy would break this capture\n// silently. That's why `graphai` is a peer dependency.\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport { GraphAILogger } from \"graphai\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { isRecord } from \"./support\";\nimport type { MulmoScriptServerLog } from \"./types\";\n\nconst capturedErrors = new AsyncLocalStorage<string[]>();\nlet loggerInstalled = false;\nlet captureLog: MulmoScriptServerLog | null = null;\n\n/** Route captured GraphAI errors into the host logger. Set once by\n * `createMulmoScriptServerOps`; the GraphAILogger sink is global, so the\n * last-configured host logger wins (one ops instance per process). */\nexport function setMulmoErrorCaptureLogger(log: MulmoScriptServerLog | null): void {\n captureLog = log;\n}\n\nfunction formatLogArg(arg: unknown): string {\n if (typeof arg === \"string\") return arg;\n if (arg instanceof Error) return arg.message;\n try {\n return JSON.stringify(arg);\n } catch {\n return String(arg);\n }\n}\n\n/**\n * Re-enable GraphAI's error level (everything else stays silenced) and\n * route it into the host logger + the per-operation capture store.\n * Call after every `setGraphAILogger(false)` — that helper disables all\n * levels including error. Idempotent.\n */\nexport function enableGraphAIErrorCapture(): void {\n GraphAILogger.setLevelEnabled(\"error\", true);\n if (loggerInstalled) return;\n loggerInstalled = true;\n GraphAILogger.setLogger((level, ...args) => {\n if (level !== \"error\") return;\n const message = args.map(formatLogArg).join(\" \");\n captureLog?.warn(\"mulmocast generation error\", { message });\n capturedErrors.getStore()?.push(message);\n });\n}\n\n// Structured-`cause` fields mulmocast attaches for i18n notifications\n// (mulmocast lib/utils/error_cause.js) — agent + error type identify\n// which provider failed; envVarName names a missing API key outright.\nconst CAUSE_FIELDS = [\"type\", \"agentName\", \"envVarName\", \"errorCode\", \"errorType\"] as const;\n\n/** Render mulmocast's structured error `cause` as \"field=value\" pairs. */\nexport function describeMulmoCause(err: unknown): string | null {\n if (!(err instanceof Error) || !isRecord(err.cause)) return null;\n const { cause } = err;\n const parts = CAUSE_FIELDS.flatMap((field) => {\n const value = cause[field];\n return typeof value === \"string\" && value !== \"\" ? [`${field}=${value}`] : [];\n });\n return parts.length > 0 ? parts.join(\" \") : null;\n}\n\n/**\n * Compose the enriched message for a failed mulmocast operation:\n * mulmocast's own message, then its structured cause, then the\n * captured underlying provider error(s). Deduped — GraphAI retries\n * log the same error more than once.\n */\nexport function composeMulmoErrorMessage(err: unknown, captured: readonly string[]): string {\n const base = errorMessage(err);\n const details = [...new Set(captured)].filter((message) => message !== \"\" && message !== base);\n return [base, describeMulmoCause(err), ...details].filter(Boolean).join(\" — \");\n}\n\n/**\n * Run a mulmocast operation, capturing GraphAI error logs emitted while\n * it executes. On failure, rethrows with the captured provider error(s)\n * appended to the message (original error kept as `cause`). Uses\n * AsyncLocalStorage so concurrent operations don't cross-attribute.\n */\nexport async function withMulmoErrorCapture<T>(operation: () => Promise<T>): Promise<T> {\n return capturedErrors.run([], async () => {\n try {\n return await operation();\n } catch (err) {\n throw new Error(composeMulmoErrorMessage(err, capturedErrors.getStore() ?? []), { cause: err });\n }\n });\n}\n","// Transport-free cores for every mulmoScript operation, moved from\n// MulmoClaude's `server/api/routes/mulmo-script-ops.ts` in phase 3 so the\n// SAME implementation backs every host surface:\n//\n// - MulmoClaude's legacy REST routes (kept for wire compat),\n// - the generic plugin dispatch (see `./dispatch`) that the package View\n// calls in both MulmoClaude and MulmoTerminal.\n//\n// Every op returns an `OpResult` — failures are data (`code` preserves the\n// HTTP mapping for REST adapters) and never exceptions. Generation ops\n// publish start/finish through the instance's edge-triggered tracker, which\n// fans out via the injected `backend.onGenerationEvent` (session channels,\n// UI pubsub — host-specific) and backs the View's mount-time\n// `pendingGenerations` snapshot.\n//\n// Host-specific transport is injected via `MulmoScriptServerBackend`; the\n// mulmocast orchestration, realpath containment, and generation-state\n// tracking all live here.\n\nimport { existsSync, mkdirSync, realpathSync, statSync, unlinkSync } from \"fs\";\nimport path from \"path\";\nimport type { FileOps } from \"gui-chat-protocol\";\nimport {\n getFileObject,\n initializeContextFromFiles,\n generateBeatImage,\n getBeatPngImagePath,\n generateBeatAudio,\n getBeatAudioPathOrUrl,\n getBeatAnimatedVideoPath,\n getBeatMoviePaths,\n generateReferenceImage,\n getReferenceImagePath,\n images,\n audio,\n movie,\n movieFilePath,\n pdf,\n pdfFilePath,\n setGraphAILogger,\n addSessionProgressCallback,\n removeSessionProgressCallback,\n} from \"mulmocast\";\nimport type { MulmoBeat, MulmoImagePromptMedia, MulmoStudioContext } from \"@mulmocast/types\";\nimport { DEFAULT_ROOT, normalizeRoot } from \"../core/contract\";\nimport type { MulmoScriptGenerationEvent } from \"../core/contract\";\nimport { normalizeStoryPath, storyRefWithin, storiesRelativePath } from \"../core/paths\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { resolveWithinRoot } from \"@mulmoclaude/core/files\";\nimport { fileToDataUri, stripDataUri } from \"./support\";\nimport { enableGraphAIErrorCapture, setMulmoErrorCaptureLogger, withMulmoErrorCapture } from \"./mulmoErrorCapture\";\nimport type {\n GenerateOpArgsWith,\n MovieGenerationResult,\n MovieProgressEvent,\n MulmoScriptServerBackend,\n MulmoScriptServerLog,\n OpFailure,\n OpResult,\n PdfGenerationResult,\n} from \"./types\";\n\ntype GenerationKind = MulmoScriptGenerationEvent[\"kind\"];\n\n// We pin pdfMode=\"slide\" + pdfSize=\"a4\" — that's the configured default\n// for the storyboard editor; mulmocast's other modes (talk / handout /\n// letter) stay reachable via the CLI for power users. (#1614)\nexport const PDF_MODE = \"slide\" as const;\nexport const PDF_SIZE = \"a4\" as const;\n\nfunction opBadRequest(error: string): OpFailure {\n return { ok: false, code: \"bad_request\", error };\n}\n\nfunction opNotFound(error: string): OpFailure {\n return { ok: false, code: \"not_found\", error };\n}\n\nfunction opServerError(error: string): OpFailure {\n return { ok: false, code: \"server_error\", error };\n}\n\nconst NOOP_LOG: MulmoScriptServerLog = { info: () => {}, warn: () => {}, error: () => {} };\n\n// Helper: build mulmo context for a story file. The explicit return\n// annotation keeps declaration emit portable — the inferred type would\n// reference mulmocast's internal usage-collector path.\nexport async function buildContext(absoluteFilePath: string, force = false): Promise<MulmoStudioContext | null | undefined> {\n // setGraphAILogger(false) silences GraphAI's chatty info/debug output\n // but also its error level — re-enable error capture so a failed\n // generation surfaces the real provider error, not just mulmocast's\n // generic \"generate error\" wrapper.\n setGraphAILogger(false);\n enableGraphAIErrorCapture();\n const files = getFileObject({\n file: absoluteFilePath,\n basedir: path.dirname(absoluteFilePath),\n grouped: true,\n });\n return initializeContextFromFiles(files, true, force);\n}\n\n// Awaited context type used by every op that calls buildContext.\nexport type StoryContext = NonNullable<Awaited<ReturnType<typeof buildContext>>>;\n\nexport interface RunStoryOpDeps {\n resolveStory?: (filePath: string, root?: string) => { ok: true; absolutePath: string } | OpFailure;\n buildContext?: (absoluteFilePath: string, force?: boolean) => Promise<StoryContext | undefined>;\n}\n\nexport interface RunStoryOpOptions<T> {\n force?: boolean | undefined;\n /** Which registered stories root `filePath` is relative to (#3014).\n * Absent = the host's default root, i.e. exactly the pre-roots path. */\n root?: string | undefined;\n /**\n * Op-specific tag included in the failure log so dashboards can\n * distinguish which op is failing (e.g. `\"generate-beat-audio\"`).\n * Falls back to a generic `\"op failed\"` entry when omitted.\n */\n operation?: string;\n /**\n * Soft-fail override for `buildContext` returning undefined. Some\n * ops (e.g. `beatAudio`) historically returned a 200 `{ audio: null }`\n * in that case so the frontend can silently retry. If provided, this\n * callback returns the fallback result instead of the default\n * server_error \"Failed to initialize mulmo context\".\n */\n onContextMissing?: () => OpResult<T>;\n}\n\n// Map each beat to its array index, keyed by beat.id (falling back to\n// a synthetic `__index__<n>` for id-less beats). Shared by the movie\n// and PDF pipelines to translate mulmocast's per-beat progress events\n// (which carry the beat id) back into an index the UI can address.\nexport function buildBeatIdIndex(beats: MulmoBeat[]): Map<string, number> {\n const idToIndex = new Map<string, number>();\n beats.forEach((beat, index) => {\n const key = beat.id ?? `__index__${index}`;\n idToIndex.set(key, index);\n });\n return idToIndex;\n}\n\n// Run `body` with a mulmocast per-beat progress callback registered.\n// `onBeat` receives each beat event's sessionType + resolved index; the\n// caller decides which sessionTypes to forward. The callback is always\n// unregistered, even when `body` throws.\n//\n// Known limitation: addSessionProgressCallback is global, so when two\n// generations for *different* scripts run concurrently, both closures\n// are invoked for every beat event and rely on idToIndex to filter out\n// the other run's events. That filter is reliable only when each beat\n// carries an explicit `id`. Beats without one fall back to\n// \"__index__${index}\", and identical fallback ids across scripts collide\n// → progress meant for script A surfaces on script B. Fixing this\n// properly needs mulmocast to attach a per-run identifier to its\n// progress events (or a global serialization gate); tracked separately.\nasync function withBeatProgress<T>(beats: MulmoBeat[], onBeat: (sessionType: string, beatIndex: number) => void, body: () => Promise<T>): Promise<T> {\n const idToIndex = buildBeatIdIndex(beats);\n const onProgress = (event: { kind: string; sessionType: string; id?: string; inSession: boolean }) => {\n if (event.kind !== \"beat\" || event.inSession || event.id === undefined) return;\n const beatIndex = idToIndex.get(event.id);\n if (beatIndex === undefined) return;\n onBeat(event.sessionType, beatIndex);\n };\n addSessionProgressCallback(onProgress);\n try {\n return await body();\n } finally {\n removeSessionProgressCallback(onProgress);\n }\n}\n\n/** Map identity for the in-flight tracker. JSON array keeps the three\n * fields unambiguous (a human-visible delimiter could collide). */\nfunction generationMapKey(kind: GenerationKind, filePath: string, key: string, root?: string): string {\n // Normalised HERE rather than by the caller. A start keyed `\" repoA \"` and a\n // finish keyed `\"repoA\"` never match, and the tracker entry then leaks for\n // the life of the process — so the one place that builds the key is the one\n // place that must not be able to get the spelling wrong (Codex on #3015).\n const normalized = normalizeRoot(root);\n // `root` is the LAST element so a call site that omits it produces exactly\n // the pre-#3014 key — the default root's entries keep their identity across\n // an upgrade, and a running generation is not orphaned by one.\n return normalized === DEFAULT_ROOT ? JSON.stringify([kind, filePath, key]) : JSON.stringify([kind, filePath, key, normalized]);\n}\n\n/**\n * Build the per-host mulmoScript server ops instance. One instance per\n * process — it owns the in-flight movie/PDF dedup sets and the\n * generation-state tracker, and binds the injected host backend.\n */\nexport function createMulmoScriptServerOps(backend: MulmoScriptServerBackend) {\n const log = backend.log ?? NOOP_LOG;\n setMulmoErrorCaptureLogger(log);\n // Root registry: the default (pre-#3014, wire `root` absent) plus whatever\n // the host registered. Resolved once — the host owns the ids, this package\n // only ever looks them up.\n const rootDirs = new Map<string, string>([[DEFAULT_ROOT, path.resolve(backend.storiesDir)]]);\n for (const [id, dir] of Object.entries(backend.extraRoots ?? {})) {\n // An empty id is the default root's own key: accepting it would re-point\n // every pre-roots caller at someone else's directory. Dropping it quietly\n // would hide the host's misconfiguration until a read returned the wrong\n // file, and this runs at boot, where throwing is the cheap failure.\n // Trim on registration too: a lookup normalizes, so an untrimmed key would\n // be unreachable.\n const trimmed = id.trim();\n if (trimmed === DEFAULT_ROOT) {\n throw new Error(\"mulmoScript: extraRoots key must not be empty — the empty id is reserved for the default stories root\");\n }\n // Two ids that trim to one key: `set` would silently keep the LAST\n // directory, so every read for `repoA` would answer from a directory the\n // card never named, with the host given no signal (CodeRabbit on #3015).\n // Same reasoning as the empty id above — a misconfiguration is cheapest to\n // fail on at boot, and silently resolving the wrong directory is the\n // failure this package refuses everywhere else.\n if (rootDirs.has(trimmed)) {\n throw new Error(`mulmoScript: extraRoots keys must be distinct after trimming — \"${trimmed}\" is registered twice`);\n }\n rootDirs.set(trimmed, path.resolve(dir));\n }\n if (rootDirs.size > 1) {\n // The pair identity is complete INSIDE this package, and stops at the host\n // boundary. A host's per-session generation store keys on\n // `(kind, filePath, key)` — `generationKey` in `@mulmobridge/protocol`,\n // which bridges also consume — so two roots running the same generation in\n // one session collapse to one entry and either finish clears the other's\n // pending state (Codex P1 on #3015).\n //\n // Widening that key is a protocol change with its own consumers, so it is\n // not this package's to make. Unreachable until a host registers a root,\n // which is exactly the moment this fires: whoever first does it is the\n // person who has to widen their session key, and they see this at boot\n // rather than discovering it from a stuck spinner.\n log.warn(\n backend.rootScopedGenerationState === true\n ? \"extra stories roots registered — reads, uploads and generation work; save/update still land in the DEFAULT root until this host passes `artifactsFor` (#3019)\"\n : \"extra stories roots registered — reads and uploads work, but GENERATION is refused until this host declares `rootScopedGenerationState` (its pending-generation state must carry the root, or it must keep none), and save/update land in the DEFAULT root until it passes `artifactsFor` (#3019)\",\n { roots: [...rootDirs.keys()].filter((id) => id !== DEFAULT_ROOT) },\n );\n }\n\n /** The registered directory for a wire `root`, or null when the host never\n * registered it. Null is a REJECTION, not a fallback to the default: an\n * unknown root must not quietly read the workspace's file of the same\n * name. */\n function rootDir(root: string | undefined): string | null {\n return rootDirs.get(normalizeRoot(root)) ?? null;\n }\n\n // ── Story path infrastructure ─────────────────────────────────\n\n // The download / status ops expect \"stories/<rel>\" (historical\n // convention, independent of the on-disk location) — the wire format\n // every endpoint keys on. Relativize against the REALPATH root when it\n // resolves: with a symlinked stories dir, mulmocast returns output\n // paths under the link's target, and relativizing against the link\n // itself would produce a traversal-like \"stories/../../…\" ref that\n // resolveStory then rejects (CodeRabbit on #2137).\n function toStoryRef(absolutePath: string, root?: string): string | null {\n // An unregistered root gets null, not the default root's base. Relativizing\n // against the default would mint a wire ref that READS BACK as a different\n // file — the same silent-substitution failure `resolveStory` rejects, and\n // this function is on the ops object, so a host can reach it directly\n // without passing through that guard (#3015 review F2).\n const dir = rootDir(root);\n if (dir === null) return null;\n const base = ensureStoriesReal(root) ?? dir;\n // The relativizing rule is a pure function in `core/paths.ts` so it can be\n // driven with `path.win32` from a POSIX machine — the case it guards\n // (no relative route across drives) is unreachable here.\n return storyRefWithin(base, absolutePath, path);\n }\n\n // Lazily realpath the stories dir on first use. We can't realpath at\n // instance creation because the directory may not exist yet (it's\n // created on demand by the save route). The cache is invalidated\n // never — once the dir exists, its realpath is stable.\n //\n // Keyed by the resolved DIRECTORY rather than the root id, so there is no\n // normalisation left to forget: `rootDir` already collapses every spelling of\n // a root to one absolute path, and two ids pointing at the same directory\n // share one entry instead of allocating a second. Keying on the raw id let\n // `\" repoA \"` and `\"repoA\"` both resolve and then cache separately, in a map\n // that is never evicted (Codex P2 on #3015).\n const storiesRealCache = new Map<string, string>();\n function ensureStoriesReal(root?: string): string | null {\n const dir = rootDir(root);\n if (dir === null) return null;\n const cached = storiesRealCache.get(dir);\n if (cached) return cached;\n try {\n // Only the DEFAULT root is created on demand. An extra root is a\n // directory the user already owns — often a git worktree — and creating\n // it here would grow `artifacts/stories/` inside their repository as a\n // side effect of a status poll. A host that registers a root is\n // responsible for it existing (#3015 review F3).\n if (normalizeRoot(root) === DEFAULT_ROOT) mkdirSync(dir, { recursive: true });\n const real = realpathSync(dir);\n storiesRealCache.set(dir, real);\n return real;\n } catch {\n return null;\n }\n }\n\n /**\n * Resolve and validate a stories wire path to its absolute realpath.\n *\n * Uses the realpath-based resolveWithinRoot helper to defeat\n * symlink-based escapes. Callers pass wire paths like\n * \"stories/foo.json\" or \"stories/__movies__/bar.mp4\". We strip the\n * leading \"stories/\" segment and resolve the remainder against the\n * realpath of the stories directory itself — this works whether\n * stories/ is a regular directory or a legitimate symlink to another\n * location. ENOENT and traversal are distinguished (404 vs 400).\n */\n function resolveStory(filePath: string, root?: string): { ok: true; absolutePath: string } | OpFailure {\n // An unregistered root is a bad request, not a fall back to the default:\n // resolving it against the workspace would hand the caller a DIFFERENT\n // file that happens to share the name.\n if (rootDir(root) === null) {\n return opBadRequest(\"Unknown stories root\");\n }\n const storiesReal = ensureStoriesReal(root);\n if (!storiesReal) {\n return opServerError(\"stories directory not available\");\n }\n // Reject absolute paths and parent traversal at the syntactic\n // level — defense in depth on top of the realpath check below.\n if (path.isAbsolute(filePath)) {\n return opBadRequest(\"Invalid filePath\");\n }\n // Accept the workspace-relative spelling \"artifacts/stories/<rel>\"\n // the tool description historically taught (the wire form was truly\n // workspace-relative before the stories dir moved under artifacts/\n // in #284) by reducing it to the canonical \"stories/<rel>\".\n const ARTIFACTS_STORIES = \"artifacts/stories\";\n const wirePath = filePath === ARTIFACTS_STORIES || filePath.startsWith(`${ARTIFACTS_STORIES}/`) ? filePath.slice(\"artifacts/\".length) : filePath;\n // Strip the optional \"stories/\" prefix so the remainder is a path\n // relative to storiesReal. Accepts both \"stories/foo.json\" (the\n // canonical caller convention) and bare \"foo.json\".\n const STORIES_PREFIX = `stories${path.sep}`;\n const relFromStories =\n wirePath === \"stories\" ? \"\" : wirePath.startsWith(STORIES_PREFIX) || wirePath.startsWith(\"stories/\") ? wirePath.slice(\"stories/\".length) : wirePath;\n // A base path with no remainder (\"stories\", \"artifacts/stories\",\n // trailing-slash variants) would resolve to the stories directory\n // itself and hand downstream ops a directory where they expect a\n // file — reject it, mirroring normalizeStoryPath's non-empty rule.\n if (relFromStories === \"\") {\n return opBadRequest(\"Invalid filePath\");\n }\n // resolveWithinRoot enforces both the realpath boundary AND\n // existence; ENOENT and traversal both produce null. Distinguish\n // them via a follow-up existsSync so 404 vs 400 stays accurate —\n // but only consult the filesystem for lexically in-root candidates:\n // a traversal path must never touch the fs (and gets a uniform\n // bad_request so responses don't leak existence outside the root).\n const resolved = resolveWithinRoot(storiesReal, relFromStories);\n if (!resolved) {\n const candidate = path.resolve(storiesReal, relFromStories);\n const inRoot = candidate === storiesReal || candidate.startsWith(storiesReal + path.sep);\n if (inRoot && !existsSync(candidate)) {\n return opNotFound(`File not found: ${filePath}`);\n }\n return opBadRequest(\"Invalid filePath\");\n }\n return { ok: true, absolutePath: resolved };\n }\n\n /**\n * Whether this root is one the host registered.\n *\n * Every other root-aware op learns this from `resolveStory`, which needs a\n * file. `pendingGenerations` needs none — it only filters an in-memory map —\n * so an unregistered root produced `{ ok: true, pending: [] }`, and a host\n * typo or a stale card read back as \"no work is running\" (Codex P2 on\n * #3015). An unknown root is a question this package cannot answer, and the\n * answer it must not give is a confident empty one.\n */\n function guardStoryRootRegistered(root: string | undefined): OpFailure | null {\n return rootDir(root) === null ? opBadRequest(`unknown stories root \"${normalizeRoot(root)}\"`) : null;\n }\n\n /**\n * Realpath containment pre-guard for wire paths handed to the phase-1\n * core's save/reopen/update executes. The core's own path guard is\n * lexical (it runs against the generic FileOps, whose read/write follows\n * symlinks), so hosts re-assert the realpath boundary here before\n * invoking it — a symlink planted below the stories dir can't read or\n * write outside the tree (Codex P1 on MulmoClaude#2133).\n *\n * Returns null when `filePath` isn't a non-empty string — shape\n * validation (including the script-vs-filePath mode check) belongs to\n * the core.\n */\n function guardStoryWirePath(filePath: unknown, root?: string): OpFailure | null {\n if (typeof filePath !== \"string\" || filePath === \"\") return null;\n const resolved = resolveStory(filePath, root);\n return resolved.ok ? null : resolved;\n }\n\n /**\n * Whether a GENERATION may run in this root.\n *\n * The pair identity is complete inside this package but stops at the host\n * boundary: a host's per-session store keys pending work by\n * `(kind, filePath, key)` — `generationKey` in `@mulmobridge/protocol`,\n * which bridges also consume. Two roots running the same generation in one\n * session would collapse to one entry, and either completion would clear the\n * other root's indicator (Codex P1 on #3015).\n *\n * Whose hazard it is decides who answers: a host declares\n * `rootScopedGenerationState` when its own pending state carries the root —\n * or when it keeps none, which is MulmoTerminal's case. It was refused for a\n * collision it cannot have (#3019).\n *\n * Absent still refuses, so a host that has not thought about this keeps the\n * shipped behaviour rather than being quietly opened up.\n */\n function guardStoryGenerationRoot(root: string | undefined): OpFailure | null {\n if (normalizeRoot(root) === DEFAULT_ROOT) return null;\n // Registration BEFORE the host's opt-in. The flag says \"this host can tell\n // two roots apart\", not \"any id is addressable\" — and the generation ops\n // publish their start event before `runStoryOp` reaches `resolveStory`, so\n // an unregistered root emitted a start/finish pair for work that never\n // existed (Codex + CodeRabbit on #3020).\n const registered = guardStoryRootRegistered(root);\n if (registered) return registered;\n if (backend.rootScopedGenerationState === true) return null;\n return opBadRequest(\"generating in a non-default stories root is not supported yet\");\n }\n\n /**\n * The write half of the same fail-closed rule.\n *\n * It lives in the ops that write rather than at their dispatch sites,\n * because a per-site guard is a list someone has to remember to extend:\n * `save` / `updateBeat` / `updateScript` were guarded and the two upload\n * kinds were not, so an image could still land in a named root\n * (CodeRabbit on #3015). `save` / `update* ` cannot follow suit — they run\n * through the package executors, not through these ops — so\n * `test_server_roots.ts` walks the whole ops surface and fails on any op\n * that is neither in the read-only allowlist nor refusing.\n */\n /**\n * Whether a WRITE may target this root.\n *\n * Reads are root-aware; writes are not. `executeMulmoScriptSave` and the\n * update executors run against one `FileOps`, bound by the host to the\n * default root, so a write naming another root would rewrite the DEFAULT\n * root's identically-named file and then announce the other one as changed\n * (#3015 review G1). Closing it was fail-closed: \"readable but not yet\n * writable\" beats \"wrote somewhere else and said so\".\n *\n * It opens per root, not globally: the host answers `artifactsFor` for the\n * roots it can serve, and a root it cannot is still refused. A host that\n * wires nothing keeps the shipped refusal (#3019).\n */\n function guardStoryWriteRoot(root: string | undefined): OpFailure | null {\n if (normalizeRoot(root) === DEFAULT_ROOT) return null;\n const registered = guardStoryRootRegistered(root);\n if (registered) return registered;\n return artifactsForRoot(root) === null ? opBadRequest(\"writing to a non-default stories root is not supported yet\") : null;\n }\n\n /**\n * The FileOps a write to this root must go through.\n *\n * The default root keeps the single `artifacts` it always had, byte for\n * byte. A named root is served only when the host both REGISTERED it\n * (`extraRoots` — the containment boundary, so a host cannot widen the\n * addressable set through this back door) and can supply a FileOps for it.\n */\n function artifactsForRoot(root: string | undefined): FileOps | null {\n const normalized = normalizeRoot(root);\n if (normalized === DEFAULT_ROOT) return backend.artifacts;\n if (rootDir(normalized) === null) return null;\n const hostOps = backend.artifactsFor?.(normalized);\n return hostOps === undefined || hostOps === null ? null : storiesScoped(hostOps);\n }\n\n /**\n * Address a named root's FileOps the way the READ side addresses it.\n *\n * The two sides disagreed about one path segment, and the disagreement was\n * silent. A read strips `stories/` and resolves under the registered\n * directory (`<root>/<rel>`); a write handed the executors' wire path\n * straight to the host's FileOps, which is rooted at that same registered\n * directory, so the bytes landed in `<root>/stories/<rel>`. `save` then\n * REPORTED SUCCESS and returned a wire path that could not be read back —\n * a card pointing at nothing (#3020 review H1, from the consuming host).\n *\n * Stripping here makes \"the directory you registered is the directory your\n * FileOps is rooted at\" true, which is what a host writes without being\n * told. The default root is untouched: its FileOps is rooted one level up,\n * at `<workspace>/artifacts`, and is shared with other plugins.\n *\n * A path that is not a stories wire path throws rather than passing\n * through. Everything reaching here has been through `normalizeStoryPath`\n * or `storyFilePath`, so one that has not is a bug — and letting it through\n * would write it somewhere nobody can read, which is the failure this whole\n * wrapper exists to end.\n */\n function storiesScoped(inner: FileOps): FileOps {\n const within = (wirePath: string): string => {\n const relative = storiesRelativePath(wirePath);\n if (relative === null) throw new Error(`mulmoScript: \"${wirePath}\" is not a stories path — a named root's FileOps takes stories paths only`);\n return relative;\n };\n // Every member is `async` so the refusal arrives as a REJECTED PROMISE,\n // the way every other FileOps failure does. Throwing synchronously out of\n // a method the caller only ever awaits would skip its `try`.\n return {\n read: async (wirePath) => inner.read(within(wirePath)),\n readBytes: async (wirePath) => inner.readBytes(within(wirePath)),\n write: async (wirePath, data) => inner.write(within(wirePath), data),\n readDir: async (wirePath) => inner.readDir(within(wirePath)),\n stat: async (wirePath) => inner.stat(within(wirePath)),\n exists: async (wirePath) => inner.exists(within(wirePath)),\n unlink: async (wirePath) => inner.unlink(within(wirePath)),\n };\n }\n\n // mulmocast shells out to ffmpeg for movie / beat rendering. When the\n // host's probe reports it absent, intercept with a clear failure\n // instead of letting the library throw an opaque spawn ENOENT\n // mid-pipeline. `undefined` means the probe hasn't completed — assume\n // available so a brief startup window never blocks a render.\n function ffmpegGuard(): OpFailure | null {\n if (backend.isFfmpegAvailable?.() === false) {\n return {\n ok: false,\n code: \"unavailable\",\n error: \"ffmpeg is not installed — movie and beat rendering are unavailable. Install ffmpeg and restart the server.\",\n };\n }\n return null;\n }\n\n // ── Generation tracker (edge-triggered) ───────────────────────\n\n // Refcounted: two concurrent generations with the same kind/filePath/key\n // (e.g. the same beat rendered from two tabs) must not have the first\n // completion erase the second run's snapshot entry, and only the first\n // start / LAST finish reach the host channels — an early completion\n // can't clear subscribers' spinners while a duplicate run is active.\n // A finish with no tracked start (the movie/PDF pipelines' per-beat\n // completion pulses) always publishes.\n const inFlightGenerations = new Map<string, { kind: GenerationKind; filePath: string; key: string; root: string; count: number }>();\n\n /** Emit `root` only when it names a non-default one: an event carrying\n * `root: \"\"` and one carrying nothing must stay indistinguishable to every\n * pre-#3014 consumer. */\n function rootField(root: string | undefined): { root?: string } {\n const normalized = normalizeRoot(root);\n return normalized === DEFAULT_ROOT ? {} : { root: normalized };\n }\n\n /** Tracker state and events key on the canonical `stories/<rel>` wire\n * form: subscribers (the View's pubsub filter, `pendingGenerations`\n * callers) match by exact string, so the accepted alias spellings\n * (`artifacts/stories/<rel>`, bare `<rel>`) must collapse to the same\n * key as the canonical one (Codex P2 on #2139). Untrusted spellings\n * pass through unchanged — they never resolve, so they can't collide. */\n function canonicalWirePath(filePath: string): string {\n return normalizeStoryPath(filePath) ?? filePath;\n }\n\n /**\n * `error` and `root` travel in an options object, NOT as two trailing\n * optional positionals.\n *\n * They were positional once, and appending `root` to the sixteen call sites\n * put it in `error`'s slot at the nine that pass no error: every start event\n * carried `error: \"<root>\"`, the root was dropped from the key, the tracker\n * entry was filed under the default root — and because the matching finish\n * DID pass both, its key differed and the entry was never deleted, leaking\n * one row per generation for the life of the process. Two adjacent optional\n * strings cannot be told apart by the type checker, so the shape is the only\n * thing that can prevent it (#3015 review).\n */\n function publishGeneration(\n chatSessionId: string | undefined,\n kind: GenerationKind,\n filePath: string,\n key: string,\n finished: boolean,\n opts: { error?: string | undefined; root?: string | undefined } = {},\n ): void {\n const { error, root } = opts;\n const wirePath = canonicalWirePath(filePath);\n // The NORMALIZED root, not the raw spelling. The tracker value and the\n // emitted event both normalize, so keying on the raw text made a start\n // written `\" repoA \"` and its finish written `\"repoA\"` two entries: the\n // finish deleted nothing and the start leaked (Codex P2 on #3015).\n const mapKey = generationMapKey(kind, wirePath, key, root);\n const existing = inFlightGenerations.get(mapKey);\n if (finished) {\n if (existing && existing.count > 1) {\n existing.count -= 1;\n return; // a duplicate run is still active — suppress the early finish\n }\n inFlightGenerations.delete(mapKey);\n } else {\n if (existing) {\n existing.count += 1;\n return; // already reported as started\n }\n inFlightGenerations.set(mapKey, { kind, filePath: wirePath, key, root: normalizeRoot(root), count: 1 });\n }\n const event: MulmoScriptGenerationEvent = {\n kind,\n filePath: wirePath,\n key,\n done: finished,\n ...(error ? { error } : {}),\n ...rootField(root),\n };\n backend.onGenerationEvent?.(chatSessionId, event);\n }\n\n /**\n * Tell every open View that this script was written.\n *\n * `origin` is the writer. A View passes its own id so it can ignore the echo of its own\n * save — reloading there would rebuild the element the caret is in, mid-keystroke. A write\n * from the agent carries no origin, so everyone reloads.\n */\n function publishScriptChanged(filePath: string, origin?: string, root?: string): void {\n backend.onScriptChanged?.({\n filePath: canonicalWirePath(filePath),\n ...(origin === undefined ? {} : { origin }),\n ...rootField(root),\n });\n }\n\n /**\n * Snapshot of generations currently in flight for one script — the View's\n * mount-time catch-up.\n *\n * Filtered on the PAIR. Filtering on `filePath` alone hands a View watching\n * `repoB/stories/deck.json` the run started for `repoA`'s identically-named\n * deck, and the caller cannot discard it because the returned event would\n * carry no root either (Codex P1 / CodeRabbit on #3015).\n */\n function pendingGenerations(filePath: string, root?: string): MulmoScriptGenerationEvent[] {\n const wirePath = canonicalWirePath(filePath);\n const wanted = normalizeRoot(root);\n return [...inFlightGenerations.values()]\n .filter((entry) => entry.filePath === wirePath && entry.root === wanted)\n .map(({ kind, key }) => ({ kind, filePath: wirePath, key, done: false, ...rootField(root) }));\n }\n\n // ── Op scaffolding ────────────────────────────────────────────\n\n /**\n * Shared scaffolding for mulmoScript ops. Resolves the wire filePath,\n * builds the mulmo context, and folds unexpected handler errors into a\n * server_error failure (with a warn breadcrumb). Accepts a `deps` param\n * so unit tests can inject fakes without the full mulmocast stack.\n */\n async function runStoryOp<T>(\n filePath: string,\n options: RunStoryOpOptions<T>,\n handler: (ctx: { absoluteFilePath: string; context: StoryContext }) => Promise<OpResult<T>>,\n deps: RunStoryOpDeps = {},\n ): Promise<OpResult<T>> {\n const resolver = deps.resolveStory ?? resolveStory;\n const build = deps.buildContext ?? buildContext;\n const resolved = resolver(filePath, options.root);\n if (!resolved.ok) return resolved;\n try {\n const context = await build(resolved.absolutePath, options.force ?? false);\n if (!context) {\n if (options.onContextMissing) return options.onContextMissing();\n return opServerError(\"Failed to initialize mulmo context\");\n }\n // withMulmoErrorCapture appends the underlying provider error\n // (missing API key, quota, …) to any mulmocast failure, which\n // otherwise reaches the client as a generic \"generate error\".\n return await withMulmoErrorCapture(() => handler({ absoluteFilePath: resolved.absolutePath, context }));\n } catch (err) {\n // Log every op failure at warn so operators get a breadcrumb even\n // when the op doesn't wrap its own try/catch.\n log.warn(\"op failed\", {\n ...(options.operation ? { operation: options.operation } : {}),\n filePath,\n error: errorMessage(err),\n });\n return opServerError(errorMessage(err));\n }\n }\n\n // ── Probe ops ─────────────────────────────────────────────────\n\n async function beatImageOp(filePath: string, beatIndex: number, root?: string): Promise<OpResult<{ image: string | null }>> {\n return runStoryOp<{ image: string | null }>(filePath, { operation: \"beat-image\", root }, async ({ context }) => {\n const { imagePath } = getBeatPngImagePath(context, beatIndex);\n if (!existsSync(imagePath)) return { ok: true, image: null };\n return { ok: true, image: await fileToDataUri(imagePath, \"image/png\") };\n });\n }\n\n // beatAudio is a probe — the frontend polls it expecting `{ audio: null }`\n // when nothing has been generated yet. Override the default\n // server_error-on-context-missing so the soft-fail contract is preserved.\n async function beatAudioOp(filePath: string, beatIndex: number, root?: string): Promise<OpResult<{ audio: string | null }>> {\n return runStoryOp<{ audio: string | null }>(\n filePath,\n { operation: \"beat-audio\", root, onContextMissing: () => ({ ok: true, audio: null }) },\n async ({ context }) => {\n const beat = context.studio.script.beats[beatIndex];\n // Probe contract: a beat index the script doesn't have soft-fails\n // like a beat with nothing generated yet, never a server error.\n if (!beat) return { ok: true, audio: null };\n const audioPath = getBeatAudioPathOrUrl(beat.text ?? \"\", context, beat, context.lang);\n if (!audioPath || !existsSync(audioPath)) return { ok: true, audio: null };\n return { ok: true, audio: await fileToDataUri(audioPath, \"audio/mpeg\") };\n },\n );\n }\n\n // Probe for a beat's generated video clip. Preference order mirrors the\n // movie-assembly pipeline's \"most processed wins\": lip-synced > with\n // sound effect > raw movie clip > animated html_tailwind render. The\n // response is the \"stories/…\" wire path so the client can stream it\n // through the host's authenticated media download.\n async function beatMovieOp(filePath: string, beatIndex: number, root?: string): Promise<OpResult<{ moviePath: string | null }>> {\n return runStoryOp<{ moviePath: string | null }>(filePath, { operation: \"beat-movie\", root }, async ({ context }) => {\n const { movieFile, soundEffectFile, lipSyncFile } = getBeatMoviePaths(context, beatIndex);\n const candidates = [lipSyncFile, soundEffectFile, movieFile, getBeatAnimatedVideoPath(context, beatIndex)];\n const existing = candidates.find((candidate) => existsSync(candidate));\n return { ok: true, moviePath: existing ? toStoryRef(existing, root) : null };\n });\n }\n\n async function characterImageOp(filePath: string, key: string, root?: string): Promise<OpResult<{ image: string | null }>> {\n return runStoryOp<{ image: string | null }>(filePath, { operation: \"character-image\", root }, async ({ context }) => {\n const imagePath = getReferenceImagePath(context, key, \"png\");\n if (!existsSync(imagePath)) return { ok: true, image: null };\n return { ok: true, image: await fileToDataUri(imagePath, \"image/png\") };\n });\n }\n\n /** Shared \"output exists and is newer than the source script\" gate for\n * movie / PDF status. A stale artifact (script edited after it was\n * generated) reports null so the UI re-offers the Generate button. */\n function freshOutputRef(outputPath: string, absoluteFilePath: string, root?: string): string | null {\n if (!existsSync(outputPath)) return null;\n const outputMtime = statSync(outputPath).mtimeMs;\n const sourceMtime = statSync(absoluteFilePath).mtimeMs;\n if (outputMtime < sourceMtime) return null;\n return toStoryRef(outputPath, root);\n }\n\n async function movieStatusOp(filePath: string, root?: string): Promise<OpResult<{ moviePath: string | null }>> {\n return runStoryOp(\n filePath,\n { operation: \"movie-status\", root, onContextMissing: () => ({ ok: true, moviePath: null }) },\n async ({ absoluteFilePath, context }) => ({ ok: true, moviePath: freshOutputRef(movieFilePath(context), absoluteFilePath, root) }),\n );\n }\n\n async function pdfStatusOp(filePath: string, root?: string): Promise<OpResult<{ pdfPath: string | null }>> {\n return runStoryOp(\n filePath,\n { operation: \"pdf-status\", root, onContextMissing: () => ({ ok: true, pdfPath: null }) },\n async ({ absoluteFilePath, context }) => ({\n ok: true,\n pdfPath: freshOutputRef(pdfFilePath(context, PDF_MODE), absoluteFilePath, root),\n }),\n );\n }\n\n // ── Generation ops ────────────────────────────────────────────\n\n async function renderBeatOp(args: GenerateOpArgsWith<\"filePath\" | \"beatIndex\">): Promise<OpResult<{ image: string }>> {\n const { filePath, beatIndex, force, chatSessionId, root } = args;\n const rootGuard = guardStoryGenerationRoot(root);\n if (rootGuard) return rootGuard;\n const ffmpeg = ffmpegGuard();\n if (ffmpeg) return ffmpeg;\n\n const mapKey = String(beatIndex);\n publishGeneration(chatSessionId, \"beatImage\", filePath, mapKey, false, { root });\n let genError: string | undefined;\n try {\n const result = await runStoryOp<{ image: string }>(filePath, { force, operation: \"render-beat\", root }, async ({ context }) => {\n await generateBeatImage({\n index: beatIndex,\n context,\n ...(force ? { args: { forceImage: true } } : {}),\n });\n const { imagePath } = getBeatPngImagePath(context, beatIndex);\n if (!existsSync(imagePath)) {\n return opServerError(\"Image was not generated\");\n }\n return { ok: true, image: await fileToDataUri(imagePath, \"image/png\") };\n });\n if (!result.ok) genError = result.error;\n return result;\n } finally {\n publishGeneration(chatSessionId, \"beatImage\", filePath, mapKey, true, { error: genError, root });\n }\n }\n\n async function generateBeatAudioOp(args: GenerateOpArgsWith<\"filePath\" | \"beatIndex\">): Promise<OpResult<{ audio: string }>> {\n const { filePath, beatIndex, force, chatSessionId, root } = args;\n const rootGuard = guardStoryGenerationRoot(root);\n if (rootGuard) return rootGuard;\n const mapKey = String(beatIndex);\n publishGeneration(chatSessionId, \"beatAudio\", filePath, mapKey, false, { root });\n let genError: string | undefined;\n try {\n const result = await runStoryOp<{ audio: string }>(filePath, { force, operation: \"generate-beat-audio\", root }, async ({ context }) => {\n await generateBeatAudio(beatIndex, context, {\n settings: process.env as Record<string, string>,\n } as Parameters<typeof generateBeatAudio>[2]);\n\n const beat = context.studio.script.beats[beatIndex];\n // The generated file still wins when present, so a beat index the\n // script doesn't have only skips the path-derivation fallback and\n // lands on the \"audio was not generated\" branch below.\n const generatedFile = context.studio.beats[beatIndex]?.audioFile;\n const audioPath = generatedFile ?? (beat ? getBeatAudioPathOrUrl(beat.text ?? \"\", context, beat, context.lang) : undefined);\n\n if (!audioPath || !existsSync(audioPath)) {\n // Logic-flow failure (not an exception) — emit a targeted\n // log. Don't write raw `beat.text` into persistent logs —\n // it's free-form user content and can contain sensitive\n // data.\n log.error(\"audio was not generated\", {\n beatIndex,\n audioPath,\n exists: audioPath ? existsSync(audioPath) : false,\n beatTextLength: typeof beat?.text === \"string\" ? beat.text.length : 0,\n audioFilePresent: Boolean(context.studio.beats[beatIndex]?.audioFile),\n });\n return opServerError(\"Audio was not generated\");\n }\n return { ok: true, audio: await fileToDataUri(audioPath, \"audio/mpeg\") };\n });\n if (!result.ok) genError = result.error;\n return result;\n } finally {\n publishGeneration(chatSessionId, \"beatAudio\", filePath, mapKey, true, { error: genError, root });\n }\n }\n\n async function renderCharacterOp(args: GenerateOpArgsWith<\"filePath\" | \"key\">): Promise<OpResult<{ image: string }>> {\n const { filePath, key, force, chatSessionId, root } = args;\n const rootGuard = guardStoryGenerationRoot(root);\n if (rootGuard) return rootGuard;\n publishGeneration(chatSessionId, \"characterImage\", filePath, key, false, { root });\n let genError: string | undefined;\n try {\n const result = await runStoryOp<{ image: string }>(filePath, { force, operation: \"render-character\", root }, async ({ context }) => {\n // `imageEntries` (not `images`) to avoid shadowing mulmocast's\n // imported `images()` pipeline stage.\n const imageEntries = context.studio.script.imageParams?.images ?? {};\n const imageEntry = imageEntries[key];\n if (!imageEntry || imageEntry.type !== \"imagePrompt\") {\n return opBadRequest(`No imagePrompt entry for key: ${key}`);\n }\n\n const index = Object.keys(imageEntries).indexOf(key);\n const imagePath = getReferenceImagePath(context, key, \"png\");\n mkdirSync(path.dirname(imagePath), { recursive: true });\n\n await generateReferenceImage({\n context,\n key,\n index,\n image: imageEntry as MulmoImagePromptMedia,\n ...(force !== undefined ? { force } : {}),\n });\n if (!existsSync(imagePath)) {\n return opServerError(\"Character image was not generated\");\n }\n return { ok: true, image: await fileToDataUri(imagePath, \"image/png\") };\n });\n if (!result.ok) genError = result.error;\n return result;\n } finally {\n publishGeneration(chatSessionId, \"characterImage\", filePath, key, true, { error: genError, root });\n }\n }\n\n // ── Upload ops ────────────────────────────────────────────────\n\n async function uploadBeatImageOp(filePath: string, beatIndex: number, imageData: string, root?: string): Promise<OpResult<{ image: string }>> {\n // No root guard: the write is already in the right root. `runStoryOp`\n // resolves through `resolveStory` — realpath containment, per root — and\n // hands the executor an ABSOLUTE path, from which `buildContext` derives\n // its `basedir`. The guard here was added defensively during #3015's\n // review and refused a write that was never wrong (#3019).\n return runStoryOp<{ image: string }>(filePath, { operation: \"upload-beat-image\", root }, async ({ context }) => {\n const { imagePath } = getBeatPngImagePath(context, beatIndex);\n // writeFileAtomic creates parent dirs and prevents a half-\n // written PNG from surviving a crash mid-write (#881 v2).\n const base64 = stripDataUri(imageData);\n await backend.writeFileAtomic(imagePath, Buffer.from(base64, \"base64\"));\n return { ok: true, image: await fileToDataUri(imagePath, \"image/png\") };\n });\n }\n\n async function uploadCharacterImageOp(filePath: string, key: string, imageData: string, root?: string): Promise<OpResult<{ image: string }>> {\n // No root guard: the write is already in the right root. `runStoryOp`\n // resolves through `resolveStory` — realpath containment, per root — and\n // hands the executor an ABSOLUTE path, from which `buildContext` derives\n // its `basedir`. The guard here was added defensively during #3015's\n // review and refused a write that was never wrong (#3019).\n return runStoryOp<{ image: string }>(filePath, { operation: \"upload-character-image\", root }, async ({ context }) => {\n const imagePath = getReferenceImagePath(context, key, \"png\");\n const base64 = stripDataUri(imageData);\n await backend.writeFileAtomic(imagePath, Buffer.from(base64, \"base64\"));\n return { ok: true, image: await fileToDataUri(imagePath, \"image/png\") };\n });\n }\n\n // ── Movie / PDF pipelines ─────────────────────────────────────\n\n // Per-instance dedup so a foreground call (SSE route or long-held\n // dispatch) and a fire-and-forget background call can't race on the same\n // script. Keyed by the realpath (absoluteFilePath) so two different wire\n // spellings of the same file still collide. Process-local — a\n // multi-process deployment would need an external lock; out of scope.\n const inFlightMovies = new Set<string>();\n\n // Same dedup model as inFlightMovies, scoped to PDF generation\n // (#1614). PDFs and movies don't share the lock — they write to\n // different output files and can safely run in parallel.\n const inFlightPdfs = new Set<string>();\n\n // Shared core for the SSE-streaming route, the long-held dispatch op, and\n // the fire-and-forget background path triggered by `autoGenerateMovie`.\n // Builds the mulmo context, runs audio→images→movie, and reports\n // per-beat progress through the supplied callback. Throws on\n // unexpected pipeline errors; returns a structured failure when the\n // pipeline runs to completion but the output file is missing.\n async function runMovieGeneration(absoluteFilePath: string, onProgressEvent: (event: MovieProgressEvent) => void): Promise<MovieGenerationResult> {\n return withMulmoErrorCapture(() => runMoviePipeline(absoluteFilePath, onProgressEvent));\n }\n\n async function runMoviePipeline(absoluteFilePath: string, onProgressEvent: (event: MovieProgressEvent) => void): Promise<MovieGenerationResult> {\n const context = await buildContext(absoluteFilePath);\n if (!context) return { ok: false, error: \"Failed to initialize mulmo context\" };\n\n return withBeatProgress(\n context.studio.script.beats as MulmoBeat[],\n (sessionType, beatIndex) => {\n if (sessionType !== \"image\" && sessionType !== \"audio\") return;\n onProgressEvent({ kind: sessionType, beatIndex });\n },\n async () => {\n // Order matters: audio() must run before images(). For html_tailwind\n // beats with `animation: true`, mulmocast only emits the per-beat\n // `_animated.mp4` when the beat's duration is already known (see\n // processHtmlTailwindAnimated in mulmocast). Durations are populated\n // by audio(), so running images() first leaves the .mp4 files\n // missing and movie() then fails in validateBeatSource.\n const audioContext = await audio(context);\n const imagesContext = await images(audioContext);\n await movie(imagesContext);\n\n const outputPath = movieFilePath(imagesContext);\n if (!existsSync(outputPath)) return { ok: false, error: \"Movie was not generated\" };\n return { ok: true, outputPath };\n },\n );\n }\n\n /**\n * Long-held foreground movie generation (the package View's\n * `generateMovie` dispatch). Resolves when the whole pipeline finishes.\n * Per-beat completions are mirrored to the generation channels so the\n * initiating View (and any other mounted View) reloads assets off disk\n * as they land — the successor of the SSE per-beat events.\n */\n async function generateMovieOp(filePath: string, chatSessionId: string | undefined, root?: string): Promise<OpResult<{ moviePath: string }>> {\n const rootGuard = guardStoryGenerationRoot(root);\n if (rootGuard) return rootGuard;\n const ffmpeg = ffmpegGuard();\n if (ffmpeg) return ffmpeg;\n const resolved = resolveStory(filePath, root);\n if (!resolved.ok) return resolved;\n const absoluteFilePath = resolved.absolutePath;\n\n if (inFlightMovies.has(absoluteFilePath)) {\n return opBadRequest(\"Movie generation is already in progress for this script\");\n }\n\n inFlightMovies.add(absoluteFilePath);\n publishGeneration(chatSessionId, \"movie\", filePath, \"\", false, { root });\n let genError: string | undefined;\n try {\n const result = await runMovieGeneration(absoluteFilePath, (event) => {\n const eventKind = event.kind === \"image\" ? \"beatImage\" : \"beatAudio\";\n publishGeneration(chatSessionId, eventKind, filePath, String(event.beatIndex), true, { root });\n });\n if (!result.ok) {\n genError = result.error;\n return opServerError(result.error);\n }\n const movieRef = toStoryRef(result.outputPath, root);\n if (movieRef === null) return opServerError(\"generated movie is outside the registered stories root\");\n return { ok: true, moviePath: movieRef };\n } catch (err) {\n genError = errorMessage(err);\n return opServerError(genError);\n } finally {\n inFlightMovies.delete(absoluteFilePath);\n publishGeneration(chatSessionId, \"movie\", filePath, \"\", true, { error: genError, root });\n }\n }\n\n function triggerAutoBackgroundMovie(absoluteFilePath: string, wireFilePath: string, chatSessionId: string | undefined, root?: string): void {\n // The same refusal as the foreground generation ops, reached the only way\n // a `void` entry point can reach it. This one takes a `root` but never\n // returned an `OpResult`, so the guard that covers every other generation\n // did not cover the one generation nobody is waiting on — which is the\n // worse half: a detached run corrupting the other root's pending state\n // has no caller to see the failure (Codex P1 on #3015).\n if (guardStoryGenerationRoot(root)) {\n log.warn(\"refused an auto background movie in a non-default stories root\", { filePath: wireFilePath, root });\n return;\n }\n if (inFlightMovies.has(absoluteFilePath)) return;\n inFlightMovies.add(absoluteFilePath);\n void runBackgroundMovieGeneration(absoluteFilePath, wireFilePath, chatSessionId, root);\n }\n\n // Detached movie generation. Reports progress through the generation\n // channels the View watches — so a user opening the canvas\n // mid-generation sees spinners, and a user opening it after completion\n // sees the finished movie loaded from disk by the View's normal\n // mount-time path. Errors are persisted to a `<filename>.error.txt`\n // sidecar next to the script (no synchronous client to alert); any\n // stale sidecar from a previous run is cleared on each new attempt.\n // Triggered server-side from the unified save route when the caller\n // passes `autoGenerateMovie: true`.\n async function runBackgroundMovieGeneration(absoluteFilePath: string, wireFilePath: string, chatSessionId: string | undefined, root?: string): Promise<void> {\n const errorSidecarPath = `${absoluteFilePath}.error.txt`;\n // Clear stale error from a previous failed run before starting; if it\n // doesn't exist that's fine. Catch any unexpected fs errors silently —\n // the worst case is the user sees an out-of-date error file later.\n try {\n unlinkSync(errorSidecarPath);\n } catch {\n // intentional: ENOENT is the common case, others non-fatal\n }\n\n publishGeneration(chatSessionId, \"movie\", wireFilePath, \"\", false, { root });\n let genError: string | undefined;\n try {\n const result = await runMovieGeneration(absoluteFilePath, (event) => {\n // Mirror per-beat completions through the generation channels so\n // subscribed Views reload the asset off disk. We fire start+finish\n // in two ticks — `setImmediate` lets the session SSE writer flush\n // the start event before the finish removes the entry, otherwise\n // Vue's batched reactivity could see a net \"no change\" and skip\n // the reload.\n const eventKind = event.kind === \"image\" ? \"beatImage\" : \"beatAudio\";\n const key = String(event.beatIndex);\n publishGeneration(chatSessionId, eventKind, wireFilePath, key, false, { root });\n setImmediate(() => publishGeneration(chatSessionId, eventKind, wireFilePath, key, true, { root }));\n });\n\n if (!result.ok) {\n genError = result.error;\n await writeErrorSidecar(errorSidecarPath, result.error);\n log.warn(\"background movie generation failed\", { filePath: wireFilePath, error: result.error });\n return;\n }\n log.info(\"background movie generation done\", {\n filePath: wireFilePath,\n outputPath: result.outputPath,\n });\n } catch (err) {\n genError = errorMessage(err);\n await writeErrorSidecar(errorSidecarPath, genError);\n log.error(\"background movie generation crashed\", { filePath: wireFilePath, error: genError });\n } finally {\n inFlightMovies.delete(absoluteFilePath);\n publishGeneration(chatSessionId, \"movie\", wireFilePath, \"\", true, { error: genError, root });\n }\n }\n\n // Atomic write so a crash mid-write can't leave a truncated sidecar.\n async function writeErrorSidecar(errorSidecarPath: string, message: string): Promise<void> {\n try {\n await backend.writeFileAtomic(errorSidecarPath, message);\n } catch (writeErr) {\n log.error(\"failed to write error sidecar\", {\n errorSidecarPath,\n error: errorMessage(writeErr),\n });\n }\n }\n\n // ── PDF (#1614) ───────────────────────────────────────────────\n\n // Shared core for the SSE-streaming route and the long-held dispatch op.\n // Mirrors the movie pipeline's per-beat progress reporting so the UI can\n // light spinners during the image pass; the PDF action itself doesn't\n // emit progress events, so only image events are forwarded. Returns a\n // structured failure when the pipeline completes but the output file is\n // missing.\n async function runPdfGeneration(context: StoryContext, onImageBeatDone: (beatIndex: number) => void): Promise<PdfGenerationResult> {\n return withMulmoErrorCapture(() => runPdfPipeline(context, onImageBeatDone));\n }\n\n async function runPdfPipeline(context: StoryContext, onImageBeatDone: (beatIndex: number) => void): Promise<PdfGenerationResult> {\n return withBeatProgress(\n context.studio.script.beats as MulmoBeat[],\n (sessionType, beatIndex) => {\n if (sessionType !== \"image\") return;\n onImageBeatDone(beatIndex);\n },\n async () => {\n const imagesContext = await images(context);\n await pdf(imagesContext, PDF_MODE, PDF_SIZE);\n const outputPath = pdfFilePath(imagesContext, PDF_MODE);\n if (!existsSync(outputPath)) return { ok: false, error: \"PDF was not generated\" };\n return { ok: true, outputPath };\n },\n );\n }\n\n /** Long-held foreground PDF generation (the package View's `generatePdf`\n * dispatch) — the PDF sibling of `generateMovieOp`. */\n async function generatePdfOp(filePath: string, chatSessionId: string | undefined, root?: string): Promise<OpResult<{ pdfPath: string }>> {\n const rootGuard = guardStoryGenerationRoot(root);\n if (rootGuard) return rootGuard;\n const ffmpeg = ffmpegGuard();\n if (ffmpeg) return ffmpeg;\n const resolved = resolveStory(filePath, root);\n if (!resolved.ok) return resolved;\n const absoluteFilePath = resolved.absolutePath;\n\n if (inFlightPdfs.has(absoluteFilePath)) {\n return opBadRequest(\"PDF generation is already in progress for this script\");\n }\n\n inFlightPdfs.add(absoluteFilePath);\n publishGeneration(chatSessionId, \"pdf\", filePath, \"\", false, { root });\n let genError: string | undefined;\n try {\n const context = await buildContext(absoluteFilePath);\n if (!context) {\n genError = \"Failed to initialize mulmo context\";\n return opServerError(genError);\n }\n const result = await runPdfGeneration(context, (beatIndex) => {\n publishGeneration(chatSessionId, \"beatImage\", filePath, String(beatIndex), true, { root });\n });\n if (!result.ok) {\n genError = result.error;\n return opServerError(result.error);\n }\n const pdfRef = toStoryRef(result.outputPath, root);\n if (pdfRef === null) return opServerError(\"generated PDF is outside the registered stories root\");\n return { ok: true, pdfPath: pdfRef };\n } catch (err) {\n genError = errorMessage(err);\n return opServerError(genError);\n } finally {\n inFlightPdfs.delete(absoluteFilePath);\n publishGeneration(chatSessionId, \"pdf\", filePath, \"\", true, { error: genError, root });\n }\n }\n\n return {\n backend,\n toStoryRef,\n resolveStory,\n guardStoryWirePath,\n guardStoryRootRegistered,\n guardStoryWriteRoot,\n artifactsForRoot,\n guardStoryGenerationRoot,\n ffmpegGuard,\n runStoryOp,\n publishGeneration,\n publishScriptChanged,\n pendingGenerations,\n beatImageOp,\n beatAudioOp,\n beatMovieOp,\n characterImageOp,\n movieStatusOp,\n pdfStatusOp,\n renderBeatOp,\n generateBeatAudioOp,\n renderCharacterOp,\n uploadBeatImageOp,\n uploadCharacterImageOp,\n inFlightMovies,\n inFlightPdfs,\n runMovieGeneration,\n runPdfGeneration,\n generateMovieOp,\n generatePdfOp,\n triggerAutoBackgroundMovie,\n };\n}\n\nexport type MulmoScriptServerOps = ReturnType<typeof createMulmoScriptServerOps>;\n","// The mulmoScript dispatch router, moved from MulmoClaude's\n// `server/plugins/mulmoscript-builtin.ts` in phase 3 so every host serves\n// the package View's `useRuntime().dispatch({ kind, … })` calls with the\n// SAME kind routing and validation. Hosts register the returned handler on\n// their dispatch channel (MulmoClaude: `registerBuiltinDispatch`;\n// MulmoTerminal: its `/api/plugin` interception).\n//\n// Response contract: every kind resolves to an `{ ok: … }` envelope (see\n// `../core/contract.ts`) — business failures are data, not thrown errors,\n// so user-facing messages stay free of transport prefixes.\n\nimport { executeMulmoScriptSave, executeUpdateBeat, executeUpdateScript, type MulmoScriptFailure } from \"../core/plugin\";\nimport { DEFAULT_ROOT, normalizeRoot } from \"../core/contract\";\nimport type { MulmoScriptExecuteContext } from \"../core/types\";\nimport type { MulmoScriptServerOps } from \"./ops\";\nimport { isRecord } from \"./support\";\nimport type { OpFailure } from \"./types\";\n\ninterface DispatchFailure {\n ok: false;\n code: \"bad_request\" | \"not_found\" | \"server_error\";\n error: string;\n}\n\nfunction fromOpFailure(failure: OpFailure): DispatchFailure {\n // \"unavailable\" (ffmpeg missing) has no slot in the contract's code\n // union — the View only reads `error`, so fold it into server_error\n // rather than widening the shared contract for one case.\n const code = failure.code === \"unavailable\" ? \"server_error\" : failure.code;\n return { ok: false, code, error: failure.error };\n}\n\nfunction fromPackageFailure(failure: MulmoScriptFailure): DispatchFailure {\n return { ok: false, code: failure.code, error: failure.error };\n}\n\nfunction invalidArgs(kind: string): DispatchFailure {\n return { ok: false, code: \"bad_request\", error: `invalid arguments for mulmoScript dispatch kind \"${kind}\"` };\n}\n\nfunction str(value: unknown): string | undefined {\n return typeof value === \"string\" && value !== \"\" ? value : undefined;\n}\n\n// Beat indexes must be non-negative integers — reject `-1` / `1.5` at the\n// dispatch boundary so invalid client input surfaces as a deterministic\n// bad_request instead of leaking into beat-indexed ops.\nfunction num(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0 ? value : undefined;\n}\n\ninterface BeatArgs {\n filePath: string;\n beatIndex: number;\n}\n\ninterface KeyArgs {\n filePath: string;\n key: string;\n}\n\n/** Pass ok results through untouched; normalize failures for the wire. */\nfunction envelope<T>(result: ({ ok: true } & T) | OpFailure): ({ ok: true } & T) | DispatchFailure {\n return result.ok ? result : fromOpFailure(result);\n}\n\nfunction beatArgs(args: Record<string, unknown>): BeatArgs | null {\n const filePath = str(args.filePath);\n const beatIndex = num(args.beatIndex);\n if (!filePath || beatIndex === undefined) return null;\n return { filePath, beatIndex };\n}\n\nfunction keyArgs(args: Record<string, unknown>): KeyArgs | null {\n const filePath = str(args.filePath);\n const key = str(args.key);\n if (!filePath || !key) return null;\n return { filePath, key };\n}\n\nconst PROBE_KINDS = new Set([\"beatImage\", \"beatAudio\", \"beatMovie\", \"characterImage\", \"movieStatus\", \"pdfStatus\"]);\nconst GENERATE_KINDS = new Set([\"renderBeat\", \"generateBeatAudio\", \"renderCharacter\", \"generateMovie\", \"generatePdf\"]);\nconst UPLOAD_KINDS = new Set([\"uploadBeatImage\", \"uploadCharacterImage\"]);\n\nexport type MulmoScriptDispatchHandler = (args: Record<string, unknown>) => Promise<unknown>;\n\n/**\n * Build the kind router over an ops instance. The save / reopen / update\n * kinds run the phase-1 core executes against the backend's artifacts\n * FileOps, guarded by the instance's realpath containment\n * (`guardStoryWirePath`) — the core's own guard is lexical.\n */\n/**\n * Stamp the root a successful result acted in.\n *\n * Only on success: a failure carries `code` and `error`, and adding a root to\n * it would invite a reader to treat the pair as addressable when the call did\n * not happen. Only when NON-default, so a result for a call that named no root\n * stays byte-identical to what this package returned before roots existed —\n * which is what keeps every existing card working untouched.\n */\nfunction withRoot(result: unknown, root: string | undefined): unknown {\n const normalized = normalizeRoot(root);\n if (normalized === DEFAULT_ROOT) return result;\n if (!isRecord(result) || result.ok !== true) return result;\n return { ...result, root: normalized };\n}\n\n/** `undefined` when `root` is absent or a string; a failure envelope otherwise. */\nfunction guardSuppliedRoot(root: unknown): { ok: false; code: string; error: string } | undefined {\n if (root === undefined || typeof root === \"string\") return undefined;\n return { ok: false, code: \"bad_request\", error: `mulmoScript root must be a string, got ${typeof root}` };\n}\n\nexport function createMulmoScriptDispatchHandler(ops: MulmoScriptServerOps): MulmoScriptDispatchHandler {\n /**\n * The executor context for a write, bound to the root it names.\n *\n * One `FileOps` was held for the whole handler, so the executors — which\n * take a WIRE path (`stories/…`) and resolve it against whatever FileOps\n * they are given — wrote a named root's script into the DEFAULT root's\n * identically-named file. Choosing here keeps `MulmoScriptExecuteContext`\n * and every executor unchanged: the root never reaches them, only the right\n * FileOps does (#3019).\n */\n function executeContextFor(root: string | undefined): MulmoScriptExecuteContext | null {\n const artifacts = ops.artifactsForRoot(root);\n return artifacts === null ? null : { files: { artifacts } };\n }\n\n async function saveKind(args: Record<string, unknown>): Promise<unknown> {\n // Which root this write lands in — see `guardStoryWriteRoot` and\n // `executeContextFor`.\n const rootGuard = ops.guardStoryWriteRoot(str(args.root));\n if (rootGuard) return fromOpFailure(rootGuard);\n const guard = ops.guardStoryWirePath(args.filePath, str(args.root));\n if (guard) return fromOpFailure(guard);\n const context = executeContextFor(str(args.root));\n if (context === null) return invalidArgs(\"save\");\n const outcome = await executeMulmoScriptSave(context, {\n script: args.script,\n filename: str(args.filename),\n filePath: str(args.filePath),\n });\n if (!outcome.ok) return fromPackageFailure(outcome);\n return { ok: true, script: outcome.script, filePath: outcome.filePath, message: outcome.message };\n }\n\n async function updateKind(kind: \"updateBeat\" | \"updateScript\", args: Record<string, unknown>): Promise<unknown> {\n const rootGuard = ops.guardStoryWriteRoot(str(args.root));\n if (rootGuard) return fromOpFailure(rootGuard);\n const guard = ops.guardStoryWirePath(args.filePath, str(args.root));\n if (guard) return fromOpFailure(guard);\n const context = executeContextFor(str(args.root));\n if (context === null) return invalidArgs(kind);\n const outcome = kind === \"updateBeat\" ? await executeUpdateBeat(context, args) : await executeUpdateScript(context, args);\n if (!outcome.ok) return fromPackageFailure(outcome);\n // After the write landed, never before: a View that reloads on a failed write would\n // discard the user's edit and show the old file back.\n ops.publishScriptChanged(str(args.filePath) ?? \"\", str(args.origin), str(args.root));\n return { ok: true };\n }\n\n const STATUS_OPS = { movieStatus: ops.movieStatusOp, pdfStatus: ops.pdfStatusOp } as const;\n const BEAT_PROBE_OPS = { beatImage: ops.beatImageOp, beatAudio: ops.beatAudioOp, beatMovie: ops.beatMovieOp } as const;\n\n async function probeKind(kind: string, args: Record<string, unknown>): Promise<unknown> {\n const statusOp = STATUS_OPS[kind as keyof typeof STATUS_OPS];\n if (statusOp) {\n const filePath = str(args.filePath);\n return filePath ? envelope(await statusOp(filePath, str(args.root))) : invalidArgs(kind);\n }\n if (kind === \"characterImage\") {\n const parsed = keyArgs(args);\n return parsed ? envelope(await ops.characterImageOp(parsed.filePath, parsed.key, str(args.root))) : invalidArgs(kind);\n }\n const parsed = beatArgs(args);\n if (!parsed) return invalidArgs(kind);\n return envelope(await BEAT_PROBE_OPS[kind as keyof typeof BEAT_PROBE_OPS](parsed.filePath, parsed.beatIndex, str(args.root)));\n }\n\n /** Movie and PDF take the whole script; the other generate kinds take a beat\n * or a character within it. */\n async function wholeScriptGenerationKind(kind: \"generateMovie\" | \"generatePdf\", args: Record<string, unknown>): Promise<unknown> {\n const filePath = str(args.filePath);\n if (!filePath) return invalidArgs(kind);\n const chatSessionId = str(args.chatSessionId);\n const root = str(args.root);\n const result = kind === \"generateMovie\" ? await ops.generateMovieOp(filePath, chatSessionId, root) : await ops.generatePdfOp(filePath, chatSessionId, root);\n return envelope(result);\n }\n\n async function generateKind(kind: string, args: Record<string, unknown>): Promise<unknown> {\n if (kind === \"generateMovie\" || kind === \"generatePdf\") return wholeScriptGenerationKind(kind, args);\n const chatSessionId = str(args.chatSessionId);\n const root = str(args.root);\n const force = args.force === true;\n if (kind === \"renderCharacter\") {\n const parsed = keyArgs(args);\n return parsed ? envelope(await ops.renderCharacterOp({ ...parsed, force, chatSessionId, root })) : invalidArgs(kind);\n }\n const parsed = beatArgs(args);\n if (!parsed) return invalidArgs(kind);\n const result =\n kind === \"renderBeat\"\n ? await ops.renderBeatOp({ ...parsed, force, chatSessionId, root })\n : await ops.generateBeatAudioOp({ ...parsed, force, chatSessionId, root });\n return envelope(result);\n }\n\n async function uploadKind(kind: string, args: Record<string, unknown>): Promise<unknown> {\n const imageData = str(args.imageData);\n if (!imageData) return invalidArgs(kind);\n if (kind === \"uploadCharacterImage\") {\n const parsed = keyArgs(args);\n return parsed ? envelope(await ops.uploadCharacterImageOp(parsed.filePath, parsed.key, imageData, str(args.root))) : invalidArgs(kind);\n }\n const parsed = beatArgs(args);\n if (!parsed) return invalidArgs(kind);\n return envelope(await ops.uploadBeatImageOp(parsed.filePath, parsed.beatIndex, imageData, str(args.root)));\n }\n\n async function pendingKind(kind: string, args: Record<string, unknown>): Promise<unknown> {\n const filePath = str(args.filePath);\n if (!filePath) return invalidArgs(kind);\n const root = str(args.root);\n // An empty snapshot for an unknown root is indistinguishable from \"no\n // work is running\" — see `guardStoryRootRegistered`.\n const rootGuard = ops.guardStoryRootRegistered(root);\n if (rootGuard) return fromOpFailure(rootGuard);\n return { ok: true, pending: ops.pendingGenerations(filePath, root) };\n }\n\n // Nothing but routing below: every kind resolves to one named handler, so\n // reading it answers \"where does this kind go\" without also having to read\n // what any of them do.\n /** Which handler serves this kind. Routing only — the caller tags the answer. */\n async function route(kind: string, args: Record<string, unknown>): Promise<unknown> {\n if (kind === \"save\") return saveKind(args);\n if (kind === \"updateBeat\" || kind === \"updateScript\") return updateKind(kind, args);\n if (PROBE_KINDS.has(kind)) return probeKind(kind, args);\n if (GENERATE_KINDS.has(kind)) return generateKind(kind, args);\n if (UPLOAD_KINDS.has(kind)) return uploadKind(kind, args);\n if (kind === \"pendingGenerations\") return pendingKind(kind, args);\n return { ok: false, code: \"bad_request\", error: `unknown mulmoScript dispatch kind \"${kind}\"` };\n }\n\n return async (args: Record<string, unknown>): Promise<unknown> => {\n const kind = str(args.kind);\n if (!kind) return invalidArgs(\"<missing>\");\n // Once, at the only entry, so no per-kind reader can forget it. `str()`\n // answers `undefined` for a number, `null`, an object — indistinguishable\n // from a root that was never supplied, which every reader below then takes\n // as the DEFAULT root. A host that serialises a root wrongly would have\n // written to, and read from, the default root's identically-named script\n // while believing it named another (Codex P2 on #3015). Absent stays\n // default; present must be a string.\n const malformedRoot = guardSuppliedRoot(args.root);\n if (malformedRoot) return malformedRoot;\n // Tagged HERE, once, rather than by each of the seventeen kinds. A host\n // builds its cards from these results and a card's identity is the pair\n // `(root, filePath)`, so a kind that forgot the tag would quietly collapse\n // two repositories' identically-named decks onto one card. Threading it\n // per-kind is exactly the shape #3015 got wrong over and over.\n return withRoot(await route(kind, args), str(args.root));\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.js","names":[],"sources":["../src/server/support.ts","../src/server/types.ts","../src/server/mulmoErrorCapture.ts","../src/server/ops.ts","../src/server/dispatch.ts"],"sourcesContent":["// Small server-side utilities. The realpath-based traversal check the ops\n// depend on used to live here as a faithful copy of the host's — it is now\n// imported from `@mulmoclaude/core/files` (#2461) so the security-critical\n// primitive cannot drift per host.\n\nimport { readFile } from \"node:fs/promises\";\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function stripDataUri(dataUri: string): string {\n return dataUri.replace(/^data:image\\/[^;]+;base64,/, \"\");\n}\n\n// Async so reading a large generated image/audio file doesn't stall the\n// host's event loop (CodeRabbit on #2137).\nexport async function fileToDataUri(filePath: string, mimeType: string): Promise<string> {\n const data = await readFile(filePath);\n return `data:${mimeType};base64,${data.toString(\"base64\")}`;\n}\n","// Contracts for the server-side ops entry (`./server`). Everything the ops\n// need from a host that ISN'T generic mulmocast work is declared here as an\n// injected backend — MulmoClaude and MulmoTerminal each supply their own\n// implementation (phase 3 of plans/done/feat-mulmoscript-plugin.md).\n\nimport type { MinimalLogger } from \"@mulmoclaude/common\";\nimport type { FileOps } from \"gui-chat-protocol\";\nimport type { MulmoScriptChangedEvent, MulmoScriptGenerationEvent } from \"../core/contract\";\n\nexport interface OpFailure {\n ok: false;\n /** REST adapter mapping: bad_request→400, not_found→404,\n * unavailable→503, server_error→500. */\n code: \"bad_request\" | \"not_found\" | \"server_error\" | \"unavailable\";\n error: string;\n}\n\nexport type OpResult<T> = ({ ok: true } & T) | OpFailure;\n\nexport interface GenerateOpArgs {\n filePath: string;\n beatIndex?: number | undefined;\n key?: string | undefined;\n force?: boolean | undefined;\n chatSessionId?: string | undefined;\n /** Which registered stories root `filePath` is relative to (#3014).\n * Absent = the host's default root. */\n root?: string | undefined;\n}\n\n/** `GenerateOpArgs` with `K` promoted to genuinely required. `Required<Pick<…>>`\n * does NOT work here: under `exactOptionalPropertyTypes` the `-?` modifier drops\n * only the `?`, leaving the explicitly declared `| undefined` in place, so the\n * op body still sees `T | undefined`. `Omit` for the rest, because intersecting\n * the whole interface would re-introduce the optional declaration. */\nexport type GenerateOpArgsWith<K extends keyof GenerateOpArgs> = { [P in K]-?: Exclude<GenerateOpArgs[P], undefined> } & Omit<GenerateOpArgs, K>;\n\nexport type MovieGenerationResult = { ok: true; outputPath: string } | { ok: false; error: string };\nexport type PdfGenerationResult = { ok: true; outputPath: string } | { ok: false; error: string };\n\nexport interface MovieProgressEvent {\n kind: \"image\" | \"audio\";\n beatIndex: number;\n}\n\n/** Host logger; every entry is already namespaced to mulmoScript by the package, so hosts just bind their own prefix/transport. */\nexport type MulmoScriptServerLog = MinimalLogger;\n\n/**\n * Host capabilities the server ops run against. Only genuinely\n * host-specific transport lives here — the mulmocast orchestration, path\n * containment, and generation-state tracking are all in-package.\n */\n/**\n * Which named-root capabilities a host has NOT wired.\n *\n * Pure and exported so the rule can be tested without building a server: the\n * condition used to key on the root COUNT alone, so a host that HAD passed\n * `artifactsFor` was told at every boot that its writes land in the default\n * root — the opposite of what the code then did (#3022).\n *\n * Reads and uploads need nothing from the host, so they never appear here.\n */\nexport function missingRootCapabilities(backend: Pick<MulmoScriptServerBackend, \"rootScopedGenerationState\" | \"artifactsFor\">): string[] {\n const missing: string[] = [];\n // The pair identity is complete INSIDE this package and stops at the host\n // boundary: a host's per-session store keys pending work on\n // `(kind, filePath, key)` — `generationKey` in `@mulmobridge/protocol` — so\n // two roots generating the same beat in one session collapse to one entry\n // (Codex P1 on #3015). A host that keeps no such store declares it.\n if (backend.rootScopedGenerationState !== true) {\n missing.push(\n \"GENERATION is refused until this host declares `rootScopedGenerationState` (its pending-generation state must carry the root, or it must keep none)\",\n );\n }\n // The save/update executors run against one FileOps; without a per-root one\n // they would rewrite the DEFAULT root's identically-named script.\n if (backend.artifactsFor === undefined) {\n missing.push(\"save/update land in the DEFAULT root until this host passes `artifactsFor`\");\n }\n return missing;\n}\n\nexport interface MulmoScriptServerBackend {\n /** Absolute path of the DEFAULT stories directory\n * (`<workspace>/artifacts/stories`). May not exist yet — the ops lazily\n * create + realpath it. A wire path with no `root` resolves here, which\n * is what makes every pre-`root` caller keep its exact behaviour. */\n storiesDir: string;\n /**\n * Additional stories roots, keyed by the id the wire `root` names\n * (#3014). Absent or empty = the single-root world this package shipped\n * with, byte for byte.\n *\n * The KEY IS OPAQUE TO THIS PACKAGE. It is looked up here and never\n * parsed, so the host owns what a root id means — a declared name, a\n * hash of the directory, an assigned handle. That decision is persisted\n * in the host's cards, so it belongs to whoever has to keep it stable,\n * and pinning it here would freeze it for every host at once.\n *\n * Registration is the containment boundary. `filePath` is a field the\n * MODEL fills (`core/definition.ts`), so the agent must never be able to\n * name a root: it may only address what the host already registered. The\n * tool definition is unchanged for exactly this reason — a host adds a\n * root, an agent cannot.\n */\n extraRoots?: Record<string, string>;\n /**\n * Whether this host can keep pending generations apart by root.\n *\n * Generation in a named root was refused outright (#3015) because\n * MulmoClaude's session store keys pending work by `(kind, filePath, key)` —\n * `generationKey` in `@mulmobridge/protocol` — so two roots generating the\n * same beat in one session collapse to one entry and either completion\n * clears the other root's indicator.\n *\n * That hazard is the HOST's, not this package's, and not every host has it:\n * MulmoTerminal ignores `chatSessionId` and publishes straight to a pubsub\n * channel the View filters by the pair, so it was being refused for a\n * collision it cannot have (#3019). The question was never \"is this root the\n * default\" but \"can this host tell two roots' generations apart\".\n *\n * Default `false` — absent means the refusal stays exactly as it shipped, so\n * a host that has not thought about this is not quietly opened up. A host\n * sets it only once its own pending-generation state carries the root (or it\n * keeps none at all).\n */\n rootScopedGenerationState?: boolean;\n /** Shared artifacts FileOps (rooted at `<workspace>/artifacts`) for the\n * save / reopen / update dispatch kinds (phase-1 core executes). */\n artifacts: FileOps;\n /**\n * The FileOps for a named root's artifacts area, or null when the host does\n * not serve that root.\n *\n * `artifacts` above is ONE FileOps bound to the default root, and the\n * save / update executors run against it. So a write naming another root\n * rewrote the DEFAULT root's identically-named script and then announced the\n * other one as changed — which is why those kinds were refused outright\n * (#3015 review G1). Reads and uploads never had the problem: they go\n * through `resolveStory` and take the absolute path it returns.\n *\n * A resolver rather than a map, because the host already has one: it knows\n * which directory an opaque root id names, and building a FileOps for it is\n * a closure per root (MulmoTerminal's `createFileOps(rootFor, label)` takes\n * a root getter for exactly this). Absent means named-root writes stay\n * refused, so a host that has not wired it keeps the shipped behaviour.\n */\n artifactsFor?: (root: string) => FileOps | null;\n /** Atomic file write (tmp alongside destination + rename; parent dirs\n * created). Hosts inject their hardened implementation. */\n writeFileAtomic: (absolutePath: string, data: string | Uint8Array) => Promise<void>;\n /** ffmpeg availability probe. `false` blocks render/movie/PDF ops with a\n * clear message; `true`/`undefined` proceeds (a boot probe may not have\n * completed yet — never block on the startup window). */\n isFfmpegAvailable?: () => boolean | undefined;\n /**\n * Generation fan-out (session channels, UI pubsub). Called on EDGE\n * transitions only — first start / last finish of concurrent same-key\n * runs — plus the finish-only per-beat pulses from the movie/PDF\n * pipelines. `chatSessionId` is undefined for callers outside a chat\n * session. The package keeps the in-flight snapshot itself.\n */\n onGenerationEvent?: (chatSessionId: string | undefined, event: MulmoScriptGenerationEvent) => void;\n /**\n * A script was written. Every open View reloads from disk, which is what makes an agent's\n * edit appear without the user reopening the canvas.\n */\n onScriptChanged?: (event: MulmoScriptChangedEvent) => void;\n log?: MulmoScriptServerLog;\n}\n","// Surfaces the underlying provider error that mulmocast swallows when a\n// generation fails. mulmocast catches the real error (missing API key,\n// quota, moderation, …), logs it via GraphAILogger.error, and rethrows a\n// generic wrapper like \"generateReferenceImage: generate error: key=x\" —\n// and `setGraphAILogger(false)` (called per request in buildContext to\n// silence GraphAI's chatty info/debug output) turns off even the error\n// level, so the true cause used to vanish entirely.\n//\n// Moved verbatim from MulmoClaude's server/utils/mulmoErrorCapture.ts in\n// phase 3 (only mulmoScript code ever used it). Hosts must resolve ONE\n// hoisted `graphai` copy shared with their `mulmocast` — GraphAILogger\n// state is module-local, and a second copy would break this capture\n// silently. That's why `graphai` is a peer dependency.\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport { GraphAILogger } from \"graphai\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { isRecord } from \"./support\";\nimport type { MulmoScriptServerLog } from \"./types\";\n\nconst capturedErrors = new AsyncLocalStorage<string[]>();\nlet loggerInstalled = false;\nlet captureLog: MulmoScriptServerLog | null = null;\n\n/** Route captured GraphAI errors into the host logger. Set once by\n * `createMulmoScriptServerOps`; the GraphAILogger sink is global, so the\n * last-configured host logger wins (one ops instance per process). */\nexport function setMulmoErrorCaptureLogger(log: MulmoScriptServerLog | null): void {\n captureLog = log;\n}\n\nfunction formatLogArg(arg: unknown): string {\n if (typeof arg === \"string\") return arg;\n if (arg instanceof Error) return arg.message;\n try {\n return JSON.stringify(arg);\n } catch {\n return String(arg);\n }\n}\n\n/**\n * Re-enable GraphAI's error level (everything else stays silenced) and\n * route it into the host logger + the per-operation capture store.\n * Call after every `setGraphAILogger(false)` — that helper disables all\n * levels including error. Idempotent.\n */\nexport function enableGraphAIErrorCapture(): void {\n GraphAILogger.setLevelEnabled(\"error\", true);\n if (loggerInstalled) return;\n loggerInstalled = true;\n GraphAILogger.setLogger((level, ...args) => {\n if (level !== \"error\") return;\n const message = args.map(formatLogArg).join(\" \");\n captureLog?.warn(\"mulmocast generation error\", { message });\n capturedErrors.getStore()?.push(message);\n });\n}\n\n// Structured-`cause` fields mulmocast attaches for i18n notifications\n// (mulmocast lib/utils/error_cause.js) — agent + error type identify\n// which provider failed; envVarName names a missing API key outright.\nconst CAUSE_FIELDS = [\"type\", \"agentName\", \"envVarName\", \"errorCode\", \"errorType\"] as const;\n\n/** Render mulmocast's structured error `cause` as \"field=value\" pairs. */\nexport function describeMulmoCause(err: unknown): string | null {\n if (!(err instanceof Error) || !isRecord(err.cause)) return null;\n const { cause } = err;\n const parts = CAUSE_FIELDS.flatMap((field) => {\n const value = cause[field];\n return typeof value === \"string\" && value !== \"\" ? [`${field}=${value}`] : [];\n });\n return parts.length > 0 ? parts.join(\" \") : null;\n}\n\n/**\n * Compose the enriched message for a failed mulmocast operation:\n * mulmocast's own message, then its structured cause, then the\n * captured underlying provider error(s). Deduped — GraphAI retries\n * log the same error more than once.\n */\nexport function composeMulmoErrorMessage(err: unknown, captured: readonly string[]): string {\n const base = errorMessage(err);\n const details = [...new Set(captured)].filter((message) => message !== \"\" && message !== base);\n return [base, describeMulmoCause(err), ...details].filter(Boolean).join(\" — \");\n}\n\n/**\n * Run a mulmocast operation, capturing GraphAI error logs emitted while\n * it executes. On failure, rethrows with the captured provider error(s)\n * appended to the message (original error kept as `cause`). Uses\n * AsyncLocalStorage so concurrent operations don't cross-attribute.\n */\nexport async function withMulmoErrorCapture<T>(operation: () => Promise<T>): Promise<T> {\n return capturedErrors.run([], async () => {\n try {\n return await operation();\n } catch (err) {\n throw new Error(composeMulmoErrorMessage(err, capturedErrors.getStore() ?? []), { cause: err });\n }\n });\n}\n","// Transport-free cores for every mulmoScript operation, moved from\n// MulmoClaude's `server/api/routes/mulmo-script-ops.ts` in phase 3 so the\n// SAME implementation backs every host surface:\n//\n// - MulmoClaude's legacy REST routes (kept for wire compat),\n// - the generic plugin dispatch (see `./dispatch`) that the package View\n// calls in both MulmoClaude and MulmoTerminal.\n//\n// Every op returns an `OpResult` — failures are data (`code` preserves the\n// HTTP mapping for REST adapters) and never exceptions. Generation ops\n// publish start/finish through the instance's edge-triggered tracker, which\n// fans out via the injected `backend.onGenerationEvent` (session channels,\n// UI pubsub — host-specific) and backs the View's mount-time\n// `pendingGenerations` snapshot.\n//\n// Host-specific transport is injected via `MulmoScriptServerBackend`; the\n// mulmocast orchestration, realpath containment, and generation-state\n// tracking all live here.\n\nimport { existsSync, mkdirSync, realpathSync, statSync, unlinkSync } from \"fs\";\nimport path from \"path\";\nimport type { FileOps } from \"gui-chat-protocol\";\nimport {\n getFileObject,\n initializeContextFromFiles,\n generateBeatImage,\n getBeatPngImagePath,\n generateBeatAudio,\n getBeatAudioPathOrUrl,\n getBeatAnimatedVideoPath,\n getBeatMoviePaths,\n generateReferenceImage,\n getReferenceImagePath,\n images,\n audio,\n movie,\n movieFilePath,\n pdf,\n pdfFilePath,\n setGraphAILogger,\n addSessionProgressCallback,\n removeSessionProgressCallback,\n} from \"mulmocast\";\nimport type { MulmoBeat, MulmoImagePromptMedia, MulmoStudioContext } from \"@mulmocast/types\";\nimport { DEFAULT_ROOT, normalizeRoot } from \"../core/contract\";\nimport type { MulmoScriptGenerationEvent } from \"../core/contract\";\nimport { normalizeStoryPath, storyRefWithin, storiesRelativePath } from \"../core/paths\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { resolveWithinRoot } from \"@mulmoclaude/core/files\";\nimport { fileToDataUri, stripDataUri } from \"./support\";\nimport { missingRootCapabilities } from \"./types\";\nimport { enableGraphAIErrorCapture, setMulmoErrorCaptureLogger, withMulmoErrorCapture } from \"./mulmoErrorCapture\";\nimport type {\n GenerateOpArgsWith,\n MovieGenerationResult,\n MovieProgressEvent,\n MulmoScriptServerBackend,\n MulmoScriptServerLog,\n OpFailure,\n OpResult,\n PdfGenerationResult,\n} from \"./types\";\n\ntype GenerationKind = MulmoScriptGenerationEvent[\"kind\"];\n\n// We pin pdfMode=\"slide\" + pdfSize=\"a4\" — that's the configured default\n// for the storyboard editor; mulmocast's other modes (talk / handout /\n// letter) stay reachable via the CLI for power users. (#1614)\nexport const PDF_MODE = \"slide\" as const;\nexport const PDF_SIZE = \"a4\" as const;\n\nfunction opBadRequest(error: string): OpFailure {\n return { ok: false, code: \"bad_request\", error };\n}\n\nfunction opNotFound(error: string): OpFailure {\n return { ok: false, code: \"not_found\", error };\n}\n\nfunction opServerError(error: string): OpFailure {\n return { ok: false, code: \"server_error\", error };\n}\n\nconst NOOP_LOG: MulmoScriptServerLog = { info: () => {}, warn: () => {}, error: () => {} };\n\n// Helper: build mulmo context for a story file. The explicit return\n// annotation keeps declaration emit portable — the inferred type would\n// reference mulmocast's internal usage-collector path.\nexport async function buildContext(absoluteFilePath: string, force = false): Promise<MulmoStudioContext | null | undefined> {\n // setGraphAILogger(false) silences GraphAI's chatty info/debug output\n // but also its error level — re-enable error capture so a failed\n // generation surfaces the real provider error, not just mulmocast's\n // generic \"generate error\" wrapper.\n setGraphAILogger(false);\n enableGraphAIErrorCapture();\n const files = getFileObject({\n file: absoluteFilePath,\n basedir: path.dirname(absoluteFilePath),\n grouped: true,\n });\n return initializeContextFromFiles(files, true, force);\n}\n\n// Awaited context type used by every op that calls buildContext.\nexport type StoryContext = NonNullable<Awaited<ReturnType<typeof buildContext>>>;\n\nexport interface RunStoryOpDeps {\n resolveStory?: (filePath: string, root?: string) => { ok: true; absolutePath: string } | OpFailure;\n buildContext?: (absoluteFilePath: string, force?: boolean) => Promise<StoryContext | undefined>;\n}\n\nexport interface RunStoryOpOptions<T> {\n force?: boolean | undefined;\n /** Which registered stories root `filePath` is relative to (#3014).\n * Absent = the host's default root, i.e. exactly the pre-roots path. */\n root?: string | undefined;\n /**\n * Op-specific tag included in the failure log so dashboards can\n * distinguish which op is failing (e.g. `\"generate-beat-audio\"`).\n * Falls back to a generic `\"op failed\"` entry when omitted.\n */\n operation?: string;\n /**\n * Soft-fail override for `buildContext` returning undefined. Some\n * ops (e.g. `beatAudio`) historically returned a 200 `{ audio: null }`\n * in that case so the frontend can silently retry. If provided, this\n * callback returns the fallback result instead of the default\n * server_error \"Failed to initialize mulmo context\".\n */\n onContextMissing?: () => OpResult<T>;\n}\n\n// Map each beat to its array index, keyed by beat.id (falling back to\n// a synthetic `__index__<n>` for id-less beats). Shared by the movie\n// and PDF pipelines to translate mulmocast's per-beat progress events\n// (which carry the beat id) back into an index the UI can address.\nexport function buildBeatIdIndex(beats: MulmoBeat[]): Map<string, number> {\n const idToIndex = new Map<string, number>();\n beats.forEach((beat, index) => {\n const key = beat.id ?? `__index__${index}`;\n idToIndex.set(key, index);\n });\n return idToIndex;\n}\n\n// Run `body` with a mulmocast per-beat progress callback registered.\n// `onBeat` receives each beat event's sessionType + resolved index; the\n// caller decides which sessionTypes to forward. The callback is always\n// unregistered, even when `body` throws.\n//\n// Known limitation: addSessionProgressCallback is global, so when two\n// generations for *different* scripts run concurrently, both closures\n// are invoked for every beat event and rely on idToIndex to filter out\n// the other run's events. That filter is reliable only when each beat\n// carries an explicit `id`. Beats without one fall back to\n// \"__index__${index}\", and identical fallback ids across scripts collide\n// → progress meant for script A surfaces on script B. Fixing this\n// properly needs mulmocast to attach a per-run identifier to its\n// progress events (or a global serialization gate); tracked separately.\nasync function withBeatProgress<T>(beats: MulmoBeat[], onBeat: (sessionType: string, beatIndex: number) => void, body: () => Promise<T>): Promise<T> {\n const idToIndex = buildBeatIdIndex(beats);\n const onProgress = (event: { kind: string; sessionType: string; id?: string; inSession: boolean }) => {\n if (event.kind !== \"beat\" || event.inSession || event.id === undefined) return;\n const beatIndex = idToIndex.get(event.id);\n if (beatIndex === undefined) return;\n onBeat(event.sessionType, beatIndex);\n };\n addSessionProgressCallback(onProgress);\n try {\n return await body();\n } finally {\n removeSessionProgressCallback(onProgress);\n }\n}\n\n/** Map identity for the in-flight tracker. JSON array keeps the three\n * fields unambiguous (a human-visible delimiter could collide). */\nfunction generationMapKey(kind: GenerationKind, filePath: string, key: string, root?: string): string {\n // Normalised HERE rather than by the caller. A start keyed `\" repoA \"` and a\n // finish keyed `\"repoA\"` never match, and the tracker entry then leaks for\n // the life of the process — so the one place that builds the key is the one\n // place that must not be able to get the spelling wrong (Codex on #3015).\n const normalized = normalizeRoot(root);\n // `root` is the LAST element so a call site that omits it produces exactly\n // the pre-#3014 key — the default root's entries keep their identity across\n // an upgrade, and a running generation is not orphaned by one.\n return normalized === DEFAULT_ROOT ? JSON.stringify([kind, filePath, key]) : JSON.stringify([kind, filePath, key, normalized]);\n}\n\n/**\n * Build the per-host mulmoScript server ops instance. One instance per\n * process — it owns the in-flight movie/PDF dedup sets and the\n * generation-state tracker, and binds the injected host backend.\n */\nexport function createMulmoScriptServerOps(backend: MulmoScriptServerBackend) {\n const log = backend.log ?? NOOP_LOG;\n setMulmoErrorCaptureLogger(log);\n // Root registry: the default (pre-#3014, wire `root` absent) plus whatever\n // the host registered. Resolved once — the host owns the ids, this package\n // only ever looks them up.\n const rootDirs = new Map<string, string>([[DEFAULT_ROOT, path.resolve(backend.storiesDir)]]);\n for (const [id, dir] of Object.entries(backend.extraRoots ?? {})) {\n // An empty id is the default root's own key: accepting it would re-point\n // every pre-roots caller at someone else's directory. Dropping it quietly\n // would hide the host's misconfiguration until a read returned the wrong\n // file, and this runs at boot, where throwing is the cheap failure.\n // Trim on registration too: a lookup normalizes, so an untrimmed key would\n // be unreachable.\n const trimmed = id.trim();\n if (trimmed === DEFAULT_ROOT) {\n throw new Error(\"mulmoScript: extraRoots key must not be empty — the empty id is reserved for the default stories root\");\n }\n // Two ids that trim to one key: `set` would silently keep the LAST\n // directory, so every read for `repoA` would answer from a directory the\n // card never named, with the host given no signal (CodeRabbit on #3015).\n // Same reasoning as the empty id above — a misconfiguration is cheapest to\n // fail on at boot, and silently resolving the wrong directory is the\n // failure this package refuses everywhere else.\n if (rootDirs.has(trimmed)) {\n throw new Error(`mulmoScript: extraRoots keys must be distinct after trimming — \"${trimmed}\" is registered twice`);\n }\n rootDirs.set(trimmed, path.resolve(dir));\n }\n warnAboutUnwiredRoots();\n\n /**\n * Tell a host which named-root capabilities it has not wired — and nothing\n * when it has wired them all.\n *\n * The condition used to be the root COUNT alone, so a host that had passed\n * `artifactsFor` was still told, at every boot, that its writes land in the\n * default root. That is not a stale wording: it is the opposite of what the\n * code then does, read by the hosts that got the wiring RIGHT (#3022, from\n * the consuming host).\n *\n * Silence when nothing is missing, because a warning that always fires is\n * one people learn to skip — and then the host that really did forget\n * `artifactsFor` cannot tell either. Each clause is emitted only when that\n * capability is actually absent, so the message says what is true for THIS\n * host rather than what was true when it was written.\n */\n function warnAboutUnwiredRoots(): void {\n if (rootDirs.size <= 1) return;\n const missing = missingRootCapabilities(backend);\n if (missing.length === 0) return;\n log.warn(`extra stories roots registered — reads and uploads work, but ${missing.join(\", and \")} (#3019)`, {\n roots: [...rootDirs.keys()].filter((id) => id !== DEFAULT_ROOT),\n });\n }\n\n /** The registered directory for a wire `root`, or null when the host never\n * registered it. Null is a REJECTION, not a fallback to the default: an\n * unknown root must not quietly read the workspace's file of the same\n * name. */\n function rootDir(root: string | undefined): string | null {\n return rootDirs.get(normalizeRoot(root)) ?? null;\n }\n\n // ── Story path infrastructure ─────────────────────────────────\n\n // The download / status ops expect \"stories/<rel>\" (historical\n // convention, independent of the on-disk location) — the wire format\n // every endpoint keys on. Relativize against the REALPATH root when it\n // resolves: with a symlinked stories dir, mulmocast returns output\n // paths under the link's target, and relativizing against the link\n // itself would produce a traversal-like \"stories/../../…\" ref that\n // resolveStory then rejects (CodeRabbit on #2137).\n function toStoryRef(absolutePath: string, root?: string): string | null {\n // An unregistered root gets null, not the default root's base. Relativizing\n // against the default would mint a wire ref that READS BACK as a different\n // file — the same silent-substitution failure `resolveStory` rejects, and\n // this function is on the ops object, so a host can reach it directly\n // without passing through that guard (#3015 review F2).\n const dir = rootDir(root);\n if (dir === null) return null;\n const base = ensureStoriesReal(root) ?? dir;\n // The relativizing rule is a pure function in `core/paths.ts` so it can be\n // driven with `path.win32` from a POSIX machine — the case it guards\n // (no relative route across drives) is unreachable here.\n return storyRefWithin(base, absolutePath, path);\n }\n\n // Lazily realpath the stories dir on first use. We can't realpath at\n // instance creation because the directory may not exist yet (it's\n // created on demand by the save route). The cache is invalidated\n // never — once the dir exists, its realpath is stable.\n //\n // Keyed by the resolved DIRECTORY rather than the root id, so there is no\n // normalisation left to forget: `rootDir` already collapses every spelling of\n // a root to one absolute path, and two ids pointing at the same directory\n // share one entry instead of allocating a second. Keying on the raw id let\n // `\" repoA \"` and `\"repoA\"` both resolve and then cache separately, in a map\n // that is never evicted (Codex P2 on #3015).\n const storiesRealCache = new Map<string, string>();\n function ensureStoriesReal(root?: string): string | null {\n const dir = rootDir(root);\n if (dir === null) return null;\n const cached = storiesRealCache.get(dir);\n if (cached) return cached;\n try {\n // Only the DEFAULT root is created on demand. An extra root is a\n // directory the user already owns — often a git worktree — and creating\n // it here would grow `artifacts/stories/` inside their repository as a\n // side effect of a status poll. A host that registers a root is\n // responsible for it existing (#3015 review F3).\n if (normalizeRoot(root) === DEFAULT_ROOT) mkdirSync(dir, { recursive: true });\n const real = realpathSync(dir);\n storiesRealCache.set(dir, real);\n return real;\n } catch {\n return null;\n }\n }\n\n /**\n * Resolve and validate a stories wire path to its absolute realpath.\n *\n * Uses the realpath-based resolveWithinRoot helper to defeat\n * symlink-based escapes. Callers pass wire paths like\n * \"stories/foo.json\" or \"stories/__movies__/bar.mp4\". We strip the\n * leading \"stories/\" segment and resolve the remainder against the\n * realpath of the stories directory itself — this works whether\n * stories/ is a regular directory or a legitimate symlink to another\n * location. ENOENT and traversal are distinguished (404 vs 400).\n */\n function resolveStory(filePath: string, root?: string): { ok: true; absolutePath: string } | OpFailure {\n // An unregistered root is a bad request, not a fall back to the default:\n // resolving it against the workspace would hand the caller a DIFFERENT\n // file that happens to share the name.\n if (rootDir(root) === null) {\n return opBadRequest(\"Unknown stories root\");\n }\n const storiesReal = ensureStoriesReal(root);\n if (!storiesReal) {\n return opServerError(\"stories directory not available\");\n }\n // Reject absolute paths and parent traversal at the syntactic\n // level — defense in depth on top of the realpath check below.\n if (path.isAbsolute(filePath)) {\n return opBadRequest(\"Invalid filePath\");\n }\n // Accept the workspace-relative spelling \"artifacts/stories/<rel>\"\n // the tool description historically taught (the wire form was truly\n // workspace-relative before the stories dir moved under artifacts/\n // in #284) by reducing it to the canonical \"stories/<rel>\".\n const ARTIFACTS_STORIES = \"artifacts/stories\";\n const wirePath = filePath === ARTIFACTS_STORIES || filePath.startsWith(`${ARTIFACTS_STORIES}/`) ? filePath.slice(\"artifacts/\".length) : filePath;\n // Strip the optional \"stories/\" prefix so the remainder is a path\n // relative to storiesReal. Accepts both \"stories/foo.json\" (the\n // canonical caller convention) and bare \"foo.json\".\n const STORIES_PREFIX = `stories${path.sep}`;\n const relFromStories =\n wirePath === \"stories\" ? \"\" : wirePath.startsWith(STORIES_PREFIX) || wirePath.startsWith(\"stories/\") ? wirePath.slice(\"stories/\".length) : wirePath;\n // A base path with no remainder (\"stories\", \"artifacts/stories\",\n // trailing-slash variants) would resolve to the stories directory\n // itself and hand downstream ops a directory where they expect a\n // file — reject it, mirroring normalizeStoryPath's non-empty rule.\n if (relFromStories === \"\") {\n return opBadRequest(\"Invalid filePath\");\n }\n // resolveWithinRoot enforces both the realpath boundary AND\n // existence; ENOENT and traversal both produce null. Distinguish\n // them via a follow-up existsSync so 404 vs 400 stays accurate —\n // but only consult the filesystem for lexically in-root candidates:\n // a traversal path must never touch the fs (and gets a uniform\n // bad_request so responses don't leak existence outside the root).\n const resolved = resolveWithinRoot(storiesReal, relFromStories);\n if (!resolved) {\n const candidate = path.resolve(storiesReal, relFromStories);\n const inRoot = candidate === storiesReal || candidate.startsWith(storiesReal + path.sep);\n if (inRoot && !existsSync(candidate)) {\n return opNotFound(`File not found: ${filePath}`);\n }\n return opBadRequest(\"Invalid filePath\");\n }\n return { ok: true, absolutePath: resolved };\n }\n\n /**\n * Whether this root is one the host registered.\n *\n * Every other root-aware op learns this from `resolveStory`, which needs a\n * file. `pendingGenerations` needs none — it only filters an in-memory map —\n * so an unregistered root produced `{ ok: true, pending: [] }`, and a host\n * typo or a stale card read back as \"no work is running\" (Codex P2 on\n * #3015). An unknown root is a question this package cannot answer, and the\n * answer it must not give is a confident empty one.\n */\n function guardStoryRootRegistered(root: string | undefined): OpFailure | null {\n return rootDir(root) === null ? opBadRequest(`unknown stories root \"${normalizeRoot(root)}\"`) : null;\n }\n\n /**\n * Realpath containment pre-guard for wire paths handed to the phase-1\n * core's save/reopen/update executes. The core's own path guard is\n * lexical (it runs against the generic FileOps, whose read/write follows\n * symlinks), so hosts re-assert the realpath boundary here before\n * invoking it — a symlink planted below the stories dir can't read or\n * write outside the tree (Codex P1 on MulmoClaude#2133).\n *\n * Returns null when `filePath` isn't a non-empty string — shape\n * validation (including the script-vs-filePath mode check) belongs to\n * the core.\n */\n function guardStoryWirePath(filePath: unknown, root?: string): OpFailure | null {\n if (typeof filePath !== \"string\" || filePath === \"\") return null;\n const resolved = resolveStory(filePath, root);\n return resolved.ok ? null : resolved;\n }\n\n /**\n * Whether a GENERATION may run in this root.\n *\n * The pair identity is complete inside this package but stops at the host\n * boundary: a host's per-session store keys pending work by\n * `(kind, filePath, key)` — `generationKey` in `@mulmobridge/protocol`,\n * which bridges also consume. Two roots running the same generation in one\n * session would collapse to one entry, and either completion would clear the\n * other root's indicator (Codex P1 on #3015).\n *\n * Whose hazard it is decides who answers: a host declares\n * `rootScopedGenerationState` when its own pending state carries the root —\n * or when it keeps none, which is MulmoTerminal's case. It was refused for a\n * collision it cannot have (#3019).\n *\n * Absent still refuses, so a host that has not thought about this keeps the\n * shipped behaviour rather than being quietly opened up.\n */\n function guardStoryGenerationRoot(root: string | undefined): OpFailure | null {\n if (normalizeRoot(root) === DEFAULT_ROOT) return null;\n // Registration BEFORE the host's opt-in. The flag says \"this host can tell\n // two roots apart\", not \"any id is addressable\" — and the generation ops\n // publish their start event before `runStoryOp` reaches `resolveStory`, so\n // an unregistered root emitted a start/finish pair for work that never\n // existed (Codex + CodeRabbit on #3020).\n const registered = guardStoryRootRegistered(root);\n if (registered) return registered;\n if (backend.rootScopedGenerationState === true) return null;\n return opBadRequest(\"generating in a non-default stories root is not supported yet\");\n }\n\n /**\n * The write half of the same fail-closed rule.\n *\n * It lives in the ops that write rather than at their dispatch sites,\n * because a per-site guard is a list someone has to remember to extend:\n * `save` / `updateBeat` / `updateScript` were guarded and the two upload\n * kinds were not, so an image could still land in a named root\n * (CodeRabbit on #3015). `save` / `update* ` cannot follow suit — they run\n * through the package executors, not through these ops — so\n * `test_server_roots.ts` walks the whole ops surface and fails on any op\n * that is neither in the read-only allowlist nor refusing.\n */\n /**\n * Whether a WRITE may target this root.\n *\n * Reads are root-aware; writes are not. `executeMulmoScriptSave` and the\n * update executors run against one `FileOps`, bound by the host to the\n * default root, so a write naming another root would rewrite the DEFAULT\n * root's identically-named file and then announce the other one as changed\n * (#3015 review G1). Closing it was fail-closed: \"readable but not yet\n * writable\" beats \"wrote somewhere else and said so\".\n *\n * It opens per root, not globally: the host answers `artifactsFor` for the\n * roots it can serve, and a root it cannot is still refused. A host that\n * wires nothing keeps the shipped refusal (#3019).\n *\n * The two refusals say different things because they are fixed in different\n * places. No `artifactsFor` at all is a capability this host has not turned\n * on. `artifactsFor` present but answering `null` for a REGISTERED root is a\n * wiring mistake inside that host — and it is the quiet one, because the\n * boot warning stays silent (the resolver WAS passed) while every write is\n * refused. One message for both read as \"the plugin cannot do this yet\",\n * which sends the host looking in the wrong place (#3024).\n */\n function guardStoryWriteRoot(root: string | undefined): OpFailure | null {\n if (normalizeRoot(root) === DEFAULT_ROOT) return null;\n const registered = guardStoryRootRegistered(root);\n if (registered) return registered;\n if (artifactsForRoot(root) !== null) return null;\n return backend.artifactsFor === undefined\n ? opBadRequest(\"writing to a non-default stories root is not supported yet\")\n : opBadRequest(`this host's \\`artifactsFor\\` returned no FileOps for the registered stories root \"${normalizeRoot(root)}\"`);\n }\n\n /**\n * The FileOps a write to this root must go through.\n *\n * The default root keeps the single `artifacts` it always had, byte for\n * byte. A named root is served only when the host both REGISTERED it\n * (`extraRoots` — the containment boundary, so a host cannot widen the\n * addressable set through this back door) and can supply a FileOps for it.\n */\n function artifactsForRoot(root: string | undefined): FileOps | null {\n const normalized = normalizeRoot(root);\n if (normalized === DEFAULT_ROOT) return backend.artifacts;\n if (rootDir(normalized) === null) return null;\n const hostOps = backend.artifactsFor?.(normalized);\n return hostOps === undefined || hostOps === null ? null : storiesScoped(hostOps);\n }\n\n /**\n * Address a named root's FileOps the way the READ side addresses it.\n *\n * The two sides disagreed about one path segment, and the disagreement was\n * silent. A read strips `stories/` and resolves under the registered\n * directory (`<root>/<rel>`); a write handed the executors' wire path\n * straight to the host's FileOps, which is rooted at that same registered\n * directory, so the bytes landed in `<root>/stories/<rel>`. `save` then\n * REPORTED SUCCESS and returned a wire path that could not be read back —\n * a card pointing at nothing (#3020 review H1, from the consuming host).\n *\n * Stripping here makes \"the directory you registered is the directory your\n * FileOps is rooted at\" true, which is what a host writes without being\n * told. The default root is untouched: its FileOps is rooted one level up,\n * at `<workspace>/artifacts`, and is shared with other plugins.\n *\n * A path that is not a stories wire path throws rather than passing\n * through. Everything reaching here has been through `normalizeStoryPath`\n * or `storyFilePath`, so one that has not is a bug — and letting it through\n * would write it somewhere nobody can read, which is the failure this whole\n * wrapper exists to end.\n */\n function storiesScoped(inner: FileOps): FileOps {\n const within = (wirePath: string): string => {\n const relative = storiesRelativePath(wirePath);\n if (relative === null) throw new Error(`mulmoScript: \"${wirePath}\" is not a stories path — a named root's FileOps takes stories paths only`);\n return relative;\n };\n // Every member is `async` so the refusal arrives as a REJECTED PROMISE,\n // the way every other FileOps failure does. Throwing synchronously out of\n // a method the caller only ever awaits would skip its `try`.\n return {\n read: async (wirePath) => inner.read(within(wirePath)),\n readBytes: async (wirePath) => inner.readBytes(within(wirePath)),\n write: async (wirePath, data) => inner.write(within(wirePath), data),\n readDir: async (wirePath) => inner.readDir(within(wirePath)),\n stat: async (wirePath) => inner.stat(within(wirePath)),\n exists: async (wirePath) => inner.exists(within(wirePath)),\n unlink: async (wirePath) => inner.unlink(within(wirePath)),\n };\n }\n\n // mulmocast shells out to ffmpeg for movie / beat rendering. When the\n // host's probe reports it absent, intercept with a clear failure\n // instead of letting the library throw an opaque spawn ENOENT\n // mid-pipeline. `undefined` means the probe hasn't completed — assume\n // available so a brief startup window never blocks a render.\n function ffmpegGuard(): OpFailure | null {\n if (backend.isFfmpegAvailable?.() === false) {\n return {\n ok: false,\n code: \"unavailable\",\n error: \"ffmpeg is not installed — movie and beat rendering are unavailable. Install ffmpeg and restart the server.\",\n };\n }\n return null;\n }\n\n // ── Generation tracker (edge-triggered) ───────────────────────\n\n // Refcounted: two concurrent generations with the same kind/filePath/key\n // (e.g. the same beat rendered from two tabs) must not have the first\n // completion erase the second run's snapshot entry, and only the first\n // start / LAST finish reach the host channels — an early completion\n // can't clear subscribers' spinners while a duplicate run is active.\n // A finish with no tracked start (the movie/PDF pipelines' per-beat\n // completion pulses) always publishes.\n const inFlightGenerations = new Map<string, { kind: GenerationKind; filePath: string; key: string; root: string; count: number }>();\n\n /** Emit `root` only when it names a non-default one: an event carrying\n * `root: \"\"` and one carrying nothing must stay indistinguishable to every\n * pre-#3014 consumer. */\n function rootField(root: string | undefined): { root?: string } {\n const normalized = normalizeRoot(root);\n return normalized === DEFAULT_ROOT ? {} : { root: normalized };\n }\n\n /** Tracker state and events key on the canonical `stories/<rel>` wire\n * form: subscribers (the View's pubsub filter, `pendingGenerations`\n * callers) match by exact string, so the accepted alias spellings\n * (`artifacts/stories/<rel>`, bare `<rel>`) must collapse to the same\n * key as the canonical one (Codex P2 on #2139). Untrusted spellings\n * pass through unchanged — they never resolve, so they can't collide. */\n function canonicalWirePath(filePath: string): string {\n return normalizeStoryPath(filePath) ?? filePath;\n }\n\n /**\n * `error` and `root` travel in an options object, NOT as two trailing\n * optional positionals.\n *\n * They were positional once, and appending `root` to the sixteen call sites\n * put it in `error`'s slot at the nine that pass no error: every start event\n * carried `error: \"<root>\"`, the root was dropped from the key, the tracker\n * entry was filed under the default root — and because the matching finish\n * DID pass both, its key differed and the entry was never deleted, leaking\n * one row per generation for the life of the process. Two adjacent optional\n * strings cannot be told apart by the type checker, so the shape is the only\n * thing that can prevent it (#3015 review).\n */\n function publishGeneration(\n chatSessionId: string | undefined,\n kind: GenerationKind,\n filePath: string,\n key: string,\n finished: boolean,\n opts: { error?: string | undefined; root?: string | undefined } = {},\n ): void {\n const { error, root } = opts;\n const wirePath = canonicalWirePath(filePath);\n // The NORMALIZED root, not the raw spelling. The tracker value and the\n // emitted event both normalize, so keying on the raw text made a start\n // written `\" repoA \"` and its finish written `\"repoA\"` two entries: the\n // finish deleted nothing and the start leaked (Codex P2 on #3015).\n const mapKey = generationMapKey(kind, wirePath, key, root);\n const existing = inFlightGenerations.get(mapKey);\n if (finished) {\n if (existing && existing.count > 1) {\n existing.count -= 1;\n return; // a duplicate run is still active — suppress the early finish\n }\n inFlightGenerations.delete(mapKey);\n } else {\n if (existing) {\n existing.count += 1;\n return; // already reported as started\n }\n inFlightGenerations.set(mapKey, { kind, filePath: wirePath, key, root: normalizeRoot(root), count: 1 });\n }\n const event: MulmoScriptGenerationEvent = {\n kind,\n filePath: wirePath,\n key,\n done: finished,\n ...(error ? { error } : {}),\n ...rootField(root),\n };\n backend.onGenerationEvent?.(chatSessionId, event);\n }\n\n /**\n * Tell every open View that this script was written.\n *\n * `origin` is the writer. A View passes its own id so it can ignore the echo of its own\n * save — reloading there would rebuild the element the caret is in, mid-keystroke. A write\n * from the agent carries no origin, so everyone reloads.\n */\n function publishScriptChanged(filePath: string, origin?: string, root?: string): void {\n backend.onScriptChanged?.({\n filePath: canonicalWirePath(filePath),\n ...(origin === undefined ? {} : { origin }),\n ...rootField(root),\n });\n }\n\n /**\n * Snapshot of generations currently in flight for one script — the View's\n * mount-time catch-up.\n *\n * Filtered on the PAIR. Filtering on `filePath` alone hands a View watching\n * `repoB/stories/deck.json` the run started for `repoA`'s identically-named\n * deck, and the caller cannot discard it because the returned event would\n * carry no root either (Codex P1 / CodeRabbit on #3015).\n */\n function pendingGenerations(filePath: string, root?: string): MulmoScriptGenerationEvent[] {\n const wirePath = canonicalWirePath(filePath);\n const wanted = normalizeRoot(root);\n return [...inFlightGenerations.values()]\n .filter((entry) => entry.filePath === wirePath && entry.root === wanted)\n .map(({ kind, key }) => ({ kind, filePath: wirePath, key, done: false, ...rootField(root) }));\n }\n\n // ── Op scaffolding ────────────────────────────────────────────\n\n /**\n * Shared scaffolding for mulmoScript ops. Resolves the wire filePath,\n * builds the mulmo context, and folds unexpected handler errors into a\n * server_error failure (with a warn breadcrumb). Accepts a `deps` param\n * so unit tests can inject fakes without the full mulmocast stack.\n */\n async function runStoryOp<T>(\n filePath: string,\n options: RunStoryOpOptions<T>,\n handler: (ctx: { absoluteFilePath: string; context: StoryContext }) => Promise<OpResult<T>>,\n deps: RunStoryOpDeps = {},\n ): Promise<OpResult<T>> {\n const resolver = deps.resolveStory ?? resolveStory;\n const build = deps.buildContext ?? buildContext;\n const resolved = resolver(filePath, options.root);\n if (!resolved.ok) return resolved;\n try {\n const context = await build(resolved.absolutePath, options.force ?? false);\n if (!context) {\n if (options.onContextMissing) return options.onContextMissing();\n return opServerError(\"Failed to initialize mulmo context\");\n }\n // withMulmoErrorCapture appends the underlying provider error\n // (missing API key, quota, …) to any mulmocast failure, which\n // otherwise reaches the client as a generic \"generate error\".\n return await withMulmoErrorCapture(() => handler({ absoluteFilePath: resolved.absolutePath, context }));\n } catch (err) {\n // Log every op failure at warn so operators get a breadcrumb even\n // when the op doesn't wrap its own try/catch.\n log.warn(\"op failed\", {\n ...(options.operation ? { operation: options.operation } : {}),\n filePath,\n error: errorMessage(err),\n });\n return opServerError(errorMessage(err));\n }\n }\n\n // ── Probe ops ─────────────────────────────────────────────────\n\n async function beatImageOp(filePath: string, beatIndex: number, root?: string): Promise<OpResult<{ image: string | null }>> {\n return runStoryOp<{ image: string | null }>(filePath, { operation: \"beat-image\", root }, async ({ context }) => {\n const { imagePath } = getBeatPngImagePath(context, beatIndex);\n if (!existsSync(imagePath)) return { ok: true, image: null };\n return { ok: true, image: await fileToDataUri(imagePath, \"image/png\") };\n });\n }\n\n // beatAudio is a probe — the frontend polls it expecting `{ audio: null }`\n // when nothing has been generated yet. Override the default\n // server_error-on-context-missing so the soft-fail contract is preserved.\n async function beatAudioOp(filePath: string, beatIndex: number, root?: string): Promise<OpResult<{ audio: string | null }>> {\n return runStoryOp<{ audio: string | null }>(\n filePath,\n { operation: \"beat-audio\", root, onContextMissing: () => ({ ok: true, audio: null }) },\n async ({ context }) => {\n const beat = context.studio.script.beats[beatIndex];\n // Probe contract: a beat index the script doesn't have soft-fails\n // like a beat with nothing generated yet, never a server error.\n if (!beat) return { ok: true, audio: null };\n const audioPath = getBeatAudioPathOrUrl(beat.text ?? \"\", context, beat, context.lang);\n if (!audioPath || !existsSync(audioPath)) return { ok: true, audio: null };\n return { ok: true, audio: await fileToDataUri(audioPath, \"audio/mpeg\") };\n },\n );\n }\n\n // Probe for a beat's generated video clip. Preference order mirrors the\n // movie-assembly pipeline's \"most processed wins\": lip-synced > with\n // sound effect > raw movie clip > animated html_tailwind render. The\n // response is the \"stories/…\" wire path so the client can stream it\n // through the host's authenticated media download.\n async function beatMovieOp(filePath: string, beatIndex: number, root?: string): Promise<OpResult<{ moviePath: string | null }>> {\n return runStoryOp<{ moviePath: string | null }>(filePath, { operation: \"beat-movie\", root }, async ({ context }) => {\n const { movieFile, soundEffectFile, lipSyncFile } = getBeatMoviePaths(context, beatIndex);\n const candidates = [lipSyncFile, soundEffectFile, movieFile, getBeatAnimatedVideoPath(context, beatIndex)];\n const existing = candidates.find((candidate) => existsSync(candidate));\n return { ok: true, moviePath: existing ? toStoryRef(existing, root) : null };\n });\n }\n\n async function characterImageOp(filePath: string, key: string, root?: string): Promise<OpResult<{ image: string | null }>> {\n return runStoryOp<{ image: string | null }>(filePath, { operation: \"character-image\", root }, async ({ context }) => {\n const imagePath = getReferenceImagePath(context, key, \"png\");\n if (!existsSync(imagePath)) return { ok: true, image: null };\n return { ok: true, image: await fileToDataUri(imagePath, \"image/png\") };\n });\n }\n\n /** Shared \"output exists and is newer than the source script\" gate for\n * movie / PDF status. A stale artifact (script edited after it was\n * generated) reports null so the UI re-offers the Generate button. */\n function freshOutputRef(outputPath: string, absoluteFilePath: string, root?: string): string | null {\n if (!existsSync(outputPath)) return null;\n const outputMtime = statSync(outputPath).mtimeMs;\n const sourceMtime = statSync(absoluteFilePath).mtimeMs;\n if (outputMtime < sourceMtime) return null;\n return toStoryRef(outputPath, root);\n }\n\n async function movieStatusOp(filePath: string, root?: string): Promise<OpResult<{ moviePath: string | null }>> {\n return runStoryOp(\n filePath,\n { operation: \"movie-status\", root, onContextMissing: () => ({ ok: true, moviePath: null }) },\n async ({ absoluteFilePath, context }) => ({ ok: true, moviePath: freshOutputRef(movieFilePath(context), absoluteFilePath, root) }),\n );\n }\n\n async function pdfStatusOp(filePath: string, root?: string): Promise<OpResult<{ pdfPath: string | null }>> {\n return runStoryOp(\n filePath,\n { operation: \"pdf-status\", root, onContextMissing: () => ({ ok: true, pdfPath: null }) },\n async ({ absoluteFilePath, context }) => ({\n ok: true,\n pdfPath: freshOutputRef(pdfFilePath(context, PDF_MODE), absoluteFilePath, root),\n }),\n );\n }\n\n // ── Generation ops ────────────────────────────────────────────\n\n async function renderBeatOp(args: GenerateOpArgsWith<\"filePath\" | \"beatIndex\">): Promise<OpResult<{ image: string }>> {\n const { filePath, beatIndex, force, chatSessionId, root } = args;\n const rootGuard = guardStoryGenerationRoot(root);\n if (rootGuard) return rootGuard;\n const ffmpeg = ffmpegGuard();\n if (ffmpeg) return ffmpeg;\n\n const mapKey = String(beatIndex);\n publishGeneration(chatSessionId, \"beatImage\", filePath, mapKey, false, { root });\n let genError: string | undefined;\n try {\n const result = await runStoryOp<{ image: string }>(filePath, { force, operation: \"render-beat\", root }, async ({ context }) => {\n await generateBeatImage({\n index: beatIndex,\n context,\n ...(force ? { args: { forceImage: true } } : {}),\n });\n const { imagePath } = getBeatPngImagePath(context, beatIndex);\n if (!existsSync(imagePath)) {\n return opServerError(\"Image was not generated\");\n }\n return { ok: true, image: await fileToDataUri(imagePath, \"image/png\") };\n });\n if (!result.ok) genError = result.error;\n return result;\n } finally {\n publishGeneration(chatSessionId, \"beatImage\", filePath, mapKey, true, { error: genError, root });\n }\n }\n\n async function generateBeatAudioOp(args: GenerateOpArgsWith<\"filePath\" | \"beatIndex\">): Promise<OpResult<{ audio: string }>> {\n const { filePath, beatIndex, force, chatSessionId, root } = args;\n const rootGuard = guardStoryGenerationRoot(root);\n if (rootGuard) return rootGuard;\n const mapKey = String(beatIndex);\n publishGeneration(chatSessionId, \"beatAudio\", filePath, mapKey, false, { root });\n let genError: string | undefined;\n try {\n const result = await runStoryOp<{ audio: string }>(filePath, { force, operation: \"generate-beat-audio\", root }, async ({ context }) => {\n await generateBeatAudio(beatIndex, context, {\n settings: process.env as Record<string, string>,\n } as Parameters<typeof generateBeatAudio>[2]);\n\n const beat = context.studio.script.beats[beatIndex];\n // The generated file still wins when present, so a beat index the\n // script doesn't have only skips the path-derivation fallback and\n // lands on the \"audio was not generated\" branch below.\n const generatedFile = context.studio.beats[beatIndex]?.audioFile;\n const audioPath = generatedFile ?? (beat ? getBeatAudioPathOrUrl(beat.text ?? \"\", context, beat, context.lang) : undefined);\n\n if (!audioPath || !existsSync(audioPath)) {\n // Logic-flow failure (not an exception) — emit a targeted\n // log. Don't write raw `beat.text` into persistent logs —\n // it's free-form user content and can contain sensitive\n // data.\n log.error(\"audio was not generated\", {\n beatIndex,\n audioPath,\n exists: audioPath ? existsSync(audioPath) : false,\n beatTextLength: typeof beat?.text === \"string\" ? beat.text.length : 0,\n audioFilePresent: Boolean(context.studio.beats[beatIndex]?.audioFile),\n });\n return opServerError(\"Audio was not generated\");\n }\n return { ok: true, audio: await fileToDataUri(audioPath, \"audio/mpeg\") };\n });\n if (!result.ok) genError = result.error;\n return result;\n } finally {\n publishGeneration(chatSessionId, \"beatAudio\", filePath, mapKey, true, { error: genError, root });\n }\n }\n\n async function renderCharacterOp(args: GenerateOpArgsWith<\"filePath\" | \"key\">): Promise<OpResult<{ image: string }>> {\n const { filePath, key, force, chatSessionId, root } = args;\n const rootGuard = guardStoryGenerationRoot(root);\n if (rootGuard) return rootGuard;\n publishGeneration(chatSessionId, \"characterImage\", filePath, key, false, { root });\n let genError: string | undefined;\n try {\n const result = await runStoryOp<{ image: string }>(filePath, { force, operation: \"render-character\", root }, async ({ context }) => {\n // `imageEntries` (not `images`) to avoid shadowing mulmocast's\n // imported `images()` pipeline stage.\n const imageEntries = context.studio.script.imageParams?.images ?? {};\n const imageEntry = imageEntries[key];\n if (!imageEntry || imageEntry.type !== \"imagePrompt\") {\n return opBadRequest(`No imagePrompt entry for key: ${key}`);\n }\n\n const index = Object.keys(imageEntries).indexOf(key);\n const imagePath = getReferenceImagePath(context, key, \"png\");\n mkdirSync(path.dirname(imagePath), { recursive: true });\n\n await generateReferenceImage({\n context,\n key,\n index,\n image: imageEntry as MulmoImagePromptMedia,\n ...(force !== undefined ? { force } : {}),\n });\n if (!existsSync(imagePath)) {\n return opServerError(\"Character image was not generated\");\n }\n return { ok: true, image: await fileToDataUri(imagePath, \"image/png\") };\n });\n if (!result.ok) genError = result.error;\n return result;\n } finally {\n publishGeneration(chatSessionId, \"characterImage\", filePath, key, true, { error: genError, root });\n }\n }\n\n // ── Upload ops ────────────────────────────────────────────────\n\n async function uploadBeatImageOp(filePath: string, beatIndex: number, imageData: string, root?: string): Promise<OpResult<{ image: string }>> {\n // No root guard: the write is already in the right root. `runStoryOp`\n // resolves through `resolveStory` — realpath containment, per root — and\n // hands the executor an ABSOLUTE path, from which `buildContext` derives\n // its `basedir`. The guard here was added defensively during #3015's\n // review and refused a write that was never wrong (#3019).\n return runStoryOp<{ image: string }>(filePath, { operation: \"upload-beat-image\", root }, async ({ context }) => {\n const { imagePath } = getBeatPngImagePath(context, beatIndex);\n // writeFileAtomic creates parent dirs and prevents a half-\n // written PNG from surviving a crash mid-write (#881 v2).\n const base64 = stripDataUri(imageData);\n await backend.writeFileAtomic(imagePath, Buffer.from(base64, \"base64\"));\n return { ok: true, image: await fileToDataUri(imagePath, \"image/png\") };\n });\n }\n\n async function uploadCharacterImageOp(filePath: string, key: string, imageData: string, root?: string): Promise<OpResult<{ image: string }>> {\n // No root guard: the write is already in the right root. `runStoryOp`\n // resolves through `resolveStory` — realpath containment, per root — and\n // hands the executor an ABSOLUTE path, from which `buildContext` derives\n // its `basedir`. The guard here was added defensively during #3015's\n // review and refused a write that was never wrong (#3019).\n return runStoryOp<{ image: string }>(filePath, { operation: \"upload-character-image\", root }, async ({ context }) => {\n const imagePath = getReferenceImagePath(context, key, \"png\");\n const base64 = stripDataUri(imageData);\n await backend.writeFileAtomic(imagePath, Buffer.from(base64, \"base64\"));\n return { ok: true, image: await fileToDataUri(imagePath, \"image/png\") };\n });\n }\n\n // ── Movie / PDF pipelines ─────────────────────────────────────\n\n // Per-instance dedup so a foreground call (SSE route or long-held\n // dispatch) and a fire-and-forget background call can't race on the same\n // script. Keyed by the realpath (absoluteFilePath) so two different wire\n // spellings of the same file still collide. Process-local — a\n // multi-process deployment would need an external lock; out of scope.\n const inFlightMovies = new Set<string>();\n\n // Same dedup model as inFlightMovies, scoped to PDF generation\n // (#1614). PDFs and movies don't share the lock — they write to\n // different output files and can safely run in parallel.\n const inFlightPdfs = new Set<string>();\n\n // Shared core for the SSE-streaming route, the long-held dispatch op, and\n // the fire-and-forget background path triggered by `autoGenerateMovie`.\n // Builds the mulmo context, runs audio→images→movie, and reports\n // per-beat progress through the supplied callback. Throws on\n // unexpected pipeline errors; returns a structured failure when the\n // pipeline runs to completion but the output file is missing.\n async function runMovieGeneration(absoluteFilePath: string, onProgressEvent: (event: MovieProgressEvent) => void): Promise<MovieGenerationResult> {\n return withMulmoErrorCapture(() => runMoviePipeline(absoluteFilePath, onProgressEvent));\n }\n\n async function runMoviePipeline(absoluteFilePath: string, onProgressEvent: (event: MovieProgressEvent) => void): Promise<MovieGenerationResult> {\n const context = await buildContext(absoluteFilePath);\n if (!context) return { ok: false, error: \"Failed to initialize mulmo context\" };\n\n return withBeatProgress(\n context.studio.script.beats as MulmoBeat[],\n (sessionType, beatIndex) => {\n if (sessionType !== \"image\" && sessionType !== \"audio\") return;\n onProgressEvent({ kind: sessionType, beatIndex });\n },\n async () => {\n // Order matters: audio() must run before images(). For html_tailwind\n // beats with `animation: true`, mulmocast only emits the per-beat\n // `_animated.mp4` when the beat's duration is already known (see\n // processHtmlTailwindAnimated in mulmocast). Durations are populated\n // by audio(), so running images() first leaves the .mp4 files\n // missing and movie() then fails in validateBeatSource.\n const audioContext = await audio(context);\n const imagesContext = await images(audioContext);\n await movie(imagesContext);\n\n const outputPath = movieFilePath(imagesContext);\n if (!existsSync(outputPath)) return { ok: false, error: \"Movie was not generated\" };\n return { ok: true, outputPath };\n },\n );\n }\n\n /**\n * Long-held foreground movie generation (the package View's\n * `generateMovie` dispatch). Resolves when the whole pipeline finishes.\n * Per-beat completions are mirrored to the generation channels so the\n * initiating View (and any other mounted View) reloads assets off disk\n * as they land — the successor of the SSE per-beat events.\n */\n async function generateMovieOp(filePath: string, chatSessionId: string | undefined, root?: string): Promise<OpResult<{ moviePath: string }>> {\n const rootGuard = guardStoryGenerationRoot(root);\n if (rootGuard) return rootGuard;\n const ffmpeg = ffmpegGuard();\n if (ffmpeg) return ffmpeg;\n const resolved = resolveStory(filePath, root);\n if (!resolved.ok) return resolved;\n const absoluteFilePath = resolved.absolutePath;\n\n if (inFlightMovies.has(absoluteFilePath)) {\n return opBadRequest(\"Movie generation is already in progress for this script\");\n }\n\n inFlightMovies.add(absoluteFilePath);\n publishGeneration(chatSessionId, \"movie\", filePath, \"\", false, { root });\n let genError: string | undefined;\n try {\n const result = await runMovieGeneration(absoluteFilePath, (event) => {\n const eventKind = event.kind === \"image\" ? \"beatImage\" : \"beatAudio\";\n publishGeneration(chatSessionId, eventKind, filePath, String(event.beatIndex), true, { root });\n });\n if (!result.ok) {\n genError = result.error;\n return opServerError(result.error);\n }\n const movieRef = toStoryRef(result.outputPath, root);\n if (movieRef === null) return opServerError(\"generated movie is outside the registered stories root\");\n return { ok: true, moviePath: movieRef };\n } catch (err) {\n genError = errorMessage(err);\n return opServerError(genError);\n } finally {\n inFlightMovies.delete(absoluteFilePath);\n publishGeneration(chatSessionId, \"movie\", filePath, \"\", true, { error: genError, root });\n }\n }\n\n function triggerAutoBackgroundMovie(absoluteFilePath: string, wireFilePath: string, chatSessionId: string | undefined, root?: string): void {\n // The same refusal as the foreground generation ops, reached the only way\n // a `void` entry point can reach it. This one takes a `root` but never\n // returned an `OpResult`, so the guard that covers every other generation\n // did not cover the one generation nobody is waiting on — which is the\n // worse half: a detached run corrupting the other root's pending state\n // has no caller to see the failure (Codex P1 on #3015).\n if (guardStoryGenerationRoot(root)) {\n log.warn(\"refused an auto background movie in a non-default stories root\", { filePath: wireFilePath, root });\n return;\n }\n if (inFlightMovies.has(absoluteFilePath)) return;\n inFlightMovies.add(absoluteFilePath);\n void runBackgroundMovieGeneration(absoluteFilePath, wireFilePath, chatSessionId, root);\n }\n\n // Detached movie generation. Reports progress through the generation\n // channels the View watches — so a user opening the canvas\n // mid-generation sees spinners, and a user opening it after completion\n // sees the finished movie loaded from disk by the View's normal\n // mount-time path. Errors are persisted to a `<filename>.error.txt`\n // sidecar next to the script (no synchronous client to alert); any\n // stale sidecar from a previous run is cleared on each new attempt.\n // Triggered server-side from the unified save route when the caller\n // passes `autoGenerateMovie: true`.\n async function runBackgroundMovieGeneration(absoluteFilePath: string, wireFilePath: string, chatSessionId: string | undefined, root?: string): Promise<void> {\n const errorSidecarPath = `${absoluteFilePath}.error.txt`;\n // Clear stale error from a previous failed run before starting; if it\n // doesn't exist that's fine. Catch any unexpected fs errors silently —\n // the worst case is the user sees an out-of-date error file later.\n try {\n unlinkSync(errorSidecarPath);\n } catch {\n // intentional: ENOENT is the common case, others non-fatal\n }\n\n publishGeneration(chatSessionId, \"movie\", wireFilePath, \"\", false, { root });\n let genError: string | undefined;\n try {\n const result = await runMovieGeneration(absoluteFilePath, (event) => {\n // Mirror per-beat completions through the generation channels so\n // subscribed Views reload the asset off disk. We fire start+finish\n // in two ticks — `setImmediate` lets the session SSE writer flush\n // the start event before the finish removes the entry, otherwise\n // Vue's batched reactivity could see a net \"no change\" and skip\n // the reload.\n const eventKind = event.kind === \"image\" ? \"beatImage\" : \"beatAudio\";\n const key = String(event.beatIndex);\n publishGeneration(chatSessionId, eventKind, wireFilePath, key, false, { root });\n setImmediate(() => publishGeneration(chatSessionId, eventKind, wireFilePath, key, true, { root }));\n });\n\n if (!result.ok) {\n genError = result.error;\n await writeErrorSidecar(errorSidecarPath, result.error);\n log.warn(\"background movie generation failed\", { filePath: wireFilePath, error: result.error });\n return;\n }\n log.info(\"background movie generation done\", {\n filePath: wireFilePath,\n outputPath: result.outputPath,\n });\n } catch (err) {\n genError = errorMessage(err);\n await writeErrorSidecar(errorSidecarPath, genError);\n log.error(\"background movie generation crashed\", { filePath: wireFilePath, error: genError });\n } finally {\n inFlightMovies.delete(absoluteFilePath);\n publishGeneration(chatSessionId, \"movie\", wireFilePath, \"\", true, { error: genError, root });\n }\n }\n\n // Atomic write so a crash mid-write can't leave a truncated sidecar.\n async function writeErrorSidecar(errorSidecarPath: string, message: string): Promise<void> {\n try {\n await backend.writeFileAtomic(errorSidecarPath, message);\n } catch (writeErr) {\n log.error(\"failed to write error sidecar\", {\n errorSidecarPath,\n error: errorMessage(writeErr),\n });\n }\n }\n\n // ── PDF (#1614) ───────────────────────────────────────────────\n\n // Shared core for the SSE-streaming route and the long-held dispatch op.\n // Mirrors the movie pipeline's per-beat progress reporting so the UI can\n // light spinners during the image pass; the PDF action itself doesn't\n // emit progress events, so only image events are forwarded. Returns a\n // structured failure when the pipeline completes but the output file is\n // missing.\n async function runPdfGeneration(context: StoryContext, onImageBeatDone: (beatIndex: number) => void): Promise<PdfGenerationResult> {\n return withMulmoErrorCapture(() => runPdfPipeline(context, onImageBeatDone));\n }\n\n async function runPdfPipeline(context: StoryContext, onImageBeatDone: (beatIndex: number) => void): Promise<PdfGenerationResult> {\n return withBeatProgress(\n context.studio.script.beats as MulmoBeat[],\n (sessionType, beatIndex) => {\n if (sessionType !== \"image\") return;\n onImageBeatDone(beatIndex);\n },\n async () => {\n const imagesContext = await images(context);\n await pdf(imagesContext, PDF_MODE, PDF_SIZE);\n const outputPath = pdfFilePath(imagesContext, PDF_MODE);\n if (!existsSync(outputPath)) return { ok: false, error: \"PDF was not generated\" };\n return { ok: true, outputPath };\n },\n );\n }\n\n /** Long-held foreground PDF generation (the package View's `generatePdf`\n * dispatch) — the PDF sibling of `generateMovieOp`. */\n async function generatePdfOp(filePath: string, chatSessionId: string | undefined, root?: string): Promise<OpResult<{ pdfPath: string }>> {\n const rootGuard = guardStoryGenerationRoot(root);\n if (rootGuard) return rootGuard;\n const ffmpeg = ffmpegGuard();\n if (ffmpeg) return ffmpeg;\n const resolved = resolveStory(filePath, root);\n if (!resolved.ok) return resolved;\n const absoluteFilePath = resolved.absolutePath;\n\n if (inFlightPdfs.has(absoluteFilePath)) {\n return opBadRequest(\"PDF generation is already in progress for this script\");\n }\n\n inFlightPdfs.add(absoluteFilePath);\n publishGeneration(chatSessionId, \"pdf\", filePath, \"\", false, { root });\n let genError: string | undefined;\n try {\n const context = await buildContext(absoluteFilePath);\n if (!context) {\n genError = \"Failed to initialize mulmo context\";\n return opServerError(genError);\n }\n const result = await runPdfGeneration(context, (beatIndex) => {\n publishGeneration(chatSessionId, \"beatImage\", filePath, String(beatIndex), true, { root });\n });\n if (!result.ok) {\n genError = result.error;\n return opServerError(result.error);\n }\n const pdfRef = toStoryRef(result.outputPath, root);\n if (pdfRef === null) return opServerError(\"generated PDF is outside the registered stories root\");\n return { ok: true, pdfPath: pdfRef };\n } catch (err) {\n genError = errorMessage(err);\n return opServerError(genError);\n } finally {\n inFlightPdfs.delete(absoluteFilePath);\n publishGeneration(chatSessionId, \"pdf\", filePath, \"\", true, { error: genError, root });\n }\n }\n\n return {\n backend,\n toStoryRef,\n resolveStory,\n guardStoryWirePath,\n guardStoryRootRegistered,\n guardStoryWriteRoot,\n artifactsForRoot,\n guardStoryGenerationRoot,\n ffmpegGuard,\n runStoryOp,\n publishGeneration,\n publishScriptChanged,\n pendingGenerations,\n beatImageOp,\n beatAudioOp,\n beatMovieOp,\n characterImageOp,\n movieStatusOp,\n pdfStatusOp,\n renderBeatOp,\n generateBeatAudioOp,\n renderCharacterOp,\n uploadBeatImageOp,\n uploadCharacterImageOp,\n inFlightMovies,\n inFlightPdfs,\n runMovieGeneration,\n runPdfGeneration,\n generateMovieOp,\n generatePdfOp,\n triggerAutoBackgroundMovie,\n };\n}\n\nexport type MulmoScriptServerOps = ReturnType<typeof createMulmoScriptServerOps>;\n","// The mulmoScript dispatch router, moved from MulmoClaude's\n// `server/plugins/mulmoscript-builtin.ts` in phase 3 so every host serves\n// the package View's `useRuntime().dispatch({ kind, … })` calls with the\n// SAME kind routing and validation. Hosts register the returned handler on\n// their dispatch channel (MulmoClaude: `registerBuiltinDispatch`;\n// MulmoTerminal: its `/api/plugin` interception).\n//\n// Response contract: every kind resolves to an `{ ok: … }` envelope (see\n// `../core/contract.ts`) — business failures are data, not thrown errors,\n// so user-facing messages stay free of transport prefixes.\n\nimport { executeMulmoScriptSave, executeUpdateBeat, executeUpdateScript, type MulmoScriptFailure } from \"../core/plugin\";\nimport { DEFAULT_ROOT, normalizeRoot } from \"../core/contract\";\nimport type { MulmoScriptExecuteContext } from \"../core/types\";\nimport type { MulmoScriptServerOps } from \"./ops\";\nimport { isRecord } from \"./support\";\nimport type { OpFailure } from \"./types\";\n\ninterface DispatchFailure {\n ok: false;\n code: \"bad_request\" | \"not_found\" | \"server_error\";\n error: string;\n}\n\nfunction fromOpFailure(failure: OpFailure): DispatchFailure {\n // \"unavailable\" (ffmpeg missing) has no slot in the contract's code\n // union — the View only reads `error`, so fold it into server_error\n // rather than widening the shared contract for one case.\n const code = failure.code === \"unavailable\" ? \"server_error\" : failure.code;\n return { ok: false, code, error: failure.error };\n}\n\nfunction fromPackageFailure(failure: MulmoScriptFailure): DispatchFailure {\n return { ok: false, code: failure.code, error: failure.error };\n}\n\nfunction invalidArgs(kind: string): DispatchFailure {\n return { ok: false, code: \"bad_request\", error: `invalid arguments for mulmoScript dispatch kind \"${kind}\"` };\n}\n\nfunction str(value: unknown): string | undefined {\n return typeof value === \"string\" && value !== \"\" ? value : undefined;\n}\n\n// Beat indexes must be non-negative integers — reject `-1` / `1.5` at the\n// dispatch boundary so invalid client input surfaces as a deterministic\n// bad_request instead of leaking into beat-indexed ops.\nfunction num(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isInteger(value) && value >= 0 ? value : undefined;\n}\n\ninterface BeatArgs {\n filePath: string;\n beatIndex: number;\n}\n\ninterface KeyArgs {\n filePath: string;\n key: string;\n}\n\n/** Pass ok results through untouched; normalize failures for the wire. */\nfunction envelope<T>(result: ({ ok: true } & T) | OpFailure): ({ ok: true } & T) | DispatchFailure {\n return result.ok ? result : fromOpFailure(result);\n}\n\nfunction beatArgs(args: Record<string, unknown>): BeatArgs | null {\n const filePath = str(args.filePath);\n const beatIndex = num(args.beatIndex);\n if (!filePath || beatIndex === undefined) return null;\n return { filePath, beatIndex };\n}\n\nfunction keyArgs(args: Record<string, unknown>): KeyArgs | null {\n const filePath = str(args.filePath);\n const key = str(args.key);\n if (!filePath || !key) return null;\n return { filePath, key };\n}\n\nconst PROBE_KINDS = new Set([\"beatImage\", \"beatAudio\", \"beatMovie\", \"characterImage\", \"movieStatus\", \"pdfStatus\"]);\nconst GENERATE_KINDS = new Set([\"renderBeat\", \"generateBeatAudio\", \"renderCharacter\", \"generateMovie\", \"generatePdf\"]);\nconst UPLOAD_KINDS = new Set([\"uploadBeatImage\", \"uploadCharacterImage\"]);\n\nexport type MulmoScriptDispatchHandler = (args: Record<string, unknown>) => Promise<unknown>;\n\n/**\n * Build the kind router over an ops instance. The save / reopen / update\n * kinds run the phase-1 core executes against the backend's artifacts\n * FileOps, guarded by the instance's realpath containment\n * (`guardStoryWirePath`) — the core's own guard is lexical.\n */\n/**\n * Stamp the root a successful result acted in.\n *\n * Only on success: a failure carries `code` and `error`, and adding a root to\n * it would invite a reader to treat the pair as addressable when the call did\n * not happen. Only when NON-default, so a result for a call that named no root\n * stays byte-identical to what this package returned before roots existed —\n * which is what keeps every existing card working untouched.\n */\nfunction withRoot(result: unknown, root: string | undefined): unknown {\n const normalized = normalizeRoot(root);\n if (normalized === DEFAULT_ROOT) return result;\n if (!isRecord(result) || result.ok !== true) return result;\n return { ...result, root: normalized };\n}\n\n/** `undefined` when `root` is absent or a string; a failure envelope otherwise. */\nfunction guardSuppliedRoot(root: unknown): { ok: false; code: string; error: string } | undefined {\n if (root === undefined || typeof root === \"string\") return undefined;\n return { ok: false, code: \"bad_request\", error: `mulmoScript root must be a string, got ${typeof root}` };\n}\n\nexport function createMulmoScriptDispatchHandler(ops: MulmoScriptServerOps): MulmoScriptDispatchHandler {\n /**\n * The executor context for a write, bound to the root it names.\n *\n * One `FileOps` was held for the whole handler, so the executors — which\n * take a WIRE path (`stories/…`) and resolve it against whatever FileOps\n * they are given — wrote a named root's script into the DEFAULT root's\n * identically-named file. Choosing here keeps `MulmoScriptExecuteContext`\n * and every executor unchanged: the root never reaches them, only the right\n * FileOps does (#3019).\n */\n function executeContextFor(root: string | undefined): MulmoScriptExecuteContext | null {\n const artifacts = ops.artifactsForRoot(root);\n return artifacts === null ? null : { files: { artifacts } };\n }\n\n async function saveKind(args: Record<string, unknown>): Promise<unknown> {\n // Which root this write lands in — see `guardStoryWriteRoot` and\n // `executeContextFor`.\n const rootGuard = ops.guardStoryWriteRoot(str(args.root));\n if (rootGuard) return fromOpFailure(rootGuard);\n const guard = ops.guardStoryWirePath(args.filePath, str(args.root));\n if (guard) return fromOpFailure(guard);\n const context = executeContextFor(str(args.root));\n if (context === null) return invalidArgs(\"save\");\n const outcome = await executeMulmoScriptSave(context, {\n script: args.script,\n filename: str(args.filename),\n filePath: str(args.filePath),\n });\n if (!outcome.ok) return fromPackageFailure(outcome);\n return { ok: true, script: outcome.script, filePath: outcome.filePath, message: outcome.message };\n }\n\n async function updateKind(kind: \"updateBeat\" | \"updateScript\", args: Record<string, unknown>): Promise<unknown> {\n const rootGuard = ops.guardStoryWriteRoot(str(args.root));\n if (rootGuard) return fromOpFailure(rootGuard);\n const guard = ops.guardStoryWirePath(args.filePath, str(args.root));\n if (guard) return fromOpFailure(guard);\n const context = executeContextFor(str(args.root));\n if (context === null) return invalidArgs(kind);\n const outcome = kind === \"updateBeat\" ? await executeUpdateBeat(context, args) : await executeUpdateScript(context, args);\n if (!outcome.ok) return fromPackageFailure(outcome);\n // After the write landed, never before: a View that reloads on a failed write would\n // discard the user's edit and show the old file back.\n ops.publishScriptChanged(str(args.filePath) ?? \"\", str(args.origin), str(args.root));\n return { ok: true };\n }\n\n const STATUS_OPS = { movieStatus: ops.movieStatusOp, pdfStatus: ops.pdfStatusOp } as const;\n const BEAT_PROBE_OPS = { beatImage: ops.beatImageOp, beatAudio: ops.beatAudioOp, beatMovie: ops.beatMovieOp } as const;\n\n async function probeKind(kind: string, args: Record<string, unknown>): Promise<unknown> {\n const statusOp = STATUS_OPS[kind as keyof typeof STATUS_OPS];\n if (statusOp) {\n const filePath = str(args.filePath);\n return filePath ? envelope(await statusOp(filePath, str(args.root))) : invalidArgs(kind);\n }\n if (kind === \"characterImage\") {\n const parsed = keyArgs(args);\n return parsed ? envelope(await ops.characterImageOp(parsed.filePath, parsed.key, str(args.root))) : invalidArgs(kind);\n }\n const parsed = beatArgs(args);\n if (!parsed) return invalidArgs(kind);\n return envelope(await BEAT_PROBE_OPS[kind as keyof typeof BEAT_PROBE_OPS](parsed.filePath, parsed.beatIndex, str(args.root)));\n }\n\n /** Movie and PDF take the whole script; the other generate kinds take a beat\n * or a character within it. */\n async function wholeScriptGenerationKind(kind: \"generateMovie\" | \"generatePdf\", args: Record<string, unknown>): Promise<unknown> {\n const filePath = str(args.filePath);\n if (!filePath) return invalidArgs(kind);\n const chatSessionId = str(args.chatSessionId);\n const root = str(args.root);\n const result = kind === \"generateMovie\" ? await ops.generateMovieOp(filePath, chatSessionId, root) : await ops.generatePdfOp(filePath, chatSessionId, root);\n return envelope(result);\n }\n\n async function generateKind(kind: string, args: Record<string, unknown>): Promise<unknown> {\n if (kind === \"generateMovie\" || kind === \"generatePdf\") return wholeScriptGenerationKind(kind, args);\n const chatSessionId = str(args.chatSessionId);\n const root = str(args.root);\n const force = args.force === true;\n if (kind === \"renderCharacter\") {\n const parsed = keyArgs(args);\n return parsed ? envelope(await ops.renderCharacterOp({ ...parsed, force, chatSessionId, root })) : invalidArgs(kind);\n }\n const parsed = beatArgs(args);\n if (!parsed) return invalidArgs(kind);\n const result =\n kind === \"renderBeat\"\n ? await ops.renderBeatOp({ ...parsed, force, chatSessionId, root })\n : await ops.generateBeatAudioOp({ ...parsed, force, chatSessionId, root });\n return envelope(result);\n }\n\n async function uploadKind(kind: string, args: Record<string, unknown>): Promise<unknown> {\n const imageData = str(args.imageData);\n if (!imageData) return invalidArgs(kind);\n if (kind === \"uploadCharacterImage\") {\n const parsed = keyArgs(args);\n return parsed ? envelope(await ops.uploadCharacterImageOp(parsed.filePath, parsed.key, imageData, str(args.root))) : invalidArgs(kind);\n }\n const parsed = beatArgs(args);\n if (!parsed) return invalidArgs(kind);\n return envelope(await ops.uploadBeatImageOp(parsed.filePath, parsed.beatIndex, imageData, str(args.root)));\n }\n\n async function pendingKind(kind: string, args: Record<string, unknown>): Promise<unknown> {\n const filePath = str(args.filePath);\n if (!filePath) return invalidArgs(kind);\n const root = str(args.root);\n // An empty snapshot for an unknown root is indistinguishable from \"no\n // work is running\" — see `guardStoryRootRegistered`.\n const rootGuard = ops.guardStoryRootRegistered(root);\n if (rootGuard) return fromOpFailure(rootGuard);\n return { ok: true, pending: ops.pendingGenerations(filePath, root) };\n }\n\n // Nothing but routing below: every kind resolves to one named handler, so\n // reading it answers \"where does this kind go\" without also having to read\n // what any of them do.\n /** Which handler serves this kind. Routing only — the caller tags the answer. */\n async function route(kind: string, args: Record<string, unknown>): Promise<unknown> {\n if (kind === \"save\") return saveKind(args);\n if (kind === \"updateBeat\" || kind === \"updateScript\") return updateKind(kind, args);\n if (PROBE_KINDS.has(kind)) return probeKind(kind, args);\n if (GENERATE_KINDS.has(kind)) return generateKind(kind, args);\n if (UPLOAD_KINDS.has(kind)) return uploadKind(kind, args);\n if (kind === \"pendingGenerations\") return pendingKind(kind, args);\n return { ok: false, code: \"bad_request\", error: `unknown mulmoScript dispatch kind \"${kind}\"` };\n }\n\n return async (args: Record<string, unknown>): Promise<unknown> => {\n const kind = str(args.kind);\n if (!kind) return invalidArgs(\"<missing>\");\n // Once, at the only entry, so no per-kind reader can forget it. `str()`\n // answers `undefined` for a number, `null`, an object — indistinguishable\n // from a root that was never supplied, which every reader below then takes\n // as the DEFAULT root. A host that serialises a root wrongly would have\n // written to, and read from, the default root's identically-named script\n // while believing it named another (Codex P2 on #3015). Absent stays\n // default; present must be a string.\n const malformedRoot = guardSuppliedRoot(args.root);\n if (malformedRoot) return malformedRoot;\n // Tagged HERE, once, rather than by each of the seventeen kinds. A host\n // builds its cards from these results and a card's identity is the pair\n // `(root, filePath)`, so a kind that forgot the tag would quietly collapse\n // two repositories' identically-named decks onto one card. Threading it\n // per-kind is exactly the shape #3015 got wrong over and over.\n return withRoot(await route(kind, args), str(args.root));\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mulmoclaude/mulmoscript-plugin",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.2",
|
|
4
4
|
"description": "presentMulmoScript — MulmoScript storyboard tool for MulmoClaude and MulmoTerminal. Browser-safe core (tool definition + save/reopen/update logic against the generic gui-chat-protocol files.artifacts capability) on `.`, Vue View/Preview on `./vue`, and the Node-only ops layer (mulmocast render/movie/PDF orchestration + dispatch router, host backends injected) on `./server`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|