Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Trajectory: Ship @relaycast/engine as standalone bun-compiled binaries

> **Status:** ✅ Completed
> **Confidence:** 85%
> **Started:** October 26, 2026 at 01:19 AM
> **Completed:** October 26, 2026 at 01:19 AM

---

## Summary

Standalone relaycast-engine binaries for 7 targets via bun build --compile with a bun:sqlite driver seam; full engine suite passes on bun:sqlite; CI builds, smoke-tests natively, and attaches assets to releases

**Approach:** Standard approach

---

## Key Decisions

### Build-entry module swap (sqlite-driver, migration-source, version) via a Bun.build plugin instead of runtime driver detection
- **Chose:** Build-entry module swap (sqlite-driver, migration-source, version) via a Bun.build plugin instead of runtime driver detection
- **Reasoning:** Node path keeps static better-sqlite3 imports with zero behaviour change and no top-level await; the binary never bundles the native addon (build fails if it would); vitest under Bun aliases the same module so the full suite runs on bun:sqlite

### Wrap bun:sqlite in a better-sqlite3-compatible facade and route drizzle through it
- **Chose:** Wrap bun:sqlite in a better-sqlite3-compatible facade and route drizzle through it
- **Reasoning:** bun:sqlite swallows errors from non-final statements in exec, silently drops prepare tails, returns null from get, and counts FK/trigger rows in changes; the facade normalizes all of these so migrations and tests behave identically

### Use a lookup-pinned node:https transport for outbound webhooks under Bun
- **Chose:** Use a lookup-pinned node:https transport for outbound webhooks under Bun
- **Reasoning:** Bun's undici shim ignores dispatcher, which silently disabled connect-time DNS pinning (SSRF guard); Bun honours lookup on node:https, verified by positive control and a mutation-checked regression test

---

## Chapters

### 1. Work
*Agent: default*

- Build-entry module swap (sqlite-driver, migration-source, version) via a Bun.build plugin instead of runtime driver detection: Build-entry module swap (sqlite-driver, migration-source, version) via a Bun.build plugin instead of runtime driver detection
- Wrap bun:sqlite in a better-sqlite3-compatible facade and route drizzle through it: Wrap bun:sqlite in a better-sqlite3-compatible facade and route drizzle through it
- Use a lookup-pinned node:https transport for outbound webhooks under Bun: Use a lookup-pinned node:https transport for outbound webhooks under Bun
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"id": "traj_rckjtzs1kvmy",
"version": 1,
"task": {
"title": "Ship @relaycast/engine as standalone bun-compiled binaries"
},
"status": "completed",
"startedAt": "2026-10-26T08:19:03.527Z",
"completedAt": "2026-10-26T08:19:04.512Z",
"agents": [
{
"name": "default",
"role": "lead",
"joinedAt": "2026-10-26T08:19:03.788Z"
}
],
"chapters": [
{
"id": "chap_b98bhicq6w3x",
"title": "Work",
"agentName": "default",
"startedAt": "2026-10-26T08:19:03.788Z",
"endedAt": "2026-10-26T08:19:04.512Z",
"events": [
{
"ts": 1793002743789,
"type": "decision",
"content": "Build-entry module swap (sqlite-driver, migration-source, version) via a Bun.build plugin instead of runtime driver detection: Build-entry module swap (sqlite-driver, migration-source, version) via a Bun.build plugin instead of runtime driver detection",
"raw": {
"question": "Build-entry module swap (sqlite-driver, migration-source, version) via a Bun.build plugin instead of runtime driver detection",
"chosen": "Build-entry module swap (sqlite-driver, migration-source, version) via a Bun.build plugin instead of runtime driver detection",
"alternatives": [],
"reasoning": "Node path keeps static better-sqlite3 imports with zero behaviour change and no top-level await; the binary never bundles the native addon (build fails if it would); vitest under Bun aliases the same module so the full suite runs on bun:sqlite"
},
"significance": "high"
},
{
"ts": 1793002744042,
"type": "decision",
"content": "Wrap bun:sqlite in a better-sqlite3-compatible facade and route drizzle through it: Wrap bun:sqlite in a better-sqlite3-compatible facade and route drizzle through it",
"raw": {
"question": "Wrap bun:sqlite in a better-sqlite3-compatible facade and route drizzle through it",
"chosen": "Wrap bun:sqlite in a better-sqlite3-compatible facade and route drizzle through it",
"alternatives": [],
"reasoning": "bun:sqlite swallows errors from non-final statements in exec, silently drops prepare tails, returns null from get, and counts FK/trigger rows in changes; the facade normalizes all of these so migrations and tests behave identically"
},
"significance": "high"
},
{
"ts": 1793002744302,
"type": "decision",
"content": "Use a lookup-pinned node:https transport for outbound webhooks under Bun: Use a lookup-pinned node:https transport for outbound webhooks under Bun",
"raw": {
"question": "Use a lookup-pinned node:https transport for outbound webhooks under Bun",
"chosen": "Use a lookup-pinned node:https transport for outbound webhooks under Bun",
"alternatives": [],
"reasoning": "Bun's undici shim ignores dispatcher, which silently disabled connect-time DNS pinning (SSRF guard); Bun honours lookup on node:https, verified by positive control and a mutation-checked regression test"
},
"significance": "high"
}
]
}
],
"retrospective": {
"summary": "Standalone relaycast-engine binaries for 7 targets via bun build --compile with a bun:sqlite driver seam; full engine suite passes on bun:sqlite; CI builds, smoke-tests natively, and attaches assets to releases",
"approach": "Standard approach",
"confidence": 0.85
},
"commits": [],
"filesChanged": [],
"projectId": "AgentWorkforce/relaycast",
"tags": [],
"_trace": {
"startRef": "752d660a9ba6cdb7d1fe692a9fd35621f45f1590",
"endRef": "752d660a9ba6cdb7d1fe692a9fd35621f45f1590"
}
}
199 changes: 199 additions & 0 deletions .github/workflows/engine-binaries.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
name: Engine binaries

# Standalone `relaycast-engine` executables (bun build --compile + bun:sqlite):
# no Node, npm, compiler, or native addon needed to run them.
#
# - pull_request / workflow_dispatch: build every target, run the engine suite
# on bun:sqlite, and smoke-test each binary on a runner that can execute it.
# Nothing is uploaded to a release.
# - workflow_call (from publish-npm.yml, after Create Release): the same, from
# the release tag, then attach the binaries and their .sha256 files to that
# release as relaycast-engine-<version>-<os>-<arch>[.exe].

on:
pull_request:
branches: [main]
paths:
- "packages/engine/**"
- "packages/types/**"
- "packages/a2a/**"
- "package-lock.json"
- ".github/workflows/engine-binaries.yml"
workflow_dispatch:
workflow_call:
inputs:
release_tag:
description: "Existing release tag (v<version>) to build from and attach binaries to"
required: true
type: string

concurrency:
group: engine-binaries-${{ inputs.release_tag || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read

env:
BUN_VERSION: "1.4.2"

jobs:
build:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 20
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.release_tag || github.sha }}
persist-credentials: false
- uses: ./.github/actions/setup
- uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ env.BUN_VERSION }}
- name: Build workspace dependencies
run: npx turbo build --filter=@relaycast/engine^...
- name: Resolve version
id: version
env:
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
version="$(node -p "require('./packages/engine/package.json').version")"
if [ -n "$RELEASE_TAG" ] && [ "v${version}" != "$RELEASE_TAG" ]; then
echo "::error::engine version ${version} does not match release tag ${RELEASE_TAG}"
exit 1
fi
echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Build all targets
working-directory: packages/engine
run: bun standalone/build.ts --outdir dist-standalone
- name: Verify checksums
working-directory: packages/engine/dist-standalone
run: |
sha256sum -c ./*.sha256
ls -l
- uses: actions/upload-artifact@v4
with:
name: engine-binaries
path: packages/engine/dist-standalone/
if-no-files-found: error
retention-days: 7

test-bun:
name: Engine suite on bun:sqlite
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.release_tag || github.sha }}
persist-credentials: false
- uses: ./.github/actions/setup
- uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ env.BUN_VERSION }}
- name: Build workspace dependencies
run: npx turbo build --filter=@relaycast/engine^...
- name: Typecheck standalone sources
working-directory: packages/engine
run: npm run typecheck:standalone
- name: bun:sqlite facade tests
working-directory: packages/engine
run: bun test standalone
- name: Engine suite under Bun
working-directory: packages/engine
env:
DO_NOT_TRACK: "1"
run: npm run test:bun

smoke:
name: Smoke (${{ matrix.target }})
needs: build
runs-on: ${{ matrix.runner }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- { target: linux-x64, runner: ubuntu-latest }
- { target: linux-arm64, runner: ubuntu-24.04-arm }
- { target: linux-musl-x64, runner: ubuntu-latest, alpine: true }
- { target: linux-musl-arm64, runner: ubuntu-24.04-arm, alpine: true }
- { target: darwin-x64, runner: macos-15-intel }
- { target: darwin-arm64, runner: macos-latest }
- { target: windows-x64, runner: windows-latest, exe: ".exe" }
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.release_tag || github.sha }}
persist-credentials: false
sparse-checkout: packages/engine/standalone
- uses: actions/download-artifact@v4
with:
name: engine-binaries
path: dist-standalone
- name: Select binary
id: binary
shell: bash
env:
VERSION: ${{ needs.build.outputs.version }}
TARGET: ${{ matrix.target }}
EXE: ${{ matrix.exe }}
run: |
path="dist-standalone/relaycast-engine-${VERSION}-${TARGET}${EXE}"
test -f "$path"
chmod +x "$path"
echo "path=$path" >> "$GITHUB_OUTPUT"
- uses: actions/setup-node@v4
if: ${{ !matrix.alpine }}
with:
node-version: "22.14.0"
- name: Smoke
if: ${{ !matrix.alpine }}
shell: bash
run: node packages/engine/standalone/smoke.mjs "${{ steps.binary.outputs.path }}" --expect-version "${{ needs.build.outputs.version }}"
- name: Smoke (Alpine)
if: ${{ matrix.alpine }}
# Node is only the test driver; the binary itself needs just musl and
# libstdc++ (Bun's musl builds link it dynamically).
run: |
docker run --rm -v "$PWD:/w" -w /w alpine:3.21 sh -c '
apk add --no-cache libstdc++ nodejs >/dev/null &&
node packages/engine/standalone/smoke.mjs "${{ steps.binary.outputs.path }}" --expect-version "${{ needs.build.outputs.version }}"
'

release:
name: Attach to release
needs: [build, test-bun, smoke]
if: ${{ inputs.release_tag }}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
name: engine-binaries
path: dist-standalone
- name: Upload release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ inputs.release_tag }}
VERSION: ${{ needs.build.outputs.version }}
run: |
set -euo pipefail
cd dist-standalone
sha256sum -c ./*.sha256
shopt -s nullglob
assets=(relaycast-engine-"${VERSION}"-*)
if [ "${#assets[@]}" -ne 14 ]; then
echo "::error::expected 7 binaries plus 7 checksums, found ${#assets[@]}: ${assets[*]}"
exit 1
fi
gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber --repo "$GITHUB_REPOSITORY"
{
echo "### Standalone engine binaries attached to ${RELEASE_TAG}"
for asset in "${assets[@]}"; do echo "- \`${asset}\`"; done
} >> "$GITHUB_STEP_SUMMARY"
15 changes: 14 additions & 1 deletion .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -846,10 +846,22 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Standalone relaycast-engine binaries (no Node/npm/native addon), built from
# the release tag and attached to the GitHub release created above. Runs only
# after Create Release succeeded, so dry runs and failed publishes skip it.
engine-binaries:
name: Engine binaries
needs: [build, create-release]
permissions:
contents: write
uses: ./.github/workflows/engine-binaries.yml
with:
release_tag: v${{ needs.build.outputs.new_version }}

# Summary
summary:
name: Summary
needs: [build, publish-packages, verify-publish, create-release]
needs: [build, publish-packages, verify-publish, create-release, engine-binaries]
runs-on: ubuntu-latest
if: always()

Expand All @@ -873,4 +885,5 @@ jobs:
echo "| Publish NPM Packages | ${{ needs.publish-packages.result == 'success' && '✅' || (needs.publish-packages.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-packages.result }} |"
echo "| Verify Publish | ${{ needs.verify-publish.result == 'success' && '✅' || (needs.verify-publish.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.verify-publish.result }} |"
echo "| Create Release | ${{ needs.create-release.result == 'success' && '✅' || (needs.create-release.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.create-release.result }} |"
echo "| Engine Binaries | ${{ needs.engine-binaries.result == 'success' && '✅' || (needs.engine-binaries.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.engine-binaries.result }} |"
} >> "$GITHUB_STEP_SUMMARY"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ relaycast-files/

# OpenNext Cloudflare build output
.open-next/

# Standalone engine binaries (packages/engine/standalone/build.ts)
dist-standalone/
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Packages without a separate changelog are covered by the cross-package notes bel

- New `trial` plan for hosted anonymous workspaces: a one-time allowance of 100 messages (channel posts, DMs, group DMs, thread replies) that never resets. `free`, paid plans and self-hosted `selfhost` are unchanged.
- `plan_limit_exceeded` errors now carry `metric`, `plan`, `limit`, `usage`, and, when the deployment has an upgrade path, `upgrade_url` and `upgrade_action` (`sign_in` or `upgrade`).
- Standalone `relaycast-engine` executables for Linux (glibc and musl, x64 and arm64), macOS (x64 and arm64), and Windows x64 are attached to each GitHub release as `relaycast-engine-<version>-<os>-<arch>[.exe]` with `.sha256` files; they run the self-host server with no Node.js, npm, compiler, or native addon.
- `relaycast-engine --version` prints the engine version.

### Fixed

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,10 @@ It listens on `http://localhost:8787` and stores state in a local SQLite file (o
`--db <path>` or `$RELAYCAST_DB_PATH`). To run it as a container, build a small image around the
`relaycast-engine` bin and expose port 8787 — any Docker/OCI host works.

No Node.js? Each GitHub release also attaches standalone `relaycast-engine` executables for Linux
(glibc and musl), macOS, and Windows — see
[Standalone binary](./docs/self-hosting.md#standalone-binary-no-nodejs).

Point any SDK at it with `baseUrl`:

```ts
Expand Down
Loading
Loading