Skip to content

The production service runner: one headless process for engine + service (#955) - #958

Merged
aarontrowbridge merged 2 commits into
mainfrom
955-service-runner
Sep 10, 2026
Merged

aarontrowbridge merged 2 commits into
mainfrom
955-service-runner

Conversation

@aarontrowbridge

Copy link
Copy Markdown
Member

Refs #955 (the hub cutover, spec spec-20260910-080000).

What

The hub has no extension host: the extension-host arrangement (spawn the vendored engine, boot the amicode service alongside) has no permanent home there — the #822 boot probe only rehearses it and exits. This adds the runner: ONE long-lived headless process that spawns the engine and serves the app.

  • src/amicode_service_runner.ts — bootAmicodeServiceRunner: engine spawn password-armed (the SAME mint convention as every spawn site: mintServerPassword() → OPENCODE_SERVER_PASSWORD, health-wait WITH the armed credential), then createAmicodeService wired exactly as startAmicodeService does (engine handle + shelf distRoot + engine-token auth). No fleetActivation is ever passed — H3: the hub runs the byte-identical unarmed base service.
  • src/amicode_service_runner_cli.ts — the env surface, bundled to bin/dist/amicode-service-runner.mjs (the mcp-amico.mjs convention; plain node):
    • AMICODE_ENGINE_BIN (default: the vendored path relative to the bundle), AMICODE_APP_DIST (required — a missing/unbuilt dist fails loud, the NEEDS-SETUP placeholder is the extension's degradation, not a hub shape), AMICODE_SERVICE_PORT (4095), AMICODE_ENGINE_PORT (4094), AMICODE_ENGINE_CWD, OPENCODE_DB (the canonical pin, passed through), AMICODE_ENGINE_PASSWORD (ops-owned credential for the frontdoor's ?auth_token= carrier; random mint when absent).
    • SIGTERM/SIGINT → graceful teardown (service stop, SIGTERM engine, 3s SIGKILL fallback). Any boot failure or engine death mid-run → non-zero exit with a named [service-runner] FAIL: <reason> — never a silent half-boot.
  • Latent upstream fix in amicode_service/server.ts: start() set this.server before the listen promise — a busy fixed port left instance state that made the caller's own ephemeral fallback die on "already running" (the wiring's fallback had the same exposure).

Tests (test/amicode_service_runner.test.ts)

Live-gated (real vendored engine + built dist, the boot-probe convention): the six #823 probe surfaces asserted through the runner's own orchestration; the OPENCODE_DB pin reaching the engine (the pinned DB file gets created/migrated); the busy-fixed-port fallback (pins the server.ts fix). Always-on fail-loud: missing engine bin / empty shelf / never-healthy engine — each a named AmicodeServiceRunnerError.reason, the spawned child torn down.

Evidence (run for real in the worktree, throwaway DB)

Runner boot lines:

[service-runner] spawning engine …/vendor/opencode/linux-x64/opencode serve --port=14094 (cwd=/tmp/…, OPENCODE_DB=/tmp/opencode/runner-955-evidence.db)
[service-runner] engine up at http://127.0.0.1:14094
[amicode-service] listening on http://127.0.0.1:14095 (43 routes; auth: per-boot Basic + engine token); app shelf mounted

Six surfaces against the runner's origin: app doc 200 text/html (not the placeholder) · bootstrap ?auth_token= (no header) 200 · anonymous asset 200 text/javascript · proxied GET /session 200 application/json ([]) · proxied GET /event 200 text/event-stream · GET /amicode/profile {"ok":true,…}. Pinned DB file created (256 KiB, engine-migrated).

Fail-loud (CLI): AMICODE_ENGINE_BIN=/nonexistent/opencode node bin/dist/amicode-service-runner.mjs → [service-runner] FAIL: no engine binary at /nonexistent/opencode — set AMICODE_ENGINE_BIN …, exit 1.

SIGTERM → [service-runner] SIGTERM — tearing down engine + service / [service-runner] stopped — service closed, engine torn down, runner exits 0, engine process gone, service port closed.

Baseline

typecheck clean; extension vitest: 8 pre-existing failures (clean main @ a7741ee, clean env: 9, of which cli_gate passes here only because a local build staged the amico-run bins) — none introduced. The runner tests: 5 live + 3 fail-loud green.

… engine + service

The hub cutover's missing orchestration: the extension host normally spawns
the engine and boots the amicode service, and the #822 boot probe only
rehearses that arrangement before exiting. The hub has no extension host —
this adds the permanent runner:

- src/amicode_service_runner.ts — bootAmicodeServiceRunner: spawns the
  vendored engine password-armed (mintServerPassword + OPENCODE_SERVER_
  PASSWORD, the ServerManager/probe idiom), health-waits WITH the armed
  credential, then boots the service through createAmicodeService exactly
  as startAmicodeService wires it (engine handle + shelf distRoot +
  engine-token auth). No fleetActivation is ever passed (H3: the hub runs
  the byte-identical unarmed base service).
- src/amicode_service_runner_cli.ts — the env-driven CLI (AMICODE_ENGINE_
  BIN / AMICODE_APP_DIST / AMICODE_SERVICE_PORT=4095 / AMICODE_ENGINE_PORT=
  4094 / AMICODE_ENGINE_CWD / AMICODE_ENGINE_PASSWORD / OPENCODE_DB pin
  passthrough), bundled by esbuild to bin/dist/amicode-service-runner.mjs
  (the mcp-amico.mjs convention). SIGTERM/SIGINT → graceful teardown; any
  boot failure or engine death exits non-zero with a named
  [service-runner] FAIL reason — never a silent half-boot.
- test/amicode_service_runner.test.ts — live-gated orchestration tests
  (real vendored engine + built dist): the six #823 probe surfaces through
  the runner's own orchestration, the OPENCODE_DB pin reaching the engine,
  the busy-fixed-port fallback; always-on fail-loud tests (missing engine
  bin, empty shelf, never-healthy engine → named reason + child torn down).
- amicode_service/server.ts — fix a latent wedge: start() set this.server
  before the listen promise, so a busy fixed port left instance state that
  made the caller's own ephemeral fallback die on 'already running'.

Ran for real against a throwaway OPENCODE_DB: all six probe surfaces PASS
through the runner's boot lines; SIGTERM tears both processes down.

Refs #955
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ilt dist

CI has no app dist: the shelf check fired before the fake engine ever
spawned, so the test asserted the wrong named reason. Give the test a stub
shelf (index.html only) — it isolates the engine-health path.
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