Skip to content

Instantly share code, notes, and snippets.

@anthonywu
Created February 1, 2026 20:13
Show Gist options
  • Select an option

  • Save anthonywu/6e4db4b22f4c29e27fc9c3ba11b5c265 to your computer and use it in GitHub Desktop.

Select an option

Save anthonywu/6e4db4b22f4c29e27fc9c3ba11b5c265 to your computer and use it in GitHub Desktop.
Moltworker Codebase Learnings - Patterns for Cloudflare Workers, error handling, WebSocket interception, and more

Moltworker Codebase Learnings

Patterns and techniques worth applying to professional projects, extracted from the moltworker codebase.


1. Error Transformation at the Boundary

Instead of leaking internal errors, rewrite them at the proxy layer:

// src/index.ts:37-46
function transformErrorMessage(message: string, host: string): string {
  if (message.includes('gateway token missing')) {
    return `Invalid or missing token. Visit https://${host}?token={YOUR_TOKEN}`;
  }
  if (message.includes('pairing required')) {
    return `Pairing required. Visit https://${host}/_admin/`;
  }
  return message;
}

Applied transparently in WebSocket relay — clients get actionable errors without backend changes.


2. Content-Type Aware Middleware

Same auth middleware, different response format based on client:

// src/auth/middleware.ts
export function createAccessMiddleware(options: { type: 'json' | 'html', redirectOnMissing?: boolean }) {
  return async (c, next) => {
    const jwt = extractJWT(c);
    if (!jwt) {
      if (options.type === 'html' && options.redirectOnMissing) {
        return c.redirect(`https://${teamDomain}`, 302);
      }
      return options.type === 'json'
        ? c.json({ error: 'Unauthorized', hint: '...' }, 401)
        : c.html('<h1>Unauthorized</h1>', 401);
    }
    // ...
  };
}

Browser gets redirect to login, API client gets JSON error.


3. Non-Blocking Cold Start with Background Init

Return loading page immediately, start container in background:

// src/index.ts:230-242
if (!isGatewayReady && acceptsHtml) {
  // Start in background (don't await!)
  c.executionCtx.waitUntil(
    ensureMoltbotGateway(sandbox, c.env).catch(console.error)
  );

  // Return loading page immediately
  return c.html(loadingPageHtml);
}

The loading page polls /api/status and reloads when ready. User sees progress, not a hanging request.


4. Sanity Check Before Destructive Sync

Prevent overwriting good backup with empty/corrupt data:

// src/gateway/sync.ts:39-58
// Verify source has critical files BEFORE rsync
const checkProc = await sandbox.startProcess('test -f /root/.clawdbot/clawdbot.json && echo "ok"');
await waitForProcess(checkProc, 5000);
if (!checkLogs.stdout?.includes('ok')) {
  return {
    success: false,
    error: 'Sync aborted: source missing clawdbot.json',
    details: 'Could indicate corruption or incomplete setup.',
  };
}

5. Process Lifecycle with Race Condition Handling

Don't just check status, wait for actual readiness:

// src/gateway/process.ts:56-77
const existingProcess = await findExistingMoltbotProcess(sandbox);
if (existingProcess) {
  // ALWAYS use full timeout — process can be "running" but not ready
  // (started by another concurrent request)
  try {
    await existingProcess.waitForPort(MOLTBOT_PORT, { timeout: STARTUP_TIMEOUT_MS });
    return existingProcess;
  } catch (e) {
    // Stuck — kill and restart
    await existingProcess.kill();
  }
}

The comment explains why — prevents premature kills from race conditions.


6. Mock Builders for Tests

Composable mocks with sensible defaults:

// src/test-utils.ts
export function createMockEnv(overrides: Partial<MoltbotEnv> = {}): MoltbotEnv {
  return {
    Sandbox: {} as any,
    ASSETS: {} as any,
    MOLTBOT_BUCKET: {} as any,
    ...overrides,
  };
}

export function createMockEnvWithR2(overrides = {}) {
  return createMockEnv({
    R2_ACCESS_KEY_ID: 'test-key-id',
    R2_SECRET_ACCESS_KEY: 'test-secret-key',
    CF_ACCOUNT_ID: 'test-account-id',
    ...overrides,
  });
}

Test code becomes: const env = createMockEnvWithR2({ DEBUG_ROUTES: 'true' }).


7. Feature-Flag Protected Debug Routes

// src/index.ts:203-209
app.use('/debug/*', async (c, next) => {
  if (c.env.DEBUG_ROUTES !== 'true') {
    return c.json({ error: 'Debug routes are disabled' }, 404);
  }
  return next();
});

Debug routes include /debug/processes?logs=true, /debug/env, /debug/ws-test (interactive WebSocket tester). Disabled by default, enable with wrangler secret put DEBUG_ROUTEStrue.


8. Config Validation with Conditional Dependencies

// src/index.ts:55-82
function validateRequiredEnv(env: MoltbotEnv): string[] {
  const missing: string[] = [];

  // Conditional: AI Gateway requires BOTH key and URL
  if (env.AI_GATEWAY_API_KEY) {
    if (!env.AI_GATEWAY_BASE_URL) {
      missing.push('AI_GATEWAY_BASE_URL (required when using AI_GATEWAY_API_KEY)');
    }
  } else if (!env.ANTHROPIC_API_KEY) {
    missing.push('ANTHROPIC_API_KEY or AI_GATEWAY_API_KEY');
  }

  return missing;
}

Error message explains the dependency: "required when using AI_GATEWAY_API_KEY".


9. WebSocket Pair for Message Interception

// src/index.ts:284-365
const [clientWs, serverWs] = Object.values(new WebSocketPair());
const containerWs = (await sandbox.wsConnect(request, port)).webSocket;

serverWs.accept();
containerWs.accept();

// Bidirectional relay with transformation
serverWs.addEventListener('message', e => containerWs.send(e.data));
containerWs.addEventListener('message', e => {
  let data = e.data;
  // Transform errors here
  serverWs.send(data);
});

return new Response(null, { status: 101, webSocket: clientWs });

Transparent proxy — client has no idea messages are being intercepted.


10. CDP Protocol Shim

src/routes/cdp.ts is ~1800 lines translating Chrome DevTools Protocol to Puppeteer calls. Key patterns:

  • Session state management — tracks nodeIds, objectIds, frames
  • Synthetic ID generation — CDP expects specific ID formats
  • Proactive events — sends Page.frameNavigated, Page.loadEventFired after navigation
  • Timing-safe authcrypto.timingSafeEqual() for secret comparison

This is a masterclass in protocol translation if you ever need to build a shim layer.


Quick Reference

Pattern Benefit
Error transformation at boundary Users get help, not stack traces
Content-type aware responses HTML vs JSON automatically
Background init + loading page No hanging cold starts
Sanity check before sync Prevent data loss
Port-based health checks Process "running" ≠ ready
Mock builders with variants Less test boilerplate
Feature-flagged debug routes Safe introspection in prod
Conditional config validation Clear dependency errors
WebSocket interception Transparent message rewriting

Cloudflare-Specific Patterns

Sandbox as Container Orchestrator

The project uses @cloudflare/sandbox to run a full Docker container inside a Durable Object:

import { getSandbox, Sandbox } from '@cloudflare/sandbox';

const sandbox = getSandbox(env.Sandbox, 'moltbot', { keepAlive: true });
await sandbox.startProcess('/usr/local/bin/start-moltbot.sh', { env: envVars });
await process.waitForPort(18789, { mode: 'tcp', timeout: 120000 });

R2 as FUSE Mount for Persistence

Container filesystem is ephemeral. R2 mounted via s3fs provides persistence:

await sandbox.mountBucket('moltbot-data', '/data/moltbot', {
  endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
  credentials: { accessKeyId, secretAccessKey },
});

Limitations:

  • No inotify/fswatch (FUSE limitation)
  • No distributed locking
  • Eventual consistency on listings
  • Use for backup/restore, not live collaboration

Cron-Based Sync

// wrangler.jsonc
"triggers": { "crons": ["*/5 * * * *"] }

Every 5 minutes: rsync local state → R2. On cold start: restore from R2 if newer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment