feat(v2): migrate container runtime to Bun, improve image build surface

Container side:
- agent-runner switches to Bun. Drops better-sqlite3 (native compile gone),
  drops tsc build step in-image AND the tsc-on-every-session-wake in the
  entrypoint — bun runs src/index.ts directly. bun:sqlite replaces
  better-sqlite3; cross-mount DB invariants (journal_mode=DELETE, busy_timeout)
  preserved. Named params converted from @name to $name because bun:sqlite
  does not auto-strip the prefix the way better-sqlite3 does.
- Tests ported from vitest to bun:test (only describe/it/expect/before/afterEach
  used, API-compatible). vitest.config.ts excludes container/agent-runner/.
- bun.lock replaces pnpm-lock.yaml + pnpm-workspace.yaml under
  container/agent-runner/. Host pnpm workspace does NOT include this tree.

Dockerfile improvements (independent of Bun but bundled while touching the file):
- tini as PID 1 for correct SIGTERM propagation (prevents half-written
  outbound.db on shutdown).
- Extracted entrypoint.sh — readable and diffable vs the old inline printf.
- BuildKit cache mounts for apt + bun install + pnpm install.
- --no-install-recommends on apt, pinned CLAUDE_CODE_VERSION, AGENT_BROWSER,
  VERCEL, BUN_VERSION.
- CJK fonts (~200MB) behind ARG INSTALL_CJK_FONTS=false; build.sh reads from
  .env; setup/container.ts reads the same .env so /setup and manual rebuild
  stay in sync.
- PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 in case any postinstall tries to pull a
  redundant Chromium.
- /home/node 755 (was 777).

Host side:
- src/container-runner.ts dynamic spawn command collapses from
  `pnpm exec tsc --outDir /tmp/dist … && node /tmp/dist/index.js` to
  `exec bun run /app/src/index.ts` — cold start ~200-500ms faster per wake.

CI:
- oven-sh/setup-bun@v2 alongside Node/pnpm. Adds explicit container
  typecheck (was documented in CLAUDE.md, not enforced) and `bun test` for
  agent-runner tests.
This commit is contained in:
gavrielc
2026-04-17 11:38:01 +03:00
parent 45c35a08f0
commit c5d0ef8b4f
16 changed files with 455 additions and 1409 deletions

View File

@@ -17,35 +17,35 @@
* src/session-manager.ts for the full set of cross-mount invariants and
* scripts/sanity-live-poll.ts for the empirical validation.
*/
import Database from 'better-sqlite3';
import { Database } from 'bun:sqlite';
import fs from 'fs';
const DEFAULT_INBOUND_PATH = '/workspace/inbound.db';
const DEFAULT_OUTBOUND_PATH = '/workspace/outbound.db';
const DEFAULT_HEARTBEAT_PATH = '/workspace/.heartbeat';
let _inbound: Database.Database | null = null;
let _outbound: Database.Database | null = null;
let _inbound: Database | null = null;
let _outbound: Database | null = null;
let _heartbeatPath: string = DEFAULT_HEARTBEAT_PATH;
/** Inbound DB — container opens read-only (host is the sole writer). */
export function getInboundDb(): Database.Database {
export function getInboundDb(): Database {
if (!_inbound) {
const dbPath = process.env.SESSION_INBOUND_DB_PATH || DEFAULT_INBOUND_PATH;
_inbound = new Database(dbPath, { readonly: true });
_inbound.pragma('busy_timeout = 5000');
_inbound.exec('PRAGMA busy_timeout = 5000');
}
return _inbound;
}
/** Outbound DB — container owns this file (sole writer). */
export function getOutboundDb(): Database.Database {
export function getOutboundDb(): Database {
if (!_outbound) {
const dbPath = process.env.SESSION_OUTBOUND_DB_PATH || DEFAULT_OUTBOUND_PATH;
_outbound = new Database(dbPath);
_outbound.pragma('journal_mode = DELETE');
_outbound.pragma('busy_timeout = 5000');
_outbound.pragma('foreign_keys = ON');
_outbound.exec('PRAGMA journal_mode = DELETE');
_outbound.exec('PRAGMA busy_timeout = 5000');
_outbound.exec('PRAGMA foreign_keys = ON');
// Lightweight forward-compat: session_state was added after the initial
// v2 schema, so older session DBs don't have it. Create it on demand
// instead of requiring a formal migration pass. Also handle the case
@@ -97,9 +97,9 @@ export function clearStaleProcessingAcks(): void {
}
/** For tests — creates in-memory DBs with the session schemas. */
export function initTestSessionDb(): { inbound: Database.Database; outbound: Database.Database } {
export function initTestSessionDb(): { inbound: Database; outbound: Database } {
_inbound = new Database(':memory:');
_inbound.pragma('foreign_keys = ON');
_inbound.exec('PRAGMA foreign_keys = ON');
_inbound.exec(`
CREATE TABLE messages_in (
id TEXT PRIMARY KEY,
@@ -132,7 +132,7 @@ export function initTestSessionDb(): { inbound: Database.Database; outbound: Dat
`);
_outbound = new Database(':memory:');
_outbound.pragma('foreign_keys = ON');
_outbound.exec('PRAGMA foreign_keys = ON');
_outbound.exec(`
CREATE TABLE messages_out (
id TEXT PRIMARY KEY,
@@ -173,6 +173,6 @@ export function closeSessionDb(): void {
* @deprecated Use getInboundDb() / getOutboundDb() instead.
* Kept for backward compatibility during migration.
*/
export function getSessionDb(): Database.Database {
export function getSessionDb(): Database {
return getInboundDb();
}

View File

@@ -53,20 +53,24 @@ export function writeMessageOut(msg: WriteMessageOut): number {
const max = Math.max(maxOut, maxIn);
const nextSeq = max % 2 === 0 ? max + 1 : max + 2; // next odd
// bun:sqlite requires named parameters to be passed with the prefix character
// in the JS object keys (better-sqlite3 auto-stripped it, bun:sqlite does not).
outbound
.prepare(
`INSERT INTO messages_out (id, seq, in_reply_to, timestamp, deliver_after, recurrence, kind, platform_id, channel_type, thread_id, content)
VALUES (@id, @seq, @in_reply_to, datetime('now'), @deliver_after, @recurrence, @kind, @platform_id, @channel_type, @thread_id, @content)`,
VALUES ($id, $seq, $in_reply_to, datetime('now'), $deliver_after, $recurrence, $kind, $platform_id, $channel_type, $thread_id, $content)`,
)
.run({
in_reply_to: null,
deliver_after: null,
recurrence: null,
platform_id: null,
channel_type: null,
thread_id: null,
...msg,
seq: nextSeq,
$id: msg.id,
$seq: nextSeq,
$in_reply_to: msg.in_reply_to ?? null,
$deliver_after: msg.deliver_after ?? null,
$recurrence: msg.recurrence ?? null,
$kind: msg.kind,
$platform_id: msg.platform_id ?? null,
$channel_type: msg.channel_type ?? null,
$thread_id: msg.thread_id ?? null,
$content: msg.content,
});
return nextSeq;

View File

@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { initTestSessionDb, closeSessionDb, getInboundDb, getOutboundDb } from './db/connection.js';
import { getUndeliveredMessages } from './db/messages-out.js';

View File

@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { initTestSessionDb, closeSessionDb, getInboundDb, getOutboundDb } from './db/connection.js';
import { getPendingMessages, markCompleted } from './db/messages-in.js';