## Summary
Prior PR removed the open-gen-ui feature but left several loose ends.
This PR completes the scrub:
1. **Source YAML** — removed `open` profile + `open-gen-ui` entries from
`showcase/shared/constraints.yaml` (was missed before; would have
re-introduced `open-gen-ui` on next generator run)
2. **Schema enum** — dropped `"open"` from `generative_ui` enum in
`showcase/shared/manifest.schema.json`
3. **Test fixture** — `invalid-genui-manifest.yaml` now uses
`[unknown-profile]` instead of `[open]`; tests still pass (validator
rejects unknown profiles)
4. **Manifest descriptions** — all 17
`showcase/packages/*/manifest.yaml` files: "5 GenUI rendering
strategies" → "4" (open-gen-ui was the 5th; now removed)
5. **Derived JSON regen** — `registry.json` regenerated cleanly via
`generate-registry.ts`
6. **Smoke-test filter fix** — `integration-smoke.spec.ts:383` now reads
top-level `i.deployed` instead of stale `i.starter?.deployed`. The old
filter returned 0 starters post-regen (silent CI skip every 6h); new
filter correctly gates on canonical top-level field.
## Context
- `showcase/shell/src/data/demo-content.json` is also regenerated but
NOT committed because it exceeds the lefthook 1MB binary-size cap
(pre-existing repo condition, separate from this PR).
- 3 starters (mastra, crewai-crews, claude-sdk-typescript) that had
manually-patched `starter.deployed: false` are now covered by smoke —
verified all 3 starter URLs respond HTTP 200 live.
## Test plan
- [ ] CI green
- [ ] Post-merge `starter-smoke` workflow picks up all 17 starters (not
zero, as was the silent broken state)
- [ ] No `open-gen-ui` references remain anywhere in showcase/
The scrub and #4084 touched the same surface: #4084 re-added an `open:`
generative_ui profile listing `open-gen-ui`/`open-gen-ui-advanced`, and
re-added both features to `constrained-explicit.allowed`. Extending the
branch's scrub to both re-additions keeps the semantic consistent with
the schema (which already dropped `open` from the approaches enum).
- `showcase/shared/constraints.yaml`: drop `open-gen-ui` +
`open-gen-ui-advanced` from `constrained-explicit.allowed`; drop main's
re-added `open:` profile entirely.
- `showcase/packages/langgraph-python/manifest.yaml`: drop the now-orphan
`open-gen-ui` + `open-gen-ui-advanced` feature and demo entries
(validator confirmed they had no allowed approach left).
- Regenerated `showcase/shell/src/data/registry.json` + sibling
`shell-docs`/`shell-dojo` registries and `constraints.json` via
`pnpm --dir showcase/scripts generate-registry`. All 17 integrations
validate.
`feature-registry.json` intentionally still defines both features — the
original scrub commits (2b996c54d, 27f886e59) left it untouched, so the
demo source files on disk also stay. Follow-up deletion if desired is
out of scope for this merge.
PR #4140 placed the liveness probe inside server.mjs under the belief it
would bind "synchronously before any await." That is not how ES modules
work — all top-level `import` statements resolve before any module-body
code runs. `@langchain/langgraph-api/server` takes ~4m30s to cold-import
on Railway (graph eval + tsx transpile), but the watchdog's 180s grace +
3x30s strikes = 270s kills the container ~4s before the probe would have
bound. Kill-loop forever.
Split into:
- `src/agent/liveness.mjs` (new entry): binds :8124/ok immediately using
only `node:http`, then dynamic-imports server.mjs so the heavy langgraph
bootstrap runs AFTER :8124 is already serving 200.
- `src/agent/server.mjs`: removed the now-dead sibling-probe code; comment
block explains why.
- `src/agent/package.json`: `start` script now runs `node --import tsx
liveness.mjs` instead of `server.mjs`.
- `entrypoint.sh`: comments updated to describe the new ordering.
Local smoke (docker build + run on linux/amd64): /ok returns 200 within
1.4s of container launch vs the previous ~4m34s.
## Summary
- Rewrite starter Dockerfile templates (`typescript`, `python`, `java`,
`dotnet`) as true multi-stage: builder has dev tools, runner is minimal.
No `pip`/`npm`/`pnpm`/`tsx`/`*-cli dev` in runtime stages.
- Encode prod-mode behaviors for `claude-sdk-typescript`, `mastra`,
`langgraph-typescript` directly in the TS template + generator
conditionals (folds in the pattern from #4132 for langgraph-ts; adds
parallel migration for claude-sdk-ts + mastra).
- Regenerate all 17 starter Dockerfiles.
- Rewrite all 17 package Dockerfiles to match the template shape
(multi-stage, venv builder for Python, 3-stage Python runtime, `USER
app`, etc.).
- Cold-start instrumentation on `claude-sdk-typescript` package
(absorbed from closed PR #4133).
- Pin `platforms: linux/amd64` in the showcase deploy workflow
(`docker/build-push-action` step) to match what Railway + GHCR require.
- Template `README.md` documenting multi-stage conventions, size target,
platform mandate, and the `dockerfile_hygiene` probe.
- `generate-starters.ts` strips the `langgraph-typescript` starter's
dead `server.mjs` + its `start` script (both reference deps only needed
in the package's prod-mode path).
## Why
Railway cold starts exceed the 180s watchdog grace when the runtime
stage boots via `langgraph-cli dev` / `npx mastra dev` / `npx tsx` — the
dev-mode work (TS compile, Rollup bundle, native libsql init) blocks the
health probe. #4132 proved the fix is to move this to image build time
for langgraph-ts. This PR generalizes that shape to all TS starters that
need it and reshapes every Dockerfile in the tree so runners no longer
carry dev toolchains.
Secondary outcomes:
- Smaller runtime images (pip/npm toolchain moved to builder only).
- Consistent `--platform linux/amd64` pinning end-to-end.
- No `pnpm install --frozen-lockfile` in the TS starter template
(starters are no-lockfile by design).
- `validate-pins` ratchet re-baselined to match the regenerated spec
strings.
## Test plan
- [ ] `docker build --platform linux/amd64` green for every starter (17)
and package (17) locally
- [ ] `pnpm -C showcase/scripts` vitest: 1085/1085 pass
- [ ] `tsc --noEmit -p showcase/scripts/tsconfig.json` clean
- [ ] `oxfmt --check` + `oxlint` clean on touched TS files
- [ ] validate-pins: count 110, hash matches updated baseline
- [ ] CI Showcase Validate green on PR HEAD
- [ ] Post-merge: showcase deploy for mastra + claude-sdk-typescript
goes green (currently flapping)
## Summary
- Root cause: #4132 migrated langgraph-typescript from `langgraph-cli
dev` (which exposed `/ok` immediately) to `@langchain/langgraph-api`
`startServer`. That server DOES register `/ok` via its meta routes, but
only binds the port AFTER `registerFromEnv` imports the graph and spawns
the N workers. On Railway that window has been exceeding the watchdog's
180s startup grace + 90s strike budget, producing an infinite restart
loop (deploy `ddc88ef9` is flapping ~every 4m30s).
- Fix: move liveness off the heavy Hono path entirely. `server.mjs` now
starts a bare `node:http` server on port 8124 (configurable via
`HEALTH_PORT`) synchronously before any `await`, answering `GET /ok`
with `{"status":"ok"}`. `entrypoint.sh` watchdog now probes
`127.0.0.1:8124/ok` instead of `:8123/ok`.
## Why
The service has been 502-ing all day. Railway deploy `ddc88ef9` never
serves a single `/api/health` 200 despite `[server] LangGraph API
listening on 0.0.0.0:8123` landing in the logs — the `listening`
callback fires after the watchdog has already killed PID 4 and triggered
a restart.
Liveness on a sibling port is independent of whether :8123 has finished
its graph import, so the probe reflects what the watchdog actually cares
about: is the Node process alive and its event loop responsive. A true
hang (event loop pinned) still fails the probe and triggers container
restart, so hang-detection is preserved.
## Test plan
- [x] Local Docker build green (`--platform linux/amd64`, depot)
- [x] Local container `curl http://127.0.0.1:8124/ok` returns `HTTP=200
{"status":"ok"}` within 20s of boot
- [x] `[server] Liveness probe listening on 0.0.0.0:8124` emitted before
`[server] LangGraph API listening on 0.0.0.0:8123`
- [ ] Railway redeploy recovers `/api/health` + `/api/smoke`
Template actually pins node:22-slim for both builder and runner stages,
and eclipse-temurin:21-jre (not -jre-alpine) for spring-ai runner. README
called out node:24 + temurin:21-jre-alpine. Align docs with reality.
Node 24 bump is a separate decision not made in this body of work.
Completes the NODE_ENV scope cleanup started in the prior Python
packages commit. Image-scope ENV NODE_ENV=production leaks into every
child process (agent subprocesses, healthchecks, shell scripts) —
several frameworks change behavior under NODE_ENV and should NOT see
'production' when running as an agent backend.
- mastra: Dockerfile drops image ENV; entrypoint.sh now invokes
'env NODE_ENV=production npx next start' (was relying on image ENV).
- spring-ai: Dockerfile drops image ENV; entrypoint.sh now scopes
NODE_ENV=production to the Next.js child only (Java agent never
interpreted NODE_ENV but the scoping matches every other package).
- langgraph-python entrypoint: 'npx next start' now inherits
NODE_ENV=production via 'env' wrapper (Dockerfile already removed
image ENV in prior commit).
All 11 Python runners previously executed as root. Bring them into parity
with langroid (the one Python package that already had the pattern):
- Create uid/gid 1001 'app' user/group before any COPY
- Add --chown=app:app to runner-stage COPY directives
- USER app before EXPOSE/CMD
- langgraph-python: add the missing COPY --from=frontend /app/public line
(every other Python package has it) plus an empty public/.gitkeep so
the copy resolves
Packages touched: ag2, agno, claude-sdk-python, crewai-crews, google-adk,
langgraph-fastapi, langgraph-python, llamaindex, ms-agent-python,
pydantic-ai, strands.
- entrypoint.template.sh: cleanup trap + final kill now tolerate no-match
(|| true) so trap doesn't abort shell when one of the three PIDs has
already exited. Package-level entrypoint.sh already had this shape.
- generate-starters.ts: claude-sdk-typescript tsc now uses --rootDir .
so emitted artifact is /app/dist/agent/index.js (matches entrypoint
expectation + runner-stage COPY). tsc was inferring agent/ as rootDir
and stripping the prefix.
- generate-starters.ts: drop server.mjs from langgraph-typescript starter
output. The starter uses 'npx @langchain/langgraph-cli dev' and never
invokes server.mjs; the file imports @langchain/langgraph-api/server
which is not in the starter's extraDependencies (resolution relied on
transitive hoist via @langchain/langgraph-cli). The file stays in the
PACKAGE where prod mode does use it.
- generate-starters.ts: assert agentDir non-empty / non-absolute before
Dockerfile substitution so the 'rm -f {{AGENT_DIR}}/package*.json' line
can never degrade into 'rm -f /package*.json' across /app.
Add showcase/starters/template/README.md covering the multi-stage build
convention (builder + runtime split), the >=40% image-size reduction
target, the linux/amd64 platform requirement for local builds, and the
dockerfile_hygiene probe rules (>=2 FROM stages, no dev-dep installs in
the runtime stage).
Expand the header comment on showcase/scripts/generate-starters.ts to
document the >=40% size target, the getAgentBuildSteps/getAgentBuildCopy
handoff in more detail, and the linux/amd64 platform pin in the deploy
workflow.
Package-level service claims to listen on :8000 but Railway health probes
fail. Narrow the cold-start window by timestamping each phase from
entrypoint exec to first /health-serving moment:
- entrypoint.sh: print `[entrypoint] pre-node <ISO>` immediately before
`node /app/agent_server.js`, pairing with the agent_server.ts prints
below to chain timestamps across the exec boundary.
- agent_server.ts: print `[agent_server] module loaded <ISO>` before any
side-effect imports (so import-time hangs surface distinctly from
init-time hangs), `[agent_server] pre-Anthropic <ISO>` right before
`new Anthropic(...)`, and include an ISO timestamp in the existing
app.listen() callback so we can read the delta directly.
No behavior change — only logs added. Next deploy cycle should reveal
which phase consumes the budget and whether the failure mode is
listen-but-unreachable vs. listen-never-reached.
Split the runner image into three stages:
- frontend (node:22-slim) builds Next.js artifacts
- agent-builder (python:3.12.13) builds /opt/venv from requirements.txt
- runner (python:3.12.13-slim) installs Node.js + copies artifacts, with no
pip and no build-essential
Mirrors the pattern already used by showcase/starters/strands/Dockerfile.
Split the runner image into three stages:
- frontend (node:22-slim) builds Next.js artifacts
- agent-builder (python:3.12.13) builds /opt/venv from requirements.txt
- runner (python:3.12.13-slim) installs Node.js + copies artifacts, with no
pip and no build-essential
Mirrors the pattern already used by showcase/starters/pydantic-ai/Dockerfile.
Split the runner image into three stages:
- frontend (node:22-slim) builds Next.js artifacts
- agent-builder (python:3.12.13) builds /opt/venv from requirements.txt
- runner (python:3.12.13-slim) installs Node.js + copies artifacts, with no
pip and no build-essential
Mirrors the pattern already used by showcase/starters/ms-agent-python/Dockerfile.
Split the runner image into three stages:
- frontend (node:22-slim) builds Next.js artifacts
- agent-builder (python:3.12.13) builds /opt/venv from requirements.txt
- runner (python:3.12.13-slim) installs Node.js + copies artifacts, with no
pip and no build-essential
Mirrors the pattern already used by showcase/starters/llamaindex/Dockerfile.
Split Python install into a dedicated agent-builder stage (python:3.12.13)
that produces /opt/venv, which the slim runner COPYs in. The runner no
longer runs pip install, so no compiler toolchain or build-essential bloats
the final image.
Preserves behavior: entrypoint.sh, port 10000, src/aimock_toggle.py,
shared_python/ -> /app/shared/python/ + PYTHONPATH. No image-level
NODE_ENV (entrypoint.sh scopes it to Next.js only).
Split Python install into a dedicated agent-builder stage (python:3.12.13)
that produces /opt/venv, which the slim runner COPYs in. The runner no
longer runs pip install, so no compiler toolchain or build-essential bloats
the final image.
Preserves behavior: entrypoint.sh, port 10000, ENV NODE_ENV=production,
shared_python/ -> /app/shared/python/ + PYTHONPATH.
Split Python install into a dedicated agent-builder stage (python:3.12.13)
that produces /opt/venv, which the slim runner COPYs in. The runner no
longer runs pip install, so no compiler toolchain or build-essential bloats
the final image.
Preserves behavior: entrypoint.sh, port 10000, ENV NODE_ENV=production,
shared_python/ -> /app/shared/python/ + PYTHONPATH.
Split Python install into a dedicated agent-builder stage (python:3.12.13)
that produces /opt/venv, which the slim runner COPYs in. The runner no
longer runs pip install, so no compiler toolchain or build-essential bloats
the final image.
Preserves behavior: entrypoint.sh, port 10000, ENV NODE_ENV=production,
shared_python/ -> /app/shared/python/ + PYTHONPATH.
There is no app/integrations/ route, so the reservation was causing
/integrations/... URLs to 404. Without it, these paths fall through to
UnscopedDocsPage via the non-integration fallthrough in [framework].
Strip snippet_framework: langgraph-python from frontmatter and
integration="langgraph-python" from all InlineDemo tags in the
main docs tree (27 files). On framework-scoped pages frameworkOverride
drives Snippet and InlineDemo; on unscoped pages FrameworkGuardedContent
already hides the body until a framework is selected, so no default
is needed.
buildNavTree now checks meta.root before including a subdirectory,
so unselected/ (and any other root:true directory) is excluded from
the main nav tree. This eliminates the flicker where SidebarLink
generated /langgraph-python/unselected/coding-agents and the
[framework] route had to server-redirect to the correct scoped URL.
On framework-scoped pages, MDX links like /quickstart rendered as plain
<a href="/quickstart"> causing RouterPivot redirect flicker. Override the
MDX `a` component when frameworkOverride is set to prepend the framework
prefix to root-relative internal links.
Next.js routes /quickstart to [framework]/[[...slug]] (dynamic segment
beats optional catch-all), where "quickstart" is not a registered
integration, causing notFound(). Fix by:
- Extracting the unscoped doc rendering logic into UnscopedDocsPage
- Falling through to it in [framework] when the slug is not a framework
- Simplifying [[...slug]]/page.tsx to handle only the root overview
SidebarLink now uses storedFramework as fallback so links on the
overview page go directly to /<framework>/<slug> without a RouterPivot
redirect. OverviewNavItem and section cards now use SidebarLink for the
same reason. FrameworkSelector "Clear selection" was navigating to
/docs/<slug> (broken); fixed to /<slug>. hrefFor now preserves the
current slug when switching frameworks from an unscoped page.
SidebarLink fallback, framework-scoped backLink, "framework-agnostic
version" banner link, FrameworkLandingPage sidebar, and brand-nav
all still pointed at /docs/* which routes to 404 via the [framework]
catch-all. Switch all to / or /<slug>.