Cut worker isolate baseline memory: stub just-bash, lazy-load cheerio (#340)

Production OOM triage: every 'Worker exceeded memory limit' burst hits
unrelated cheap routes right after a deploy — the 128MB limit is per
isolate, and the main worker's eagerly-evaluated module graph is what
crowds it, not any single request.

- Alias just-bash to a throwing stub (worker never uses it): removes
  just-bash + turndown + @mixmark-io/domino from the bundle. The chain
  was pulled in eagerly by @cloudflare/think via the SamChatAgent
  re-export in src/server.ts; SAM only uses its own MCP tools.
- Disable Think's workspace bash tool on SamChatAgent so the stub is
  unreachable at runtime.
- Dynamic-import page-analyzer (cheerio) in the site-audit crawl step
  so it evaluates only when an audit runs, not in every isolate.
- Drop the turndown CJS alias workaround (#339): turndown is no longer
  in the graph at all.

Main eager server chunk: 14,434 kB -> 11,712 kB (-19%); cheerio's
503 kB now a lazy chunk.
This commit is contained in:
Ben Senescu 2026-07-03 22:23:35 -04:00 committed by GitHub
parent 93cf32081a
commit 1a74904b67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 56 additions and 10 deletions

View File

@ -78,6 +78,12 @@ type SamContext = {
* the app DB, so every session in a project shares them. * the app DB, so every session in a project shares them.
*/ */
export class SamChatAgent extends Think { export class SamChatAgent extends Think {
// SAM's toolset is the MCP tools from beforeTurn; it has no use for Think's
// workspace bash tool, whose just-bash dependency is stubbed out of the
// bundle anyway (see vite.config.ts) to keep ~30 MB of eagerly-evaluated
// source out of every isolate's baseline heap.
override workspaceBash = false;
// Session row + project, resolved once per DO lifetime (the binding is // Session row + project, resolved once per DO lifetime (the binding is
// immutable). Null until a turn/provider needs it — and left null when the // immutable). Null until a turn/provider needs it — and left null when the
// registry row is gone, which beforeTurn turns into a polite refusal. // registry row is gone, which beforeTurn turns into a polite refusal.

View File

@ -0,0 +1,33 @@
/**
* Build-time stand-in for `just-bash`, wired up via the vite.config.ts alias.
*
* `@cloudflare/think` eagerly imports just-bash (~21 MB of source, plus
* turndown and the 8.6 MB @mixmark-io/domino DOM implementation) at module
* init, and SamChatAgent is re-exported from src/server.ts, so the whole chain
* lands in the main worker's startup module graph — raising every isolate's
* baseline heap toward the 128 MB limit (production OOM bursts on unrelated
* routes after each deploy). SAM only uses its own MCP toolset and disables
* Think's workspace bash tool (`workspaceBash = false`), so the real library
* is unreachable; this stub keeps it out of the bundle entirely.
*
* Remove once https://github.com/cloudflare/agents/issues/1673 lands and
* @cloudflare/think loads just-bash lazily.
*/
const STUBBED_MESSAGE =
"just-bash is stubbed out of the worker bundle (see vite.config.ts); " +
"the workspace bash tool is disabled for this deployment";
// Covers every named import in the dependency graph: `Bash` (Think's
// workspace bash tool) and `defineCommand` (agents/skills bash scripts).
// Both are only reached when a model invokes bash, which SAM never exposes.
// Must stay a class: call sites construct it with `new Bash({...})`.
// oxlint-disable-next-line typescript-eslint/no-extraneous-class
export class Bash {
constructor() {
throw new Error(STUBBED_MESSAGE);
}
}
export function defineCommand(): never {
throw new Error(STUBBED_MESSAGE);
}

View File

@ -1,4 +1,3 @@
import { analyzeHtml } from "@/server/lib/audit/page-analyzer";
import type { StepPageResult } from "@/server/lib/audit/types"; import type { StepPageResult } from "@/server/lib/audit/types";
import { isSameOrigin, normalizeUrl } from "@/server/lib/audit/url-utils"; import { isSameOrigin, normalizeUrl } from "@/server/lib/audit/url-utils";
@ -31,6 +30,11 @@ export async function crawlPage(
} }
const html = await response.text(); const html = await response.text();
// Dynamic import keeps cheerio (page-analyzer's HTML parser) out of the
// worker's startup module graph: SiteAuditWorkflow is re-exported from
// src/server.ts, so a static import would evaluate cheerio in every
// isolate's baseline heap, not just when an audit actually crawls.
const { analyzeHtml } = await import("@/server/lib/audit/page-analyzer");
const analysis = analyzeHtml( const analysis = analyzeHtml(
html, html,
finalUrl, finalUrl,

View File

@ -1,3 +1,4 @@
import { fileURLToPath } from "node:url";
import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import { defineConfig, loadEnv } from "vite"; import { defineConfig, loadEnv } from "vite";
import tsConfigPaths from "vite-tsconfig-paths"; import tsConfigPaths from "vite-tsconfig-paths";
@ -23,17 +24,19 @@ export default defineConfig(({ mode }) => {
return { return {
resolve: { resolve: {
alias: { alias: {
// TODO: Remove this workaround once fixed upstream — either turndown // TODO: Remove this workaround once @cloudflare/think stops eagerly
// drops the bare require from its ESM build, or @cloudflare/think // importing just-bash at module init
// stops eagerly importing just-bash/turndown at module init
// (https://github.com/cloudflare/agents/issues/1673). // (https://github.com/cloudflare/agents/issues/1673).
// //
// turndown's ESM build (pulled in via just-bash's html-to-markdown // just-bash (plus its turndown → @mixmark-io/domino chain, ~30 MB of
// command) contains a bare CommonJS `require("@mixmark-io/domino")` // source) is only used by Think's workspace bash tool, which SAM
// that the Cloudflare Workers runtime rejects at deploy time (error // disables — but the eager import drags it into the main worker's
// 10021). Its CJS build goes through Vite's CommonJS transform, which // startup module graph, inflating every isolate's baseline heap
// rewrites that require into a bundled import. // toward the 128 MB limit (production OOM bursts on unrelated
turndown: "turndown/lib/turndown.cjs.js", // routes). Alias it to a throwing stub so it never ships.
"just-bash": fileURLToPath(
new URL("./src/server/lib/just-bash-stub.ts", import.meta.url),
),
}, },
}, },
envPrefix: [ envPrefix: [