Skip to content

feat(bcode-serve): serve-only binary variant for headless containers - #148

Open
sauravpanda wants to merge 6 commits into
mainfrom
claude/cold-startup-times-cd25a7
Open

feat(bcode-serve): serve-only binary variant for headless containers#148
sauravpanda wants to merge 6 commits into
mainfrom
claude/cold-startup-times-cd25a7

Conversation

@sauravpanda

@sauravpanda sauravpanda commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Issue for this PR

Closes #

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

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 serve spends 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 of provider/provider, session/processor or tool/registry gets 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:

  • bytecode compilation — skips JS parse at boot, −33% spawn-to-listening
  • dropping the 23 unused commands + the embedded web UI — worth only ~30ms on its own, but 81MB of binary once bytecode is on (vs 16MB without, since dead code also carries compiled bytecode)

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/opencode is 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's script/build.ts.
  • install.sh is 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).
  • The canonical release step is unmodified and still builds every standard target. The new step runs after it, is 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:

curl -fsSL https://bcode.sh/bytecode | bash -s -- --no-modify-path --version ${BCODE_VERSION}

Before this is usable

  1. Merge, so install-bytecode.sh exists on main.
  2. Cut a new release. No existing release carries 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.
  3. Map bcode.sh/bytecodeinstall-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 /install is mapped today.

To test before any of that, build the binary directly with bun run --cwd packages/bcode-serve script/build.ts --targets linux-arm64 and COPY it into the image.

Reviewer notes

  • The new build script duplicates the Bun.build config rather than editing upstream's. To catch drift, its smoke test boots serve and waits for the listening banner instead of running --version — a missing build-time define would sail straight past --version and only fail in production.
  • install-bytecode.sh is deliberately much shorter than install.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-path is a no-op, accepted so an existing invocation works verbatim.
  • Bytecode is on for this variant only, still off for the standard binary. The binary grows 2.3x, and while boot only pages in ~80MB more (not the full file delta), that trade needs measuring on the target runtime's storage before applying it anywhere else.
  • The asset is named -serve rather than -bytecode because the user-visible difference is functional (only bcode serve exists; 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?

  • Built darwin-arm64 and cross-compiled linux-arm64 + linux-x64 from the new package
  • Endpoint parity vs a standard binary — /session, /doc, /event, /openapi.json return identical statuses
  • bcode run / bcode tui exit 1 on the variant rather than silently succeeding
  • Benchmarked spawn-to-listening-banner, interleaved A/B, 7 rounds per variant
  • Installer tested end-to-end against a local HTTP server with a real tarball: downloads, extracts, installs, passes its --version check, and the installed binary serves. Also verified the missing-asset path reports a clear error, plus the non-linux and bad-arch guards
  • bun run typecheck clean in the new package (added to the root typecheck filter); bash -n clean on the new script

Not verified end-to-end: the gh release upload path, since running it would write to the real repo. Worth watching the first release — and because the step is continue-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

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread .github/workflows/release.yml Outdated
…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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread .github/workflows/release.yml Outdated
Comment thread packages/bcode-serve/script/build.ts
Comment thread packages/bcode-serve/script/build.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread .github/workflows/release.yml Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread install-bytecode.sh Outdated
Comment thread install-bytecode.sh Outdated
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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread install-bytecode.sh Outdated
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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread .github/workflows/release.yml
`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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant