feat(bcode-serve): serve-only binary variant for headless containers - #148
feat(bcode-serve): serve-only binary variant for headless containers#148sauravpanda wants to merge 6 commits into
Conversation
The V4 worker spends most of its cold startup waiting for `bcode serve` to become ready: `worker.bcode.listen` is ~1.50s p50 in production, ~54% of the measured worker cold path (ENG-5671). Profiling the compiled binary shows the cost is spread across the whole server import graph rather than concentrated in a few deferrable modules — whichever of `provider`, `session/processor` or `tool/registry` is imported first pays ~200ms and the rest then cost ~0ms. So there is no cheap module to defer, and restructuring 112 imports is not worth the merge friction against upstream. Two levers that do work, applied together: - bytecode compilation: skips JS parse at boot, -33% spawn-to-listening - dropping the 23 unused command modules and the embedded web UI The second matters mostly for size, and the two compound: excluding the unused commands saves 16MB without bytecode but 81MB with it, since dead code also carries compiled bytecode. Measured on darwin-arm64, medians of 7, no web UI: entrypoint plain bytecode opencode index.ts 108MB / 412ms 310MB / 266ms bcode-serve 92MB / 381ms 229MB / 257ms On linux-arm64 (the container target) the variant is 257MB vs 148MB standard — +110MB on a 744MB image, well inside AgentCore's 2GB cap. This lives in its own package so `packages/opencode`, which is forked from upstream and synced regularly, stays byte-for-byte untouched. Everything is additive: the canonical build step still runs first and unmodified, the new step is `continue-on-error` and writes different asset names, so it can never block or clobber a normal release. Cloud consumes the release asset through the install one-liner, so install.sh gains `--variant serve`. `check_version` had to learn about it too — a variant swap keeps the same version string, so the "already installed" short-circuit would otherwise skip a standard -> serve switch. The new build script duplicates the Bun.build config rather than editing the upstream one; to catch drift its smoke test boots `serve` and waits for the listening banner instead of just running `--version`, which a missing build-time `define` would sail straight past.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b8f651ffff
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…nstall.sh alone Reverts the `--variant serve` flag added to install.sh in the previous commit and ships a separate `install-bytecode.sh` instead, intended to be hosted at bcode.sh/bytecode alongside (not replacing) bcode.sh/install. install.sh is the published, documented entrypoint that the README and every existing doc point at. Adding a flag to it meant every future change to the variant touched the script humans install with. A second script and a second URL keeps the blast radius at zero: nothing can change what /install serves. The new script takes install.sh's flags, so switching is a one-word change to the URL in a Dockerfile: curl -fsSL https://bcode.sh/bytecode | bash -s -- --no-modify-path --version $V It is deliberately much shorter than install.sh: the variant ships one build per arch (no baseline/AVX2 or musl detection), is linux-only, and targets containers that set PATH in the Dockerfile (no shell-rc editing, no uv hint). `--no-modify-path` is accepted as a no-op so an existing invocation works verbatim. Releases predating this variant do not publish the asset, so a missing asset reports that explicitly rather than letting tar fail on a 404 body. Note: the bcode.sh domain and its path routing are not configured in this repo — only the scripts live here. Mapping /bytecode to this file has to happen wherever /install is currently mapped.
There was a problem hiding this comment.
3 issues found and verified against the latest diff
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name=".github/workflows/release.yml">
<violation number="1" location=".github/workflows/release.yml:175">
P1: The serve assets will never be built by this workflow because `packages/bcode-serve/script/build.ts` is not executable, so the direct invocation fails; the continue-on-error setting hides the failure and publishes a release without the opt-in variant. Invoking the script through Bun (or making the file executable) would preserve the intended additive release behavior.</violation>
</file>
<file name="packages/bcode-serve/script/build.ts">
<violation number="1" location="packages/bcode-serve/script/build.ts:59">
P1: Serve installation fails on x64 hosts without AVX2 because this variant has no baseline asset. Add and publish the x64 baseline target (and its musl combination where applicable), or make the installer select a compatible serve asset.</violation>
<violation number="2" location="packages/bcode-serve/script/build.ts:189">
P2: Every successful native smoke test leaves its 30-second timeout armed, delaying build-script exit by up to 30 seconds. Retain the timer handle and clear it in `finally` after either race branch settles.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cd05aaec66
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
2 issues found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="install-bytecode.sh">
<violation number="1" location="install-bytecode.sh:134">
P1: Alpine/musl containers can download this Linux asset successfully but the resulting `bcode` cannot start because it is the glibc build; the installer should either detect musl and select a published serve-musl asset or reject musl before downloading.</violation>
<violation number="2" location="install-bytecode.sh:175">
P2: A failed `--version` check leaves the previous installation overwritten; validating `${tmp_dir}/${APP}` before `mv` would prevent an incompatible or corrupt download from bricking an existing `bcode`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Repo is public; internal ticket IDs don't belong in it. Benchmark figures belonged in the PR discussion, not inline — they go stale and were restating the rationale at length. Comments now say why something is the way it is and stop there.
…l ordering Four issues from automated review, all confirmed: build.ts was committed 100644 while the canonical build script is 100755, so `./packages/bcode-serve/script/build.ts` exited 126 (permission denied). With continue-on-error on that step, every release would have shipped without the variant and still looked green. Fixed the mode, and invoke via `bun` so a lost executable bit can't silently disable the step again. The smoke test's Promise.race left its 30s timeout armed after the server came up. An armed timer keeps the event loop alive, so a ~2s build took 32s. Clear it in `finally`. The installer requested the glibc asset unconditionally, so an Alpine host downloaded a binary that cannot exec. It now detects musl and selects the matching asset; the release step publishes the musl targets to go with it (the build script already enumerated them). Non-AVX2 x64 has no baseline asset for this variant, so that case is rejected with a pointer to /install rather than installing a binary that SIGILLs. The AVX2 probe only trips when /proc/cpuinfo is readable — unknown is not the same as absent. The `--version` check ran after the binary was already moved into place, so a bad download replaced a working install and then failed. Validate in the temp dir first and only move on success.
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="install-bytecode.sh">
<violation number="1" location="install-bytecode.sh:184">
P2: Install fails on systems with a `noexec` temporary directory even when the configured destination can execute binaries, because `--version` is now run from `${TMPDIR:-/tmp}`. Staging and validating a temporary copy inside the executable install directory would preserve the existing-install safety without imposing this extra mount requirement.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
My previous fix validated the download by running `--version` from the temp dir, which broke installs on hosts that mount /tmp noexec — common hardening, and a regression: before that change the binary was only ever executed from the install dir. Stage inside the install dir instead. That keeps the safety property (a corrupt or wrong-libc download can no longer replace a working bcode) without requiring an exec-capable /tmp, since the install dir has to allow exec anyway. The final step becomes a same-filesystem `mv`, so the swap is atomic and no reader can observe a half-written binary. The staging file is cleaned up on every exit path, and the failure message now names noexec on the install dir as a cause.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7c1664b12c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
`Checkout` has no `ref:`, so on `workflow_dispatch` the tree is the dispatch ref (usually main) while the upload still targets `inputs.tag`. Publishing from there would put main's code inside that tag's assets. The serve step now compares HEAD against the tag's commit and skips with a visible warning when they differ, rather than uploading mislabelled binaries. The warning matters because the step is continue-on-error and would otherwise skip silently. The canonical build step has the same exposure — this is pre-existing, not introduced here, and fixing it properly means adding `ref:` to `Checkout` for the whole job. That changes the standard release path, which this PR otherwise leaves alone, so it belongs in its own change.
Issue for this PR
Closes #
Type of change
What does this PR do?
Adds a second bcode binary variant, built for headless containers, that starts noticeably faster than the standard one. Nothing that currently ships changes.
A container running
bcode servespends most of its cold start waiting for the server to be ready. Profiling the compiled binary shows the cost is spread evenly across the server import graph rather than concentrated anywhere deferrable — whichever ofprovider/provider,session/processorortool/registrygets imported first pays ~200ms and the others then cost ~0ms, because they all pull one shared core. So there's no cheap module to lazy-load, and restructuring 112 imports isn't worth the merge friction against upstream.Two things do work, and they compound:
Together: 445ms → 297ms spawn-to-listening on darwin-arm64, medians of 7 interleaved runs. On linux-arm64 the variant is 257MB vs 148MB standard — +110MB on a 744MB image, well inside the 2GB cap.
Everything here is additive
packages/opencodeis byte-for-byte untouched. That tree is forked from upstream and synced regularly, so the new entrypoint and build live in their own package rather than as flags on upstream'sscript/build.ts.install.shis byte-for-byte untouched. It's the published path the README points at. The variant gets its own script,install-bytecode.sh, for a second URL (bcode.sh/bytecode).continue-on-error, and writes different asset names, so it can't block or clobber a normal release.Cloud switches with a one-word change to the URL — the new script takes the same flags:
Before this is usable
install-bytecode.shexists onmain.bcode-linux-*-serve.tar.gz— the workflow step that builds it only lands with this PR, so the asset first appears on a tag cut after the merge. Pinning an older version will fail; the script reports that case explicitly.bcode.sh/bytecode→install-bytecode.sh. The domain and its path routing aren't configured in this repo — only the scripts live here — so this has to happen wherever/installis mapped today.To test before any of that, build the binary directly with
bun run --cwd packages/bcode-serve script/build.ts --targets linux-arm64andCOPYit into the image.Reviewer notes
Bun.buildconfig rather than editing upstream's. To catch drift, its smoke test bootsserveand waits for the listening banner instead of running--version— a missing build-timedefinewould sail straight past--versionand only fail in production.install-bytecode.shis deliberately much shorter thaninstall.sh: one build per arch (no baseline/AVX2 or musl detection), linux-only, and containers set PATH in the Dockerfile (no shell-rc editing).--no-modify-pathis a no-op, accepted so an existing invocation works verbatim.-serverather than-bytecodebecause the user-visible difference is functional (onlybcode serveexists; everything else exits 1), while bytecode is just how it's compiled. Happy to rename if the URL and asset should match.How did you verify your code works?
/session,/doc,/event,/openapi.jsonreturn identical statusesbcode run/bcode tuiexit 1 on the variant rather than silently succeeding--versioncheck, and the installed binary serves. Also verified the missing-asset path reports a clear error, plus the non-linux and bad-arch guardsbun run typecheckclean in the new package (added to the root typecheck filter);bash -nclean on the new scriptNot verified end-to-end: the
gh release uploadpath, since running it would write to the real repo. Worth watching the first release — and because the step iscontinue-on-error, a failure there won't turn the job red, so check the asset list in the run summary.Screenshots / recordings
n/a — no UI change.
Checklist