| name | preview-runtime-stability |
| description | Diagnose and fix managed preview startup failures for TanStack Start or Vite apps in UNS-SWE executor sandboxes. Use when the user reports "Failed to load preview", "Preview loading timed out", "dev server exited: exit status 1", `{"status":500,"unhandled":true,"message":"HTTPError"}`, Tier0 SDK SSR/noExternal issues, preview_start, preview/status, live preview, Vite dev server, port 5173, /api/health, stale npm/node processes, sandbox preview process cleanup problems, missing node_modules or uninstalled dependencies, npm install/npm ci dependency restore (e.g. uploaded projects shipped without node_modules), or frequent full page reloads / broken HMR / React Fast Refresh / Vite optimizeDeps re-optimization during development. |
Preview Runtime Stability
Purpose
Use this skill when the user says Failed to load preview, Preview loading timed out, or when a managed preview is stuck, repeatedly times out, or reports port conflicts such as Port 5173 is already in use. The goal is to make preview start/stop idempotent inside one executor Pod.
This skill also covers two adjacent runtime concerns: restoring dependencies when an uploaded project ships without node_modules (the preview cannot start until they are installed), and fixing frequent full page reloads (broken HMR) during development.
Core Diagnosis
A preview failure is usually not an app build problem when all are true:
npm install or dependency restore completed successfully.
npm run build succeeds.
- Preview logs include
Port 5173 is in use or trying another one.
- The platform keeps probing the declared preview port, usually
5173.
Likely root cause: an old npm -> sh -> node/vite process chain survived preview stop/restart and still owns the declared port.
Preview Port Contract
For TanStack Start or Vite apps in UNS-SWE templates, keep the preview/dev server on port 5173 consistently.
- Set
[[services]].localPort = 5173 in artifact.toml when the service is used for managed preview/runtime in this template.
- Set
[services.preview].port = 5173.
- Ensure
[services.preview].args starts dev with --host 0.0.0.0 --port 5173.
- Ensure
package.json scripts.dev starts Vite/TanStack on port 5173, for example vite dev --host 0.0.0.0 --port 5173.
- Do not leave preview on
3000, 3001, or allow Vite to drift to 5174/5175; keep artifact.toml and package.json synchronized.
Known Error Playbooks
Dependencies not installed (missing or partial node_modules)
Error examples:
Cannot find module 'react' (or '@tanstack/react-start', 'vite', ...)
sh: vite: command not found
Failed to resolve import "..." — the package is not installed
This happens when an uploaded or imported project ships without node_modules (common when a user uploads a zip that excludes dependencies). The Core Diagnosis assumes dependency restore already succeeded; if it did not, no port/health work will help — install first. node_modules can also be partial after an interrupted install, so treat a missing node_modules/.bin/vite or any startup Cannot find module as "restore dependencies".
Detect:
[ -d node_modules ] && [ -x node_modules/.bin/vite ] && echo "deps present" || echo "deps missing/partial"
Fix sequence:
- Confirm you are at the project root (the directory holding
package.json).
- If a managed install is already running, wait for it to finish; do not start a second install that writes
node_modules concurrently.
- Restore dependencies: run
npm ci when package-lock.json exists (clean, lockfile-exact), otherwise npm install.
- Always restore before
npm run build or starting preview; build/preview keeps failing until install completes.
- After install succeeds, run the preflight and start preview normally.
[ -f package-lock.json ] && npm ci || npm install
Dev server exited with status 1
Error examples:
dev server exited: exit status 1
error when starting dev server:
Error: Port 5173 is already in use
Treat exit status 1 as "Vite crashed before readiness", not as a generic preview timeout. Always inspect preview logs or the dev server stderr before editing app code.
Common causes, in order:
- Stale preview process still owns port
5173. This is the most common cause when vite.config.ts uses server.strictPort = true; Vite exits immediately instead of drifting to 5174.
- Route tree or file-route compile error after adding, renaming, or deleting files under
src/routes.
- Missing dependency or stale import after removing component libraries, for example imports from deleted
@/components/ui or @/components/mes.
- CSS/Tailwind startup failure, especially stale
@import entries for deleted packages.
- Server-only imports leaking into client-bundled route/component files, such as using
@tanstack/react-start/server outside a server handler, middleware, or createServerFn().handler(...) body.
Fix sequence:
- Check preview logs. If the log contains
Port 5173 is already in use, run the preflight cleanup and restart preview. Do not change application code for a stale-port failure.
- If the log shows a route-generation or import error, fix the exact file path in the stack trace, then restart preview.
- If the log shows a deleted UI/MES component import, replace it with scaffold-native Tailwind/local components. Do not restore the removed component library unless the user explicitly asks.
- If the log shows a CSS import error, remove the stale import from
src/styles/globals.css or add the missing package only when it is intentionally part of the template.
- If the log shows
does not provide an export named from @tanstack/react-start/server, move that call inside a valid server boundary.
- When logs are unavailable or ambiguous, first verify port ownership and
/api/health; then run npm run build only if dependencies are installed and the managed install is not running.
Useful checks:
PREVIEW_PORT=5173 HEALTH_PATH=/api/health ./.agents/skills/preview-runtime-stability/scripts/preview_preflight.sh --cleanup
rg -n '@/components/(ui|mes)' src --glob '!routeTree.gen.ts'
rg -n '@tanstack/react-start/server|from "motion/react"' src --glob '!routeTree.gen.ts'
rg -n '@import "shadcn|@import "@/components|@import "@tier0' src/styles
The second command intentionally over-reports. @tanstack/react-start/server is allowed in server-route handlers, middleware, src/lib/auth.ts, and inside createServerFn().handler(...) bodies. motion/react is allowed only in the shared src/lib/motion.ts wrapper.
Tier0 SDK SSR external mismatch
Error examples:
Named export ... not found
does not provide an export named ...
require is not defined in ES module scope
ReferenceError: exports is not defined in ES module scope
When the stack points at @tier0/sdk, @tier0/sdk/mq, or mqtt during SSR,
separate SDK package format failures from app-code eager loading failures. The
current template uses @tier0/sdk@0.1.3, which ships dual ESM/CJS output. It
still must remain external in SSR and be loaded from server code through
@/lib/tier0 so optional platform I/O does not run during page initialization.
Fix sequence:
- Open
vite.config.ts.
- Keep
pg external because it is a Node/Postgres runtime dependency.
- Ensure SDK packages are externalized for Vite SSR:
ssr: {
external: ["pg", "@tier0/sdk", "mqtt"],
}
- Do not add
@tier0/sdk or mqtt to ssr.noExternal. Let package exports
choose the correct runtime condition instead of forcing Vite to rebundle
SDK internals.
- Search for top-level SDK imports in app services, route loaders, pages, or
generated wrappers:
rg -n '@/lib/tier0|@tier0/sdk/openapi|@tier0/sdk/mq' src/services src/routes src/components src/lib
- If an optional operation such as UNS dispatch, Flow publish, or MQTT command
send imports the SDK at module top level, move the SDK call behind
@/lib/tier0 lazy loaders and invoke it only inside the action that needs
platform I/O. Top-level imports of lazy helper functions from @/lib/tier0
are fine; top-level calls to those helpers are not. Do not import SDK values
while rendering a page or loading preview.
- Do not replace the SDK with fallback MQTT clients or hand-written UNS/Flow
fetch wrappers.
- Run
npm run build:check when available.
TanStack Start HTTPError 500
Error example:
{"status":500,"unhandled":true,"message":"HTTPError"}
This means an HTTP-shaped error escaped through TanStack Start/Router instead of being translated into a normal Response.json(...). It is usually not fixed by restarting the preview process.
Common causes:
- An API route calls
requireAuth() or a service that throws HttpError, but the handler is not wrapped in withErrors(...).
- A service or loader throws
new HttpError(...) from a route beforeLoad, loader, or createServerFn where no route error mapper converts it to a Response.
- Preview auth headers/session are missing or invalid, so authenticated routes hit an auth error before the UI can render.
PREVIEW_USER_ROLE does not exist in PERMISSION_MATRIX, so the middleware redirects to /login or auth checks fail unexpectedly.
- A public endpoint needed by preview, especially
/api/health, was changed to require auth or throw.
Fix sequence:
- Identify the request path that returned the JSON error. Do not assume it is
/api/health.
- For
src/routes/api/**, wrap every handler in withErrors(...) and keep the route body thin: auth, parse, service call, Response.json.
- Keep
HttpError throwing in services and route handlers only when a withErrors boundary will catch it. In page loaders or createServerFn calls, return null, throw redirect(...), or map the error locally.
- Verify preview env in
artifact.toml: SESSION_SECRET, PREVIEW_USER_ID, PREVIEW_USER_NAME, and PREVIEW_USER_ROLE are set.
- Verify
PREVIEW_USER_ROLE exactly matches a key in PERMISSION_MATRIX; the scaffold default is admin.
- Verify
vite.config.ts keeps the preview gateway header plugin before tanstackStart(...), so preview requests receive X-App-User-* headers and a signed mes-session cookie.
- Keep
/api/health, /api/manifest, and /api/auth/** public in src/start.ts; preview readiness depends on /api/health staying unauthenticated.
Useful checks:
rg -n 'requireAuth|HttpError|withErrors' src/routes src/services src/lib
rg -n 'PREVIEW_USER_ROLE|SESSION_SECRET|PREVIEW_USER_ID' artifact.toml
rg -n 'PERMISSION_MATRIX|ADMIN_ROLE' src/lib/permissions.ts
Artifact preview port mismatch
Error example:
cannot restart preview: restart preview: parse artifact.toml: artifact: service "web" preview.port=3000 but must be 5173 (PreviewPort constant)
This is a configuration error, not a runtime process conflict. Do not keep retrying preview restart before fixing the files.
Fix sequence:
- Open
artifact.toml and find the web service.
- Set
[services.preview].port = 5173.
- Ensure
[services.preview].args passes --host 0.0.0.0 --port 5173 to the dev server.
- Keep
[[services]].localPort aligned with the preview/runtime contract when this template is used for managed preview.
- Open
package.json and ensure scripts.dev starts Vite/TanStack on port 5173, for example vite dev --host 0.0.0.0 --port 5173.
- Retry preview only after the artifact and package script are synchronized.
Preview loading timed out
Error example:
Preview loading timed out. Try refreshing and send this error to the agent if the problem persists.
Treat this as a readiness failure. First determine whether the dev server is absent, bound to the wrong port, unhealthy, or still compiling.
Fix sequence:
- Check preview logs before changing code. Look for install failures, build errors, route/runtime exceptions, or Vite binding to a port other than
5173.
- Verify
artifact.toml uses preview port 5173 and the preview args include --host 0.0.0.0 --port 5173.
- Verify
package.json scripts.dev also uses port 5173; if it omits --port 5173, add it.
- Probe
http://127.0.0.1:5173/api/health from inside the sandbox when possible.
- If the port is occupied but health is not 2xx, run the preflight cleanup and restart preview.
- If the server is compiling slowly but healthy afterward, increase the configured preview health timeout only when the app consistently needs more startup time.
- If health never succeeds, fix the application runtime error or missing
/api/health route before retrying preview.
Frequent full page reloads during dev/HMR
Symptom: editing a file reloads the whole page instead of hot-updating, repeatedly. The dev server is healthy and on 5173; this is a developer-experience defect, not a startup failure. Do not restart preview to "fix" it — change the code/config below.
Two distinct root causes:
-
React Fast Refresh incompatibility (mixed exports). Fast Refresh only hot-replaces a module when every export is a React component. If a .tsx file also exports a constant, plain function, or hook, editing it (or any module that invalidates it) forces a full reload. The worst offenders are widely-imported files such as an app shell or shared overlay/layout module, because they reload on almost every edit.
Fix: move the non-component exports (constants, helpers, hooks) into a sibling .ts module and re-import them, so each .tsx exports components only. export type/export interface are fine (type-only, erased at build). TanStack route files that export Route are handled by the router plugin and are exempt.
for f in $(rg -l --glob 'src/**/*.tsx' '^export (default )?(function|const) [A-Z]'); do
rg -q '^export (const|function|async function) [a-z]' "$f" && echo "$f"
done
-
optimizeDeps not pre-bundling code-split deps. When Vite discovers a dependency after its initial scan (because the dep is only imported inside a code-split route/component), it re-optimizes and forces a full reload. Watch dev logs for optimized dependencies changed. reloading or new dependencies optimized.
Fix: add such client deps to vite.config.ts optimizeDeps.include, using the exact imported subpath (e.g. "motion/react", not "motion"), then restart dev once.
To keep generated apps clean from the start, enforce both as code conventions: component .tsx files export components only, and optimizeDeps.include lists client deps reached only through code-split boundaries.
Required Runtime Behavior
Before every preview start:
- Ensure dependencies are installed. If
node_modules is missing or incomplete (uploaded projects may ship without it), run npm ci when package-lock.json exists, otherwise npm install, before any build or preview.
- Check whether the declared preview port is already listening.
- If occupied, probe
http://127.0.0.1:${PREVIEW_PORT}/api/health.
- If health is 2xx, reuse the running preview and return ready instead of starting a second process.
- If occupied but unhealthy, kill the old preview process group, wait for the port to free, then start the new preview.
- If the port is free, start preview normally.
When preview stop/restart runs:
- Stop by process group, not only by parent PID.
- Send SIGTERM first.
- Wait a few seconds.
- If the process group or port is still alive, send SIGKILL.
- Verify the declared port is free, unless the preview was intentionally reused because it is healthy.
For each session:
- Allow only one preview supervisor/task at a time.
- A new start request should reuse a healthy running preview, or replace an unhealthy one under a session-level lock.
- Do not allow Vite to silently drift from
5173 to 5174/5175; use strict port behavior or treat drift as failure.
Bundled Scripts
Use these scripts as reference implementations or direct smoke tests in the sandbox:
scripts/preview_preflight.sh checks the port, probes health, and optionally cleans unhealthy listeners.
scripts/preview_stop_process_group.sh stops a preview by PID/PGID or by the listener on the preview port.
scripts/test_preview_port_lifecycle.sh verifies healthy reuse and unhealthy cleanup behavior with temporary Node test servers.
Suggested Commands
Run preflight before starting preview:
PREVIEW_PORT=5173 HEALTH_PATH=/api/health ./.agents/skills/preview-runtime-stability/scripts/preview_preflight.sh --cleanup
Stop preview robustly:
PREVIEW_PORT=5173 ./.agents/skills/preview-runtime-stability/scripts/preview_stop_process_group.sh
Run the lifecycle test:
./.agents/skills/preview-runtime-stability/scripts/test_preview_port_lifecycle.sh
Implementation Notes
- Prefer
127.0.0.1 for in-Pod health checks.
- Record
pid, pgid, session_id, task_id, port, and health_url for every preview start.
- Add logs for
port_occupied, health_status, reuse_existing_preview, cleanup_unhealthy_preview, sigterm_sent, sigkill_sent, and port_free_after_stop.
- If
lsof is unavailable in the image, install it or provide an equivalent port-to-PID resolver. The bundled scripts can also use fuser or ss when available.